// dice-parser.js function roll(sides) { return Math.floor(Math.random() * sides) + 1; } function tokenize(expr) { const re = /(\d*d\d+(?:adv|dis|kh\d+|kl\d+)?|\d+|[+\-*\/()])/gi; const tokens = []; let m; while ((m = re.exec(expr)) !== null) tokens.push(m[0].toLowerCase()); return tokens; } function parseDiceToken(tok) { const m = tok.match(/^(\d*)d(\d+)(adv|dis|kh(\d+)|kl(\d+))?$/i); if (!m) return null; const count = parseInt(m[1] || '1'); const sides = parseInt(m[2]); const mod = (m[3] || '').toLowerCase(); if (count < 1 || count > 1000 || sides < 2 || sides > 10000) throw new Error(`Invalid dice: ${tok}`); return { count, sides, mod }; } export function parse(expr) { const tokens = tokenize(expr); if (!tokens.length) throw new Error('Empty expression'); let pos = 0; const rolls = []; function peek() { return tokens[pos]; } function consume() { return tokens[pos++]; } function parseExpr() { return parseAddSub(); } function parseAddSub() { let left = parseMulDiv(); while (peek() === '+' || peek() === '-') { const op = consume(); const right = parseMulDiv(); left = { value: op === '+' ? left.value + right.value : left.value - right.value, rolls: [...left.rolls, ...right.rolls], }; } return left; } function parseMulDiv() { let left = parseUnary(); while (peek() === '*' || peek() === '/') { const op = consume(); const right = parseUnary(); if (op === '/' && right.value === 0) throw new Error('Division by zero'); left = { value: op === '*' ? left.value * right.value : Math.floor(left.value / right.value), rolls: [...left.rolls, ...right.rolls], }; } return left; } function parseUnary() { if (peek() === '-') { consume(); const r = parsePrimary(); return { value: -r.value, rolls: r.rolls }; } return parsePrimary(); } function parsePrimary() { const tok = peek(); if (!tok) throw new Error('Unexpected end of expression'); if (tok === '(') { consume(); const inner = parseExpr(); if (peek() !== ')') throw new Error('Missing closing )'); consume(); return inner; } const diceInfo = parseDiceToken(tok); if (diceInfo) { consume(); return rollDice(diceInfo); } if (/^\d+$/.test(tok)) { consume(); return { value: parseInt(tok), rolls: [] }; } throw new Error(`Unexpected token: ${tok}`); } function rollDice({ count, sides, mod }) { let rawRolls, kept; if (mod === 'adv') { rawRolls = [roll(sides), roll(sides)]; kept = [Math.max(...rawRolls)]; } else if (mod === 'dis') { rawRolls = [roll(sides), roll(sides)]; kept = [Math.min(...rawRolls)]; } else if (mod.startsWith('kh')) { const k = parseInt(mod.slice(2)); rawRolls = Array.from({ length: count }, () => roll(sides)); kept = [...rawRolls].sort((a, b) => b - a).slice(0, k); } else if (mod.startsWith('kl')) { const k = parseInt(mod.slice(2)); rawRolls = Array.from({ length: count }, () => roll(sides)); kept = [...rawRolls].sort((a, b) => a - b).slice(0, k); } else { rawRolls = Array.from({ length: count }, () => roll(sides)); kept = rawRolls.slice(); } const entry = { sides, rawRolls, kept, mod, value: kept.reduce((a, b) => a + b, 0) }; rolls.push(entry); return { value: entry.value, rolls: [entry] }; } const result = parseExpr(); if (pos < tokens.length) throw new Error(`Unexpected token: ${tokens[pos]}`); return { total: result.value, rolls: result.rolls }; }