import { describe, expect, it } from "vitest"; import { applyCommand, activePlayer, boardView } from "../src/game"; import { cellKey, edgeKey, SIDES, type Cell, type Side } from "../src/board"; import type { CardInstance } from "../src/cards"; import { newExpansionGame as newGame, must, giveCard, toRound2, faceOff } from "./helpers"; describe("magic wands", () => { it("blaster wand: charges on first use, once per turn, discards when spent", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); 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"], target: { kind: "player", playerId: defender }, }); state = must(state, defender, { type: "pass" }); expect(state.players.find((p) => p.id === defender)!.life).toBe(12); expect(state.wandCharges[wand.instanceId]).toBe(1); const a = state.players.find((p) => p.id === attacker)!; expect(a.displayed).toContain(wand.instanceId); // A second use the same turn is refused ("once per turn") — even with // adrenaline-style second attacks it is the WAND that is limited. expect(applyCommand(state, attacker, { type: "cast", instanceId: wand.instanceId, target: { kind: "player", playerId: defender }, }).ok).toBe(false); // Next turn: the last charge fires and the wand crumbles to the discard. state = must(state, attacker, { type: "endTurn", draw: 0 }); state = must(state, defender, { type: "endTurn", draw: 0 }); state = must(state, attacker, { type: "cast", instanceId: wand.instanceId, target: { kind: "player", playerId: defender }, }); state = must(state, defender, { type: "pass" }); expect(state.players.find((p) => p.id === defender)!.life).toBe(9); const a2 = state.players.find((p) => p.id === attacker)!; expect(a2.hand.some((c) => c.instanceId === wand.instanceId)).toBe(false); expect(state.wandCharges[wand.instanceId]).toBeUndefined(); }); it("wands fire even under NO SPELL, and Absorb Spell cannot eat them", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); // Defender silences the attacker first: give defender the first move. state.sustained.push({ id: "fx-ns", cardId: "no-spell", casterId: defender, targetId: attacker, remainingTurns: 3, data: {}, }); const wand = giveCard(state, attacker, "blaster-wand"); giveCard(state, attacker, "number-2", "N", 1); giveCard(state, defender, "absorb-spell", "AS", 0); // Fireball is silenced... const fb = giveCard(state, attacker, "fireball", "F", 2); expect(applyCommand(state, attacker, { type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender }, }).ok).toBe(false); // ...but the wand is isolated from the wizard. state = must(state, attacker, { type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-2#N"], target: { kind: "player", playerId: defender }, }); // Absorb Spell has no effect on magic wands. expect(applyCommand(state, defender, { type: "counteract", instanceId: "absorb-spell#AS" }).ok).toBe(false); state = must(state, defender, { type: "pass" }); expect(state.players.find((p) => p.id === defender)!.life).toBe(12); }); it("sticky wand webs the target: movement -3 and fire burns hotter", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); const wand = giveCard(state, attacker, "sticky-wand"); giveCard(state, attacker, "number-3", "N", 1); state = must(state, attacker, { type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-3#N"], target: { kind: "player", playerId: defender }, }); state = must(state, defender, { type: "pass" }); state = must(state, attacker, { type: "endTurn", draw: 0 }); // Webbed: base 3 movement drops to 0. expect(activePlayer(state).id).toBe(defender); expect(state.turn.movementAllowance).toBe(0); }); it("shift wand shoves a wizard through a wall", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); const d = state.players.find((p) => p.id === defender)!; // Find a walled direction beside the defender with a real cell behind it. const view = boardView(state); let through: Cell | null = null; for (const side of SIDES) { const k = edgeKey(d.position, side); const dest = { x: d.position.x + (side === "E" ? 1 : side === "W" ? -1 : 0), y: d.position.y + (side === "S" ? 1 : side === "N" ? -1 : 0) }; if (view.edges[k] === "wall" && view.cells[cellKey(dest)]) { through = dest; break; } } if (!through) throw new Error("setup: seed 42 lost its adjacent wall"); const wand = giveCard(state, attacker, "shift-wand"); giveCard(state, attacker, "number-2", "N", 1); state = must(state, attacker, { type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-2#N"], target: { kind: "player", playerId: defender }, params: { cell: through }, }); state = must(state, defender, { type: "pass" }); expect(cellKey(state.players.find((p) => p.id === defender)!.position)).toBe(cellKey(through)); }); it("warp wand opens a wall for one turn only", () => { let { state } = newGame(); const me = activePlayer(state); const view = boardView(state); let edge: { cell: Cell; side: Side } | null = null; for (const side of SIDES) { const k = edgeKey(me.position, side); const dest = { x: me.position.x + (side === "E" ? 1 : side === "W" ? -1 : 0), y: me.position.y + (side === "S" ? 1 : side === "N" ? -1 : 0) }; if (view.edges[k] === "wall" && view.cells[cellKey(dest)]) { edge = { cell: me.position, side }; break; } } if (!edge) throw new Error("setup: seed 42 lost its adjacent wall"); const key = edgeKey(edge.cell, edge.side); const wand = giveCard(state, me.id, "warp-wand"); giveCard(state, me.id, "number-2", "N", 1); state = must(state, me.id, { type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-2#N"], target: { kind: "edge", cell: edge.cell, side: edge.side }, }); // Open now: walk through. state = must(state, me.id, { type: "move", direction: edge.side }); // At end of turn the wall reappears. state = must(state, me.id, { type: "endTurn", draw: 0 }); expect(boardView(state).edges[key]).toBe("wall"); }); it("deja-vu retrieves from the discard, but never a wand", () => { let { state } = newGame(); const me = activePlayer(state); state.discard.push({ instanceId: "fireball#D", cardId: "fireball" }); state.discard.push({ instanceId: "blaster-wand#D", cardId: "blaster-wand" }); const dv = giveCard(state, me.id, "deja-vu"); expect(applyCommand(state, me.id, { type: "cast", instanceId: dv.instanceId, params: { cardId: "blaster-wand" }, }).ok).toBe(false); state = must(state, me.id, { type: "cast", instanceId: dv.instanceId, params: { cardId: "fireball" } }); expect(state.players.find((p) => p.id === me.id)!.hand.some((c) => c.cardId === "fireball")).toBe(true); }); }); describe("dropping objects", () => { it("a charged wand can be dropped and picked up, charges intact", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); const wand = giveCard(state, attacker, "blaster-wand"); giveCard(state, attacker, "number-3", "N", 1); // Charge it with a 3 (one charge spent on the shot, two remain). state = must(state, attacker, { type: "cast", instanceId: wand.instanceId, numberInstanceIds: ["number-3#N"], target: { kind: "player", playerId: defender }, }); state = must(state, defender, { type: "pass" }); expect(state.wandCharges[wand.instanceId]).toBe(2); // Set it down: leaves the hand and the displayed list, lands on the floor. state = must(state, attacker, { type: "dropObject", instanceId: wand.instanceId }); const owner = state.players.find((p) => p.id === attacker)!; expect(owner.hand.some((c) => c.instanceId === wand.instanceId)).toBe(false); expect(owner.displayed.includes(wand.instanceId)).toBe(false); const key = cellKey(owner.position); expect((state.groundObjects[key] ?? []).some((c) => c.instanceId === wand.instanceId)).toBe(true); // The wizard standing there picks it up on their turn — charges travel. state = must(state, attacker, { type: "endTurn", draw: 0 }); state = must(state, defender, { type: "pickUpObject", instanceId: wand.instanceId }); const thief = state.players.find((p) => p.id === defender)!; expect(thief.hand.some((c) => c.instanceId === wand.instanceId)).toBe(true); expect(state.wandCharges[wand.instanceId]).toBe(2); expect(state.players.find((p) => p.id === defender)!.id).toBe(activePlayer(state).id); }); it("the dagger and wizardblade can be set down; spell cards cannot", () => { let { state } = newGame(); state = toRound2(state); const who = activePlayer(state).id; const dagger = giveCard(state, who, "dagger"); const fireball = giveCard(state, who, "fireball", "F", 1); state = must(state, who, { type: "dropObject", instanceId: dagger.instanceId }); expect((state.groundObjects[cellKey(activePlayer(state).position)] ?? []) .some((c) => c.cardId === "dagger")).toBe(true); const refused = applyCommand(state, who, { type: "dropObject", instanceId: fireball.instanceId }); expect(refused.ok).toBe(false); }); });