初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
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。`);
|
||||
Reference in New Issue
Block a user