import assert from 'node:assert/strict'; import { test } from 'node:test'; import { Board } from '../src/board'; import { Game } from '../src/game'; 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 }); // 1-3:start(1) a b(3) / d c const level13 = () => Board.fromCells([c('start', 0, 0, 1, 1, 1), c('a', 1, 0), c('b', 2, 0, 1, 1, 3), c('d', 1, 1), c('c', 2, 1)]); test('有线索 1 时只能从它起手', () => { const g = new Game(level13()); const ev = g.apply({ type: 'start', cell: 1 }); assert.deepEqual(ev, [{ 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(Board.fromCells([c('a', 0, 0), c('b', 1, 0, 1, 1, 2)])); assert.equal(g.apply({ type: 'start', cell: 0 })[0].type, 'step'); }); test('追加只能到相邻未走格;冲突不打断;撤销恢复', () => { const b = level13(); const g = new Game(b); const id = (s: string) => b.cellById(s).index; g.apply({ type: 'start', cell: id('start') }); assert.deepEqual(g.apply({ type: 'append', cell: id('c') }), []); // 不相邻 g.apply({ type: 'append', cell: id('a') }); const ev = g.apply({ type: 'append', cell: id('d') }); // 第 3 步没落到预定的 b,立即冲突 assert.equal(ev[0].type, 'step'); assert.ok(ev.some(e => e.type === 'step' && e.conflict && !e.clueHit)); assert.equal(g.isConflict(id('d')), true); assert.equal(g.stepOf(id('b')), 0, '无需先走到线索格,就能判断该步位置错误'); g.apply({ type: 'append', cell: id('c') }); const conflict = g.apply({ type: 'append', cell: id('b') }); assert.equal(conflict[0].type, 'step'); assert.equal((conflict[0] as { conflict: boolean }).conflict, true); assert.equal(g.isConflict(id('b')), true); assert.equal(g.path.length, 5); assert.equal(g.status, 'drawing', '填满但有冲突不算过关'); // 走到头了但没过关 → deadEnd 事件 assert.ok(conflict.some((e) => e.type === 'deadEnd')); const un = g.apply({ type: 'undo' }); assert.deepEqual(un, [{ type: 'unstep', cell: id('b'), step: 5 }]); assert.equal(g.isConflict(id('b')), false); assert.equal(g.stepOf(id('b')), 0); }); test('全走完且线索全对 → cleared,之后锁住', () => { const b = level13(); const g = new Game(b); const id = (s: string) => b.cellById(s).index; for (const s of ['start', 'a', 'b', 'c']) g.apply(g.path.length ? { type: 'append', cell: id(s) } : { type: 'start', cell: id(s) }); const ev = g.apply({ type: 'append', cell: id('d') }); assert.ok(ev.some((e) => e.type === 'cleared')); assert.equal(g.status, 'cleared'); assert.deepEqual(g.apply({ type: 'undo' }), []); assert.equal(g.path.length, 5); }); 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.equal(g.remaining, 5); assert.deepEqual(g.apply({ type: 'reset' }), [], '空盘重开不产生事件'); }); test('撤销到空盘回到 empty', () => { const g = new Game(level13()); g.apply({ type: 'start', cell: 0 }); g.apply({ type: 'undo' }); assert.equal(g.status, 'empty'); assert.equal(g.last, -1); }); test('直接回退移除后缀冲突,保留前缀;非法目标、末端和过关后均忽略', () => { const g = new Game(level13()); for (const cell of [0, 1, 3, 4, 2]) g.apply({ type: g.path.length ? 'append' : 'start', cell }); assert.equal(g.hasConflict(), true); g.apply({ type: 'rewind', cell: 1 }); assert.deepEqual(g.path, [0, 1]); assert.equal(g.hasConflict(), false); for (const cell of [-1, 4, 1]) assert.deepEqual(g.apply({ type: 'rewind', cell }), []); for (const cell of [2, 4, 3]) g.apply({ type: 'append', cell }); assert.equal(g.isCleared, true); assert.deepEqual(g.apply({ type: 'rewind', cell: 0 }), []); assert.equal(g.path.length, 5); });