The automaton guards gold only within sight (X2XN stall)

The guard-the-gold rung cast SAFE/GLUE at a floor treasure with no
line-of-sight check; the engine refused the cast and the fallback
burned the bot's whole turn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-20 21:21:00 -04:00
co-authored by Claude Fable 5
parent db0eeebcd5
commit e30f3f0903
2 changed files with 58 additions and 3 deletions
+4 -2
View File
@@ -1034,9 +1034,11 @@ function selfCare(view: GameView, style: AutomatonStyle, tier: TierTraits): Comm
const myFloorTreasure = tier.guardGold const myFloorTreasure = tier.guardGold
? view.treasures.find((t) => t.owner === view.you && t.position && !t.carriedBy) ? view.treasures.find((t) => t.owner === view.you && t.position && !t.carriedBy)
: undefined; : undefined;
if (myFloorTreasure && enemyNear) { // Both spells demand line of sight to the gold; a blind cast would be
// refused and cost the whole turn.
if (myFloorTreasure && enemyNear && sightedCellsFor(view).has(cellKey(myFloorTreasure.position!))) {
const safe = inHand(view, "safe"); const safe = inHand(view, "safe");
if (safe) { if (safe && !view.squareContents[cellKey(myFloorTreasure.position!)]) {
return { type: "cast", instanceId: safe.instanceId, target: { kind: "cell", cell: myFloorTreasure.position! } }; return { type: "cast", instanceId: safe.instanceId, target: { kind: "cell", cell: myFloorTreasure.position! } };
} }
const glue = inHand(view, "glue"); const glue = inHand(view, "glue");
+54 -1
View File
@@ -6,7 +6,7 @@ import {
type PlayerId, type PlayerId,
} from "../src/game"; } from "../src/game";
import { cellKey, edgeKey } from "../src/board"; import { cellKey, edgeKey } from "../src/board";
import { viewFor } from "../src/view"; import { sightedCellsFor, viewFor } from "../src/view";
import { automatonCommand, automatonFallback, type AutomatonStyle, type AutomatonTier } from "../src/automaton"; import { automatonCommand, automatonFallback, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
/** Whose input does the maze want right now? */ /** Whose input does the maze want right now? */
@@ -170,6 +170,59 @@ describe("the clockwork does not waste counters on pointless targets", () => {
}); });
}); });
describe("the clockwork guards gold only within sight", () => {
// Game X2XN: the bot blind-cast SAFE at a treasure it could not see, the
// engine refused it, and the fallback burned the whole turn.
function goldOnTheFloor() {
let { state } = createGame({ playerIds: ["bot", "foe"], seed: 7, sets: ["basic", "expansion1"], deckRev: 36 });
while (actingSeat(state) !== "bot") {
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
const bot = state.players.find((p) => p.id === "bot")!;
const foe = state.players.find((p) => p.id === "foe")!;
foe.position = { ...bot.position };
bot.hand = [{ instanceId: "safe#T", cardId: "safe" }];
const gold = state.treasures.find((t) => t.owner === "bot")!;
gold.carriedBy = null;
return { state, bot, gold };
}
it("never blind-casts SAFE at a treasure out of sight", () => {
const { state, bot, gold } = goldOnTheFloor();
const sighted = sightedCellsFor(viewFor(state, "bot"));
const hidden = Object.keys(state.board.cells).find(
(k) => !sighted.has(k) && !state.squareContents[k],
)!;
const [x, y] = hidden.split(",").map(Number);
gold.position = { x: x!, y: y! };
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
// Whatever the brain picks, the engine must accept it — a refusal
// forfeits the bot's turn.
const chosen = cmd ?? automatonFallback(viewFor(state, "bot"), "archmage");
expect(applyCommand(state, "bot", chosen).ok).toBe(true);
});
it("still locks up gold it can see", () => {
const { state, bot, gold } = goldOnTheFloor();
const sighted = sightedCellsFor(viewFor(state, "bot"));
const seen = [...sighted].find(
(k) => k !== cellKey(bot.position) && !state.squareContents[k],
)!;
const [x, y] = seen.split(",").map(Number);
gold.position = { x: x!, y: y! };
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
expect(cmd).toEqual({
type: "cast", instanceId: "safe#T",
target: { kind: "cell", cell: gold.position },
});
expect(applyCommand(state, "bot", cmd!).ok).toBe(true);
});
});
describe("a clogged hand gets shed, not hoarded", () => { describe("a clogged hand gets shed, not hoarded", () => {
it("the bot discards dead weight so the end-of-turn draw has room", () => { it("the bot discards dead weight so the end-of-turn draw has room", () => {
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 13 }); let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 13 });