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