Files
NumberSwipe_New/modes/03-hex-walls/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

74 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 发布前校验:levels/*.json 每一关必须连通、恰好 1 条满足线索的哈密顿路径;
// 第 3 关起至少 1 堵墙,且去掉全部墙后解不再唯一(GDD §4.6)。
// 用法:npm run validate (全部包)
// npm run validate -- 1-7 (只看某关,并打印解)
import { readdirSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadPack, parsePack } from '../src/levels';
import { redundantWalls, solve, spinnerReport, wallsMatter } from '../src/solver';
import { pathToIds, renderAscii } from './ascii';
const here = dirname(fileURLToPath(import.meta.url));
const levelsDir = join(here, '..', 'levels');
const only = process.argv[2];
let failed = 0;
let total = 0;
for (const file of readdirSync(levelsDir).filter((f) => f.endsWith('.json')).sort()) {
const pack = parsePack(JSON.parse(readFileSync(join(levelsDir, file), 'utf8')));
console.log(`\n== ${file} · ${pack.title} · ${pack.levels.length} 关`);
let levels;
try {
levels = loadPack(pack);
} catch (e) {
console.log(` ✗ 解析失败: ${(e as Error).message}`);
failed++;
continue;
}
for (const lv of levels) {
if (only && lv.def.id !== only) continue;
total++;
const b = lv.board;
const clues = b.cells.filter((c) => c.clue !== null).map((c) => c.clue!).sort((a, z) => a - z);
const t0 = performance.now();
const paths = solve(b, { limit: 3 });
const ms = (performance.now() - t0).toFixed(1);
const free = solve(b, { ignoreClues: true, limit: 500 }).length;
const noWall = solve(b.withoutWalls(), { limit: 500 }).length;
const problems: string[] = [];
if (!b.isConnected()) problems.push('去墙后不连通');
if (paths.length === 0) problems.push('无解');
if (paths.length > 1) problems.push(`多解 (≥${paths.length})`);
const wallCount = b.walls.length + b.spinners.length;
if (lv.index >= 3 || pack.pack >= 2) {
if (wallCount === 0) problems.push('第 3 关起每关至少 1 堵墙');
else if (paths.length === 1 && !wallsMatter(b)) problems.push('去墙后仍唯一,墙没参与推理');
}
const redundant = paths.length === 1 ? redundantWalls(b) : [];
if (paths.length === 1) {
for (const r of spinnerReport(b)) {
if (r.removable) problems.push(`转墙 ${r.index} 冗余`);
if (r.freezable.length) problems.push(`转墙 ${r.index} 可换成静态墙(边 ${r.freezable.join('/')})`);
}
}
const ok = problems.length === 0;
if (!ok) failed++;
const mark = ok ? '✓' : '✗';
console.log(
` ${mark} ${lv.def.id.padEnd(5)} N=${String(b.n).padStart(2)} 墙=${b.walls.length}${b.spinners.length ? '+转' + b.spinners.length : ''} 线索=${clues.join(',').padEnd(10)} 无线索路径=${String(free >= 500 ? '500+' : free).padStart(4)} 去墙解=${String(noWall >= 500 ? '500+' : noWall).padStart(4)} ${ms}ms` +
(redundant.length ? ` 可删墙=${redundant.map((i) => `${b.cells[b.walls[i].a].id}|${b.cells[b.walls[i].b].id}`).join(',')}` : '') +
(ok ? '' : ` ← ${problems.join(';')}`),
);
if (only || !ok) {
console.log(renderAscii(b).replace(/^/gm, ' '));
for (const p of paths) console.log(' 解: ' + pathToIds(b, p));
if (only && paths.length === 1) console.log(renderAscii(b, paths[0]).replace(/^/gm, ' '));
}
}
}
console.log(`\n${total - failed}/${total} 关通过`);
process.exit(failed ? 1 : 0);