The clockwork reads absorb's fine print
An automaton answered NO SPELL with an Absorb — which "has no effect on duration-based spells." Afflictions sat outside the price table and drew the phantom two-point default, tripping the point-soaking rungs. An affliction's weight is now its duration, and the two counters that touch only points — ABSORB and REVERSE — stand aside for it, leaving the ones that genuinely bite durations: full shield, remove curse, blunt, reflection, shieldstone numbers. Pinned: under NO SPELL with both in hand, the bot blunts, never absorbs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
adf463effa
commit
ea3b9e5bd6
@@ -315,16 +315,19 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma
|
|||||||
return { type: "pass" };
|
return { type: "pass" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An affliction's weight is its duration, not points it does not carry.
|
||||||
const incoming = stack.attackCard
|
const incoming = stack.attackCard
|
||||||
? attackId === "stone-dead"
|
? attackId === "stone-dead"
|
||||||
// NUMBER times the stones in the victim's hand — our hand.
|
// NUMBER times the stones in the victim's hand — our hand.
|
||||||
? (stack.numberValue ?? 1) * view.yourHand.filter((c) => STONES.has(c.cardId)).length
|
? (stack.numberValue ?? 1) * view.yourHand.filter((c) => STONES.has(c.cardId)).length
|
||||||
|
: affliction
|
||||||
|
? Math.max(2, stack.numberValue ?? 2)
|
||||||
: (atk ? atk.base + (atk.perNumber ? stack.numberValue ?? 1 : 0) : 2)
|
: (atk ? atk.base + (atk.perNumber ? stack.numberValue ?? 1 : 0) : 2)
|
||||||
: 1;
|
: 1;
|
||||||
if (stack.kind === "spell") {
|
if (stack.kind === "spell") {
|
||||||
// REVERSE turns the biggest blasts into a meal.
|
// REVERSE eats points; a pure duration offers it nothing.
|
||||||
const reverse = find("reverse");
|
const reverse = find("reverse");
|
||||||
if (reverse && incoming >= 4 - flinch) return { type: "counteract", instanceId: reverse.instanceId };
|
if (reverse && !affliction && incoming >= 4 - flinch) return { type: "counteract", instanceId: reverse.instanceId };
|
||||||
// ABSORB SPELL steals the good ones for later.
|
// ABSORB SPELL steals the good ones for later.
|
||||||
const absorbSpell = find("absorb-spell");
|
const absorbSpell = find("absorb-spell");
|
||||||
if (absorbSpell && incoming >= 3) return { type: "counteract", instanceId: absorbSpell.instanceId };
|
if (absorbSpell && incoming >= 3) return { type: "counteract", instanceId: absorbSpell.instanceId };
|
||||||
@@ -342,8 +345,10 @@ function respond(view: GameView, style: AutomatonStyle, tier: TierTraits): Comma
|
|||||||
const half = find("reflection");
|
const half = find("reflection");
|
||||||
if (half && incoming >= 3) return { type: "counteract", instanceId: half.instanceId };
|
if (half && incoming >= 3) return { type: "counteract", instanceId: half.instanceId };
|
||||||
}
|
}
|
||||||
|
// ABSORB soaks points only ("no effect on duration-based spells");
|
||||||
|
// BLUNT and the stones work on durations too.
|
||||||
const absorb = find("absorb");
|
const absorb = find("absorb");
|
||||||
if (absorb && incoming >= 2 - flinch && incoming <= 3) return { type: "counteract", instanceId: absorb.instanceId };
|
if (absorb && !affliction && incoming >= 2 - flinch && incoming <= 3) return { type: "counteract", instanceId: absorb.instanceId };
|
||||||
const blunt = find("blunt");
|
const blunt = find("blunt");
|
||||||
if (blunt && incoming >= 2 - flinch) return { type: "counteract", instanceId: blunt.instanceId };
|
if (blunt && incoming >= 2 - flinch) return { type: "counteract", instanceId: blunt.instanceId };
|
||||||
// The berserker shares its pain out of spite.
|
// The berserker shares its pain out of spite.
|
||||||
|
|||||||
@@ -117,8 +117,7 @@ describe("automaton vs automaton", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("the clockwork does not waste counters on pointless targets", () => {
|
function underAttackShared(attackId: string, defenderHand: { instanceId: string; cardId: string }[], rig?: (s: GameState, defender: string) => void) {
|
||||||
function underAttack(attackId: string, defenderHand: { instanceId: string; cardId: string }[], rig?: (s: GameState, defender: string) => void) {
|
|
||||||
let { state } = createGame({ playerIds: ["human", "bot"], seed: 42, sets: ["basic", "expansion1"], deckRev: 13 });
|
let { state } = createGame({ playerIds: ["human", "bot"], seed: 42, sets: ["basic", "expansion1"], deckRev: 13 });
|
||||||
// burn round 1
|
// burn round 1
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
@@ -145,8 +144,9 @@ describe("the clockwork does not waste counters on pointless targets", () => {
|
|||||||
return r.state;
|
return r.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe("the clockwork does not waste counters on pointless targets", () => {
|
||||||
it("passes on drop-object rather than absorbing nothing", () => {
|
it("passes on drop-object rather than absorbing nothing", () => {
|
||||||
const state = underAttack("drop-object", [
|
const state = underAttackShared("drop-object", [
|
||||||
{ instanceId: "absorb#T", cardId: "absorb" },
|
{ instanceId: "absorb#T", cardId: "absorb" },
|
||||||
{ instanceId: "blunt#T", cardId: "blunt" },
|
{ instanceId: "blunt#T", cardId: "blunt" },
|
||||||
{ instanceId: "dagger#T", cardId: "dagger" },
|
{ instanceId: "dagger#T", cardId: "dagger" },
|
||||||
@@ -156,7 +156,7 @@ describe("the clockwork does not waste counters on pointless targets", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shields a drop-object aimed at its carried treasure", () => {
|
it("shields a drop-object aimed at its carried treasure", () => {
|
||||||
const state = underAttack("drop-object", [
|
const state = underAttackShared("drop-object", [
|
||||||
{ instanceId: "full-shield#T", cardId: "full-shield" },
|
{ instanceId: "full-shield#T", cardId: "full-shield" },
|
||||||
{ instanceId: "dagger#T", cardId: "dagger" },
|
{ instanceId: "dagger#T", cardId: "dagger" },
|
||||||
], (s, defender) => {
|
], (s, defender) => {
|
||||||
@@ -203,3 +203,14 @@ describe("a clogged hand gets shed, not hoarded", () => {
|
|||||||
throw new Error("never reached the end of the turn");
|
throw new Error("never reached the end of the turn");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("the clockwork honors absorb's fine print", () => {
|
||||||
|
it("answers NO SPELL with blunt, never absorb — durations soak no points", () => {
|
||||||
|
const state = underAttackShared("no-spell", [
|
||||||
|
{ instanceId: "absorb#T", cardId: "absorb" },
|
||||||
|
{ instanceId: "blunt#T", cardId: "blunt" },
|
||||||
|
]);
|
||||||
|
const cmd = automatonCommand(viewFor(state, "bot"), "hunter", "archmage");
|
||||||
|
expect(cmd).toEqual({ type: "counteract", instanceId: "blunt#T" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user