ADD joins two number cards on movement, as printed

"You may add two NUMBER cards together for any single action" —
movement included. The engine accepts a second movement number when an
ADD accompanies it (once per turn); the client spends your Add
automatically and says so on the button. 120 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 23:41:37 -04:00
co-authored by Claude Fable 5
parent 42ea9b8f2c
commit 89961bbb59
3 changed files with 52 additions and 7 deletions
+20 -4
View File
@@ -133,6 +133,8 @@ export interface TurnState {
secondAttackUsed: boolean;
/** Wand instances already used this turn ("maximum of once per turn"). */
wandsUsed: string[];
/** ADD spent to allow a second movement number card this turn. */
movementAddUsed: boolean;
/** SLOW: "his attacks [reduce] to every other turn". */
attackForbidden: boolean;
actionsEnded: boolean;
@@ -525,7 +527,7 @@ export type CastTarget =
export type Command =
| { type: "move"; direction: Side }
| { type: "playNumberForMovement"; instanceId: string }
| { type: "playNumberForMovement"; instanceId: string; addInstanceId?: string }
| { type: "punch"; targetId: PlayerId }
| { type: "warpStep" }
| { type: "moveCreature"; creatureId: string; direction: Side }
@@ -2948,6 +2950,7 @@ export function createGame(config: GameConfig): { state: GameState; events: Game
attackUsed: false,
secondAttackUsed: false,
wandsUsed: [],
movementAddUsed: false,
attackForbidden: false,
actionsEnded: false,
},
@@ -3062,7 +3065,7 @@ export function applyCommand(state: GameState, playerId: PlayerId, command: Comm
switch (command.type) {
case "move": return doMove(state, command.direction);
case "playNumberForMovement": return doPlayNumberForMovement(state, command.instanceId);
case "playNumberForMovement": return doPlayNumberForMovement(state, command.instanceId, command.addInstanceId);
case "punch": return doPunch(state, command.targetId);
case "warpStep": return doWarpStep(state);
case "moveCreature": return doMoveCreature(state, command.creatureId, command.direction);
@@ -3378,17 +3381,29 @@ function doWarpStep(prev: GameState): CommandResult {
return { ok: true, state, events: [{ type: "warpStepped", player: p.id, from, to: p.position }] };
}
function doPlayNumberForMovement(prev: GameState, instanceId: string): CommandResult {
function doPlayNumberForMovement(prev: GameState, instanceId: string, addInstanceId?: string): CommandResult {
const blocked = requireActionsAvailable(prev);
if (blocked) return err(blocked);
if (prev.turn.numberPlayedForMovement) return err("only one number card may boost movement per turn");
const mover = activePlayer(prev);
if (sustainedOn(prev, mover.id, "slow").length > 0) {
return err("you are slowed — no number cards for movement");
}
// "You may add two NUMBER cards together for any single action" — an ADD
// permits one extra movement number this turn.
if (prev.turn.numberPlayedForMovement) {
if (!addInstanceId) return err("only one number card may boost movement per turn (ADD permits a second)");
if (prev.turn.movementAddUsed) return err("ADD already joined two numbers to your movement");
}
const state = clone(prev);
const p = activePlayer(state);
if (state.turn.numberPlayedForMovement && addInstanceId) {
const addCard = p.hand.find((c) => c.instanceId === addInstanceId);
if (!addCard || addCard.cardId !== "add") return err("ADD card not in hand");
takeFromHand(p, addInstanceId);
state.discard.push(addCard);
state.turn.movementAddUsed = true;
}
const card = takeFromHand(p, instanceId);
if (!card) return err("card not in hand");
if (!isNumberCard(card.cardId)) return err("not a number card");
@@ -4604,6 +4619,7 @@ function beginTurnFor(state: GameState, events: GameEvent[], index: number): voi
attackUsed: false,
secondAttackUsed: false,
wandsUsed: [],
movementAddUsed: false,
attackForbidden,
actionsEnded: false,
};
@@ -246,3 +246,23 @@ describe("thumb of god (divine meteor)", () => {
expect(allObjects.some((c) => c.cardId === "dagger")).toBe(true);
});
});
describe("add for movement", () => {
it("an ADD permits a second number card on movement, once", () => {
let { state } = newGame();
const me = activePlayer(state).id;
giveCard(state, me, "number-3", "N1", 0);
giveCard(state, me, "number-2", "N2", 1);
giveCard(state, me, "add", "A", 2);
giveCard(state, me, "number-4", "N3", 3);
state = must(state, me, { type: "playNumberForMovement", instanceId: "number-3#N1" });
expect(state.turn.movementAllowance).toBe(6);
// Second without ADD: refused.
expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2" }).ok).toBe(false);
// With ADD: allowed.
state = must(state, me, { type: "playNumberForMovement", instanceId: "number-2#N2", addInstanceId: "add#A" });
expect(state.turn.movementAllowance).toBe(8);
// A third is refused even with another add wished for.
expect(applyCommand(state, me, { type: "playNumberForMovement", instanceId: "number-4#N3" }).ok).toBe(false);
});
});