The fallen drop what they carry, however they fall (rev 30)

A wizard eliminated by treasuresLost took any carried treasure into the
void with them — the killed path drops it, but checkVictory's branch
never did. At rev 30 the treasure lands where they stood, with the
usual treasureDropped event (onHomeOf included, so a capture completed
by the falling is honored in the same breath). Stored games replay the
vanishing unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-19 12:25:41 -04:00
co-authored by Claude Fable 5
parent f1743e6fe5
commit 62c4680d1f
4 changed files with 56 additions and 2 deletions
+16
View File
@@ -5821,6 +5821,22 @@ function checkVictory(state: GameState, events: GameEvent[]): void {
state.discard.push(...p.hand.splice(0));
p.displayed = [];
events.push({ type: "playerEliminated", player: p.id, reason: "treasuresLost" });
// A treasure in the fallen wizard's arms lands where they stood, as
// it does when a wizard is killed (rules rev 30; earlier games let
// it vanish with them and replay so).
if ((state.config.deckRev ?? 1) >= 30 && p.carriedTreasureId) {
const t = state.treasures.find((t) => t.id === p.carriedTreasureId)!;
t.carriedBy = null;
t.position = p.position;
p.carriedTreasureId = null;
events.push({
type: "treasureDropped",
player: p.id,
treasureId: t.id,
at: p.position,
onHomeOf: homeOwnerAt(state, p.position),
});
}
if ((state.config.deckRev ?? 1) >= 14) sweepEliminated(state, p.id);
}
}
+38
View File
@@ -224,3 +224,41 @@ describe("treasures and victory", () => {
expect(result.ok).toBe(false);
});
});
describe("elimination by lost treasures drops what the fallen carried (rev 30)", () => {
it("the carried treasure lands where the wizard stood", () => {
let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic"], deckRev: 30 });
const A = state.players.find((p) => p.id === "a")!;
const B = state.players.find((p) => p.id === "b")!;
const C = state.players.find((p) => p.id === "c")!;
// C's first treasure already sits captured on B's home...
const c1 = state.treasures.filter((t) => t.owner === "c")[0]!;
c1.position = { ...B.home };
// ...C carries one of B's treasures...
const bt = state.treasures.filter((t) => t.owner === "b")[0]!;
bt.position = null;
bt.carriedBy = "c";
C.carriedTreasureId = bt.id;
// ...and A, standing on their own home, drops C's second treasure there.
const c2 = state.treasures.filter((t) => t.owner === "c")[1]!;
c2.position = null;
c2.carriedBy = "a";
A.carriedTreasureId = c2.id;
A.position = { ...A.home };
while (state.players[state.turn.activeIndex]!.id !== "a") {
const r = applyCommand(state, state.players[state.turn.activeIndex]!.id, { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
const r = applyCommand(state, "a", { type: "dropTreasure" });
if (!r.ok) throw new Error(r.error);
state = r.state;
// C is eliminated by lost treasures — and B's treasure lies where C stood.
const cAfter = state.players.find((p) => p.id === "c")!;
expect(cAfter.alive).toBe(false);
expect(cAfter.carriedTreasureId).toBeNull();
const btAfter = state.treasures.find((t) => t.id === bt.id)!;
expect(btAfter.carriedBy).toBeNull();
expect(btAfter.position).toEqual(cAfter.position);
});
});
+1 -1
View File
@@ -54,7 +54,7 @@ export interface Room {
const rooms = new Map<string, Room>();
/** Rules revision new games are dealt under (stored games keep their own). */
const RULES_REV = 29;
const RULES_REV = 30;
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 29,
deckRev: 30,
};
const { state, events } = createGame(config);
for (const e of events) {