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 }; }