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
@@ -238,3 +238,60 @@
[ 941634ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1 [ 941634ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 947338ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1 [ 947338ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 950320ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1 [ 950320ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 954153ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 957484ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 961665ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 965207ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 968046ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 971084ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 973824ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 980166ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 984179ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 989191ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 995232ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 997946ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1001234ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1006913ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1011516ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1014303ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1018041ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1021328ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1026098ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1030726ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1035086ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1041304ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1044467ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1049226ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1052558ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1058154ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1061028ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1065787ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1071637ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1075522ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1080951ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1085930ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1088820ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1092037ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1098390ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1102678ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1105217ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1109419ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1115559ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1121996ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1126779ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1132972ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1137004ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1140326ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1144869ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1150935ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1153701ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1157899ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1164235ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1169866ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1175925ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1182032ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1185270ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1190429ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1194642ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1197213ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
[ 1202483ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8899/ws' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8899/assets/index-Di4XAJUy.js:1
+15 -4
View File
@@ -27,6 +27,13 @@ export interface Warp {
from: { cell: Cell; side: Side }; from: { cell: Cell; side: Side };
/** ...you arrive at this cell (entering through `side`). */ /** ...you arrive at this cell (entering through `side`). */
to: { cell: Cell; side: 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 { export interface AssembledBoard {
@@ -38,6 +45,8 @@ export interface AssembledBoard {
/** Cells that are part of the map (all cells inside placed sectors). */ /** Cells that are part of the map (all cells inside placed sectors). */
cells: Record<string, true>; cells: Record<string, true>;
warps: Warp[]; warps: Warp[];
/** Pre-rev-7 games saw through every warp; their replays keep that sight. */
sightThroughAllWarps?: boolean;
/** Per player-sector info, in placement order. */ /** Per player-sector info, in placement order. */
homes: Cell[]; homes: Cell[];
treasureSpaces: Cell[][]; treasureSpaces: Cell[][];
@@ -119,7 +128,7 @@ export interface AssemblyOptions {
* openings pair straight across the map — correct for the 4p square; the * openings pair straight across the map — correct for the 4p square; the
* standard 2p column crosses its side openings and must pass pairs. * 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) { 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 ca = openingCell(a.sector, a.side);
const cb = openingCell(b.sector, b.side); const cb = openingCell(b.sector, b.side);
warps.push({ from: { cell: ca, side: a.side }, to: { cell: cb, side: b.side } }); const aisle = kind === "aisle" ? { aisle: true as const } : {};
warps.push({ from: { cell: cb, side: b.side }, to: { cell: ca, side: a.side } }); 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 { } else {
for (let i = 0; i < placements.length; i++) { for (let i = 0; i < placements.length; i++) {
@@ -363,6 +373,7 @@ export function hasWarpLineOfSight(
}; };
const EPS = 1e-9; const EPS = 1e-9;
for (const w of board.warps) { for (const w of board.warps) {
if (!w.aisle && !board.sightThroughAllWarps) continue;
const mouthA = w.from.cell; const mouthA = w.from.cell;
const sideA = w.from.side; const sideA = w.from.side;
const mouthB = w.to.cell; 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, * 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 * and bars monsters from his square. Rev 6: ANTI-ANTI cannot pin a
* teleport escape ("does not work against escape" — the card face). * 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; deckRev?: number;
} }
@@ -290,8 +292,17 @@ export interface GameState {
/** The board with dynamic wall changes applied — use for movement and LOS. */ /** The board with dynamic wall changes applied — use for movement and LOS. */
export function boardView(state: GameState): AssembledBoard { export function boardView(state: GameState): AssembledBoard {
if (Object.keys(state.edgeOverrides).length === 0) return state.board; // Rev 7 restricts warp sight to the aisle corners; earlier games keep the
return { ...state.board, edges: { ...state.board.edges, ...state.edgeOverrides } }; // 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[] { 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, { const board = assembleBoard(placements, {
warpPairs: [ 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: 0, side: "N" }, { sector: 2, side: "S" }],
[{ sector: 1, side: "W" }, { sector: 2, side: "E" }], [{ sector: 1, side: "W" }, { sector: 2, side: "E" }],
[{ sector: 0, side: "E" }, { sector: 1, side: "S" }], [{ sector: 0, side: "E" }, { sector: 1, side: "S" }],
@@ -106,10 +106,10 @@ export function setupBoard(playerCount: number, rng: RngState): SetupResult {
warpPairs: [ warpPairs: [
[{ sector: 0, side: "N" }, { sector: 4, side: "S" }], // tip to tip [{ sector: 0, side: "N" }, { sector: 4, side: "S" }], // tip to tip
[{ sector: 1, side: "W" }, { sector: 3, side: "E" }], [{ sector: 1, side: "W" }, { sector: 3, side: "E" }],
[{ sector: 0, side: "W" }, { sector: 1, side: "N" }], // aisle-warp corners [{ sector: 0, side: "W" }, { sector: 1, side: "N" }, "aisle"], // aisle-warp corners
[{ sector: 0, side: "E" }, { sector: 3, side: "N" }], [{ sector: 0, side: "E" }, { sector: 3, side: "N" }, "aisle"],
[{ sector: 4, side: "W" }, { sector: 1, side: "S" }], [{ sector: 4, side: "W" }, { sector: 1, side: "S" }, "aisle"],
[{ sector: 4, side: "E" }, { sector: 3, side: "S" }], [{ sector: 4, side: "E" }, { sector: 3, side: "S" }, "aisle"],
], ],
}); });
return { board, rng: rngN }; return { board, rng: rngN };
+37 -45
View File
@@ -138,39 +138,26 @@ describe("player counts 2-6", () => {
}); });
}); });
describe("line of sight through wraparound openings", () => { describe("sight and the wraparound openings", () => {
const twoSector: SectorPlacement[] = [ const twoSector: SectorPlacement[] = [
{ boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 }, { boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 },
{ boardId: "board-b", origin: { x: 0, y: 5 }, rotation: 0 }, { boardId: "board-b", origin: { x: 0, y: 5 }, rotation: 0 },
]; ];
it("sees straight down the corridor, out one mouth and in the other", () => { it("lettered wraparounds carry movement only — no sight across the map", () => {
const board = assembleBoard(twoSector); const board = assembleBoard(twoSector);
// (2,0) N wraps to (2,9): the mouths see each other through the join. // (2,0) N wraps to (2,9) for walking, but no rule grants sight there,
expect(hasLineOfSight(board, { x: 2, y: 0 }, { x: 2, y: 9 })).toBe(false); // and DIMENSIONAL WARP's own face denies it for warps generally.
expect(sightBetween(board, { x: 2, y: 0 }, { x: 2, y: 9 })).toBe(false);
expect(sightBetween(board, { x: 2, y: 9 }, { x: 2, y: 0 })).toBe(false);
});
it("pre-rev-7 games keep the wider sight their commands were validated under", () => {
const board = { ...assembleBoard(twoSector), sightThroughAllWarps: true };
expect(sightBetween(board, { x: 2, y: 0 }, { x: 2, y: 9 })).toBe(true); 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); // Even then: walls inside the corridor still block, and a filled mouth chokes.
}); 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);
it("demands a straight corridor: off-axis viewers see nothing", () => {
const board = assembleBoard(twoSector);
expect(sightBetween(board, { x: 1, y: 0 }, { x: 2, y: 9 })).toBe(false);
expect(sightBetween(board, { x: 3, y: 1 }, { x: 2, y: 9 })).toBe(false);
});
it("walls inside either corridor still block the warp sight", () => {
const board = assembleBoard(twoSector);
// Layout A: the wall between (2,0) and (2,1)'s row (home column sight is
// cut above row 2 — pinned earlier) blocks a deeper viewer's warp sight.
const deepViewer = { x: 2, y: 2 };
expect(hasLineOfSight(board, deepViewer, { x: 2, y: 0 })).toBe(false);
expect(sightBetween(board, deepViewer, { x: 2, y: 9 })).toBe(false);
});
it("a filled mouth chokes the tunnel", () => {
const board = assembleBoard(twoSector);
const stone = { "2,9": true as const };
expect(sightBetween(board, { x: 2, y: 0 }, { x: 2, y: 8 }, stone)).toBe(false);
}); });
}); });
@@ -237,29 +224,34 @@ describe("setup diagram pairings (rulebook Set-Up Diagram)", () => {
}); });
}); });
describe("sight through warps treats the boards as adjacent", () => { describe("the aisle warp treats its corner as adjacent — sight included", () => {
const twoSector: SectorPlacement[] = [ it("sight passes through the AUTO WARP, diagonals and all", () => {
{ boardId: "board-a", origin: { x: 0, y: 0 }, rotation: 0 }, const { board } = setupBoardForPins(3, createRng(7));
{ boardId: "board-b", origin: { x: 0, y: 5 }, rotation: 0 }, const aisle = board.warps.find((w) => w.aisle)!;
]; expect(aisle).toBeDefined();
// From the aisle mouth, squares beyond the corner are visible — including
it("diagonal lines pass through the one-cell mouth like any doorway", () => { // at least one OFF the straight axis (the adjacency promise).
const board = assembleBoard(twoSector); const mouth = aisle.from.cell;
// From SOME warp mouth, at least one square off the straight corridor const axisVertical = aisle.from.side === "N" || aisle.from.side === "S";
// axis must be visible through the opening — the adjacency promise. let seen = 0, offAxis = 0;
let offAxis = 0;
for (const w of board.warps) {
const mouth = w.from.cell;
const axisVertical = w.from.side === "N" || w.from.side === "S";
for (const key of Object.keys(board.cells)) { for (const key of Object.keys(board.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number]; const [x, y] = key.split(",").map(Number) as [number, number];
const onAxis = axisVertical ? x === mouth.x : y === mouth.y; if (hasLineOfSight(board, mouth, { x, y })) continue;
if (onAxis) continue; if (sightBetween(board, mouth, { x, y })) {
if (!hasLineOfSight(board, mouth, { x, y }) && sightBetween(board, mouth, { x, y })) { seen++;
offAxis++; if (axisVertical ? x !== mouth.x : y !== mouth.y) offAxis++;
}
} }
} }
expect(seen).toBeGreaterThan(0);
expect(offAxis).toBeGreaterThan(0); expect(offAxis).toBeGreaterThan(0);
// A lettered (non-aisle) mouth on the same board sees nothing extra.
const lettered = board.warps.find((w) => !w.aisle)!;
const lm = lettered.from.cell;
let extra = 0;
for (const key of Object.keys(board.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number];
if (!hasLineOfSight(board, lm, { x, y }) && sightBetween(board, lm, { x, y })) extra++;
}
expect(extra).toBe(0);
}); });
}); });
+1 -1
View File
@@ -46,7 +46,7 @@ export interface Room {
const rooms = new Map<string, Room>(); const rooms = new Map<string, Room>();
/** Rules revision new games are dealt under (stored games keep their own). */ /** Rules revision new games are dealt under (stored games keep their own). */
const RULES_REV = 6; const RULES_REV = 7;
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+1 -1
View File
@@ -101,7 +101,7 @@ class LocalGame {
seed, seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"], sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}), ...(colors ? { colors } : {}),
deckRev: 6, deckRev: 7,
}; };
const { state, events } = createGame(config); const { state, events } = createGame(config);
this.config = config; this.config = config;