- 玩法一 数字一笔:规则库、求解器、26 关、竖屏触摸 Web 原型、测试 - 玩法二 连连气球:连消畅玩版核心、Web 原型、Cocos 球槽组件与导出脚本 - 玩法三 六边一笔:规则库、求解器、10 关 + 转墙实验包、Web 原型、测试 - 产品文档、UI 设计规范、各玩法 GDD / HANDOFF - 文档移除全部外部参考作引用;删除玩法二 v0 旧稿
115 lines
5.3 KiB
TypeScript
115 lines
5.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import { test } from 'node:test';
|
||
import { HexBoard } from '../src/board';
|
||
import { Game } from '../src/game';
|
||
import type { HexCellDef } from '../src/types';
|
||
|
||
const c = (id: string, q: number, r: number, clue?: number): HexCellDef => ({ id, q, r, clue });
|
||
|
||
// 1-3:a(1) b / c d(4),墙 a|b
|
||
const level13 = () => HexBoard.fromCells([c('a', 0, 0, 1), c('b', 1, 0), c('c', 0, 1), c('d', 1, 1, 4)], [['a', 'b']]);
|
||
|
||
test('有线索 1 时只能从它起手', () => {
|
||
const g = new Game(level13());
|
||
assert.deepEqual(g.apply({ type: 'start', cell: 1 }), [{ type: 'rejectStart', cell: 1, clueCell: 0 }]);
|
||
assert.equal(g.status, 'empty');
|
||
assert.equal(g.apply({ type: 'start', cell: 0 })[0].type, 'step');
|
||
assert.equal(g.status, 'drawing');
|
||
});
|
||
|
||
test('无线索 1 时任意格可起手', () => {
|
||
const g = new Game(HexBoard.fromCells([c('a', 0, 0), c('b', 1, 0, 2)]));
|
||
assert.equal(g.apply({ type: 'start', cell: 0 })[0].type, 'step');
|
||
});
|
||
|
||
test('穿墙 → wallHit 事件,不追加', () => {
|
||
const b = level13();
|
||
const g = new Game(b);
|
||
g.apply({ type: 'start', cell: 0 });
|
||
const ev = g.apply({ type: 'append', cell: 1 });
|
||
assert.deepEqual(ev, [{ type: 'wallHit', from: 0, to: 1, wall: 0, spinner: -1 }]);
|
||
assert.equal(g.path.length, 1);
|
||
assert.deepEqual(g.wallTo(1), { wall: 0, spinner: -1 });
|
||
assert.equal(g.canAppend(1), false);
|
||
assert.equal(g.canAppend(2), true);
|
||
});
|
||
|
||
test('追加只能到相邻无墙未走格;冲突不打断;撤销恢复', () => {
|
||
const b = level13();
|
||
const g = new Game(b);
|
||
const id = (s: string) => b.cellById(s).index;
|
||
g.apply({ type: 'start', cell: id('a') });
|
||
assert.deepEqual(g.apply({ type: 'append', cell: id('d') }), []); // (1,1) 与 (0,0) 不相邻
|
||
g.apply({ type: 'append', cell: id('c') });
|
||
const ev = g.apply({ type: 'append', cell: id('d') }); // d=3,冲突
|
||
assert.equal(ev[0].type, 'step');
|
||
assert.equal((ev[0] as { conflict: boolean }).conflict, true);
|
||
assert.equal(g.isConflict(id('d')), true);
|
||
const last = g.apply({ type: 'append', cell: id('b') });
|
||
assert.equal(g.path.length, 4);
|
||
assert.equal(g.status, 'drawing', '填满但有冲突不算过关');
|
||
assert.ok(last.some((e) => e.type === 'deadEnd'));
|
||
g.apply({ type: 'undo' });
|
||
const un = g.apply({ type: 'undo' });
|
||
assert.deepEqual(un, [{ type: 'unstep', cell: id('d'), step: 3 }]);
|
||
assert.equal(g.isConflict(id('d')), false);
|
||
});
|
||
|
||
test('全走完且线索全对 → cleared,之后锁住', () => {
|
||
const b = level13();
|
||
const g = new Game(b);
|
||
const id = (s: string) => b.cellById(s).index;
|
||
g.apply({ type: 'start', cell: id('a') });
|
||
g.apply({ type: 'append', cell: id('c') });
|
||
g.apply({ type: 'append', cell: id('b') });
|
||
const ev = g.apply({ type: 'append', cell: id('d') });
|
||
assert.ok(ev.some((e) => e.type === 'cleared'));
|
||
assert.ok(ev.some((e) => e.type === 'step' && e.clueHit));
|
||
assert.equal(g.status, 'cleared');
|
||
assert.deepEqual(g.apply({ type: 'undo' }), []);
|
||
assert.deepEqual(g.apply({ type: 'append', cell: 0 }), []);
|
||
});
|
||
|
||
test('release / resume / reset 流转', () => {
|
||
const g = new Game(level13());
|
||
g.apply({ type: 'start', cell: 0 });
|
||
g.apply({ type: 'release' });
|
||
assert.equal(g.status, 'idle');
|
||
g.apply({ type: 'resume' });
|
||
assert.equal(g.status, 'drawing');
|
||
assert.deepEqual(g.apply({ type: 'reset' }), [{ type: 'reset' }]);
|
||
assert.equal(g.status, 'empty');
|
||
assert.deepEqual(g.apply({ type: 'reset' }), [], '空盘重开不产生事件');
|
||
g.apply({ type: 'start', cell: 0 });
|
||
g.apply({ type: 'undo' });
|
||
assert.equal(g.status, 'empty');
|
||
});
|
||
|
||
test('转墙:每走一格转 120°,按已走格数判定', () => {
|
||
// 花形 7 格,轴心在中心格 o 的第 4 个角(顶部),周围三格 o / nw / ne
|
||
const cells = [c('o', 0, 0, 1), c('e', 1, 0), c('ne', 1, -1), c('nw', 0, -1), c('w', -1, 0), c('sw', -1, 1), c('se', 0, 1)];
|
||
const b = HexBoard.fromCells(cells, [], [{ cell: 'o', corner: 4, phase: 0 }]);
|
||
const sp = b.spinners[0];
|
||
const id = (s: string) => b.cellById(s).index;
|
||
// 角 4 夹在方向 240°(nw) 与 300°(ne) 之间:边 0 = nw-ne,边 1 = o-ne,边 2 = o-nw
|
||
assert.deepEqual(sp.edges[0], [Math.min(id('nw'), id('ne')), Math.max(id('nw'), id('ne'))]);
|
||
assert.deepEqual(sp.edges[1], [id('o'), id('ne')]);
|
||
assert.deepEqual(sp.edges[2], [id('o'), id('nw')]);
|
||
// t=0 挡 nw-ne;t=1 挡 o-ne;t=2 挡 o-nw
|
||
assert.equal(b.spinnerBlocking(id('nw'), id('ne'), 0), 0);
|
||
assert.equal(b.spinnerBlocking(id('o'), id('ne'), 1), 0);
|
||
assert.equal(b.spinnerBlocking(id('o'), id('nw'), 2), 0);
|
||
assert.equal(b.spinnerBlocking(id('o'), id('ne'), 0), -1);
|
||
const g = new Game(b);
|
||
g.apply({ type: 'start', cell: id('o') }); // 已走 1 格 → 墙挡 o-ne
|
||
const hit = g.apply({ type: 'append', cell: id('ne') });
|
||
assert.deepEqual(hit, [{ type: 'wallHit', from: id('o'), to: id('ne'), wall: -1, spinner: 0 }]);
|
||
assert.equal(g.apply({ type: 'append', cell: id('nw') })[0].type, 'step'); // t=1 时 o-nw 开着
|
||
// 现在已走 2 格 → 墙挡 o-nw(已走,无所谓);nw-ne 开着
|
||
assert.equal(g.apply({ type: 'append', cell: id('ne') })[0].type, 'step');
|
||
g.apply({ type: 'undo' });
|
||
g.apply({ type: 'undo' });
|
||
assert.equal(g.path.length, 1);
|
||
assert.deepEqual(g.apply({ type: 'append', cell: id('ne') })[0].type, 'wallHit', '撤销后墙转回去');
|
||
});
|