Implement engine core: board assembly, movement, turns, combat, victory
Pure deterministic game core in @wizwar/engine: seeded RNG (mulberry32, state in GameState so seed+commands replays identically), sector assembly with rotation, junction merging, and wraparound warps (configurable pairings; the 2p diagram crosses its side openings), movement (3 + one number card), geometric line of sight, deck building from the verified card data (asserts 125/200 totals), and the command-to-event reducer: setup with TRAP! redraw and die-roll first player, punching (no combat round 1, no self-attack, once per turn), damage/death with killer-takes-cards and forced discard, treasure stealing with both victory conditions, pick-up-ends-turn, and end-of-turn draw. Events carry full spatial detail for future replay rendering; private card knowledge rides on visibleTo events with a redaction helper. 21 tests passing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
11209cdc5d
commit
a8884592a4
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user