Round two of the owner's tactics: exile, wand-slip, finishers, boosts

- TELEPORT OPPONENT exiles a delivery in progress — the thief of the
  bot's gold, or any carrier within six of home — to the square farthest
  (by walking distance) from the victim's own home; a sealed pocket wins
  outright. No LOS needed for the destination, per the card.
- WARP WAND joins the roadwork: a wall slips open for a 4+-step shortcut
  when the crossing fits this turn's remaining legs (the wall returns at
  end of turn); charges itself with the smallest number on first use.
- ADRENALINE is the finisher: cast when no single attack in hand kills
  the target but the top two together do; the attack window now honors
  the second swing.
- STRENGTH doubles a thrown dagger or rock exactly when that turns a
  wound into a kill.
- EXTEND rides afflictions, doubling the misery's stay.
- MEGA-MONSTER doubles a pet's stride (movement over life) so the
  menagerie actually catches people.
All six leave the bottom discard tier.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-17 20:33:45 -04:00
co-authored by Claude Fable 5
parent 4bda6087b1
commit 3087f00bdc
2 changed files with 176 additions and 8 deletions
+53
View File
@@ -374,3 +374,56 @@ describe("the widened spellbook", () => {
if (!r.ok) throw new Error(r.error);
});
});
describe("round two of the owner's tactics", () => {
it("exiles a thief carrying its gold to the far end of nowhere", () => {
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic", "expansion1"], deckRev: 24 });
// burn round 1 and reach the bot's turn
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 thief = state.players.find((p) => p.id === "other")!;
const t = state.treasures.find((t) => t.owner === "bot")!;
t.position = null;
t.carriedBy = "other";
thief.carriedTreasureId = t.id;
// The thief is a step from delivering; the bot watches from beside them.
thief.position = { x: thief.home.x, y: thief.home.y === 0 ? 1 : thief.home.y - 1 };
bot.position = { ...thief.position };
bot.hand = [{ instanceId: "teleport-opponent#T", cardId: "teleport-opponent" }];
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
expect(cmd).toMatchObject({ type: "cast", instanceId: "teleport-opponent#T" });
const dest = (cmd as { params: { cell: { x: number; y: number } } }).params.cell;
// The chosen square is a long march from the thief's own home.
const d = Math.abs(dest.x - thief.home.x) + Math.abs(dest.y - thief.home.y);
expect(d).toBeGreaterThanOrEqual(5);
const r = applyCommand(state, "bot", cmd!);
if (!r.ok) throw new Error(r.error);
});
it("casts adrenaline when two blows finish what one cannot", () => {
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic", "expansion1"], deckRev: 24 });
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")!;
prey.position = { ...bot.position };
prey.life = 7; // fireball (5) alone cannot; fireball + dagger (3) can
bot.hand = [
{ instanceId: "adrenaline#T", cardId: "adrenaline" },
{ instanceId: "fireball#T", cardId: "fireball" },
{ instanceId: "dagger#T", cardId: "dagger" },
{ instanceId: "number-2#T", cardId: "number-2" },
];
const cmd = automatonCommand(viewFor(state, "bot"), "berserker", "archmage");
expect(cmd).toMatchObject({ type: "cast", instanceId: "adrenaline#T" });
const r = applyCommand(state, "bot", cmd!);
if (!r.ok) throw new Error(r.error);
});
});