No movement number is spent on a turn that ends at a grab
Room M9QJ, seq 190: a berserker played a 5 to charge a distant enemy, walked two squares, crossed a treasure, and the gold-underfoot rung — which outranks everything — ended its actions with six paid moves unwalked. Before spending a number, the march now checks whether any goal treasure lies within the free legs: if the turn is going to end at a grab, the number stays in hand. (Even a charging berserker stoops for loot; it just shouldn't pay toll first.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
co-authored by
Claude Fable 5
parent
63e8d4c545
commit
5310fbdca9
@@ -1396,7 +1396,15 @@ export function automatonCommand(
|
|||||||
(c.cardId === "blaster-wand" && view.wandCharges[c.instanceId] == null);
|
(c.cardId === "blaster-wand" && view.wandCharges[c.instanceId] == null);
|
||||||
});
|
});
|
||||||
const spendable = wantsNumber ? numbers.slice(0, -1) : numbers;
|
const spendable = wantsNumber ? numbers.slice(0, -1) : numbers;
|
||||||
if (!view.turn.numberPlayedForMovement && path.distance > movesLeft) {
|
// A treasure within free walking ends this turn at the grab (the
|
||||||
|
// gold-underfoot rung outranks everything) — a number played first
|
||||||
|
// would evaporate. Even a charging berserker stoops for loot.
|
||||||
|
const goldWithinLegs = !self.carriedTreasureId &&
|
||||||
|
(() => {
|
||||||
|
const dHere = distancesFrom(view, [self.position], canUnlock);
|
||||||
|
return [...treasureGoals(view)].some((k) => (dHere.get(k) ?? Infinity) <= movesLeft);
|
||||||
|
})();
|
||||||
|
if (!view.turn.numberPlayedForMovement && path.distance > movesLeft && !goldWithinLegs) {
|
||||||
const helper = spendable.find(
|
const helper = spendable.find(
|
||||||
(n) => movesLeft + (cardDef(n.cardId).value ?? 0) >= path.distance,
|
(n) => movesLeft + (cardDef(n.cardId).value ?? 0) >= path.distance,
|
||||||
) ?? (path.distance > movesLeft + 2 ? spendable[spendable.length - 1] : undefined);
|
) ?? (path.distance > movesLeft + 2 ? spendable[spendable.length - 1] : undefined);
|
||||||
|
|||||||
@@ -494,3 +494,29 @@ describe("a pact once signed is honored", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("no number is wasted on a turn that ends at a grab", () => {
|
||||||
|
it("gold two free steps away: the berserker charges without spending its 5", () => {
|
||||||
|
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic", "expansion1"], deckRev: 29 });
|
||||||
|
for (let guard = 0; guard < 10 && !(actingSeat(state) === "bot" && state.turn.round > 1); guard++) {
|
||||||
|
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 prey = state.players.find((p) => p.id === "other")!;
|
||||||
|
// Enemy far across the maze; their treasure lies one step from the bot.
|
||||||
|
prey.position = { x: 0, y: 0 };
|
||||||
|
bot.position = { x: 5, y: 5 };
|
||||||
|
const t = state.treasures.find((t) => t.owner === "other")!;
|
||||||
|
t.position = { x: 5, y: 6 };
|
||||||
|
t.carriedBy = null;
|
||||||
|
state.edgeOverrides[edgeKey({ x: 5, y: 5 }, "S")] = "open";
|
||||||
|
bot.hand = [
|
||||||
|
{ instanceId: "number-5#T", cardId: "number-5" },
|
||||||
|
{ instanceId: "number-2#T", cardId: "number-2" },
|
||||||
|
];
|
||||||
|
const cmd = automatonCommand(viewFor(state, "bot"), "berserker", "archmage");
|
||||||
|
expect(cmd?.type).not.toBe("playNumberForMovement");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user