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:
co-authored by
Claude Fable 5
parent
9493873b64
commit
ef5336caa8
@@ -31,13 +31,15 @@ interface TierTraits {
|
|||||||
amplify: boolean;
|
amplify: boolean;
|
||||||
ambush: boolean;
|
ambush: boolean;
|
||||||
guardGold: boolean;
|
guardGold: boolean;
|
||||||
|
/** Shed dead cards at end of turn to keep the draw flowing. */
|
||||||
|
sheds: boolean;
|
||||||
buffs: boolean;
|
buffs: boolean;
|
||||||
dejaVu: boolean;
|
dejaVu: boolean;
|
||||||
}
|
}
|
||||||
const TIERS: Record<AutomatonTier, TierTraits> = {
|
const TIERS: Record<AutomatonTier, TierTraits> = {
|
||||||
apprentice: { draw: 1, counterThrift: 2, afflictions: false, amplify: false, ambush: false, guardGold: false, buffs: false, dejaVu: false },
|
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 },
|
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 },
|
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. */
|
/** 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);
|
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[] {
|
function worstCards(view: GameView, n: number): string[] {
|
||||||
return [...view.yourHand]
|
return [...view.yourHand]
|
||||||
.sort((a, b) => discardValue(a) - discardValue(b))
|
.sort((a, b) => discardValue(a) - discardValue(b))
|
||||||
@@ -558,7 +579,7 @@ export function automatonCommand(
|
|||||||
const self = me(view);
|
const self = me(view);
|
||||||
const here = cellKey(self.position);
|
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.
|
// Deliver or grab treasure underfoot.
|
||||||
if (self.carriedTreasureId && here === cellKey(self.home)) return { type: "dropTreasure" };
|
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. */
|
/** The safe fallback when the automaton's choice was refused. */
|
||||||
|
|||||||
@@ -123,10 +123,13 @@ describe("the clockwork does not waste counters on pointless targets", () => {
|
|||||||
// burn round 1
|
// burn round 1
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
const r = applyCommand(state, actingSeat(state), { type: "endTurn", draw: 0 });
|
||||||
|
if (!r.ok) throw new Error(`setup: ${r.error}`);
|
||||||
state = r.state;
|
state = r.state;
|
||||||
}
|
}
|
||||||
while (actingSeat(state) !== "human") {
|
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 human = state.players.find((p: { id: string }) => p.id === "human")!;
|
||||||
const bot = state.players.find((p: { id: string }) => p.id === "bot")!;
|
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" });
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user