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>
This commit is contained in:
Eric Wagoner
2026-08-16 15:25:00 -04:00
co-authored by Claude Fable 5
parent 8af8962330
commit 493f3bc567
7 changed files with 131 additions and 60 deletions
+15 -4
View File
@@ -27,6 +27,13 @@ export interface Warp {
from: { cell: Cell; side: Side };
/** ...you arrive at this cell (entering through `side`). */
to: { cell: Cell; side: Side };
/**
* An aisle (AUTO) warp joins two edges that physically meet at a corner:
* "for all purposes, treat the connected board edges as though they are
* adjacent" — including line of sight. The lettered wraparounds carry
* movement only; no rule grants sight across the map.
*/
aisle?: boolean;
}
export interface AssembledBoard {
@@ -38,6 +45,8 @@ export interface AssembledBoard {
/** Cells that are part of the map (all cells inside placed sectors). */
cells: Record<string, true>;
warps: Warp[];
/** Pre-rev-7 games saw through every warp; their replays keep that sight. */
sightThroughAllWarps?: boolean;
/** Per player-sector info, in placement order. */
homes: Cell[];
treasureSpaces: Cell[][];
@@ -119,7 +128,7 @@ export interface AssemblyOptions {
* openings pair straight across the map — correct for the 4p square; the
* standard 2p column crosses its side openings and must pass pairs.
*/
warpPairs?: [OpeningRef, OpeningRef][];
warpPairs?: ([OpeningRef, OpeningRef] | [OpeningRef, OpeningRef, "aisle"])[];
}
/**
@@ -204,11 +213,12 @@ export function assembleBoard(
};
if (options.warpPairs) {
for (const [a, b] of options.warpPairs) {
for (const [a, b, kind] of options.warpPairs) {
const ca = openingCell(a.sector, a.side);
const cb = openingCell(b.sector, b.side);
warps.push({ from: { cell: ca, side: a.side }, to: { cell: cb, side: b.side } });
warps.push({ from: { cell: cb, side: b.side }, to: { cell: ca, side: a.side } });
const aisle = kind === "aisle" ? { aisle: true as const } : {};
warps.push({ from: { cell: ca, side: a.side }, to: { cell: cb, side: b.side }, ...aisle });
warps.push({ from: { cell: cb, side: b.side }, to: { cell: ca, side: a.side }, ...aisle });
}
} else {
for (let i = 0; i < placements.length; i++) {
@@ -363,6 +373,7 @@ export function hasWarpLineOfSight(
};
const EPS = 1e-9;
for (const w of board.warps) {
if (!w.aisle && !board.sightThroughAllWarps) continue;
const mouthA = w.from.cell;
const sideA = w.from.side;
const mouthB = w.to.cell;
+13 -2
View File
@@ -218,6 +218,8 @@ export interface GameConfig {
* BIG MAN pushes occupants ahead, steps over floor hazards for 2 points,
* and bars monsters from his square. Rev 6: ANTI-ANTI cannot pin a
* teleport escape ("does not work against escape" — the card face).
* Rev 7: sight passes only through aisle-warp corners, not the lettered
* wraparounds ("no rule grants it; DIMENSIONAL WARP explicitly denies it").
*/
deckRev?: number;
}
@@ -290,8 +292,17 @@ export interface GameState {
/** The board with dynamic wall changes applied — use for movement and LOS. */
export function boardView(state: GameState): AssembledBoard {
if (Object.keys(state.edgeOverrides).length === 0) return state.board;
return { ...state.board, edges: { ...state.board.edges, ...state.edgeOverrides } };
// Rev 7 restricts warp sight to the aisle corners; earlier games keep the
// wider sight their commands were validated against.
const legacySight = (state.config.deckRev ?? 1) < 7;
if (Object.keys(state.edgeOverrides).length === 0) {
return legacySight ? { ...state.board, sightThroughAllWarps: true } : state.board;
}
return {
...state.board,
edges: { ...state.board.edges, ...state.edgeOverrides },
...(legacySight ? { sightThroughAllWarps: true } : {}),
};
}
export function sustainedOn(state: GameState, playerId: PlayerId, cardId?: string): SustainedEffect[] {
+5 -5
View File
@@ -68,7 +68,7 @@ export function setupBoard(playerCount: number, rng: RngState): SetupResult {
];
const board = assembleBoard(placements, {
warpPairs: [
[{ sector: 0, side: "W" }, { sector: 1, side: "N" }], // AUTO WARP corner
[{ 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" }],
@@ -106,10 +106,10 @@ export function setupBoard(playerCount: number, rng: RngState): SetupResult {
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-warp corners
[{ sector: 0, side: "E" }, { sector: 3, side: "N" }],
[{ sector: 4, side: "W" }, { sector: 1, side: "S" }],
[{ sector: 4, side: "E" }, { sector: 3, side: "S" }],
[{ 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 };