Swap Meet trades treasures — a carried treasure is a carried item

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
This commit is contained in:
Eric Wagoner
2026-08-17 22:19:29 -04:00
co-authored by Claude Fable 5
parent 00517e08b1
commit 3aef6694f3
3 changed files with 158 additions and 18 deletions
@@ -365,3 +365,83 @@ describe("swap meet trades carried items (rules rev 26)", () => {
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);
});
});