From 13ce8c442ccfdf710b25ab9d15dc2d5f7c752c13 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Mon, 17 Aug 2026 09:18:32 -0400 Subject: [PATCH] The clockwork throws more spells and fewer fists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Punching was mostly deck arithmetic — nineteen castable damage cards in a 196-card deck — but three leaks made it worse. The march was burning the biggest NUMBER card on long walks, starving waterbolts and lightning; the biggest number is now reserved whenever an attack in hand wants it. Heave-Ho, priced for defense, was being cast without a treasure to throw (refused, turn stumbled) — now never cast. Illusionary Attack was cast without naming a spell to fake — now it fakes a fireball, the best-selling lie. And STONE DEAD joins the arsenal against anyone flashing displayed stones: number times the visible count, cast when the arithmetic beats the alternatives. Measured across thirty berserker mirrors: attack casts up a quarter, the tier handicap sharper (25-5), no stalls. Co-Authored-By: Claude Fable 5 --- packages/engine/src/automaton.ts | 36 ++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/automaton.ts b/packages/engine/src/automaton.ts index 44db2bb..ffb43d6 100644 --- a/packages/engine/src/automaton.ts +++ b/packages/engine/src/automaton.ts @@ -383,6 +383,9 @@ function bestAttack(view: GameView, targetId: PlayerId, tier: TierTraits): Comma for (const c of view.yourHand) { const atk = ATTACKS[c.cardId]; if (!atk) continue; + // Heave-Ho throws the treasure we are carrying: priced for defense, + // never worth casting. + if (c.cardId === "heave-ho") continue; if (atk.sameSquare && !together) continue; // A wand needs its charges set by a number on first use. const isWand = c.cardId === "blaster-wand"; @@ -401,12 +404,31 @@ function bestAttack(view: GameView, targetId: PlayerId, tier: TierTraits): Comma cmd: { type: "cast", instanceId: c.instanceId, target: { kind: "player", playerId: targetId }, + // An illusion needs a spell to imitate; a fireball sells best. + ...(c.cardId === "illusionary-attack" ? { params: { cardId: "fireball" } } : {}), ...(withNumber ? { numberInstanceIds: [biggest.instanceId] } : {}), ...(amplified ? { amplifyInstanceIds: [amplify.instanceId] } : {}), }, }; } } + // STONE DEAD: number times the stones the victim carries — the displayed + // ones are the visible floor of that count. + const sd = view.yourHand.find((c) => c.cardId === "stone-dead"); + if (sd && biggest) { + const shown = target.displayed.filter((c) => STONES.has(c.cardId)).length; + const dmg = biggestValue * shown; + if (dmg >= 4 && dmg > (best?.damage ?? 0)) { + best = { + damage: dmg, + cmd: { + type: "cast", instanceId: sd.instanceId, + target: { kind: "player", playerId: targetId }, + numberInstanceIds: [biggest.instanceId], + }, + }; + } + } return best?.cmd ?? null; } @@ -690,9 +712,19 @@ export function automatonCommand( const movesLeft = view.turn.movementAllowance - view.turn.movementUsed; if (!view.turn.numberPlayedForMovement && path.distance > movesLeft) { const numbers = numbersInHand(view); - const helper = numbers.find( + // The war chest: when an attack in hand wants a number, the + // biggest one is reserved — a waterbolt unfired outbids a longer + // march, and walking is free next turn. + const wantsNumber = view.yourHand.some((c) => { + const atk = ATTACKS[c.cardId]; + if (!atk) return false; + return atk.perNumber || atk.needsNumber === true || + (c.cardId === "blaster-wand" && view.wandCharges[c.instanceId] == null); + }); + const spendable = wantsNumber ? numbers.slice(0, -1) : numbers; + const helper = spendable.find( (n) => movesLeft + (cardDef(n.cardId).value ?? 0) >= path.distance, - ) ?? (path.distance > movesLeft + 2 ? numbers[numbers.length - 1] : undefined); + ) ?? (path.distance > movesLeft + 2 ? spendable[spendable.length - 1] : undefined); if (helper) { return { type: "playNumberForMovement", instanceId: helper.instanceId }; }