export interface ParsedDice { count: number; sides: number; modifier: number; raw: string; } export interface RollResult { rolls: number[]; modifier: number; total: number; expression: string; error?: string; } export function parseDice(expression: string): ParsedDice | null { const cleaned = expression.trim().toLowerCase().replace(/\s/g, ""); const match = cleaned.match(/^(\d*)d(\d+)([+-]\d+)?$/); if (!match) return null; const count = match[1] ? parseInt(match[1], 10) : 1; const sides = parseInt(match[2], 10); const modifier = match[3] ? parseInt(match[3], 10) : 0; if (count < 1 || count > 100 || sides < 1 || sides > 100) return null; return { count, sides, modifier, raw: expression.trim() }; } export function rollDice( expression: string, options?: { advantage?: boolean; disadvantage?: boolean }, ): RollResult { const parsed = parseDice(expression); if (!parsed) { return { rolls: [], modifier: 0, total: 0, expression, error: `Couldn't parse: ${expression}`, }; } const { count, sides, modifier } = parsed; if ( (options?.advantage || options?.disadvantage) && count === 1 && sides === 20 ) { const roll1 = Math.floor(Math.random() * sides) + 1; const roll2 = Math.floor(Math.random() * sides) + 1; const chosen = options.advantage ? Math.max(roll1, roll2) : Math.min(roll1, roll2); return { rolls: [roll1, roll2], modifier, total: chosen + modifier, expression, }; } const rolls: number[] = []; for (let i = 0; i < count; i++) { rolls.push(Math.floor(Math.random() * sides) + 1); } const sum = rolls.reduce((a, b) => a + b, 0); return { rolls, modifier, total: sum + modifier, expression, }; }