import { describe, expect, it } from "vitest"; import { applyCommand, activePlayer, displays, handLimit, sustainedOn, type GameState, type PlayerId } from "../src/game"; import type { CardInstance } from "../src/cards"; import { newGame, must, giveCard, toRound2, faceOff, castAt } from "./helpers"; /** Display a stone for a player during their turn. */ function displayStone(state: GameState, playerId: PlayerId, stoneId: string, slot = 0): GameState { const stone = giveCard(state, playerId, stoneId, "S" + stoneId, slot); return must(state, playerId, { type: "cast", instanceId: stone.instanceId }); } describe("magic stones", () => { it("bloodstone shaves one point off every hit", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); // Defender displays bloodstone on their own turn first. state = must(state, attacker, { type: "endTurn", draw: 0 }); state = displayStone(state, defender, "bloodstone"); expect(displays(state.players.find((p) => p.id === defender)!, "bloodstone")).toBe(true); state = must(state, defender, { type: "endTurn", draw: 0 }); const fb = giveCard(state, attacker, "fireball", "F", 1); state = castAt(state, attacker, defender, fb); expect(state.players.find((p) => p.id === defender)!.life).toBe(11); // 5-1 }); it("soulstone keeps the last three points safe from spells", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); state = must(state, attacker, { type: "endTurn", draw: 0 }); state = displayStone(state, defender, "soulstone"); const d = state.players.find((p) => p.id === defender)!; d.life = 5; state = must(state, defender, { type: "endTurn", draw: 0 }); const fb = giveCard(state, attacker, "fireball", "F", 1); state = castAt(state, attacker, defender, fb); expect(state.players.find((p) => p.id === defender)!.life).toBe(3); // floored, not 0 // But a punch (physical) can finish the job past the floor. state = must(state, attacker, { type: "endTurn", draw: 0 }); state = must(state, defender, { type: "endTurn", draw: 0 }); state = must(state, attacker, { type: "punch", targetId: defender }); state = must(state, defender, { type: "pass" }); expect(state.players.find((p) => p.id === defender)!.life).toBe(2); }); it("brainstone draws two now and raises the hand limit to nine", () => { let { state } = newGame(); const me = activePlayer(state).id; state = displayStone(state, me, "brainstone"); const p = state.players.find((pl) => pl.id === me)!; expect(p.hand.length).toBe(9); // 7 - 1 slot replaced + kept + 2 drawn expect(handLimit(p)).toBe(9); // Re-displaying is refused (no double draw). const again = applyCommand(state, me, { type: "cast", instanceId: "brainstone#Sbrainstone" }); expect(again.ok).toBe(false); }); it("powerstone adds one to number cards; speedstone adds movement", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); state = displayStone(state, attacker, "powerstone"); const lb = giveCard(state, attacker, "lightning-blast", "L", 1); giveCard(state, attacker, "number-3", "N", 2); state = castAt(state, attacker, defender, lb, { numberInstanceIds: ["number-3#N"] }); expect(state.players.find((p) => p.id === defender)!.life).toBe(11); // 3+1 // Speedstone: allowance 4 on the next turn. (The stunned defender's turn // is skipped, so play comes straight back to the attacker.) state = displayStone(state, attacker, "speedstone", 1); state = must(state, attacker, { type: "endTurn", draw: 0 }); expect(activePlayer(state).id).toBe(attacker); expect(state.turn.movementAllowance).toBe(4); }); it("shieldstone lets number cards counteract spells", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); state = must(state, attacker, { type: "endTurn", draw: 0 }); state = displayStone(state, defender, "shieldstone"); state = must(state, defender, { type: "endTurn", draw: 0 }); const fb = giveCard(state, attacker, "fireball", "F", 1); giveCard(state, defender, "number-4", "N4", 1); state = must(state, attacker, { type: "cast", instanceId: fb.instanceId, target: { kind: "player", playerId: defender }, }); state = must(state, defender, { type: "counteract", instanceId: "number-4#N4" }); state = must(state, attacker, { type: "pass" }); state = must(state, defender, { type: "pass" }); expect(state.players.find((p) => p.id === defender)!.life).toBe(14); // 5-4 }); it("shadowstone feeds physical damage back as life", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); state = displayStone(state, attacker, "shadowstone"); const dagger = giveCard(state, attacker, "dagger", "D", 1); state = castAt(state, attacker, defender, dagger); expect(state.players.find((p) => p.id === defender)!.life).toBe(12); expect(state.players.find((p) => p.id === attacker)!.life).toBe(18); // +3 }); it("fireball burns displayed stones off when damage lands", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); state = must(state, attacker, { type: "endTurn", draw: 0 }); state = displayStone(state, defender, "speedstone"); state = must(state, defender, { type: "endTurn", draw: 0 }); const fb = giveCard(state, attacker, "fireball", "F", 1); state = castAt(state, attacker, defender, fb); const d = state.players.find((p) => p.id === defender)!; expect(d.hand.some((c) => c.cardId === "speedstone")).toBe(false); expect(d.displayed.length).toBe(0); }); }); describe("slow death", () => { it("bleeds one life per card drawn, forever", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); const sd = giveCard(state, attacker, "slow-death"); state = castAt(state, attacker, defender, sd); expect(sustainedOn(state, defender, "slow-death").length).toBe(1); state = must(state, attacker, { type: "endTurn", draw: 0 }); // Defender discards two to make room, then draws two: 2 damage. const d = state.players.find((p) => p.id === defender)!; const toDiscard = d.hand.slice(0, 2).map((c) => c.instanceId); state = must(state, defender, { type: "discard", instanceIds: toDiscard }); state = must(state, defender, { type: "endTurn", draw: 2 }); expect(state.players.find((p) => p.id === defender)!.life).toBe(13); // It never expires — many turns later it still bleeds. for (let i = 0; i < 4; i++) { state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 }); } expect(sustainedOn(state, defender, "slow-death").length).toBe(1); }); it("bloodstone cancels slow death's per-draw point", () => { let { state } = newGame(); state = toRound2(state); const { attacker, defender } = faceOff(state); const sd = giveCard(state, attacker, "slow-death"); state = castAt(state, attacker, defender, sd); state = must(state, attacker, { type: "endTurn", draw: 0 }); state = displayStone(state, defender, "bloodstone"); const d = state.players.find((p) => p.id === defender)!; const toDiscard = d.hand.filter((c) => c.cardId !== "bloodstone").slice(0, 2).map((c) => c.instanceId); state = must(state, defender, { type: "discard", instanceIds: toDiscard }); state = must(state, defender, { type: "endTurn", draw: 2 }); expect(state.players.find((p) => p.id === defender)!.life).toBe(15); // 1-1=0 per draw }); });