diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 1e24b12..7677bb8 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -3335,6 +3335,17 @@ function clone(state: GameState): GameState { return structuredClone(state); } +/** Everything an eliminated wizard leaves behind goes with them. + * "If you die, any monster controlled by you immediately disappears." */ +function sweepEliminated(state: GameState, playerId: PlayerId): void { + state.sustained = state.sustained.filter((s) => s.targetId !== playerId && s.casterId !== playerId); + state.creatures = state.creatures.filter((c) => c.controllerId !== playerId); + for (const a of state.ambushes.filter((a) => a.ownerId === playerId)) { + state.discard.push(a.via, a.spell, ...a.numbers); + } + state.ambushes = state.ambushes.filter((a) => a.ownerId !== playerId); +} + function requireActionsAvailable(state: GameState): string | null { if (state.turn.actionsEnded) return "your turn's actions ended when you picked up an object"; return null; @@ -5094,13 +5105,7 @@ function applyDamage( target.finalHand = [...target.hand]; events.push({ type: "died", player: target.id, killedBy: attackerId }); events.push({ type: "playerEliminated", player: target.id, reason: "killed" }); - state.sustained = state.sustained.filter((s) => s.targetId !== target.id && s.casterId !== target.id); - // "If you die, any monster controlled by you immediately disappears." - state.creatures = state.creatures.filter((c) => c.controllerId !== target.id); - for (const a of state.ambushes.filter((a) => a.ownerId === target.id)) { - state.discard.push(a.via, a.spell, ...a.numbers); - } - state.ambushes = state.ambushes.filter((a) => a.ownerId !== target.id); + sweepEliminated(state, target.id); if (target.carriedTreasureId) { const t = state.treasures.find((t) => t.id === target.carriedTreasureId)!; @@ -5281,6 +5286,7 @@ 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" }); + if ((state.config.deckRev ?? 1) >= 14) sweepEliminated(state, p.id); } } diff --git a/packages/engine/test/creatures.test.ts b/packages/engine/test/creatures.test.ts index 257d43e..a058bd4 100644 --- a/packages/engine/test/creatures.test.ts +++ b/packages/engine/test/creatures.test.ts @@ -494,3 +494,35 @@ describe("monsters roll to hit the hidden (rules rev 13)", () => { expect(JSON.stringify(state.rng)).toBe(rngBefore); }); }); + +describe("elimination sweeps the board either way (rules rev 14)", () => { + it("a treasure-eliminated wizard's fire imp vanishes with them", () => { + let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"], deckRev: 14 }); + const victim = state.players.find((p) => p.id === "c")!; + state.creatures.push({ + id: "imp1", kind: "fire-imp", controllerId: "c", + position: { ...victim.position }, + damage: 0, maxDamage: 5, movesPerTurn: 0, movementUsed: 0, + attackUsed: false, justCreated: false, + wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], + }); + state.sustained.push({ + id: "fx-med", cardId: "medusa", casterId: "a", targetId: "c", + remainingTurns: 3, data: {}, + }); + // Both of c's treasures sit captured on living enemies' home bases. + const mine = state.treasures.filter((t) => t.owner === "c"); + expect(mine.length).toBe(2); + mine[0]!.position = { ...state.players.find((p) => p.id === "a")!.home }; + mine[0]!.carriedBy = null; + mine[1]!.position = { ...state.players.find((p) => p.id === "b")!.home }; + mine[1]!.carriedBy = null; + const active = activePlayer(state).id; + const r = applyCommand(state, active, { type: "endTurn", draw: 0 }); + if (!r.ok) throw new Error(r.error); + state = r.state; + expect(state.players.find((p) => p.id === "c")!.alive).toBe(false); + expect(state.creatures.some((c) => c.controllerId === "c")).toBe(false); + expect(state.sustained.some((s) => s.targetId === "c")).toBe(false); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 9e07b5c..0ffddf8 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 = 13; +const RULES_REV = 14; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index f538b7a..1e7c28e 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -1367,7 +1367,7 @@ peekCard = { instanceId: `peek-${e.id}`, cardId: e.cardId }; peekCreatureId = null; }}> - ✦ {spellName(e.cardId)} · {e.remainingTurns} + ✦ {spellName(e.cardId)}{e.remainingTurns < 9000 ? ` · ${e.remainingTurns}` : ""} {/each} diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index 134090c..1567c09 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -134,7 +134,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), - deckRev: 13, + deckRev: 14, }; const { state, events } = createGame(config); for (const e of events) {