import { describe, expect, it } from "vitest"; import { calculateEncounterDifficulty, crToXp, } from "../encounter-difficulty.js"; describe("crToXp", () => { it("returns 0 for CR 0", () => { expect(crToXp("0")).toBe(0); }); it("returns 25 for CR 1/8", () => { expect(crToXp("1/8")).toBe(25); }); it("returns 50 for CR 1/4", () => { expect(crToXp("1/4")).toBe(50); }); it("returns 100 for CR 1/2", () => { expect(crToXp("1/2")).toBe(100); }); it("returns 200 for CR 1", () => { expect(crToXp("1")).toBe(200); }); it("returns 155000 for CR 30", () => { expect(crToXp("30")).toBe(155000); }); it("returns 0 for unknown CR", () => { expect(crToXp("99")).toBe(0); expect(crToXp("")).toBe(0); expect(crToXp("abc")).toBe(0); }); }); describe("calculateEncounterDifficulty", () => { it("returns trivial when monster XP is below Low threshold", () => { // 4x level 1: Low = 200, Moderate = 300, High = 400 // 1x CR 0 = 0 XP → trivial const result = calculateEncounterDifficulty([1, 1, 1, 1], ["0"]); expect(result.tier).toBe("trivial"); expect(result.totalMonsterXp).toBe(0); expect(result.partyBudget).toEqual({ low: 200, moderate: 300, high: 400, }); }); it("returns low for 4x level 1 vs Bugbear (CR 1)", () => { // DMG example: 4x level 1 PCs vs 1 Bugbear (CR 1, 200 XP) // Low = 200, Moderate = 300 → 200 >= 200 but < 300 → Low const result = calculateEncounterDifficulty([1, 1, 1, 1], ["1"]); expect(result.tier).toBe("low"); expect(result.totalMonsterXp).toBe(200); }); it("returns moderate for 5x level 3 vs 1125 XP", () => { // 5x level 3: Low = 750, Moderate = 1125, High = 2000 // 1125 XP >= 1125 Moderate but < 2000 High → Moderate // Using CR 3 (700) + CR 2 (450) = 1150 XP ≈ 1125 threshold // Let's use exact: 5 * 225 = 1125 moderate budget // Need monsters that sum exactly to 1125: CR 3 (700) + CR 2 (450) = 1150 const result = calculateEncounterDifficulty([3, 3, 3, 3, 3], ["3", "2"]); expect(result.tier).toBe("moderate"); expect(result.totalMonsterXp).toBe(1150); expect(result.partyBudget.moderate).toBe(1125); }); it("returns high when XP meets High threshold", () => { // 4x level 1: High = 400 // 2x CR 1 = 400 XP → High const result = calculateEncounterDifficulty([1, 1, 1, 1], ["1", "1"]); expect(result.tier).toBe("high"); expect(result.totalMonsterXp).toBe(400); }); it("caps at high when XP far exceeds threshold", () => { // 4x level 1: High = 400 // CR 30 = 155000 XP → still High (no tier above) const result = calculateEncounterDifficulty([1, 1, 1, 1], ["30"]); expect(result.tier).toBe("high"); expect(result.totalMonsterXp).toBe(155000); }); it("handles mixed party levels", () => { // 3x level 3 + 1x level 2 // level 3: low=150, mod=225, high=400 (x3 = 450, 675, 1200) // level 2: low=100, mod=150, high=200 (x1 = 100, 150, 200) // Total: low=550, mod=825, high=1400 const result = calculateEncounterDifficulty([3, 3, 3, 2], ["3"]); expect(result.partyBudget).toEqual({ low: 550, moderate: 825, high: 1400, }); expect(result.totalMonsterXp).toBe(700); expect(result.tier).toBe("low"); }); it("returns trivial with empty monster array", () => { const result = calculateEncounterDifficulty([5, 5], []); expect(result.tier).toBe("trivial"); expect(result.totalMonsterXp).toBe(0); }); it("returns high with empty party array (zero budget thresholds)", () => { // Domain function treats empty party as zero budgets — any XP exceeds all thresholds. // The useDifficulty hook guards this path by returning null when no leveled PCs exist. const result = calculateEncounterDifficulty([], ["1"]); expect(result.tier).toBe("high"); expect(result.totalMonsterXp).toBe(200); expect(result.partyBudget).toEqual({ low: 0, moderate: 0, high: 0 }); }); it("handles fractional CRs", () => { const result = calculateEncounterDifficulty( [1, 1, 1, 1], ["1/8", "1/4", "1/2"], ); expect(result.totalMonsterXp).toBe(175); // 25 + 50 + 100 expect(result.tier).toBe("trivial"); // 175 < 200 Low }); it("ignores unknown CRs (0 XP)", () => { const result = calculateEncounterDifficulty([1, 1, 1, 1], ["unknown"]); expect(result.totalMonsterXp).toBe(0); expect(result.tier).toBe("trivial"); }); });