import { describe, expect, it } from "vitest"; import { assembleBoard, edgeState, hasLineOfSight, layoutIds, stepTarget, type SectorPlacement, } from "../src/board"; import { buildDeck } from "../src/cards"; describe("card data", () => { it("basic deck is exactly 125 cards (official 6e rulebook)", () => { expect(buildDeck(["basic"]).length).toBe(125); }); it("basic + expansion1 is exactly 200 cards (125 + 75)", () => { expect(buildDeck(["basic", "expansion1"]).length).toBe(200); }); it("number cards follow the official distribution", () => { const deck = buildDeck(["basic"]); const count = (id: string) => deck.filter((c) => c.cardId === id).length; expect(count("number-2")).toBe(12); expect(count("number-3")).toBe(10); expect(count("number-4")).toBe(7); expect(count("number-5")).toBe(4); expect(count("number-6")).toBe(1); }); }); describe("board assembly", () => { it("knows all six verified layouts", () => { expect(layoutIds().sort()).toEqual( ["board-a", "board-b", "board-c", "board-d", "board-e", "board-f"].sort(), ); }); const twoSector: SectorPlacement[] = [ { boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 }, { boardId: "board-b", origin: { x: 0, y: 5 }, rotation: 0 }, ]; it("merges the junction between stacked sectors into a single wall with a centered corridor", () => { const board = assembleBoard(twoSector); // Junction edges y=4/5: wall everywhere except the centered opening x=2. expect(edgeState(board, { x: 0, y: 4 }, "S")).toBe("wall"); expect(edgeState(board, { x: 1, y: 4 }, "S")).toBe("wall"); expect(edgeState(board, { x: 2, y: 4 }, "S")).toBe("open"); expect(edgeState(board, { x: 3, y: 4 }, "S")).toBe("wall"); expect(edgeState(board, { x: 4, y: 4 }, "S")).toBe("wall"); const step = stepTarget(board, { x: 2, y: 4 }, "S"); expect(step).toEqual({ kind: "step", to: { x: 2, y: 5 } }); }); it("wraps the vertical openings top-to-bottom by default", () => { const board = assembleBoard(twoSector); const up = stepTarget(board, { x: 2, y: 0 }, "N"); expect(up).toEqual({ kind: "warp", to: { x: 2, y: 9 } }); const down = stepTarget(board, { x: 2, y: 9 }, "S"); expect(down).toEqual({ kind: "warp", to: { x: 2, y: 0 } }); }); it("honors explicit crossed pairings (2-player diagram)", () => { const board = assembleBoard(twoSector, { warpPairs: [ [{ sector: 0, side: "N" }, { sector: 1, side: "S" }], [{ sector: 0, side: "W" }, { sector: 1, side: "E" }], [{ sector: 0, side: "E" }, { sector: 1, side: "W" }], ], }); // Leaving the top sector's west opening arrives at the bottom sector's east. expect(stepTarget(board, { x: 0, y: 2 }, "W")).toEqual({ kind: "warp", to: { x: 4, y: 7 } }); expect(stepTarget(board, { x: 4, y: 7 }, "E")).toEqual({ kind: "warp", to: { x: 0, y: 2 } }); }); it("blocks steps through walls and doors", () => { const board = assembleBoard([{ boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 }]); // Layout A wall V (2,1)|(2,2): 1-indexed row 2 col 1 east = 0-indexed (0,1) E. expect(stepTarget(board, { x: 0, y: 1 }, "E")).toEqual({ kind: "blocked", by: "wall" }); // Layout A door H (3,2)-(4,2): 0-indexed (1,2) S. expect(stepTarget(board, { x: 1, y: 2 }, "S")).toEqual({ kind: "blocked", by: "door" }); }); it("rotating a sector 180 degrees preserves its wall count", () => { const flat = assembleBoard([{ boardId: "board-c", origin: { x: 0, y: 0 }, rotation: 0 }]); const rotated = assembleBoard([{ boardId: "board-c", origin: { x: 0, y: 0 }, rotation: 180 }]); const countWalls = (b: typeof flat) => Object.values(b.edges).filter((e) => e === "wall").length; const countDoors = (b: typeof flat) => Object.values(b.edges).filter((e) => e === "door").length; expect(countWalls(rotated)).toBe(countWalls(flat)); expect(countDoors(rotated)).toBe(countDoors(flat)); // Home stays centered under rotation. expect(rotated.homes[0]).toEqual({ x: 2, y: 2 }); }); it("computes line of sight blocked by walls", () => { const board = assembleBoard([{ boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 }]); // Straight down col 0 from (0,0): wall H (3,1)|(4,1) = 0-indexed (0,2) S blocks. expect(hasLineOfSight(board, { x: 0, y: 0 }, { x: 0, y: 1 })).toBe(true); expect(hasLineOfSight(board, { x: 0, y: 0 }, { x: 0, y: 4 })).toBe(false); // Home (2,2) sees one cell up (open edge), but the wall between rows 1-2 // of the home column blocks sight to the top row. expect(hasLineOfSight(board, { x: 2, y: 2 }, { x: 2, y: 1 })).toBe(true); expect(hasLineOfSight(board, { x: 2, y: 2 }, { x: 2, y: 0 })).toBe(false); }); }); describe("player counts 2-6", () => { it("assembles a coherent board for every supported count", async () => { const { setupBoard } = await import("../src/setups"); const { createRng } = await import("../src/rng"); for (const n of [2, 3, 4, 5, 6]) { const { board } = setupBoard(n, createRng(n * 7)); expect(board.homes.length).toBe(n); expect(board.placements.length).toBe(n); // Every warp starts and ends on real cells, through open edges. for (const w of board.warps) { expect(board.cells[`${w.from.cell.x},${w.from.cell.y}`]).toBe(true); expect(board.cells[`${w.to.cell.x},${w.to.cell.y}`]).toBe(true); } // No two sectors share a layout. const ids = board.placements.map((p) => p.boardId); expect(new Set(ids).size).toBe(n); } }); it("full games start at every count", async () => { const { createGame } = await import("../src/game"); for (const n of [3, 5, 6]) { const ids = Array.from({ length: n }, (_, i) => `w${i}`); const { state } = createGame({ playerIds: ids, seed: 99 + n, sets: ["basic", "expansion1"] }); expect(state.players.length).toBe(n); expect(state.treasures.length).toBe(n * 2); } }); });