From 7df58d29fa1aaeb86d30f6fc177ab615b5cc2f5b Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Mon, 17 Aug 2026 17:47:14 -0400 Subject: [PATCH] =?UTF-8?q?Rev=2021:=20collapsing=20walls=20crush=20anyone?= =?UTF-8?q?=20=E2=80=94=20monsters=20included?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESTROY WALL's "anyone in either square next to the wall takes 4 points" swept only wizards; a troll leaned on the falling masonry untouched. The expansion sheet is explicit — "if a card specifies 'Opponent' or 'Anyone' as a target, this includes Monsters" — so the collapse now crushes creatures in both squares, at the rim breach's far side too. Earlier ledgers keep their charmed monsters. Co-Authored-By: Claude Fable 5 --- packages/engine/src/game.ts | 16 ++++++++++ packages/engine/test/creatures.test.ts | 43 ++++++++++++++++++++++++++ packages/server/src/rooms.ts | 2 +- packages/web/src/local.svelte.ts | 2 +- 4 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index 27c8498..16910df 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -1108,6 +1108,13 @@ const CARD_EFFECTS: Record applyDamage(state, events, p, 4, "collapsing wall", caster.id, "physical"); } } + if ((state.config.deckRev ?? 1) >= 21) { + for (const cr of [...state.creatures]) { + if (cellKey(cr.position) === cellKey(far.cell)) { + damageCreature(state, events, cr, 4, "collapsing wall"); + } + } + } state.board.warps.push( { from: { cell, side }, to: { cell: far.cell, side: far.side } }, { from: { cell: far.cell, side: far.side }, to: { cell, side } }, @@ -1121,6 +1128,15 @@ const CARD_EFFECTS: Record applyDamage(state, events, p, 4, "collapsing wall", caster.id, "physical"); } } + // "Anyone in either square" includes monsters — the expansion sheet + // says so in as many words (rules rev 21). + if ((state.config.deckRev ?? 1) >= 21) { + for (const cr of [...state.creatures]) { + if (cellKey(cr.position) === cellKey(c)) { + damageCreature(state, events, cr, 4, "collapsing wall"); + } + } + } } checkVictory(state, events); return null; diff --git a/packages/engine/test/creatures.test.ts b/packages/engine/test/creatures.test.ts index 0d25822..3022954 100644 --- a/packages/engine/test/creatures.test.ts +++ b/packages/engine/test/creatures.test.ts @@ -640,3 +640,46 @@ describe("a mid-round democratic monster claws on the next turn (rules rev 18)", expect(r.state.stack?.creatureId ?? null).toBe(fresh.id); }); }); + +describe("collapsing walls crush monsters too (rules rev 21)", () => { + function wallRig(deckRev: number) { + let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev }); + state = toRound2(state); + const view = boardView(state); + for (const [key, edge] of Object.entries(view.edges)) { + if (edge !== "wall") continue; + const [kind, coords] = key.split(":") as [string, string]; + const [x, y] = coords.split(",").map(Number) as [number, number]; + const side = kind === "V" ? ("E" as Side) : ("S" as Side); + const other = { x: x + (side === "E" ? 1 : 0), y: y + (side === "S" ? 1 : 0) }; + if (!view.cells[cellKey(other)]) continue; + const caster = activePlayer(state); + caster.position = { x, y }; + state.creatures.push({ + id: "tr1", kind: "troll", controllerId: caster.id, + position: { ...other }, damage: 0, maxDamage: 6, movesPerTurn: 3, + movementUsed: 0, attackUsed: false, justCreated: false, + wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], + }); + const dw = giveCard(state, caster.id, "destroy-wall"); + const r = applyCommand(state, caster.id, { + type: "cast", instanceId: dw.instanceId, target: { kind: "edge", cell: { x, y }, side }, + }); + if (!r.ok) throw new Error(r.error); + return r.state; + } + throw new Error("setup: no interior wall found"); + } + + it("rev 21: the adjacent troll takes the four points", () => { + const state = wallRig(21); + const troll = state.creatures.find((c) => c.id === "tr1"); + // Four points on a six-point troll: hurt but standing (or regenerating). + expect(troll?.damage).toBe(4); + }); + + it("earlier revisions leave the troll dusty but unharmed", () => { + const state = wallRig(20); + expect(state.creatures.find((c) => c.id === "tr1")!.damage).toBe(0); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 978e644..5b56c1e 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 = 20; +const RULES_REV = 21; const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index a67bae4..3faf6dd 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: 20, + deckRev: 21, }; const { state, events } = createGame(config); for (const e of events) {