The imp's fire is a spell; the soulstone floor holds (rules rev 8)

Eric flagged both mid-game, and the second one killed me while he
typed: the scorch carried no damage kind, slipping past SOULSTONE
entirely, and the stone's own clamp guarded only from above — a bearer
already at 3 or less had no protection at all, the exact opposite of
"last three points cannot be taken by spell attack." At rev 8 the
floor holds from below (spell damage at or under the floor is simply
immune), and per the FAQ the scorch is a SPELL: it opens a
counteraction window, burns as magical fire, and FULL SHIELD parts it.
One victim's window at a time; the sweep finds the rest next pass.
Older games — including tonight's, where the flaw was fatal — replay
their recorded flames unchanged; all 34 ledgers verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
Eric Wagoner
2026-08-28 23:36:48 -04:00
co-authored by Claude Fable 5
parent 5dde7b4c21
commit 019deedd85
2 changed files with 109 additions and 15 deletions
+39 -9
View File
@@ -236,8 +236,10 @@ export interface CastParams {
* Rev 6: STRENGTH's treasure-tear opens a counteraction window ("this would
* be an attack") instead of resolving instantly.
* Rev 7: DISEASE is a self-cast plague ("You're the carrier!") the caster
* carries it, sharing a square bites both directions, no counteraction. */
export const CURRENT_RULES_REV = 7;
* carries it, sharing a square bites both directions, no counteraction.
* Rev 8: the fire imp's scorch is a SPELL (FAQ) counteractable, magical
* and SOULSTONE's floor holds even at 3 life or below. */
export const CURRENT_RULES_REV = 8;
export interface GameConfig {
playerIds: PlayerId[];
@@ -603,7 +605,7 @@ export type GameEvent =
| { type: "attackResolved"; attacker: PlayerId; defender: PlayerId; attackCardId: string | null; damageDealt: number; reflectedDamage: number; fullyStopped: boolean; redirected: boolean }
| { type: "damaged"; player: PlayerId; amount: number; source: string; lifeAfter: number;
soaks?: { what: "bloodstone" | "soulstone"; amount: number }[] }
| { type: "damageImmune"; player: PlayerId; source: string; because: "medusa" | "bloodstone" }
| { type: "damageImmune"; player: PlayerId; source: string; because: "medusa" | "bloodstone" | "soulstone" }
| { type: "lifeGained"; player: PlayerId; amount: number; source: string; lifeAfter: number }
| { type: "stunned"; player: PlayerId; turnsLost: number }
| { type: "knockedBack"; player: PlayerId; from: Cell; to: Cell; squares: number }
@@ -3010,7 +3012,20 @@ function impCheck(state: GameState, events: GameEvent[], onlyPlayer?: PlayerId):
if (!gameLos(state, imp.position, p.position)) continue;
imp.scorchedThisTurn.push(p.id);
events.push({ type: "impScorches", creatureId: imp.id, player: p.id });
applyDamage(state, events, p, 2, "fire imp", null);
// Rev 8, per the FAQ: every imp attack is technically a spell — the
// scorch opens a counteraction window and burns as MAGICAL fire
// (soulstone's floor holds). One victim's window at a time; the
// sweep finds the rest on its next pass. Older games keep the
// instant kindless burn their ledgers recorded.
if ((state.config.deckRev ?? 1) >= 8) {
if (!state.stack) {
openCreatureStack(state, imp, p, 2, undefined, "spell");
break;
}
imp.scorchedThisTurn.pop(); // window busy: burn on a later sweep
} else {
applyDamage(state, events, p, 2, "fire imp", null);
}
}
}
checkVictory(state, events);
@@ -3168,6 +3183,7 @@ function openCreatureStack(
victim: PlayerState,
damage: number,
touch?: "wraith" | "claw",
kind: "physical" | "spell" = "physical",
): void {
state.stack = {
attackerId: creature.controllerId,
@@ -3178,7 +3194,7 @@ function openCreatureStack(
extendFactor: 1,
powerAttackPoints: 0,
params: { damage },
kind: "physical",
kind,
counters: [],
waitingOn: victim.id,
creatureId: creature.id,
@@ -6213,10 +6229,24 @@ function applyDamage(
soaks.push({ what: "bloodstone", amount: 1 });
}
// SOULSTONE: "Last three points ... can only be lost to physical damage."
if (damageKind === "spell" && displays(target, "soulstone") && target.life > 3) {
const clamped = Math.min(amount, target.life - 3);
if (clamped < amount) soaks.push({ what: "soulstone", amount: amount - clamped });
amount = clamped;
// Rev 8: the floor holds even from below — a bearer already at 3 or
// less takes NOTHING from spells. Earlier revisions clamped only from
// above (a bearer at 1 could be burned to death), and their ledgers
// replay that flaw.
if (damageKind === "spell" && displays(target, "soulstone")) {
if ((state.config.deckRev ?? 1) >= 8) {
const clamped = Math.max(0, Math.min(amount, target.life - 3));
if (clamped === 0) {
events.push({ type: "damageImmune", player: target.id, source, because: "soulstone" });
return;
}
if (clamped < amount) soaks.push({ what: "soulstone", amount: amount - clamped });
amount = clamped;
} else if (target.life > 3) {
const clamped = Math.min(amount, target.life - 3);
if (clamped < amount) soaks.push({ what: "soulstone", amount: amount - clamped });
amount = clamped;
}
}
target.life -= amount;