import assert from 'node:assert/strict'; import { test } from 'node:test'; import { Board } from '../src/board'; 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('只认四向邻接,角对角不算', () => { const b = Board.fromCells([c('a', 0, 0), c('b', 1, 1), c('c', 1, 0)]); assert.equal(b.areAdjacent(0, 1), false); assert.equal(b.areAdjacent(0, 2), true); assert.equal(b.areAdjacent(2, 1), true); }); test('2×2 可以同时贴多个 1×1;只碰角的不算', () => { // 1-4 的盘:start(0,0) 与 big(1,1,2,2) 只碰角 const b = Board.fromCells([c('start', 0, 0, 1, 1, 1), c('a', 1, 0), c('b', 2, 0), c('big', 1, 1, 2, 2, 3)]); const big = b.cellById('big').index; assert.deepEqual([...b.adj[big]].sort(), [1, 2]); assert.equal(b.areAdjacent(0, big), false); }); test('横长条与其长边上的两个格都相邻', () => { const b = Board.fromCells([c('H', 1, 1, 2, 1), c('p', 1, 0), c('q', 2, 0), c('r', 3, 1), c('s', 0, 2)]); const H = b.cellById('H').index; assert.equal(b.areAdjacent(H, b.cellById('p').index), true); assert.equal(b.areAdjacent(H, b.cellById('q').index), true); assert.equal(b.areAdjacent(H, b.cellById('r').index), true); assert.equal(b.areAdjacent(H, b.cellById('s').index), false); }); test('空洞返回 -1,坐标会平移到原点', () => { const b = Board.fromCells([c('a', 2, 3), c('b', 3, 3), c('d', 3, 4)]); assert.equal(b.cols, 2); assert.equal(b.rows, 2); assert.equal(b.cellAtUnit(0, 0), 0); assert.equal(b.cellAtUnit(0, 1), -1); assert.equal(b.cellAtUnit(5, 5), -1); }); test('共享边线段', () => { const b = Board.fromCells([c('B', 0, 0, 2, 2), c('x', 2, 1), c('y', 1, 2)]); assert.deepEqual(b.sharedEdge(0, 1), { x1: 2, y1: 1, x2: 2, y2: 2, dir: 'v' }); assert.deepEqual(b.sharedEdge(0, 2), { x1: 1, y1: 2, x2: 2, y2: 2, dir: 'h' }); assert.equal(b.sharedEdge(1, 2), null); }); test('3×3 大连块:合法、长边邻接、共享边合并', () => { const b = Board.fromCells([c('O', 0, 0, 3, 3), c('a', 3, 1), c('d', 1, 3), c('e', 3, 3)]); const O = b.cellById('O'); assert.equal(O.isRect, true); assert.equal(b.areAdjacent(O.index, b.cellById('a').index), true); assert.equal(b.areAdjacent(O.index, b.cellById('d').index), true); assert.equal(b.areAdjacent(O.index, b.cellById('e').index), false, '角对角不算'); assert.equal(b.cellAtUnit(2, 2), O.index); assert.deepEqual(b.sharedEdge(O.index, b.cellById('a').index), { x1: 3, y1: 1, x2: 3, y2: 2, dir: 'v' }); assert.deepEqual(b.sharedEdge(O.index, b.cellById('d').index), { x1: 1, y1: 3, x2: 2, y2: 3, dir: 'h' }); }); test('非法盘面直接抛错', () => { assert.throws(() => Board.fromCells([c('a', 0, 0), c('b', 0, 0)]), /重叠/); assert.throws(() => Board.fromCells([c('a', 0, 0, 3, 1)]), /尺寸/); assert.throws(() => Board.fromCells([c('a', 0, 0, 1, 1, 5)]), /线索/); assert.throws(() => Board.fromCells([c('a', 0, 0, 1, 1, 1), c('b', 1, 0, 1, 1, 1)]), /重复/); }); // ---------- 异形格(俄罗斯方块拼块) ---------- const s = (id: string, x: number, y: number, w: number, h: number, shape: Array<[number, number]>, clue?: number): CellDef => ({ id, x, y, w, h, shape, clue, }); test('异形格:L4 展开成单位集合,缺口是空洞', () => { const b = Board.fromCells([s('L', 1, 1, 2, 3, [[0, 0], [0, 1], [0, 2], [1, 2]]), c('a', 0, 0)]); const L = b.cellById('L'); assert.equal(L.isRect, false); assert.deepEqual([...L.units].sort(), [[1, 1], [1, 2], [1, 3], [2, 3]].sort()); assert.equal(b.cellAtUnit(2, 1), -1, 'bbox 里缺口 (2,1) 是空洞'); assert.equal(b.cellAtUnit(2, 3), L.index); assert.equal(b.cellById('a').isRect, true); }); test('异形格:旋转与镜像都在目录内', () => { const variants: Array> = [ [[0, 0], [0, 1], [0, 2], [1, 2]], // L [[0, 0], [1, 0], [2, 0], [0, 1]], // J(L 的镜像) [[0, 0], [1, 0], [1, 1], [2, 1]], // L 旋转 90° [[1, 0], [2, 0], [0, 1], [1, 1]], // S [[0, 0], [0, 1], [1, 1], [1, 2]], // Z(S 的镜像) [[0, 0], [1, 0], [2, 0], [1, 1]], // T [[0, 0], [0, 1], [1, 0]], // L3 ]; for (const shape of variants) { const w = Math.max(...shape.map(([x]) => x)) + 1; const h = Math.max(...shape.map(([, y]) => y)) + 1; assert.doesNotThrow(() => Board.fromCells([s('P', 0, 1, w, h, shape), c('a', 0, 0)]), JSON.stringify(shape)); } }); test('异形格:非法 shape 抛错', () => { assert.throws(() => Board.fromCells([s('L', 0, 0, 2, 2, [[0, 0], [1, 1]])]), /不连通/); assert.throws(() => Board.fromCells([s('L', 0, 0, 2, 2, [[0, 0], [0, 0]])]), /重复/); assert.throws(() => Board.fromCells([s('L', 0, 0, 2, 2, [[0, 0], [-1, 0]])]), /为负/); assert.throws(() => Board.fromCells([s('L', 0, 0, 2, 2, [[0, 0], [0, 1], [0, 2]])]), /不符/); assert.throws(() => Board.fromCells([s('L', 0, 0, 3, 3, [[0, 0], [1, 0], [2, 0], [2, 1], [0, 1]])]), /单位数/); }); test('异形格邻接:认每一条共享单位边', () => { // L 占 (1,1)(1,2)(1,3)(2,3),缺口在 (2,1)(2,2) const b = Board.fromCells([ s('L', 1, 1, 2, 3, [[0, 0], [0, 1], [0, 2], [1, 2]]), c('a', 2, 1), // 缺口里的格:与 (1,1) 共边 → 相邻 c('d', 2, 2), // 与 (1,2) 共边 → 相邻 c('e', 3, 1), // 只挨到缺口,不与任何 L 单位共边 → 不相邻 ]); const L = b.cellById('L').index; assert.equal(b.areAdjacent(L, b.cellById('a').index), true); assert.equal(b.areAdjacent(L, b.cellById('d').index), true); assert.equal(b.areAdjacent(L, b.cellById('e').index), false); }); test('异形格共享边:共线单位边合并成一整段;自身返回 null', () => { const b = Board.fromCells([c('B', 0, 0, 2, 2), c('V', 2, 0, 1, 2)]); assert.deepEqual(b.sharedEdge(0, 1), { x1: 2, y1: 0, x2: 2, y2: 2, dir: 'v' }, '2×2 与竖条的两条单位边合并'); assert.equal(b.sharedEdge(0, 0), null); }); test('异形格锚点在形内', () => { const b = Board.fromCells([s('L', 1, 1, 2, 3, [[0, 0], [0, 1], [0, 2], [1, 2]]), c('a', 0, 0)]); const [ux, uy] = b.anchorUnit(b.cellById('L').index); assert.ok(b.cellById('L').units.some(([x, y]) => x === ux && y === uy), `锚点 (${ux},${uy}) 必须在 L 内`); const [cx, cy] = b.anchorCenter(b.cellById('a').index); assert.deepEqual([cx, cy], [0.5, 0.5], '矩形格锚点 = bbox 中心'); });