初始提交:三玩法 Web 原型与设计文档

- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
cabinw
2026-09-16 14:30:59 +08:00
commit 8026e4628d
169 changed files with 22162 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
// 发布前校验:levels/*.json 每一关必须连通、恰好 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 { Board } from '../src/board';
import { clueLabel } from '../src/clue';
import { solve } 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 big = b.cells.filter((c) => c.w * c.h > 1).length;
const clues = b.cells.filter((c) => c.clue !== null).map((c) => c.clue!).sort((a, z) => a - z);
const parityCells = b.cells.filter((c) => c.parity !== null);
const t0 = performance.now();
const paths = solve(b, { limit: 3 });
const ms = (performance.now() - t0).toFixed(1);
const all = solve(b, { ignoreClues: true, 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})`);
// 奇偶线索必须能排除错误路线,不能只是普通棋盘上的天然黑白奇偶。
for (const cell of parityCells) {
const defs = lv.def.cells.map((c) => {
const { parity, ...rest } = c;
return c.id === cell.id ? rest : c;
});
if (solve(Board.fromCells(defs), { limit: 2 }).length === 1) {
problems.push(`冗余奇偶线索 ${cell.id}=${clueLabel(cell)}`);
}
}
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)} 大格=${big} 线索=${clues.join(',').padEnd(10)} 无线索路径=${String(all >= 500 ? '500+' : all).padStart(4)} ${ms}ms` +
(parityCells.length ? ` 奇偶=${parityCells.map((c) => `${c.id}:${clueLabel(c)}`).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);