Files
wizwar6e/packages/engine/test/board.test.ts
T
Eric WagonerandClaude Fable 5 4a91d56d24 Credibility pass: the collapse loses its scaffolding
Blind reviews over the rev-37..40 features and the gate collapse. The
scripted surgery's scars come out: bare brace blocks unwrapped (with one
hiding an unreachable scrambleHands), the amputated comment healed, the
dead events parameter unthreaded from the illusion-sight chain, the
laziness probe in perceivedBoard collapsed to the rule it always produced,
GameView.deckRev retired with its last reader, the ward's arming stub
told honestly, and every comment or test title still citing the retired
revision numbering reworded to the rule it guards. Rim mechanics unify on
rimWarpMouth and breachRim; the test suite gains plainRimWall and
pushSustained and loses its loop-shaped scars and rev-named helpers.
No behavior changes; 255 tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 13:04:01 -04:00

318 lines
15 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
assembleBoard,
edgeState,
hasLineOfSight,
sightBetween,
layoutIds,
stepTarget,
type SectorPlacement,
} from "../src/board";
import { buildDeck } from "../src/cards";
import { edgeKey, type Side } from "../src/board";
import { activePlayer, applyCommand, boardView, createGame, gameLos } from "../src/game";
import { createRng } from "../src/rng";
import { setupBoard } from "../src/setups";
import { giveCard } from "./helpers";
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", () => {
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", () => {
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);
}
});
});
describe("sight and the wraparound openings", () => {
const twoSector: SectorPlacement[] = [
{ boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 },
{ boardId: "board-b", origin: { x: 0, y: 5 }, rotation: 0 },
];
it("the open sides carry sight: mouths see each other through the join", () => {
const board = assembleBoard(twoSector);
expect(hasLineOfSight(board, { x: 2, y: 0 }, { x: 2, y: 9 })).toBe(false);
expect(sightBetween(board, { x: 2, y: 0 }, { x: 2, y: 9 })).toBe(true);
expect(sightBetween(board, { x: 2, y: 9 }, { x: 2, y: 0 })).toBe(true);
});
it("walls inside the corridor still block, and a filled mouth chokes", () => {
const board = assembleBoard(twoSector);
expect(sightBetween(board, { x: 2, y: 2 }, { x: 2, y: 9 })).toBe(false);
expect(sightBetween(board, { x: 2, y: 0 }, { x: 2, y: 8 }, { "2,9": true })).toBe(false);
});
});
describe("setup diagram pairings (rulebook Set-Up Diagram)", () => {
function onEdge(c: { x: number; y: number }, origin: { x: number; y: number }, side: string): boolean {
if (side === "N") return c.y === origin.y && c.x >= origin.x && c.x < origin.x + 5;
if (side === "S") return c.y === origin.y + 4 && c.x >= origin.x && c.x < origin.x + 5;
if (side === "W") return c.x === origin.x && c.y >= origin.y && c.y < origin.y + 5;
return c.x === origin.x + 4 && c.y >= origin.y && c.y < origin.y + 5;
}
function hasWarp(board: { warps: { from: { cell: { x: number; y: number } }; to: { cell: { x: number; y: number } } }[] },
a: { o: { x: number; y: number }; s: string }, b: { o: { x: number; y: number }; s: string }): boolean {
return board.warps.some((w) => onEdge(w.from.cell, a.o, a.s) && onEdge(w.to.cell, b.o, b.s));
}
it("3 players: the aisle-warp arc plus B, C, A letter pairs", () => {
const { board } = setupBoard(3, createRng(7));
const top = { x: 5, y: 0 }, bl = { x: 0, y: 5 }, br = { x: 5, y: 5 };
expect(hasWarp(board, { o: top, s: "W" }, { o: bl, s: "N" })).toBe(true); // Aisle Warp
expect(hasWarp(board, { o: top, s: "N" }, { o: br, s: "S" })).toBe(true); // B
expect(hasWarp(board, { o: top, s: "E" }, { o: bl, s: "S" })).toBe(true); // C
expect(hasWarp(board, { o: bl, s: "W" }, { o: br, s: "E" })).toBe(true); // A
});
it("5 players: opposite tips wrap, notch corners arc around", () => {
const { board } = setupBoard(5, createRng(7));
const n = { x: 5, y: 0 }, w = { x: 0, y: 5 }, e = { x: 10, y: 5 }, s = { x: 5, y: 10 };
expect(hasWarp(board, { o: n, s: "N" }, { o: s, s: "S" })).toBe(true); // A
expect(hasWarp(board, { o: w, s: "W" }, { o: e, s: "E" })).toBe(true); // B
expect(hasWarp(board, { o: n, s: "W" }, { o: w, s: "N" })).toBe(true); // corners
expect(hasWarp(board, { o: n, s: "E" }, { o: e, s: "N" })).toBe(true);
expect(hasWarp(board, { o: s, s: "W" }, { o: w, s: "S" })).toBe(true);
expect(hasWarp(board, { o: s, s: "E" }, { o: e, s: "S" })).toBe(true);
});
it("6 players: straight-across wrap on both axes", () => {
const { board } = setupBoard(6, createRng(7));
for (const w of board.warps) {
const colinear = w.from.cell.x === w.to.cell.x || w.from.cell.y === w.to.cell.y;
expect(colinear).toBe(true);
}
});
it("relocation discards the aisle warp: only opposite edges connect after", () => {
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 7, sets: ["basic"] });
const bent = (warps: typeof state.board.warps) =>
warps.filter((w) => w.from.cell.x !== w.to.cell.x && w.from.cell.y !== w.to.cell.y);
expect(bent(state.board.warps).length).toBeGreaterThan(0); // the arc exists
const me = activePlayer(state);
const rel = giveCard(state, me.id, "relocate-sector");
// Slide the top sector from (5,0) across to (0,0): an L becomes a block.
const r = applyCommand(state, me.id, {
type: "cast", instanceId: rel.instanceId,
target: { kind: "cell", cell: { x: 0, y: 0 } }, params: { cell: { x: 7, y: 2 } },
});
if (!r.ok) throw new Error(r.error);
expect(bent(r.state.board.warps).length).toBe(0); // "only opposite board edges connect"
});
});
describe("the aisle warp treats its corner as adjacent — sight included", () => {
it("sight passes through the AUTO WARP, diagonals and all", () => {
const { board } = setupBoard(3, createRng(7));
const aisle = board.warps.find(
(w) => w.from.cell.x !== w.to.cell.x && w.from.cell.y !== w.to.cell.y,
)!;
expect(aisle).toBeDefined();
// From the aisle mouth, squares beyond the corner are visible — including
// at least one OFF the straight axis (the adjacency promise).
const mouth = aisle.from.cell;
const axisVertical = aisle.from.side === "N" || aisle.from.side === "S";
let seen = 0, offAxis = 0;
for (const key of Object.keys(board.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number];
if (hasLineOfSight(board, mouth, { x, y })) continue;
if (sightBetween(board, mouth, { x, y })) {
seen++;
if (axisVertical ? x !== mouth.x : y !== mouth.y) offAxis++;
}
}
expect(seen).toBeGreaterThan(0);
expect(offAxis).toBeGreaterThan(0);
});
});
describe("bricking over a warp mouth", () => {
it("create wall seals a lettered opening: no passage, no sight", () => {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"] });
// round 1 passes
for (let i = 0; i < 2; i++) {
const r = applyCommand(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
const w = state.board.warps[0]!;
const caster = activePlayer(state);
caster.position = { ...w.from.cell };
caster.hand[0] = { instanceId: "cw#1", cardId: "create-wall" };
const r = applyCommand(state, caster.id, {
type: "cast", instanceId: "cw#1",
target: { kind: "edge", cell: w.from.cell, side: w.from.side },
});
if (!r.ok) throw new Error(r.error);
state = r.state;
// Passage: stepping into the warp is now a wall.
const step = stepTarget(boardView(state), w.from.cell, w.from.side);
expect(step.kind).toBe("blocked");
// Sight: the far mouth no longer carries warp sight.
expect(gameLos(state, w.from.cell, w.to.cell)).toBe(false);
// One tunnel: the FAR mouth is walled too, and unwalling it reopens both.
const farStep = stepTarget(boardView(state), w.to.cell, w.to.side);
expect(farStep.kind).toBe("blocked");
const caster2 = activePlayer(state);
caster2.position = { ...w.to.cell };
caster2.hand[0] = { instanceId: "dw#1", cardId: "destroy-wall" };
const r2 = applyCommand(state, caster2.id, {
type: "cast", instanceId: "dw#1",
target: { kind: "edge", cell: w.to.cell, side: w.to.side },
});
if (!r2.ok) throw new Error(r2.error);
expect(stepTarget(boardView(r2.state), w.from.cell, w.from.side).kind).toBe("warp");
});
});
describe("breaching the rim", () => {
it("destroying a perimeter wall opens both sides as a new warp", () => {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"] });
for (let i = 0; i < 2; i++) {
const r = applyCommand(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
// Find a rim wall with a wrap counterpart that is NOT already an opening.
const view = boardView(state);
for (const key of Object.keys(view.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number];
for (const side of ["N", "E", "S", "W"] as Side[]) {
const n = { x: x + (side === "E" ? 1 : side === "W" ? -1 : 0), y: y + (side === "S" ? 1 : side === "N" ? -1 : 0) };
if (view.cells[`${n.x},${n.y}`]) continue;
if ((view.edges[edgeKey({ x, y }, side)] ?? "open") !== "wall") continue;
const caster = activePlayer(state);
caster.position = { x, y };
caster.hand[0] = { instanceId: "dw#1", cardId: "destroy-wall" };
const warpsBefore = state.board.warps.length;
const r = applyCommand(state, caster.id, {
type: "cast", instanceId: "dw#1", target: { kind: "edge", cell: { x, y }, side },
});
if (!r.ok) throw new Error(r.error);
if (r.state.board.warps.length === warpsBefore) continue; // no counterpart in this row
expect(r.state.board.warps.length).toBe(warpsBefore + 2);
const w = r.state.board.warps[warpsBefore]!;
expect(stepTarget(boardView(r.state), w.from.cell, w.from.side).kind).toBe("warp");
return;
}
}
throw new Error("setup: no breachable rim wall found");
});
});