import { describe, expect, it } from "vitest"; import { applyCommand, activePlayer, boardView, creatureAt, type GameState, type PlayerId, createGame } from "../src/game"; import { cellKey, SIDES, stepTarget, type Cell, type Side } from "../src/board"; import type { CardInstance } from "../src/cards"; import { newExpansionGame as newGame, must, giveCard, toRound2, emptyNeighborCell } from "./helpers"; /** Summon a creature next to its creator (round 2+, consumes the attack). */ function summon(state: GameState, playerId: PlayerId, kind: string, tag = "S") { const me = state.players.find((p) => p.id === playerId)!; const spot = emptyNeighborCell(state, me.position); const card = giveCard(state, playerId, kind, tag); const next = must(state, playerId, { type: "cast", instanceId: card.instanceId, target: { kind: "cell", cell: spot.cell }, }); return { state: next, at: spot.cell }; } describe("expansion deck", () => { it("basic + expansion1 builds the full 200-card game", () => { const { state } = newGame(); const total = state.deck.length + state.discard.length + state.players.reduce((s, p) => s + p.hand.length, 0); expect(total).toBe(200); }); }); describe("monsters", () => { it("summoning uses your attack; the monster moves now but attacks next turn", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "skeleton"); state = r.state; const skeleton = creatureAt(state, r.at)!; expect(skeleton.kind).toBe("skeleton"); expect(state.turn.attackUsed).toBe(true); // It can move on the creation turn... const view = boardView(state); for (const side of SIDES) { if (stepTarget(view, skeleton.position, side).kind === "step") { state = must(state, me, { type: "moveCreature", creatureId: skeleton.id, direction: side }); break; } } // ...but cannot attack until next turn. const other = state.players.find((p) => p.id !== me)!; other.position = { ...state.creatures[0]!.position }; const refused = applyCommand(state, me, { type: "creatureAttack", creatureId: state.creatures[0]!.id, targetId: other.id, }); expect(refused.ok).toBe(false); // Next turn: the skeleton punches for 2. state = must(state, me, { type: "endTurn", draw: 0 }); state = must(state, other.id, { type: "endTurn", draw: 0 }); const victim = state.players.find((p) => p.id !== me)!; victim.position = { ...state.creatures[0]!.position }; state = must(state, me, { type: "creatureAttack", creatureId: state.creatures[0]!.id, targetId: victim.id, }); expect(state.players.find((p) => p.id !== me)!.life).toBe(13); }); it("a monster will not obey the enemy and will not strike its creator", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "skeleton"); state = r.state; const id = state.creatures[0]!.id; const other = state.players.find((p) => p.id !== me)!.id; state = must(state, me, { type: "endTurn", draw: 0 }); expect(applyCommand(state, other, { type: "moveCreature", creatureId: id, direction: "N" }).ok).toBe(false); state = must(state, other, { type: "endTurn", draw: 0 }); const creator = state.players.find((p) => p.id === me)!; creator.position = { ...state.creatures[0]!.position }; expect(applyCommand(state, me, { type: "creatureAttack", creatureId: id, targetId: me }).ok).toBe(false); }); it("the troll rolls a D4 for damage and regenerates at its creator's turn end", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "troll"); state = r.state; const troll = state.creatures[0]!; // Hurt the troll, then watch a point come back at end of turn. const fb = giveCard(state, me, "fireball", "F", 1); // (can't attack again this turn — adrenaline not in play — so wound it via // test surgery instead) void fb; troll.damage = 3; state = must(state, me, { type: "endTurn", draw: 0 }); expect(state.creatures[0]!.damage).toBe(2); }); it("the wraith slips through one wall per turn and its touch steals cards", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "wraith"); state = r.state; const wraith = state.creatures[0]!; // Walk it through a wall if one is adjacent. const view = boardView(state); for (const side of SIDES) { const t = stepTarget(view, wraith.position, side); if (t.kind === "blocked" && t.by === "wall") { const dest = { x: wraith.position.x + (side === "E" ? 1 : side === "W" ? -1 : 0), y: wraith.position.y + (side === "S" ? 1 : side === "N" ? -1 : 0) }; if (!view.cells[cellKey(dest)]) continue; state = must(state, me, { type: "moveCreature", creatureId: wraith.id, direction: side }); expect(state.creatures[0]!.wallPassUsed).toBe(1); break; } } // Its touch: 2 damage and a random card lost. March it onto the enemy. state = must(state, me, { type: "endTurn", draw: 0 }); const enemy = state.players.find((p) => p.id !== me)!; state = must(state, enemy.id, { type: "endTurn", draw: 0 }); const w = state.creatures[0]!; const enemy2 = state.players.find((p) => p.id !== me)!; enemy2.position = { ...w.position }; // A touch requires the WRAITH to enter the square — step it away and back. const view2 = boardView(state); for (const side of SIDES) { const t = stepTarget(view2, w.position, side); if (t.kind === "step") { const back: Side = side === "N" ? "S" : side === "S" ? "N" : side === "E" ? "W" : "E"; state = must(state, me, { type: "moveCreature", creatureId: w.id, direction: side }); state = must(state, me, { type: "moveCreature", creatureId: w.id, direction: back }); break; } } const bitten = state.players.find((p) => p.id !== me)!; expect(bitten.life).toBe(13); expect(bitten.hand.length).toBe(6); }); it("the fire imp scorches on sight and dies only to water", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "fire-imp"); state = r.state; const imp = state.creatures[0]!; // Fireball cannot destroy it... state = must(state, me, { type: "endTurn", draw: 0 }); const enemy = state.players.find((p) => p.id !== me)!; // (enemy may have been scorched at turn start if in LOS — note life) const enemyNow = state.players.find((p) => p.id !== me)!; enemyNow.position = { ...imp.position }; // stand at the imp for clear sight const fb = giveCard(state, enemy.id, "fireball", "F", 0); state = must(state, enemy.id, { type: "cast", instanceId: fb.instanceId, target: { kind: "creature", creatureId: imp.id }, }); expect(state.creatures.length).toBe(1); // ...but a waterbolt douses it instantly. state = must(state, enemy.id, { type: "endTurn", draw: 0 }); state = must(state, me, { type: "endTurn", draw: 0 }); state.players.find((p) => p.id !== me)!.position = { ...state.creatures[0]!.position }; const wb = giveCard(state, enemy.id, "waterbolt", "W", 0); state = must(state, enemy.id, { type: "cast", instanceId: wb.instanceId, target: { kind: "creature", creatureId: imp.id }, }); expect(state.creatures.length).toBe(0); }); it("the democratic monster is moved by every player", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "democratic-monster"); state = r.state; const id = state.creatures[0]!.id; state = must(state, me, { type: "endTurn", draw: 0 }); const other = activePlayer(state).id; expect(other).not.toBe(me); const view = boardView(state); for (const side of SIDES) { if (stepTarget(view, state.creatures[0]!.position, side).kind === "step") { const result = applyCommand(state, other, { type: "moveCreature", creatureId: id, direction: side }); expect(result.ok).toBe(true); break; } } }); it("mega-monster doubles a monster's toughness; dispel un-creates it", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "skeleton"); state = r.state; const id = state.creatures[0]!.id; const mm = giveCard(state, me, "mega-monster", "MM", 1); state = must(state, me, { type: "cast", instanceId: mm.instanceId, target: { kind: "creature", creatureId: id }, }); expect(state.creatures[0]!.maxDamage).toBe(8); // "Doubles the existing life-points OR movement rate" — the other choice. const movesBefore = state.creatures[0]!.movesPerTurn; const mm2 = giveCard(state, me, "mega-monster", "MM2", 3); state = must(state, me, { type: "cast", instanceId: mm2.instanceId, target: { kind: "creature", creatureId: id }, params: { boost: "movement" }, }); expect(state.creatures[0]!.movesPerTurn).toBe(movesBefore * 2); expect(state.creatures[0]!.maxDamage).toBe(8); const dc = giveCard(state, me, "dispel-creation", "DC", 2); state = must(state, me, { type: "cast", instanceId: dc.instanceId, target: { kind: "cell", cell: state.creatures[0]!.position }, }); expect(state.creatures.length).toBe(0); }); it("monsters vanish when their creator dies", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "skeleton"); state = r.state; const creator = state.players.find((p) => p.id === me)!; const enemy = state.players.find((p) => p.id !== me)!; creator.life = 1; enemy.position = { ...creator.position }; state = must(state, me, { type: "endTurn", draw: 0 }); state = must(state, enemy.id, { type: "punch", targetId: me }); state = must(state, me, { type: "pass" }); expect(state.players.find((p) => p.id === me)!.alive).toBe(false); expect(state.creatures.length).toBe(0); }); }); describe("expansion support cards", () => { it("adrenaline allows a second attack in one turn", () => { let { state } = newGame(); state = toRound2(state); const attacker = activePlayer(state); const defender = state.players.find((p) => p.id !== attacker.id)!; defender.position = { ...attacker.position }; const adr = giveCard(state, attacker.id, "adrenaline"); giveCard(state, attacker.id, "number-2", "N", 1); state = must(state, attacker.id, { type: "cast", instanceId: adr.instanceId, numberInstanceIds: ["number-2#N"], }); const fb1 = giveCard(state, attacker.id, "fireball", "F1", 0); state = must(state, attacker.id, { type: "cast", instanceId: fb1.instanceId, target: { kind: "player", playerId: defender.id } }); state = must(state, defender.id, { type: "pass" }); const fb2 = giveCard(state, attacker.id, "fireball", "F2", 0); state = must(state, attacker.id, { type: "cast", instanceId: fb2.instanceId, target: { kind: "player", playerId: defender.id } }); state = must(state, defender.id, { type: "pass" }); expect(state.players.find((p) => p.id === defender.id)!.life).toBe(5); // A third is refused. const fb3 = giveCard(state, attacker.id, "fireball", "F3", 0); expect(applyCommand(state, attacker.id, { type: "cast", instanceId: fb3.instanceId, target: { kind: "player", playerId: defender.id }, }).ok).toBe(false); }); it("shadow costs a life point per turn and dies to any damage", () => { let { state } = newGame(); state = toRound2(state); const me = activePlayer(state).id; const r = summon(state, me, "shadow"); state = r.state; expect(state.creatures[0]!.kind).toBe("shadow"); const lifeBefore = state.players.find((p) => p.id === me)!.life; // Around to my next turn: upkeep costs 1. state = must(state, me, { type: "endTurn", draw: 0 }); state = must(state, activePlayer(state).id, { type: "endTurn", draw: 0 }); expect(state.players.find((p) => p.id === me)!.life).toBe(lifeBefore - 1); // Any damage destroys it. const enemy = state.players.find((p) => p.id !== me)!.id; state = must(state, me, { type: "endTurn", draw: 0 }); state.players.find((p) => p.id === enemy)!.position = { ...state.creatures[0]!.position }; const fb = giveCard(state, enemy, "fireball", "F", 0); state = must(state, enemy, { type: "cast", instanceId: fb.instanceId, target: { kind: "creature", creatureId: state.creatures[0]!.id }, }); expect(state.creatures.length).toBe(0); }); }); describe("counteracting a creature's blow (rules rev 4)", () => { function rev4() { return createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 4 }); } function wraithOnVictim(state: GameState) { state = toRound2(state); const controller = activePlayer(state); const victim = state.players.find((p) => p.id !== controller.id)!; const spot = emptyNeighborCell(state, controller.position); state.creatures.push({ id: "w1", kind: "wraith", controllerId: controller.id, position: { ...controller.position }, damage: 0, maxDamage: 4, movesPerTurn: 3, movementUsed: 0, wallPassesPerTurn: 1, wallPassUsed: 0, attackUsed: false, justCreated: false, scorchedThisTurn: [], }); victim.position = { ...spot.cell }; return { state, controller: controller.id, victim: victim.id, side: spot.side }; } it("BLUNT halves the wraith's touch; the card theft still lands", () => { let { state } = rev4(); const rig = wraithOnVictim(state); state = rig.state; giveCard(state, rig.victim, "blunt", "B", 0); const handBefore = state.players.find((p) => p.id === rig.victim)!.hand.filter(Boolean).length; state = must(state, rig.controller, { type: "moveCreature", creatureId: "w1", direction: rig.side }); expect(state.stack?.creatureId).toBe("w1"); state = must(state, rig.victim, { type: "counteract", instanceId: "blunt#B" }); state = must(state, rig.controller, { type: "pass" }); state = must(state, rig.victim, { type: "pass" }); const v = state.players.find((p) => p.id === rig.victim)!; expect(v.life).toBe(14); // 2 halved up to 1 expect(v.hand.length).toBe(handBefore - 2); // blunt spent + a card stolen }); it("FULL REFLECTION turns the touch back on the wraith, theft and all", () => { let { state } = rev4(); const rig = wraithOnVictim(state); state = rig.state; giveCard(state, rig.victim, "full-reflection", "FR", 0); state = must(state, rig.controller, { type: "moveCreature", creatureId: "w1", direction: rig.side }); state = must(state, rig.victim, { type: "counteract", instanceId: "full-reflection#FR" }); state = must(state, rig.controller, { type: "pass" }); state = must(state, rig.victim, { type: "pass" }); const v = state.players.find((p) => p.id === rig.victim)!; expect(v.life).toBe(15); const wraith = state.creatures.find((c) => c.id === "w1")!; expect(wraith.damage).toBe(2); }); it("legacy games (rev < 4) keep the instant touch", () => { let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"] }); const rig = wraithOnVictim(state); state = rig.state; state = must(state, rig.controller, { type: "moveCreature", creatureId: "w1", direction: rig.side }); expect(state.stack).toBeNull(); expect(state.players.find((p) => p.id === rig.victim)!.life).toBe(13); }); }); describe("big man (rules rev 5)", () => { function bigRig() { let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev: 5 }); state = toRound2(state); const giant = activePlayer(state); state.sustained.push({ id: "fx-big", cardId: "big-man", casterId: giant.id, targetId: giant.id, remainingTurns: 9, data: {}, }); return { state, giant }; } /** Any square with two open steps in a straight line (test surgery spot). */ function straightRun(state: GameState): { at: Cell; side: Side; mid: Cell; far: Cell } { const view = boardView(state); for (const key of Object.keys(view.cells)) { const [x, y] = key.split(",").map(Number) as [number, number]; const at = { x, y }; if (view.homes.some((h) => cellKey(h) === key)) continue; for (const side of SIDES) { const t1 = stepTarget(view, at, side); if (t1.kind !== "step") continue; const t2 = stepTarget(view, t1.to, side); if (t2.kind !== "step") continue; return { at, side, mid: t1.to, far: t2.to }; } } throw new Error("no straight corridor anywhere — impossible"); } it("pushes a wizard down the corridor as he advances", () => { const rig = bigRig(); let state = rig.state; const victim = state.players.find((p) => p.id !== rig.giant.id)!; const run = straightRun(state); const giant = state.players.find((p) => p.id === rig.giant.id)!; giant.position = { ...run.at }; victim.position = { ...run.mid }; state = must(state, rig.giant.id, { type: "move", direction: run.side }); const after = state.players.find((p) => p.id !== rig.giant.id)!; expect(cellKey(after.position)).toBe(cellKey(run.far)); expect(cellKey(state.players.find((p) => p.id === rig.giant.id)!.position)).toBe(cellKey(run.mid)); }); it("no room to shove means no way forward", () => { const rig = bigRig(); const state = rig.state; const victim = state.players.find((p) => p.id !== rig.giant.id)!; const view = boardView(state); // Find a step whose far side is walled: victim there cannot be pushed. for (const side of SIDES) { const t1 = stepTarget(view, rig.giant.position, side); if (t1.kind !== "step") continue; const t2 = stepTarget(view, t1.to, side); if (t2.kind === "step") continue; victim.position = { ...t1.to }; const r = applyCommand(state, rig.giant.id, { type: "move", direction: side }); expect(r.ok).toBe(false); return; } throw new Error("setup: seed offered no walled pocket beside the home"); }); it("monsters cannot enter the giant's square", () => { const rig = bigRig(); let state = rig.state; const view = boardView(state); const spot = emptyNeighborCell(state, rig.giant.position); state.creatures.push({ id: "t1", kind: "troll", controllerId: state.players.find((p) => p.id !== rig.giant.id)!.id, position: { ...spot.cell }, damage: 0, maxDamage: 8, movesPerTurn: 3, movementUsed: 0, wallPassesPerTurn: 0, wallPassUsed: 0, attackUsed: false, justCreated: false, scorchedThisTurn: [], }); state = must(state, rig.giant.id, { type: "endTurn", draw: 0 }); const controller = activePlayer(state).id; const back = (["N", "S", "E", "W"] as const).find((d) => { const t = stepTarget(view, spot.cell, d); return t.kind === "step" && cellKey(t.to) === cellKey(rig.giant.position); })!; const r = applyCommand(state, controller, { type: "moveCreature", creatureId: "t1", direction: back }); expect(r.ok).toBe(false); }); it("steps over a pit for two movement points, never entering it", () => { const rig = bigRig(); let state = rig.state; const run = straightRun(state); state.players.find((p) => p.id === rig.giant.id)!.position = { ...run.at }; state.squareContents[cellKey(run.mid)] = { kind: "pit", damage: 0, createdBy: rig.giant.id }; state = must(state, rig.giant.id, { type: "move", direction: run.side, over: true }); const g = state.players.find((p) => p.id === rig.giant.id)!; expect(cellKey(g.position)).toBe(cellKey(run.far)); expect(state.turn.movementUsed).toBe(2); expect(g.inPit).toBe(false); }); }); describe("monsters roll to hit the hidden (rules rev 13)", () => { function creatureVsInvisible(deckRev: number) { let { state } = createGame({ playerIds: ["alice", "bob"], seed: 42, sets: ["basic", "expansion1"], deckRev }); state = toRound2(state); const me = activePlayer(state).id; const victim = state.players.find((p) => p.id !== me)!; const r = summon(state, me, "skeleton"); state = r.state; const bones = state.creatures[0]!; bones.justCreated = false; bones.attackUsed = false; bones.position = { ...victim.position }; state.sustained.push({ id: "fx1", cardId: "invisible", casterId: victim.id, targetId: victim.id, remainingTurns: 3, data: {}, }); return { state, me, victim, skeletonId: bones.id }; } it("rev 13: the skeleton's blow takes the 1-in-4 roll — 'any attack' means any", () => { let { state, me, victim, skeletonId } = creatureVsInvisible(13); const rngBefore = JSON.stringify(state.rng); state = must(state, me, { type: "creatureAttack", creatureId: skeletonId, targetId: victim.id }); state = must(state, victim.id, { type: "pass" }); // The die was consumed, whichever way it landed. expect(JSON.stringify(state.rng)).not.toBe(rngBefore); }); it("earlier revisions keep the old certainty: no roll, the blow just lands", () => { let { state, me, victim, skeletonId } = creatureVsInvisible(12); const lifeBefore = state.players.find((p) => p.id === victim.id)!.life; const rngBefore = JSON.stringify(state.rng); state = must(state, me, { type: "creatureAttack", creatureId: skeletonId, targetId: victim.id }); state = must(state, victim.id, { type: "pass" }); expect(state.players.find((p) => p.id === victim.id)!.life).toBeLessThan(lifeBefore); // No die was consumed on the way. expect(JSON.stringify(state.rng)).toBe(rngBefore); }); }); describe("elimination sweeps the board either way (rules rev 14)", () => { it("a treasure-eliminated wizard's fire imp vanishes with them", () => { let { state } = createGame({ playerIds: ["a", "b", "c"], seed: 42, sets: ["basic", "expansion1"], deckRev: 14 }); const victim = state.players.find((p) => p.id === "c")!; state.creatures.push({ id: "imp1", kind: "fire-imp", controllerId: "c", position: { ...victim.position }, damage: 0, maxDamage: 5, movesPerTurn: 0, movementUsed: 0, attackUsed: false, justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], }); state.sustained.push({ id: "fx-med", cardId: "medusa", casterId: "a", targetId: "c", remainingTurns: 3, data: {}, }); // Both of c's treasures sit captured on living enemies' home bases. const mine = state.treasures.filter((t) => t.owner === "c"); expect(mine.length).toBe(2); mine[0]!.position = { ...state.players.find((p) => p.id === "a")!.home }; mine[0]!.carriedBy = null; mine[1]!.position = { ...state.players.find((p) => p.id === "b")!.home }; mine[1]!.carriedBy = null; const active = activePlayer(state).id; const r = applyCommand(state, active, { type: "endTurn", draw: 0 }); if (!r.ok) throw new Error(r.error); state = r.state; expect(state.players.find((p) => p.id === "c")!.alive).toBe(false); expect(state.creatures.some((c) => c.controllerId === "c")).toBe(false); expect(state.sustained.some((s) => s.targetId === "c")).toBe(false); }); }); describe("creatures walk open doorways", () => { it("a skeleton passes a door whose lock was removed", () => { let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 16 }); state = toRound2(state); const view = boardView(state); for (const [key, edge] of Object.entries(view.edges)) { if (edge !== "door") continue; const [kind, coords] = key.split(",").length ? key.split(":") as [string, string] : ["", ""]; const [x, y] = coords.split(",").map(Number) as [number, number]; const side = kind === "V" ? ("E" as Side) : ("S" as Side); state.doorStates[key] = "removed"; state.creatures.push({ id: "sk1", kind: "skeleton", controllerId: activePlayer(state).id, position: { x, y }, damage: 0, maxDamage: 4, movesPerTurn: 3, movementUsed: 0, attackUsed: false, justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], }); const r = applyCommand(state, activePlayer(state).id, { type: "moveCreature", creatureId: "sk1", direction: side, }); if (!r.ok) throw new Error(r.error); const sk = r.state.creatures.find((c) => c.id === "sk1")!; expect(cellKey(sk.position)).toBe(cellKey({ x: x + (side === "E" ? 1 : 0), y: y + (side === "S" ? 1 : 0) })); return; } throw new Error("setup: seed 42 grew a maze with no doors"); }); }); describe("the wave carries monsters (rules rev 17)", () => { it("a cornered skeleton takes the waterwall crush", () => { let { state } = createGame({ playerIds: ["a", "b"], seed: 42, sets: ["basic", "expansion1"], deckRev: 17 }); state = toRound2(state); const caster = activePlayer(state); // A skeleton pinned against solid stone, one square from the wave's edge. const view = boardView(state); for (const [key, edge] of Object.entries(view.edges)) { if (edge !== "wall") continue; const [kind, coords] = key.split(":") as [string, string]; if (kind !== "H") continue; const [x, y] = coords.split(",").map(Number) as [number, number]; // Wave from this wall southward washes the square below. const below = { x, y: y + 1 }; if (!view.cells[cellKey(below)]) continue; state.creatures.push({ id: "sk1", kind: "skeleton", controllerId: caster.id, position: { ...below }, damage: 0, maxDamage: 4, movesPerTurn: 3, movementUsed: 0, attackUsed: false, justCreated: false, wallPassesPerTurn: 0, wallPassUsed: 0, scorchedThisTurn: [], }); // Pin it: stone directly south of it. state.squareContents[cellKey({ x, y: y + 2 })] = { kind: "stone", damage: 0, createdBy: caster.id }; caster.position = { x, y }; const stw = giveCard(state, caster.id, "stone-to-water"); const r = applyCommand(state, caster.id, { type: "cast", instanceId: stw.instanceId, target: { kind: "edge", cell: { x, y }, side: "S" }, }); if (!r.ok) throw new Error(r.error); const sk = r.state.creatures.find((c) => c.id === "sk1"); // Washed against the stone: it took crush damage (or died of it). if (sk) expect(sk.damage).toBeGreaterThan(0); return; } throw new Error("setup: no horizontal wall with a floor below"); }); });