UNPKG

ttrpg-lib-dice

Version:

A small library for rolling dice in tabletop games

131 lines (130 loc) 4.52 kB
import { Dice } from './domain/Dice'; const Operations = { '+': (a, b) => a + b, '-': (a, b) => a - b, }; const operationRegExp = /([-+])/; /** * Generates a cryptographically secure random integer between min (inclusive) and max (inclusive) */ export const secureRandomInteger = (min, max) => { const range = max - min + 1; const maxUint32 = 0xffffffff; const limit = maxUint32 - (maxUint32 % range); let randomValue; do { randomValue = crypto.getRandomValues(new Uint32Array(1))[0]; } while (randomValue >= limit); return min + (randomValue % range); }; export const getRandomArrayItem = (arr) => { if (!arr.length) { throw new Error('Empty array'); } return arr[secureRandomInteger(0, arr.length - 1)]; }; export const getRandomArrayItems = (arr, count) => { if (count > arr.length) { throw new Error('Requested more elements than are present in the array'); } if (!count) { throw new Error('Count should be more than 0'); } const result = []; const usedIndices = new Set(); while (result.length < count) { const index = secureRandomInteger(0, arr.length - 1); if (!usedIndices.has(index)) { usedIndices.add(index); result.push(arr[index]); } } return result; }; export const roll = (dice = Dice.d100) => secureRandomInteger(1, dice); const isDiceRoll = (formula) => /^\d*d\d+$/.test(formula.trim()); const isInteger = (formula) => /^\d+$/.test(formula.trim()); const rollNumberOfDice = (formula) => { const [numDice, numSides] = formula.split('d').map(Number); if (isNaN(numDice) || isNaN(numSides)) { throw new Error('Invalid dice formula'); } let total = 0; let i = 0; do { total += roll(numSides); } while (++i < numDice); return total; }; const getTokens = (formula) => formula.split(operationRegExp); const resolveToken = (token) => { if (isDiceRoll(token)) { return rollNumberOfDice(token); } if (isInteger(token)) { return parseInt(token, 10); } if (token in Operations) { return Operations[token]; } throw new Error(`Invalid token: ${token}`); }; export const isValidDiceFormula = (formula) => { const diceRollPattern = /^(\d*d\d+|\d+)((\s*[-+]\s*(\d*d\d+|\d+))\s*)*$/; return diceRollPattern.test(formula.trim()); }; export const rollDiceFormula = (formula) => { if (!isValidDiceFormula(formula)) { throw new Error(`Invalid dice formula, allowed characters are +-, numbers and dices (d6 etc.): ${formula}`); } const tokens = getTokens(formula).map(resolveToken); let total = tokens[0]; for (let i = 1; i < tokens.length; i += 2) { const operation = tokens[i]; const value = tokens[i + 1]; if (!Number.isInteger(value) || !Number.isInteger(total) || typeof operation !== 'function') { console.error({ operation, tokens, total, value }); throw new Error('Logic error, cannot parse tokens'); } total = operation(total, value); } return total; }; /** * TODO fix issues, publish */ export const rollDiceFormulaDetailed = (formula) => { if (!isValidDiceFormula(formula)) { // throw new Error('Invalid dice formula, allowed characters are +-, numbers and dices (d6 etc.)') } const subFormulas = formula.split(','); const res = subFormulas.map((subFormula) => { const tokens = getTokens(subFormula).map(resolveToken); // FIXME const res = [subFormula, tokens[0], [tokens[0]]]; for (let i = 1; i < tokens.length; i += 2) { const operation = tokens[i]; const value = tokens[i + 1]; if (!Number.isInteger(value) || !Number.isInteger(res) || typeof operation !== 'function') { console.error({ operation, tokens, total: res, value }); throw new Error('Logic error, cannot parse tokens'); } res[1] = operation(res[1], value); res[2].push(value); } return res; }); return res; }; /** * O(n) Fisher–Yates shuffle (crypto‐secure) */ export const shuffleArray = (arr) => { // clone so we don’t mutate the original const result = arr.slice(); for (let i = result.length - 1; i > 0; i--) { // pick an index in [0..i] const j = secureRandomInteger(0, i); [result[i], result[j]] = [result[j], result[i]]; } return result; };