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>
135 lines
5.9 KiB
TypeScript
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)`);
|
|
}
|