From 4f91396e3a573234cc99787c9f3fa1dde7c734a3 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Wed, 25 Feb 2026 09:28:09 -0500 Subject: [PATCH] Add keyword integration tests Exercises exhaust, ethereal, retain, poison, unplayable, and ironclad heal-on-victory in a single test file using manual state construction to control hand contents without relying on random draws. --- src/keywords.test.js | 168 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/keywords.test.js diff --git a/src/keywords.test.js b/src/keywords.test.js new file mode 100644 index 0000000..3c25b93 --- /dev/null +++ b/src/keywords.test.js @@ -0,0 +1,168 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { initCards } from "./cards.js"; +import { checkCombatEnd, resolveEnemyTurn, startTurn } from "./combat.js"; +import { initEnemies } from "./enemies.js"; +import { createRunState, endCombat } from "./run.js"; +import { createCombatFromRun, drawCards, endTurn, playCard } from "./state.js"; + +beforeAll(async () => { + await Promise.all([initCards(), initEnemies()]); +}); + +describe("keyword integration", () => { + test("exhaust card removed from cycle after play", () => { + const run = createRunState("ironclad"); + let state = createCombatFromRun(run, "jaw_worm"); + state = drawCards(state, 5); + + // force true_grit into hand at index 0 + const player = { + ...state.players[0], + hand: ["true_grit", ...state.players[0].hand.slice(1)], + }; + state = { ...state, players: [player], player }; + + state = playCard(state, 0); + expect(state.players[0].exhaustPile).toContain("true_grit"); + + state = endTurn(state); + state = { ...state, combat: { ...state.combat, dieResult: 3 } }; + state = resolveEnemyTurn(state); + state = startTurn(state); + + expect(state.players[0].hand).not.toContain("true_grit"); + expect(state.players[0].drawPile).not.toContain("true_grit"); + expect(state.players[0].discardPile).not.toContain("true_grit"); + expect(state.players[0].exhaustPile).toContain("true_grit"); + }); + + test("ethereal card exhausts if not played by end of turn", () => { + const run = createRunState("ironclad"); + let state = createCombatFromRun(run, "jaw_worm"); + + const player = { ...state.players[0], hand: ["carnage", "strike_r"] }; + state = { ...state, players: [player], player }; + + state = endTurn(state); + + expect(state.players[0].exhaustPile).toContain("carnage"); + expect(state.players[0].discardPile).not.toContain("carnage"); + expect(state.players[0].discardPile).toContain("strike_r"); + expect(state.players[0].exhaustPile).not.toContain("strike_r"); + }); + + test("retained card stays through turn cycle", () => { + const run = createRunState("ironclad"); + let state = createCombatFromRun(run, "jaw_worm"); + + const player = { ...state.players[0], hand: ["equilibrium", "strike_r"] }; + state = { ...state, players: [player], player }; + + state = endTurn(state); + + expect(state.players[0].hand).toContain("equilibrium"); + expect(state.players[0].discardPile).toContain("strike_r"); + + state = { ...state, combat: { ...state.combat, dieResult: 3 } }; + state = resolveEnemyTurn(state); + state = startTurn(state); + + // equilibrium was retained, startTurn draws 5 more on top of it + expect(state.players[0].hand).toContain("equilibrium"); + expect(state.players[0].hand).toHaveLength(6); + }); + + test("poison ticks down and kills enemy", () => { + const run = createRunState("ironclad"); + let state = createCombatFromRun(run, "jaw_worm"); + + const enemy = { ...state.enemies[0], poison: 3, hp: 5 }; + state = { + ...state, + enemies: [enemy], + enemy, + combat: { ...state.combat, dieResult: 3 }, + }; + + state = resolveEnemyTurn(state); + expect(state.enemies[0].hp).toBe(2); + expect(state.enemies[0].poison).toBe(2); + + const dying = { ...state.enemies[0], poison: 5, hp: 3 }; + state = { + ...state, + enemies: [dying], + enemy: dying, + combat: { ...state.combat, dieResult: 3 }, + }; + + state = resolveEnemyTurn(state); + expect(state.enemies[0].hp).toBe(0); + expect(checkCombatEnd(state)).toBe("victory"); + }); + + test("unplayable card blocks play", () => { + const run = createRunState("ironclad"); + let state = createCombatFromRun(run, "jaw_worm"); + + const player = { ...state.players[0], hand: ["tactician", "strike_r"] }; + state = { ...state, players: [player], player }; + + const result = playCard(state, 0); + expect(result).toBeNull(); + expect(state.players[0].hand).toEqual(["tactician", "strike_r"]); + }); + + test("ironclad heals after victory", () => { + let run = createRunState("ironclad"); + run = { ...run, hp: 8 }; + + const updated = endCombat(run, 8); + expect(updated.hp).toBe(9); + expect(updated.combatCount).toBe(1); + }); + + test("full combat: exhaust + poison + victory heal", () => { + let run = createRunState("ironclad"); + run = { ...run, hp: 10 }; + + let state = createCombatFromRun(run, "jaw_worm"); + state = startTurn(state); + + // inject true_grit into hand and set enemy poison + const player = { + ...state.players[0], + hand: ["true_grit", ...state.players[0].hand.slice(1)], + }; + const enemy = { ...state.enemies[0], poison: 5 }; + state = { + ...state, + players: [player], + player, + enemies: [enemy], + enemy, + }; + + state = playCard(state, 0); + expect(state.players[0].exhaustPile).toContain("true_grit"); + + // set enemy hp below poison so it dies from the tick + const dying = { ...state.enemies[0], hp: 4 }; + state = { + ...state, + enemies: [dying], + enemy: dying, + combat: { ...state.combat, dieResult: 3 }, + }; + + state = endTurn(state); + state = resolveEnemyTurn(state); + + expect(checkCombatEnd(state)).toBe("victory"); + + const finalHp = state.players[0].hp; + const afterRun = endCombat(run, finalHp); + expect(afterRun.hp).toBe(finalHp + 1); + expect(afterRun.combatCount).toBe(1); + }); +});