130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
// 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 };
|
|
} |