The table's reading prevails: open sides carry sight
An hour of rules archaeology, summarized: DIMENSIONAL WARP denies LOS explicitly (its tokens stay sightless); the AUTO WARP describes how to trace sight across a join; and on the lettered wraparounds the texts are silent — no grant, no denial. The FAQ's Permawarp rulings even have area effects counting distance through warps. Where the texts are silent, the edition's own table breaks the tie, and the table says the open sides see. Board openings carry sight in every revision (a permissive change: every stored game's log replays clean); the brief rev-7 restriction and its legacy flag are gone, along with the sophistry that justified them. The adjacency geometry stands. The friend's sticky-wand shot through the C opening was legal after all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
493f3bc567
commit
4833a229e1
@@ -27,12 +27,7 @@ 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.
|
||||
*/
|
||||
/** An aisle (AUTO) warp joins two edges that meet at a corner. */
|
||||
aisle?: boolean;
|
||||
}
|
||||
|
||||
@@ -45,8 +40,6 @@ 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[][];
|
||||
@@ -353,10 +346,12 @@ function onSegment(ax: number, ay: number, bx: number, by: number, px: number, p
|
||||
}
|
||||
|
||||
/**
|
||||
* Sight through a wraparound opening. Rulebook: "If casting a spell, or
|
||||
* checking line of sight through the AUTO WARP, treat it as a straight
|
||||
* line, and the two connected boards as though they were adjacent" — and
|
||||
* the lettered openings reconnect edges the same way. So the far side is
|
||||
* Sight through a wraparound opening — the boards "have open sides", and
|
||||
* the AUTO WARP rule shows how to trace sight across a join: "treat it as
|
||||
* a straight line, and the two connected boards as though they were
|
||||
* adjacent". The texts never deny the lettered openings what they grant
|
||||
* the aisle (only DIMENSIONAL WARP's tokens carry an explicit "no L.O.S."
|
||||
* — and those are not board warps). So the far side is
|
||||
* virtually abutted at the mouth (rotated if the pairing turns a corner,
|
||||
* as the aisle warps do), and the center-to-center line must pass through
|
||||
* the one-cell opening: diagonals through the gap are as legal as they are
|
||||
@@ -373,7 +368,6 @@ 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;
|
||||
|
||||
@@ -218,8 +218,10 @@ 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").
|
||||
* Rev 7 briefly restricted warp sight to aisle corners; the table's
|
||||
* reading prevailed and all board openings carry sight in every revision.
|
||||
* DIMENSIONAL WARP's tokens never do ("There is no L.O.S. through the
|
||||
* warp." — the card face).
|
||||
*/
|
||||
deckRev?: number;
|
||||
}
|
||||
@@ -292,17 +294,8 @@ export interface GameState {
|
||||
|
||||
/** The board with dynamic wall changes applied — use for movement and LOS. */
|
||||
export function boardView(state: GameState): AssembledBoard {
|
||||
// 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 } : {}),
|
||||
};
|
||||
if (Object.keys(state.edgeOverrides).length === 0) return state.board;
|
||||
return { ...state.board, edges: { ...state.board.edges, ...state.edgeOverrides } };
|
||||
}
|
||||
|
||||
export function sustainedOn(state: GameState, playerId: PlayerId, cardId?: string): SustainedEffect[] {
|
||||
|
||||
@@ -144,18 +144,15 @@ describe("sight and the wraparound openings", () => {
|
||||
{ boardId: "board-b", origin: { x: 0, y: 5 }, rotation: 0 },
|
||||
];
|
||||
|
||||
it("lettered wraparounds carry movement only — no sight across the map", () => {
|
||||
it("the open sides carry sight: mouths see each other through the join", () => {
|
||||
const board = assembleBoard(twoSector);
|
||||
// (2,0) N wraps to (2,9) for walking, but no rule grants sight there,
|
||||
// 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);
|
||||
expect(hasLineOfSight(board, { x: 2, y: 0 }, { x: 2, y: 9 })).toBe(false);
|
||||
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);
|
||||
});
|
||||
|
||||
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);
|
||||
// Even then: walls inside the corridor still block, and a filled mouth chokes.
|
||||
it("walls inside the corridor still block, and a filled mouth chokes", () => {
|
||||
const board = assembleBoard(twoSector);
|
||||
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);
|
||||
});
|
||||
@@ -244,14 +241,5 @@ describe("the aisle warp treats its corner as adjacent — sight included", () =
|
||||
}
|
||||
expect(seen).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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user