The clockwork learns to shed a dead hand — if its tier knows how

Two archmages idled turn after turn: hands clogged with situational
neutrals, and doEndTurn caps the draw by the room left, so a full
hand never refreshed. Nothing to cast, nothing to draw, nothing done.

The brain now sheds dead weight before ending the turn — only cards
it has no play for; good counters and attacks stay hoarded — so the
draw always has room. The trick is repertoire, so it is tier-gated:
adepts and archmages churn, the apprentice hoards like the novice it
is. Measured across forty berserker mirrors that sharpened the
handicap from two-to-one to three-to-one with zero stalled games.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 22:11:18 -04:00
co-authored by Claude Fable 5
parent 9493873b64
commit ef5336caa8
2 changed files with 65 additions and 6 deletions
+26 -5
View File
@@ -31,13 +31,15 @@ interface TierTraits {
amplify: boolean;
ambush: boolean;
guardGold: boolean;
/** Shed dead cards at end of turn to keep the draw flowing. */
sheds: boolean;
buffs: boolean;
dejaVu: boolean;
}
const TIERS: Record<AutomatonTier, TierTraits> = {
apprentice: { draw: 1, counterThrift: 2, afflictions: false, amplify: false, ambush: false, guardGold: false, buffs: false, dejaVu: false },
adept: { draw: 2, counterThrift: 0, afflictions: true, amplify: false, ambush: false, guardGold: true, buffs: true, dejaVu: true },
archmage: { draw: 2, counterThrift: 0, afflictions: true, amplify: true, ambush: true, guardGold: true, buffs: true, dejaVu: true },
apprentice: { draw: 1, counterThrift: 2, afflictions: false, amplify: false, ambush: false, guardGold: false, buffs: false, dejaVu: false, sheds: false },
adept: { draw: 2, counterThrift: 0, afflictions: true, amplify: false, ambush: false, guardGold: true, buffs: true, dejaVu: true, sheds: true },
archmage: { draw: 2, counterThrift: 0, afflictions: true, amplify: true, ambush: true, guardGold: true, buffs: true, dejaVu: true, sheds: true },
};
/** Damage attacks the clockwork understands: flat damage plus per-number scaling. */
@@ -234,6 +236,25 @@ function overLimit(view: GameView): number {
return Math.max(0, view.yourHand.length - 7);
}
/** End the turn drawing fully — shedding dead weight first if the hand is
* too full to take the draw. doEndTurn caps the draw by the room left, so a
* clogged hand never refreshes; only cards the brain has no play for are
* shed (good counters and attacks are hoarded, as a human would). */
function endTurnDrawing(view: GameView, tier: TierTraits): Command {
const limit = me(view).displayed.some((c) => c.cardId === "brainstone") ? 9 : 7;
const deficit = tier.draw - (limit - view.yourHand.length);
if (tier.sheds && deficit > 0) {
const shed = [...view.yourHand]
.filter((c) => discardValue(c) <= 3)
.sort((a, b) => discardValue(a) - discardValue(b))
.slice(0, deficit)
.map((c) => c.instanceId);
if (shed.length > 0) return { type: "discard", instanceIds: shed };
}
return { type: "endTurn", draw: tier.draw };
}
function worstCards(view: GameView, n: number): string[] {
return [...view.yourHand]
.sort((a, b) => discardValue(a) - discardValue(b))
@@ -558,7 +579,7 @@ export function automatonCommand(
const self = me(view);
const here = cellKey(self.position);
if (view.turn.actionsEnded) return { type: "endTurn", draw: tier.draw };
if (view.turn.actionsEnded) return endTurnDrawing(view, tier);
// Deliver or grab treasure underfoot.
if (self.carriedTreasureId && here === cellKey(self.home)) return { type: "dropTreasure" };
@@ -681,7 +702,7 @@ export function automatonCommand(
}
}
return { type: "endTurn", draw: tier.draw };
return endTurnDrawing(view, tier);
}
/** The safe fallback when the automaton's choice was refused. */