The thief-chase serves the blow, not the nearness
An automaton beside the wizard carrying its gold shuttled on and off their square until every move was spent — the chase goal flipped between "the thief's cell" and "the gold" with each step, because it keyed on whether the thief stood underfoot. The chase now claims the march only while the blow it exists to deliver is still live (an attack in hand, the turn's attack unspent, casting open): those gates hold still as the clockwork walks, so the goal cannot flip mid-march. Caught with the strike live, it stands its ground; otherwise the gold has its legs for the turn. A regression walks the exact scene and forbids the A-B-A shuttle. The bot-vs-bot harness also learns to route WARD's pending moment to the treasure's owner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
71a3c81670
commit
168e57ab91
@@ -1374,19 +1374,25 @@ export function automatonCommand(
|
|||||||
const ownGoldCells = new Set(view.treasures
|
const ownGoldCells = new Set(view.treasures
|
||||||
.filter((t) => t.owner === view.you && t.position && !t.carriedBy)
|
.filter((t) => t.owner === view.you && t.position && !t.carriedBy)
|
||||||
.map((t) => cellKey(t.position!)));
|
.map((t) => cellKey(t.position!)));
|
||||||
// A thief already underfoot is not a destination — with the chase
|
// The chase exists to deliver a blow, so it claims the march only
|
||||||
// moot, march on the real goals (deliver, grab) instead of standing
|
// while that blow is still live: an attack in hand, this turn's
|
||||||
// on them forever.
|
// attack unspent, casting not yet closed. Those gates hold still as
|
||||||
const thiefAfar = thief && cellKey(thief.position) !== here ? thief : null;
|
// the clockwork walks — a goal keyed to the thief's nearness flips
|
||||||
|
// underfoot and shuttles the walker on and off their square. Caught
|
||||||
|
// with the strike still live, it stands its ground (a goal underfoot
|
||||||
|
// ends the march); otherwise the gold has its legs for the turn.
|
||||||
|
const strikeLive = !view.turn.attackUsed && !view.turn.attackForbidden &&
|
||||||
|
!view.turn.actionsEnded && view.yourHand.some((c) => ATTACKS[c.cardId] != null);
|
||||||
|
const chasing = thief !== null && strikeLive;
|
||||||
const objectives = cursedIdiot && ownGoldCells.size > 0
|
const objectives = cursedIdiot && ownGoldCells.size > 0
|
||||||
? ownGoldCells
|
? ownGoldCells
|
||||||
: thiefAfar
|
: chasing
|
||||||
? new Set([cellKey(thiefAfar.position)])
|
? new Set([cellKey(thief.position)])
|
||||||
: style === "berserker" && !self.carriedTreasureId
|
: style === "berserker" && !self.carriedTreasureId
|
||||||
? (enemyCells.size > 0 ? enemyCells : gold)
|
? (enemyCells.size > 0 ? enemyCells : gold)
|
||||||
: gold;
|
: gold;
|
||||||
const path =
|
const path =
|
||||||
pathToward(view, self.position, objectives, { avoidNearEnemies: style === "worrier" && !thiefAfar, canUnlock }) ??
|
pathToward(view, self.position, objectives, { avoidNearEnemies: style === "worrier" && !chasing, canUnlock }) ??
|
||||||
pathToward(view, self.position, objectives, { canUnlock }) ??
|
pathToward(view, self.position, objectives, { canUnlock }) ??
|
||||||
pathToward(view, self.position, enemyCells, { canUnlock }) ??
|
pathToward(view, self.position, enemyCells, { canUnlock }) ??
|
||||||
// No clean road to anything: grit the teeth and wade the hazards,
|
// No clean road to anything: grit the teeth and wade the hazards,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { pushSustained } from "./helpers";
|
|||||||
/** Whose input does the maze want right now? */
|
/** Whose input does the maze want right now? */
|
||||||
function actingSeat(state: GameState): PlayerId {
|
function actingSeat(state: GameState): PlayerId {
|
||||||
return (
|
return (
|
||||||
|
state.wardPending?.ownerId ??
|
||||||
state.stack?.waitingOn ??
|
state.stack?.waitingOn ??
|
||||||
state.pendingDiscard ??
|
state.pendingDiscard ??
|
||||||
state.chaosPending?.queue[0] ??
|
state.chaosPending?.queue[0] ??
|
||||||
@@ -735,3 +736,42 @@ describe("the clockwork flees the dread", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the thief-chase holds one goal per turn", () => {
|
||||||
|
it("a clockwork beside its thief never shuttles on and off their square", () => {
|
||||||
|
let { state } = createGame({ playerIds: ["thief", "bot"], seed: 42, sets: ["basic", "expansion1"] });
|
||||||
|
// Round 2, thief's turn burned; the bot acts with a full allowance.
|
||||||
|
while (state.turn.round < 2 || state.players[state.turn.activeIndex]!.id !== "bot") {
|
||||||
|
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||||
|
if (!r.ok) throw new Error(r.error);
|
||||||
|
state = r.state;
|
||||||
|
}
|
||||||
|
const thief = state.players.find((p) => p.id === "thief")!;
|
||||||
|
const bot = state.players.find((p) => p.id === "bot")!;
|
||||||
|
// The thief carries the bot's gold and stands one step away.
|
||||||
|
const mine = state.treasures.find((t) => t.owner === "bot")!;
|
||||||
|
mine.carriedBy = "thief";
|
||||||
|
mine.position = null;
|
||||||
|
thief.position = { x: 2, y: 4 };
|
||||||
|
bot.position = { x: 2, y: 5 };
|
||||||
|
state.edgeOverrides[edgeKey({ x: 2, y: 4 }, "S")] = "open";
|
||||||
|
// A number card and no attacks: the blow the chase serves cannot land.
|
||||||
|
bot.hand = [];
|
||||||
|
bot.hand.push({ cardId: "number-2", instanceId: "N2" } as never);
|
||||||
|
const visited = [cellKey(bot.position)];
|
||||||
|
for (let guard = 0; guard < 40; guard++) {
|
||||||
|
const view = viewFor(state, "bot");
|
||||||
|
const cmd = automatonCommand(view, "hunter", "archmage") ?? automatonFallback(view, "archmage");
|
||||||
|
if (cmd.type === "endTurn") break;
|
||||||
|
const r = applyCommand(state, "bot", cmd);
|
||||||
|
if (!r.ok) break;
|
||||||
|
state = r.state;
|
||||||
|
const at = cellKey(state.players.find((p) => p.id === "bot")!.position);
|
||||||
|
if (cmd.type === "move" && at !== visited[visited.length - 1]) visited.push(at);
|
||||||
|
}
|
||||||
|
// No step may return to the square just departed: A-B-A is the shuttle.
|
||||||
|
for (let i = 2; i < visited.length; i++) {
|
||||||
|
expect(visited[i]).not.toBe(visited[i - 2]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user