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

115 lines
5.3 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 { 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', '撤销后墙转回去');
});