The MASTER KEY earns its display

"Display Immediately," says the card — now the key lives up to it.
Cast bare (a new Display button beside the hold-the-door checkbox),
it goes straight on display; once displayed, it turns in every lock
its bearer walks through — no more casting at each door — with the
door relocking behind them at turn's end like any picked lock, and
the doorUnlocked event still swinging the door in the reels. Cast at
an adjacent door it still works one lock like PICK LOCK, including
holding the door open for others. A JAMmed LOCK refuses it as ever,
and the adjacent peek through workable locks already rode the rev-2
sight rules. Replay-safe ungated: bare casts were never recorded
(they were refused), displayed cards still count against the hand
limit, and walking through a locked door only widens what is
accepted.

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:46:23 -04:00
co-authored by Claude Fable 5
parent 805fb09782
commit 64e4e243dc
3 changed files with 81 additions and 3 deletions
@@ -506,3 +506,60 @@ describe("a held door is an open doorway to the eye", () => {
if (!refused.ok) expect(refused.error).toContain("line of sight");
});
});
describe("the displayed MASTER KEY", () => {
function keyRig() {
let { state } = createGame({ playerIds: ["keeper", "watcher"], seed: 42, sets: ["basic"] });
state = toRound2(state);
const view = boardView(state);
for (const [key, edge] of Object.entries(view.edges)) {
if (edge !== "door") continue;
const [kind, coords] = key.split(":") as [string, string];
const [x, y] = coords.split(",").map(Number) as [number, number];
const cell = { x, y };
const side = kind === "V" ? ("E" as Side) : ("S" as Side);
const keeper = activePlayer(state);
keeper.position = { ...cell };
return { state, cell, side, key, keeper };
}
throw new Error("setup: seed 42 grew a maze with no doors");
}
it("cast bare, the key goes on display — once", () => {
const { state, keeper } = keyRig();
const mk = giveCard(state, keeper.id, "master-key", "MK");
const cast = applyCommand(state, keeper.id, { type: "cast", instanceId: mk.instanceId });
expect(cast.ok).toBe(true);
if (cast.ok) {
const after = cast.state.players.find((p) => p.id === keeper.id)!;
expect(after.displayed).toContain(mk.instanceId);
expect(after.hand.some((c) => c.instanceId === mk.instanceId)).toBe(true);
const again = applyCommand(cast.state, keeper.id, { type: "cast", instanceId: mk.instanceId });
expect(again.ok).toBe(false);
}
});
it("its bearer walks through locked doors, which relock behind them", () => {
const { state, cell, side, key, keeper } = keyRig();
const mk = giveCard(state, keeper.id, "master-key", "MK");
keeper.displayed.push(mk.instanceId);
const r = applyCommand(state, keeper.id, { type: "move", direction: side });
expect(r.ok).toBe(true);
if (r.ok) {
const after = r.state.players.find((p) => p.id === keeper.id)!;
expect(cellKey(after.position)).toBe(cellKey(neighbor(cell, side)));
// Unlocked for the turn — the relock rides the usual end-of-turn sweep.
expect(r.state.openDoorEdges).toContain(key);
expect(r.events.some((e) => e.type === "doorUnlocked")).toBe(true);
}
});
it("a JAMmed LOCK refuses even the master key", () => {
const { state, side, key, keeper } = keyRig();
const mk = giveCard(state, keeper.id, "master-key", "MK");
keeper.displayed.push(mk.instanceId);
state.doorStates[key] = "jammed";
const r = applyCommand(state, keeper.id, { type: "move", direction: side });
expect(r.ok).toBe(false);
});
});