The clockwork stops absorbing nothing

An automaton answered DROP OBJECT with an Absorb, then a Blunt —
two counters drained against a spell that carries no points. The
brain's price table listed only the classic damage spells; anything
unknown was guessed at 2 points, which tripped every damage-counter
rung. Utility attacks (dropping, dragging, swapping, peeking) now
route past those rungs entirely: the bot passes, or spends a full
stop when the effect actually threatens something — a shield for the
treasure it carries, a worrier's refusal to be dragged. Stone Dead,
Butt Head, Heave-Ho, and Illusionary Attack join the price table
while the hood is up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 21:39:34 -04:00
co-authored by Claude Fable 5
parent 8a460a0eed
commit 2950380e23
2 changed files with 74 additions and 1 deletions
+25 -1
View File
@@ -53,8 +53,18 @@ const ATTACKS: Record<string, { base: number; perNumber: boolean; needsNumber?:
"large-rock": { base: 2, perNumber: false },
"blaster-wand": { base: 3, perNumber: false },
disease: { base: 3, perNumber: false, sameSquare: true },
"butt-head": { base: 2, perNumber: false },
"heave-ho": { base: 2, perNumber: false },
"illusionary-attack": { base: 3, perNumber: false },
};
/** Attacks that carry no points: dropping, dragging, swapping, peeking.
* A damage counter drains against nothing — only a full stop denies them. */
const UTILITY_ATTACKS = new Set([
"drop-object", "teleport-opponent", "mental-force", "mental-swap", "swap",
"swap-meet", "card-erasure", "telepath", "sticky-wand", "shift-wand",
]);
/**
* Afflictions cast at an enemy: no damage, but a turn of misery. Cast with a
* number for the duration where one helps.
@@ -269,8 +279,22 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma
const attackId = stack.attackCard?.cardId ?? null;
const atk = attackId ? ATTACKS[attackId] : null;
const affliction = attackId ? AFFLICTIONS[attackId] != null : false;
if (attackId && UTILITY_ATTACKS.has(attackId)) {
const stopper = find("full-shield") ?? find("force-field");
const stakes =
(attackId === "drop-object" && me(view).carriedTreasureId != null) ||
((attackId === "teleport-opponent" || attackId === "mental-force" || attackId === "swap") &&
style === "worrier");
if (stopper && stakes) return { type: "counteract", instanceId: stopper.instanceId };
return { type: "pass" };
}
const incoming = stack.attackCard
? (atk ? atk.base + (atk.perNumber ? stack.numberValue ?? 1 : 0) : 2)
? attackId === "stone-dead"
// NUMBER times the stones in the victim's hand — our hand.
? (stack.numberValue ?? 1) * view.yourHand.filter((c) => STONES.has(c.cardId)).length
: (atk ? atk.base + (atk.perNumber ? stack.numberValue ?? 1 : 0) : 2)
: 1;
if (stack.kind === "spell") {
// REVERSE turns the biggest blasts into a meal.