Counteraction answers follow the FAQ: absorb reflections, not shields
"It's not letting me cast Absorb Spell on your Full Shield" — and Jolly's FAQ says exactly why: "ABSORB SPELL will work on FULL REFLECTION or REFLECTION, but not FULL SHIELD, as that is not used against you, but cast upon the player using it." The refusal stands, now with that reason in the error — and the neighboring case the engine wrongly refused is fixed: an attacker may answer a reflection with ABSORB SPELL, nullifying it and taking the reflection card into their hand, with the original spell then landing unturned. And the card face corrects my own hours-old work: ANTI-ANTI "does not work against escape, such as SHRINK, TELEPORT, or INVISIBLE" — so under rules rev 6 it can no longer pin a teleport escape. Earlier games keep their stored chains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
31a727d02c
commit
de446c2ad5
@@ -216,7 +216,8 @@ export interface GameConfig {
|
||||
* and CHAOS honors FULL SHIELD sit-outs and refuses REFLECTIONS. Rev 4:
|
||||
* creature blows open a counteraction window like any attack. Rev 5:
|
||||
* BIG MAN pushes occupants ahead, steps over floor hazards for 2 points,
|
||||
* and bars monsters from his square.
|
||||
* and bars monsters from his square. Rev 6: ANTI-ANTI cannot pin a
|
||||
* teleport escape ("does not work against escape" — the card face).
|
||||
*/
|
||||
deckRev?: number;
|
||||
}
|
||||
@@ -4621,9 +4622,40 @@ function doCounteract(prev: GameState, playerId: PlayerId, instanceId: string, p
|
||||
}
|
||||
|
||||
if (playerId === stack.attackerId) {
|
||||
if (card.cardId !== "anti-anti") return err("only ANTI-ANTI can counteract a counteraction");
|
||||
const targetCounter = [...stack.counters].reverse().find((c) => !c.nullified);
|
||||
// ABSORB SPELL "will work on other COUNTERACTIONS" — but only ones used
|
||||
// against YOU: "will work on FULL REFLECTION or REFLECTION, but not FULL
|
||||
// SHIELD, as that is not used against you" (official FAQ).
|
||||
if (card.cardId === "absorb-spell") {
|
||||
if (!targetCounter) return err("no counteraction to absorb");
|
||||
const id = targetCounter.card.cardId;
|
||||
if (id !== "reflection" && id !== "full-reflection") {
|
||||
return err(`${cardDef(id).name} is not cast at you — it cannot be absorbed`);
|
||||
}
|
||||
takeFromHand(player, instanceId);
|
||||
state.discard.push(card);
|
||||
targetCounter.nullified = true;
|
||||
// The absorbed reflection leaves the discard for the absorber's hand.
|
||||
const di = state.discard.findIndex((c) => c.instanceId === targetCounter.card.instanceId);
|
||||
if (di !== -1) {
|
||||
state.discard.splice(di, 1);
|
||||
player.hand.push(targetCounter.card);
|
||||
if (player.hand.length > handLimit(player)) state.pendingDiscard = player.id;
|
||||
}
|
||||
stack.waitingOn = stack.defenderId;
|
||||
return {
|
||||
ok: true,
|
||||
state,
|
||||
events: [{ type: "counterNullified", player: targetCounter.player, card: targetCounter.card, by: card }],
|
||||
};
|
||||
}
|
||||
if (card.cardId !== "anti-anti") return err("only ANTI-ANTI or ABSORB SPELL can answer a counteraction");
|
||||
if (!targetCounter) return err("no counteraction to nullify");
|
||||
// "Does not work against escape, such as SHRINK, TELEPORT, or INVISIBLE"
|
||||
// (the card face; rules rev 6 — earlier games replay their old chains).
|
||||
if ((state.config.deckRev ?? 1) >= 6 && targetCounter.card.cardId === "teleport") {
|
||||
return err("ANTI-ANTI does not work against escape");
|
||||
}
|
||||
takeFromHand(player, instanceId);
|
||||
state.discard.push(card);
|
||||
targetCounter.nullified = true;
|
||||
|
||||
@@ -555,7 +555,7 @@ describe("teleport as a counteraction", () => {
|
||||
expect(cellKey(after.position)).toBe(cellKey(escape));
|
||||
});
|
||||
|
||||
it("ANTI-ANTI pins the boots: the escape never happens and the spell lands", () => {
|
||||
it("ANTI-ANTI pins the boots in pre-rev-6 games (their stored chains replay)", () => {
|
||||
let { state } = newGame();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
@@ -670,3 +670,67 @@ describe("slime holds spells", () => {
|
||||
expect(Object.keys(state.slimeTraps).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("answering counteractions (FAQ rulings)", () => {
|
||||
function rev6() {
|
||||
return createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic"], deckRev: 6 });
|
||||
}
|
||||
|
||||
it("ABSORB SPELL steals a FULL REFLECTION out of the air", () => {
|
||||
let { state } = rev6();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const fb = giveCard(state, attacker, "fireball");
|
||||
const ab = giveCard(state, attacker, "absorb-spell", "AB", 1);
|
||||
giveCard(state, defender, "full-reflection", "FR", 0);
|
||||
state = must(state, attacker, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender },
|
||||
});
|
||||
state = must(state, defender, { type: "counteract", instanceId: "full-reflection#FR" });
|
||||
state = must(state, attacker, { type: "counteract", instanceId: ab.instanceId });
|
||||
state = must(state, defender, { type: "pass" });
|
||||
const a = state.players.find((p) => p.id === attacker)!;
|
||||
const d = state.players.find((p) => p.id === defender)!;
|
||||
expect(a.hand.some((c) => c.instanceId === "full-reflection#FR")).toBe(true);
|
||||
expect(a.life).toBe(15); // nothing reflected
|
||||
expect(d.life).toBe(10); // the fireball lands after all
|
||||
});
|
||||
|
||||
it("FULL SHIELD cannot be absorbed — it is not cast at you", () => {
|
||||
let { state } = rev6();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const fb = giveCard(state, attacker, "fireball");
|
||||
const ab = giveCard(state, attacker, "absorb-spell", "AB", 1);
|
||||
giveCard(state, defender, "full-shield", "FS", 0);
|
||||
state = must(state, attacker, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender },
|
||||
});
|
||||
state = must(state, defender, { type: "counteract", instanceId: "full-shield#FS" });
|
||||
const r = applyCommand(state, attacker, { type: "counteract", instanceId: ab.instanceId });
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.error).toMatch(/cannot be absorbed/);
|
||||
});
|
||||
|
||||
it("ANTI-ANTI does not work against a teleport escape (rev 6)", () => {
|
||||
let { state } = rev6();
|
||||
state = toRound2(state);
|
||||
const { attacker, defender } = faceOff(state);
|
||||
const fb = giveCard(state, attacker, "fireball");
|
||||
const aa = giveCard(state, attacker, "anti-anti", "AA", 1);
|
||||
const tp = giveCard(state, defender, "teleport", "TP", 0);
|
||||
const d = state.players.find((p) => p.id === defender)!;
|
||||
const escape = { x: d.position.x, y: d.position.y + 1 };
|
||||
state = must(state, attacker, {
|
||||
type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender },
|
||||
});
|
||||
state = must(state, defender, { type: "counteract", instanceId: tp.instanceId, params: { cell: escape } });
|
||||
const r = applyCommand(state, attacker, { type: "counteract", instanceId: aa.instanceId });
|
||||
expect(r.ok).toBe(false);
|
||||
state = must(state, attacker, { type: "pass" });
|
||||
state = must(state, defender, { type: "pass" });
|
||||
const after = state.players.find((p) => p.id === defender)!;
|
||||
expect(after.life).toBe(15);
|
||||
expect(cellKey(after.position)).toBe(cellKey(escape));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface Room {
|
||||
const rooms = new Map<string, Room>();
|
||||
|
||||
/** Rules revision new games are dealt under (stored games keep their own). */
|
||||
const RULES_REV = 5;
|
||||
const RULES_REV = 6;
|
||||
|
||||
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class LocalGame {
|
||||
seed,
|
||||
sets: expansion ? ["basic", "expansion1"] : ["basic"],
|
||||
...(colors ? { colors } : {}),
|
||||
deckRev: 5,
|
||||
deckRev: 6,
|
||||
};
|
||||
const { state, events } = createGame(config);
|
||||
this.config = config;
|
||||
|
||||
Reference in New Issue
Block a user