Reflected Swap Meet: the reflector chooses the trade (rev 27)

'FULL REFLECTION lets the other player decide which objects, if any,
will be swapped' — the reflector's choice now rides their counteract
(params.cardId, reflector's item first), resolved with the roles
swapped when the reflection settles; 'none' or no choice trades
nothing. The client walks the reflector through the same give-and-take
picker, with a 'swap nothing' refusal.

Also from room XRT7: the trade picker now honors each room's own rules
revision (older rooms trade only object-typed cards), so it can no
longer offer a wizardblade a rev-25 engine will quietly refuse — and a
fizzled swap finally says so in the log instead of vanishing.

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:36:00 -04:00
co-authored by Claude Fable 5
parent 3aef6694f3
commit 2bb2ca62d8
6 changed files with 161 additions and 12 deletions
+69 -3
View File
@@ -383,6 +383,14 @@
attachedNumber = attachedNumber?.instanceId === card.instanceId ? null : card;
return;
}
// Fully reflecting a SWAP MEET: "lets the other player decide which
// objects, if any, will be swapped" — the choice rides the counter.
if (youMustRespond && card.cardId === "full-reflection" &&
view.stack?.attackCard?.cardId === "swap-meet" && view.deckRev >= 27) {
reflectSwapCard = card;
reflectSwapMine = null;
return;
}
dispatch({
type: "counteract", instanceId: card.instanceId,
...(attachedNumber ? { numberInstanceIds: [attachedNumber.instanceId] } : {}),
@@ -838,11 +846,55 @@
// hand cards are not on the table).
let swapMeetTarget = $state<string | null>(null);
let swapMine = $state<string | null>(null);
// Reflecting a SWAP MEET: the reflector picks the trade — or none at all.
let reflectSwapCard = $state<CardInstance | null>(null);
let reflectSwapMine = $state<string | null>(null);
const reflectMyTradables = $derived.by(() => {
if (!view || !reflectSwapCard) return [];
const opts = new Map<string, string>();
for (const c of view.yourHand) {
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
}
if (me?.carriedTreasureId) {
const t = view.treasures.find((t) => t.id === me.carriedTreasureId);
opts.set("treasure", `${t?.owner ?? "the"}'s treasure (carried)`);
}
return [...opts].map(([key, label]) => ({ key, label }));
});
const reflectTheirTradables = $derived.by(() => {
if (!view || !reflectSwapCard || !view.stack) return [];
const them = view.players.find((p) => p.id === view.stack!.attackerId);
if (!them) return [];
const opts = new Map<string, string>();
for (const c of them.displayed) {
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
}
if (them.carriedTreasureId && (reflectSwapMine === "treasure" || !me?.carriedTreasureId)) {
const t = view.treasures.find((t) => t.id === them.carriedTreasureId);
opts.set("treasure", `${t?.owner ?? "the"}'s treasure (carried)`);
}
return [...opts].map(([key, label]) => ({ key, label }));
});
function castReflectSwap(pair: string) {
if (!reflectSwapCard) return;
dispatch({
type: "counteract", instanceId: reflectSwapCard.instanceId,
params: { cardId: pair },
});
reflectSwapCard = null;
reflectSwapMine = null;
}
// Older rooms' rules trade only object-typed cards (rev < 26): the picker
// must offer exactly what that room's engine will honor.
function tradableHere(cardId: string): boolean {
if (!view) return false;
return view.deckRev >= 26 ? isMovableObject(cardId) : cardDef(cardId).cardType === "object";
}
const myTradables = $derived.by(() => {
if (!view) return [];
const opts = new Map<string, string>();
for (const c of view.yourHand) {
if (isMovableObject(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
}
if (me?.carriedTreasureId) {
const t = view.treasures.find((t) => t.id === me.carriedTreasureId);
@@ -856,7 +908,7 @@
if (!them) return [];
const opts = new Map<string, string>();
for (const c of them.displayed) {
if (isMovableObject(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
if (tradableHere(c.cardId)) opts.set(c.cardId, cardDef(c.cardId).name);
}
// Their treasure is claimable unless your arms are already full.
if (them.carriedTreasureId && (swapMine === "treasure" || !me?.carriedTreasureId)) {
@@ -1460,7 +1512,21 @@
<div class="slip winner">🏆 {view.winner} wins!</div>
{:else if youMustRespond}
<div class="slip urgent">
{#if view.stack?.defenderId === view.you}
{#if reflectSwapCard}
Your reflection turns the trade over to you —
{#if !reflectSwapMine}
give your:
{#each reflectMyTradables as c (c.key)}
<button class="stamp tiny" onclick={() => (reflectSwapMine = c.key)}>{c.label}</button>
{/each}
{:else}
and take their:
{#each reflectTheirTradables as c (c.key)}
<button class="stamp tiny" onclick={() => castReflectSwap(`${reflectSwapMine};${c.key}`)}>{c.label}</button>
{/each}
{/if}
<button class="hint-cancel" onclick={() => castReflectSwap("none")}>swap nothing</button>
{:else if view.stack?.defenderId === view.you}
Under attack — respond by your hand.
{:else}
Your spell is countered — respond by your hand.
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 26,
deckRev: 27,
};
const { state, events } = createGame(config);
for (const e of events) {
+1
View File
@@ -137,6 +137,7 @@ export function humanize(e: GameEvent): string | null {
case "illusionBelieved": return e.believed ? `${e.player} flinches — the illusion feels real!` : `${e.player} laughs off the illusion.`;
case "itemStolen": return `${e.to} picks ${e.from}'s pocket.`;
case "itemsSwapped": return `${e.a} and ${e.b} swap items.`;
case "swapFizzled": return `${e.player}'s trade comes to nothing — the named items were not there to swap.`;
case "wardSprung": return `${e.owner}'s treasure was WARDED — it bites ${e.victim}!`;
case "curseRemoved": return `${e.caster} lifts a curse from ${e.target}.`;
case "objectEnchanted": return `An object gleams with Swarthmore's enchantment.`;