import { tokenize, parse, roll, applyModifier, rollFormula } from "./index"; import type { DieResult } from "./index"; describe("dice engine — tokenizer", () => { test("tokenizes NdX", () => { expect(tokenize("3d6")).toEqual([{ type: "dice", count: 3, sides: 6 }]); }); test("tokenizes bare dX as a single die", () => { expect(tokenize("d20")).toEqual([{ type: "dice", count: 1, sides: 20 }]); }); test("tokenizes a pool literal", () => { expect(tokenize("{d4,d8}")).toEqual([ { type: "pool_start" }, { type: "dice", count: 1, sides: 4 }, { type: "comma" }, { type: "dice", count: 1, sides: 8 }, { type: "pool_end" }, ]); }); test("tokenizes a keep-highest modifier", () => { expect(tokenize("3d6kh2")).toEqual([ { type: "dice", count: 3, sides: 6 }, { type: "modifier", modType: "kh", count: 2 }, ]); }); test("ignores whitespace", () => { expect(tokenize(" 3d6 + 5 ")).toEqual([ { type: "dice", count: 3, sides: 6 }, { type: "plus" }, { type: "number", value: 5 }, ]); }); }); describe("dice engine — parser", () => { test("builds one pool for NdX", () => { const pools = parse(tokenize("3d6")); expect(pools).toHaveLength(1); expect(pools[0].dice).toHaveLength(3); expect(pools[0].dice.every((d) => d.sides === 6)).toBe(true); }); test("builds a combined pool list for +", () => { const pools = parse(tokenize("3d6+5")); expect(pools).toHaveLength(2); expect(pools[1].dice[0]).toMatchObject({ sides: 0, value: 5 }); }); test("marks a subtracted pool negative", () => { const pools = parse(tokenize("3d6-2")); expect(pools).toHaveLength(2); expect(pools[1].isNegative).toBe(true); expect(pools[1].dice[0].isNegative).toBe(true); }); }); describe("dice engine — applyModifier", () => { const dice = (...values: number[]): DieResult[] => values.map((value) => ({ sides: 6, value, isNegative: false })); test("keeps the highest N", () => { const { keptRolls, droppedRolls } = applyModifier(dice(2, 6, 4), { type: "kh", count: 2, }); expect(keptRolls.map((r) => r.value).sort()).toEqual([4, 6]); expect(droppedRolls.map((r) => r.value)).toEqual([2]); }); test("drops the lowest N", () => { const { keptRolls, droppedRolls } = applyModifier(dice(2, 6, 4, 1), { type: "dl", count: 1, }); expect(keptRolls.map((r) => r.value).sort()).toEqual([2, 4, 6]); expect(droppedRolls.map((r) => r.value)).toEqual([1]); }); test("keeps all when there is no modifier", () => { const { keptRolls, droppedRolls } = applyModifier(dice(1, 2), undefined); expect(keptRolls).toHaveLength(2); expect(droppedRolls).toHaveLength(0); }); }); describe("dice engine — roll", () => { test("rolls 3d6 within range", () => { for (let i = 0; i < 25; i++) { const result = roll("3d6"); expect(result.total).toBeGreaterThanOrEqual(3); expect(result.total).toBeLessThanOrEqual(18); expect(result.pools).toHaveLength(1); } }); test("rolls d20 within range", () => { const result = roll("d20"); expect(result.total).toBeGreaterThanOrEqual(1); expect(result.total).toBeLessThanOrEqual(20); }); test("adds fixed numbers to the total", () => { const result = roll("3d6+5"); expect(result.total).toBeGreaterThanOrEqual(8); expect(result.total).toBeLessThanOrEqual(23); }); test("keep-highest keeps N dice and totals only those", () => { const result = roll("3d6kh2"); const [pool] = result.pools; expect(pool.keptRolls).toHaveLength(2); expect(pool.droppedRolls).toHaveLength(1); expect(result.total).toBe(pool.subtotal); }); test("emits matching HTML and plain-text detail", () => { const result = roll("2d6"); expect(result.detail).toContain(String(result.total)); expect(result.detail).toContain(""); expect(result.plainDetail).toContain(String(result.total)); }); }); describe("dice engine — rollFormula", () => { test("normalizes a successful roll", () => { const result = rollFormula("3d6"); expect(result.success).toBe(true); expect(result.formula).toBe("3d6"); expect(result.result.total).toBeGreaterThanOrEqual(3); expect(result.result.pools[0].rolls).toHaveLength(3); expect(result.result.pools[0].keptRolls).toHaveLength(3); }); test("supports pool literals and modifiers", () => { const result = rollFormula("{d4,d8}kh2-5"); expect(result.success).toBe(true); expect(result.result.pools.length).toBeGreaterThanOrEqual(2); }); });