The archmage cuts off a winning delivery and aims at the leader when no kill is on the table
Reuben's three wins today were walked home unseen: the clockwork fought whoever stood in front of it. Now a delivery that would end the game pulls every automaton toward that wizard's home (where the banked gold is lifted off the doorstep) or the road to it, and among visible targets the thief still dies first, a kill is still taken, and only then does the aim shift to whoever has more treasures banked. Measured with deploy/brain-trial.mjs, which seats the working tree's brain against an older checkout's at one table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
1b18dffd28
commit
be96dd6047
@@ -8,7 +8,7 @@ import {
|
||||
} from "../src/game";
|
||||
import { cellKey, edgeKey } from "../src/board";
|
||||
import { sightedCellsFor, viewFor } from "../src/view";
|
||||
import { automatonCommand, automatonFallback, drawUnderSlowDeath, pathToward, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
|
||||
import { automatonCommand, automatonFallback, drawUnderSlowDeath, interceptCell, pathToward, type AutomatonStyle, type AutomatonTier } from "../src/automaton";
|
||||
import { pushSustained } from "./helpers";
|
||||
|
||||
/** Whose input does the maze want right now? */
|
||||
@@ -1191,3 +1191,85 @@ describe("the clockwork under a curse, and before a pit", () => {
|
||||
throw new Error("no straight three-square run on this board");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the common enemy and the interception", () => {
|
||||
/** Past the opening round, at the bot's turn, every hand emptied. */
|
||||
function tableOf(players: PlayerId[], seed = 7): GameState {
|
||||
let { state } = createGame({ playerIds: players, seed, sets: ["basic", "expansion1"] });
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||
if (!r.ok) throw new Error(r.error);
|
||||
state = r.state;
|
||||
}
|
||||
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;
|
||||
}
|
||||
for (const p of state.players) p.hand = [];
|
||||
return state;
|
||||
}
|
||||
const bank = (state: GameState, owner: PlayerId, at: PlayerId) => {
|
||||
const t = state.treasures.find((t) => t.owner === owner && t.position && !t.carriedBy)!;
|
||||
t.position = { ...state.players.find((p) => p.id === at)!.home };
|
||||
};
|
||||
|
||||
it("aims at the wizard with more banked than itself when no kill is on the table", () => {
|
||||
const state = tableOf(["bot", "weak", "leader"]);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
for (const id of ["weak", "leader"]) state.players.find((p) => p.id === id)!.position = { ...bot.position };
|
||||
state.players.find((p) => p.id === "weak")!.life = 10;
|
||||
state.players.find((p) => p.id === "leader")!.life = 12;
|
||||
bank(state, "weak", "leader");
|
||||
bot.hand = [{ instanceId: "fireball#T", cardId: "fireball" }];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toMatchObject({ type: "cast", instanceId: "fireball#T", target: { kind: "player", playerId: "leader" } });
|
||||
});
|
||||
|
||||
it("still takes a kill over the leader", () => {
|
||||
const state = tableOf(["bot", "weak", "leader"]);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
for (const id of ["weak", "leader"]) state.players.find((p) => p.id === id)!.position = { ...bot.position };
|
||||
state.players.find((p) => p.id === "weak")!.life = 5;
|
||||
state.players.find((p) => p.id === "leader")!.life = 12;
|
||||
bank(state, "weak", "leader");
|
||||
bot.hand = [{ instanceId: "fireball#T", cardId: "fireball" }];
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toMatchObject({ type: "cast", target: { kind: "player", playerId: "weak" } });
|
||||
});
|
||||
|
||||
it("cuts off a delivery that would win: their home when it can get there first, else their road", () => {
|
||||
const state = tableOf(["bot", "foe"], 5);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const foe = state.players.find((p) => p.id === "foe")!;
|
||||
// The foe has one banked and carries the second, far from home.
|
||||
bank(state, "bot", "foe");
|
||||
const second = state.treasures.find((t) => t.owner === "bot" && t.position && !t.carriedBy &&
|
||||
cellKey(t.position) !== cellKey(foe.home))!;
|
||||
second.position = null; second.carriedBy = "foe"; foe.carriedTreasureId = second.id;
|
||||
foe.position = { ...bot.home };
|
||||
bot.position = { x: foe.home.x, y: foe.home.y === 0 ? 1 : foe.home.y - 1 };
|
||||
const view = viewFor(state, "bot");
|
||||
const cut = interceptCell(view, foe, bot.position, false);
|
||||
expect(cellKey(cut)).toBe(cellKey(foe.home));
|
||||
// The march heads there — and, arrived, lifts the banked gold off
|
||||
// their doorstep, which unmakes the win outright.
|
||||
const step = pathToward(view, bot.position, new Set([cellKey(cut)]), {});
|
||||
expect(automatonCommand(view, "hunter", "archmage")).toEqual({ type: "move", direction: step!.dir });
|
||||
bot.position = { ...foe.home };
|
||||
expect(automatonCommand(viewFor(state, "bot"), "hunter", "archmage")).toMatchObject({ type: "pickUpTreasure" });
|
||||
});
|
||||
|
||||
it("leaves a carrier alone whose delivery would not win", () => {
|
||||
const state = tableOf(["bot", "foe"], 5);
|
||||
const bot = state.players.find((p) => p.id === "bot")!;
|
||||
const foe = state.players.find((p) => p.id === "foe")!;
|
||||
const t = state.treasures.find((t) => t.owner === "bot" && t.position && !t.carriedBy)!;
|
||||
t.position = null; t.carriedBy = "foe"; foe.carriedTreasureId = t.id;
|
||||
// Nothing banked: the foe is merely a thief of my gold — the chase
|
||||
// rule, not the interception, decides; with no attack in hand the
|
||||
// hunter goes for gold as before.
|
||||
const plain = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(plain?.type).toBe("move");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user