- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
98 lines
4.5 KiB
TypeScript
98 lines
4.5 KiB
TypeScript
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 被挡,无解');
|
||
});
|