Files
NumberSwipe_New/modes/03-hex-walls/tests/solver.test.ts
T
cabinw 8026e4628d 初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试
- 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本
- 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试
- 产品文档、UI 设计规范、各玩法 GDD / HANDOFF
- 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
2026-09-16 14:30:59 +08:00

98 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 被挡,无解');
});