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`);