import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; import { HexBoard } from '../src/board'; import { loadPack, parsePack } from '../src/levels'; import { hint, redundantWalls, solve, spinnerReport, uniqueSolution, wallsMatter } from '../src/solver'; import type { HexCellDef } from '../src/types'; const c = (id: string, q: number, r: number, clue?: number): HexCellDef => ({ id, q, r, clue }); const pack = parsePack(JSON.parse(readFileSync(new URL('../levels/pack-1-tutorial.json', import.meta.url), 'utf8'))); const levels = loadPack(pack); test('第一包恰好 10 关,每关连通且唯一解', () => { assert.equal(levels.length, 10); for (const lv of levels) assert.equal(lv.board.spinners.length, 0, '第一包不含转墙'); for (const lv of levels) { assert.ok(lv.board.isConnected(), `${lv.def.id} 不连通`); const paths = solve(lv.board, { limit: 2 }); assert.equal(paths.length, 1, `${lv.def.id} 解数 ${paths.length}`); assert.equal(paths[0].length, lv.board.n); // 解满足线索 paths[0].forEach((cell, i) => { const clue = lv.board.cells[cell].clue; if (clue !== null) assert.equal(clue, i + 1, `${lv.def.id} 线索不符`); }); } }); test('第 3 关起每关至少 1 堵墙,且墙参与唯一性、无冗余墙', () => { for (const lv of levels) { if (lv.index < 3) continue; assert.ok(lv.board.walls.length >= 1, `${lv.def.id} 没有墙`); assert.ok(wallsMatter(lv.board), `${lv.def.id} 去墙后仍唯一`); assert.deepEqual(redundantWalls(lv.board), [], `${lv.def.id} 有冗余墙`); } }); test('前 8 关都有线索 1', () => { for (const lv of levels) if (lv.index <= 8) assert.notEqual(lv.board.startClueCell, -1, lv.def.id); }); test('墙改变解集合:同一盘有墙 1 解、无墙 2 解', () => { const b = HexBoard.fromCells([c('a', 0, 0, 1), c('b', 1, 0), c('c', 0, 1), c('d', 1, 1, 4)], [['a', 'b']]); assert.deepEqual(uniqueSolution(b), [0, 2, 1, 3]); assert.equal(solve(b.withoutWalls()).length, 2); }); test('无线索 1 时枚举全部起点;无解 / 多解返回 null', () => { const b = HexBoard.fromCells([c('a', 0, 0), c('b', 1, 0), c('c', 2, 0)]); assert.equal(solve(b).length, 2); assert.equal(uniqueSolution(b), null); const b2 = HexBoard.fromCells([c('a', 0, 0), c('b', 1, 0), c('c', 2, 0, 2)]); assert.equal(solve(b2).length, 0); }); test('随机模式也能找到解', () => { const b = levels[9].board; let seed = 1; const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647; assert.equal(solve(b, { limit: 1, random: rnd }).length, 1); }); test('提示四态', () => { const sol = [0, 2, 1, 3]; assert.deepEqual(hint(sol, []), { kind: 'start', cell: 0 }); assert.deepEqual(hint(sol, [0, 2]), { kind: 'next', cell: 1 }); assert.deepEqual(hint(sol, [0, 1, 3]), { kind: 'backtrack', cell: 0, undoCount: 2 }); assert.deepEqual(hint(sol, [2, 0]), { kind: 'restart', cell: 0 }); assert.deepEqual(hint(sol, sol), { kind: 'none' }); }); const pack2 = parsePack(JSON.parse(readFileSync(new URL('../levels/pack-2-spinner.json', import.meta.url), 'utf8'))); const levels2 = loadPack(pack2); test('第二包(转墙实验):每关唯一解,转墙不冗余且不能用静态墙替代', () => { assert.ok(levels2.length >= 3); for (const lv of levels2) { assert.ok(lv.board.spinners.length >= 1, `${lv.def.id} 没有转墙`); assert.equal(solve(lv.board, { limit: 2 }).length, 1, `${lv.def.id} 不唯一`); assert.ok(wallsMatter(lv.board), `${lv.def.id} 去墙后仍唯一`); for (const r of spinnerReport(lv.board)) { assert.equal(r.removable, false, `${lv.def.id} 转墙 ${r.index} 冗余`); assert.deepEqual(r.freezable, [], `${lv.def.id} 转墙 ${r.index} 可用静态墙替代`); } } }); test('求解器按步号判定转墙:同一盘不同相位 2 解 / 1 解 / 0 解', () => { // a(0,0)=1, b(1,0), d(1,-1);转墙轴在 b 的第 3 个角(左上),周围三格 a/b/d:边 0 = a|d,1 = b|d,2 = a|b const mk = (phase: number) => HexBoard.fromCells([c('a', 0, 0, 1), c('b', 1, 0), c('d', 1, -1)], [], [{ cell: 'b', corner: 3, phase }]); const b0 = mk(0); assert.deepEqual(b0.spinners[0].edges, [[0, 2], [1, 2], [0, 1]]); assert.equal(solve(b0).length, 2, '相位 0:a,b,d 与 a,d,b 都通'); assert.deepEqual(solve(mk(1)), [[0, 2, 1]], '相位 1:起手时 a|b 被挡,只能 a,d,b'); assert.equal(solve(mk(2)).length, 0, '相位 2:a→b 后 b|d 被挡,无解'); });