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:
co-authored by
Claude Fable 5
parent
12ece6065a
commit
eabc10f18c
@@ -1008,6 +1008,42 @@ describe("interception, escape, and hazard sense", () => {
|
||||
expect(applyCommand(state, "bot", cmd!).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("cracks a safe over the prize instead of grabbing at the lid forever", () => {
|
||||
let { state } = createGame({ playerIds: ["foe", "bot"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toBot(state);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const chest = state.treasures.find((t) => t.owner === "foe" && t.position)!;
|
||||
state.squareContents[cellKey(chest.position!)] = { kind: "safe", damage: 0, createdBy: "foe" };
|
||||
bot.position = { ...chest.position! };
|
||||
bot.hand = [{ cardId: "dispel-creation", instanceId: "DC" }];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toMatchObject({ type: "cast", instanceId: "DC", target: { kind: "cell" } });
|
||||
const r = applyCommand(state, "bot", cmd!);
|
||||
expect(r.ok).toBe(true);
|
||||
// The box gone, the very next thought is the grab.
|
||||
const next = automatonCommand(viewFor(r.state, "bot"), "hunter", "archmage");
|
||||
expect(next).toMatchObject({ type: "pickUpTreasure" });
|
||||
});
|
||||
|
||||
it("never proposes the doomed grab on a safe it cannot open", () => {
|
||||
let { state } = createGame({ playerIds: ["foe", "bot"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toBot(state);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const chest = state.treasures.find((t) => t.owner === "foe" && t.position)!;
|
||||
state.squareContents[cellKey(chest.position!)] = { kind: "safe", damage: 0, createdBy: "foe" };
|
||||
bot.position = { ...chest.position! };
|
||||
bot.hand = [{ cardId: "number-3", instanceId: "N3" }];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
if (!cmd) break;
|
||||
expect(cmd.type).not.toBe("pickUpTreasure");
|
||||
const r = applyCommand(state, "bot", cmd);
|
||||
expect(r.ok).toBe(true);
|
||||
state = r.state;
|
||||
if (cmd.type === "endTurn") break;
|
||||
}
|
||||
});
|
||||
|
||||
it("a pressed carrier of any temperament turns to mist", () => {
|
||||
let { state } = createGame({ playerIds: ["foe", "bot"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toBot(state);
|
||||
|
||||
@@ -109,6 +109,47 @@ describe("terrain", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("safes and lock cards", () => {
|
||||
function safeRig() {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] });
|
||||
state = toRound2(state);
|
||||
const raider = activePlayer(state);
|
||||
const owner = state.players.find((p) => p.id !== raider.id)!;
|
||||
const chest = state.treasures.find((t) => t.owner === owner.id && t.position)!;
|
||||
state.squareContents[cellKey(chest.position!)] = { kind: "safe", damage: 0, createdBy: owner.id };
|
||||
raider.position = { ...chest.position! };
|
||||
return { state, raider: raider.id, chest };
|
||||
}
|
||||
|
||||
it("a pick lock aimed at the safe's square opens it until turn's end", () => {
|
||||
let { state, raider, chest } = safeRig();
|
||||
expect(applyCommand(state, raider, { type: "pickUpTreasure" }).ok).toBe(false);
|
||||
const pl = giveCard(state, raider, "pick-lock", "PL", 0);
|
||||
const here = state.players.find((p) => p.id === raider)!.position;
|
||||
state = must(state, raider, {
|
||||
type: "cast", instanceId: pl.instanceId, target: { kind: "cell", cell: { ...here } },
|
||||
});
|
||||
expect(state.openSafes).toContain(cellKey(here));
|
||||
state = must(state, raider, { type: "pickUpTreasure" });
|
||||
expect(state.treasures.find((t) => t.id === chest.id)!.carriedBy).toBe(raider);
|
||||
state = must(state, raider, { type: "endTurn", draw: 0 });
|
||||
expect(state.openSafes).toEqual([]);
|
||||
});
|
||||
|
||||
it("the key must be at or beside the safe", () => {
|
||||
let { state, raider } = safeRig();
|
||||
const p = state.players.find((q) => q.id === raider)!;
|
||||
const box = { ...p.position };
|
||||
p.position = { x: (box.x + 3) % 10, y: (box.y + 3) % 10 };
|
||||
const pl = giveCard(state, raider, "pick-lock", "PL", 0);
|
||||
const r = applyCommand(state, raider, {
|
||||
type: "cast", instanceId: pl.instanceId, target: { kind: "cell", cell: box },
|
||||
});
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.error).toMatch(/beside the safe/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("objects", () => {
|
||||
it("a thrown dagger does physical damage full shield cannot stop, then lies on the floor", () => {
|
||||
let { state } = newGame();
|
||||
|
||||
Reference in New Issue
Block a user