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.
This commit is contained in:
Jared Miller 2026-02-25 09:28:09 -05:00
parent ac45cb8758
commit 4f91396e3a
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

168
src/keywords.test.js Normal file
View file

@ -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);
});
});