From 7585a5e88d5d52123c82240788771c922d40a601 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Fri, 28 Aug 2026 12:58:57 -0400 Subject: [PATCH] Two idle-bot loops cured; visionstone reaches the walls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Game eleven's silent turns, diagnosed from the ledger. Automaton III proposed DESTROY WALL at a wall its visionstone could see — but the engine's edge-target casts never honored the stone, despite the declared policy that it is the bearer's sight wherever sight is asked. castSightEdge now dissolves one wall for the look, and the three raw losToEdge gates (create-wall, destroy-wall, attacks on walls) route through it, gaining visionstone and bent-corner sight alike. Automaton II cast REMOVE CURSE with no patient and no ailment named; the rung now writes the full prescription. Refusal-fallback loops read as idle bots — both replays march again. Ledgers verified. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF --- packages/engine/src/automaton.ts | 12 ++++++++++-- packages/engine/src/game.ts | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts index 6020729..860a242 100644 --- a/packages/engine/src/automaton.ts +++ b/packages/engine/src/automaton.ts @@ -1071,11 +1071,19 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm } if (!tier.buffs) return null; // the apprentice's book ends at the stones // A curse on the clockwork gets scrubbed off. - const cursed = view.sustained.some( + // The engine wants the whole prescription — patient and ailment both — + // or the cast is refused and the fallback reads as an idle bot. + const myCurse = view.sustained.find( (e) => e.targetId === view.you && e.casterId !== view.you && AFFLICTIONS[e.cardId] != null, ); const cleanse = inHand(view, "remove-curse"); - if (cursed && cleanse) return { type: "cast", instanceId: cleanse.instanceId }; + if (myCurse && cleanse) { + return { + type: "cast", instanceId: cleanse.instanceId, + target: { kind: "player", playerId: view.you }, + params: { cardId: myCurse.cardId }, + }; + } // Three or more wizards: LIFESAVER takes elimination off the table. const lifesaver = inHand(view, "lifesaver"); if (lifesaver && view.players.length > 2) return { type: "cast", instanceId: lifesaver.instanceId }; diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index d06e543..177a755 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -521,7 +521,9 @@ function castSight( return !!cmd.aroundCornerInstanceId && bentLos(state, caster, caster.position, to); } -/** Sight for an edge-aimed cast (walls, doors), bend-aware like castSight. */ +/** Sight for an edge-aimed cast (walls, doors), bend-aware like castSight. + * VISIONSTONE is the bearer's sight here too — one wall or door, any one, + * falls away for the look, exactly as it does for square-aimed casts. */ function castSightEdge( state: GameState, caster: PlayerState, @@ -531,6 +533,14 @@ function castSightEdge( side: Side, ): boolean { if (losToEdge(board, caster.position, cell, side)) return true; + if (displays(caster, "visionstone")) { + for (const [key, edge] of Object.entries(board.edges)) { + if (edge === "open") continue; + const edges = { ...board.edges }; + delete edges[key]; + if (losToEdge({ ...board, edges }, caster.position, cell, side)) return true; + } + } if (!cmd.aroundCornerInstanceId) return false; for (const key of Object.keys(board.cells)) { const [mx, my] = key.split(",").map(Number) as [number, number]; @@ -1211,7 +1221,7 @@ const CARD_EFFECTS: Record } const key = edgeKey(cell, side); if ((view.edges[key] ?? "open") !== "open") return "there is already something in that wall line"; - if (!losToEdge(view, caster.position, cell, side)) return "no line of sight to the wall line"; + if (!castSightEdge(state, caster, cmd, view, cell, side)) return "no line of sight to the wall line"; state.edgeOverrides[key] = "wall"; state.createdEdges[key] = true; events.push({ type: "wallCreated", caster: caster.id, edge: { cell, side } }); @@ -1235,7 +1245,7 @@ const CARD_EFFECTS: Record const key = edgeKey(cell, side); const current = view.edges[key] ?? "open"; if (current === "open") return "there is no wall there"; - if (!losToEdge(view, caster.position, cell, side)) return "no line of sight to the wall"; + if (!castSightEdge(state, caster, cmd, view, cell, side)) return "no line of sight to the wall"; state.edgeOverrides[key] = "open"; delete state.doorStates[key]; delete state.createdEdges[key]; @@ -5122,7 +5132,7 @@ function doCast(prev: GameState, cmd: Extract): Comma if (effect.sameSquare && !touchesEdge(caster.position, cell, side)) { return err("you must stand beside the wall"); } - if (effect.requiresLos && !losToEdge(view, caster.position, cell, side)) { + if (effect.requiresLos && !castSightEdge(state, caster, cmd, view, cell, side)) { return err("no line of sight to the wall"); } const dmg =