From ac53773fe4a1bbcaf58979281a187aec1918b25d Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 19:31:57 -0400 Subject: [PATCH] The dimming aid stops lying about boobytrap, glue, and safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three cards sat wrongly in the creation bucket of eligibleCellsFor, so the targeting dim demanded sighted empty squares the engine never asks for. Boobytrap tokens go anywhere on the board but solid stone — no sight required; a trap you can see coming is a poor trap. Glue and the safe were worse than restricted: inverted. Both seize a square that HOLDS a loose object or treasure, exactly the squares the empty- square rule dimmed out. Each now has its own branch mirroring the engine's actual checks, pinned by tests. Co-Authored-By: Claude Fable 5 --- packages/engine/src/view.ts | 25 ++++++++++++++- .../engine/test/expansion-terrain.test.ts | 32 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/view.ts b/packages/engine/src/view.ts index 2aafee8..fc9c648 100644 --- a/packages/engine/src/view.ts +++ b/packages/engine/src/view.ts @@ -185,7 +185,7 @@ export function sightedCellsFor(view: GameView): Set { const CREATION_CARD_IDS = new Set([ "fill-square-with-stone", "thornbush", "killer-ooze", "rosebush", "dust-cloud", - "fill-square-with-slime", "create-pit", "handful-of-tacks", "glue", "safe", "boobytrap", + "fill-square-with-slime", "create-pit", "handful-of-tacks", ]); const SUMMON_CARD_IDS = new Set([ "troll", "skeleton", "wraith", "fire-imp", "democratic-monster", "shadow", @@ -256,6 +256,29 @@ export function eligibleCellsFor(view: GameView, cardId: string): Set | return out; } + if (cardId === "boobytrap") { + // Tokens go anywhere on the board except solid stone — no sight needed; + // a trap you can see coming is a poor trap. + return new Set(cells.filter((k) => view.squareContents[k]?.kind !== "stone")); + } + + if (cardId === "glue" || cardId === "safe") { + // Both seize a square that HOLDS something — a loose object or an + // unclaimed treasure — within the caster's sight. The safe additionally + // needs the square clear of terrain to build around its prize. + const out = new Set(); + for (const k of cells) { + if (cardId === "safe" && view.squareContents[k]) continue; + const hasObject = + (view.groundObjects[k] ?? []).length > 0 || + view.treasures.some((t) => t.position && !t.carriedBy && key(t.position.x, t.position.y) === k); + if (!hasObject) continue; + if (!sighted.has(k)) continue; + out.add(k); + } + return out; + } + if (cardId === "stone-to-water") { return new Set(cells.filter((k) => view.squareContents[k]?.kind === "stone")); } diff --git a/packages/engine/test/expansion-terrain.test.ts b/packages/engine/test/expansion-terrain.test.ts index a6c4ad0..7ad7ba4 100644 --- a/packages/engine/test/expansion-terrain.test.ts +++ b/packages/engine/test/expansion-terrain.test.ts @@ -161,3 +161,35 @@ describe("expansion terrain", () => { // Wave side effects vary by geometry; the melt itself is the pinned behavior. }); }); + +describe("eligibility dimming mirrors the engine", () => { + it("boobytrap tokens go anywhere but solid stone, sight be damned", async () => { + const { viewFor, eligibleCellsFor } = await import("../src/view"); + let { state } = newGame(); + const caster = activePlayer(state).id; + const view = viewFor(state, caster); + const lit = eligibleCellsFor(view, "boobytrap")!; + // Every board square is fair game (the fresh maze holds no solid stone), + // including squares far outside the caster's sight. + expect(lit.size).toBe(Object.keys(view.board.cells).length); + }); + + it("glue lights only sighted squares that hold something", async () => { + const { viewFor, eligibleCellsFor } = await import("../src/view"); + let { state } = newGame(); + const caster = activePlayer(state); + // Drop a dagger at the caster's feet — the one guaranteed-sighted object. + const here = { ...caster.position }; + state.groundObjects[cellKey(here)] = [{ instanceId: "dagger#G", cardId: "dagger" }]; + const lit = eligibleCellsFor(viewFor(state, caster.id), "glue")!; + expect(lit.has(cellKey(here))).toBe(true); + // Empty squares stay dim — glue needs something to glue down. + for (const k of lit) { + const view = viewFor(state, caster.id); + const held = + (view.groundObjects[k] ?? []).length > 0 || + view.treasures.some((t) => t.position && cellKey(t.position) === k); + expect(held).toBe(true); + } + }); +});