Pacts are honored; Swap Meet trades items, not card names (rev 26)

The pact fix: a clockwork no longer attacks a wizard it holds a BUDDY
pact on — Eric watched one sign the pact and punch him the next turn.
A pact is torn up for exactly one thing: a kill. And the post-attack
pact is signed only by a wizard who wants OUT of the fight (hauling
gold, bleeding, or with other enemies left) — a healthy duelist keeps
its options, and mutual-pact stalemates stay out of bot wars.

Swap Meet drops the 'name a card' prompt for a real trade picker:
click the other trader, choose one of your carried items, then claim
one of their displayed items. Rules rev 26 widens the engine's match
from object-typed cards to every movable object (daggers, rocks,
wands, stones); older games matched narrowly and replay so.

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:14:57 -04:00
co-authored by Claude Fable 5
parent 06faea0c79
commit 00517e08b1
7 changed files with 140 additions and 14 deletions
+46 -1
View File
@@ -290,7 +290,7 @@
const CREATURE_TARGET_CARDS = new Set(["mega-monster"]);
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner"]);
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu", "thief", "swap-meet", "remove-curse", "swarthmores-enchantment", "illusionary-attack"]);
const NAMED_CARDS = new Set(["card-erasure", "drop-object", "deja-vu", "thief", "remove-curse", "swarthmores-enchantment", "illusionary-attack"]);
const attackVsWall = $derived(
selectedCard != null && cardDef(selectedCard.cardId).cardType === "attack" && !EDGE_CARDS.has(selectedCard.cardId),
);
@@ -309,6 +309,8 @@
function clearSelection() {
punchWallMode = false;
swapMeetTarget = null;
swapMine = null;
ambushVia = null;
ambushTrigger = null;
ambushSpell = null;
@@ -797,6 +799,10 @@
pendingCellFor = playerId;
return; // next: click the destination cell
}
if (selectedCard.cardId === "swap-meet") {
swapMeetTarget = playerId;
return; // next: pick your item, then theirs
}
const cmd: Parameters<typeof net.command>[0] = {
type: "cast",
instanceId: selectedCard.instanceId,
@@ -827,6 +833,26 @@
dispatch({ type: "endTurn", draw: drawCount });
}
// SWAP MEET: pick one of your carried items, then one of theirs you can
// see (their displayed items — hidden hand cards are not on the table).
let swapMeetTarget = $state<string | null>(null);
let swapMine = $state<string | null>(null);
const myTradables = $derived(view ? view.yourHand.filter((c) => isMovableObject(c.cardId)) : []);
const theirTradables = $derived.by(() => {
if (!view || !swapMeetTarget) return [];
const t = view.players.find((p) => p.id === swapMeetTarget);
return t ? t.displayed.filter((c) => isMovableObject(c.cardId)) : [];
});
function castSwapMeet(theirs: string) {
if (!selectedCard || !swapMeetTarget || !swapMine) return;
dispatch({
type: "cast", instanceId: selectedCard.instanceId,
target: { kind: "player", playerId: swapMeetTarget },
params: { cardId: `${swapMine};${theirs}` },
});
clearSelection();
}
let pickChoice = $state(false);
function pickUp() {
if (treasuresHere.length > 1) { pickChoice = true; return; }
@@ -1673,6 +1699,25 @@
<span> then click the target</span>
{/if}
{/if}
{#if selectedCard?.cardId === "swap-meet"}
{#if !swapMeetTarget}
<span> click the other trader</span>
{:else if myTradables.length === 0}
<span> you carry no items to trade</span>
{:else if !swapMine}
<span> trade away your:</span>
{#each myTradables as c (c.instanceId)}
<button class="stamp tiny" onclick={() => (swapMine = c.cardId)}>{cardDef(c.cardId).name}</button>
{/each}
{:else if theirTradables.length === 0}
<span> they show no item you could claim</span>
{:else}
<span> and take their:</span>
{#each theirTradables as c (c.instanceId)}
<button class="stamp tiny" onclick={() => castSwapMeet(c.cardId)}>{cardDef(c.cardId).name}</button>
{/each}
{/if}
{/if}
{#if selectedCard?.cardId === "waterbolt"}
<label class="inline">damage <input class="num-input" type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
<span>(knockback {numberTotal - wbDamage})</span>
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 25,
deckRev: 26,
};
const { state, events } = createGame(config);
for (const e of events) {