// 发布前校验: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);