Rev 19: breaching the rim opens a new warp across the maze

Destroying a wall on the board's outer edge used to leave a one-sided
hole into nothing. The table's physics prevail: the outer rim wraps,
so the breach goes clean through — the opposite perimeter wall in the
same row or column crumbles too (collapse damage and all), and a new
warp pair opens between the two edges, shimmering at both mouths.
Rev-gated: stored games hold one-sided breaches and replay so. Sector
moves recompute the wraparounds and forget improvised openings, as
the maze's own reshaping always has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 17:25:25 -04:00
co-authored by Claude Fable 5
parent 766ec0a726
commit a17b5238f0
6 changed files with 95 additions and 2 deletions
+36
View File
@@ -9,6 +9,7 @@ import {
type SectorPlacement,
} from "../src/board";
import { buildDeck } from "../src/cards";
import { edgeKey, type Side } from "../src/board";
import { activePlayer, applyCommand, boardView, createGame, gameLos } from "../src/game";
import { createRng } from "../src/rng";
import { setupBoard } from "../src/setups";
@@ -279,3 +280,38 @@ describe("bricking over a warp mouth", () => {
expect(stepTarget(boardView(r2.state), w.from.cell, w.from.side).kind).toBe("warp");
});
});
describe("breaching the rim (rules rev 19)", () => {
it("destroying a perimeter wall opens both sides as a new warp", () => {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev: 19 });
for (let i = 0; i < 2; i++) {
const r = applyCommand(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
// Find a rim wall with a wrap counterpart that is NOT already an opening.
const view = boardView(state);
for (const key of Object.keys(view.cells)) {
const [x, y] = key.split(",").map(Number) as [number, number];
for (const side of ["N", "E", "S", "W"] as Side[]) {
const n = { x: x + (side === "E" ? 1 : side === "W" ? -1 : 0), y: y + (side === "S" ? 1 : side === "N" ? -1 : 0) };
if (view.cells[`${n.x},${n.y}`]) continue;
if ((view.edges[edgeKey({ x, y }, side)] ?? "open") !== "wall") continue;
const caster = activePlayer(state);
caster.position = { x, y };
caster.hand[0] = { instanceId: "dw#1", cardId: "destroy-wall" };
const warpsBefore = state.board.warps.length;
const r = applyCommand(state, caster.id, {
type: "cast", instanceId: "dw#1", target: { kind: "edge", cell: { x, y }, side },
});
if (!r.ok) throw new Error(r.error);
if (r.state.board.warps.length === warpsBefore) continue; // no counterpart in this row
expect(r.state.board.warps.length).toBe(warpsBefore + 2);
const w = r.state.board.warps[warpsBefore]!;
expect(stepTarget(boardView(r.state), w.from.cell, w.from.side).kind).toBe("warp");
return;
}
}
throw new Error("setup: no breachable rim wall found");
});
});