Files
wizwar6e/packages/engine/src/setups.ts
T
Eric WagonerandClaude Fable 5 493f3bc567 Rev 7: sight passes only through the aisle warps
The cards settle what the morning's feature got wrong. DIMENSIONAL
WARP: "There is no L.O.S. through the warp." The AUTO WARP: "for all
purposes, treat the connected board edges as though they are
adjacent" — an explicit grant, for edges that physically meet at a
corner. The lettered wraparounds get no grant at all: their rule is
about walking. So the pattern is that warps carry movement, not
sight, unless the text says otherwise — and only the aisle corners
say otherwise.

Warps now carry an aisle flag (the 3-player AUTO WARP and the
5-player corner arcs); hasWarpLineOfSight honors only those, with
the adjacency geometry — diagonals through the corner mouth —
unchanged from this morning where it rightfully applies. Games begun
before rev 7 keep the wider sight their logged commands were
validated against, via a legacy flag boardView stamps from the
game's rules revision. Tests pin all three states: lettered mouths
see nothing, aisle mouths see the corner fan, legacy games see
everything they used to.

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

135 lines
5.9 KiB
TypeScript

// Board configurations from the 6e rulebook's setup diagram. Sectors are
// shuffled and randomly rotated, then laid out per player count; each player
// gets the sector containing their home star.
//
// All five layouts are verified against the rulebook's Set-Up Diagram
// (research/images/6e-rules-setup-diagram.jpg): 2p crossed pairs, 3p stair
// with the Aisle Warp arc, 4p and 6p straight-across, 5p plus with corner
// warps at every notch. board.test.ts pins each pairing.
import {
assembleBoard,
type AssembledBoard,
type Rotation,
type SectorPlacement,
} from "./board";
import { layoutIds } from "./board";
import { shuffle, nextInt, type RngState } from "./rng";
export const SUPPORTED_PLAYER_COUNTS = [2, 3, 4, 5, 6] as const;
export interface SetupResult {
board: AssembledBoard;
rng: RngState;
}
export function setupBoard(playerCount: number, rng: RngState): SetupResult {
const [shuffled, rng1] = shuffle(rng, layoutIds());
let rngN = rng1;
const pick = (n: number): { id: string; rotation: Rotation }[] => {
const chosen: { id: string; rotation: Rotation }[] = [];
for (let i = 0; i < n; i++) {
const [r, next] = nextInt(rngN, 4);
rngN = next;
chosen.push({ id: shuffled[i]!, rotation: (r * 90) as Rotation });
}
return chosen;
};
if (playerCount === 2) {
const picks = pick(2);
const placements: SectorPlacement[] = [
{ boardId: picks[0]!.id, origin: { x: 0, y: 0 }, rotation: picks[0]!.rotation },
{ boardId: picks[1]!.id, origin: { x: 0, y: 5 }, rotation: picks[1]!.rotation },
];
// Diagram: top {top A, left C, right B}, bottom {left B, right C, bottom A}
// — A wraps vertically; the side openings pair CROSSED (top-left to
// bottom-right and top-right to bottom-left).
const board = assembleBoard(placements, {
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" }],
],
});
return { board, rng: rngN };
}
if (playerCount === 3) {
// The rulebook's stair/L: one sector on the top row (offset right), two
// on the bottom row. The two openings facing the concave notch join via
// the AUTO WARP; the convex openings pair as below (interpretation of
// the diagram's letters — geometrically matched pairs).
const picks = pick(3);
const placements: SectorPlacement[] = [
{ boardId: picks[0]!.id, origin: { x: 5, y: 0 }, rotation: picks[0]!.rotation },
{ boardId: picks[1]!.id, origin: { x: 0, y: 5 }, rotation: picks[1]!.rotation },
{ boardId: picks[2]!.id, origin: { x: 5, y: 5 }, rotation: picks[2]!.rotation },
];
const board = assembleBoard(placements, {
warpPairs: [
[{ sector: 0, side: "W" }, { sector: 1, side: "N" }, "aisle"], // AUTO WARP corner
[{ sector: 0, side: "N" }, { sector: 2, side: "S" }],
[{ sector: 1, side: "W" }, { sector: 2, side: "E" }],
[{ sector: 0, side: "E" }, { sector: 1, side: "S" }],
],
});
return { board, rng: rngN };
}
if (playerCount === 4) {
const picks = pick(4);
const placements: SectorPlacement[] = [
{ boardId: picks[0]!.id, origin: { x: 0, y: 0 }, rotation: picks[0]!.rotation },
{ boardId: picks[1]!.id, origin: { x: 5, y: 0 }, rotation: picks[1]!.rotation },
{ boardId: picks[2]!.id, origin: { x: 0, y: 5 }, rotation: picks[2]!.rotation },
{ boardId: picks[3]!.id, origin: { x: 5, y: 5 }, rotation: picks[3]!.rotation },
];
// 4p square wraps straight across (left openings to right openings on the
// same rows, top to bottom on the same columns) — the default pairing.
const board = assembleBoard(placements);
return { board, rng: rngN };
}
if (playerCount === 5) {
// The plus/cross: four arms around a center. Opposite arm tips wrap to
// each other; the notch-facing side openings warp around the corners.
const picks = pick(5);
const placements: SectorPlacement[] = [
{ boardId: picks[0]!.id, origin: { x: 5, y: 0 }, rotation: picks[0]!.rotation }, // N arm
{ boardId: picks[1]!.id, origin: { x: 0, y: 5 }, rotation: picks[1]!.rotation }, // W arm
{ boardId: picks[2]!.id, origin: { x: 5, y: 5 }, rotation: picks[2]!.rotation }, // center
{ boardId: picks[3]!.id, origin: { x: 10, y: 5 }, rotation: picks[3]!.rotation }, // E arm
{ boardId: picks[4]!.id, origin: { x: 5, y: 10 }, rotation: picks[4]!.rotation }, // S arm
];
const board = assembleBoard(placements, {
warpPairs: [
[{ sector: 0, side: "N" }, { sector: 4, side: "S" }], // tip to tip
[{ sector: 1, side: "W" }, { sector: 3, side: "E" }],
[{ sector: 0, side: "W" }, { sector: 1, side: "N" }, "aisle"], // aisle-warp corners
[{ sector: 0, side: "E" }, { sector: 3, side: "N" }, "aisle"],
[{ sector: 4, side: "W" }, { sector: 1, side: "S" }, "aisle"],
[{ sector: 4, side: "E" }, { sector: 3, side: "S" }, "aisle"],
],
});
return { board, rng: rngN };
}
if (playerCount === 6) {
// 3x2 rectangle: straight-across wrap (the default pairing).
const picks = pick(6);
const placements: SectorPlacement[] = [
{ boardId: picks[0]!.id, origin: { x: 0, y: 0 }, rotation: picks[0]!.rotation },
{ boardId: picks[1]!.id, origin: { x: 5, y: 0 }, rotation: picks[1]!.rotation },
{ boardId: picks[2]!.id, origin: { x: 10, y: 0 }, rotation: picks[2]!.rotation },
{ boardId: picks[3]!.id, origin: { x: 0, y: 5 }, rotation: picks[3]!.rotation },
{ boardId: picks[4]!.id, origin: { x: 5, y: 5 }, rotation: picks[4]!.rotation },
{ boardId: picks[5]!.id, origin: { x: 10, y: 5 }, rotation: picks[5]!.rotation },
];
const board = assembleBoard(placements);
return { board, rng: rngN };
}
throw new Error(`unsupported player count: ${playerCount} (supported: 2-6)`);
}