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:
co-authored by
Claude Fable 5
parent
6189b4935c
commit
12ece6065a
@@ -176,6 +176,8 @@ export interface TurnState {
|
|||||||
/** A reflected LIGHTNING BLAST stuns mid-turn: no end-of-turn draw either
|
/** 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"). */
|
* (FAQ: "you lose the rest of your turn and can't draw cards"). */
|
||||||
drawForbidden?: boolean;
|
drawForbidden?: boolean;
|
||||||
|
/** MAD DASH: every movement gain this turn doubles — numbers, POWER RUN. */
|
||||||
|
madDash?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CastStack {
|
export interface CastStack {
|
||||||
@@ -236,7 +238,7 @@ export interface CastParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The revision new games are dealt under; GameConfig.deckRev pins it per game. */
|
/** 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 {
|
export interface GameConfig {
|
||||||
playerIds: PlayerId[];
|
playerIds: PlayerId[];
|
||||||
@@ -278,6 +280,10 @@ export interface GameConfig {
|
|||||||
* walked, permanently — and a FULL REFLECTION returns a permanent
|
* walked, permanently — and a FULL REFLECTION returns a permanent
|
||||||
* curse (SLOW DEATH, WALKING DEAD, IDIOT) onto its caster instead of
|
* curse (SLOW DEATH, WALKING DEAD, IDIOT) onto its caster instead of
|
||||||
* letting it evaporate.
|
* 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;
|
deckRev?: number;
|
||||||
}
|
}
|
||||||
@@ -738,6 +744,7 @@ export type GameEvent =
|
|||||||
| { type: "cardDisplayed"; player: PlayerId; card: CardInstance }
|
| { type: "cardDisplayed"; player: PlayerId; card: CardInstance }
|
||||||
| { type: "extraTurnGranted"; player: PlayerId }
|
| { type: "extraTurnGranted"; player: PlayerId }
|
||||||
| { type: "lifeTraded"; player: PlayerId; points: number; newAllowance: number }
|
| { type: "lifeTraded"; player: PlayerId; points: number; newAllowance: number }
|
||||||
|
| { type: "madDash"; player: PlayerId; newAllowance: number }
|
||||||
| { type: "trapSprung"; player: PlayerId; cardId?: string }
|
| { type: "trapSprung"; player: PlayerId; cardId?: string }
|
||||||
| { type: "died"; player: PlayerId; killedBy: PlayerId | null }
|
| { type: "died"; player: PlayerId; killedBy: PlayerId | null }
|
||||||
| { type: "handTaken"; from: PlayerId; to: PlayerId; count: number }
|
| { 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 (!Number.isInteger(points) || points < 1) return "choose how many life points to trade";
|
||||||
if (points >= caster.life) return "that trade would kill you";
|
if (points >= caster.life) return "that trade would kill you";
|
||||||
caster.life -= points;
|
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 });
|
events.push({ type: "lifeTraded", player: caster.id, points, newAllowance: state.turn.movementAllowance });
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@@ -1871,8 +1878,23 @@ const CARD_EFFECTS: Record<string, AttackEffect | NeutralEffect | CounterEffect>
|
|||||||
kind: "neutral",
|
kind: "neutral",
|
||||||
// "Doubles your movement (including NUMBER cards and other add-ons) for
|
// "Doubles your movement (including NUMBER cards and other add-ons) for
|
||||||
// one turn. You cannot carry treasures while exerting yourself."
|
// 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";
|
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;
|
state.turn.movementAllowance *= 2;
|
||||||
events.push({ type: "lifeTraded", player: caster.id, points: 0, newAllowance: state.turn.movementAllowance });
|
events.push({ type: "lifeTraded", player: caster.id, points: 0, newAllowance: state.turn.movementAllowance });
|
||||||
return null;
|
return null;
|
||||||
@@ -4494,7 +4516,7 @@ function doPlayNumberForMovement(prev: GameState, instanceId: string, addInstanc
|
|||||||
|
|
||||||
const value = numberValue(card.cardId) + (displays(p, "powerstone") ? 1 : 0);
|
const value = numberValue(card.cardId) + (displays(p, "powerstone") ? 1 : 0);
|
||||||
state.discard.push(card);
|
state.discard.push(card);
|
||||||
state.turn.movementAllowance += value;
|
state.turn.movementAllowance += state.turn.madDash ? value * 2 : value;
|
||||||
state.turn.numberPlayedForMovement = true;
|
state.turn.numberPlayedForMovement = true;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -84,6 +84,49 @@ describe("expansion combat cards", () => {
|
|||||||
expect(state.players.find((p) => p.id === defender)!.life).toBe(16);
|
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", () => {
|
it("mental swap trades entire hands", () => {
|
||||||
let { state } = newGame();
|
let { state } = newGame();
|
||||||
state = toRound2(state);
|
state = toRound2(state);
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export function humanize(e: GameEvent): string | null {
|
|||||||
? `${e.player}'s bloodstone drinks the whole blow — no damage.`
|
? `${e.player}'s bloodstone drinks the whole blow — no damage.`
|
||||||
: `${e.player} is stone — the damage has no effect.`;
|
: `${e.player} is stone — the damage has no effect.`;
|
||||||
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
case "lifeGained": return `${e.player} gains ${e.amount} life (${e.source}) — now ${e.lifeAfter}.`;
|
||||||
|
case "madDash": return `${e.player} MAD DASHES — every stride doubles: ${e.newAllowance} movement this turn!`;
|
||||||
case "spellSustained": return `${spellName(e.cardId)} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
case "spellSustained": return `${spellName(e.cardId)} settles over ${e.target} (${e.turns} turn${e.turns === 1 ? "" : "s"}).`;
|
||||||
case "spellExpired": return `${spellName(e.cardId)} wears off ${e.target}.`;
|
case "spellExpired": return `${spellName(e.cardId)} wears off ${e.target}.`;
|
||||||
case "teleported": return e.by === e.player
|
case "teleported": return e.by === e.player
|
||||||
|
|||||||
Reference in New Issue
Block a user