ttrpg-lib-dice
Version:
A small library for rolling dice in tabletop games
142 lines (141 loc) • 5.23 kB
JavaScript
;
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;