import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { test } from 'node:test'; import { Board } from '../src/board'; import { loadPack, parsePack } from '../src/levels'; import { hint, solve, uniqueSolution } from '../src/solver'; import type { CellDef } from '../src/types'; const c = (id: string, x: number, y: number, w = 1, h = 1, clue?: number): CellDef => ({ id, x, y, w, h, clue }); test('关卡包每一关恰好一个解', () => { for (const file of ['pack-1-tutorial.json', 'pack-2-advanced.json', 'pack-3-parity.json']) { const pack = parsePack(JSON.parse(readFileSync(new URL(`../levels/${file}`, import.meta.url), 'utf8'))); const levels = loadPack(pack); assert.equal(levels.length, file === 'pack-3-parity.json' ? 6 : 10, `${file} 关数不符`); for (const lv of levels) { const paths = solve(lv.board, { limit: 3 }); assert.equal(paths.length, 1, `${lv.def.id} 应唯一解,实际 ${paths.length}`); assert.equal(paths[0].length, lv.board.n); } } }); test('GDD §7.3 样例盘:15 格、两块 2×2、线索落在大格上,唯一解且 B 是第 6 步', () => { // A A a b c / A A . d e / f g h B B / . i j B B / 1 l m . . const b = Board.fromCells([ c('A', 0, 0, 2, 2), c('a', 2, 0), c('b', 3, 0), c('c', 4, 0), c('d', 3, 1), c('e', 4, 1), c('f', 0, 2), c('g', 1, 2), c('h', 2, 2), c('B', 3, 2, 2, 2, 6), c('i', 1, 3), c('j', 2, 3), c('one', 0, 4, 1, 1, 1), c('l', 1, 4), c('m', 2, 4), ]); assert.equal(b.n, 15); const paths = solve(b); assert.equal(paths.length, 1, '样例盘恰好唯一解'); const p = paths[0]; assert.deepEqual( p.map((i) => b.cells[i].id), ['one', 'l', 'm', 'j', 'h', 'B', 'd', 'e', 'c', 'b', 'a', 'A', 'f', 'g', 'i'], ); // 从 j 往左绕 i 会让 B 最早第 8 步才到(红 8),所以第 5 步必须是 h、第 6 步进 B assert.equal(b.cells[p[4]].id, 'h'); assert.equal(b.cells[p[5]].id, 'B'); }); test('无线索 1 的盘:任意起点都会被枚举', () => { const b = Board.fromCells([c('a', 0, 0), c('b', 1, 0), c('d', 1, 1, 1, 1, 3)]); const paths = solve(b); assert.equal(paths.length, 1); assert.equal(b.cells[paths[0][0]].id, 'a'); }); test('多解与无解', () => { assert.equal(solve(Board.fromCells([c('a', 0, 0), c('b', 1, 0), c('c', 2, 0)])).length, 2); assert.equal(uniqueSolution(Board.fromCells([c('a', 0, 0), c('b', 1, 0), c('c', 2, 0)])), null); assert.equal(solve(Board.fromCells([c('a', 0, 0, 1, 1, 1), c('b', 1, 0), c('c', 2, 0, 1, 1, 2)])).length, 0); }); test('提示:起点 / 下一步 / 退回分叉 / 起手错', () => { const sol = [0, 1, 2, 3, 4]; assert.deepEqual(hint(sol, []), { kind: 'start', cell: 0 }); assert.deepEqual(hint(sol, [0, 1]), { kind: 'next', cell: 2 }); assert.deepEqual(hint(sol, [0, 1, 7, 8]), { kind: 'backtrack', cell: 1, undoCount: 2 }); assert.deepEqual(hint(sol, [3, 2]), { kind: 'restart', cell: 0 }); assert.deepEqual(hint(sol, sol), { kind: 'none' }); });