/**
* @module RandomUtils
*/
import { is } from "./utils.js";
import { seed } from "./seed.js";
const DIE_PARSER = /\d+d\d+/g;
const RANDOM_STRING = /\{[^\{\}]*\|[^\{\}]*\}/g;
const START_DATE = 1571011491488;
/**
* Generates a unique string (not guaranteed to be globally unique, but certainly unique in any
* given context).
*
* @return {String} a context unique string
* @example
* guid()
* => "6v2w2762322fwsds"
*
*/
export function guid() {
return (
Math.floor(seed.random() * Math.pow(10, 15)).toString(36) +
(seed.timestamp() - START_DATE).toString(36)
);
}
/**
* Returns a random number based on a die roll string or a provided number. Math operations are supported.
*
* @param {Number} number the maximum value to return
* @return {Number} a value from 1 to N
* @example
* roll(8)
* => 4
*
* @also
* @param {String} notation a notation for a dice roll
* @return {Number} a die roll value
* @example
* roll("3d6+2")
* => 9
* roll("(2d4*10)+500")
* => 540
*/
function evaluateArithmetic(expr) {
let pos = 0;
function skipWs() {
while (pos < expr.length && expr[pos] === " ") pos++;
}
function parseFactor() {
skipWs();
if (expr[pos] === "(") {
pos++;
const val = parseExpr();
pos++;
skipWs();
return val;
}
const start = pos;
if (expr[pos] === "-") pos++;
while (pos < expr.length && expr[pos] >= "0" && expr[pos] <= "9") pos++;
skipWs();
return parseInt(expr.substring(start, pos), 10);
}
function parseTerm() {
let left = parseFactor();
while (pos < expr.length && (expr[pos] === "*" || expr[pos] === "/")) {
const op = expr[pos++];
left = op === "*" ? left * parseFactor() : left / parseFactor();
}
return left;
}
function parseExpr() {
let left = parseTerm();
while (pos < expr.length && (expr[pos] === "+" || expr[pos] === "-")) {
const op = expr[pos++];
left = op === "+" ? left + parseTerm() : left - parseTerm();
}
return left;
}
return parseExpr();
}
export function roll(value) {
if (typeof value === "number") {
return value > 0 ? ~~(seed.random() * value) + 1 : 0;
} else if (typeof value === "string") {
// Finds and extracts dice notation, rolls the die, and then puts the
// result into the full expression. Then evaluates the arithmetic.
value = value.replace(DIE_PARSER, (value) => {
const split = value.split("d");
const rolls = parseInt(split[0], 10);
const face = parseInt(split[1], 10);
let result = 0;
for (let i = 0; i < rolls; i++) {
result += ~~(seed.random() * face) + 1;
}
return result;
});
try {
return evaluateArithmetic(value);
} catch (e) {
return 0;
}
}
throw new Error("Invalid value: " + value);
}
/**
* Test against a percentage that something will occur.
*
* @example
* if (test(80)) {
* // Happens 80% of the time.
* }
* test(15, 'A', 'B')
* => 'B'
*
* @param percentage {Number} The percentage chance that the function returns true
* @param [value1] {any} the value to return if the test succeeds
* @param [value2] {any} the value to return if the test fails
* @return {Boolean|any} true if test passes, false otherwise
*/
export function test(percentage, onTrue, onFalse) {
if (percentage < 0) {
console.warn(`Adjusting percentage ${percentage} to 0`);
percentage = 0;
}
if (percentage > 100) {
console.warn(`Adjusting percentage ${percentage} to 100`);
percentage = 100;
}
if (onTrue !== undefined || onFalse !== undefined) {
return roll(100) <= percentage ? onTrue : onFalse;
}
return roll(100) <= percentage;
}
/**
* Return a random value from a "strategy" value, recursively. An element will be selected
* from an array, a function will be executed for its return value, and a string with
* variants will be returned with a variant selected. The value is then checked again to
* see if it can still be randomized further, and this process continues until a primitive
* value is returned (a number, object, string, or boolean).
*
* @example
* random("A")
* => "A"
* random(['A','B','C']);
* => 'A'
* random("{Big|Bad|Black} Dog");
* => 'Bad Dog'
* random(() => ["A","B","C"]);
* => "B"
*
* @param value {String|Array|Function} A string with optional variants, or an array from
* which to select an element, or a function that returns a value.
* @return {Object} a single randomized instance, based on the value passed in
*/
export function random(value) {
if (is(value, "Array")) {
value = value.length ? value[~~(seed.random() * value.length)] : null;
return random(value);
} else if (is(value, "Function")) {
return random(value());
} else if (is(value, "String")) {
return value.replace(RANDOM_STRING, function (token) {
return random(token.substring(1, token.length - 1).split("|"));
});
}
return value;
}
/**
* Randomly shuffle the position of the elements in an array (uses Fisher-Yates shuffle). `random()`
* is usually more efficient, but if you need to iterate through a set of values in a random order,
* without traversing the same element more than once, `shuffle()` is a better way to randomize
* your data.
* @example
* shuffle(['A','B','C'])
* => ['C','A','B']
*
* @param array {Array} The array to shuffle (in place)
*/
export function shuffle(array) {
let j, temp;
for (let i = array.length - 1; i > 0; i--) {
j = ~~(seed.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
/**
* A function that takes an array and returns a random element in the array. Does not
* execute functions, select a string out of string expressions, or perform any other
* recursive randomization.
* @param {collection} an array
* @returns one element of the array
*/
export function randomElement(collection) {
return collection[~~(seed.random() * collection.length)];
}
/**
* Generate a whole random integer, on a normal (Gaussian, "bell curve")
* distribution. This random number generator will give more useful results
* than `random()` if you do not want the numbers to be evenly distributed.
* Another option is to use the die roller, e.g. "2d6" is not an even
* distribution.
*
* @param stdev {Number} The amount of variance from the mean, where about
* 68% of the numbers will be a number +/- this amount.
* @param [mean=0] {Number} The mean around which random numbers will be
* generated.
* @param [negative=false] {Boolean} should the returned value be negative?
* This is false by default and will only return positive numbers
*
* @return a random number
*/
export function gaussian(stdev, mean = 0, negative = false, safetyLoops = 1) {
if (safetyLoops > 20) {
throw Error("Too many loops, you’ve probably mocked seed.random");
}
let delta = 0;
if (typeof stdev === "string") {
[stdev, mean, delta] = parseGaussianStr(stdev);
}
let x = 0,
y = 0,
rds,
c;
// Uses Box-Muller transform: https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
// Two values get generated. You could cache the y value, but the time savings
// is trivial and this causes issues when mocking randomness for the tests. So don't.
do {
x = seed.random() * 2 - 1;
y = seed.random() * 2 - 1;
rds = x * x + y * y;
} while (rds === 0 || rds > 1);
c = Math.sqrt((-2 * Math.log(rds)) / rds);
const result = Math.round(x * c * stdev) + mean;
if (!negative && result < 0) {
return gaussian(stdev, mean, negative, ++safetyLoops);
}
return parseInt(result) + delta;
}
function parseGaussianStr(string) {
let delta = 0;
let neg = string.includes("-");
const [stdevStr, meanStr, deltaStr] = string.split(/[\/+\-]/);
if (typeof deltaStr !== "undefined") {
delta = parseInt(deltaStr);
if (neg) {
delta = -delta;
}
}
return [parseInt(stdevStr), parseInt(meanStr), delta];
}