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. */
+39 -1
View File
@@ -123,10 +123,13 @@ describe("the clockwork does not waste counters on pointless targets", () => {
// burn round 1
for (let i = 0; i < 2; i++) {
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(`setup: ${r.error}`);
state = r.state;
}
while (actingSeat(state) !== "human") {
state = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 }).state;
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(`setup: ${r.error}`);
state = r.state;
}
const human = state.players.find((p: { id: string }) => p.id === "human")!;
const bot = state.players.find((p: { id: string }) => p.id === "bot")!;
@@ -165,3 +168,38 @@ describe("the clockwork does not waste counters on pointless targets", () => {
expect(cmd).toEqual({ type: "counteract", instanceId: "full-shield#T" });
});
});
describe("a clogged hand gets shed, not hoarded", () => {
it("the bot discards dead weight so the end-of-turn draw has room", () => {
let { state } = createGame({ playerIds: ["bot", "other"], seed: 42, sets: ["basic"], deckRev: 13 });
while (actingSeat(state) !== "bot") {
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
if (!r.ok) throw new Error(r.error);
state = r.state;
}
const bot = state.players.find((p) => p.id === "bot")!;
// Seven situational neutrals the brain has no play for: a dead hand.
bot.hand = Array.from({ length: 7 }, (_, i) => (
{ instanceId: `illusion-wall#${i}`, cardId: "illusion-wall" }
));
// Walk the bot's turn until it wants to end: it must shed before drawing.
for (let guard = 0; guard < 30; guard++) {
const view = viewFor(state, "bot");
const cmd = automatonCommand(view, "hunter", "archmage")!;
if (cmd.type === "discard") {
expect(cmd.instanceIds.length).toBe(2);
const r = applyCommand(state, "bot", cmd);
if (!r.ok) throw new Error(r.error);
state = r.state;
const next = automatonCommand(viewFor(state, "bot"), "hunter", "archmage")!;
expect(next).toEqual({ type: "endTurn", draw: 2 });
return;
}
if (cmd.type === "endTurn") throw new Error("ended the turn without shedding a dead hand");
const r = applyCommand(state, "bot", cmd);
if (!r.ok) throw new Error(`${cmd.type}: ${r.error}`);
state = r.state;
}
throw new Error("never reached the end of the turn");
});
});