Slow Death's draw is one blow, countered against its total (table ruling)
Two cards drawn is a two-point blow: ABSORB soaks up to three of the total, BLUNT halves it rounding up (so it finally earns a place — 2 becomes 1), counters may chain, declining takes the rest. The client labels each counter with the exact points it leaves; the bots weigh the total against their floor. Amends rev 13 minutes after it shipped; every production ledger still replays clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
92613d7386
commit
8cfd911a3e
@@ -1276,12 +1276,17 @@ export function automatonCommand(
|
||||
|
||||
if (view.slowDeathPending) {
|
||||
if (view.slowDeathPending.playerId !== you) return null;
|
||||
// Soak only when the bites reach toward the floor — the card is worth
|
||||
// more against a fireball than one point of rot.
|
||||
const absorb = inHand(view, "absorb");
|
||||
// Counter only when the total is worth a card or the floor is near —
|
||||
// an Absorb is worth more against a fireball than one point of rot.
|
||||
const pts = view.slowDeathPending.points;
|
||||
if (absorb && me(view).life - pts <= 4) {
|
||||
return { type: "slowDeathChoice", absorbInstanceId: absorb.instanceId };
|
||||
const pressed = me(view).life - pts <= 4;
|
||||
const absorb = inHand(view, "absorb");
|
||||
if (absorb && (pts >= 2 || pressed)) {
|
||||
return { type: "slowDeathChoice", counterInstanceId: absorb.instanceId };
|
||||
}
|
||||
const blunt = inHand(view, "blunt");
|
||||
if (blunt && pts >= 2 && pressed) {
|
||||
return { type: "slowDeathChoice", counterInstanceId: blunt.instanceId };
|
||||
}
|
||||
return { type: "slowDeathChoice" };
|
||||
}
|
||||
|
||||
+34
-24
@@ -284,9 +284,10 @@ export interface GameConfig {
|
||||
* number riding the cast fuels it, and numbers or POWER RUN points
|
||||
* played under the dash double; older games doubled only the base
|
||||
* allowance and consumed the rider without effect.
|
||||
* Rev 13: SLOW DEATH's bites pause for a victim holding ABSORB — each
|
||||
* absorb soaks exactly one point (FAQ: "only stop the damage incurred
|
||||
* by one card"); older games bit instantly.
|
||||
* Rev 13: SLOW DEATH's per-draw bites land as ONE blow that pauses
|
||||
* for a victim holding ABSORB or BLUNT — countered against the total
|
||||
* (absorb soaks up to 3, blunt halves rounding up); older games bit
|
||||
* instantly.
|
||||
*/
|
||||
deckRev?: number;
|
||||
}
|
||||
@@ -304,8 +305,9 @@ export interface GameState {
|
||||
/** A treasure was just grabbed and its owner holds WARD:
|
||||
* the table waits while they choose to play it "at that time" or not. */
|
||||
wardPending: { ownerId: PlayerId; takerId: PlayerId } | null;
|
||||
/** SLOW DEATH's bites hang while the victim decides whether an ABSORB
|
||||
* soaks a point ("only stop the damage incurred by one card"). */
|
||||
/** SLOW DEATH's bites hang while the victim weighs counteractions:
|
||||
* the draw's total is one blow — ABSORB soaks up to three of it,
|
||||
* BLUNT halves it rounding up (table ruling). */
|
||||
slowDeathPending: { playerId: PlayerId; points: number } | null;
|
||||
/** CHAOS is landing: each queued player may play FULL SHIELD to sit out. */
|
||||
chaosPending: { casterId: PlayerId; excluded: PlayerId[]; queue: PlayerId[] } | null;
|
||||
@@ -752,7 +754,7 @@ export type GameEvent =
|
||||
| { type: "madDash"; player: PlayerId; newAllowance: number }
|
||||
| { type: "safeOpened"; player: PlayerId; cell: Cell; withCardId: string }
|
||||
| { type: "slowDeathWindow"; player: PlayerId; points: number }
|
||||
| { type: "slowDeathAbsorbed"; player: PlayerId; remaining: number }
|
||||
| { type: "slowDeathCountered"; player: PlayerId; cardId: string; remaining: number }
|
||||
| { type: "safeDamaged"; attacker: PlayerId; cell: Cell; amount: number; total: number }
|
||||
| { type: "safeSmashed"; attacker: PlayerId; cell: Cell }
|
||||
| { type: "trapSprung"; player: PlayerId; cardId?: string }
|
||||
@@ -796,7 +798,7 @@ export type Command =
|
||||
| { type: "testIllusion"; cell: Cell; side: Side }
|
||||
| { type: "armWard" }
|
||||
| { type: "wardChoice"; play: boolean }
|
||||
| { type: "slowDeathChoice"; absorbInstanceId?: string }
|
||||
| { type: "slowDeathChoice"; counterInstanceId?: string }
|
||||
| { type: "warpStep" }
|
||||
| { type: "moveCreature"; creatureId: string; direction: Side }
|
||||
| { type: "creatureWarpStep"; creatureId: string }
|
||||
@@ -3901,8 +3903,8 @@ function applyCommandInner(state: GameState, playerId: PlayerId, command: Comman
|
||||
// SLOW DEATH's window: the bites hang while the victim weighs an absorb.
|
||||
if (state.slowDeathPending) {
|
||||
if (playerId !== state.slowDeathPending.playerId) return err("waiting on the slow death's victim");
|
||||
if (command.type !== "slowDeathChoice") return err("soak a point with an Absorb or take the bites");
|
||||
return doSlowDeathChoice(state, playerId, command.absorbInstanceId);
|
||||
if (command.type !== "slowDeathChoice") return err("counter the bites or take them");
|
||||
return doSlowDeathChoice(state, playerId, command.counterInstanceId);
|
||||
}
|
||||
|
||||
if (state.pendingDiscard) {
|
||||
@@ -6704,10 +6706,12 @@ function applySlowDeathOnDraw(state: GameState, events: GameEvent[], p: PlayerSt
|
||||
}
|
||||
const bites = cardsDrawn * (curses.length - blessed);
|
||||
if (bites === 0) return;
|
||||
// Rev 13, per the FAQ: ABSORB "will only stop the damage incurred by one
|
||||
// card" — one point, one bite. A victim holding an ABSORB gets the choice
|
||||
// before the bites land. Older games bit instantly and replay so.
|
||||
if ((state.config.deckRev ?? 1) >= 13 && p.hand.some((c) => c.cardId === "absorb")) {
|
||||
// Rev 13 (table ruling): the draw's bites land as ONE blow, countered
|
||||
// against the total — ABSORB soaks up to three, BLUNT halves rounding
|
||||
// up. A victim holding either gets the window; older games bit
|
||||
// instantly and replay so.
|
||||
if ((state.config.deckRev ?? 1) >= 13 &&
|
||||
p.hand.some((c) => c.cardId === "absorb" || c.cardId === "blunt")) {
|
||||
state.slowDeathPending = { playerId: p.id, points: bites };
|
||||
events.push({ type: "slowDeathWindow", player: p.id, points: bites });
|
||||
return;
|
||||
@@ -6719,22 +6723,28 @@ function applySlowDeathOnDraw(state: GameState, events: GameEvent[], p: PlayerSt
|
||||
checkVictory(state, events);
|
||||
}
|
||||
|
||||
/** The victim's answer to hanging SLOW DEATH bites: each ABSORB soaks
|
||||
* exactly one point ("the damage incurred by one card" is one bite);
|
||||
* declining, or running out of absorbs, takes the rest. */
|
||||
function doSlowDeathChoice(prev: GameState, playerId: PlayerId, absorbInstanceId?: string): CommandResult {
|
||||
/** The victim's answer to hanging SLOW DEATH bites. The batch is one
|
||||
* blow countered against its total: ABSORB soaks up to three points,
|
||||
* BLUNT halves it rounding up, and counters may chain as they would
|
||||
* against any attack. Declining takes what remains. */
|
||||
function doSlowDeathChoice(prev: GameState, playerId: PlayerId, counterInstanceId?: string): CommandResult {
|
||||
const state = clone(prev);
|
||||
const pending = state.slowDeathPending!;
|
||||
const p = state.players.find((q) => q.id === playerId)!;
|
||||
const events: GameEvent[] = [];
|
||||
if (absorbInstanceId) {
|
||||
const card = p.hand.find((c) => c.instanceId === absorbInstanceId);
|
||||
if (!card || card.cardId !== "absorb") return err("that is not an Absorb in your hand");
|
||||
takeFromHand(p, absorbInstanceId);
|
||||
if (counterInstanceId) {
|
||||
const card = p.hand.find((c) => c.instanceId === counterInstanceId);
|
||||
if (!card || (card.cardId !== "absorb" && card.cardId !== "blunt")) {
|
||||
return err("only an Absorb or a Blunt answers the rot");
|
||||
}
|
||||
takeFromHand(p, counterInstanceId);
|
||||
state.discard.push(card);
|
||||
pending.points -= 1;
|
||||
events.push({ type: "slowDeathAbsorbed", player: p.id, remaining: pending.points });
|
||||
if (pending.points > 0 && p.hand.some((c) => c.cardId === "absorb")) {
|
||||
pending.points = card.cardId === "absorb"
|
||||
? Math.max(0, pending.points - 3)
|
||||
: Math.ceil(pending.points / 2);
|
||||
events.push({ type: "slowDeathCountered", player: p.id, cardId: card.cardId, remaining: pending.points });
|
||||
if (pending.points > 0 &&
|
||||
p.hand.some((c) => c.cardId === "absorb" || c.cardId === "blunt")) {
|
||||
return { ok: true, state, events };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ describe("slow death", () => {
|
||||
expect(sustainedOn(state, defender, "slow-death").length).toBe(1);
|
||||
});
|
||||
|
||||
it("an absorb in hand pauses the bites; each soaks one point (rev 13)", () => {
|
||||
it("the draw's bites land as one blow: an absorb soaks up to three (rev 13)", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
@@ -159,31 +159,50 @@ describe("slow death", () => {
|
||||
state = must(state, defender, { type: "discard", instanceIds: toDiscard });
|
||||
giveCard(state, defender, "absorb", "AB", 0);
|
||||
state = must(state, defender, { type: "endTurn", draw: 2 });
|
||||
// Two bites hang; nobody else may act.
|
||||
// Two bites hang as one blow; nobody else may act.
|
||||
expect(state.slowDeathPending).toEqual({ playerId: defender, points: 2 });
|
||||
expect(applyCommand(state, attacker, { type: "endTurn", draw: 0 }).ok).toBe(false);
|
||||
// One absorb soaks one point; the other lands.
|
||||
state = must(state, defender, { type: "slowDeathChoice", absorbInstanceId: "absorb#AB" });
|
||||
// One absorb soaks the whole total (up to three).
|
||||
state = must(state, defender, { type: "slowDeathChoice", counterInstanceId: "absorb#AB" });
|
||||
expect(state.slowDeathPending).toBeNull();
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(14);
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(15);
|
||||
expect(state.players.find((p) => p.id === defender)!.hand.some((c) => c.cardId === "absorb")).toBe(false);
|
||||
});
|
||||
|
||||
it("without an absorb the bites land instantly, as in older decks", () => {
|
||||
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 12 });
|
||||
it("a blunt halves the blow's total, rounding up (rev 13)", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const sd = giveCard(state, attacker, "slow-death");
|
||||
state = castAt(state, attacker, defender, sd);
|
||||
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
||||
const d = state.players.find((p) => p.id === defender)!;
|
||||
giveCard(state, defender, "absorb", "AB", 0);
|
||||
const toDiscard = d.hand.filter((c) => c.cardId !== "absorb").slice(0, 2).map((c) => c.instanceId);
|
||||
const toDiscard = d.hand.slice(0, 2).map((c) => c.instanceId);
|
||||
state = must(state, defender, { type: "discard", instanceIds: toDiscard });
|
||||
giveCard(state, defender, "blunt", "BL", 0);
|
||||
state = must(state, defender, { type: "endTurn", draw: 2 });
|
||||
// Rev 12: even with an absorb in hand, the old decks bite instantly.
|
||||
expect(state.slowDeathPending ?? null).toBeNull();
|
||||
expect(state.slowDeathPending).toEqual({ playerId: defender, points: 2 });
|
||||
state = must(state, defender, { type: "slowDeathChoice", counterInstanceId: "blunt#BL" });
|
||||
// ceil(2 / 2) = 1 lands.
|
||||
expect(state.slowDeathPending).toBeNull();
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(14);
|
||||
});
|
||||
|
||||
it("declining the window takes the whole total", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const sd = giveCard(state, attacker, "slow-death");
|
||||
state = castAt(state, attacker, defender, sd);
|
||||
state = must(state, attacker, { type: "endTurn", draw: 0 });
|
||||
const d = state.players.find((p) => p.id === defender)!;
|
||||
const toDiscard = d.hand.slice(0, 2).map((c) => c.instanceId);
|
||||
state = must(state, defender, { type: "discard", instanceIds: toDiscard });
|
||||
giveCard(state, defender, "absorb", "AB", 0);
|
||||
state = must(state, defender, { type: "endTurn", draw: 2 });
|
||||
state = must(state, defender, { type: "slowDeathChoice" });
|
||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(13);
|
||||
expect(state.players.find((p) => p.id === defender)!.hand.some((c) => c.cardId === "absorb")).toBe(true);
|
||||
});
|
||||
|
||||
it("a reverse at the casting turns the curse: a point gained per draw (rev 11)", () => {
|
||||
|
||||
Reference in New Issue
Block a user