All checks were successful
Build and Deploy Nuxt / build (push) Successful in 58s
134 lines
3.8 KiB
JavaScript
134 lines
3.8 KiB
JavaScript
// DiceParser.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 peek = () => tokens[pos];
|
|
const consume = () => 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,
|
|
steps: [...left.steps, { type: 'op', op }, ...right.steps],
|
|
};
|
|
}
|
|
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),
|
|
steps: [...left.steps, { type: 'op', op }, ...right.steps],
|
|
};
|
|
}
|
|
return left;
|
|
}
|
|
|
|
function parseUnary() {
|
|
if (peek() === '-') {
|
|
consume();
|
|
const r = parsePrimary();
|
|
return { value: -r.value, steps: [{ type: 'op', op: '-' }, ...r.steps] };
|
|
}
|
|
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 {
|
|
value: inner.value,
|
|
steps: [{ type: 'op', op: '(' }, ...inner.steps, { type: 'op', op: ')' }],
|
|
};
|
|
}
|
|
|
|
const diceInfo = parseDiceToken(tok);
|
|
if (diceInfo) {
|
|
consume();
|
|
return rollDice(diceInfo);
|
|
}
|
|
|
|
if (/^\d+$/.test(tok)) {
|
|
consume();
|
|
const v = parseInt(tok);
|
|
return { value: v, steps: [{ type: 'const', value: v }] };
|
|
}
|
|
|
|
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) };
|
|
return {
|
|
value: entry.value,
|
|
steps: [{ type: 'dice', entry }],
|
|
};
|
|
}
|
|
|
|
const result = parseExpr();
|
|
if (pos < tokens.length) throw new Error(`Unexpected token: ${tokens[pos]}`);
|
|
return { total: result.value, steps: result.steps };
|
|
} |