The clockwork wades, shelters, and honors pacts it can actually break
Game 6XDY's prisoners expose a nest of turn-burners. The pathfinder treated pits, ooze, bushes, and slime as walls in transit, so bots stood "trapped" in pockets any human would wade out of: the march now falls back to a teeth-gritted hazard route when no clean road exists. Bots no longer aim attacks into (or out of) a thornbush's shelter the engine refuses; WIZARDBLADE learns it is a blade (same square only); a cursed IDIOT skips every rung the curse would refuse instead of burning turns on them; BUDDY's orientation is righted — whoever pacted YOU is untouchable, your own pacts are merely precious — and a thief standing underfoot no longer becomes a null destination that freezes two laden wizards on the same square forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
76207c6264
commit
39ddbb452d
@@ -283,6 +283,81 @@ describe("the clockwork honors IDIOT's march", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("the clockwork wades hazards rather than surrender", () => {
|
||||
it("takes the slime road when no clean path to anything exists", () => {
|
||||
let { state } = createGame({ playerIds: ["bot", "foe"], seed: 7, sets: ["basic", "expansion1"] });
|
||||
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")!;
|
||||
bot.hand = [];
|
||||
// Wall the bot into its square except one side; slime that one exit.
|
||||
const sides = ["N", "S", "E", "W"] as const;
|
||||
const board = state.board;
|
||||
const open = sides.filter((s) => {
|
||||
const n = { x: bot.position.x + (s === "E" ? 1 : s === "W" ? -1 : 0),
|
||||
y: bot.position.y + (s === "S" ? 1 : s === "N" ? -1 : 0) };
|
||||
return board.cells[cellKey(n)] !== undefined;
|
||||
});
|
||||
const exit = open[0]!;
|
||||
for (const s2 of sides) {
|
||||
if (s2 !== exit) state.edgeOverrides[edgeKey(bot.position, s2)] = "wall";
|
||||
else state.edgeOverrides[edgeKey(bot.position, s2)] = "open";
|
||||
}
|
||||
const beyond = { x: bot.position.x + (exit === "E" ? 1 : exit === "W" ? -1 : 0),
|
||||
y: bot.position.y + (exit === "S" ? 1 : exit === "N" ? -1 : 0) };
|
||||
state.squareContents[cellKey(beyond)] = { kind: "slime", damage: 0, createdBy: "foe" };
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||
expect(cmd).toEqual({ type: "move", direction: exit });
|
||||
expect(applyCommand(state, "bot", cmd!).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the clockwork respects the bush's shelter", () => {
|
||||
function faceOffWithFireball() {
|
||||
let { state } = createGame({ playerIds: ["bot", "foe"], seed: 7, sets: ["basic", "expansion1"] });
|
||||
for (let i = 0; i < 2; 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;
|
||||
}
|
||||
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: "fireball#T", cardId: "fireball" }];
|
||||
return { state, bot, foe };
|
||||
}
|
||||
|
||||
it("never aims at a wizard sheltered in a thornbush", () => {
|
||||
const { state, foe } = faceOffWithFireball();
|
||||
state.squareContents[cellKey(foe.position)] = { kind: "thornbush", damage: 0, createdBy: "foe" };
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage")
|
||||
?? automatonFallback(viewFor(state, "bot"), "archmage");
|
||||
// Whatever it picks, the engine must accept it — and it must not be
|
||||
// the refused fireball that would burn the whole turn.
|
||||
expect(cmd).not.toMatchObject({ type: "cast", instanceId: "fireball#T" });
|
||||
expect(applyCommand(state, "bot", cmd).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("never attacks out of its own bush", () => {
|
||||
const { state, bot, foe } = faceOffWithFireball();
|
||||
foe.position = { x: bot.position.x, y: bot.position.y };
|
||||
state.squareContents[cellKey(bot.position)] = { kind: "thornbush", damage: 0, createdBy: "foe" };
|
||||
foe.position = { ...bot.position };
|
||||
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage")
|
||||
?? automatonFallback(viewFor(state, "bot"), "archmage");
|
||||
expect(cmd).not.toMatchObject({ type: "cast", instanceId: "fireball#T" });
|
||||
expect(applyCommand(state, "bot", cmd).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a clogged hand gets shed, not hoarded", () => {
|
||||
it("the bot discards dead weight so the end-of-turn draw has room", () => {
|
||||
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"] });
|
||||
|
||||
Reference in New Issue
Block a user