Safes finally open to keys — and the clockwork stops clawing at the lid

'All LOCK-type cards will work on it' was never wired: openSafes was
reset every turn and filled by nothing. A PICK LOCK or MASTER KEY
aimed at a safe's square (underfoot or beside) now opens it until
turn's end, with its own chronicle line; the dead never-emitted
safeOpened variant and its null humanize go. The automaton — H4EN's
Automaton II spent six turns grabbing at a locked lid saying 'I meant
to do that' — now cracks the box first (key or DISPEL CREATION, which
it held the whole time) and drops unopenable safes from its goals
rather than marching to one forever.

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-09-01 09:22:56 -04:00
co-authored by Claude Fable 5
parent 12ece6065a
commit eabc10f18c
6 changed files with 119 additions and 3 deletions
+12 -1
View File
@@ -703,7 +703,6 @@ export type GameEvent =
| { type: "boobytrapPlacedPrivate"; visibleTo: PlayerId; realCell: Cell }
| { type: "objectsGlued"; caster: PlayerId; at: Cell; turns: number }
| { type: "safeCreated"; caster: PlayerId; at: Cell }
| { type: "safeOpened"; player: PlayerId; at: Cell }
| { type: "itemsTraded"; caster: PlayerId; a: Cell; b: Cell }
| { type: "stoneTurnedToWater"; caster: PlayerId; at: Cell | null; edge?: { cell: Cell; side: Side } }
| { type: "handsSwapped"; a: PlayerId; b: PlayerId }
@@ -745,6 +744,7 @@ export type GameEvent =
| { type: "extraTurnGranted"; player: PlayerId }
| { type: "lifeTraded"; player: PlayerId; points: number; newAllowance: number }
| { type: "madDash"; player: PlayerId; newAllowance: number }
| { type: "safeOpened"; player: PlayerId; cell: Cell; withCardId: string }
| { type: "trapSprung"; player: PlayerId; cardId?: string }
| { type: "died"; player: PlayerId; killedBy: PlayerId | null }
| { type: "handTaken"; from: PlayerId; to: PlayerId; count: number }
@@ -3588,6 +3588,17 @@ function unlockDoor(
cardId: string,
opts: { requireAdjacent: boolean },
): string | null {
// "All LOCK-type cards will work on it" — aimed at a SAFE's square
// (underfoot or beside), the key opens it until the turn ends.
const cell = cmd.target?.kind === "cell" ? cmd.target.cell : null;
if (cell && state.squareContents[cellKey(cell)]?.kind === "safe") {
const d = Math.abs(caster.position.x - cell.x) + Math.abs(caster.position.y - cell.y);
if (d > 1) return "you must be at or beside the safe";
const key = cellKey(cell);
if (!state.openSafes.includes(key)) state.openSafes.push(key);
events.push({ type: "safeOpened", player: caster.id, cell: { ...cell }, withCardId: cardId });
return null;
}
const found = doorTarget(state, cmd);
if (typeof found === "string") return found;
const key = edgeKey(found.cell, found.side);