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