import type { DiceKind, RollResult } from "@/types/rules"; export type DiceRoller = (sides: number) => number; const defaultDiceRoller: DiceRoller = (sides) => Math.floor(Math.random() * sides) + 1; function assertRollInRange(value: number, sides: number) { if (!Number.isInteger(value) || value < 1 || value > sides) { throw new Error(`Dice roller returned invalid value ${value} for d${sides}.`); } } function clampTotal(total: number, min: number, max: number) { return Math.max(min, Math.min(max, total)); } export function rollD3(roller: DiceRoller = defaultDiceRoller): RollResult { const value = roller(6); assertRollInRange(value, 6); const total = Math.ceil(value / 2); return { diceKind: "d3", rolls: [value], total, modifiedTotal: total, }; } export function rollD6(roller: DiceRoller = defaultDiceRoller): RollResult { const value = roller(6); assertRollInRange(value, 6); return { diceKind: "d6", rolls: [value], total: value, modifiedTotal: value, }; } export function roll2D6(roller: DiceRoller = defaultDiceRoller): RollResult { const first = roller(6); const second = roller(6); assertRollInRange(first, 6); assertRollInRange(second, 6); const total = first + second; return { diceKind: "2d6", rolls: [first, second], primary: first, secondary: second, total, modifiedTotal: total, }; } export function rollD66(roller: DiceRoller = defaultDiceRoller): RollResult { const primary = roller(6); const secondary = roller(6); assertRollInRange(primary, 6); assertRollInRange(secondary, 6); const total = primary * 10 + secondary; return { diceKind: "d66", rolls: [primary, secondary], primary, secondary, total, modifiedTotal: total, }; } export function applyRollModifier( roll: RollResult, modifier = 0, limits?: { min: number; max: number }, ): RollResult { if (roll.total === undefined) { throw new Error("Cannot apply a modifier to a roll without a total."); } const nextTotal = roll.total + modifier; const boundedTotal = limits ? clampTotal(nextTotal, limits.min, limits.max) : nextTotal; return { ...roll, modifier, modifiedTotal: boundedTotal, clamped: limits ? boundedTotal !== nextTotal : false, }; } export function rollDice( diceKind: DiceKind, roller: DiceRoller = defaultDiceRoller, ): RollResult { switch (diceKind) { case "d3": return rollD3(roller); case "d6": return rollD6(roller); case "2d6": return roll2D6(roller); case "d66": return rollD66(roller); } }