Files
wizwar6e/packages/engine/test/board.test.ts
T
Eric WagonerandClaude Fable 5 85b40be113 Support 2-6 players: the L, the cross, and the 3x2 from the rulebook
Three player counts join the square and the column: 3 players get the
stair/L with the AUTO WARP joining the two openings that face the
concave notch (convex openings pair geometrically — flagged as
interpretation of the diagram's letters); 5 players get the plus/cross
with tip-to-tip wraps and aisle-warp corners; 6 players get the 3x2
rectangle with straight-across wraps. All six verified layouts are
drawn on, so no count repeats a sector. Rooms accept up to six seats;
the lobby copy follows. 122 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:08:15 -04:00

139 lines
5.9 KiB
TypeScript

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);
}
});
});