初始提交:三玩法 Web 原型与设计文档
- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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' });
|
||||
});
|
||||
Reference in New Issue
Block a user