diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index ffa96d8..db801e2 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -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); } } diff --git a/packages/engine/test/game.test.ts b/packages/engine/test/game.test.ts index cff2420..5968296 100644 --- a/packages/engine/test/game.test.ts +++ b/packages/engine/test/game.test.ts @@ -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); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 86d274d..9a65c24 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -54,7 +54,7 @@ export interface Room { const rooms = new Map(); /** 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"; diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index ab15745..a4b5ebf 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -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) {