Files
NumberSwipe_New/modes/01-number-stroke/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

77 lines
3.0 KiB
TypeScript

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' });
});