The trade grammar gains a 'treasure' token (never present in stored ledgers, so no gate needed): a dagger can buy back the treasure in a thief's arms, or two armfuls of gold can change hands outright. The one-treasure carry limit and WEAKNESS both hold in a trade as at a grab — a one-way treasure needs an open slot and an able back. The picker offers each side's carried treasure alongside displayed items, withholding theirs when your own arms are already full. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
448 lines
22 KiB
TypeScript
448 lines
22 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { applyCommand, activePlayer, boardView, createGame, gameLos, sustainedOn } from "../src/game";
|
|
import { cellKey, stepTarget } from "../src/board";
|
|
import type { CardInstance } from "../src/cards";
|
|
import { newExpansionGame as newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers";
|
|
|
|
describe("expansion combat cards", () => {
|
|
it("power attack burns life for extra damage", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const fb = giveCard(state, attacker, "fireball");
|
|
giveCard(state, attacker, "power-attack", "PA", 1);
|
|
state = castAt(state, attacker, defender, fb, {
|
|
powerAttackInstanceId: "power-attack#PA", powerAttackPoints: 3,
|
|
});
|
|
expect(state.players.find((p) => p.id === defender)!.life).toBe(7); // 5+3
|
|
expect(state.players.find((p) => p.id === attacker)!.life).toBe(12);
|
|
});
|
|
|
|
it("weakness doubles damage taken and forbids carrying treasure", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const wk = giveCard(state, attacker, "weakness");
|
|
giveCard(state, attacker, "number-3", "N", 1);
|
|
state = castAt(state, attacker, defender, wk, { numberInstanceIds: ["number-3#N"] });
|
|
expect(sustainedOn(state, defender, "weakness").length).toBe(1);
|
|
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
|
const t = state.treasures.find((t) => t.owner === attacker && t.position)!;
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
d.position = { ...t.position! };
|
|
expect(applyCommand(state, defender, { type: "pickUpTreasure" }).ok).toBe(false);
|
|
state = must(state, defender, { type: "endTurn", draw: 0 });
|
|
const fb = giveCard(state, attacker, "fireball", "F", 0);
|
|
const d2 = state.players.find((p) => p.id === defender)!;
|
|
d2.position = { ...state.players.find((p) => p.id === attacker)!.position };
|
|
state = castAt(state, attacker, defender, fb);
|
|
expect(state.players.find((p) => p.id === defender)!.life).toBe(5); // 5x2
|
|
});
|
|
|
|
it("walking dead bleeds half a point per space walked", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const wd = giveCard(state, attacker, "walking-dead");
|
|
state = castAt(state, attacker, defender, wd);
|
|
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
|
// Defender walks: every second step costs a point.
|
|
let lifeStart = state.players.find((p) => p.id === defender)!.life;
|
|
let steps = 0;
|
|
for (const dir of ["N", "S", "E", "W", "N", "S"] as const) {
|
|
const r = applyCommand(state, defender, { type: "move", direction: dir });
|
|
if (r.ok) { state = r.state; steps++; }
|
|
if (steps === 2) break;
|
|
}
|
|
expect(steps).toBe(2);
|
|
expect(state.players.find((p) => p.id === defender)!.life).toBe(lifeStart - 1);
|
|
});
|
|
|
|
it("mental swap trades entire hands", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const ms = giveCard(state, attacker, "mental-swap");
|
|
const aCards = state.players.find((p) => p.id === attacker)!.hand.map((c) => c.instanceId);
|
|
const dCards = state.players.find((p) => p.id === defender)!.hand.map((c) => c.instanceId);
|
|
state = castAt(state, attacker, defender, ms);
|
|
const aAfter = state.players.find((p) => p.id === attacker)!.hand.map((c) => c.instanceId);
|
|
expect(aAfter).toEqual(dCards);
|
|
// (the swap card itself was consumed from the attacker's hand pre-swap)
|
|
expect(state.players.find((p) => p.id === defender)!.hand.map((c) => c.instanceId))
|
|
.toEqual(aCards.filter((id) => id !== ms.instanceId));
|
|
});
|
|
|
|
it("butt-head rams for the distance charged", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const attacker = activePlayer(state);
|
|
const defender = state.players.find((p) => p.id !== attacker.id)!;
|
|
// Stand them 3 apart on the same column if possible; else same square +N.
|
|
defender.position = { x: attacker.position.x, y: attacker.position.y >= 3 ? attacker.position.y - 3 : attacker.position.y + 3 };
|
|
const bh = giveCard(state, attacker.id, "butt-head");
|
|
state = castAt(state, attacker.id, defender.id, bh);
|
|
const a = state.players.find((p) => p.id === attacker.id)!;
|
|
const d = state.players.find((p) => p.id === defender.id)!;
|
|
expect(cellKey(a.position)).toBe(cellKey(d.position));
|
|
expect(d.life).toBe(12); // 3 spaces = 3 damage
|
|
});
|
|
|
|
it("empathy turns an attack back on its caster as well", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
// Defender raises empathy on their own turn.
|
|
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
|
const em = giveCard(state, defender, "empathy", "E", 0);
|
|
giveCard(state, defender, "number-3", "N", 1);
|
|
state = must(state, defender, {
|
|
type: "cast", instanceId: em.instanceId, numberInstanceIds: ["number-3#N"],
|
|
});
|
|
state = must(state, defender, { type: "endTurn", draw: 0 });
|
|
const fb = giveCard(state, attacker, "fireball", "F", 0);
|
|
state = castAt(state, attacker, defender, fb);
|
|
expect(state.players.find((p) => p.id === defender)!.life).toBe(10);
|
|
expect(state.players.find((p) => p.id === attacker)!.life).toBe(10);
|
|
});
|
|
|
|
it("ward springs when a trapped treasure is grabbed", () => {
|
|
let { state } = newGame();
|
|
const me = activePlayer(state);
|
|
const enemy = state.players.find((p) => p.id !== me.id)!;
|
|
giveCard(state, enemy.id, "ward", "W", 0);
|
|
const treasure = state.treasures.find((t) => t.owner === enemy.id && t.position)!;
|
|
me.position = { ...treasure.position! };
|
|
state = must(state, me.id, { type: "pickUpTreasure" });
|
|
expect(state.players.find((p) => p.id === me.id)!.life).toBe(12);
|
|
expect(state.players.find((p) => p.id === enemy.id)!.hand.some((c) => c.cardId === "ward")).toBe(false);
|
|
});
|
|
|
|
it("opportunity fire opens an out-of-turn attack window", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const active = activePlayer(state).id;
|
|
const lurker = state.players.find((p) => p.id !== active)!;
|
|
lurker.position = { ...state.players.find((p) => p.id === active)!.position };
|
|
const of_ = giveCard(state, lurker.id, "opportunity-fire", "OF", 0);
|
|
const fb = giveCard(state, lurker.id, "fireball", "F", 1);
|
|
// Out of turn: play opportunity fire, then the attack.
|
|
state = must(state, lurker.id, { type: "cast", instanceId: of_.instanceId });
|
|
expect(state.outOfTurnWindow?.playerId).toBe(lurker.id);
|
|
state = must(state, lurker.id, {
|
|
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: active },
|
|
});
|
|
state = must(state, active, { type: "pass" });
|
|
expect(state.players.find((p) => p.id === active)!.life).toBe(10);
|
|
// Turn structure is intact: the original player is still active.
|
|
expect(activePlayer(state).id).toBe(active);
|
|
expect(state.outOfTurnWindow).toBeNull();
|
|
});
|
|
|
|
it("idiot marches its victim toward their own treasure and forbids casting", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const id = giveCard(state, attacker, "idiot");
|
|
state = castAt(state, attacker, defender, id);
|
|
expect(sustainedOn(state, defender, "idiot").length).toBe(1);
|
|
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
|
// Casting is refused; moving is steered (any direction request works).
|
|
const fb = giveCard(state, defender, "fireball", "F", 0);
|
|
expect(applyCommand(state, defender, {
|
|
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: attacker },
|
|
}).ok).toBe(false);
|
|
const before = state.players.find((p) => p.id === defender)!.position;
|
|
const r = applyCommand(state, defender, { type: "move", direction: "N" });
|
|
if (r.ok) {
|
|
const after = r.state.players.find((p) => p.id === defender)!.position;
|
|
expect(cellKey(after)).not.toBe(cellKey(before));
|
|
} else {
|
|
expect(r.error).toMatch(/blocked/);
|
|
}
|
|
});
|
|
|
|
it("idiot at rev 23 forbids item handling and punches but allows counteractions", () => {
|
|
let { state } = createGame({
|
|
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 23,
|
|
});
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
// The victim carries an OPPONENT'S treasure into the spell.
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
const stolen = state.treasures.find((t) => t.owner === attacker)!;
|
|
stolen.carriedBy = defender;
|
|
stolen.position = null;
|
|
d.carriedTreasureId = stolen.id;
|
|
const id = giveCard(state, attacker, "idiot");
|
|
state = castAt(state, attacker, defender, id);
|
|
expect(sustainedOn(state, defender, "idiot").length).toBe(1);
|
|
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
|
|
|
// No dropping the carried treasure (no capturing it on your home, either).
|
|
const drop = applyCommand(state, defender, { type: "dropTreasure" });
|
|
expect(drop.ok).toBe(false);
|
|
if (!drop.ok) expect(drop.error).toMatch(/treasure/);
|
|
// No punching the tormentor.
|
|
expect(applyCommand(state, defender, { type: "punch", targetId: attacker }).ok).toBe(false);
|
|
// Goal-aiding spells stay castable (FAQ: "You could, however, destroy a wall").
|
|
const sp = giveCard(state, defender, "speed", "SP", 0);
|
|
expect(applyCommand(state, defender, { type: "cast", instanceId: sp.instanceId }).ok).toBe(true);
|
|
// Counteractions are expressly allowed: absorb an incoming fireball.
|
|
state = must(state, defender, { type: "endTurn", draw: 0 });
|
|
const fb = giveCard(state, attacker, "fireball", "F", 0);
|
|
state = must(state, attacker, {
|
|
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender },
|
|
});
|
|
giveCard(state, defender, "absorb-spell", "AB", 1);
|
|
expect(applyCommand(state, defender, { type: "counteract", instanceId: "absorb-spell#AB" }).ok).toBe(true);
|
|
});
|
|
|
|
it("idiot at rev 23 has no effect on a victim carrying their own treasure", () => {
|
|
let { state } = createGame({
|
|
playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 23,
|
|
});
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
const own = state.treasures.find((t) => t.owner === defender)!;
|
|
own.carriedBy = defender;
|
|
own.position = null;
|
|
d.carriedTreasureId = own.id;
|
|
const id = giveCard(state, attacker, "idiot");
|
|
state = castAt(state, attacker, defender, id);
|
|
expect(sustainedOn(state, defender, "idiot").length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("swap home bases", () => {
|
|
it("trades homes when both bases hold equal treasures", () => {
|
|
let { state } = newGame();
|
|
const me = activePlayer(state);
|
|
const other = state.players.find((p) => p.id !== me.id)!;
|
|
other.position = { ...me.position }; // LOS guaranteed
|
|
const myHome = { ...me.home };
|
|
const theirHome = { ...other.home };
|
|
const shb = giveCard(state, me.id, "swap-home-bases");
|
|
state = must(state, me.id, {
|
|
type: "cast", instanceId: shb.instanceId, target: { kind: "player", playerId: other.id },
|
|
});
|
|
expect(cellKey(state.players.find((p) => p.id === me.id)!.home)).toBe(cellKey(theirHome));
|
|
expect(cellKey(state.players.find((p) => p.id === other.id)!.home)).toBe(cellKey(myHome));
|
|
});
|
|
});
|
|
|
|
describe("thumb of god (divine meteor)", () => {
|
|
it("scatters every token near where the die lands", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const me = activePlayer(state);
|
|
// Sprinkle the blast zone: a ground object and the enemy nearby.
|
|
const enemy = state.players.find((p) => p.id !== me.id)!;
|
|
enemy.position = { ...me.position };
|
|
state.groundObjects[cellKey(me.position)] = [{ instanceId: "dagger#G", cardId: "dagger" }];
|
|
const tog = giveCard(state, me.id, "thumb-of-god");
|
|
state = must(state, me.id, {
|
|
type: "cast", instanceId: tog.instanceId, target: { kind: "cell", cell: me.position },
|
|
});
|
|
expect(state.turn.attackUsed).toBe(true);
|
|
// The dagger moved somewhere on the board.
|
|
const allObjects = Object.values(state.groundObjects).flat();
|
|
expect(allObjects.some((c) => c.cardId === "dagger")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("add for movement", () => {
|
|
it("an ADD permits a second number card on movement, once", () => {
|
|
let { state } = newGame();
|
|
const me = activePlayer(state).id;
|
|
giveCard(state, me, "number-3", "N1", 0);
|
|
giveCard(state, me, "number-2", "N2", 1);
|
|
giveCard(state, me, "add", "A", 2);
|
|
giveCard(state, me, "number-4", "N3", 3);
|
|
state = must(state, me, { type: "playNumberForMovement", instanceId: "number-3#N1" });
|
|
expect(state.turn.movementAllowance).toBe(6);
|
|
// Second without ADD: refused.
|
|
expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2" }).ok).toBe(false);
|
|
// With ADD: allowed.
|
|
state = must(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2", addInstanceId: "add#A" });
|
|
expect(state.turn.movementAllowance).toBe(8);
|
|
// A third is refused even with another add wished for.
|
|
expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-4#N3" }).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("ambushes (async interrupts)", () => {
|
|
it("an armed Opportunity Fire springs when prey walks into sight", () => {
|
|
let { state } = newGame();
|
|
state = toRound2(state);
|
|
const owner = activePlayer(state);
|
|
const of_ = giveCard(state, owner.id, "opportunity-fire", "OF", 0);
|
|
const fb = giveCard(state, owner.id, "fireball", "FB", 1);
|
|
state = must(state, owner.id, {
|
|
type: "setAmbush", instanceId: of_.instanceId, trigger: { kind: "los" },
|
|
spellInstanceId: fb.instanceId,
|
|
});
|
|
const o = state.players.find((p) => p.id === owner.id)!;
|
|
expect(o.hand.some((c) => c.cardId === "fireball")).toBe(false);
|
|
expect(state.ambushes.length).toBe(1);
|
|
state = must(state, owner.id, { type: "endTurn", draw: 0 });
|
|
|
|
const preyNow = state.players.find((p) => p.id !== owner.id)!;
|
|
const ownerNow = state.players.find((p) => p.id === owner.id)!;
|
|
// Find a step that goes from a no-LOS cell into a LOS cell.
|
|
let found: { from: { x: number; y: number }; side: "N" | "S" | "E" | "W" } | null = null;
|
|
outer: for (const key of Object.keys(state.board.cells)) {
|
|
const [x, y] = key.split(",").map(Number) as [number, number];
|
|
const cell = { x, y };
|
|
if (!gameLos(state, ownerNow.position, cell)) continue;
|
|
for (const side of ["N", "S", "E", "W"] as const) {
|
|
const dx = side === "E" ? 1 : side === "W" ? -1 : 0;
|
|
const dy = side === "S" ? 1 : side === "N" ? -1 : 0;
|
|
const from = { x: x - dx, y: y - dy };
|
|
if (!state.board.cells[cellKey(from)]) continue;
|
|
if (gameLos(state, ownerNow.position, from)) continue;
|
|
const st = stepTarget(boardView(state), from, side);
|
|
if (st.kind === "step" && cellKey(st.to) === key) { found = { from, side }; break outer; }
|
|
}
|
|
}
|
|
expect(found).not.toBeNull();
|
|
preyNow.position = found!.from;
|
|
state = must(state, preyNow.id, { type: "move", direction: found!.side });
|
|
expect(state.stack).not.toBeNull();
|
|
expect(state.stack!.attackerId).toBe(owner.id);
|
|
expect(state.stack!.attackCard!.cardId).toBe("fireball");
|
|
expect(state.ambushes.length).toBe(0);
|
|
state = must(state, preyNow.id, { type: "pass" });
|
|
expect(state.players.find((p) => p.id === preyNow.id)!.life).toBe(10);
|
|
});
|
|
|
|
it("cancelling an ambush returns the committed cards", () => {
|
|
let { state } = newGame();
|
|
const owner = activePlayer(state);
|
|
const int_ = giveCard(state, owner.id, "interrupt", "I", 0);
|
|
const lb = giveCard(state, owner.id, "lightning-blast", "LB", 1);
|
|
giveCard(state, owner.id, "number-3", "N", 2);
|
|
state = must(state, owner.id, {
|
|
type: "setAmbush", instanceId: int_.instanceId, trigger: { kind: "near" },
|
|
spellInstanceId: lb.instanceId, numberInstanceIds: ["number-3#N"],
|
|
});
|
|
const id = state.ambushes[0]!.id;
|
|
state = must(state, owner.id, { type: "cancelAmbush", ambushId: id });
|
|
const o = state.players.find((p) => p.id === owner.id)!;
|
|
expect(o.hand.some((c) => c.cardId === "interrupt")).toBe(true);
|
|
expect(o.hand.some((c) => c.cardId === "lightning-blast")).toBe(true);
|
|
expect(state.ambushes.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("swap meet trades carried items (rules rev 26)", () => {
|
|
function rig(deckRev: number) {
|
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev });
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const sm = giveCard(state, attacker, "swap-meet");
|
|
giveCard(state, attacker, "dagger", "D", 1);
|
|
giveCard(state, defender, "blaster-wand", "W", 0);
|
|
state = must(state, attacker, {
|
|
type: "cast", instanceId: sm.instanceId,
|
|
target: { kind: "player", playerId: defender },
|
|
params: { cardId: "dagger;blaster-wand" },
|
|
});
|
|
state = must(state, defender, { type: "pass" });
|
|
return { state, attacker, defender };
|
|
}
|
|
|
|
it("a dagger trades for a wand — both are carried items", () => {
|
|
const { state, attacker, defender } = rig(26);
|
|
expect(state.players.find((p) => p.id === attacker)!.hand.some((c) => c.cardId === "blaster-wand")).toBe(true);
|
|
expect(state.players.find((p) => p.id === defender)!.hand.some((c) => c.cardId === "dagger")).toBe(true);
|
|
});
|
|
|
|
it("older revisions matched only object-typed cards and replay so", () => {
|
|
const { state, attacker, defender } = rig(25);
|
|
expect(state.players.find((p) => p.id === attacker)!.hand.some((c) => c.cardId === "dagger")).toBe(true);
|
|
expect(state.players.find((p) => p.id === defender)!.hand.some((c) => c.cardId === "blaster-wand")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("swap meet trades treasures too", () => {
|
|
function treasureRig(theirsToken: string, mineToken: string) {
|
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 26 });
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const a = state.players.find((p) => p.id === attacker)!;
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
// The defender hauls one of the attacker's own treasures.
|
|
const stolen = state.treasures.find((t) => t.owner === attacker)!;
|
|
stolen.position = null; stolen.carriedBy = defender; d.carriedTreasureId = stolen.id;
|
|
const sm = giveCard(state, attacker, "swap-meet");
|
|
giveCard(state, attacker, "dagger", "D", 1);
|
|
state = must(state, attacker, {
|
|
type: "cast", instanceId: sm.instanceId,
|
|
target: { kind: "player", playerId: defender },
|
|
params: { cardId: `${mineToken};${theirsToken}` },
|
|
});
|
|
state = must(state, defender, { type: "pass" });
|
|
return { state, attacker, defender, a, d, stolen };
|
|
}
|
|
|
|
it("a dagger buys back the treasure in the thief's arms", () => {
|
|
const { state, attacker, defender } = treasureRig("treasure", "dagger");
|
|
const a = state.players.find((p) => p.id === attacker)!;
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
const t = state.treasures.find((tr) => tr.owner === attacker && tr.carriedBy)!;
|
|
expect(a.carriedTreasureId).toBe(t.id);
|
|
expect(t.carriedBy).toBe(attacker);
|
|
expect(d.carriedTreasureId).toBeNull();
|
|
expect(d.hand.some((c) => c.cardId === "dagger")).toBe(true);
|
|
});
|
|
|
|
it("the trade refuses to overload full arms", () => {
|
|
// The attacker also carries a treasure: claiming theirs with a dagger
|
|
// would mean two in hand — the swap quietly cannot happen.
|
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 26 });
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const a = state.players.find((p) => p.id === attacker)!;
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
const t1 = state.treasures.find((t) => t.owner === attacker)!;
|
|
t1.position = null; t1.carriedBy = attacker; a.carriedTreasureId = t1.id;
|
|
const t2 = state.treasures.find((t) => t.owner === defender)!;
|
|
t2.position = null; t2.carriedBy = defender; d.carriedTreasureId = t2.id;
|
|
const sm = giveCard(state, attacker, "swap-meet");
|
|
giveCard(state, attacker, "dagger", "D", 1);
|
|
state = must(state, attacker, {
|
|
type: "cast", instanceId: sm.instanceId,
|
|
target: { kind: "player", playerId: defender },
|
|
params: { cardId: "dagger;treasure" },
|
|
});
|
|
state = must(state, defender, { type: "pass" });
|
|
expect(state.players.find((p) => p.id === attacker)!.carriedTreasureId).toBe(t1.id);
|
|
expect(state.players.find((p) => p.id === defender)!.carriedTreasureId).toBe(t2.id);
|
|
});
|
|
|
|
it("treasure for treasure trades both armfuls", () => {
|
|
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 26 });
|
|
state = toRound2(state);
|
|
const { attacker, defender } = faceOff(state);
|
|
const a = state.players.find((p) => p.id === attacker)!;
|
|
const d = state.players.find((p) => p.id === defender)!;
|
|
const t1 = state.treasures.find((t) => t.owner === attacker)!;
|
|
t1.position = null; t1.carriedBy = attacker; a.carriedTreasureId = t1.id;
|
|
const t2 = state.treasures.find((t) => t.owner === defender)!;
|
|
t2.position = null; t2.carriedBy = defender; d.carriedTreasureId = t2.id;
|
|
const sm = giveCard(state, attacker, "swap-meet");
|
|
state = must(state, attacker, {
|
|
type: "cast", instanceId: sm.instanceId,
|
|
target: { kind: "player", playerId: defender },
|
|
params: { cardId: "treasure;treasure" },
|
|
});
|
|
state = must(state, defender, { type: "pass" });
|
|
expect(state.players.find((p) => p.id === attacker)!.carriedTreasureId).toBe(t2.id);
|
|
expect(state.players.find((p) => p.id === defender)!.carriedTreasureId).toBe(t1.id);
|
|
expect(state.treasures.find((t) => t.id === t1.id)!.carriedBy).toBe(defender);
|
|
expect(state.treasures.find((t) => t.id === t2.id)!.carriedBy).toBe(attacker);
|
|
});
|
|
});
|