'It will relock behind you' means at your back (rules rev 4)

Both PICK LOCK and MASTER KEY promise the door relocks behind the
walker — not that it stands unlocked for the turn. Passing through
any door unlocked this turn now shuts it at once at the walker's
back, unless a hand holds it open; the pick-and-peek stays (an
unpassed door keeps its opening until turn's end), and holding via a
targeted cast works as ever, displayed key included. The displayed
key's own walk-through likewise leaves no lingering opening for
bystanders — their peek needs a held door. Frozen behind rev 4 so
stored games keep their turn-long openings. 18 ledgers verified; 277
tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-25 20:53:01 -04:00
co-authored by Claude Fable 5
parent 64e4e243dc
commit beb4947ad8
2 changed files with 27 additions and 12 deletions
+17 -4
View File
@@ -226,7 +226,7 @@ export interface CastParams {
} }
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */ /** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
export const CURRENT_RULES_REV = 3; export const CURRENT_RULES_REV = 4;
export interface GameConfig { export interface GameConfig {
playerIds: PlayerId[]; playerIds: PlayerId[];
@@ -245,6 +245,9 @@ export interface GameConfig {
* Rev 3: VISIONSTONE pierces its one wall for every sight the game * Rev 3: VISIONSTONE pierces its one wall for every sight the game
* asks of its bearer creations and utility spells included not * asks of its bearer creations and utility spells included not
* only direct attacks. * only direct attacks.
* Rev 4: "the door will relock behind you" means it passing through
* an unlocked door shuts it at the walker's back unless a hand holds
* it; unpassed, it relocks at turn's end as before.
*/ */
deckRev?: number; deckRev?: number;
} }
@@ -3950,14 +3953,24 @@ function doMove(prev: GameState, direction: Side, over = false): CommandResult {
} else if (edge === "door" && } else if (edge === "door" &&
(doorIsOpen(state, key) || (displays(p, "master-key") && state.doorStates[key] !== "jammed"))) { (doorIsOpen(state, key) || (displays(p, "master-key") && state.doorStates[key] !== "jammed"))) {
// The displayed MASTER KEY turns in every lock it meets: the walker // The displayed MASTER KEY turns in every lock it meets: the walker
// passes without another cast, and the door relocks behind them at // passes without another cast, and the door RELOCKS BEHIND THEM at
// turn's end like any picked lock. A JAMmed LOCK still refuses it. // once — "door relocks behind you" grants no lingering opening, so
// bystanders see only a shut door (the bearer's own threshold peek
// rides the key itself). A JAMmed LOCK still refuses it. To hold a
// door open, cast the key at it as ever.
if (!doorIsOpen(state, key)) { if (!doorIsOpen(state, key)) {
state.openDoorEdges.push(key);
events.push({ type: "doorUnlocked", player: p.id, edge: parseEdgeKey(key), withCardId: "master-key" }); events.push({ type: "doorUnlocked", player: p.id, edge: parseEdgeKey(key), withCardId: "master-key" });
} }
p.position = dest; p.position = dest;
via = "step"; via = "step";
// "The door will relock behind you": stepping through an unlocked
// door shuts it at the walker's back unless a hand holds it open.
// (Rev 4; earlier games kept their turn-long openings.)
if ((state.config.deckRev ?? 1) >= 4 &&
state.openDoorEdges.includes(key) && !state.heldDoors.some((h) => h.key === key)) {
state.openDoorEdges = state.openDoorEdges.filter((k) => k !== key);
events.push({ type: "doorsRelocked", count: 1 });
}
} else if (edge === "firewall") { } else if (edge === "firewall") {
// "Passing through it does four points of magical damage." Firewalls // "Passing through it does four points of magical damage." Firewalls
// burn even a MIST-BODY. // burn even a MIST-BODY.
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { applyCommand, activePlayer, boardView, createGame, sustainedOn, type GameState } from "../src/game"; import { applyCommand, activePlayer, boardView, createGame, sustainedOn, type GameState } from "../src/game";
import { cellKey, edgeKey, neighbor, type Side } from "../src/board"; import { cellKey, edgeKey, neighbor, opposite, type Side } from "../src/board";
import type { CardInstance } from "../src/cards"; import type { CardInstance } from "../src/cards";
import { newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers"; import { newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers";
@@ -111,7 +111,7 @@ describe("doors", () => {
throw new Error("no door on this board"); throw new Error("no door on this board");
} }
it("pick lock opens an adjacent door until end of turn", () => { it("pick lock opens an adjacent door — and it relocks behind the walker", () => {
let { state } = newGame(); let { state } = newGame();
const me = activePlayer(state); const me = activePlayer(state);
const door = findDoor(state); const door = findDoor(state);
@@ -130,9 +130,8 @@ describe("doors", () => {
state = must(state, me.id, { type: "move", direction: dir }); state = must(state, me.id, { type: "move", direction: dir });
expect(cellKey(activePlayer(state).position)).toBe(cellKey(other)); expect(cellKey(activePlayer(state).position)).toBe(cellKey(other));
// Relocks when the turn ends. // "It will relock behind you": shut at the walker's back at once —
state = must(state, me.id, { type: "endTurn", draw: 0 }); // the way back is barred without another pick.
state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 });
expect(state.openDoorEdges.length).toBe(0); expect(state.openDoorEdges.length).toBe(0);
expect(applyCommand(state, me.id, { type: "move", direction: dir === "E" ? "W" : "N" }).ok).toBe(false); expect(applyCommand(state, me.id, { type: "move", direction: dir === "E" ? "W" : "N" }).ok).toBe(false);
}); });
@@ -539,7 +538,7 @@ describe("the displayed MASTER KEY", () => {
} }
}); });
it("its bearer walks through locked doors, which relock behind them", () => { it("its bearer walks through locked doors, which relock at their back", () => {
const { state, cell, side, key, keeper } = keyRig(); const { state, cell, side, key, keeper } = keyRig();
const mk = giveCard(state, keeper.id, "master-key", "MK"); const mk = giveCard(state, keeper.id, "master-key", "MK");
keeper.displayed.push(mk.instanceId); keeper.displayed.push(mk.instanceId);
@@ -548,9 +547,12 @@ describe("the displayed MASTER KEY", () => {
if (r.ok) { if (r.ok) {
const after = r.state.players.find((p) => p.id === keeper.id)!; const after = r.state.players.find((p) => p.id === keeper.id)!;
expect(cellKey(after.position)).toBe(cellKey(neighbor(cell, side))); expect(cellKey(after.position)).toBe(cellKey(neighbor(cell, side)));
// Unlocked for the turn — the relock rides the usual end-of-turn sweep. // "Door relocks behind you": no lingering opening for bystanders.
expect(r.state.openDoorEdges).toContain(key); expect(r.state.openDoorEdges).not.toContain(key);
expect(r.events.some((e) => e.type === "doorUnlocked")).toBe(true); expect(r.events.some((e) => e.type === "doorUnlocked")).toBe(true);
// The key turns again on the way back.
const back = applyCommand(r.state, keeper.id, { type: "move", direction: opposite(side) });
expect(back.ok).toBe(true);
} }
}); });