Discard valuation knows whose hand it is; the shed spares the playbook
The end-of-turn shed was eating cards the brain itself casts: BIG MAN, MIST BODY, SHRINK, LIFESAVER, and STONE DEAD all sat at or below the shed threshold. But worth is contextual — LIFESAVER is genuinely dead at a two-seat table, BIG MAN outside the berserker's book — so discardValue now takes the view and style: STONE DEAD joins the attacks tier outright, the rest are kept exactly by the wizard who can play them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
co-authored by
Claude Fable 5
parent
78085e26fb
commit
4e14926c0c
@@ -112,18 +112,25 @@ function numbersInHand(view: GameView): CardInstance[] {
|
|||||||
.sort((a, b) => (cardDef(a.cardId).value ?? 0) - (cardDef(b.cardId).value ?? 0));
|
.sort((a, b) => (cardDef(a.cardId).value ?? 0) - (cardDef(b.cardId).value ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Cards the automaton happily discards to churn its hand, worst first. */
|
/** Cards the automaton happily discards to churn its hand, worst first.
|
||||||
function discardValue(c: CardInstance): number {
|
* A card is only worth keeping to the wizard who can actually play it —
|
||||||
|
* LIFESAVER is dead weight at a two-seat table, BIG MAN outside the
|
||||||
|
* berserker's book — so the valuation knows whose hand this is. */
|
||||||
|
function discardValue(c: CardInstance, view: GameView, style?: AutomatonStyle): number {
|
||||||
const def = cardDef(c.cardId);
|
const def = cardDef(c.cardId);
|
||||||
if (def.cardType === "counteraction" || def.cardType === "neutral/counteraction") return 9;
|
if (def.cardType === "counteraction" || def.cardType === "neutral/counteraction") return 9;
|
||||||
if (SUMMONS.has(c.cardId) || UNLOCKS.includes(c.cardId) || STONES.has(c.cardId)) return 8;
|
if (SUMMONS.has(c.cardId) || UNLOCKS.includes(c.cardId) || STONES.has(c.cardId)) return 8;
|
||||||
if (ATTACKS[c.cardId] != null || AFFLICTIONS[c.cardId] != null) return 7;
|
if (ATTACKS[c.cardId] != null || AFFLICTIONS[c.cardId] != null ||
|
||||||
|
c.cardId === "stone-dead") return 7;
|
||||||
if (c.cardId === "speed" || c.cardId === "interrupt" || c.cardId === "opportunity-fire" ||
|
if (c.cardId === "speed" || c.cardId === "interrupt" || c.cardId === "opportunity-fire" ||
|
||||||
c.cardId === "ward" || c.cardId === "drop-object" || c.cardId === "gift-from-above" ||
|
c.cardId === "ward" || c.cardId === "drop-object" || c.cardId === "gift-from-above" ||
|
||||||
c.cardId === "deja-vu" || c.cardId === "amplify" || c.cardId === "safe" ||
|
c.cardId === "deja-vu" || c.cardId === "amplify" || c.cardId === "safe" ||
|
||||||
c.cardId === "glue" || c.cardId === "destroy-wall" ||
|
c.cardId === "glue" || c.cardId === "destroy-wall" ||
|
||||||
c.cardId === "pass-through-wall" || c.cardId === "create-wall" ||
|
c.cardId === "pass-through-wall" || c.cardId === "create-wall" ||
|
||||||
c.cardId === "fill-square-with-stone" || c.cardId === "thornbush") return 6;
|
c.cardId === "fill-square-with-stone" || c.cardId === "thornbush") return 6;
|
||||||
|
if (c.cardId === "lifesaver" && view.players.length > 2) return 6;
|
||||||
|
if (c.cardId === "big-man" && style === "berserker") return 6;
|
||||||
|
if ((c.cardId === "mist-body" || c.cardId === "shrink") && style === "worrier") return 6;
|
||||||
if (def.cardType === "number") return 4 + (def.value ?? 0);
|
if (def.cardType === "number") return 4 + (def.value ?? 0);
|
||||||
if (def.cardType === "attack") return 3;
|
if (def.cardType === "attack") return 3;
|
||||||
return 2; // situational neutrals go first
|
return 2; // situational neutrals go first
|
||||||
@@ -455,12 +462,12 @@ function overLimit(view: GameView): number {
|
|||||||
* too full to take the draw. doEndTurn caps the draw by the room left, so a
|
* 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
|
* clogged hand never refreshes; only cards the brain has no play for are
|
||||||
* shed (good counters and attacks are hoarded, as a human would). */
|
* shed (good counters and attacks are hoarded, as a human would). */
|
||||||
function endTurnDrawing(view: GameView, tier: TierTraits): Command {
|
function endTurnDrawing(view: GameView, tier: TierTraits, style?: AutomatonStyle): Command {
|
||||||
const deficit = tier.draw - (handLimitOf(view) - view.yourHand.length);
|
const deficit = tier.draw - (handLimitOf(view) - view.yourHand.length);
|
||||||
if (tier.sheds && deficit > 0) {
|
if (tier.sheds && deficit > 0) {
|
||||||
const shed = [...view.yourHand]
|
const shed = [...view.yourHand]
|
||||||
.filter((c) => discardValue(c) <= 3)
|
.filter((c) => discardValue(c, view, style) <= 3)
|
||||||
.sort((a, b) => discardValue(a) - discardValue(b))
|
.sort((a, b) => discardValue(a, view, style) - discardValue(b, view, style))
|
||||||
.slice(0, deficit)
|
.slice(0, deficit)
|
||||||
.map((c) => c.instanceId);
|
.map((c) => c.instanceId);
|
||||||
if (shed.length > 0) return { type: "discard", instanceIds: shed };
|
if (shed.length > 0) return { type: "discard", instanceIds: shed };
|
||||||
@@ -468,9 +475,9 @@ function endTurnDrawing(view: GameView, tier: TierTraits): Command {
|
|||||||
return { type: "endTurn", draw: tier.draw };
|
return { type: "endTurn", draw: tier.draw };
|
||||||
}
|
}
|
||||||
|
|
||||||
function worstCards(view: GameView, n: number): string[] {
|
function worstCards(view: GameView, n: number, style?: AutomatonStyle): string[] {
|
||||||
return [...view.yourHand]
|
return [...view.yourHand]
|
||||||
.sort((a, b) => discardValue(a) - discardValue(b))
|
.sort((a, b) => discardValue(a, view, style) - discardValue(b, view, style))
|
||||||
.slice(0, n)
|
.slice(0, n)
|
||||||
.map((c) => c.instanceId);
|
.map((c) => c.instanceId);
|
||||||
}
|
}
|
||||||
@@ -807,7 +814,7 @@ export function automatonCommand(
|
|||||||
if (view.phase !== "playing") return null;
|
if (view.phase !== "playing") return null;
|
||||||
|
|
||||||
if (view.pendingDiscard === you) {
|
if (view.pendingDiscard === you) {
|
||||||
return { type: "discard", instanceIds: worstCards(view, Math.max(1, overLimit(view))) };
|
return { type: "discard", instanceIds: worstCards(view, Math.max(1, overLimit(view)), style) };
|
||||||
}
|
}
|
||||||
if (view.chaosPending && view.chaosPending.queue[0] === you) {
|
if (view.chaosPending && view.chaosPending.queue[0] === you) {
|
||||||
const shield = inHand(view, "full-shield");
|
const shield = inHand(view, "full-shield");
|
||||||
@@ -824,7 +831,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 endTurnDrawing(view, tier);
|
if (view.turn.actionsEnded) return endTurnDrawing(view, tier, style);
|
||||||
|
|
||||||
// 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" };
|
||||||
@@ -988,7 +995,7 @@ export function automatonCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return endTurnDrawing(view, tier);
|
return endTurnDrawing(view, tier, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The safe fallback when the automaton's choice was refused. */
|
/** The safe fallback when the automaton's choice was refused. */
|
||||||
|
|||||||
Reference in New Issue
Block a user