Two idle-bot loops cured; visionstone reaches the walls

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-28 12:58:57 -04:00
co-authored by Claude Fable 5
parent b99c22f14a
commit 7585a5e88d
2 changed files with 24 additions and 6 deletions
+10 -2
View File
@@ -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 };
+14 -4
View File
@@ -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<string, AttackEffect | NeutralEffect | CounterEffect>
}
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<string, AttackEffect | NeutralEffect | CounterEffect>
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<Command, { type: "cast" }>): 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 =