A bare wand lights a single charge

'ANY CARD MAY BE PLAYED WITHOUT A NUMBER CARD, but its power is only 1.'
The wand first-use gate was the engine's one remaining missing-number
refusal — everything else already defaults its power to 1. A wand cast
bare now charges to one, fires once, and crumbles; the client hint says
so. Ungated: the refused casts never reached a ledger.

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-18 10:47:05 -04:00
co-authored by Claude Fable 5
parent 1e5b7ee985
commit 4720ec25ff
3 changed files with 21 additions and 8 deletions
+3 -2
View File
@@ -4523,8 +4523,9 @@ function doCast(prev: GameState, cmd: Extract<Command, { type: "cast" }>): Comma
if (!isWand) return null;
let charges = st.wandCharges[inHand.instanceId];
if (charges === undefined) {
if (mods.numbers.length === 0) return "a wand's first use needs a number card to set its charges";
charges = mods.magnitude.power; // AMPLIFY and ADD both work here
// "ANY CARD MAY BE PLAYED WITHOUT A NUMBER CARD, but its power is
// only 1" — a bare first use lights a single charge.
charges = mods.magnitude.power; // 1 without a number; AMPLIFY and ADD work here
st.wandCharges[inHand.instanceId] = charges;
if (!wielder.displayed.includes(inHand.instanceId)) wielder.displayed.push(inHand.instanceId);
events.push({ type: "wandCharged", player: wielder.id, card: inHand, charges });
+17 -5
View File
@@ -12,11 +12,6 @@ describe("magic wands", () => {
const wand = giveCard(state, attacker, "blaster-wand");
giveCard(state, attacker, "number-2", "N", 1);
// First use without a number card is refused.
expect(applyCommand(state, attacker, {
type: "cast", instanceId: wand.instanceId, target: { kind: "player", playerId: defender },
}).ok).toBe(false);
// Charged with a 2: fires (3 damage), one charge left, wand displayed.
state = must(state, attacker, {
type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-2#N"],
@@ -210,3 +205,20 @@ describe("dropping objects", () => {
expect(refused.ok).toBe(false);
});
});
describe("a bare wand lights a single charge", () => {
it("'played without a number card, its power is only 1': one shot, then dust", () => {
let { state } = newGame();
state = toRound2(state);
const { attacker, defender } = faceOff(state);
const bw = giveCard(state, attacker, "blaster-wand");
state = must(state, attacker, {
type: "cast", instanceId: bw.instanceId, target: { kind: "player", playerId: defender },
});
state = must(state, defender, { type: "pass" });
// The single charge fired (3 flat damage) and the wand crumbled.
expect(state.players.find((p) => p.id === defender)!.life).toBe(12);
expect(state.players.find((p) => p.id === attacker)!.hand.some((c) => c.cardId === "blaster-wand")).toBe(false);
expect(state.wandCharges[bw.instanceId]).toBeUndefined();
});
});
+1 -1
View File
@@ -1818,7 +1818,7 @@
<span>— {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"}</span>
{/if}
{#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber}
<span>— first use: tap a NUMBER card to set the wand's charges, then click a target</span>
<span>— first use sets the charges: tap a NUMBER card first, or cast bare for a single charge</span>
{:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}
<span>— click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}</span>
{/if}