A reflected Lightning Blast ends the caster's turn on the spot (rev 28)

FAQ: 'If this gets FULLY REFLECTED, you lose the rest of your turn and
cannot draw cards (in addition to damage).' When the returned bolt
stuns its own caster mid-turn, their remaining actions end at once and
the end-of-turn draw comes up empty (new turn.drawForbidden flag); the
normal stun still costs them the next turn. The affected player sees
'Your own bolt has left you reeling' in place of the usual spent-turn
slip. Older revisions played on and stored games replay so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
This commit is contained in:
Eric Wagoner
2026-08-17 23:47:25 -04:00
co-authored by Claude Fable 5
parent 67157ad3a2
commit 1d7407cb20
5 changed files with 62 additions and 4 deletions
+13 -1
View File
@@ -167,6 +167,9 @@ export interface TurnState {
/** SLOW: "his attacks [reduce] to every other turn". */
attackForbidden: boolean;
actionsEnded: boolean;
/** A reflected LIGHTNING BLAST stuns mid-turn: no end-of-turn draw either
* (FAQ: "you lose the rest of your turn and can't draw cards"). */
drawForbidden?: boolean;
}
export interface CastStack {
@@ -845,6 +848,15 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (ctx.damageDealt <= 0 || !ctx.defender.alive) return;
ctx.defender.lostTurns++;
ctx.events.push({ type: "stunned", player: ctx.defender.id, turnsLost: 1 });
// Stunned mid-own-turn (the bolt came back): FAQ — "If this gets FULLY
// REFLECTED, you lose the rest of your turn and can't draw cards (in
// addition to damage)." Rules rev 28; older games played on and replay so.
const st = ctx.state;
if ((st.config.deckRev ?? 1) >= 28 &&
st.players[st.turn.activeIndex]!.id === ctx.defender.id) {
st.turn.actionsEnded = true;
st.turn.drawForbidden = true;
}
},
},
powerthrust: { kind: "attack", requiresLos: true, baseDamage: (n) => 2 + (n ?? 0) },
@@ -5955,7 +5967,7 @@ function doEndTurn(prev: GameState, draw: number): CommandResult {
const events: GameEvent[] = [];
const room = handLimit(p) - p.hand.length;
const count = Math.min(draw, room);
const count = state.turn.drawForbidden ? 0 : Math.min(draw, room);
if (count > 0) {
const drawn: CardInstance[] = [];
let toDraw = count;
+42
View File
@@ -1023,3 +1023,45 @@ describe("the reflected-attack window (rules rev 25)", () => {
expect(state.players.find((p) => p.id === attacker)!.life).toBe(5);
});
});
describe("a reflected lightning blast ends the caster's turn (rules rev 28)", () => {
function boltRig(deckRev: number) {
let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic"], deckRev });
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const lb = giveCard(state, attacker, "lightning-blast");
giveCard(state, attacker, "number-3", "N", 1);
giveCard(state, defender, "full-reflection", "FR", 0);
state = must(state, attacker, {
type: "cast", instanceId: lb.instanceId, numberInstanceIds: ["number-3#N"],
target: { kind: "player", playerId: defender },
});
state = must(state, defender, { type: "counteract", instanceId: "full-reflection#FR" });
state = must(state, attacker, { type: "pass" });
// rev 25+: the returned bolt opens its window; the caster declines to answer.
if (state.stack) state = must(state, attacker, { type: "pass" });
return { state, attacker, defender };
}
it("FAQ: 'you lose the rest of your turn and can't draw cards'", () => {
let { state, attacker } = boltRig(28);
expect(state.players.find((p) => p.id === attacker)!.life).toBe(12);
expect(state.turn.actionsEnded).toBe(true);
// No more marching, and the end-of-turn draw comes up empty.
expect(applyCommand(state, attacker, { type: "move", direction: "N" }).ok).toBe(false);
const before = state.players.find((p) => p.id === attacker)!.hand.length;
state = must(state, attacker, { type: "endTurn", draw: 2 });
expect(state.players.find((p) => p.id === attacker)!.hand.length).toBe(before);
// And the stun still costs the next turn.
expect(state.players.find((p) => p.id === attacker)!.lostTurns
+ (activePlayer(state).id === attacker ? 1 : 0)).toBeGreaterThanOrEqual(1);
});
it("rev 27 keeps the old behavior: the caster plays on and draws", () => {
let { state, attacker } = boltRig(27);
expect(state.turn.actionsEnded).toBe(false);
const before = state.players.find((p) => p.id === attacker)!.hand.length;
state = must(state, attacker, { type: "endTurn", draw: 2 });
expect(state.players.find((p) => p.id === attacker)!.hand.length).toBeGreaterThan(before);
});
});
+1 -1
View File
@@ -54,7 +54,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 = 27;
const RULES_REV = 28;
const ROOM_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+5 -1
View File
@@ -1587,7 +1587,11 @@
</div>
{/if}
{#if actionsSpent}
<div class="slip">Your spellwork is spent for this turn — all that remains is to draw and end it.</div>
{#if view.turn.drawForbidden}
<div class="slip urgent">Your own bolt has left you reeling — the rest of this turn is lost, and no cards come with it.</div>
{:else}
<div class="slip">Your spellwork is spent for this turn — all that remains is to draw and end it.</div>
{/if}
{/if}
{#if view.yourAmbushes.length > 0}
+1 -1
View File
@@ -136,7 +136,7 @@ class LocalGame {
seed,
sets: expansion ? ["basic", "expansion1"] : ["basic"],
...(colors ? { colors } : {}),
deckRev: 27,
deckRev: 28,
};
const { state, events } = createGame(config);
for (const e of events) {