Mad Dash doubles everything it promises (rules rev 12)

'Including NUMBER cards and other add-ons' now means it: a number
riding the cast fuels the dash — (base + number) × 2 — and numbers or
POWER RUN points played under it double as well. The 5BM4 five that
vanished into a borrowed 'burns 0 life for speed' line gets its own
madDash event and chronicle voice. Older games doubled only the base
and replay so, pinned at deckRev 11.

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-30 16:25:30 -04:00
co-authored by Claude Fable 5
parent 6189b4935c
commit 12ece6065a
3 changed files with 70 additions and 4 deletions
+26 -4
View File
@@ -176,6 +176,8 @@ export interface TurnState {
/** 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;
/** MAD DASH: every movement gain this turn doubles — numbers, POWER RUN. */
madDash?: boolean;
}
export interface CastStack {
@@ -236,7 +238,7 @@ export interface CastParams {
}
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
export const CURRENT_RULES_REV = 11;
export const CURRENT_RULES_REV = 12;
export interface GameConfig {
playerIds: PlayerId[];
@@ -278,6 +280,10 @@ export interface GameConfig {
* walked, permanently and a FULL REFLECTION returns a permanent
* curse (SLOW DEATH, WALKING DEAD, IDIOT) onto its caster instead of
* letting it evaporate.
* Rev 12: MAD DASH doubles "NUMBER cards and other add-ons" too a
* 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.
*/
deckRev?: number;
}
@@ -738,6 +744,7 @@ export type GameEvent =
| { type: "cardDisplayed"; player: PlayerId; card: CardInstance }
| { type: "extraTurnGranted"; player: PlayerId }
| { type: "lifeTraded"; player: PlayerId; points: number; newAllowance: number }
| { type: "madDash"; player: PlayerId; newAllowance: number }
| { type: "trapSprung"; player: PlayerId; cardId?: string }
| { type: "died"; player: PlayerId; killedBy: PlayerId | null }
| { type: "handTaken"; from: PlayerId; to: PlayerId; count: number }
@@ -1411,7 +1418,7 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
if (!Number.isInteger(points) || points < 1) return "choose how many life points to trade";
if (points >= caster.life) return "that trade would kill you";
caster.life -= points;
state.turn.movementAllowance += points;
state.turn.movementAllowance += state.turn.madDash ? points * 2 : points;
events.push({ type: "lifeTraded", player: caster.id, points, newAllowance: state.turn.movementAllowance });
return null;
},
@@ -1871,8 +1878,23 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
kind: "neutral",
// "Doubles your movement (including NUMBER cards and other add-ons) for
// one turn. You cannot carry treasures while exerting yourself."
resolve: (state, events, caster) => {
resolve: (state, events, caster, _cmd, magnitude) => {
if (caster.carriedTreasureId) return "you cannot mad-dash while carrying a treasure";
// Rev 12: "including NUMBER cards and other add-ons" — a number
// riding the cast is the turn's movement number, and every movement
// gain after the dash doubles too. Older games doubled only the base
// allowance, ate the rider, and spoke in POWER RUN's voice.
if ((state.config.deckRev ?? 1) >= 12) {
const fuel = magnitude.numberValue ?? 0;
if (fuel > 0) {
if (state.turn.numberPlayedForMovement) return "a number already fuels this turn's movement";
state.turn.numberPlayedForMovement = true;
}
state.turn.madDash = true;
state.turn.movementAllowance = (state.turn.movementAllowance + fuel) * 2;
events.push({ type: "madDash", player: caster.id, newAllowance: state.turn.movementAllowance });
return null;
}
state.turn.movementAllowance *= 2;
events.push({ type: "lifeTraded", player: caster.id, points: 0, newAllowance: state.turn.movementAllowance });
return null;
@@ -4494,7 +4516,7 @@ function doPlayNumberForMovement(prev: GameState, instanceId: string, addInstanc
const value = numberValue(card.cardId) + (displays(p, "powerstone") ? 1 : 0);
state.discard.push(card);
state.turn.movementAllowance += value;
state.turn.movementAllowance += state.turn.madDash ? value * 2 : value;
state.turn.numberPlayedForMovement = true;
return {
ok: true,
@@ -84,6 +84,49 @@ describe("expansion combat cards", () => {
expect(state.players.find((p) => p.id === defender)!.life).toBe(16);
});
it("mad dash doubles the base, the riding number, and later fuel (rev 12)", () => {
let { state } = newGame();
state = toRound2(state);
const me = activePlayer(state).id;
const md = giveCard(state, me, "mad-dash");
giveCard(state, me, "exp1-number-5", "N5", 1);
state = must(state, me, {
type: "cast", instanceId: md.instanceId, numberInstanceIds: ["exp1-number-5#N5"],
});
expect(state.turn.movementAllowance).toBe(16); // (3 + 5) × 2
// The rider was the turn's movement number: a bare second is refused.
giveCard(state, me, "number-2", "N2", 0);
expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2" }).ok).toBe(false);
// POWER RUN points double under the dash too.
const pr = giveCard(state, me, "power-run", "PR", 2);
state = must(state, me, { type: "cast", instanceId: pr.instanceId, params: { points: 2 } });
expect(state.turn.movementAllowance).toBe(20); // 16 + 2×2
});
it("a number played after a bare mad dash doubles as well (rev 12)", () => {
let { state } = newGame();
state = toRound2(state);
const me = activePlayer(state).id;
const md = giveCard(state, me, "mad-dash");
state = must(state, me, { type: "cast", instanceId: md.instanceId });
expect(state.turn.movementAllowance).toBe(6); // 3 × 2
giveCard(state, me, "exp1-number-5", "N5", 0);
state = must(state, me, { type: "playNumberForMovement", instanceId: "exp1-number-5#N5" });
expect(state.turn.movementAllowance).toBe(16); // 6 + 5×2
});
it("older decks double only the base and eat the rider (rev ≤11)", () => {
let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 11 });
state = toRound2(state);
const me = activePlayer(state).id;
const md = giveCard(state, me, "mad-dash");
giveCard(state, me, "exp1-number-5", "N5", 1);
state = must(state, me, {
type: "cast", instanceId: md.instanceId, numberInstanceIds: ["exp1-number-5#N5"],
});
expect(state.turn.movementAllowance).toBe(6); // 3 × 2, the five wasted
});
it("mental swap trades entire hands", () => {
let { state } = newGame();
state = toRound2(state);