Files
NumberSwipe_New/modes/02-connect-1234/tools/validate-flow.ts
T
cabinw 8026e4628d 初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
2026-09-16 14:30:59 +08:00

54 lines
3.4 KiB
TypeScript

import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { FlowGame } from '../src/flow';
import { findFlowChains, canFlowAppend } from '../src/flow-rules';
import { Gesture } from '../src/input';
import { HEIGHT, WIDTH } from '../src/config';
import { Random } from '../src/random';
let states = 0, maxOverlap = 0;
function check(game: FlowGame): void {
states++;
assert(game.balls.length <= 24); assert.equal(new Set(game.balls.map(b => b.id)).size, game.balls.length);
for (const a of game.balls) {
assert(Number.isFinite(a.x) && Number.isFinite(a.y)); assert(a.n >= 1 && a.n < 10);
assert(a.x >= a.r && a.x <= WIDTH - a.r && a.y >= a.r - 2 && a.y <= HEIGHT - a.r, `outside: ${JSON.stringify(a)}`);
for (const b of game.balls) if (a.id < b.id) {
const overlap = a.r + b.r - Math.hypot(a.x - b.x, a.y - b.y);
maxOverlap = Math.max(maxOverlap, overlap); assert(overlap < 2, `overlap ${overlap}`);
}
}
assert(findFlowChains(game.balls).length > 0, 'dead board');
}
const report = [];
for (const strategy of ['random', 'guided'] as const) {
const sessions = strategy === 'random' ? 24 : 8;
let combos = 0, reactions = 0, flown = 0, breezes = 0, minChoices = Infinity;
const times: number[] = [], runs = [];
for (let seed = 1; seed <= sessions; seed++) {
const game = new FlowGame(seed * 937), rng = new Random(seed), input = new Gesture(() => game.balls, canFlowAppend);
check(game); let comboGestures = 0;
for (let turn = 0; turn < 100; turn++) {
const paths = findFlowChains(game.balls); minChoices = Math.min(minChoices, paths.length);
const ids = strategy === 'guided' ? game.hint() : paths[Math.floor(rng.next() * paths.length)];
assert(ids.length >= 3, 'hint missing');
if (strategy === 'guided') {
const targets = ids.map(id => game.balls.find(b => b.id === id)!);
input.down(targets[0]); targets.slice(1).forEach(b => input.move(b)); assert.deepEqual(input.up(), ids, 'unplayable hint');
}
const started = performance.now(), result = game.commit(ids); times.push(performance.now() - started);
assert(result); if (result.stages.length > 1) {combos++;comboGestures++;} check(game);
assert(result.stages.length <= (result.doubled ? 4 : 3));
}
flown += game.stats.flown; reactions += game.stats.reactions; breezes += game.breezes;
runs.push({seed:seed * 937,score:game.stats.score,peak:game.stats.maxNumber,flown:game.stats.flown,comboGestures,breezes:game.breezes});
}
times.sort((a,b)=>a-b);
const metrics = {strategy,sessions,gestures:sessions*100,comboGestures:combos,comboRate:combos/(sessions*100),reactions,flown,breezes,minPaths:minChoices,
commitMs:{median:times[Math.floor(times.length*.5)],p95:times[Math.floor(times.length*.95)],max:times[times.length-1]}};
report.push({...metrics,runs}); console.log(JSON.stringify(metrics));
}
const result = {generatedAt:new Date().toISOString(),note:'固定种子算法抽样;随机策略从合法链均匀抽取,提示策略通过真实手势控制器。连消率不是玩家数据。',states,maxOverlap,strategies:report};
mkdirSync(new URL('../docs/qa-v2/',import.meta.url),{recursive:true});
writeFileSync(new URL('../docs/qa-v2/validation.json',import.meta.url),JSON.stringify(result,null,2)+'\n');
console.log(`检查 ${states} 个稳定盘面;最大重叠 ${maxOverlap.toFixed(3)}px。`);