UNPKG

ttrpg-lib-dice

Version:

A small library for rolling dice in tabletop games

142 lines (141 loc) 5.23 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.shuffleArray = exports.rollDiceFormulaDetailed = exports.rollDiceFormula = exports.isValidDiceFormula = exports.roll = exports.getRandomArrayItems = exports.getRandomArrayItem = exports.secureRandomInteger = void 0; const Dice_1 = require("./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) */ 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); }; exports.secureRandomInteger = secureRandomInteger; const getRandomArrayItem = (arr) => { if (!arr.length) { throw new Error('Empty array'); } return arr[(0, exports.secureRandomInteger)(0, arr.length - 1)]; }; exports.getRandomArrayItem = getRandomArrayItem; 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 = (0, exports.secureRandomInteger)(0, arr.length - 1); if (!usedIndices.has(index)) { usedIndices.add(index); result.push(arr[index]); } } return result; }; exports.getRandomArrayItems = getRandomArrayItems; const roll = (dice = Dice_1.Dice.d100) => (0, exports.secureRandomInteger)(1, dice); exports.roll = roll; 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 += (0, exports.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}`); }; const isValidDiceFormula = (formula) => { const diceRollPattern = /^(\d*d\d+|\d+)((\s*[-+]\s*(\d*d\d+|\d+))\s*)*$/; return diceRollPattern.test(formula.trim()); }; exports.isValidDiceFormula = isValidDiceFormula; const rollDiceFormula = (formula) => { if (!(0, exports.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; }; exports.rollDiceFormula = rollDiceFormula; /** * TODO fix issues, publish */ const rollDiceFormulaDetailed = (formula) => { if (!(0, exports.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; }; exports.rollDiceFormulaDetailed = rollDiceFormulaDetailed; /** * O(n) Fisher–Yates shuffle (crypto‐secure) */ 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 = (0, exports.secureRandomInteger)(0, i); [result[i], result[j]] = [result[j], result[i]]; } return result; }; exports.shuffleArray = shuffleArray;