Rev 17: a warp mouth on a pit's rim is a way off it (M4Q7)

Kestrel stood beside a pit dug against the board's west edge, walls
north and south of it, and was told there was nothing to land on — the
rim check counted only neighbouring squares, and the warp mouth beyond
the pit is not one. From rev 17 a mouth on the rim is an exit like any
square, named at a fork like any other; every older game takes the
mouth when no square offers, so the refused crossing goes through as
it stands. The jump through a mouth carries `via: "warp"`: the board
shimmers both mouths instead of streaking across the maze, and the
reel cuts rather than gliding. The automaton leaves a pit only by its
exits. Three tests walk the rim under both readings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-05 22:20:18 -04:00
co-authored by Claude Fable 5.1
parent dc4d2a1221
commit 461c1a5c83
6 changed files with 124 additions and 22 deletions
@@ -537,3 +537,74 @@ describe("the boobytrap keeps its secret", () => {
}
});
});
describe("a pit on the board's rim (M4Q7)", () => {
/** A square on the outer rim with a warp mouth on one side, entered from
* the square opposite the mouth; its other neighbouring squares listed. */
function rimPitSite(state: GameState) {
const view = boardView(state);
for (const k of Object.keys(view.cells)) {
const [x, y] = k.split(",").map(Number) as [number, number];
const pit = { x, y };
if (state.squareContents[k] || view.homes.some((h) => cellKey(h) === k)) continue;
for (const mouth of SIDES) {
if (stepTarget(view, pit, mouth).kind !== "warp") continue;
const entry = stepTarget(view, pit, opposite(mouth));
if (entry.kind !== "step" || state.squareContents[cellKey(entry.to)]) continue;
const floor = SIDES.filter((d) => d !== mouth && d !== opposite(mouth) && stepTarget(view, pit, d).kind === "step");
if (floor.length === 0) continue;
return { pit, mouth, from: entry.to, floor, far: stepTarget(view, pit, mouth) as { kind: "warp"; to: Cell } };
}
}
throw new Error("no rim pit site on this board");
}
const stoneUp = (state: GameState, pit: Cell, sides: Side[], by: string) => {
for (const d of sides) state.squareContents[cellKey(neighbor(pit, d))] = { kind: "stone", damage: 0, createdBy: by };
};
for (const deckRev of [17, 16]) {
it(`walls either side, the warp mouth is the way off (rev ${deckRev})`, () => {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev });
const site = rimPitSite(state);
const me = activePlayer(state);
me.position = { ...site.from };
state.squareContents[cellKey(site.pit)] = { kind: "pit", damage: 0, createdBy: me.id };
stoneUp(state, site.pit, site.floor, me.id);
const r = applyCommand(state, me.id, { type: "move", direction: site.mouth });
expect(r.ok).toBe(true);
if (!r.ok) return;
const p = r.state.players.find((p) => p.id === me.id)!;
if (p.inPit) return;
expect(cellKey(p.position)).toBe(cellKey(site.far.to));
expect(r.events.some((e) => e.type === "jumpedPit" && e.via === "warp")).toBe(true);
});
}
it("a square beside the mouth: rev 17 asks which, rev 16 takes the square", () => {
for (const deckRev of [17, 16]) {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev });
const site = rimPitSite(state);
const me = activePlayer(state);
me.position = { ...site.from };
state.squareContents[cellKey(site.pit)] = { kind: "pit", damage: 0, createdBy: me.id };
stoneUp(state, site.pit, site.floor.slice(1), me.id);
const bare = applyCommand(state, me.id, { type: "move", direction: site.mouth });
if (deckRev >= 17) {
expect(bare.ok).toBe(false);
if (!bare.ok) expect(bare.error).toMatch(/click the square to land on/);
const named = applyCommand(state, me.id, { type: "move", direction: site.mouth, exit: site.mouth });
expect(named.ok).toBe(true);
if (named.ok) {
const p = named.state.players.find((p) => p.id === me.id)!;
expect(p.inPit || cellKey(p.position) === cellKey(site.far.to)).toBe(true);
}
} else {
expect(bare.ok).toBe(true);
if (bare.ok) {
const p = bare.state.players.find((p) => p.id === me.id)!;
expect(p.inPit || cellKey(p.position) === cellKey(neighbor(site.pit, site.floor[0]!))).toBe(true);
}
const mouth = applyCommand(state, me.id, { type: "move", direction: site.mouth, exit: site.mouth });
expect(mouth.ok).toBe(false);
}
}
});
});