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

58 lines
3.1 KiB
TypeScript

import assert from 'node:assert/strict';
import { writeFileSync } from 'node:fs';
import { Game } from '../src/game';
import { LEVELS } from '../src/levels';
import { goalView } from '../src/goals';
import { findChains } from '../src/rules';
import { Gesture } from '../src/input';
import { HEIGHT, WIDTH } from '../src/config';
import { Random } from '../src/random';
assert.equal(LEVELS.length, 10);
const report: unknown[] = [];
let states = 0, maxOverlap = 0;
function check(g: Game): void {
states++;
for (const a of g.balls) {
assert(Number.isFinite(a.x) && Number.isFinite(a.y));
assert(a.x >= a.r && a.x <= WIDTH - a.r && a.y <= HEIGHT - a.r && a.y >= a.r - 2, `ball outside: ${JSON.stringify(a)}`);
for (const b of g.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}`);
}
}
}
for (const l of LEVELS) {
assert(l.initial.every(n => n >= 1 && n <= 4));
assert(l.refill.length > 0 && l.refill.every(n => n >= 1 && n <= 4));
const g = new Game(l), input = new Gesture(() => g.balls); check(g);
const openings = findChains(g.balls).length;
const moves: unknown[] = [];
for (const ids of l.solution || []) {
const balls = ids.map(id => g.balls.find(b => b.id === id)!);
input.down(balls[0]); balls.slice(1).forEach(b => input.move(b));
assert.deepEqual(input.chain, ids, `L${l.id} touch route`);
const result = g.commit(input.up()); assert(result); check(g);
moves.push({ ids, numbers: balls.map(b => b.n), created: result.created.n, score: g.stats.score });
}
assert.equal(g.status, 'won', `L${l.id}`);
assert.equal(l.par, g.stats.merges);
report.push({ id: l.id, title: l.title, seed: l.seed, budget: l.moves, solutionMoves: g.stats.merges,
spareMoves: g.movesLeft, score: g.stats.score, openings, goals: l.goals.map(goal => goalView(goal, g.stats)), moves });
console.log(`L${String(l.id).padStart(2,'0')} ${l.title}: ${g.stats.merges}/${l.moves} 步,${g.stats.score} 分,最高 ${g.stats.maxNumber}`);
}
let randomMerges = 0, longestRun = 0;
for (let seed = 1; seed <= 128; seed++) {
const r = new Random(seed), g = new Game({ ...LEVELS[7], seed: seed * 937 }, true); check(g);
for (let turn = 0; turn < 30 && g.status === 'playing'; turn++) {
const paths = findChains(g.balls); const path = paths[Math.floor(r.next() * paths.length)];
assert(g.commit(path)); check(g); randomMerges++; longestRun = Math.max(longestRun, turn + 1);
if (g.snapshot().status === 'stuck' && g.shuffles) g.shuffle();
}
}
assert(randomMerges >= 500, `随机合成覆盖不足: ${randomMerges}`);
const result = { generatedAt: new Date().toISOString(), levels: report, physics: { checkedStates: states, randomMerges, longestRun, maxOverlap } };
writeFileSync(new URL('../docs/qa/validation.json', import.meta.url), JSON.stringify(result, null, 2) + '\n');
console.log(`物理检查:${states} 个稳定盘面,${randomMerges} 次随机合成,最长连续 ${longestRun} 次,最大重叠 ${maxOverlap.toFixed(3)}px`);