From 9c8fc84b869c78ea75b6551551025e61fe98fc9f Mon Sep 17 00:00:00 2001 From: Harry Mustoe-Playfair Date: Tue, 29 Oct 2024 08:51:52 +0000 Subject: [PATCH] feat: Update rng to latest and greatest implementation. Keeps things dependency free as it's still fully embedded. --- dist/types/default/functions.d.ts | 2 +- dist/types/index.d.ts | 4 +- dist/types/number.d.ts | 279 +++++ dist/types/rng.d.ts | 350 ++++-- dist/types/rng/interface.d.ts | 1607 ++++++++++++++++++++++++++ dist/types/rng/pool.d.ts | 44 + dist/types/rng/predictable.d.ts | 64 +- dist/types/rng/queue.d.ts | 32 + dist/types/table.d.ts | 6 +- dist/types/table/pool.d.ts | 10 +- dist/types/table/pool/entry.d.ts | 7 +- dist/types/ultraloot.d.ts | 12 +- dist/ultraloot.cjs | 1763 +++++++++++++++++++++++++--- dist/ultraloot.js | 1750 +++++++++++++++++++++++++--- dist/ultraloot.min.js | 2 +- dist/ultraloot.mjs | 1770 ++++++++++++++++++++++++++--- package.json | 4 +- src/default/conditions.ts | 1 - src/default/functions.ts | 3 +- src/index.ts | 4 +- src/number.ts | 465 ++++++++ src/rng.ts | 1311 +++++++++++++++++---- src/rng/interface.ts | 1550 +++++++++++++++++++++++++ src/rng/pool.ts | 116 ++ src/rng/predictable.ts | 75 +- src/rng/queue.ts | 116 ++ src/table.ts | 9 +- src/table/pool.ts | 13 +- src/table/pool/entry.ts | 7 +- src/ultraloot.ts | 19 +- tests/rng.test.ts | 577 ---------- tests/rng/chancy.test.ts | 360 ++++++ tests/rng/distributions.test.ts | 750 ++++++++++++ tests/rng/numbervalidator.test.ts | 138 +++ tests/rng/pool.test.ts | 143 +++ tests/rng/rng.test.ts | 885 +++++++++++++++ 36 files changed, 12821 insertions(+), 1427 deletions(-) create mode 100644 dist/types/number.d.ts create mode 100644 dist/types/rng/interface.d.ts create mode 100644 dist/types/rng/pool.d.ts create mode 100644 dist/types/rng/queue.d.ts create mode 100644 src/number.ts create mode 100644 src/rng/interface.ts create mode 100644 src/rng/pool.ts create mode 100644 src/rng/queue.ts delete mode 100644 tests/rng.test.ts create mode 100644 tests/rng/chancy.test.ts create mode 100644 tests/rng/distributions.test.ts create mode 100644 tests/rng/numbervalidator.test.ts create mode 100644 tests/rng/pool.test.ts create mode 100644 tests/rng/rng.test.ts diff --git a/dist/types/default/functions.d.ts b/dist/types/default/functions.d.ts index 794f019..ff98791 100644 --- a/dist/types/default/functions.d.ts +++ b/dist/types/default/functions.d.ts @@ -1,4 +1,4 @@ -import { RngInterface } from './../rng'; +import { RngInterface } from './../rng/interface'; import LootTableEntryResult from './../table/pool/entry/result'; type InheritLooterSignature = ({ looted, looter, args }: { looted: LootTableEntryResult; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts index d82e40a..2f618d4 100644 --- a/dist/types/index.d.ts +++ b/dist/types/index.d.ts @@ -1,5 +1,7 @@ export * from './ultraloot'; +export * from './number'; export * from './rng'; +export * from './rng/interface'; export * from './rng/predictable'; export * from './table'; export * from './table/manager'; @@ -8,7 +10,7 @@ export * from './table/pool/entry'; export * from './table/pool/entry/result'; export * from './table/pool/entry/results'; import { UltraLoot } from './ultraloot'; -export { UltraLoot as UltraLoot }; +export { UltraLoot }; export { default as Rng } from './rng'; export { default as PredictableRng } from './rng/predictable'; export { default as LootTable } from './table'; diff --git a/dist/types/number.d.ts b/dist/types/number.d.ts new file mode 100644 index 0000000..c1852c4 --- /dev/null +++ b/dist/types/number.d.ts @@ -0,0 +1,279 @@ +/** + * @category Number Validator + */ +export declare class NumberValidationError extends Error { +} +/** + * @category Number Validator + */ +export declare class ArrayNumberValidator { + #private; + /** + * Descriptive name for this validation + */ + name: string; + constructor(numbers: number[], name?: string); + get numbers(): number[]; + set numbers(numbers: number[]); + /** + * Specify the numbers to validate + */ + all(numbers: number[]): this; + /** + * Specify the numbers to validate + */ + validate(numbers: number | number[]): this | NumberValidator; + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potatoes = [0, 1]; + * validate(potatoes).varname('potatoes').gt(2); // "Expected every component of potatoes to be > 2, got 0" + */ + varname(name: string): this; + /** + * Get the sum of our numbers + */ + sum(): number; + /** + * Validates whether the total of all of our numbers is close to sum, with a maximum difference of diff + * @param sum The sum + * @param diff The maximum difference + * @param msg Error message + * @throws {@link NumberValidationError} If they do not sum close to the correct amount + */ + sumcloseto(sum: number, diff?: number, msg?: string): this; + /** + * Validates whether the total of all of our numbers is equal (===) to sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to the correct amount + */ + sumto(sum: number, msg?: string): this; + /** + * Validates whether the total of all of our numbers is < sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to < sum + */ + sumtolt(sum: number, msg?: string): this; + /** + * Validates whether the total of all of our numbers is > sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to > sum + */ + sumtogt(sum: number, msg?: string): this; + /** + * Validates whether the total of all of our numbers is <= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to <= sum + */ + sumtolteq(sum: number, msg?: string): this; + /** + * Validates whether the total of all of our numbers is >= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to >= sum + */ + sumtogteq(sum: number, msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all integers + */ + int(msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all positive + */ + positive(msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all negative + */ + negative(msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all between from and to + */ + between(from: number, to: number, msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all between or equal to from and to + */ + betweenEq(from: number, to: number, msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all > n + */ + gt(n: number, msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all >= n + */ + gteq(n: number, msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all < n + */ + lt(n: number, msg?: string): this; + /** + * @throws {@link NumberValidationError} if numbers are not all <= n + */ + lteq(n: number, msg?: string): this; +} +/** + * Validate numbers in a fluent fashion. + * + * Each validator method accepts a message as the last parameter + * for customising the error message. + * + * @category Number Validator + * + * @example + * const n = new NumberValidator(); + * n.validate(0).gt(1); // NumberValidationError + * + * @example + * const n = new NumberValidator(); + * const probability = -0.1; + * n.validate(probability).gteq(0, 'Probabilities should always be >= 0'); // NumberValidationError('Probabilities should always be >= 0'). + */ +export declare class NumberValidator { + #private; + /** + * The name of the variable being validated - shows up in error messages. + */ + name: string; + constructor(number?: number, name?: string); + get number(): number | undefined; + set number(number: number | undefined); + /** + * Returns an ArrayNumberValidator for all the numbers + */ + all(numbers: number[], name?: string): ArrayNumberValidator; + assertNumber(num?: number): num is number; + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potato = 1; + * validate(potato).varname('potato').gt(2); // "Expected potato to be greater than 2, got 1" + * @param {string} name [description] + */ + varname(name: string): this; + /** + * Specify the number to be validated + */ + validate(number: number | number[]): ArrayNumberValidator | this; + /** + * Asserts that the number is an integer + * @throws {@link NumberValidationError} if ths number is not an integer + */ + int(msg?: string): this; + /** + * Asserts that the number is > 0 + * @throws {@link NumberValidationError} if the number is not positive + */ + positive(msg?: string): this; + /** + * Asserts that the number is < 0 + * @throws {@link NumberValidationError} if the number is not negative + */ + negative(msg?: string): this; + /** + * Asserts that the from < number < to + * @throws {@link NumberValidationError} if it is outside or equal to those bounds + */ + between(from: number, to: number, msg?: string): this; + /** + * Asserts that the from <= number <= to + * @throws {@link NumberValidationError} if it is outside those bounds + */ + betweenEq(from: number, to: number, msg?: string): this; + /** + * Asserts that number > n + * @throws {@link NumberValidationError} if it is less than or equal to n + */ + gt(n: number, msg?: string): this; + /** + * Asserts that number >= n + * @throws {@link NumberValidationError} if it is less than n + */ + gteq(n: number, msg?: string): this; + /** + * Asserts that number < n + * @throws {@link NumberValidationError} if it is greater than or equal to n + */ + lt(n: number, msg?: string): this; + /** + * Asserts that number <= n + * @throws {@link NumberValidationError} if it is greater than n + */ + lteq(n: number, msg?: string): this; +} +/** + * Validates a number or an array of numbers, with a fluent interface. + * + * If passed an array, it will return an ArrayNumberValidator + * + * If passed anything else, it will return a NumberValidator + * + * You can pass the things in as a one key object, and it will automatically + * set the name for you. + * + * @category Number Validator + * + * @example + * // Validate single numbers + * validate(2).gt(1).lt(3); // doesn't throw + * validate(3).gt(1).lt(3); // throws + * + * @example + * // Validate in the object fashion so it automatically sets the name + * let myVar = 5; + * validate({ myVar }).gt(10); // throws `Expected myVar to be greater than 10, got 5` + * + * @example + * // Also used with arrays of numbers + * validate([1, 2, 3]).lt(10); // doesn't throw + * validate([1, 2, 3]).sumto(6); // doesn't throw + * validate([1, 2, 3, 4]).sumtolt(9); // throws + * + * @example + * // All single number validations + * validate(1).int(); + * validate(1).positive(); + * validate(-1).negative(); + * validate(1).between(0, 2); + * validate(1).betweenEq(1, 2); + * validate(1).gt(0); + * validate(1).gteq(1); + * validate(1).lt(2); + * validate(1).lteq(1); + * + * @example + * // All array of numbers validations + * validate([1, 2, 3]).sumcloseto(6); + * validate([1, 2, 3.0001]).sumcloseto(6, 0.001); + * validate([1, 2, 3]).sumto(6); + * validate([1, 2, 3]).sumtolt(7); + * validate([1, 2, 3]).sumtogt(5); + * validate([1, 2, 3]).sumtolteq(6); + * validate([1, 2, 3]).sumtogteq(1); + * validate([1, 2, 3]).int(); + * validate([1, 2, 3]).positive(); + * validate([-1, -2, -4]).negative(); + * validate([1, 2, 3]).between(0, 4); + * validate([1, 2, 3]).betweenEq(1, 3); + * validate([1, 2, 3]).gt(0); + * validate([1, 2, 3]).gteq(1); + * validate([1, 2, 3]).lt(4); + * validate([1, 2, 3]).lteq(3); + * + * @see {@link NumberValidator} + * @see {@link ArrayNumberValidator} + * @param {number | number[]} number [description] + */ +declare function validate(number?: Record): NumberValidator; +declare function validate(number?: Record): ArrayNumberValidator; +declare function validate(number?: number): NumberValidator; +declare function validate(number?: number[]): ArrayNumberValidator; +export default validate; diff --git a/dist/types/rng.d.ts b/dist/types/rng.d.ts index 4e94a98..106773e 100644 --- a/dist/types/rng.d.ts +++ b/dist/types/rng.d.ts @@ -1,136 +1,294 @@ -export interface RandomInterface { - random(): number; -} -export interface DiceInterface { - n: number; - d: number; - plus: number; -} -/** - * @interface - * @prop mean Used for "normal" type chancy results to determine the mean - * @prop stddev Used for "normal" type chancy results to determine the stddev - * @prop min The minimum possible result - * @prop max The maximum possible result - * @prop type The type of result, can be "normal", "normal_int", "integer" or "random" - * @prop power The power factor to pass to the random function - basically skews results one way or the other - * @prop skew Skew to use when using a "normal" or "normal_int" distribution - */ -export interface ChancyInterface { - mean?: number; - stddev?: number; - min?: number; - max?: number; - type?: string; - skew?: number; -} -export type Chancy = ChancyInterface | string | number; -export type Seed = string | number; -export interface RngInterface { - predictable(seed?: Seed): RngInterface; - hashStr(str: string): string | number; - convertStringToNumber(str: string): number; - getSeed(): number; - sameAs(other: RngInterface): boolean; - seed(seed: Seed): this; - percentage(): number; - random(from?: number, to?: number, skew?: number): number; - chance(n: number, chanceIn?: number): boolean; - chanceTo(from: number, to: number): boolean; - randInt(from?: number, to?: number, skew?: number): number; - uniqid(prefix?: string, random?: boolean): string; - uniqstr(len?: number): string; - randBetween(from: number, to: number, skew: number): number; - normal(args?: NormalArgs): number; - chancyInt(input: Chancy): number; - chancy(input: Chancy): number; - choice(data: Array): any; - weightedChoice(data: Record | Array | Map): any; - dice(n: string | DiceInterface | number, d?: number, plus?: number): number; - parseDiceString(string: string): DiceInterface; - clamp(number: number, lower: number, upper: number): number; - bin(val: number, bins: number, min: number, max: number): number; - serialize(): any; -} -export interface RngConstructor { - new (seed?: Seed): RngInterface; - unserialize(rng: any): RngInterface; - chancyMin(input: Chancy): number; - chancyMax(input: Chancy): number; - parseDiceString(string: string): DiceInterface; - diceMin(n: string | DiceInterface | number, d?: number, plus?: number): number; - diceMax(n: string | DiceInterface | number, d?: number, plus?: number): number; -} +import Pool from './rng/pool'; +import { DiceInterface, Distribution, Chancy, ChancyNumeric, Seed, Randfunc, RngInterface, RngDistributionsInterface } from './rng/interface'; export interface SerializedRng { mask: number; seed: number; m_z: number; } -export type NormalArgs = { - mean?: number; - stddev?: number; - max?: number; - min?: number; - skew?: number; - skewtype?: string; -}; -export declare abstract class RngAbstract implements RngInterface { +export declare class MaxRecursionsError extends Error { +} +export declare class NonRandomRandomError extends Error { +} +/** + * This abstract class implements most concrete implementations of + * functions, as the only underlying changes are likely to be to the + * uniform random number generation, and how that is handled. + * + * All the typedoc documentation for this has been sharded out to RngInterface + * in a separate file. + */ +export declare abstract class RngAbstract implements RngInterface, RngDistributionsInterface { #private; constructor(seed?: Seed); getSeed(): number; - sameAs(other: RngAbstract): boolean; + sameAs(other: RngInterface): boolean; + randomSource(source?: Randfunc | null): this; + getRandomSource(): Randfunc | null | undefined; protected setSeed(seed?: Seed): this; seed(seed?: Seed): this; serialize(): any; + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized: SerializedRng): RngInterface; predictable(seed?: Seed): RngInterface; + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ static predictable(this: new (seed: Seed) => T, seed: Seed): T; - hashStr(str: string): number; - convertStringToNumber(str: string): number; + protected hashStr(str: string): number; + protected convertStringToNumber(str: string): number; protected _random(): number; + /** + * Internal source of uniformly distributed random numbers between 0 and 1, [0, 1) + * + * Simplest implementation would be Math.random() + */ + protected abstract _next(): number; percentage(): number; + probability(): number; random(from?: number, to?: number, skew?: number): number; chance(n: number, chanceIn?: number): boolean; chanceTo(from: number, to: number): boolean; randInt(from?: number, to?: number, skew?: number): number; - uniqid(prefix?: string, random?: boolean): string; - uniqstr(len?: number): string; + uniqid(prefix?: string): string; + randomString(len?: number): string; randBetween(from?: number, to?: number, skew?: number): number; scale(number: number, from: number, to: number, min?: number, max?: number): number; scaleNorm(number: number, from: number, to: number): number; shouldThrowOnMaxRecursionsReached(): boolean; - normal({ mean, stddev, max, min, skew }?: NormalArgs, depth?: number): number; - boxMuller(mean?: number, stddev?: number): number; + shouldThrowOnMaxRecursionsReached(val: boolean): this; + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + * @throws {@link MaxRecursionsError} If the function recurses too many times in trying to generate in bounds numbers + */ + normal({ mean, stddev, max, min, skew }?: { + mean?: number; + stddev?: number; + max?: number; + min?: number; + skew?: number; + }, depth?: number): number; + gaussian({ mean, stddev, skew }?: { + mean?: number; + stddev?: number; + skew?: number; + }): number; + boxMuller(mean?: number | { + mean?: number; + stddev?: number; + }, stddev?: number): number; + irwinHall(n?: number | { + n?: number; + }): number; + bates(n?: number | { + n?: number; + }): number; + batesgaussian(n?: number | { + n?: number; + }): number; + bernoulli(p?: number | { + p?: number; + }): number; + exponential(rate?: number | { + rate?: number; + }): number; + pareto({ shape, scale, location }?: { + shape?: number; + scale?: number; + location?: number; + }): number; + poisson(lambda?: number | { + lambda?: number; + }): number; + hypergeometric({ N, K, n, k }?: { + N?: number; + K?: number; + n?: number; + k?: number; + }): number; + rademacher(): -1 | 1; + binomial({ n, p }?: { + n?: number; + p?: number; + }): number; + betaBinomial({ alpha, beta, n }?: { + alpha?: number; + beta?: number; + n?: number; + }): number; + beta({ alpha, beta }?: { + alpha?: number; + beta?: number; + }): number; + gamma({ shape, rate, scale }?: { + shape?: number; + rate?: number; + scale?: number; + }): number; + studentsT(nu?: number | { + nu?: number; + }): number; + wignerSemicircle(R?: number | { + R?: number; + }): number; + kumaraswamy({ alpha, beta }?: { + alpha?: number; + beta?: number; + }): number; + hermite({ lambda1, lambda2 }?: { + lambda1?: number; + lambda2?: number; + }): number; + chiSquared(k?: number | { + k?: number; + }): number; + rayleigh(scale?: number | { + scale?: number; + }): number; + logNormal({ mean, stddev }?: { + mean?: number; + stddev?: number; + }): number; + cauchy({ median, scale }?: { + median?: number; + scale?: number; + }): number; + laplace({ mean, scale }?: { + mean?: number; + scale?: number; + }): number; + logistic({ mean, scale }?: { + mean?: number; + scale?: number; + }): number; + /** + * Returns the support of the given distribution. + * + * @see [Wikipedia - Support (mathematics)](https://en.wikipedia.org/wiki/Support_(mathematics)#In_probability_and_measure_theory) + */ + support(distribution: Distribution): string | undefined; chancyInt(input: Chancy): number; - chancy(input: Chancy): number; + chancy(input: T[], depth?: number): T; + chancy(input: ChancyNumeric, depth?: number): number; + private chancyMinMax; + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + chancyMin(input: Chancy): number; + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + chancyMax(input: Chancy): number; + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ static chancyMin(input: Chancy): number; + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ static chancyMax(input: Chancy): number; choice(data: Array): any; + weights(data: Array): Map; + weightedChoice(data: Record | Array | Map): any; + pool(entries?: T[]): Pool; + protected static parseDiceArgs(n?: string | Partial | number | number[], d?: number, plus?: number): DiceInterface; + protected parseDiceArgs(n?: string | Partial | number | number[], d?: number, plus?: number): DiceInterface; /** - * data format: - * { - * choice1: 1, - * choice2: 2, - * choice3: 3, - * } + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities */ - weightedChoice(data: Record | Array | Map): any; - protected static parseDiceArgs(n?: string | DiceInterface | number | number[], d?: number, plus?: number): DiceInterface; - parseDiceArgs(n?: string | DiceInterface | number | number[], d?: number, plus?: number): DiceInterface; static parseDiceString(string: string): DiceInterface; - static diceMax(n?: string | DiceInterface | number | number[], d?: number, plus?: number): number; - static diceMin(n?: string | DiceInterface | number | number[], d?: number, plus?: number): number; - dice(n?: string | DiceInterface | number | number[], d?: number, plus?: number): number; + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + diceMax(n?: string | Partial | number | number[], d?: number, plus?: number): number; + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + diceMin(n?: string | Partial | number | number[], d?: number, plus?: number): number; + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + static diceMax(n?: string | Partial | number | number[], d?: number, plus?: number): number; + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + static diceMin(n?: string | Partial | number | number[], d?: number, plus?: number): number; + diceExpanded(n?: string | Partial | number | number[], d?: number, plus?: number): { + dice: number[]; + plus: number; + total: number; + }; + dice(n?: string | Partial | number | number[], d?: number, plus?: number): number; + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ parseDiceString(string: string): DiceInterface; - clamp(number: number, lower: number, upper: number): number; + clamp(number: number, lower?: number, upper?: number): number; bin(val: number, bins: number, min: number, max: number): number; } -export default class Rng extends RngAbstract implements RngInterface { +/** + * @category Main Class + */ +declare class Rng extends RngAbstract implements RngInterface, RngDistributionsInterface { #private; constructor(seed?: Seed); + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ + static predictable(this: new (seed: Seed) => Rng, seed: Seed): Rng; serialize(): any; - sameAs(other: Rng): boolean; + sameAs(other: any): boolean; + /** @hidden */ + getMask(): number; + /** @hidden */ + getMz(): number; + /** @hidden */ + setMask(mask: number): void; + /** @hidden */ + setMz(mz: number): void; + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized: SerializedRng): Rng; seed(i?: Seed): this; - protected _random(): number; + protected _next(): number; } +export default Rng; diff --git a/dist/types/rng/interface.d.ts b/dist/types/rng/interface.d.ts new file mode 100644 index 0000000..3b12d42 --- /dev/null +++ b/dist/types/rng/interface.d.ts @@ -0,0 +1,1607 @@ +import Pool from './pool'; +export interface DiceInterface { + n: number; + d: number; + plus: number; +} +export interface DiceReturnInterface { + dice: number[]; + plus: number; + total: number; +} +/** + * Distributions allowed for use in chancy + * @type {String} + */ +export type Distribution = 'normal' | 'boxMuller' | 'gaussian' | keyof RngDistributionsInterface; +/** + * Most of the properties here are used to pass on to the resepective distribution functions. + * + * This basically takes the form: + * + * ```ts + * type ChancyInterface = { + * type?: string, // name of a distribution - must be valid + * min?: number, // always available + * max?: number, // always available + * ...otherArgs, // The relevant args for the distribution named above, all optional + * } + * ``` + * + * Specifying it this way allows TypeScript to perform a sort of validation for objects and + * parameters passed to the chancy function, but will require a bit of maintenance on the user + * end. + * + * @privateRemarks + * + * Unfortunately, this **does** mean we have to maintain call signatures in two places - both + * in the distribution functions, and here, because it's tightly coupled to the way Chancy can + * be called - but this added complexity in development allows for flexibility for the end user. + * + * @see {@link RngInterface.chancy} + */ +export type ChancyInterface = ((({ + type?: 'random'; + skew?: number; +}) | ({ + type: 'integer' | 'int'; + skew?: number; +}) | ({ + type: 'dice'; + dice?: string; + n?: number; + d?: number; + plus?: number; +}) | ({ + type: 'normal' | 'normal_int' | 'normal_integer'; + mean?: number; + stddev?: number; + skew?: number; +}) | ({ + type: 'bates'; + n?: number; +}) | ({ + type: 'batesgaussian'; + n?: number; +}) | ({ + type: 'bernoulli'; + p?: number; +}) | ({ + type: 'beta'; + alpha?: number; + beta?: number; +}) | ({ + type: 'betaBinomial'; + alpha?: number; + beta?: number; + n?: number; +}) | ({ + type: 'binomial'; + n?: number; + p?: number; +}) | ({ + type: 'boxMuller'; + mean?: number; + stddev?: number; +}) | ({ + type: 'cauchy'; + median?: number; + scale?: number; +}) | ({ + type: 'chiSquared'; + k?: number; +}) | ({ + type: 'exponential'; + rate?: number; +}) | ({ + type: 'gamma'; + shape?: number; + rate?: number; + scale?: number; +}) | ({ + type: 'gaussian'; + mean?: number; + stddev?: number; + skew?: number; +}) | ({ + type: 'hermite'; + lambda1?: number; + lambda2?: number; +}) | ({ + type: 'hypergeometric'; + N?: number; + K?: number; + n?: number; + k?: number; +}) | ({ + type: 'irwinHall'; + n?: number; +}) | ({ + type: 'kumaraswamy'; + alpha?: number; + beta?: number; +}) | ({ + type: 'laplace'; + mean?: number; + scale?: number; +}) | ({ + type: 'logistic'; + mean?: number; + scale?: number; +}) | ({ + type: 'logNormal'; + mean?: number; + stddev?: number; +}) | ({ + type: 'pareto'; + shape?: number; + scale?: number; + location?: number; +}) | ({ + type: 'poisson'; + lambda?: number; +}) | ({ + type: 'rademacher'; +}) | ({ + type: 'rayleigh'; + scale?: number; +}) | ({ + type: 'studentsT'; + nu?: number; +}) | ({ + type: 'wignerSemicircle'; + R?: number; +})) & { + min?: number; + max?: number; +}); +/** + * Chancy inputs that lead to strictly numeric results. + * @see {@link RngInterface.chancy} + */ +export type ChancyNumeric = ChancyInterface | string | number | number[]; +/** + * Special Chancy type - for feeding to the 'chancy' function + * @see {@link RngInterface.chancy} + */ +export type Chancy = ChancyNumeric | any[]; +/** + * Valid seeds for feeding to the RNG + */ +export type Seed = string | number; +/** + * Interface for a random function that returns a number, given no arguments + * that is uniformly distributed. + */ +export interface Randfunc { + (): number; +} +/** + * Basic interface required for Rng implementations. + * + * Use this as an interface if you don't need all the advanced distributions + */ +export interface RngInterface { + /** + * Whether this is the same as another object interfacting RngInterface, i.e. + * they will generate the same next number. + * @param other The thing to compare + */ + sameAs(other: RngInterface): boolean; + /** + * Seed the random number generator with the given seed. + * + * @group Seeding + * @param {Seed} seed Can be a string or a number + */ + seed(seed: Seed): this; + /** + * Get the current seed. + * + * Note this may not be the same as the set seed if numbers have been generated + * since its inception. Also, strings are usually transformed to numbers. + * + * @group Seeding + * @return The current seed + */ + getSeed(): number; + /** + * Whether we are going to throw if max recursions is reached + */ + shouldThrowOnMaxRecursionsReached(): boolean; + /** + * Sets whether we should throw if max recursions is reached. + */ + shouldThrowOnMaxRecursionsReached(val: boolean): this; + /** + * Create a new instance. Will use a globally set seed, so every instance + * returnd by this should generate the same numbers. + * + * @group Seeding + * @return An Rng instance, set to the given seed + */ + predictable(): RngInterface; + /** + * Create a new instance with the given seed + * + * @group Seeding + * @param seed + * @return An Rng instance, set to the given seed + */ + predictable(seed: Seed): RngInterface; + /** + * Pass a function that will return uniform random numbers as the source + * of this rng's randomness. + * + * Supersedes and seed setting. + * + * @example + * + * const rng = new Rng(); + * rng.randomSource(() => 1); + * + * assert(rng.random() === 1); // true + * + * @group Seeding + * @param func The random function + */ + randomSource(func?: Randfunc): this; + /** + * Returns a unique 14 character string. + * + * Highly collision resistant, and strictly incrementing + * + * Useful for using as object IDs or HTML ids (with prefix). + * + * @group Utilities + * @param prefix A prefix to include on the output + * @return a 14 character string + */ + uniqid(prefix?: string): string; + /** + * Returns a unique string of length len + * + * @group Utilities + * @param len Length of the string to generate + * @return A string of length "len" + */ + randomString(len?: number): string; + /** + * Scales a number from [min, max] to [from, to]. + * + * Some might call this linear interpolation. + * + * Min and max default to 0 and 1 respectively + * + * @example + * rng.scale(0.5, 100, 200); // 150 + * rng.scale(0, 100, 200); // 100 + * rng.scale(1, 100, 200); // 200 + * rng.scale(5, 100, 200, 0, 10); // 150 + * + * @group Utilities + * @param number The number - must be 0 <= number <= 1 + * @param from The min number to scale to. When number === min, will return from + * @param to The max number to scale to. When number === max, will return to + * @param min The minimum number can take, default 0 + * @param max The maximum number can take, default 1 + * @return A number scaled to the interval [from, to] + */ + scale(number: number, from: number, to: number, min?: number, max?: number): number; + /** + * Scales a number from [0, 1] to [from, to]. + * + * Some might call this linear interpolation + * + * @example + * rng.scaleNorm(0.5, 100, 200); // 150 + * rng.scaleNorm(0, 100, 200); // 100 + * rng.scaleNorm(1, 100, 200); // 200 + * + * @group Utilities + * @param number The number - must be 0 <= number <= 1 + * @param from The min number to scale to. When number === 0, will return from + * @param to The max number to scale to. When number === 1, will return to + * @return A normal number scaled to the interval [from, to] + */ + scaleNorm(number: number, from: number, to: number): number; + /** + * Alias of randBetween + * + * @group Random Number Generation + * @see {@link randBetween} + */ + random(from?: number, to?: number, skew?: number): number; + /** + * @group Random Number Generation + * @return A random number from [0, 1) + */ + randBetween(): number; + /** + * @group Random Number Generation + * @param from Lower bound, inclusive + * @return A random number from [from, from+1) + */ + randBetween(from?: number): number; + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randBetween(0, 10); + * // is the same as + * rng.randBetween(10, 0); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, exclusive + * @return A random number from [from, to) + */ + randBetween(from?: number, to?: number): number; + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randBetween(0, 10, 0); + * // is the same as + * rng.randBetween(10, 0, 0); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, exclusive + * @param skew A number by which the numbers should skew. Negative skews towards from, and positive towards to. + * @return A random number from [from, to) skewed a bit skew direction + */ + randBetween(from: number, to: number, skew: number): number; + /** + * @group Random Number Generation + * @return A random integer from [0, 1] + */ + randInt(): number; + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randInt(0, 10); + * // is the same as + * rng.randInt(10, 0); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, inclusive + * @return A random integer from [from, to] + */ + randInt(from?: number, to?: number): number; + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randInt(0, 10, 1); + * // is the same as + * rng.randInt(10, 0, 1); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, inclusive + * @param skew A number by which the numbers should skew. Negative skews towards from, and positive towards to. + * @return A random integer from [from, to] skewed a bit in skew direction + */ + randInt(from?: number, to?: number, skew?: number): number; + /** + * @group Random Number Generation + * @return A percentage [0, 100] + */ + percentage(): number; + /** + * @group Random Number Generation + * @return A probability [0, 1] + */ + probability(): number; + /** + * Results of an "n" in "chanceIn" chance of something happening. + * + * @example + * A "1 in 10" chance would be: + * + * ```ts + * rng.chance(1, 10); + * ``` + * + * @group Boolean Results + * @param n Numerator + * @param chanceIn Denominator + * @return Success or not + */ + chance(n: number, chanceIn?: number): boolean; + /** + * Results of an "from" to "to" chance of something happening. + * + * @example + * A "500 to 1" chance would be: + * + * ```ts + * rng.chanceTo(500, 1); + * ``` + * + * @group Boolean Results + * @param from Left hand side + * @param to Right hand side + * @return Success or not + */ + chanceTo(from: number, to: number): boolean; + /** + * The chancy function has a very flexible calling pattern. + * + * You can pass it a dice string, an object or a number. + * + * * If passed a dice string, it will do a roll of that dice. + * * If passed a number, it will return that number + * * If passed a config object, it will return a randomly generated number based on that object + * + * The purpose of this is to have easily serialised random signatures that you can pass to a single + * function easily. + * + * All chancy distribution functions (that is, when called with ChancyInterface) can be called with min and + * max parameters, however, it's highly advised to tune your parameters someway else. + * + * Basically, the function will just keep resampling until a figure inside the range is generated. This can + * quickly lead to large recursion depths for out of bounds inputs, at which point an error is thrown. + * + * @example + * + * rng.chancy(1); // returns 1 + * rng.chancy('1d6'); // returns an int between 1 and 6 [1, 6] + * + * @example + * + * rng.chancy({ min: 10 }); // Equivalent to calling rng.random(10, Number.MAX_SAFE_INTEGER) + * rng.chancy({ max: 10 }); // Equivalent to calling rng.random(0, 10) + * rng.chancy({ min: 0, max: 1 }); // Equivalent to calling rng.random(0, 1) + * + * @example + * + * rng.chancy({ type: 'integer', min: 10 }); // Equivalent to calling rng.randInt(10, Number.MAX_SAFE_INTEGER) + * rng.chancy({ type: 'integer', max: 10 }); // Equivalent to calling rng.randInt(0, 10) + * rng.chancy({ type: 'integer', min: 10, max: 20 }); // Equivalent to calling rng.randInt(10, 20) + * + * @example + * + * rng.chancy({ type: 'normal', ...args }); // Equivalent to calling rng.normal(args) + * rng.chancy({ type: 'normal_integer', ...args }); // Equivalent to calling Math.floor(rng.normal(args)) + * + * @example + * + * // You can call any of the 'distribution' type functions with chancy as well. + * rng.chancy({ type: 'boxMuller', ...args }); // Equivalent to calling rng.boxMuller(args) + * rng.chancy({ type: 'bates', ...args }); // Equivalent to calling rng.bates(args) + * rng.chancy({ type: 'exponential', ...args }); // Equivalent to calling rng.exponential(args) + * + * @example + * + * This is your monster file ```monster.json```: + * + * ```json + * { + * "id": "monster", + * "hp": {"min": 1, "max": 6, "type": "integer"}, + * "attack": "1d4" + * } + * ``` + * + * How about a stronger monster, with normally distributed health ```strong_monster.json```: + * + * ```json + * { + * "id": "strong_monster", + * "hp": {"min": 10, "max": 20, "type": "normal_integer"}, + * "attack": "1d6+1" + * } + * ``` + * + * Or something like this for ```boss_monster.json``` which has a fixed HP: + * + * ```json + * { + * "id": "boss_monster", + * "hp": 140, + * "attack": "2d10+4" + * } + * ``` + * + * Then in your code: + * + * ```ts + * import {Rng, Chancy} from GameRng; + * + * const rng = new Rng(); + * + * class Monster { + * hp = 10; + * id; + * attack = '1d4'; + * constructor ({id, hp = 10, attack} : {id: string, hp: Chancy, attack: Chancy} = {}) { + * this.id = options.id; + * this.hp = rng.chancy(hp); + * if (attack) this.attack = attack; + * } + * attack () { + * return rng.chancy(this.attack); + * } + * } + * + * const spec = await fetch('strong_monster.json').then(a => a.json()); + * const monster = new Monster(spec); + * ``` + * + * @see {@link Chancy} for details on input types + * @see {@link RngDistributionsInterface} for details on the different distributions that can be passed to the "type" param + * @group Random Number Generation + * @param input Chancy type input + * @return A randomly generated number or an element from a passed array + */ + chancy(input: ChancyNumeric): number; + chancy(input: T[]): T; + /** + * Rounds the results of a chancy call so that it's always an integer. + * + * Not _quite_ equivalent to Math.round(rng.chancy(input)) because it will also + * transform {type: 'random'} to {type: 'integer'} which aren't quite the same. + * + * 'random' has a range of [min, max) whereas interger is [min, max] (inclusive of max). + * + * @group Random Number Generation + * @see {@link chancy} + */ + chancyInt(input: Chancy): number; + /** + * Determines the minimum value a Chancy input can take. + * + * @group Result Prediction + * @param input Chancy input + * @return Minimum value a call to chancy with these args can take + */ + chancyMin(input: Chancy): number; + /** + * Determines the maximum value a Chancy input can take. + * + * @group Result Prediction + * @param input Chancy input + * @return Maximum value a call to chancy with these args can take + */ + chancyMax(input: Chancy): number; + /** + * Outputs what the distribution supports in terms of output + */ + support(input: Distribution): string | undefined; + /** + * Takes a random choice from an array of values, with equal weight. + * + * @group Choices + * @param data The values to choose from + * @return The random choice from the array + */ + choice(data: Array): any; + /** + * Given an array, gives a key:weight Map of entries in the array based + * on how many times they appear in the array. + * + * @example + * + * const weights = rng.weights(['a', 'b', 'c', 'a']); + * assert(weights['a'] === 2); + * assert(weights['b'] === 1); + * assert(weights['c'] === 1); + * + * @group Utilities + * @param data The values to choose from + * @return The weights of the array + */ + weights(data: Array): Map; + /** + * Takes a random key from an object with a key:number pattern + * + * Using a Map allows objects to be specified as keys, can be useful for + * choosing between concrete objects. + * + * @example + * + * Will return: + * + * * 'a' 1/10 of the time + * * 'b' 2/10 of the time + * * 'c' 3/10 of the time + * * 'd' 3/10 of the time + * + * ```ts + * rng.weightedChoice({ + * a: 1, + * b: 2, + * c: 3, + * d: 4 + * }); + * ``` + * + * @example + * + * Will return: + * + * * diamond 1/111 of the time + * * ruby 10/111 of the time + * * pebble 100/111 of the time + * + * ```ts + * const diamond = new Item('diamond'); + * const ruby = new Item('ruby'); + * const pebble = new Item('pebble'); + * + * const choices = new Map(); + * choices.set(diamond, 1); + * choices.set(ruby, 10); + * choices.set(pebble, 100); + * + * rng.weightedChoice(choices); + * ``` + * + * @group Choices + * @see [Map Object - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + * @see [Map Object - w3schools](https://www.w3schools.com/js/js_maps.asp) + * @param data The values to choose from + * @return The random choice from the array + */ + weightedChoice(data: Record | Array | Map): any; + /** + * Returns a Pool with the given entries based on this RNG. + * + * Pools allow you to draw (without replacement) from them. + * + * @example + * + * const pool = rng.pool(['a', 'b', 'c', 'd']); + * + * pool.draw(); // 'b' + * pool.draw(); // 'c' + * pool.draw(); // 'a' + * pool.draw(); // 'd' + * pool.draw(); // PoolEmptyError('No more elements left to draw from in pool.') + * + * @param entries An array of anything + */ + pool(entries?: T[]): Pool; + /** + * Rolls dice and returns the results in an expanded format. + * + * @example + * rng.diceExpanded('2d6+1'); // returns { dice: [3, 5], plus: 1, total: 9 } + * + * @group Random Number Generation + * @throws Error if the given input is invalid. + */ + diceExpanded(dice: string): DiceReturnInterface; + diceExpanded(options: Partial): DiceReturnInterface; + diceExpanded(dice: number[]): DiceReturnInterface; + diceExpanded(n: number, d: number, plus: number): DiceReturnInterface; + /** + * Given a string dice representation, roll it + * @example + * + * rng.dice('1d6'); + * rng.dice('3d6+10'); + * rng.dice('1d20-1'); + * rng.dice('d10'); + * + * @group Random Number Generation + * @param dice e.g. 1d6+5 + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice(dice: string): number; + /** + * Given an object representation of a dice, roll it + * @example + * + * rng.dice({ d: 6 }); + * rng.dice({ n: 3, d: 6, plus: 10 }); + * rng.dice({ n: 1, d: 20, plus: -1 }); + * rng.dice({ n: 1, d: 10 }); + * + * @group Random Number Generation + * @param options {n, d, plus} format of dice roll + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice({ n, d, plus }: Partial): number; + /** + * Roll "n" x "d" sided dice and add "plus" + * @group Random Number Generation + * @param options [n, d, plus] format of dice roll + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice([n, d, plus]: number[]): number; + /** + * Roll "n" x "d" sided dice and add "plus" + * @group Random Number Generation + * @param n The number of dice to roll + * @param d The number of faces on the dice + * @param plus The number to add at the end + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice(n: number, d?: number, plus?: number): number; + /** + * Parses a string representation of a dice and gives the + * object representation {n, d, plus} + * @group Utilities + * @param string String dice representation, e.g. '1d6' + * @returns The dice representation object + * @throws Error if the given input is invalid. + */ + parseDiceString(string: string): DiceInterface; + /** + * Gives the minimum result of a call to dice with these arguments + * @group Result Prediction + * @see {@link dice} + */ + diceMin(n: string | DiceInterface | number, d?: number, plus?: number): number; + /** + * Gives the maximum result of a call to dice with these arguments + * @group Result Prediction + * @see {@link dice} + */ + diceMax(n: string | DiceInterface | number, d?: number, plus?: number): number; + /** + * Clamps a number to lower and upper bounds, inclusive + * + * @example + * rng.clamp(5, 0, 1); // 1 + * rng.clamp(-1, 0, 1); // 0 + * rng.clamp(0.5, 0, 1); // 0.5 + * @group Utilities + */ + clamp(number: number, lower: number, upper: number): number; + /** + * Gets the bin "val" sits in when between "min" and "max" is separated by "bins" number of bins + * + * This is right aligning, so .5 rounds up + * + * This is useful when wanting only a discrete number values between two endpoints + * + * @example + * + * rng.bin(1.3, 11, 0, 10); // 1 + * rng.bin(4.9, 11, 0, 10); // 5 + * rng.bin(9.9, 11, 0, 10); // 10 + * rng.bin(0.45, 11, 0, 10); // 0 + * rng.bin(0.50, 11, 0, 10); // 1 + * + * @group Utilities + * @param val The value to bin + * @param bins The number of bins + * @param min Minimum value + * @param max Maximum value + * @return The corresponding bin (left aligned) + */ + bin(val: number, bins: number, min: number, max: number): number; + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + normal(options?: { + mean?: number; + stddev?: number; + max?: number; + min?: number; + skew?: number; + }, depth?: number): number; + /** + * Generates a gaussian normal number, but with a special skewing procedure + * that is sometimes useful. + * + * @example + * + * rng.gaussian({ mean: 0.5, stddev: 0.5, skew: -1 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Distributions + * @param [options] + * @param [options.mean] + * @param [options.stddev] Must be > 0 + * @param [options.skew] + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + gaussian(options?: { + mean?: number; + stddev?: number; + skew?: number; + }): number; + /** + * Generates a Gaussian normal value via Box–Muller transform + * + * There are two ways of calling, either with an object with mean and stddev + * as keys, or just with two params, the mean and stddev + * + * Support: [0, 1] + * + * @example + * + * rng.boxMuller({ mean: 0.5, stddev: 1 }); + * rng.boxMuller(0.5, 1); + * + * @group Distributions + * @see [Box–Muller transform - Wikipedia](https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform) + * @param [options] + * @param [options.mean] - The mean of the underlying normal distribution. + * @param [options.stddev] - The standard deviation of the underlying normal distribution. + * @returns A value from the Log-Normal distribution. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + boxMuller(options?: { + mean?: number; + stddev?: number; + }): number; + /** + * Generates a Gaussian normal value via Box–Muller transform + * + * There are two ways of calling, either with an object with mean and stddev + * as keys, or just with two params, the mean and stddev + * + * Support: [0, 1] + * + * @example + * + * rng.boxMuller({ mean: 0.5, stddev: 1 }); + * rng.boxMuller(0.5, 1); + * + * @group Distributions + * @see [Box–Muller transform - Wikipedia](https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform) + * @param [mean] - The mean of the underlying normal distribution. + * @param [stddev] - The standard deviation of the underlying normal distribution. + * @returns A value from the Log-Normal distribution. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + boxMuller(mean?: number, stddev?: number): number; + /** + * A serialized, storable version of this RNG that can be + * unserialized with unserialize + * + * @group Serialization + */ + serialize(): any; +} +/** + * A version of RngInterface that includes lots of extra distributions. + * + * Depending on your integration, you may not need this. + * + */ +export interface RngDistributionsInterface { + /** + * Sum of n uniformly distributed values + * + * Support: [0, n] + * + * @example + * + * rng.irwinHall({ n: 6 }); + * rng.irwinHall(6); + * + * @see [Irwin-Hall Distribution - Wikipedia](https://en.wikipedia.org/wiki/Irwin%E2%80%93Hall_distribution) + * @group Distributions + * @param [options] + * @param [options.n]- Number of values to sum + * @returns The sum + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + irwinHall(options?: { + n?: number; + }): number; + /** + * Sum of n uniformly distributed values + * + * Support: [0, n] + * + * @example + * + * rng.irwinHall({ n: 6 }); + * rng.irwinHall(6); + * + * @see [Irwin-Hall Distribution - Wikipedia](https://en.wikipedia.org/wiki/Irwin%E2%80%93Hall_distribution) + * @group Distributions + * @param n - Number of values to sum + * @returns The sum + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + irwinHall(n?: number): number; + /** + * Mean of n uniformly distributed values + * + * Support: [0, 1] + * + * @example + * + * rng.bates({ n: 6 }); + * rng.bates(6); + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param [options] + * @param [options.n] - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bates(options?: { + n?: number; + }): number; + /** + * Mean of n uniformly distributed values + * + * Support: [0, 1] + * + * @example + * + * rng.bates({ n: 6 }); + * rng.bates(6); + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param n - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bates(n?: number): number; + /** + * A version of the bates distribution that returns gaussian normally distributed results, + * with n acting as a shape parameter. + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param n - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + batesgaussian(n: number): number; + /** + * A version of the bates distribution that returns gaussian normally distributed results, + * with n acting as a shape parameter. + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param [options] + * @param [options.n] - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + batesgaussian(options: { + n?: number; + }): number; + /** + * Probability that random number is less than p, returns 1 or 0. + * + * Support: {0, 1} + * + * @example + * + * rng.bernoulli({ p: 0.5 }); + * rng.bernoulli(0.5); + * + * @see [Bernoulli distribution - Wikipedia](https://en.wikipedia.org/wiki/Bernoulli_distribution) + * @group Distributions + * @param [options] + * @param [options.p] The probability of success, from [0 to 1], default 0.5 + * @returns 1 or 0, depending on if random number was less than p + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bernoulli(options?: { + p?: number; + }): number; + /** + * Probability that random number is less than p, returns 1 or 0. + * + * Support: {0, 1} + * + * @example + * + * rng.bernoulli({ p: 0.5 }); + * rng.bernoulli(0.5); + * + * @see [Bernoulli distribution - Wikipedia](https://en.wikipedia.org/wiki/Bernoulli_distribution) + * @group Distributions + * @param [p = 0.5] The probability of success, from [0 to 1], default 0.5 + * @returns 1 or 0, depending on if random number was less than p + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bernoulli(p?: number): number; + /** + * This function uses the inverse transform sampling method to generate + * an exponentially distributed random variable. + * + * Support: [0, ∞) + * + * @example + * + * rng.exponential({ rate: 1 }); + * rng.exponential(1); + * + * @param [options = {}] + * @param [options.rate = 1] The rate, must be > 0, default 1 + * @see [Exponential distribution - Wikipedia](https://en.wikipedia.org/wiki/Exponential_distribution) + * @group Distributions + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + exponential(options?: { + rate?: number; + }): number; + /** + * This function uses the inverse transform sampling method to generate + * an exponentially distributed random variable. + * + * Support: [0, ∞) + * + * @example + * + * rng.exponential({ rate: 1 }); + * rng.exponential(1); + * + * @param [rate = 1] The rate, must be > 0, default 1 + * @see [Exponential distribution - Wikipedia](https://en.wikipedia.org/wiki/Exponential_distribution) + * @group Distributions + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + exponential(rate?: number): number; + /** + * Generates a value from the Generalized Pareto distribution. + * + * Support: [scale, ∞) + * + * @example + * + * rng.pareto({ shape: 0.5, scale: 1, location: 0 }); + * rng.pareto({ location: 0 }); + * rng.pareto({ scale: 1 }); + * rng.pareto({ shape: 0.5 }); + * + * @see [Pareto distribution - Wikipedia](https://en.wikipedia.org/wiki/Pareto_distribution) + * @group Distributions + * @param [options] + * @param [options.shape=0.5] - The shape parameter, must be >= 0, default 0.5. + * @param [options.scale=1] - The scale parameter, must be positive ( > 0), default 1. + * @param [options.location=0] - The location parameter, default 0. + * @returns A value from the Generalized Pareto distribution, [scale, ∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + pareto(options?: { + shape?: number; + scale?: number; + location?: number; + }): number; + /** + * This function uses the fact that the Poisson distribution can be generated using a series of + * random numbers multiplied together until their product is less than e^(-lambda). The number + * of terms needed is the Poisson-distributed random variable. + * + * Support: {1, 2, 3 ...} + * + * @example + * + * rng.poisson({ lambda: 1 }); + * rng.poisson(1); + * + * @see [Poisson distribution - Wikipedia](https://en.wikipedia.org/wiki/Poisson_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.lambda = 1] Control parameter, must be positive, default 1. + * @returns Poisson distributed random number, {1, 2, 3 ...} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + poisson(options?: { + lambda?: number; + }): number; + /** + * This function uses the fact that the Poisson distribution can be generated using a series of + * random numbers multiplied together until their product is less than e^(-lambda). The number + * of terms needed is the Poisson-distributed random variable. + * + * Support: {1, 2, 3 ...} + * + * @example + * + * rng.poisson({ lambda: 1 }); + * rng.poisson(1); + * + * @see [Poisson distribution - Wikipedia](https://en.wikipedia.org/wiki/Poisson_distribution) + * @group Distributions + * @param [lambda = 1] Control parameter, must be positive, default 1. + * @returns Poisson distributed random number, {1, 2, 3 ...} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + poisson(lambda?: number): number; + /** + * This function uses combinations to calculate probabilities for the hypergeometric distribution. + * + * The hypergeometric distribution is a discrete probability distribution that describes the probability of k + * successes (random draws for which the object drawn has a specified feature) in + * n draws, without replacement, from a finite population of size N that contains exactly + * K objects with that feature + * + * Support: {max(0, n+K-N), ..., min(n, K)} + * + * @example + * + * rng.hypergeometric({ N: 50, K: 10, n: 5 }); + * rng.hypergeometric({ N: 50, K: 10, n: 5, k: 2 }); + * + * @see [Hypergeometric distribution - Wikipedia](https://en.wikipedia.org/wiki/Hypergeometric_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.N = 50] - The population size, must be positive integer. + * @param [options.K = 10] - The number of successes in the population, must be positive integer lteq N. + * @param [options.n = 5] - The number of draws, must be positive integer lteq N. + * @param [options.k] - The number of observed successes, must be positive integer lteq K and n. + * @returns The probability of exactly k successes in n draws, {max(0, n+K-N), ..., min(n, K)}. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + hypergeometric(options?: { + N?: number; + K?: number; + n?: number; + k?: number; + }): number; + /** + * Generates a value from the Rademacher distribution. + * + * Support: {-1, 1} + * + * @example + * + * rng.rademacher(); + * + * @see [Rademacher distribution](https://en.wikipedia.org/wiki/Rademacher_distribution) + * @group Distributions + * @returns either -1 or 1 with 50% probability + */ + rademacher(): -1 | 1; + /** + * Generates a value from the Binomial distribution. + * + * Probability distribution of getting number of successes of n trials of a boolean trial with probability p + * + * Support: {0, 1, 2, ..., n} + * + * @example + * + * rng.binomial({ n = 1, p = 0.5 }); + * rng.binomial({ n = 100, p = 0.1 }); + * + * @see [Binomial distribution - Wikipedia](https://en.wikipedia.org/wiki/Binomial_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.n = 1] - The number of trials, must be positive integer, default 1. + * @param [options.p = 0.6] - The probability of success, must be a number between 0 and 1 inclusive, default 0.5. + * @returns The number of successes, {0, 1, 2, ..., n}. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + binomial(options?: { + n?: number; + p?: number; + }): number; + /** + * Generates a value from the Beta-binomial distribution. + * + * Support: {0, 1, 2, ..., n} + * + * @example + * + * rng.betaBinomial({ alpha = 1, beta = 2, n = 10 }) + * rng.betaBinomial({ n = 100 }) + * + * @see [Beta-binomial distribution - Wikipedia](https://en.wikipedia.org/wiki/Beta-binomial_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.alpha = 1] - The alpha parameter of the Beta distribution, default 1, must be positive. + * @param [options.beta = 1] - The beta parameter of the Beta distribution, default 1, must be positive. + * @param [options.n = 1] - The number of trials, default 1, must be positive integer. + * @returns The number of successes in n trials, {0, 1, 2, ..., n} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + betaBinomial(options?: { + alpha?: number; + beta?: number; + n?: number; + }): number; + /** + * Generates a value from the Beta distribution. + * + * Support: (0, 1) + * + * @example + * + * rng.beta({ alpha = 0.5, beta = 0.5 }) + * rng.beta({ alpha = 1, beta = 2 }) + * rng.beta({ beta = 1 }) + * + * @see [Beta distribution - Wikipedia](https://en.wikipedia.org/wiki/Beta_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.alpha = 0.5] - The alpha parameter of the Beta distribution, must be positive, default 0.5. + * @param [options.beta = 0.5] - The beta parameter of the Beta distribution, must be positive, default 0.5. + * @returns A value from the Beta distribution, (0, 1). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + beta(options?: { + alpha?: number; + beta?: number; + }): number; + /** + * Generates a random number from the Gamma distribution. + * + * Support: (0, ∞) + * + * @example + * + * rng.gamma({ shape = 0.5, rate = 0.5 }) + * rng.gamma({ shape = 0.5, scale = 2 }) + * rng.gamma({ shape = 0.5, rate = 0.5, scale = 2 }) // Redundant as scale = 1 / rate + * rng.gamma({ shape = 0.5, rate = 2, scale = 2 }) // Error('Cannot supply scale and rate') + * + * @see [Gamma distribution - Wikipedia](https://en.wikipedia.org/wiki/Gamma_distribution) + * @group Distributions + * @see [stdlib Gamma function](https://github.com/stdlib-js/random-base-gamma/blob/main/lib/gamma.js#L39) + * @param [options = {}] + * @param [options.shape = 1] - The shape parameter, must be postive, default 1. + * @param [options.rate = 1] - The rate parameter, must be postive, default 1. + * @param [options.scale] - The scale parameter, must be postive, ( = 1/rate). + * @returns A random number from the Gamma distribution, from (0, ∞). + * @throws {Error} If both scale and rate are given and are not reciprocals of each other + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + gamma(options?: { + shape?: number; + rate?: number; + scale?: number; + }): number; + /** + * Generates a value from the Student's t-distribution. + * + * Support: (-∞, ∞) + * + * @example: + * + * rng.studentsT({ nu: 10 }) + * rng.studentsT(10) + * + * @see [Student's t-distribution - Wikipedia](https://en.wikipedia.org/wiki/Student%27s_t-distribution) + * @group Distributions + * @param [options = {}] + * @param [options.nu = 1] - The degrees of freedom, default 1, must be positive. + * @returns A value from the Student's t-distribution, (-∞, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + studentsT(options?: { + nu?: number; + }): number; + /** + * Generates a value from the Student's t-distribution. + * + * Support: (-∞, ∞) + * + * @example: + * + * rng.studentsT({ nu: 10 }) + * rng.studentsT(10) + * + * @see [Student's t-distribution - Wikipedia](https://en.wikipedia.org/wiki/Student%27s_t-distribution) + * @group Distributions + * @param [nu = 1] The degrees of freedom, must be positive, default 1 + * @returns A value from the Student's t-distribution, (-∞, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + studentsT(nu?: number): number; + /** + * Generates a value from the Wigner semicircle distribution. + * + * Support: [-R; +R] + * + * @example: + * + * rng.wignerSemicircle({ R: 1 }) + * rng.wignerSemicircle(1) + * + * @see [Wigner Semicircle Distribution - Wikipedia](https://en.wikipedia.org/wiki/Wigner_semicircle_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.R = 1] - The radius of the semicircle, must be positive, default 1. + * @returns A value from the Wigner semicircle distribution, [-R; +R]. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + wignerSemicircle(options?: { + R?: number; + }): number; + /** + * Generates a value from the Wigner semicircle distribution. + * + * Support: [-R; +R] + * + * @example: + * + * rng.wignerSemicircle({ R: 1 }) + * rng.wignerSemicircle(1) + * + * @see [Wigner Semicircle Distribution - Wikipedia](https://en.wikipedia.org/wiki/Wigner_semicircle_distribution) + * @group Distributions + * @param [R = 1] - The radius of the semicircle, must be positive, default 1. + * @returns A value from the Wigner semicircle distribution, [-R; +R]. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + wignerSemicircle(R?: number): number; + /** + * Generates a value from the Kumaraswamy distribution. + * + * Support: (0, 1) + * + * @example: + * + * rng.kumaraswamy({ alpha: 1, beta: 2 }) + * rng.kumaraswamy({ alpha: 1 }) + * rng.kumaraswamy({ beta: 2 }) + * + * @see [Kumaraswamy Distribution - Wikipedia](https://en.wikipedia.org/wiki/Kumaraswamy_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.alpha = 0.5] - The first shape parameter of the Kumaraswamy distribution, must be positive. + * @param [options.beta = 0.5] - The second shape parameter of the Kumaraswamy distribution, must be positive. + * @returns A value from the Kumaraswamy distribution, (0, 1). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + kumaraswamy(options?: { + alpha?: number; + beta?: number; + }): number; + /** + * Generates a value from the Hermite distribution. + * + * Support: {0, 1, 2, 3, ...} + * + * @example: + * + * rng.hermite({ lambda1: 1, lambda2: 2 }) + * rng.hermite({ lambda1: 1 }) + * rng.hermite({ lambda2: 2 }) + * + * @see [Hermite Distribution - Wikipedia](https://en.wikipedia.org/wiki/Hermite_distribution) + * @group Distributions + * @param [options] + * @param [options.lambda1 = 1] - The mean of the first Poisson process, must be positive. + * @param [options.lambda2 = 2] - The mean of the second Poisson process, must be positive. + * @returns A value from the Hermite distribution, {0, 1, 2, 3, ...} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + hermite(options?: { + lambda1?: number; + lambda2?: number; + }): number; + /** + * Generates a value from the Chi-squared distribution. + * + * Support: [0, ∞) + * + * @example: + * + * rng.chiSquared({ k: 2 }) + * rng.chiSquared(2) // Equivalent + * + * @see [Chi-squared Distribution - Wikipedia](https://en.wikipedia.org/wiki/Chi-squared_distribution) + * @group Distributions + * @param [options] + * @param [options.k] - The degrees of freedom, must be a postive integer - default 1. + * @returns A value from the Chi-squared distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + chiSquared(options?: { + k?: number; + }): number; + /** + * Generates a value from the Chi-squared distribution. + * + * Support: [0, ∞) + * + * @example: + * + * rng.chiSquared({ k: 2 }) + * rng.chiSquared(2) // Equivalent + * + * @see [Chi-squared Distribution - Wikipedia](https://en.wikipedia.org/wiki/Chi-squared_distribution) + * @group Distributions + * @param [k = 1] - The degrees of freedom, must be a postive integer - default 1.. + * @returns A value from the Chi-squared distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + chiSquared(k?: number): number; + /** + * Generates a value from the Rayleigh distribution + * + * Support: [0, ∞) + * + * @example: + * + * rng.rayleigh({ scale: 2 }) + * rng.rayleigh(2) // Equivalent + * + * @see [Rayleigh Distribution - Wikipedia](https://en.wikipedia.org/wiki/Rayleigh_distribution) + * @group Distributions + * @param [options] + * @param [options.scale] - The scale parameter of the Rayleigh distribution, must be > 0 - default 1. + * @returns A value from the Rayleigh distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + rayleigh(options?: { + scale?: number; + }): number; + /** + * Generates a value from the Rayleigh distribution + * + * Support: [0, ∞) + * + * @example: + * + * rng.rayleigh({ scale: 2 }) + * rng.rayleigh(2) // Equivalent + * + * @see [Rayleigh Distribution - Wikipedia](https://en.wikipedia.org/wiki/Rayleigh_distribution) + * @group Distributions + * @param [scale = 1] - The scale parameter of the Rayleigh distribution, must be > 0 - default 1. + * @returns A value from the Rayleigh distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + rayleigh(scale?: number): number; + /** + * Generates a value from the Log-Normal distribution. + * + * Support: (0, ∞) + * + * @example: + * + * rng.logNormal({ mean: 2, stddev: 1 }) + * rng.logNormal({ mean: 2 }) + * rng.logNormal({ stddev: 1 }) + * + * @see [Log Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Log-normal_distribution) + * @group Distributions + * @param [options] + * @param [options.mean] - The mean of the underlying normal distribution - default 0. + * @param [options.stddev] - The standard deviation of the underlying normal distribution, must be positive - default 1. + * @returns A value from the Log-Normal distribution (0, ∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + logNormal(options?: { + mean?: number; + stddev?: number; + }): number; + /** + * Generates a value from the Cauchy distribution. + * + * Support: (-∞, +∞) + * + * @example: + * + * rng.cauchy({ median: 2, scale: 1 }) + * rng.cauchy({ median: 2 }) + * rng.cauchy({ scale: 1 }) + * + * @see [Cauchy Distribution - Wikipedia](https://en.wikipedia.org/wiki/Cauchy_distribution) + * @group Distributions + * @param [options] + * @param [options.median]- The location parameter (median, sometimes called x0) - default 0. + * @param [options.scale]- The scale parameter, must be positive - default 1. + * @returns A value from the Cauchy distribution (-∞, +∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + cauchy(options?: { + median?: number; + scale?: number; + }): number; + /** + * Generates a value from the Laplace distribution. + * + * Support: (-∞, +∞) + * + * @example: + * + * rng.laplace({ mean: 2, scale: 1 }) + * rng.laplace({ mean: 2 }) + * rng.laplace({ scale: 1 }) + * + * @see [Laplace Distribution - Wikipedia](https://en.wikipedia.org/wiki/Laplace_distribution) + * @group Distributions + * @param [options] + * @param [options.mean]- The location parameter (mean) - default 0. + * @param [options.scale]- The scale parameter, must be positive - default 1. + * @returns A value from the Laplace distribution (-∞, +∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + laplace(options?: { + mean?: number; + scale?: number; + }): number; + /** + * Generates a value from the Logistic distribution. + * + * Support: (-∞, +∞) + * + * @example: + * + * rng.logistic({ mean: 2, scale: 1 }) + * rng.logistic({ mean: 2 }) + * rng.logistic({ scale: 1 }) + * + * @see [Laplace Distribution - Wikipedia](https://en.wikipedia.org/wiki/Logistic_distribution) + * @group Distributions + * @param [options] + * @param [options.mean]- The location parameter (mean) - default 0. + * @param [options.scale]- The scale parameter, must be positive - default 1. + * @returns A value from the Logistic distribution (-∞, +∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + logistic(options?: { + mean?: number; + scale?: number; + }): number; +} +export interface RngConstructor { + new (seed?: Seed): RngInterface; + predictable(this: new (seed: Seed) => RngConstructor, seed: Seed): RngConstructor; + /** + * @group Serialization + */ + unserialize(rng: any): any; + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + chancyMin(input: Chancy): number; + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + chancyMax(input: Chancy): number; + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ + parseDiceString(string: string): DiceInterface; + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + diceMin(n: string | DiceInterface | number, d?: number, plus?: number): number; + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + diceMax(n: string | DiceInterface | number, d?: number, plus?: number): number; +} diff --git a/dist/types/rng/pool.d.ts b/dist/types/rng/pool.d.ts new file mode 100644 index 0000000..e6f0bc2 --- /dev/null +++ b/dist/types/rng/pool.d.ts @@ -0,0 +1,44 @@ +import { RngInterface } from './interface'; +/** + * @category Pool + */ +export declare class PoolEmptyError extends Error { +} +/** + * @category Pool + */ +export declare class PoolNotEnoughElementsError extends Error { +} +/** + * Allows for randomly drawing from a pool of entries without replacement + * @category Pool + */ +export default class Pool { + #private; + rng: RngInterface; + constructor(entries?: EntryType[], rng?: RngInterface); + private copyArray; + setEntries(entries: EntryType[]): this; + getEntries(): EntryType[]; + set entries(entries: EntryType[]); + get entries(): EntryType[]; + get length(): number; + setRng(rng: RngInterface): this; + getRng(): RngInterface; + add(entry: EntryType): void; + empty(): this; + isEmpty(): boolean; + /** + * Draw an element from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + */ + draw(): EntryType; + /** + * Draw n elements from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + * @throws {@link PoolNotEnoughElementsError} if the pool does not have enough elements to draw n values + */ + drawMany(n: number): EntryType[]; +} diff --git a/dist/types/rng/predictable.d.ts b/dist/types/rng/predictable.d.ts index 980ee59..3ebb6f5 100644 --- a/dist/types/rng/predictable.d.ts +++ b/dist/types/rng/predictable.d.ts @@ -1,9 +1,65 @@ -import { RngAbstract, RngInterface, Seed } from './../rng'; +import { RngAbstract } from './../rng'; +import { RngDistributionsInterface, RngInterface, Seed } from './interface'; /** + * * An Rng type that can be used to give predictable results * for testing purposes, and giving known results. + * + * You can set an array of results that will be returned from called to _next() + * + * Note: To avoid unexpected results when using this in place of regular Rng, it is + * only allowed to make the results spread from [0, 1) + * + * The numbers are returned and cycled, so once you reach the end of the list, it will + * just keep on going. + * + * @category Other Rngs + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0]; + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0, 0.5]; + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.0 + * + * @example + * // The setEvenSpread and evenSpread methods can be used to generate + * // n numbers between [0, 1) with even gaps between + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.setEvenSpread(11); + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.5 + * prng.random(); // 0.6 + * prng.random(); // 0.7 + * prng.random(); // 0.8 + * prng.random(); // 0.9 + * prng.random(); // 0.9999999... + * prng.random(); // 0.0 */ -export default class Rng extends RngAbstract implements RngInterface { +export default class PredictableRng extends RngAbstract implements RngInterface, RngDistributionsInterface { counter: number; protected _results: number[]; constructor(seed?: Seed, results?: number[]); @@ -11,7 +67,7 @@ export default class Rng extends RngAbstract implements RngInterface { set results(results: number[]); evenSpread(n: number): number[]; setEvenSpread(n: number): this; - sameAs(other: Rng): boolean; + sameAs(other: any): boolean; reset(): this; - protected _random(): number; + protected _next(): number; } diff --git a/dist/types/rng/queue.d.ts b/dist/types/rng/queue.d.ts new file mode 100644 index 0000000..017b162 --- /dev/null +++ b/dist/types/rng/queue.d.ts @@ -0,0 +1,32 @@ +export default class Dequeue { + size: number; + elements: T[]; + constructor(length?: T[] | number); + get length(): number; + push(el: T): T | undefined; + pop(): T | undefined; + full(): boolean; + empty(): void; + get(i: number): T; + allSame(): boolean; +} +export declare class NumberQueue extends Dequeue { + sum(): number; + avg(): number; +} +export declare class NonRandomDetector extends Dequeue { + minsequencelength: number; + errormessage: string; + constructor(length?: T[] | number, minsequencelength?: number); + push(el: T): T | undefined; + detectLoop(msg?: string): void; + protected loopDetected(msg?: string): void; + /** + * Checks if there is a repeating sequence longer than a specified length in an array of numbers. + * + * @param {number[]} arr - The array of numbers. + * @param {number} n - The minimum length of the repeating sequence. + * @returns {boolean} True if a repeating sequence longer than length n is found, otherwise false. + */ + hasRepeatingSequence(arr: T[], n: number): boolean; +} diff --git a/dist/types/table.d.ts b/dist/types/table.d.ts index 21b183f..6e32f0e 100644 --- a/dist/types/table.d.ts +++ b/dist/types/table.d.ts @@ -3,7 +3,7 @@ import { default as LootTablePool, LootTablePoolDefinition } from './table/pool' import { FunctionDefinition, ConditionDefinition } from './table/pool/entry'; import LootTableEntryResult from './table/pool/entry/result'; import LootTableEntryResults from './table/pool/entry/results'; -import { RngInterface, Chancy } from './rng'; +import { RngInterface, ChancyNumeric } from './rng/interface'; /** * Object used when creating a loot table. */ @@ -36,7 +36,7 @@ export interface TableRollInterface { context?: any; result?: LootTableEntryResults; rng?: RngInterface; - n?: Chancy; + n?: ChancyNumeric; } export interface TablePoolRollInterface { pool: LootTablePool; @@ -44,7 +44,7 @@ export interface TablePoolRollInterface { context?: any; result?: LootTableEntryResults; rng?: RngInterface; - n?: Chancy; + n?: ChancyNumeric; } export default class LootTable { name?: string; diff --git a/dist/types/table/pool.d.ts b/dist/types/table/pool.d.ts index f617805..4e69011 100644 --- a/dist/types/table/pool.d.ts +++ b/dist/types/table/pool.d.ts @@ -2,14 +2,14 @@ import { default as LootTableEntry, LootTableEntryDefinition, ConditionDefinitio import LootTableEntryResult from './pool/entry/result'; import LootTableEntryResults from './pool/entry/results'; import { default as LootTable } from './../table'; -import { RngInterface, Chancy } from './../rng'; +import { RngInterface, ChancyNumeric } from './../rng/interface'; export interface LootTablePoolDefinition { name?: string; id?: string; conditions?: Array; functions?: Array; - rolls?: Chancy; - nulls?: Chancy; + rolls?: ChancyNumeric; + nulls?: ChancyNumeric; entries?: Array; template?: Partial; } @@ -18,8 +18,8 @@ export default class LootPool { id?: string; conditions: Array; functions: Array; - rolls: Chancy; - nulls: Chancy; + rolls: ChancyNumeric; + nulls: ChancyNumeric; entries: Array; template: Partial; static NULLKEY: string; diff --git a/dist/types/table/pool/entry.d.ts b/dist/types/table/pool/entry.d.ts index c83fe07..13fc580 100644 --- a/dist/types/table/pool/entry.d.ts +++ b/dist/types/table/pool/entry.d.ts @@ -1,5 +1,6 @@ import LootTable from './../../table'; -import { default as RNG, RngInterface, Chancy } from './../../rng'; +import RNG from './../../rng'; +import { RngInterface, ChancyNumeric } from './../../rng/interface'; import LootTableEntryResult from './entry/result'; import LootTableEntryResults from './entry/results'; export type LootTableEntryDefinition = { @@ -9,7 +10,7 @@ export type LootTableEntryDefinition = { unique?: boolean; weight?: number; item?: any; - qty?: Chancy; + qty?: ChancyNumeric; functions?: Array; conditions?: Array; }; @@ -30,7 +31,7 @@ export default class LootTableEntry { name?: string; weight: number; item?: any; - qty: Chancy; + qty: ChancyNumeric; functions: Array; conditions: Array; rng?: RngInterface; diff --git a/dist/types/ultraloot.d.ts b/dist/types/ultraloot.d.ts index a2eef44..b1c4aa6 100644 --- a/dist/types/ultraloot.d.ts +++ b/dist/types/ultraloot.d.ts @@ -3,7 +3,7 @@ import { default as LootTablePool, LootTablePoolDefinition } from './table/pool' import { default as LootTableEntry, LootTableEntryDefinition, FunctionDefinition, ConditionDefinition } from './table/pool/entry'; import LootTableEntryResult from './table/pool/entry/result'; import LootTableEntryResults from './table/pool/entry/results'; -import { Seed, RngInterface, RngConstructor, Chancy } from './rng'; +import { Seed, RngInterface, RngConstructor, ChancyNumeric } from './rng/interface'; declare const VERSION_KEY = "__version__"; /** * This is for easily creating loot tables using a json like @@ -25,8 +25,8 @@ export type LootTablePoolEasyDefinition = { conditions?: Array; functions?: Array; template?: LootTableEntryDefinition; - rolls?: Chancy; - nulls?: Chancy; + rolls?: ChancyNumeric; + nulls?: ChancyNumeric; entries?: Array; }; /** @@ -47,8 +47,8 @@ export type LootTablePoolJsonDefinition = { id?: string; conditions?: Array; functions?: Array; - rolls?: Chancy; - nulls?: Chancy; + rolls?: ChancyNumeric; + nulls?: ChancyNumeric; entries: Array; }; /** @@ -61,7 +61,7 @@ export type LootTableEntryJsonDefinition = { stackable?: boolean; weight?: number; item?: any; - qty?: Chancy; + qty?: ChancyNumeric; functions?: Array; conditions?: Array; }; diff --git a/dist/ultraloot.cjs b/dist/ultraloot.cjs index 7d1eda5..738597e 100644 --- a/dist/ultraloot.cjs +++ b/dist/ultraloot.cjs @@ -271,21 +271,682 @@ if (debug) { /***/ }), -/***/ 629: +/***/ 623: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ A: () => (/* binding */ Rng), -/* harmony export */ U: () => (/* binding */ RngAbstract) +/* harmony export */ Ay: () => (__WEBPACK_DEFAULT_EXPORT__), +/* harmony export */ Bh: () => (/* binding */ ArrayNumberValidator), +/* harmony export */ Ol: () => (/* binding */ NumberValidator), +/* harmony export */ X: () => (/* binding */ NumberValidationError) /* harmony export */ }); -const MAX_RECURSIONS = 100; +/** + * @category Number Validator + */ +const assert = (truthy, msg = 'Assertion failed') => { + if (!truthy) { + throw new NumberValidationError(msg); + } +}; +/** + * @category Number Validator + */ +class NumberValidationError extends Error { +} +/** + * @category Number Validator + */ +class ArrayNumberValidator { + /** + * The numbers to be validated + */ + #numbers = []; + /** + * Descriptive name for this validation + */ + name = 'numbers'; + constructor(numbers, name = 'numbers') { + this.numbers = numbers; + this.name = name; + } + get numbers() { + return this.#numbers; + } + set numbers(numbers) { + for (const number of numbers) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + } + this.#numbers = numbers; + } + /** + * Specify the numbers to validate + */ + all(numbers) { + this.numbers = numbers; + return this; + } + /** + * Specify the numbers to validate + */ + validate(numbers) { + if (!Array.isArray(numbers)) { + return new NumberValidator(numbers); + } + return this.all(numbers); + } + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potatoes = [0, 1]; + * validate(potatoes).varname('potatoes').gt(2); // "Expected every component of potatoes to be > 2, got 0" + */ + varname(name) { + this.name = name; + return this; + } + /** + * Get the sum of our numbers + */ + sum() { + return this.numbers.reduce((a, b) => a + b, 0); + } + /** + * Validates whether the total of all of our numbers is close to sum, with a maximum difference of diff + * @param sum The sum + * @param diff The maximum difference + * @param msg Error message + * @throws {@link NumberValidationError} If they do not sum close to the correct amount + */ + sumcloseto(sum, diff = 0.0001, msg) { + assert(Math.abs(this.sum() - sum) < diff, msg ?? `Expected sum of ${this.name} to be within ${diff} of ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is equal (===) to sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to the correct amount + */ + sumto(sum, msg) { + assert(this.sum() === sum, msg ?? `Expected sum of ${this.name} to be equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is < sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to < sum + */ + sumtolt(sum, msg) { + assert(this.sum() < sum, msg ?? `Expected sum of ${this.name} to be less than ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is > sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to > sum + */ + sumtogt(sum, msg) { + assert(this.sum() > sum, msg ?? `Expected sum of ${this.name} to be greater than ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is <= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to <= sum + */ + sumtolteq(sum, msg) { + assert(this.sum() <= sum, msg ?? `Expected sum of ${this.name} to be less than or equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is >= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to >= sum + */ + sumtogteq(sum, msg) { + assert(this.sum() >= sum, msg ?? `Expected sum of ${this.name} to be greater than or equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all integers + */ + int(msg) { + this.numbers.forEach(a => validate(a).int(msg ?? `Expected every component of ${this.name} to be an integer, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all positive + */ + positive(msg) { + this.numbers.forEach(a => validate(a).positive(msg ?? `Expected every component of ${this.name} to be postiive, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all negative + */ + negative(msg) { + this.numbers.forEach(a => validate(a).negative(msg ?? `Expected every component of ${this.name} to be negative, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all between from and to + */ + between(from, to, msg) { + this.numbers.forEach(a => validate(a).between(from, to, msg ?? `Expected every component of ${this.name} to be between ${from} and ${to}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all between or equal to from and to + */ + betweenEq(from, to, msg) { + this.numbers.forEach(a => validate(a).betweenEq(from, to, msg ?? `Expected every component of ${this.name} to be between or equal to ${from} and ${to}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all > n + */ + gt(n, msg) { + this.numbers.forEach(a => validate(a).gt(n, msg ?? `Expected every component of ${this.name} to be > ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all >= n + */ + gteq(n, msg) { + this.numbers.forEach(a => validate(a).gteq(n, msg ?? `Expected every component of ${this.name} to be >= ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all < n + */ + lt(n, msg) { + this.numbers.forEach(a => validate(a).lt(n, msg ?? `Expected every component of ${this.name} to be < ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all <= n + */ + lteq(n, msg) { + this.numbers.forEach(a => validate(a).lteq(n, msg ?? `Expected every component of ${this.name} to be <= ${n}, got ${a}`)); + return this; + } +} +/** + * Validate numbers in a fluent fashion. + * + * Each validator method accepts a message as the last parameter + * for customising the error message. + * + * @category Number Validator + * + * @example + * const n = new NumberValidator(); + * n.validate(0).gt(1); // NumberValidationError + * + * @example + * const n = new NumberValidator(); + * const probability = -0.1; + * n.validate(probability).gteq(0, 'Probabilities should always be >= 0'); // NumberValidationError('Probabilities should always be >= 0'). + */ +class NumberValidator { + /** + * The number being tested. + */ + #number; + /** + * The name of the variable being validated - shows up in error messages. + */ + name = 'number'; + constructor(number = 0, name = 'number') { + this.number = number; + this.name = name; + } + get number() { + return this.#number; + } + set number(number) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + this.#number = number; + } + /** + * Returns an ArrayNumberValidator for all the numbers + */ + all(numbers, name) { + return new ArrayNumberValidator(numbers, name ?? this.name); + } + assertNumber(num) { + assert(typeof this.number !== 'undefined', 'No number passed to validator.'); + return true; + } + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potato = 1; + * validate(potato).varname('potato').gt(2); // "Expected potato to be greater than 2, got 1" + * @param {string} name [description] + */ + varname(name) { + this.name = name; + return this; + } + /** + * Specify the number to be validated + */ + validate(number) { + if (Array.isArray(number)) { + return this.all(number); + } + this.number = number; + return this; + } + /** + * Asserts that the number is an integer + * @throws {@link NumberValidationError} if ths number is not an integer + */ + int(msg) { + if (this.assertNumber(this.number)) + assert(Number.isInteger(this.number), msg ?? `Expected ${this.name} to be an integer, got ${this.number}`); + return this; + } + /** + * Asserts that the number is > 0 + * @throws {@link NumberValidationError} if the number is not positive + */ + positive(msg) { + return this.gt(0, msg ?? `Expected ${this.name} to be positive, got ${this.number}`); + } + /** + * Asserts that the number is < 0 + * @throws {@link NumberValidationError} if the number is not negative + */ + negative(msg) { + return this.lt(0, msg ?? `Expected ${this.name} to be negative, got ${this.number}`); + } + /** + * Asserts that the from < number < to + * @throws {@link NumberValidationError} if it is outside or equal to those bounds + */ + between(from, to, msg) { + if (this.assertNumber(this.number)) + assert(this.number > from && this.number < to, msg ?? `Expected ${this.name} to be between ${from} and ${to}, got ${this.number}`); + return this; + } + /** + * Asserts that the from <= number <= to + * @throws {@link NumberValidationError} if it is outside those bounds + */ + betweenEq(from, to, msg) { + if (this.assertNumber(this.number)) + assert(this.number >= from && this.number <= to, msg ?? `Expected ${this.name} to be between or equal to ${from} and ${to}, got ${this.number}`); + return this; + } + /** + * Asserts that number > n + * @throws {@link NumberValidationError} if it is less than or equal to n + */ + gt(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number > n, msg ?? `Expected ${this.name} to be greater than ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number >= n + * @throws {@link NumberValidationError} if it is less than n + */ + gteq(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number >= n, msg ?? `Expected ${this.name} to be greater than or equal to ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number < n + * @throws {@link NumberValidationError} if it is greater than or equal to n + */ + lt(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number < n, msg ?? `Expected ${this.name} to be less than ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number <= n + * @throws {@link NumberValidationError} if it is greater than n + */ + lteq(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number <= n, msg ?? `Expected ${this.name} to be less than or equal to ${n}, got ${this.number}`); + return this; + } +} +function validate(number) { + if (Array.isArray(number)) { + return new ArrayNumberValidator(number); + } + else if (typeof number === 'object') { + const entries = Object.entries(number); + if (entries.length === 0) { + throw new Error('Empty object provided'); + } + const [name, value] = entries[0]; + return validate(value).varname(name); + } + else { + return new NumberValidator(number); + } +} +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (validate); + + +/***/ }), + +/***/ 673: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + YG: () => (/* binding */ MaxRecursionsError), + Qs: () => (/* binding */ NonRandomRandomError), + Up: () => (/* binding */ RngAbstract), + Ay: () => (/* binding */ src_rng) +}); + +// EXTERNAL MODULE: ./src/number.ts +var src_number = __webpack_require__(623); +;// ./src/rng/pool.ts + +/** + * @category Pool + */ +class PoolEmptyError extends Error { +} +/** + * @category Pool + */ +class PoolNotEnoughElementsError extends Error { +} +/** + * Allows for randomly drawing from a pool of entries without replacement + * @category Pool + */ +class Pool { + rng; + #entries = []; + constructor(entries = [], rng) { + this.entries = entries; + if (rng) { + this.rng = rng; + } + else { + this.rng = new src_rng(); + } + } + copyArray(arr) { + return Array.from(arr); + } + setEntries(entries) { + this.entries = entries; + return this; + } + getEntries() { + return this.#entries; + } + set entries(entries) { + this.#entries = this.copyArray(entries); + } + get entries() { + return this.#entries; + } + get length() { + return this.#entries.length; + } + setRng(rng) { + this.rng = rng; + return this; + } + getRng() { + return this.rng; + } + add(entry) { + this.#entries.push(entry); + } + empty() { + this.#entries = []; + return this; + } + isEmpty() { + return this.length <= 0; + } + /** + * Draw an element from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + */ + draw() { + if (this.length === 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length === 1) { + return this.#entries.splice(0, 1)[0]; + } + const idx = this.rng.randInt(0, this.#entries.length - 1); + return this.#entries.splice(idx, 1)[0]; + } + /** + * Draw n elements from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + * @throws {@link PoolNotEnoughElementsError} if the pool does not have enough elements to draw n values + */ + drawMany(n) { + if (n < 0) { + throw new Error('Cannot draw < 0 elements from pool'); + } + if (this.length === 0 && n > 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length < n) { + throw new PoolNotEnoughElementsError(`Tried to draw ${n} elements from pool with only ${this.length} entries.`); + } + const result = []; + for (let i = 0; i < n; i++) { + const idx = this.rng.randInt(0, this.#entries.length - 1); + result.push(this.#entries.splice(idx, 1)[0]); + } + return result; + } +} + +;// ./src/rng/queue.ts +class Dequeue { + size; + elements = []; + constructor(length = 1) { + if (Array.isArray(length)) { + this.elements = length; + this.size = this.elements.length; + } + else { + this.size = length; + } + } + get length() { + return this.elements.length; + } + push(el) { + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + pop() { + return this.elements.pop(); + } + full() { + return this.length >= this.size; + } + empty() { + this.elements = []; + } + get(i) { + return this.elements[i]; + } + allSame() { + if (this.length > 0) { + return this.elements.every(a => a === this.elements[0]); + } + return true; + } +} +class NumberQueue extends (/* unused pure expression or super */ null && (Dequeue)) { + sum() { + return this.elements.reduce((a, b) => a + b, 0); + } + avg() { + return this.sum() / this.length; + } +} +class LoopDetectedError extends Error { +} +class NonRandomDetector extends Dequeue { + minsequencelength = 2; + errormessage = 'Loop detected in input data. Randomness source not random?'; + constructor(length = 1, minsequencelength = 2) { + super(length); + if (this.size > 10000) { + throw new Error('Cannot detect loops for more than 10000 elements'); + } + this.minsequencelength = minsequencelength; + } + push(el) { + this.detectLoop(); + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + detectLoop(msg) { + if (this.full()) { + if (this.allSame()) { + this.loopDetected(msg); + } + if (this.hasRepeatingSequence(this.elements, this.minsequencelength)) { + this.loopDetected(msg); + } + } + } + loopDetected(msg) { + throw new LoopDetectedError(msg ?? this.errormessage); + } + /** + * Checks if there is a repeating sequence longer than a specified length in an array of numbers. + * + * @param {number[]} arr - The array of numbers. + * @param {number} n - The minimum length of the repeating sequence. + * @returns {boolean} True if a repeating sequence longer than length n is found, otherwise false. + */ + hasRepeatingSequence(arr, n) { + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + let k = 0; + while (j + k < arr.length && arr[i + k] === arr[j + k]) { + k++; + if (k > n) { + return true; + } + } + } + } + return false; + } +} + +;// ./src/rng.ts + + + +/** + * Safeguard against huge loops. If loops unintentionally grow beyond this + * arbitrary limit, bail out.. + */ +const LOOP_MAX = 10000000; +/** + * Safeguard against too much recursion - if a function recurses more than this, + * we know we have a problem. + * + * Max recursion limit is around ~1000 anyway, so would get picked up by interpreter. + */ +const MAX_RECURSIONS = 500; const THROW_ON_MAX_RECURSIONS_REACHED = true; -const diceRe = /^ *([0-9]+) *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; -const diceReNoInit = /^ *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; +const PREDICTABLE_SEED = 5789938451; +const SAMERANDOM_MAX = 10; +const diceRe = /^ *([+-]? *[0-9_]*) *[dD] *([0-9_]+) *([+-]? *[0-9_.]*) *$/; const strToNumberCache = {}; const diceCache = {}; +class MaxRecursionsError extends Error { +} +class NonRandomRandomError extends Error { +} +function sum(numbersFirstArg, ...numbers) { + if (Array.isArray(numbersFirstArg)) { + return numbersFirstArg.reduce((a, b) => a + b, 0); + } + return numbers.reduce((a, b) => a + b, 0); +} +function isNumeric(input) { + return (typeof input === 'number') || (!isNaN(parseFloat(input)) && isFinite(input)); +} +/** + * This abstract class implements most concrete implementations of + * functions, as the only underlying changes are likely to be to the + * uniform random number generation, and how that is handled. + * + * All the typedoc documentation for this has been sharded out to RngInterface + * in a separate file. + */ class RngAbstract { #seed = 0; + #monotonic = 0; + #lastuniqid = 0; + #randFunc; + #shouldThrowOnMaxRecursionsReached; + #distributions = [ + 'normal', + 'gaussian', + 'boxMuller', + 'irwinHall', + 'bates', + 'batesgaussian', + 'bernoulli', + 'exponential', + 'pareto', + 'poisson', + 'hypergeometric', + 'rademacher', + 'binomial', + 'betaBinomial', + 'beta', + 'gamma', + 'studentsT', + 'wignerSemicircle', + 'kumaraswamy', + 'hermite', + 'chiSquared', + 'rayleigh', + 'logNormal', + 'cauchy', + 'laplace', + 'logistic', + ]; constructor(seed) { this.setSeed(seed); } @@ -293,7 +954,17 @@ class RngAbstract { return this.#seed; } sameAs(other) { - return this.#seed === other.#seed; + if (other instanceof RngAbstract) { + return this.#seed === other.#seed && this.#randFunc === other.#randFunc; + } + return false; + } + randomSource(source) { + this.#randFunc = source; + return this; + } + getRandomSource() { + return this.#randFunc; } setSeed(seed) { if (typeof seed !== 'undefined' && seed !== null) { @@ -316,6 +987,10 @@ class RngAbstract { seed: this.#seed, }; } + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized) { const { constructor } = Object.getPrototypeOf(this); const rng = new constructor(serialized.seed); @@ -324,11 +999,15 @@ class RngAbstract { } predictable(seed) { const { constructor } = Object.getPrototypeOf(this); - const newSelf = new constructor(seed); + const newSelf = new constructor(seed ?? PREDICTABLE_SEED); return newSelf; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ static predictable(seed) { - return new this(seed); + return new this(seed ?? PREDICTABLE_SEED); } hashStr(str) { let hash = 0; @@ -352,23 +1031,36 @@ class RngAbstract { return num; } _random() { - return Math.random(); + if (typeof this.#randFunc === 'function') { + return this.#randFunc(); + } + return this._next(); } percentage() { return this.randBetween(0, 100); } + probability() { + return this.randBetween(0, 1); + } random(from = 0, to = 1, skew = 0) { return this.randBetween(from, to, skew); } chance(n, chanceIn = 1) { + (0,src_number/* default */.Ay)({ chanceIn }).positive(); + (0,src_number/* default */.Ay)({ n }).positive(); const chance = n / chanceIn; return this._random() <= chance; } // 500 to 1 chance, for example chanceTo(from, to) { - return this._random() <= (from / (from + to)); + return this.chance(from, from + to); } randInt(from = 0, to = 1, skew = 0) { + (0,src_number/* default */.Ay)({ from }).int(); + (0,src_number/* default */.Ay)({ to }).int(); + if (from === to) { + return from; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -379,14 +1071,21 @@ class RngAbstract { } return Math.floor(rand * ((to + 1) - from)) + from; } - // Not deterministic - uniqid(prefix = '', random = false) { - const sec = Date.now() * 1000 + Math.random() * 1000; + uniqid(prefix = '') { + const now = Date.now() * 1000; + if (this.#lastuniqid === now) { + this.#monotonic++; + } + else { + this.#monotonic = Math.round(this._random() * 100); + } + const sec = now + this.#monotonic; const id = sec.toString(16).replace(/\./g, '').padEnd(14, '0'); - return `${prefix}${id}${random ? `.${Math.trunc(Math.random() * 100000000)}` : ''}`; + this.#lastuniqid = now; + return `${prefix}${id}`; } - // Deterministic - uniqstr(len = 6) { + randomString(len = 6) { + (0,src_number/* default */.Ay)({ len }).gt(0); const str = []; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const alen = 61; @@ -395,7 +1094,10 @@ class RngAbstract { } return str.join(''); } - randBetween(from = 0, to = 1, skew = 0) { + randBetween(from = 0, to, skew = 0) { + if (typeof to === 'undefined') { + to = from + 1; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -407,57 +1109,97 @@ class RngAbstract { return this.scaleNorm(rand, from, to); } scale(number, from, to, min = 0, max = 1) { - if (number > max) - throw new Error(`Number ${number} is greater than max of ${max}`); - if (number < min) - throw new Error(`Number ${number} is less than min of ${min}`); + (0,src_number/* default */.Ay)({ number }).lteq(max); + (0,src_number/* default */.Ay)({ number }).gteq(min); // First we scale the number in the range [0-1) number = (number - min) / (max - min); return this.scaleNorm(number, from, to); } scaleNorm(number, from, to) { - if (number > 1 || number < 0) - throw new Error(`Number must be < 1 and > 0, got ${number}`); + (0,src_number/* default */.Ay)({ number }).betweenEq(0, 1); return (number * (to - from)) + from; } - shouldThrowOnMaxRecursionsReached() { + shouldThrowOnMaxRecursionsReached(val) { + if (typeof val === 'boolean') { + this.#shouldThrowOnMaxRecursionsReached = val; + return this; + } + if (typeof this.#shouldThrowOnMaxRecursionsReached !== 'undefined') { + return this.#shouldThrowOnMaxRecursionsReached; + } return THROW_ON_MAX_RECURSIONS_REACHED; } - // Gaussian number between 0 and 1 - normal({ mean, stddev = 1, max, min, skew = 0 } = {}, depth = 0) { - if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { - throw new Error('Max recursive calls to rng normal function. This might be as a result of using predictable random numbers?'); - } - let num = this.boxMuller(); - num = num / 10.0 + 0.5; // Translate to 0 -> 1 - if (depth > MAX_RECURSIONS) { - num = Math.min(Math.max(num, 0), 1); + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + * @throws {@link MaxRecursionsError} If the function recurses too many times in trying to generate in bounds numbers + */ + normal({ mean, stddev, max, min, skew = 0 } = {}, depth = 0) { + if (typeof min === 'undefined' && typeof max === 'undefined') { + return this.gaussian({ mean, stddev, skew }); } - else { - if (num > 1 || num < 0) { - return this.normal({ mean, stddev, max, min, skew }, depth + 1); // resample between 0 and 1 - } + if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError(`Max recursive calls to rng normal function. This might be as a result of using predictable random numbers, or inappropriate arguments? Args: ${JSON.stringify({ mean, stddev, max, min, skew })}`); } + let num = this.bates(7); if (skew < 0) { num = 1 - (Math.pow(num, Math.pow(2, skew))); } else { num = Math.pow(num, Math.pow(2, -skew)); } + if (typeof mean === 'undefined' && + typeof stddev === 'undefined' && + typeof max !== 'undefined' && + typeof min !== 'undefined') { + // This is a simple scaling of the bates distribution. + return this.scaleNorm(num, min, max); + } + num = (num * 10) - 5; if (typeof mean === 'undefined') { mean = 0; if (typeof max !== 'undefined' && typeof min !== 'undefined') { - num *= max - min; - num += min; + mean = (max + min) / 2; + if (typeof stddev === 'undefined') { + stddev = Math.abs(max - min) / 10; + } } - else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + stddev = 0.1; } + num = num * stddev + mean; } else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + if (typeof max !== 'undefined' && typeof min !== 'undefined') { + stddev = Math.abs(max - min) / 10; + } + else { + stddev = 0.1; + } + } num = num * stddev + mean; } if (depth <= MAX_RECURSIONS && ((typeof max !== 'undefined' && num > max) || (typeof min !== 'undefined' && num < min))) { @@ -476,48 +1218,489 @@ class RngAbstract { } return num; } - // Standard Normal variate using Box-Muller transform. + gaussian({ mean = 0, stddev = 1, skew = 0 } = {}) { + (0,src_number/* default */.Ay)({ stddev }).positive(); + if (skew === 0) { + return this.boxMuller({ mean, stddev }); + } + let num = this.boxMuller({ mean: 0, stddev: 1 }); + num = num / 10.0 + 0.5; // Translate to 0 -> 1 + if (skew < 0) { + num = 1 - (Math.pow(num, Math.pow(2, skew))); + } + else { + num = Math.pow(num, Math.pow(2, -skew)); + } + num = num * 10; + num = num - 5; + num = num * stddev + mean; + return num; + } boxMuller(mean = 0, stddev = 1) { + if (typeof mean === 'object') { + ({ mean = 0, stddev = 1 } = mean); + } + (0,src_number/* default */.Ay)({ stddev }).gteq(0); const u = 1 - this._random(); // Converting [0,1) to (0,1] const v = this._random(); const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); // Transform to the desired mean and standard deviation: return z * stddev + mean; } + irwinHall(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().positive(); + let sum = 0; + for (let i = 0; i < n; i++) { + sum += this._random(); + } + return sum; + } + bates(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().positive(); + return this.irwinHall({ n }) / n; + } + batesgaussian(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().gt(1); + return (this.irwinHall({ n }) / Math.sqrt(n)) - ((1 / Math.sqrt(1 / n)) / 2); + } + bernoulli(p = 0.5) { + if (typeof p === 'object') { + ({ p = 0.5 } = p); + } + (0,src_number/* default */.Ay)({ p }).lteq(1).gteq(0); + return this._random() < p ? 1 : 0; + } + exponential(rate = 1) { + if (typeof rate === 'object') { + ({ rate = 1 } = rate); + } + (0,src_number/* default */.Ay)({ rate }).gt(0); + return -Math.log(1 - this._random()) / rate; + } + pareto({ shape = 0.5, scale = 1, location = 0 } = {}) { + (0,src_number/* default */.Ay)({ shape }).gteq(0); + (0,src_number/* default */.Ay)({ scale }).positive(); + const u = this._random(); + if (shape !== 0) { + return location + (scale / shape) * (Math.pow(u, -shape) - 1); + } + else { + return location - scale * Math.log(u); + } + } + poisson(lambda = 1) { + if (typeof lambda === 'object') { + ({ lambda = 1 } = lambda); + } + (0,src_number/* default */.Ay)({ lambda }).positive(); + const L = Math.exp(-lambda); + let k = 0; + let p = 1; + let i = 0; + const nq = new NonRandomDetector(SAMERANDOM_MAX, 2); + do { + k++; + const r = this._random(); + nq.push(r); + p *= r; + nq.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the poisson distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${r}`); + } while (p > L && i++ < LOOP_MAX); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in poisson - bailing out - possible parameter error, or using non-random source?'); + } + return k - 1; + } + hypergeometric({ N = 50, K = 10, n = 5, k } = {}) { + (0,src_number/* default */.Ay)({ N }).int().positive(); + (0,src_number/* default */.Ay)({ K }).int().positive().lteq(N); + (0,src_number/* default */.Ay)({ n }).int().positive().lteq(N); + if (typeof k === 'undefined') { + k = this.randInt(0, Math.min(K, n)); + } + (0,src_number/* default */.Ay)({ k }).int().betweenEq(0, Math.min(K, n)); + function logFactorial(x) { + let res = 0; + for (let i = 2; i <= x; i++) { + res += Math.log(i); + } + return res; + } + function logCombination(a, b) { + return logFactorial(a) - logFactorial(b) - logFactorial(a - b); + } + const logProb = logCombination(K, k) + logCombination(N - K, n - k) - logCombination(N, n); + return Math.exp(logProb); + } + rademacher() { + return this._random() < 0.5 ? -1 : 1; + } + binomial({ n = 1, p = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ n }).int().positive(); + (0,src_number/* default */.Ay)({ p }).betweenEq(0, 1); + let successes = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + successes++; + } + } + return successes; + } + betaBinomial({ alpha = 1, beta = 1, n = 1 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).positive(); + (0,src_number/* default */.Ay)({ beta }).positive(); + (0,src_number/* default */.Ay)({ n }).int().positive(); + const bd = (alpha, beta) => { + let x = this._random(); + let y = this._random(); + x = Math.pow(x, 1 / alpha); + y = Math.pow(y, 1 / beta); + return x / (x + y); + }; + const p = bd(alpha, beta); + let k = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + k++; + } + } + return k; + } + beta({ alpha = 0.5, beta = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).positive(); + (0,src_number/* default */.Ay)({ beta }).positive(); + const gamma = (alpha) => { + let x = 0; + for (let i = 0; i < alpha; i++) { + const r = this._random(); + x += -Math.log(r); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in beta - bailing out - possible parameter error, or using non-random source?'); + } + } + return x; + }; + const x = gamma(alpha); + const y = gamma(beta); + return x / (x + y); + } + gamma({ shape = 1, rate, scale } = {}) { + (0,src_number/* default */.Ay)({ shape }).positive(); + if (typeof scale !== 'undefined' && typeof rate !== 'undefined' && rate !== 1 / scale) { + throw new Error('Cannot supply rate and scale'); + } + if (typeof scale !== 'undefined') { + (0,src_number/* default */.Ay)({ scale }).positive(); + rate = 1 / scale; + } + if (typeof rate === 'undefined') { + rate = 1; + } + if (rate) { + (0,src_number/* default */.Ay)({ rate }).positive(); + } + let flg; + let x2; + let v0; + let v1; + let x; + let u; + let v = 1; + const d = shape - 1 / 3; + const c = 1.0 / Math.sqrt(9.0 * d); + let i = 0; + flg = true; + const nq1 = new NonRandomDetector(SAMERANDOM_MAX); + while (flg && i++ < LOOP_MAX) { + let j = 0; + const nq2 = new NonRandomDetector(SAMERANDOM_MAX); + do { + x = this.normal(); + nq2.push(x); + nq2.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul ofthe looped way of generating.`); + v = 1.0 + (c * x); + } while (v <= 0.0 && j++ < LOOP_MAX); + if ((j + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma inner loop - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + v *= Math.pow(v, 2); + x2 = Math.pow(x, 2); + v0 = 1.0 - (0.331 * x2 * x2); + v1 = (0.5 * x2) + (d * (1.0 - v + Math.log(v))); + u = this._random(); + nq1.push(u); + nq1.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${u}`); + if (u < v0 || Math.log(u) < v1) { + flg = false; + } + } + if ((i + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + return rate * d * v; + } + studentsT(nu = 1) { + if (typeof nu === 'object') { + ({ nu = 1 } = nu); + } + (0,src_number/* default */.Ay)({ nu }).positive(); + const normal = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + const chiSquared = this.gamma({ shape: nu / 2, rate: 2 }); + return normal / Math.sqrt(chiSquared / nu); + } + wignerSemicircle(R = 1) { + if (typeof R === 'object') { + ({ R = 1 } = R); + } + (0,src_number/* default */.Ay)({ R }).gt(0); + const theta = this._random() * 2 * Math.PI; + return R * Math.cos(theta); + } + kumaraswamy({ alpha = 0.5, beta = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).gt(0); + (0,src_number/* default */.Ay)({ beta }).gt(0); + const u = this._random(); + return Math.pow(1 - Math.pow(1 - u, 1 / beta), 1 / alpha); + } + hermite({ lambda1 = 1, lambda2 = 2 } = {}) { + (0,src_number/* default */.Ay)({ lambda1 }).gt(0); + (0,src_number/* default */.Ay)({ lambda2 }).gt(0); + const x1 = this.poisson({ lambda: lambda1 }); + const x2 = this.poisson({ lambda: lambda2 }); + return x1 + x2; + } + chiSquared(k = 1) { + if (typeof k === 'object') { + ({ k = 1 } = k); + } + (0,src_number/* default */.Ay)({ k }).positive().int(); + let sum = 0; + for (let i = 0; i < k; i++) { + const z = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + sum += z * z; + } + return sum; + } + rayleigh(scale = 1) { + if (typeof scale === 'object') { + ({ scale = 1 } = scale); + } + (0,src_number/* default */.Ay)({ scale }).gt(0); + return scale * Math.sqrt(-2 * Math.log(this._random())); + } + logNormal({ mean = 0, stddev = 1 } = {}) { + (0,src_number/* default */.Ay)({ stddev }).gt(0); + const normal = mean + stddev * Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + return Math.exp(normal); + } + cauchy({ median = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random(); + return median + scale * Math.tan(Math.PI * (u - 0.5)); + } + laplace({ mean = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random() - 0.5; + return mean - scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)); + } + logistic({ mean = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random(); + return mean + scale * Math.log(u / (1 - u)); + } + /** + * Returns the support of the given distribution. + * + * @see [Wikipedia - Support (mathematics)](https://en.wikipedia.org/wiki/Support_(mathematics)#In_probability_and_measure_theory) + */ + support(distribution) { + const map = { + random: '[min, max)', + integer: '[min, max]', + normal: '(-INF, INF)', + boxMuller: '(-INF, INF)', + gaussian: '(-INF, INF)', + irwinHall: '[0, n]', + bates: '[0, 1]', + batesgaussian: '(-INF, INF)', + bernoulli: '{0, 1}', + exponential: '[0, INF)', + pareto: '[scale, INF)', + poisson: '{1, 2, 3 ...}', + hypergeometric: '{max(0, n+K-N), ..., min(n, K)}', + rademacher: '{-1, 1}', + binomial: '{0, 1, 2, ..., n}', + betaBinomial: '{0, 1, 2, ..., n}', + beta: '(0, 1)', + gamma: '(0, INF)', + studentsT: '(-INF, INF)', + wignerSemicircle: '[-R; +R]', + kumaraswamy: '(0, 1)', + hermite: '{0, 1, 2, 3, ...}', + chiSquared: '[0, INF)', + rayleigh: '[0, INF)', + logNormal: '(0, INF)', + cauchy: '(-INF, +INF)', + laplace: '(-INF, +INF)', + logistic: '(-INF, +INF)', + }; + return map[distribution]; + } chancyInt(input) { if (typeof input === 'number') { return Math.round(input); } + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyInt'); + } + } + let choice = this.choice(input); + if (typeof choice !== 'number') { + choice = parseFloat(choice); + } + return Math.round(choice); + } if (typeof input === 'object') { - input.type = 'integer'; + const type = input.type ?? 'random'; + if (type === 'random') { + input.type = 'integer'; + } + else if (type === 'normal') { + input.type = 'normal_integer'; + } } - return this.chancy(input); + return Math.round(this.chancy(input)); } - chancy(input) { + chancy(input, depth = 0) { + if (depth >= MAX_RECURSIONS) { + if (this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError('Max recursions reached in chancy. Usually a case of badly chosen min/max values.'); + } + else { + return 0; + } + } + if (Array.isArray(input)) { + return this.choice(input); + } if (typeof input === 'string') { return this.dice(input); } if (typeof input === 'object') { + input.type = input.type ?? 'random'; + if (input.type === 'random' || + input.type === 'int' || + input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; + } + } switch (input.type) { - case 'normal': - return this.normal(input); - break; + case 'random': + return this.random(input.min, input.max, input.skew); + case 'int': + case 'integer': + return this.randInt(input.min, input.max, input.skew); case 'normal_integer': + case 'normal_int': return Math.floor(this.normal(input)); - break; - case 'integer': - return this.randInt(input.min ?? 0, input.max ?? 1, input.skew ?? 0); - break; - default: - return this.random(input.min ?? 0, input.max ?? 1, input.skew ?? 0); + case 'dice': + return this.chancyMinMax(this.dice(input.dice ?? input), input, depth); + case 'rademacher': + return this.chancyMinMax(this.rademacher(), input, depth); + case 'normal': + case 'gaussian': + case 'boxMuller': + case 'irwinHall': + case 'bates': + case 'batesgaussian': + case 'bernoulli': + case 'exponential': + case 'pareto': + case 'poisson': + case 'hypergeometric': + case 'binomial': + case 'betaBinomial': + case 'beta': + case 'gamma': + case 'studentsT': + case 'wignerSemicircle': + case 'kumaraswamy': + case 'hermite': + case 'chiSquared': + case 'rayleigh': + case 'logNormal': + case 'cauchy': + case 'laplace': + case 'logistic': + return this.chancyMinMax(this[input.type](input), input, depth); } + throw new Error(`Invalid input type given to chancy: "${input.type}".`); } if (typeof input === 'number') { return input; } throw new Error('Invalid input given to chancy'); } + chancyMinMax(result, input, depth = 0) { + const { min, max } = input; + if ((depth + 1) >= MAX_RECURSIONS && !this.shouldThrowOnMaxRecursionsReached()) { + if (typeof min !== 'undefined') { + result = Math.max(min, result); + } + if (typeof max !== 'undefined') { + result = Math.min(max, result); + } + // always returns something in bounds. + return result; + } + if (typeof min !== 'undefined' && result < min) { + return this.chancy(input, depth + 1); + } + if (typeof max !== 'undefined' && result > max) { + return this.chancy(input, depth + 1); + } + return result; + } + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + chancyMin(input) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMin(input); + } + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + chancyMax(input) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMax(input); + } + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ static chancyMin(input) { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMin array input'); + } + } + return Math.min(...input); + } if (typeof input === 'string') { return this.diceMin(input); } @@ -525,30 +1708,93 @@ class RngAbstract { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } switch (input.type) { + case 'dice': + return this.diceMin(input.dice); case 'normal': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'normal_integer': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'integer': return input.min ?? 0; - break; - default: + case 'random': return input.min ?? 0; + case 'boxMuller': + return Number.NEGATIVE_INFINITY; + case 'gaussian': + return Number.NEGATIVE_INFINITY; + case 'irwinHall': + return 0; + case 'bates': + return 0; + case 'batesgaussian': + return Number.NEGATIVE_INFINITY; + case 'bernoulli': + return 0; + case 'exponential': + return 0; + case 'pareto': + return input.scale ?? 1; + case 'poisson': + return 1; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { N = 50, K = 10, n = 5 } = input; + return Math.max(0, (n + K - N)); + case 'rademacher': + return -1; + case 'binomial': + return 0; + case 'betaBinomial': + return 0; + case 'beta': + return Number.EPSILON; + case 'gamma': + return Number.EPSILON; + case 'studentsT': + return Number.NEGATIVE_INFINITY; + case 'wignerSemicircle': + return -1 * (input.R ?? 10); + case 'kumaraswamy': + return Number.EPSILON; + case 'hermite': + return 0; + case 'chiSquared': + return 0; + case 'rayleigh': + return 0; + case 'logNormal': + return Number.EPSILON; + case 'cauchy': + return Number.NEGATIVE_INFINITY; + case 'laplace': + return Number.NEGATIVE_INFINITY; + case 'logistic': + return Number.NEGATIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMin'); + throw new Error('Invalid input supplied to chancyMin'); } + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ static chancyMax(input) { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMax array input'); + } + } + return Math.max(...input); + } if (typeof input === 'string') { return this.diceMax(input); } @@ -556,40 +1802,94 @@ class RngAbstract { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } switch (input.type) { + case 'dice': + return this.diceMax(input.dice); case 'normal': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'normal_integer': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'integer': return input.max ?? 1; - break; - default: + case 'random': return input.max ?? 1; + case 'boxMuller': + return Number.POSITIVE_INFINITY; + case 'gaussian': + return Number.POSITIVE_INFINITY; + case 'irwinHall': + return (input.n ?? 6); + case 'bates': + return 1; + case 'batesgaussian': + return Number.POSITIVE_INFINITY; + case 'bernoulli': + return 1; + case 'exponential': + return Number.POSITIVE_INFINITY; + case 'pareto': + return Number.POSITIVE_INFINITY; + case 'poisson': + return Number.MAX_SAFE_INTEGER; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { K = 10, n = 5 } = input; + return Math.min(n, K); + case 'rademacher': + return 1; + case 'binomial': + return (input.n ?? 1); + case 'betaBinomial': + return (input.n ?? 1); + case 'beta': + return 1; + case 'gamma': + return Number.POSITIVE_INFINITY; + case 'studentsT': + return Number.POSITIVE_INFINITY; + case 'wignerSemicircle': + return (input.R ?? 10); + case 'kumaraswamy': + return 1; + case 'hermite': + return Number.MAX_SAFE_INTEGER; + case 'chiSquared': + return Number.POSITIVE_INFINITY; + case 'rayleigh': + return Number.POSITIVE_INFINITY; + case 'logNormal': + return Number.POSITIVE_INFINITY; + case 'cauchy': + return Number.POSITIVE_INFINITY; + case 'laplace': + return Number.POSITIVE_INFINITY; + case 'logistic': + return Number.POSITIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMax'); + throw new Error('Invalid input supplied to chancyMax'); } choice(data) { return this.weightedChoice(data); } - /** - * data format: - * { - * choice1: 1, - * choice2: 2, - * choice3: 3, - * } - */ + weights(data) { + const chances = new Map(); + data.forEach(function (a) { + let init = 0; + if (chances.has(a)) { + init = chances.get(a); + } + chances.set(a, init + 1); + }); + return chances; + } weightedChoice(data) { let total = 0; let id; @@ -601,11 +1901,10 @@ class RngAbstract { if (data.length === 1) { return data[0]; } - const chances = new Map(); - data.forEach(function (a) { - chances.set(a, 1); - }); - return this.weightedChoice(chances); + const chances = this.weights(data); + const result = this.weightedChoice(chances); + chances.clear(); + return result; } if (data instanceof Map) { // Some shortcuts @@ -657,6 +1956,9 @@ class RngAbstract { // random >= total, just return the last id. return id; } + pool(entries) { + return new Pool(entries, this); + } static parseDiceArgs(n = 1, d = 6, plus = 0) { if (n === null || typeof n === 'undefined' || arguments.length <= 0) { throw new Error('Dice expects at least one argument'); @@ -669,114 +1971,200 @@ class RngAbstract { [n, d, plus] = n; } else { - d = n.d; - plus = n.plus; - n = n.n; + if (typeof n.n === 'undefined' && + typeof n.d === 'undefined' && + typeof n.plus === 'undefined') { + throw new Error('Invalid input given to dice related function - dice object must have at least one of n, d or plus properties.'); + } + ({ n = 1, d = 6, plus = 0 } = n); } } + (0,src_number/* default */.Ay)({ n }).int(`Expected n to be an integer, got ${n}`); + (0,src_number/* default */.Ay)({ d }).int(`Expected d to be an integer, got ${d}`); return { n, d, plus }; } parseDiceArgs(n = 1, d = 6, plus = 0) { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceArgs(n); } + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ static parseDiceString(string) { // dice string like 5d10+1 if (!diceCache[string]) { + const trimmed = string.replace(/ +/g, ''); + if (/^[+-]*[\d.]+$/.test(trimmed)) { + return { n: 0, d: 0, plus: parseFloat(trimmed) }; + } if (diceRe.test(string)) { - const result = diceRe.exec(string.replace(/ +/g, '')); + const result = diceRe.exec(trimmed); if (result !== null) { diceCache[string] = { - n: (parseInt(result[1]) / 1 || 1), - d: (parseInt(result[2]) / 1 || 1), - plus: (parseFloat(result[3]) / 1 || 0), + n: parseInt(result[1]), + d: parseInt(result[2]), + plus: parseFloat(result[3]), }; + if (Number.isNaN(diceCache[string].n)) { + diceCache[string].n = 1; + } + if (Number.isNaN(diceCache[string].d)) { + diceCache[string].d = 6; + } + if (Number.isNaN(diceCache[string].plus)) { + diceCache[string].plus = 0; + } } } - else if (diceReNoInit.test(string)) { - const result = diceReNoInit.exec(string.replace(/ +/g, '')); - if (result !== null) { - diceCache[string] = { - n: 1, - d: (parseInt(result[1]) / 1 || 1), - plus: (parseFloat(result[2]) / 1 || 0), - }; - } + if (typeof diceCache[string] === 'undefined') { + throw new Error(`Could not parse dice string ${string}`); } } return diceCache[string]; } + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + diceMax(n, d, plus) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMax(n, d, plus); + } + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + diceMin(n, d, plus) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMin(n, d, plus); + } + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ static diceMax(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return (n * d) + plus; } + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ static diceMin(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return n + plus; } - dice(n = 1, d = 6, plus = 0) { + diceExpanded(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); if (typeof n === 'number') { - let nval = Math.max(n, 1); - const dval = Math.max(d, 1); + let nval = n; + const dval = Math.max(d, 0); if (d === 1) { - return plus + 1; + return { dice: Array(n).fill(d), plus, total: (n * d + plus) }; + } + if (n === 0 || d === 0) { + return { dice: [], plus, total: plus }; } - let sum = plus || 0; + const multiplier = nval < 0 ? -1 : 1; + nval *= multiplier; + const results = { dice: [], plus, total: plus }; while (nval > 0) { - sum += this.randInt(1, dval); + results.dice.push(multiplier * this.randInt(1, dval)); nval--; } - return sum; + results.total = sum(results.dice) + plus; + return results; } throw new Error('Invalid arguments given to dice'); } + dice(n, d, plus) { + return this.diceExpanded(n, d, plus).total; + } + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ parseDiceString(string) { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceString(string); } clamp(number, lower, upper) { - if (upper !== undefined) { + if (typeof upper !== 'undefined') { number = number <= upper ? number : upper; } - if (lower !== undefined) { + if (typeof lower !== 'undefined') { number = number >= lower ? number : lower; } return number; } bin(val, bins, min, max) { + (0,src_number/* default */.Ay)({ val }).gt(min).lt(max); const spread = max - min; return (Math.round(((val - min) / spread) * (bins - 1)) / (bins - 1) * spread) + min; } } +/** + * @category Main Class + */ class Rng extends RngAbstract { #mask; #seed = 0; + #randFunc; #m_z = 0; constructor(seed) { super(seed); this.#mask = 0xffffffff; this.#m_z = 987654321; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ + static predictable(seed) { + return new this(seed ?? PREDICTABLE_SEED); + } serialize() { return { - mask: this.#mask, + mask: this.getMask(), seed: this.getSeed(), - m_z: this.#m_z, + m_z: this.getMz(), }; } sameAs(other) { - const s = other.serialize(); - return this.#seed === s.seed && - this.#mask === s.mask && - this.#m_z === s.m_z; + if (other instanceof Rng) { + return this.getRandomSource() === other.getRandomSource() && + this.getSeed() === other.getSeed() && + this.getMask() === other.getMask() && + this.getMz() === other.getMz(); + } + return false; + } + /** @hidden */ + getMask() { + return this.#mask; + } + /** @hidden */ + getMz() { + return this.#m_z; } + /** @hidden */ + setMask(mask) { + this.#mask = mask; + } + /** @hidden */ + setMz(mz) { + this.#m_z = mz; + } + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized) { const rng = new this(); rng.setSeed(serialized.seed); - rng.#mask = serialized.mask; - rng.#seed = serialized.seed; - rng.#m_z = serialized.m_z; + rng.setMask(serialized.mask); + rng.setMz(serialized.m_z); return rng; } seed(i) { @@ -784,7 +2172,7 @@ class Rng extends RngAbstract { this.#m_z = 987654321; return this; } - _random() { + _next() { this.#m_z = (36969 * (this.#m_z & 65535) + (this.#m_z >> 16)) & this.#mask; this.setSeed((18000 * (this.getSeed() & 65535) + (this.getSeed() >> 16)) & this.#mask); let result = ((this.#m_z << 16) + this.getSeed()) & this.#mask; @@ -792,6 +2180,7 @@ class Rng extends RngAbstract { return result + 0.5; } } +/* harmony default export */ const src_rng = (Rng); /***/ }), @@ -805,7 +2194,7 @@ class Rng extends RngAbstract { /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(334); /* harmony import */ var _table_pool__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(425); /* harmony import */ var _table_pool_entry_results__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(673); @@ -840,8 +2229,8 @@ class LootTable { this.pools = pools; this.fn = fn; this.ul = ul; - this.rng = rng ?? (ul ? ul.getRng() : new _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A()); - this.id = id ?? this.rng.uniqstr(6); + this.rng = rng ?? (ul ? ul.getRng() : new _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay()); + this.id = id ?? this.rng.randomString(6); } // Register a function for use in loot pools registerFunction(name, fn) { @@ -1010,9 +2399,9 @@ class LootTable { totalWeight += (entry.weight ?? 1); } } - const rollsMax = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMax(pool.rolls); - const rollsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(pool.rolls); - const nullsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(pool.nulls); + const rollsMax = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMax(pool.rolls); + const rollsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(pool.rolls); + const nullsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(pool.nulls); for (const entry of pool.getEntries()) { if (entry instanceof LootTable || entry.isTable()) { let table; @@ -1040,8 +2429,8 @@ class LootTable { entries.push({ entry, weight: entry.weight / totalWeight, - min: nullsMin > 0 ? 0 : (rollsMin * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(entry.qty)), - max: rollsMax * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMax(entry.qty), + min: nullsMin > 0 ? 0 : (rollsMin * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(entry.qty)), + max: rollsMax * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMax(entry.qty), }); } } @@ -1192,7 +2581,7 @@ class LootTable { /* harmony import */ var _pool_entry_result__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(668); /* harmony import */ var _pool_entry_results__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(784); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(673); @@ -1218,7 +2607,7 @@ class LootPool { this.functions = functions ?? []; this.rolls = rolls; this.nulls = nulls; - this.id = id ?? (new _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A()).uniqstr(6); + this.id = id ?? (new _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .Ay()).randomString(6); this.template = template; if (entries) { for (const entry of entries) { @@ -1455,7 +2844,7 @@ class LootPool { /* harmony export */ }); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(334); /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(784); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(673); /* harmony import */ var _entry_result__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(668); /* harmony import */ var _entry_results__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(219); @@ -1489,7 +2878,7 @@ class LootTableEntry { this.conditions = conditions ?? []; } getRng(rng) { - return rng ?? this.rng ?? (this.rng = new _rng__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A()); + return rng ?? this.rng ?? (this.rng = new _rng__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .Ay()); } setRng(rng) { this.rng = rng; @@ -1765,7 +3154,7 @@ class LootTableEntryResults extends Array { /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(784); /* harmony import */ var _table_pool__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* harmony import */ var _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(50); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(673); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(330); /* harmony import */ var _default_functions__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(663); /* harmony import */ var _default_conditions__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(494); @@ -1903,7 +3292,7 @@ class UltraLoot { 'chanceTo', 'randInt', 'uniqid', - 'uniqstr', + 'randomString', 'randBetween', 'normal', 'chancyInt', @@ -1925,7 +3314,7 @@ class UltraLoot { if (this.isRng(rng)) { return rng; } - const RngConstructor = this.rngConstructor ?? _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A; + const RngConstructor = this.rngConstructor ?? _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .Ay; return new RngConstructor(rng); } registerFunction(name, fn) { @@ -2299,7 +3688,7 @@ class UltraLoot { pools: [] }; clone.pools = []; - const keyToUse = table.filename ?? this.getRng().uniqstr(6); + const keyToUse = table.filename ?? this.getRng().randomString(6); had.add(table); if (includeRng) { clone.rng = table.rng?.serialize() ?? null; @@ -2331,7 +3720,7 @@ class UltraLoot { entryClone.functions = entry.functions; } if (entryClone.item instanceof _table__WEBPACK_IMPORTED_MODULE_1__/* ["default"] */ .A) { - const subKeyToUse = entryClone.item.filename ?? this.getRng().uniqstr(6); + const subKeyToUse = entryClone.item.filename ?? this.getRng().randomString(6); if (had.has(entryClone.item)) { throw new RecursiveTableError('Recursive requirement detected - cannot serialize recursively required tables.'); } @@ -2726,7 +4115,7 @@ module.exports = require("fs"); /***/ 330: /***/ ((module) => { -module.exports = {"rE":"0.1.1"}; +module.exports = {"rE":"0.3.0"}; /***/ }) @@ -2792,31 +4181,93 @@ __webpack_require__.r(__webpack_exports__); // EXPORTS __webpack_require__.d(__webpack_exports__, { + ArrayNumberValidator: () => (/* reexport */ number/* ArrayNumberValidator */.Bh), LootTable: () => (/* reexport */ src_table/* default */.A), LootTableEntry: () => (/* reexport */ entry/* default */.A), LootTableEntryResult: () => (/* reexport */ result/* default */.A), LootTableEntryResults: () => (/* reexport */ results/* default */.A), LootTableManager: () => (/* reexport */ LootTableManager), LootTablePool: () => (/* reexport */ pool/* default */.A), - PredictableRng: () => (/* reexport */ Rng), + MaxRecursionsError: () => (/* reexport */ rng/* MaxRecursionsError */.YG), + NonRandomRandomError: () => (/* reexport */ rng/* NonRandomRandomError */.Qs), + NumberValidationError: () => (/* reexport */ number/* NumberValidationError */.X), + NumberValidator: () => (/* reexport */ number/* NumberValidator */.Ol), + PredictableRng: () => (/* reexport */ PredictableRng), RecursiveTableError: () => (/* reexport */ ultraloot/* RecursiveTableError */.Bc), - Rng: () => (/* reexport */ rng/* default */.A), - RngAbstract: () => (/* reexport */ rng/* RngAbstract */.U), + Rng: () => (/* reexport */ rng/* default */.Ay), + RngAbstract: () => (/* reexport */ rng/* RngAbstract */.Up), UltraLoot: () => (/* reexport */ ultraloot/* UltraLoot */.tZ), "default": () => (/* binding */ src) }); // EXTERNAL MODULE: ./src/ultraloot.ts var ultraloot = __webpack_require__(224); -// EXTERNAL MODULE: ./src/rng.ts -var rng = __webpack_require__(629); +// EXTERNAL MODULE: ./src/number.ts +var number = __webpack_require__(623); +// EXTERNAL MODULE: ./src/rng.ts + 2 modules +var rng = __webpack_require__(673); ;// ./src/rng/predictable.ts /** + * * An Rng type that can be used to give predictable results * for testing purposes, and giving known results. + * + * You can set an array of results that will be returned from called to _next() + * + * Note: To avoid unexpected results when using this in place of regular Rng, it is + * only allowed to make the results spread from [0, 1) + * + * The numbers are returned and cycled, so once you reach the end of the list, it will + * just keep on going. + * + * @category Other Rngs + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0]; + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0, 0.5]; + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.0 + * + * @example + * // The setEvenSpread and evenSpread methods can be used to generate + * // n numbers between [0, 1) with even gaps between + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.setEvenSpread(11); + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.5 + * prng.random(); // 0.6 + * prng.random(); // 0.7 + * prng.random(); // 0.8 + * prng.random(); // 0.9 + * prng.random(); // 0.9999999... + * prng.random(); // 0.0 */ -class Rng extends rng/* RngAbstract */.U { +class PredictableRng extends rng/* RngAbstract */.Up { counter = 0; _results = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; constructor(seed, results) { @@ -2856,14 +4307,18 @@ class Rng extends rng/* RngAbstract */.U { return this; } sameAs(other) { - return this.results.sort().join(',') === other.results.sort().join(',') && - this.counter === other.counter; + if (other instanceof PredictableRng) { + return this.results.join(',') === other.results.join(',') && + this.counter === other.counter && + this.getRandomSource() === other.getRandomSource(); + } + return false; } reset() { this.counter = 0; return this; } - _random() { + _next() { return this.results[this.counter++ % this.results.length]; } } @@ -2943,6 +4398,8 @@ var result = __webpack_require__(668); + + // This provides an easy way of using ultraloot in browser. // It can be instantiated by new UltraLoot() and submodules can be diff --git a/dist/ultraloot.js b/dist/ultraloot.js index 15ac18e..97484f9 100644 --- a/dist/ultraloot.js +++ b/dist/ultraloot.js @@ -271,21 +271,680 @@ if (debug) { /***/ }), -/***/ 629: +/***/ 623: /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ A: () => (/* binding */ Rng), -/* harmony export */ U: () => (/* binding */ RngAbstract) +/* harmony export */ Ay: () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); -const MAX_RECURSIONS = 100; +/* unused harmony exports NumberValidationError, ArrayNumberValidator, NumberValidator */ +/** + * @category Number Validator + */ +const assert = (truthy, msg = 'Assertion failed') => { + if (!truthy) { + throw new NumberValidationError(msg); + } +}; +/** + * @category Number Validator + */ +class NumberValidationError extends Error { +} +/** + * @category Number Validator + */ +class ArrayNumberValidator { + /** + * The numbers to be validated + */ + #numbers = []; + /** + * Descriptive name for this validation + */ + name = 'numbers'; + constructor(numbers, name = 'numbers') { + this.numbers = numbers; + this.name = name; + } + get numbers() { + return this.#numbers; + } + set numbers(numbers) { + for (const number of numbers) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + } + this.#numbers = numbers; + } + /** + * Specify the numbers to validate + */ + all(numbers) { + this.numbers = numbers; + return this; + } + /** + * Specify the numbers to validate + */ + validate(numbers) { + if (!Array.isArray(numbers)) { + return new NumberValidator(numbers); + } + return this.all(numbers); + } + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potatoes = [0, 1]; + * validate(potatoes).varname('potatoes').gt(2); // "Expected every component of potatoes to be > 2, got 0" + */ + varname(name) { + this.name = name; + return this; + } + /** + * Get the sum of our numbers + */ + sum() { + return this.numbers.reduce((a, b) => a + b, 0); + } + /** + * Validates whether the total of all of our numbers is close to sum, with a maximum difference of diff + * @param sum The sum + * @param diff The maximum difference + * @param msg Error message + * @throws {@link NumberValidationError} If they do not sum close to the correct amount + */ + sumcloseto(sum, diff = 0.0001, msg) { + assert(Math.abs(this.sum() - sum) < diff, msg ?? `Expected sum of ${this.name} to be within ${diff} of ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is equal (===) to sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to the correct amount + */ + sumto(sum, msg) { + assert(this.sum() === sum, msg ?? `Expected sum of ${this.name} to be equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is < sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to < sum + */ + sumtolt(sum, msg) { + assert(this.sum() < sum, msg ?? `Expected sum of ${this.name} to be less than ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is > sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to > sum + */ + sumtogt(sum, msg) { + assert(this.sum() > sum, msg ?? `Expected sum of ${this.name} to be greater than ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is <= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to <= sum + */ + sumtolteq(sum, msg) { + assert(this.sum() <= sum, msg ?? `Expected sum of ${this.name} to be less than or equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is >= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to >= sum + */ + sumtogteq(sum, msg) { + assert(this.sum() >= sum, msg ?? `Expected sum of ${this.name} to be greater than or equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all integers + */ + int(msg) { + this.numbers.forEach(a => validate(a).int(msg ?? `Expected every component of ${this.name} to be an integer, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all positive + */ + positive(msg) { + this.numbers.forEach(a => validate(a).positive(msg ?? `Expected every component of ${this.name} to be postiive, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all negative + */ + negative(msg) { + this.numbers.forEach(a => validate(a).negative(msg ?? `Expected every component of ${this.name} to be negative, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all between from and to + */ + between(from, to, msg) { + this.numbers.forEach(a => validate(a).between(from, to, msg ?? `Expected every component of ${this.name} to be between ${from} and ${to}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all between or equal to from and to + */ + betweenEq(from, to, msg) { + this.numbers.forEach(a => validate(a).betweenEq(from, to, msg ?? `Expected every component of ${this.name} to be between or equal to ${from} and ${to}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all > n + */ + gt(n, msg) { + this.numbers.forEach(a => validate(a).gt(n, msg ?? `Expected every component of ${this.name} to be > ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all >= n + */ + gteq(n, msg) { + this.numbers.forEach(a => validate(a).gteq(n, msg ?? `Expected every component of ${this.name} to be >= ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all < n + */ + lt(n, msg) { + this.numbers.forEach(a => validate(a).lt(n, msg ?? `Expected every component of ${this.name} to be < ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all <= n + */ + lteq(n, msg) { + this.numbers.forEach(a => validate(a).lteq(n, msg ?? `Expected every component of ${this.name} to be <= ${n}, got ${a}`)); + return this; + } +} +/** + * Validate numbers in a fluent fashion. + * + * Each validator method accepts a message as the last parameter + * for customising the error message. + * + * @category Number Validator + * + * @example + * const n = new NumberValidator(); + * n.validate(0).gt(1); // NumberValidationError + * + * @example + * const n = new NumberValidator(); + * const probability = -0.1; + * n.validate(probability).gteq(0, 'Probabilities should always be >= 0'); // NumberValidationError('Probabilities should always be >= 0'). + */ +class NumberValidator { + /** + * The number being tested. + */ + #number; + /** + * The name of the variable being validated - shows up in error messages. + */ + name = 'number'; + constructor(number = 0, name = 'number') { + this.number = number; + this.name = name; + } + get number() { + return this.#number; + } + set number(number) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + this.#number = number; + } + /** + * Returns an ArrayNumberValidator for all the numbers + */ + all(numbers, name) { + return new ArrayNumberValidator(numbers, name ?? this.name); + } + assertNumber(num) { + assert(typeof this.number !== 'undefined', 'No number passed to validator.'); + return true; + } + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potato = 1; + * validate(potato).varname('potato').gt(2); // "Expected potato to be greater than 2, got 1" + * @param {string} name [description] + */ + varname(name) { + this.name = name; + return this; + } + /** + * Specify the number to be validated + */ + validate(number) { + if (Array.isArray(number)) { + return this.all(number); + } + this.number = number; + return this; + } + /** + * Asserts that the number is an integer + * @throws {@link NumberValidationError} if ths number is not an integer + */ + int(msg) { + if (this.assertNumber(this.number)) + assert(Number.isInteger(this.number), msg ?? `Expected ${this.name} to be an integer, got ${this.number}`); + return this; + } + /** + * Asserts that the number is > 0 + * @throws {@link NumberValidationError} if the number is not positive + */ + positive(msg) { + return this.gt(0, msg ?? `Expected ${this.name} to be positive, got ${this.number}`); + } + /** + * Asserts that the number is < 0 + * @throws {@link NumberValidationError} if the number is not negative + */ + negative(msg) { + return this.lt(0, msg ?? `Expected ${this.name} to be negative, got ${this.number}`); + } + /** + * Asserts that the from < number < to + * @throws {@link NumberValidationError} if it is outside or equal to those bounds + */ + between(from, to, msg) { + if (this.assertNumber(this.number)) + assert(this.number > from && this.number < to, msg ?? `Expected ${this.name} to be between ${from} and ${to}, got ${this.number}`); + return this; + } + /** + * Asserts that the from <= number <= to + * @throws {@link NumberValidationError} if it is outside those bounds + */ + betweenEq(from, to, msg) { + if (this.assertNumber(this.number)) + assert(this.number >= from && this.number <= to, msg ?? `Expected ${this.name} to be between or equal to ${from} and ${to}, got ${this.number}`); + return this; + } + /** + * Asserts that number > n + * @throws {@link NumberValidationError} if it is less than or equal to n + */ + gt(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number > n, msg ?? `Expected ${this.name} to be greater than ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number >= n + * @throws {@link NumberValidationError} if it is less than n + */ + gteq(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number >= n, msg ?? `Expected ${this.name} to be greater than or equal to ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number < n + * @throws {@link NumberValidationError} if it is greater than or equal to n + */ + lt(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number < n, msg ?? `Expected ${this.name} to be less than ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number <= n + * @throws {@link NumberValidationError} if it is greater than n + */ + lteq(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number <= n, msg ?? `Expected ${this.name} to be less than or equal to ${n}, got ${this.number}`); + return this; + } +} +function validate(number) { + if (Array.isArray(number)) { + return new ArrayNumberValidator(number); + } + else if (typeof number === 'object') { + const entries = Object.entries(number); + if (entries.length === 0) { + throw new Error('Empty object provided'); + } + const [name, value] = entries[0]; + return validate(value).varname(name); + } + else { + return new NumberValidator(number); + } +} +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (validate); + + +/***/ }), + +/***/ 673: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + Up: () => (/* binding */ RngAbstract), + Ay: () => (/* binding */ src_rng) +}); + +// UNUSED EXPORTS: MaxRecursionsError, NonRandomRandomError + +// EXTERNAL MODULE: ./src/number.ts +var src_number = __webpack_require__(623); +;// ./src/rng/pool.ts + +/** + * @category Pool + */ +class PoolEmptyError extends Error { +} +/** + * @category Pool + */ +class PoolNotEnoughElementsError extends Error { +} +/** + * Allows for randomly drawing from a pool of entries without replacement + * @category Pool + */ +class Pool { + rng; + #entries = []; + constructor(entries = [], rng) { + this.entries = entries; + if (rng) { + this.rng = rng; + } + else { + this.rng = new src_rng(); + } + } + copyArray(arr) { + return Array.from(arr); + } + setEntries(entries) { + this.entries = entries; + return this; + } + getEntries() { + return this.#entries; + } + set entries(entries) { + this.#entries = this.copyArray(entries); + } + get entries() { + return this.#entries; + } + get length() { + return this.#entries.length; + } + setRng(rng) { + this.rng = rng; + return this; + } + getRng() { + return this.rng; + } + add(entry) { + this.#entries.push(entry); + } + empty() { + this.#entries = []; + return this; + } + isEmpty() { + return this.length <= 0; + } + /** + * Draw an element from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + */ + draw() { + if (this.length === 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length === 1) { + return this.#entries.splice(0, 1)[0]; + } + const idx = this.rng.randInt(0, this.#entries.length - 1); + return this.#entries.splice(idx, 1)[0]; + } + /** + * Draw n elements from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + * @throws {@link PoolNotEnoughElementsError} if the pool does not have enough elements to draw n values + */ + drawMany(n) { + if (n < 0) { + throw new Error('Cannot draw < 0 elements from pool'); + } + if (this.length === 0 && n > 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length < n) { + throw new PoolNotEnoughElementsError(`Tried to draw ${n} elements from pool with only ${this.length} entries.`); + } + const result = []; + for (let i = 0; i < n; i++) { + const idx = this.rng.randInt(0, this.#entries.length - 1); + result.push(this.#entries.splice(idx, 1)[0]); + } + return result; + } +} + +;// ./src/rng/queue.ts +class Dequeue { + size; + elements = []; + constructor(length = 1) { + if (Array.isArray(length)) { + this.elements = length; + this.size = this.elements.length; + } + else { + this.size = length; + } + } + get length() { + return this.elements.length; + } + push(el) { + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + pop() { + return this.elements.pop(); + } + full() { + return this.length >= this.size; + } + empty() { + this.elements = []; + } + get(i) { + return this.elements[i]; + } + allSame() { + if (this.length > 0) { + return this.elements.every(a => a === this.elements[0]); + } + return true; + } +} +class NumberQueue extends (/* unused pure expression or super */ null && (Dequeue)) { + sum() { + return this.elements.reduce((a, b) => a + b, 0); + } + avg() { + return this.sum() / this.length; + } +} +class LoopDetectedError extends Error { +} +class NonRandomDetector extends Dequeue { + minsequencelength = 2; + errormessage = 'Loop detected in input data. Randomness source not random?'; + constructor(length = 1, minsequencelength = 2) { + super(length); + if (this.size > 10000) { + throw new Error('Cannot detect loops for more than 10000 elements'); + } + this.minsequencelength = minsequencelength; + } + push(el) { + this.detectLoop(); + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + detectLoop(msg) { + if (this.full()) { + if (this.allSame()) { + this.loopDetected(msg); + } + if (this.hasRepeatingSequence(this.elements, this.minsequencelength)) { + this.loopDetected(msg); + } + } + } + loopDetected(msg) { + throw new LoopDetectedError(msg ?? this.errormessage); + } + /** + * Checks if there is a repeating sequence longer than a specified length in an array of numbers. + * + * @param {number[]} arr - The array of numbers. + * @param {number} n - The minimum length of the repeating sequence. + * @returns {boolean} True if a repeating sequence longer than length n is found, otherwise false. + */ + hasRepeatingSequence(arr, n) { + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + let k = 0; + while (j + k < arr.length && arr[i + k] === arr[j + k]) { + k++; + if (k > n) { + return true; + } + } + } + } + return false; + } +} + +;// ./src/rng.ts + + + +/** + * Safeguard against huge loops. If loops unintentionally grow beyond this + * arbitrary limit, bail out.. + */ +const LOOP_MAX = 10000000; +/** + * Safeguard against too much recursion - if a function recurses more than this, + * we know we have a problem. + * + * Max recursion limit is around ~1000 anyway, so would get picked up by interpreter. + */ +const MAX_RECURSIONS = 500; const THROW_ON_MAX_RECURSIONS_REACHED = true; -const diceRe = /^ *([0-9]+) *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; -const diceReNoInit = /^ *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; +const PREDICTABLE_SEED = 5789938451; +const SAMERANDOM_MAX = 10; +const diceRe = /^ *([+-]? *[0-9_]*) *[dD] *([0-9_]+) *([+-]? *[0-9_.]*) *$/; const strToNumberCache = {}; const diceCache = {}; +class MaxRecursionsError extends Error { +} +class NonRandomRandomError extends (/* unused pure expression or super */ null && (Error)) { +} +function sum(numbersFirstArg, ...numbers) { + if (Array.isArray(numbersFirstArg)) { + return numbersFirstArg.reduce((a, b) => a + b, 0); + } + return numbers.reduce((a, b) => a + b, 0); +} +function isNumeric(input) { + return (typeof input === 'number') || (!isNaN(parseFloat(input)) && isFinite(input)); +} +/** + * This abstract class implements most concrete implementations of + * functions, as the only underlying changes are likely to be to the + * uniform random number generation, and how that is handled. + * + * All the typedoc documentation for this has been sharded out to RngInterface + * in a separate file. + */ class RngAbstract { #seed = 0; + #monotonic = 0; + #lastuniqid = 0; + #randFunc; + #shouldThrowOnMaxRecursionsReached; + #distributions = [ + 'normal', + 'gaussian', + 'boxMuller', + 'irwinHall', + 'bates', + 'batesgaussian', + 'bernoulli', + 'exponential', + 'pareto', + 'poisson', + 'hypergeometric', + 'rademacher', + 'binomial', + 'betaBinomial', + 'beta', + 'gamma', + 'studentsT', + 'wignerSemicircle', + 'kumaraswamy', + 'hermite', + 'chiSquared', + 'rayleigh', + 'logNormal', + 'cauchy', + 'laplace', + 'logistic', + ]; constructor(seed) { this.setSeed(seed); } @@ -293,7 +952,17 @@ class RngAbstract { return this.#seed; } sameAs(other) { - return this.#seed === other.#seed; + if (other instanceof RngAbstract) { + return this.#seed === other.#seed && this.#randFunc === other.#randFunc; + } + return false; + } + randomSource(source) { + this.#randFunc = source; + return this; + } + getRandomSource() { + return this.#randFunc; } setSeed(seed) { if (typeof seed !== 'undefined' && seed !== null) { @@ -316,6 +985,10 @@ class RngAbstract { seed: this.#seed, }; } + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized) { const { constructor } = Object.getPrototypeOf(this); const rng = new constructor(serialized.seed); @@ -324,11 +997,15 @@ class RngAbstract { } predictable(seed) { const { constructor } = Object.getPrototypeOf(this); - const newSelf = new constructor(seed); + const newSelf = new constructor(seed ?? PREDICTABLE_SEED); return newSelf; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ static predictable(seed) { - return new this(seed); + return new this(seed ?? PREDICTABLE_SEED); } hashStr(str) { let hash = 0; @@ -352,23 +1029,36 @@ class RngAbstract { return num; } _random() { - return Math.random(); + if (typeof this.#randFunc === 'function') { + return this.#randFunc(); + } + return this._next(); } percentage() { return this.randBetween(0, 100); } + probability() { + return this.randBetween(0, 1); + } random(from = 0, to = 1, skew = 0) { return this.randBetween(from, to, skew); } chance(n, chanceIn = 1) { + (0,src_number/* default */.Ay)({ chanceIn }).positive(); + (0,src_number/* default */.Ay)({ n }).positive(); const chance = n / chanceIn; return this._random() <= chance; } // 500 to 1 chance, for example chanceTo(from, to) { - return this._random() <= (from / (from + to)); + return this.chance(from, from + to); } randInt(from = 0, to = 1, skew = 0) { + (0,src_number/* default */.Ay)({ from }).int(); + (0,src_number/* default */.Ay)({ to }).int(); + if (from === to) { + return from; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -379,14 +1069,21 @@ class RngAbstract { } return Math.floor(rand * ((to + 1) - from)) + from; } - // Not deterministic - uniqid(prefix = '', random = false) { - const sec = Date.now() * 1000 + Math.random() * 1000; + uniqid(prefix = '') { + const now = Date.now() * 1000; + if (this.#lastuniqid === now) { + this.#monotonic++; + } + else { + this.#monotonic = Math.round(this._random() * 100); + } + const sec = now + this.#monotonic; const id = sec.toString(16).replace(/\./g, '').padEnd(14, '0'); - return `${prefix}${id}${random ? `.${Math.trunc(Math.random() * 100000000)}` : ''}`; + this.#lastuniqid = now; + return `${prefix}${id}`; } - // Deterministic - uniqstr(len = 6) { + randomString(len = 6) { + (0,src_number/* default */.Ay)({ len }).gt(0); const str = []; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const alen = 61; @@ -395,7 +1092,10 @@ class RngAbstract { } return str.join(''); } - randBetween(from = 0, to = 1, skew = 0) { + randBetween(from = 0, to, skew = 0) { + if (typeof to === 'undefined') { + to = from + 1; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -407,57 +1107,97 @@ class RngAbstract { return this.scaleNorm(rand, from, to); } scale(number, from, to, min = 0, max = 1) { - if (number > max) - throw new Error(`Number ${number} is greater than max of ${max}`); - if (number < min) - throw new Error(`Number ${number} is less than min of ${min}`); + (0,src_number/* default */.Ay)({ number }).lteq(max); + (0,src_number/* default */.Ay)({ number }).gteq(min); // First we scale the number in the range [0-1) number = (number - min) / (max - min); return this.scaleNorm(number, from, to); } scaleNorm(number, from, to) { - if (number > 1 || number < 0) - throw new Error(`Number must be < 1 and > 0, got ${number}`); + (0,src_number/* default */.Ay)({ number }).betweenEq(0, 1); return (number * (to - from)) + from; } - shouldThrowOnMaxRecursionsReached() { + shouldThrowOnMaxRecursionsReached(val) { + if (typeof val === 'boolean') { + this.#shouldThrowOnMaxRecursionsReached = val; + return this; + } + if (typeof this.#shouldThrowOnMaxRecursionsReached !== 'undefined') { + return this.#shouldThrowOnMaxRecursionsReached; + } return THROW_ON_MAX_RECURSIONS_REACHED; } - // Gaussian number between 0 and 1 - normal({ mean, stddev = 1, max, min, skew = 0 } = {}, depth = 0) { - if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { - throw new Error('Max recursive calls to rng normal function. This might be as a result of using predictable random numbers?'); - } - let num = this.boxMuller(); - num = num / 10.0 + 0.5; // Translate to 0 -> 1 - if (depth > MAX_RECURSIONS) { - num = Math.min(Math.max(num, 0), 1); + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + * @throws {@link MaxRecursionsError} If the function recurses too many times in trying to generate in bounds numbers + */ + normal({ mean, stddev, max, min, skew = 0 } = {}, depth = 0) { + if (typeof min === 'undefined' && typeof max === 'undefined') { + return this.gaussian({ mean, stddev, skew }); } - else { - if (num > 1 || num < 0) { - return this.normal({ mean, stddev, max, min, skew }, depth + 1); // resample between 0 and 1 - } + if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError(`Max recursive calls to rng normal function. This might be as a result of using predictable random numbers, or inappropriate arguments? Args: ${JSON.stringify({ mean, stddev, max, min, skew })}`); } + let num = this.bates(7); if (skew < 0) { num = 1 - (Math.pow(num, Math.pow(2, skew))); } else { num = Math.pow(num, Math.pow(2, -skew)); } + if (typeof mean === 'undefined' && + typeof stddev === 'undefined' && + typeof max !== 'undefined' && + typeof min !== 'undefined') { + // This is a simple scaling of the bates distribution. + return this.scaleNorm(num, min, max); + } + num = (num * 10) - 5; if (typeof mean === 'undefined') { mean = 0; if (typeof max !== 'undefined' && typeof min !== 'undefined') { - num *= max - min; - num += min; + mean = (max + min) / 2; + if (typeof stddev === 'undefined') { + stddev = Math.abs(max - min) / 10; + } } - else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + stddev = 0.1; } + num = num * stddev + mean; } else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + if (typeof max !== 'undefined' && typeof min !== 'undefined') { + stddev = Math.abs(max - min) / 10; + } + else { + stddev = 0.1; + } + } num = num * stddev + mean; } if (depth <= MAX_RECURSIONS && ((typeof max !== 'undefined' && num > max) || (typeof min !== 'undefined' && num < min))) { @@ -476,48 +1216,489 @@ class RngAbstract { } return num; } - // Standard Normal variate using Box-Muller transform. + gaussian({ mean = 0, stddev = 1, skew = 0 } = {}) { + (0,src_number/* default */.Ay)({ stddev }).positive(); + if (skew === 0) { + return this.boxMuller({ mean, stddev }); + } + let num = this.boxMuller({ mean: 0, stddev: 1 }); + num = num / 10.0 + 0.5; // Translate to 0 -> 1 + if (skew < 0) { + num = 1 - (Math.pow(num, Math.pow(2, skew))); + } + else { + num = Math.pow(num, Math.pow(2, -skew)); + } + num = num * 10; + num = num - 5; + num = num * stddev + mean; + return num; + } boxMuller(mean = 0, stddev = 1) { + if (typeof mean === 'object') { + ({ mean = 0, stddev = 1 } = mean); + } + (0,src_number/* default */.Ay)({ stddev }).gteq(0); const u = 1 - this._random(); // Converting [0,1) to (0,1] const v = this._random(); const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); // Transform to the desired mean and standard deviation: return z * stddev + mean; } + irwinHall(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().positive(); + let sum = 0; + for (let i = 0; i < n; i++) { + sum += this._random(); + } + return sum; + } + bates(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().positive(); + return this.irwinHall({ n }) / n; + } + batesgaussian(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().gt(1); + return (this.irwinHall({ n }) / Math.sqrt(n)) - ((1 / Math.sqrt(1 / n)) / 2); + } + bernoulli(p = 0.5) { + if (typeof p === 'object') { + ({ p = 0.5 } = p); + } + (0,src_number/* default */.Ay)({ p }).lteq(1).gteq(0); + return this._random() < p ? 1 : 0; + } + exponential(rate = 1) { + if (typeof rate === 'object') { + ({ rate = 1 } = rate); + } + (0,src_number/* default */.Ay)({ rate }).gt(0); + return -Math.log(1 - this._random()) / rate; + } + pareto({ shape = 0.5, scale = 1, location = 0 } = {}) { + (0,src_number/* default */.Ay)({ shape }).gteq(0); + (0,src_number/* default */.Ay)({ scale }).positive(); + const u = this._random(); + if (shape !== 0) { + return location + (scale / shape) * (Math.pow(u, -shape) - 1); + } + else { + return location - scale * Math.log(u); + } + } + poisson(lambda = 1) { + if (typeof lambda === 'object') { + ({ lambda = 1 } = lambda); + } + (0,src_number/* default */.Ay)({ lambda }).positive(); + const L = Math.exp(-lambda); + let k = 0; + let p = 1; + let i = 0; + const nq = new NonRandomDetector(SAMERANDOM_MAX, 2); + do { + k++; + const r = this._random(); + nq.push(r); + p *= r; + nq.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the poisson distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${r}`); + } while (p > L && i++ < LOOP_MAX); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in poisson - bailing out - possible parameter error, or using non-random source?'); + } + return k - 1; + } + hypergeometric({ N = 50, K = 10, n = 5, k } = {}) { + (0,src_number/* default */.Ay)({ N }).int().positive(); + (0,src_number/* default */.Ay)({ K }).int().positive().lteq(N); + (0,src_number/* default */.Ay)({ n }).int().positive().lteq(N); + if (typeof k === 'undefined') { + k = this.randInt(0, Math.min(K, n)); + } + (0,src_number/* default */.Ay)({ k }).int().betweenEq(0, Math.min(K, n)); + function logFactorial(x) { + let res = 0; + for (let i = 2; i <= x; i++) { + res += Math.log(i); + } + return res; + } + function logCombination(a, b) { + return logFactorial(a) - logFactorial(b) - logFactorial(a - b); + } + const logProb = logCombination(K, k) + logCombination(N - K, n - k) - logCombination(N, n); + return Math.exp(logProb); + } + rademacher() { + return this._random() < 0.5 ? -1 : 1; + } + binomial({ n = 1, p = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ n }).int().positive(); + (0,src_number/* default */.Ay)({ p }).betweenEq(0, 1); + let successes = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + successes++; + } + } + return successes; + } + betaBinomial({ alpha = 1, beta = 1, n = 1 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).positive(); + (0,src_number/* default */.Ay)({ beta }).positive(); + (0,src_number/* default */.Ay)({ n }).int().positive(); + const bd = (alpha, beta) => { + let x = this._random(); + let y = this._random(); + x = Math.pow(x, 1 / alpha); + y = Math.pow(y, 1 / beta); + return x / (x + y); + }; + const p = bd(alpha, beta); + let k = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + k++; + } + } + return k; + } + beta({ alpha = 0.5, beta = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).positive(); + (0,src_number/* default */.Ay)({ beta }).positive(); + const gamma = (alpha) => { + let x = 0; + for (let i = 0; i < alpha; i++) { + const r = this._random(); + x += -Math.log(r); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in beta - bailing out - possible parameter error, or using non-random source?'); + } + } + return x; + }; + const x = gamma(alpha); + const y = gamma(beta); + return x / (x + y); + } + gamma({ shape = 1, rate, scale } = {}) { + (0,src_number/* default */.Ay)({ shape }).positive(); + if (typeof scale !== 'undefined' && typeof rate !== 'undefined' && rate !== 1 / scale) { + throw new Error('Cannot supply rate and scale'); + } + if (typeof scale !== 'undefined') { + (0,src_number/* default */.Ay)({ scale }).positive(); + rate = 1 / scale; + } + if (typeof rate === 'undefined') { + rate = 1; + } + if (rate) { + (0,src_number/* default */.Ay)({ rate }).positive(); + } + let flg; + let x2; + let v0; + let v1; + let x; + let u; + let v = 1; + const d = shape - 1 / 3; + const c = 1.0 / Math.sqrt(9.0 * d); + let i = 0; + flg = true; + const nq1 = new NonRandomDetector(SAMERANDOM_MAX); + while (flg && i++ < LOOP_MAX) { + let j = 0; + const nq2 = new NonRandomDetector(SAMERANDOM_MAX); + do { + x = this.normal(); + nq2.push(x); + nq2.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul ofthe looped way of generating.`); + v = 1.0 + (c * x); + } while (v <= 0.0 && j++ < LOOP_MAX); + if ((j + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma inner loop - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + v *= Math.pow(v, 2); + x2 = Math.pow(x, 2); + v0 = 1.0 - (0.331 * x2 * x2); + v1 = (0.5 * x2) + (d * (1.0 - v + Math.log(v))); + u = this._random(); + nq1.push(u); + nq1.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${u}`); + if (u < v0 || Math.log(u) < v1) { + flg = false; + } + } + if ((i + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + return rate * d * v; + } + studentsT(nu = 1) { + if (typeof nu === 'object') { + ({ nu = 1 } = nu); + } + (0,src_number/* default */.Ay)({ nu }).positive(); + const normal = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + const chiSquared = this.gamma({ shape: nu / 2, rate: 2 }); + return normal / Math.sqrt(chiSquared / nu); + } + wignerSemicircle(R = 1) { + if (typeof R === 'object') { + ({ R = 1 } = R); + } + (0,src_number/* default */.Ay)({ R }).gt(0); + const theta = this._random() * 2 * Math.PI; + return R * Math.cos(theta); + } + kumaraswamy({ alpha = 0.5, beta = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).gt(0); + (0,src_number/* default */.Ay)({ beta }).gt(0); + const u = this._random(); + return Math.pow(1 - Math.pow(1 - u, 1 / beta), 1 / alpha); + } + hermite({ lambda1 = 1, lambda2 = 2 } = {}) { + (0,src_number/* default */.Ay)({ lambda1 }).gt(0); + (0,src_number/* default */.Ay)({ lambda2 }).gt(0); + const x1 = this.poisson({ lambda: lambda1 }); + const x2 = this.poisson({ lambda: lambda2 }); + return x1 + x2; + } + chiSquared(k = 1) { + if (typeof k === 'object') { + ({ k = 1 } = k); + } + (0,src_number/* default */.Ay)({ k }).positive().int(); + let sum = 0; + for (let i = 0; i < k; i++) { + const z = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + sum += z * z; + } + return sum; + } + rayleigh(scale = 1) { + if (typeof scale === 'object') { + ({ scale = 1 } = scale); + } + (0,src_number/* default */.Ay)({ scale }).gt(0); + return scale * Math.sqrt(-2 * Math.log(this._random())); + } + logNormal({ mean = 0, stddev = 1 } = {}) { + (0,src_number/* default */.Ay)({ stddev }).gt(0); + const normal = mean + stddev * Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + return Math.exp(normal); + } + cauchy({ median = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random(); + return median + scale * Math.tan(Math.PI * (u - 0.5)); + } + laplace({ mean = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random() - 0.5; + return mean - scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)); + } + logistic({ mean = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random(); + return mean + scale * Math.log(u / (1 - u)); + } + /** + * Returns the support of the given distribution. + * + * @see [Wikipedia - Support (mathematics)](https://en.wikipedia.org/wiki/Support_(mathematics)#In_probability_and_measure_theory) + */ + support(distribution) { + const map = { + random: '[min, max)', + integer: '[min, max]', + normal: '(-INF, INF)', + boxMuller: '(-INF, INF)', + gaussian: '(-INF, INF)', + irwinHall: '[0, n]', + bates: '[0, 1]', + batesgaussian: '(-INF, INF)', + bernoulli: '{0, 1}', + exponential: '[0, INF)', + pareto: '[scale, INF)', + poisson: '{1, 2, 3 ...}', + hypergeometric: '{max(0, n+K-N), ..., min(n, K)}', + rademacher: '{-1, 1}', + binomial: '{0, 1, 2, ..., n}', + betaBinomial: '{0, 1, 2, ..., n}', + beta: '(0, 1)', + gamma: '(0, INF)', + studentsT: '(-INF, INF)', + wignerSemicircle: '[-R; +R]', + kumaraswamy: '(0, 1)', + hermite: '{0, 1, 2, 3, ...}', + chiSquared: '[0, INF)', + rayleigh: '[0, INF)', + logNormal: '(0, INF)', + cauchy: '(-INF, +INF)', + laplace: '(-INF, +INF)', + logistic: '(-INF, +INF)', + }; + return map[distribution]; + } chancyInt(input) { if (typeof input === 'number') { return Math.round(input); } + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyInt'); + } + } + let choice = this.choice(input); + if (typeof choice !== 'number') { + choice = parseFloat(choice); + } + return Math.round(choice); + } if (typeof input === 'object') { - input.type = 'integer'; + const type = input.type ?? 'random'; + if (type === 'random') { + input.type = 'integer'; + } + else if (type === 'normal') { + input.type = 'normal_integer'; + } } - return this.chancy(input); + return Math.round(this.chancy(input)); } - chancy(input) { + chancy(input, depth = 0) { + if (depth >= MAX_RECURSIONS) { + if (this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError('Max recursions reached in chancy. Usually a case of badly chosen min/max values.'); + } + else { + return 0; + } + } + if (Array.isArray(input)) { + return this.choice(input); + } if (typeof input === 'string') { return this.dice(input); } if (typeof input === 'object') { + input.type = input.type ?? 'random'; + if (input.type === 'random' || + input.type === 'int' || + input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; + } + } switch (input.type) { - case 'normal': - return this.normal(input); - break; + case 'random': + return this.random(input.min, input.max, input.skew); + case 'int': + case 'integer': + return this.randInt(input.min, input.max, input.skew); case 'normal_integer': + case 'normal_int': return Math.floor(this.normal(input)); - break; - case 'integer': - return this.randInt(input.min ?? 0, input.max ?? 1, input.skew ?? 0); - break; - default: - return this.random(input.min ?? 0, input.max ?? 1, input.skew ?? 0); + case 'dice': + return this.chancyMinMax(this.dice(input.dice ?? input), input, depth); + case 'rademacher': + return this.chancyMinMax(this.rademacher(), input, depth); + case 'normal': + case 'gaussian': + case 'boxMuller': + case 'irwinHall': + case 'bates': + case 'batesgaussian': + case 'bernoulli': + case 'exponential': + case 'pareto': + case 'poisson': + case 'hypergeometric': + case 'binomial': + case 'betaBinomial': + case 'beta': + case 'gamma': + case 'studentsT': + case 'wignerSemicircle': + case 'kumaraswamy': + case 'hermite': + case 'chiSquared': + case 'rayleigh': + case 'logNormal': + case 'cauchy': + case 'laplace': + case 'logistic': + return this.chancyMinMax(this[input.type](input), input, depth); } + throw new Error(`Invalid input type given to chancy: "${input.type}".`); } if (typeof input === 'number') { return input; } throw new Error('Invalid input given to chancy'); } + chancyMinMax(result, input, depth = 0) { + const { min, max } = input; + if ((depth + 1) >= MAX_RECURSIONS && !this.shouldThrowOnMaxRecursionsReached()) { + if (typeof min !== 'undefined') { + result = Math.max(min, result); + } + if (typeof max !== 'undefined') { + result = Math.min(max, result); + } + // always returns something in bounds. + return result; + } + if (typeof min !== 'undefined' && result < min) { + return this.chancy(input, depth + 1); + } + if (typeof max !== 'undefined' && result > max) { + return this.chancy(input, depth + 1); + } + return result; + } + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + chancyMin(input) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMin(input); + } + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + chancyMax(input) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMax(input); + } + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ static chancyMin(input) { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMin array input'); + } + } + return Math.min(...input); + } if (typeof input === 'string') { return this.diceMin(input); } @@ -525,30 +1706,93 @@ class RngAbstract { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } switch (input.type) { + case 'dice': + return this.diceMin(input.dice); case 'normal': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'normal_integer': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'integer': return input.min ?? 0; - break; - default: + case 'random': return input.min ?? 0; + case 'boxMuller': + return Number.NEGATIVE_INFINITY; + case 'gaussian': + return Number.NEGATIVE_INFINITY; + case 'irwinHall': + return 0; + case 'bates': + return 0; + case 'batesgaussian': + return Number.NEGATIVE_INFINITY; + case 'bernoulli': + return 0; + case 'exponential': + return 0; + case 'pareto': + return input.scale ?? 1; + case 'poisson': + return 1; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { N = 50, K = 10, n = 5 } = input; + return Math.max(0, (n + K - N)); + case 'rademacher': + return -1; + case 'binomial': + return 0; + case 'betaBinomial': + return 0; + case 'beta': + return Number.EPSILON; + case 'gamma': + return Number.EPSILON; + case 'studentsT': + return Number.NEGATIVE_INFINITY; + case 'wignerSemicircle': + return -1 * (input.R ?? 10); + case 'kumaraswamy': + return Number.EPSILON; + case 'hermite': + return 0; + case 'chiSquared': + return 0; + case 'rayleigh': + return 0; + case 'logNormal': + return Number.EPSILON; + case 'cauchy': + return Number.NEGATIVE_INFINITY; + case 'laplace': + return Number.NEGATIVE_INFINITY; + case 'logistic': + return Number.NEGATIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMin'); + throw new Error('Invalid input supplied to chancyMin'); } + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ static chancyMax(input) { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMax array input'); + } + } + return Math.max(...input); + } if (typeof input === 'string') { return this.diceMax(input); } @@ -556,40 +1800,94 @@ class RngAbstract { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } switch (input.type) { + case 'dice': + return this.diceMax(input.dice); case 'normal': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'normal_integer': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'integer': return input.max ?? 1; - break; - default: + case 'random': return input.max ?? 1; + case 'boxMuller': + return Number.POSITIVE_INFINITY; + case 'gaussian': + return Number.POSITIVE_INFINITY; + case 'irwinHall': + return (input.n ?? 6); + case 'bates': + return 1; + case 'batesgaussian': + return Number.POSITIVE_INFINITY; + case 'bernoulli': + return 1; + case 'exponential': + return Number.POSITIVE_INFINITY; + case 'pareto': + return Number.POSITIVE_INFINITY; + case 'poisson': + return Number.MAX_SAFE_INTEGER; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { K = 10, n = 5 } = input; + return Math.min(n, K); + case 'rademacher': + return 1; + case 'binomial': + return (input.n ?? 1); + case 'betaBinomial': + return (input.n ?? 1); + case 'beta': + return 1; + case 'gamma': + return Number.POSITIVE_INFINITY; + case 'studentsT': + return Number.POSITIVE_INFINITY; + case 'wignerSemicircle': + return (input.R ?? 10); + case 'kumaraswamy': + return 1; + case 'hermite': + return Number.MAX_SAFE_INTEGER; + case 'chiSquared': + return Number.POSITIVE_INFINITY; + case 'rayleigh': + return Number.POSITIVE_INFINITY; + case 'logNormal': + return Number.POSITIVE_INFINITY; + case 'cauchy': + return Number.POSITIVE_INFINITY; + case 'laplace': + return Number.POSITIVE_INFINITY; + case 'logistic': + return Number.POSITIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMax'); + throw new Error('Invalid input supplied to chancyMax'); } choice(data) { return this.weightedChoice(data); } - /** - * data format: - * { - * choice1: 1, - * choice2: 2, - * choice3: 3, - * } - */ + weights(data) { + const chances = new Map(); + data.forEach(function (a) { + let init = 0; + if (chances.has(a)) { + init = chances.get(a); + } + chances.set(a, init + 1); + }); + return chances; + } weightedChoice(data) { let total = 0; let id; @@ -601,11 +1899,10 @@ class RngAbstract { if (data.length === 1) { return data[0]; } - const chances = new Map(); - data.forEach(function (a) { - chances.set(a, 1); - }); - return this.weightedChoice(chances); + const chances = this.weights(data); + const result = this.weightedChoice(chances); + chances.clear(); + return result; } if (data instanceof Map) { // Some shortcuts @@ -657,6 +1954,9 @@ class RngAbstract { // random >= total, just return the last id. return id; } + pool(entries) { + return new Pool(entries, this); + } static parseDiceArgs(n = 1, d = 6, plus = 0) { if (n === null || typeof n === 'undefined' || arguments.length <= 0) { throw new Error('Dice expects at least one argument'); @@ -669,114 +1969,200 @@ class RngAbstract { [n, d, plus] = n; } else { - d = n.d; - plus = n.plus; - n = n.n; + if (typeof n.n === 'undefined' && + typeof n.d === 'undefined' && + typeof n.plus === 'undefined') { + throw new Error('Invalid input given to dice related function - dice object must have at least one of n, d or plus properties.'); + } + ({ n = 1, d = 6, plus = 0 } = n); } } + (0,src_number/* default */.Ay)({ n }).int(`Expected n to be an integer, got ${n}`); + (0,src_number/* default */.Ay)({ d }).int(`Expected d to be an integer, got ${d}`); return { n, d, plus }; } parseDiceArgs(n = 1, d = 6, plus = 0) { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceArgs(n); } + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ static parseDiceString(string) { // dice string like 5d10+1 if (!diceCache[string]) { + const trimmed = string.replace(/ +/g, ''); + if (/^[+-]*[\d.]+$/.test(trimmed)) { + return { n: 0, d: 0, plus: parseFloat(trimmed) }; + } if (diceRe.test(string)) { - const result = diceRe.exec(string.replace(/ +/g, '')); + const result = diceRe.exec(trimmed); if (result !== null) { diceCache[string] = { - n: (parseInt(result[1]) / 1 || 1), - d: (parseInt(result[2]) / 1 || 1), - plus: (parseFloat(result[3]) / 1 || 0), + n: parseInt(result[1]), + d: parseInt(result[2]), + plus: parseFloat(result[3]), }; + if (Number.isNaN(diceCache[string].n)) { + diceCache[string].n = 1; + } + if (Number.isNaN(diceCache[string].d)) { + diceCache[string].d = 6; + } + if (Number.isNaN(diceCache[string].plus)) { + diceCache[string].plus = 0; + } } } - else if (diceReNoInit.test(string)) { - const result = diceReNoInit.exec(string.replace(/ +/g, '')); - if (result !== null) { - diceCache[string] = { - n: 1, - d: (parseInt(result[1]) / 1 || 1), - plus: (parseFloat(result[2]) / 1 || 0), - }; - } + if (typeof diceCache[string] === 'undefined') { + throw new Error(`Could not parse dice string ${string}`); } } return diceCache[string]; } + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + diceMax(n, d, plus) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMax(n, d, plus); + } + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + diceMin(n, d, plus) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMin(n, d, plus); + } + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ static diceMax(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return (n * d) + plus; } + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ static diceMin(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return n + plus; } - dice(n = 1, d = 6, plus = 0) { + diceExpanded(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); if (typeof n === 'number') { - let nval = Math.max(n, 1); - const dval = Math.max(d, 1); + let nval = n; + const dval = Math.max(d, 0); if (d === 1) { - return plus + 1; + return { dice: Array(n).fill(d), plus, total: (n * d + plus) }; + } + if (n === 0 || d === 0) { + return { dice: [], plus, total: plus }; } - let sum = plus || 0; + const multiplier = nval < 0 ? -1 : 1; + nval *= multiplier; + const results = { dice: [], plus, total: plus }; while (nval > 0) { - sum += this.randInt(1, dval); + results.dice.push(multiplier * this.randInt(1, dval)); nval--; } - return sum; + results.total = sum(results.dice) + plus; + return results; } throw new Error('Invalid arguments given to dice'); } + dice(n, d, plus) { + return this.diceExpanded(n, d, plus).total; + } + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ parseDiceString(string) { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceString(string); } clamp(number, lower, upper) { - if (upper !== undefined) { + if (typeof upper !== 'undefined') { number = number <= upper ? number : upper; } - if (lower !== undefined) { + if (typeof lower !== 'undefined') { number = number >= lower ? number : lower; } return number; } bin(val, bins, min, max) { + (0,src_number/* default */.Ay)({ val }).gt(min).lt(max); const spread = max - min; return (Math.round(((val - min) / spread) * (bins - 1)) / (bins - 1) * spread) + min; } } +/** + * @category Main Class + */ class Rng extends RngAbstract { #mask; #seed = 0; + #randFunc; #m_z = 0; constructor(seed) { super(seed); this.#mask = 0xffffffff; this.#m_z = 987654321; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ + static predictable(seed) { + return new this(seed ?? PREDICTABLE_SEED); + } serialize() { return { - mask: this.#mask, + mask: this.getMask(), seed: this.getSeed(), - m_z: this.#m_z, + m_z: this.getMz(), }; } sameAs(other) { - const s = other.serialize(); - return this.#seed === s.seed && - this.#mask === s.mask && - this.#m_z === s.m_z; + if (other instanceof Rng) { + return this.getRandomSource() === other.getRandomSource() && + this.getSeed() === other.getSeed() && + this.getMask() === other.getMask() && + this.getMz() === other.getMz(); + } + return false; + } + /** @hidden */ + getMask() { + return this.#mask; + } + /** @hidden */ + getMz() { + return this.#m_z; } + /** @hidden */ + setMask(mask) { + this.#mask = mask; + } + /** @hidden */ + setMz(mz) { + this.#m_z = mz; + } + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized) { const rng = new this(); rng.setSeed(serialized.seed); - rng.#mask = serialized.mask; - rng.#seed = serialized.seed; - rng.#m_z = serialized.m_z; + rng.setMask(serialized.mask); + rng.setMz(serialized.m_z); return rng; } seed(i) { @@ -784,7 +2170,7 @@ class Rng extends RngAbstract { this.#m_z = 987654321; return this; } - _random() { + _next() { this.#m_z = (36969 * (this.#m_z & 65535) + (this.#m_z >> 16)) & this.#mask; this.setSeed((18000 * (this.getSeed() & 65535) + (this.getSeed() >> 16)) & this.#mask); let result = ((this.#m_z << 16) + this.getSeed()) & this.#mask; @@ -792,6 +2178,7 @@ class Rng extends RngAbstract { return result + 0.5; } } +/* harmony default export */ const src_rng = (Rng); /***/ }), @@ -805,7 +2192,7 @@ class Rng extends RngAbstract { /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(334); /* harmony import */ var _table_pool__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(425); /* harmony import */ var _table_pool_entry_results__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(673); @@ -840,8 +2227,8 @@ class LootTable { this.pools = pools; this.fn = fn; this.ul = ul; - this.rng = rng ?? (ul ? ul.getRng() : new _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A()); - this.id = id ?? this.rng.uniqstr(6); + this.rng = rng ?? (ul ? ul.getRng() : new _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay()); + this.id = id ?? this.rng.randomString(6); } // Register a function for use in loot pools registerFunction(name, fn) { @@ -1010,9 +2397,9 @@ class LootTable { totalWeight += (entry.weight ?? 1); } } - const rollsMax = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMax(pool.rolls); - const rollsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(pool.rolls); - const nullsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(pool.nulls); + const rollsMax = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMax(pool.rolls); + const rollsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(pool.rolls); + const nullsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(pool.nulls); for (const entry of pool.getEntries()) { if (entry instanceof LootTable || entry.isTable()) { let table; @@ -1040,8 +2427,8 @@ class LootTable { entries.push({ entry, weight: entry.weight / totalWeight, - min: nullsMin > 0 ? 0 : (rollsMin * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(entry.qty)), - max: rollsMax * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMax(entry.qty), + min: nullsMin > 0 ? 0 : (rollsMin * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(entry.qty)), + max: rollsMax * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMax(entry.qty), }); } } @@ -1192,7 +2579,7 @@ class LootTable { /* harmony import */ var _pool_entry_result__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(668); /* harmony import */ var _pool_entry_results__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(784); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(673); @@ -1218,7 +2605,7 @@ class LootPool { this.functions = functions ?? []; this.rolls = rolls; this.nulls = nulls; - this.id = id ?? (new _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A()).uniqstr(6); + this.id = id ?? (new _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .Ay()).randomString(6); this.template = template; if (entries) { for (const entry of entries) { @@ -1455,7 +2842,7 @@ class LootPool { /* harmony export */ }); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(334); /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(784); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(673); /* harmony import */ var _entry_result__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(668); /* harmony import */ var _entry_results__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(219); @@ -1489,7 +2876,7 @@ class LootTableEntry { this.conditions = conditions ?? []; } getRng(rng) { - return rng ?? this.rng ?? (this.rng = new _rng__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A()); + return rng ?? this.rng ?? (this.rng = new _rng__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .Ay()); } setRng(rng) { this.rng = rng; @@ -1765,7 +3152,7 @@ class LootTableEntryResults extends Array { /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(784); /* harmony import */ var _table_pool__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* harmony import */ var _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(50); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(673); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(330); /* harmony import */ var _default_functions__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(663); /* harmony import */ var _default_conditions__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(494); @@ -1903,7 +3290,7 @@ class UltraLoot { 'chanceTo', 'randInt', 'uniqid', - 'uniqstr', + 'randomString', 'randBetween', 'normal', 'chancyInt', @@ -1925,7 +3312,7 @@ class UltraLoot { if (this.isRng(rng)) { return rng; } - const RngConstructor = this.rngConstructor ?? _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A; + const RngConstructor = this.rngConstructor ?? _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .Ay; return new RngConstructor(rng); } registerFunction(name, fn) { @@ -2299,7 +3686,7 @@ class UltraLoot { pools: [] }; clone.pools = []; - const keyToUse = table.filename ?? this.getRng().uniqstr(6); + const keyToUse = table.filename ?? this.getRng().randomString(6); had.add(table); if (includeRng) { clone.rng = table.rng?.serialize() ?? null; @@ -2331,7 +3718,7 @@ class UltraLoot { entryClone.functions = entry.functions; } if (entryClone.item instanceof _table__WEBPACK_IMPORTED_MODULE_1__/* ["default"] */ .A) { - const subKeyToUse = entryClone.item.filename ?? this.getRng().uniqstr(6); + const subKeyToUse = entryClone.item.filename ?? this.getRng().randomString(6); if (had.has(entryClone.item)) { throw new RecursiveTableError('Recursive requirement detected - cannot serialize recursively required tables.'); } @@ -2726,7 +4113,7 @@ module.exports = require("fs"); /***/ 330: /***/ ((module) => { -module.exports = {"rE":"0.1.1"}; +module.exports = {"rE":"0.3.0"}; /***/ }) @@ -2793,19 +4180,74 @@ __webpack_require__.d(__webpack_exports__, { "default": () => (/* binding */ src) }); -// UNUSED EXPORTS: LootTable, LootTableEntry, LootTableEntryResult, LootTableEntryResults, LootTableManager, LootTablePool, PredictableRng, RecursiveTableError, Rng, RngAbstract, UltraLoot +// UNUSED EXPORTS: ArrayNumberValidator, LootTable, LootTableEntry, LootTableEntryResult, LootTableEntryResults, LootTableManager, LootTablePool, MaxRecursionsError, NonRandomRandomError, NumberValidationError, NumberValidator, PredictableRng, RecursiveTableError, Rng, RngAbstract, UltraLoot // EXTERNAL MODULE: ./src/ultraloot.ts var ultraloot = __webpack_require__(224); -// EXTERNAL MODULE: ./src/rng.ts -var rng = __webpack_require__(629); +// EXTERNAL MODULE: ./src/rng.ts + 2 modules +var rng = __webpack_require__(673); ;// ./src/rng/predictable.ts /** + * * An Rng type that can be used to give predictable results * for testing purposes, and giving known results. + * + * You can set an array of results that will be returned from called to _next() + * + * Note: To avoid unexpected results when using this in place of regular Rng, it is + * only allowed to make the results spread from [0, 1) + * + * The numbers are returned and cycled, so once you reach the end of the list, it will + * just keep on going. + * + * @category Other Rngs + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0]; + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0, 0.5]; + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.0 + * + * @example + * // The setEvenSpread and evenSpread methods can be used to generate + * // n numbers between [0, 1) with even gaps between + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.setEvenSpread(11); + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.5 + * prng.random(); // 0.6 + * prng.random(); // 0.7 + * prng.random(); // 0.8 + * prng.random(); // 0.9 + * prng.random(); // 0.9999999... + * prng.random(); // 0.0 */ -class Rng extends rng/* RngAbstract */.U { +class PredictableRng extends rng/* RngAbstract */.Up { counter = 0; _results = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; constructor(seed, results) { @@ -2845,14 +4287,18 @@ class Rng extends rng/* RngAbstract */.U { return this; } sameAs(other) { - return this.results.sort().join(',') === other.results.sort().join(',') && - this.counter === other.counter; + if (other instanceof PredictableRng) { + return this.results.join(',') === other.results.join(',') && + this.counter === other.counter && + this.getRandomSource() === other.getRandomSource(); + } + return false; } reset() { this.counter = 0; return this; } - _random() { + _next() { return this.results[this.counter++ % this.results.length]; } } @@ -2930,6 +4376,8 @@ var results = __webpack_require__(219); + + // This provides an easy way of using ultraloot in browser. // It can be instantiated by new UltraLoot() and submodules can be diff --git a/dist/ultraloot.min.js b/dist/ultraloot.min.js index 17fafc6..2b2d77a 100644 --- a/dist/ultraloot.min.js +++ b/dist/ultraloot.min.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.UltraLoot=e():t.UltraLoot=e()}(this,(()=>(()=>{"use strict";var __webpack_modules__={494:(t,e,n)=>{n.r(e),n.d(e,{dependContext:()=>r,dependLooter:()=>s});var o=n(185);const r=({context:t,args:e})=>!e||(0,o.fE)(t,e),s=({looter:t,args:e})=>!e||(0,o.fE)(t,e)},663:(t,e,n)=>{n.r(e),n.d(e,{inheritContext:()=>s,inheritLooter:()=>r,setToRandomChoice:()=>i});var o=n(185);const r=({looted:t,looter:e,args:n})=>{(n=n??{}).lootedProperty=n.lootedProperty??n.property,n.looterProperty=n.looterProperty??n.property,n.looterProperty&&n.lootedProperty&&(0,o._c)(t,n.lootedProperty,(0,o.m0)(e,n.looterProperty,n.default))},s=({looted:t,context:e,args:n})=>{(n=n??{}).lootedProperty=n.lootedProperty??n.property,n.contextProperty=n.contextProperty??n.property,n.contextProperty&&n.lootedProperty&&(0,o._c)(t,n.lootedProperty,(0,o.m0)(e,n.contextProperty,n.default))},i=({rng:t,looted:e,args:n})=>{n=n??{};const{property:r,choices:s}=n;r&&e&&s&&(0,o._c)(e,r,t.weightedChoice(s))}},334:(t,e,n)=>{n.d(e,{A:()=>i});let o=!1;o=!1;const r=(...t)=>{};let s={debug:r,v:r,vv:r,vi:r,ve:r,vg:r,vge:r,vgc:r,vt:r,d:r,g:r,ge:r,gc:r,t:r,te:r,time:r,timeEnd:r,group:r,groupEnd:r,groupCollapsed:r,log:r,error:r,table:r,info:r};o&&(s={...s,debug:function(t){o&&t()},d:console.log,g:console.group,ge:console.groupEnd,gc:console.groupCollapsed,group:console.group,groupEnd:console.groupEnd,groupCollapsed:console.groupCollapsed,log:console.log,error:console.error,table:console.table,info:console.info},s={...s,v:console.log,vi:console.info,ve:console.error,vg:console.group,vge:console.groupEnd,vgc:console.groupCollapsed,vt:console.table,t:console.time,te:console.timeEnd,time:console.time,timeEnd:console.timeEnd},s.vv=console.log);const i=s},629:(t,e,n)=>{n.d(e,{A:()=>a,U:()=>l});const o=/^ *([0-9]+) *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/,r=/^ *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/,s={},i={};class l{#t=0;constructor(t){this.setSeed(t)}getSeed(){return this.#t}sameAs(t){return this.#t===t.#t}setSeed(t){return null==t?this.setSeed(Math.ceil(1e8*Math.random())):("string"==typeof t&&(t=this.convertStringToNumber(t)),this.#t=t,this)}seed(t){return this.setSeed(t),this}serialize(){return{seed:this.#t}}static unserialize(t){const{constructor:e}=Object.getPrototypeOf(this),n=new e(t.seed);return n.setSeed(t.seed),n}predictable(t){const{constructor:e}=Object.getPrototypeOf(this);return new e(t)}static predictable(t){return new this(t)}hashStr(t){let e,n,o=0;if(0===t.length)return o;for(e=0;er)throw new Error(`Number ${t} is greater than max of ${r}`);if(t1||t<0)throw new Error(`Number must be < 1 and > 0, got ${t}`);return t*(n-e)+e}shouldThrowOnMaxRecursionsReached(){return true}normal({mean:t,stddev:e=1,max:n,min:o,skew:r=0}={},s=0){if(s>100&&this.shouldThrowOnMaxRecursionsReached())throw new Error("Max recursive calls to rng normal function. This might be as a result of using predictable random numbers?");let i=this.boxMuller();if(i=i/10+.5,s>100)i=Math.min(Math.max(i,0),1);else if(i>1||i<0)return this.normal({mean:t,stddev:e,max:n,min:o,skew:r},s+1);return i=r<0?1-Math.pow(i,Math.pow(2,r)):Math.pow(i,Math.pow(2,-r)),void 0===t?(t=0,void 0!==n&&void 0!==o?(i*=n-o,i+=o):(i*=10,i-=5)):(i*=10,i-=5,i=i*e+t),s<=100&&(void 0!==n&&i>n||void 0!==o&&i{n+=t}))}else{const o=Object.keys(t);if(0===o.length)return null;if(1===o.length)return o[0];for(e in t){if(t[e]<0)throw new Error("Probability cannot be negative");n+=t[e]}}const o=this._random()*n;let r=0;if(t instanceof Map){for(const[e,n]of t)if(r+=n,o0;)s+=this.randInt(1,r),o--;return s}throw new Error("Invalid arguments given to dice")}parseDiceString(t){const{constructor:e}=Object.getPrototypeOf(this);return e.parseDiceString(t)}clamp(t,e,n){return void 0!==n&&(t=t<=n?t:n),void 0!==e&&(t=t>=e?t:e),t}bin(t,e,n,o){const r=o-n;return Math.round((t-n)/r*(e-1))/(e-1)*r+n}}class a extends l{#e;#t=0;#n=0;constructor(t){super(t),this.#e=4294967295,this.#n=987654321}serialize(){return{mask:this.#e,seed:this.getSeed(),m_z:this.#n}}sameAs(t){const e=t.serialize();return this.#t===e.seed&&this.#e===e.mask&&this.#n===e.m_z}static unserialize(t){const e=new this;return e.setSeed(t.seed),e.#e=t.mask,e.#t=t.seed,e.#n=t.m_z,e}seed(t){return super.seed(t),this.#n=987654321,this}_random(){this.#n=36969*(65535&this.#n)+(this.#n>>16)&this.#e,this.setSeed(18e3*(65535&this.getSeed())+(this.getSeed()>>16)&this.#e);let t=(this.#n<<16)+this.getSeed()&this.#e;return t/=4294967296,t+.5}}},784:(t,e,n)=>{n.d(e,{A:()=>l});var o=n(334),r=n(425),s=n(219),i=n(629);class l{name;id;fn;ul;rng;pools=[];functions={};conditions={};borrowed=new Set;constructor({name:t,rng:e,id:n,pools:o=[],fn:r,ul:s}={}){this.name=t,this.pools=o,this.fn=r,this.ul=s,this.rng=e??(s?s.getRng():new i.A),this.id=n??this.rng.uniqstr(6)}registerFunction(t,e){this.functions[t]=e}registerCondition(t,e){this.conditions[t]=e}get filename(){return this.fn??this.id??this.name}set filename(t){this.fn=t}get ultraloot(){return this.ul}set ultraloot(t){this.ul=t}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}borrow(t){return this.borrowed.add(t),this}unborrow(t){return this.borrowed.delete(t),this}getPools(){return this.pools}setRng(t){return this.rng=t,this}rollBasics({rng:t,looter:e,context:n,n:r=1}){const s=t??this.rng,i=s.chancy(r);return o.A.gc(`Table: ${this.description} | Rolling table ${i} times (from chancy(${JSON.stringify(r)}))`,{looter:e,context:n}),[s,i]}rollSync({looter:t,context:e,result:n=new s.A,rng:r,n:i=1}={}){const[l,a]=this.rollBasics({rng:r,n:i,looter:t,context:e});for(const o of this.pools)this.rollPoolSync({n:a,pool:o,rng:l,looter:t,context:e,result:n});return o.A.ge(),n}async roll({looter:t,context:e,result:n=new s.A,rng:r,n:i=1}={}){const[l,a]=this.rollBasics({rng:r,n:i,looter:t,context:e});for(const o of this.pools)await this.rollPool({n:a,pool:o,rng:l,looter:t,context:e,result:n});return o.A.ge(),n}rollPoolSync({pool:t,looter:e,context:n,result:o=new s.A,rng:r,n:i=1}){const l=r??this.rng,a=l.chancy(i);for(let r=0;re||n.hasFunction(t)),!1)}hasCondition(t){return void 0!==this.conditions[t.function]||Array.from(this.borrowed).reduce(((e,n)=>e||n.hasCondition(t)),!1)}createPool(t){const e=new r.A(t);return this.pools.push(e),e}addPool(t){return t instanceof r.A?this.pools.push(t):this.createPool(t),this}getPotentialDrops(){const t=[];for(const e of this.pools){let n=0;for(const t of e.getEntries())n+=t instanceof l?1:t.weight??1;const o=i.A.chancyMax(e.rolls),r=i.A.chancyMin(e.rolls),s=i.A.chancyMin(e.nulls);for(const a of e.getEntries())if(a instanceof l||a.isTable()){let e,n=1;a instanceof l?(n=1,e=a):a.isTable()&&(n=a.weight??1,e=a.getItem());const i=e.getPotentialDrops();for(const e of i)t.push({entry:e.entry,weight:e.weight/n,min:s>0?0:r*e.min,max:o*e.max})}else t.push({entry:a,weight:a.weight/n,min:s>0?0:r*i.A.chancyMin(a.qty),max:o*i.A.chancyMax(a.qty)})}return t}async applyFunction(t,{rng:e,looted:n,looter:o,context:r,result:s}){if(void 0!==this.functions[t.function])return await this.functions[t.function]({rng:e,looted:n,looter:o,context:r,result:s,args:t.args});{for(const i of Array.from(this.borrowed))if(i.hasFunction(t))return await i.applyFunction(t,{rng:e,looted:n,looter:o,context:r,result:s});const i=`Function ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerFunction(name, function).`;if(this.ultraloot){if(this.ultraloot.hasFunction(t.function))return await this.ultraloot.applyFunction(t,{rng:e,looted:n,looter:o,context:r,result:s});if(this.ultraloot.throwOnMissingFunctions)throw new Error(i);console.error(i)}else console.error(i)}}async applyCondition(t,{rng:e,looter:n,context:o,result:r}){if(void 0===this.conditions[t.function]){for(const s of Array.from(this.borrowed))if(s.hasCondition(t))return await s.applyCondition(t,{rng:e,looter:n,context:o,result:r});const s=`Condition ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerCondition(name, condition_function).`;if(this.ultraloot){if(this.ultraloot.hasCondition(t.function))return await this.ultraloot.applyCondition(t,{rng:e,looter:n,context:o,result:r});if(this.ultraloot.throwOnMissingConditions)throw new Error(s);return console.error(`CR: ${s}`),!0}return console.error(`CR: ${s}`),!0}return await this.conditions[t.function]({rng:e,looter:n,context:o,result:r,args:t.args})}applyFunctionSync(t,{rng:e,looted:n,looter:o,context:r,result:s}){if(void 0!==this.functions[t.function])return this.functions[t.function]({rng:e,looted:n,looter:o,context:r,result:s,args:t.args});{for(const i of Array.from(this.borrowed))if(i.hasFunction(t))return i.applyFunctionSync(t,{rng:e,looted:n,looter:o,context:r,result:s});const i=`Function ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerFunction(name, function).`;if(this.ultraloot){if(this.ultraloot.hasFunction(t.function))return this.ultraloot.applyFunctionSync(t,{rng:e,looted:n,looter:o,context:r,result:s});if(this.ultraloot.throwOnMissingFunctions)throw new Error(i);console.error(i)}else console.error(i)}}applyConditionSync(t,{rng:e,looter:n,context:o,result:r}){if(void 0===this.conditions[t.function]){for(const s of Array.from(this.borrowed))if(s.hasCondition(t))return s.applyConditionSync(t,{rng:e,looter:n,context:o,result:r});const s=`Condition ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerCondition(name, condition_function).`;if(this.ultraloot){if(this.ultraloot.hasCondition(t.function))return this.ultraloot.applyConditionSync(t,{rng:e,looter:n,context:o,result:r});if(this.ultraloot.throwOnMissingConditions)throw new Error(s);return console.error(s),!0}return console.error(s),!0}const s=this.conditions[t.function]({rng:e,looter:n,context:o,result:r,args:t.args});if(s instanceof Promise)throw new Error("Cannot return promise from sync condition call");return s}}},425:(t,e,n)=>{n.d(e,{A:()=>c});var o=n(334),r=n(50),s=n(668),i=n(219),l=n(784),a=n(629);class c{name;id;conditions=[];functions=[];rolls=1;nulls=0;entries=[];template={};static NULLKEY="__NULL__fd2a99d2-26c0-4454-a284-34578b94e0f6";constructor({name:t,id:e,conditions:n=[],functions:o=[],rolls:r=1,nulls:s=0,entries:i=[],template:l={}}={}){if(this.name=t,this.conditions=n??[],this.functions=o??[],this.rolls=r,this.nulls=s,this.id=e??(new a.A).uniqstr(6),this.template=l,i)for(const t of i)this.addEntry(t)}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}createEntry(t){const e=new r.A({...this.template??{},...t});return this.entries.push(e),e}addEntry(t,e){return t instanceof l.A&&(t=new r.A({...this.template??{},...e??{},id:t.id,item:t})),t instanceof r.A?this.entries.push(t):this.createEntry(t),this}getEntries(){return this.entries}async roll({rng:t,table:e,looter:n,context:s,result:a=new i.A}){const u=t.chancyInt(this.rolls);o.A.gc(`Pool ${this.description} | Rolling pool ${u} times (from chancy(${JSON.stringify(this.rolls)}))`);const h={};t.chancy(this.nulls)>0&&(h[c.NULLKEY]=t.chancy(this.nulls));for(const r in this.entries){const i=this.entries[r];if(i instanceof l.A)h[r]=1;else{const l=await i.applyConditions({rng:t,table:e,looter:n,context:s,result:a});o.A.vv(`Pool ${this.description} | Result of calling await a.applyConditions was ${JSON.stringify(l)}`),l&&(h[r]=t.chancy(i.weight??1))}}o.A.vv(`Pool ${this.description} | Choices:`,h);const _=new i.A;let f=!0;for(const r of this.conditions){const i=await e.applyCondition(r,{rng:t,looter:n,context:s,result:a});if(o.A.v(`Pool ${this.description} | Testing function "${r.function}" resulted in ${JSON.stringify(i)}`),f=f&&i,!f){o.A.v(`Pool ${this.description} | Function "${r.function}" stopped this from being added`);break}}if(o.A.v(`Pool ${this.description} | After applying conditions, add was ${JSON.stringify(f)}`),f)for(let i=0;i0&&(h[c.NULLKEY]=t.chancy(this.nulls)),this.entries.forEach(((o,r)=>{o instanceof l.A?h[r]=1:o.applyConditionsSync({rng:t,table:e,looter:n,context:s,result:a})&&(h[r]=t.chancy(o.weight??1))})),o.A.vv(`Pool ${this.description} | Choices:`,h);const _=new i.A;let f=!0;for(const r of this.conditions){const i=e.applyConditionSync(r,{rng:t,looter:n,context:s,result:a});if(o.A.v(`Pool ${this.description} | Testing function "${r.function}" resulted in ${JSON.stringify(i)}`),f=f&&i,!f){o.A.v(`Pool ${this.description} | Function "${r.function}" stopped this from being added`);break}}if(o.A.v(`Pool ${this.description} | After applying conditions, add was ${JSON.stringify(f)}`),f)for(let i=0;i0)if(t.stackable)l.push(t);else for(let e=0;e0)if(t.stackable)l.push(t);else for(let e=0;e{n.d(e,{A:()=>a});var o=n(334),r=n(784),s=n(629),i=n(668),l=n(219);class a{id;stackable=!0;unique=!1;name;weight=1;item;qty=1;functions;conditions;rng;constructor({id:t,stackable:e=!0,unique:n=!1,name:o,weight:r=1,item:s,functions:i=[],conditions:l=[],qty:a=1}={}){this.id=t,this.name=o,this.stackable=e,this.unique=n,this.weight=r,this.item=s,this.qty=a,this.functions=i??[],this.conditions=l??[]}getRng(t){return t??this.rng??(this.rng=new s.A)}setRng(t){this.rng=t}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}getItem(){return this.item??this.id}deepCloneObject(t){return JSON.parse(JSON.stringify(t))}cloneItem(){return null===this.item?null:"object"==typeof this.item?"function"==typeof this.item.clone?this.item.clone(this.item):this.deepCloneObject(this.item):this.item}isTable(){return this.getItem()instanceof r.A}resultDefinition(t){return{id:this.id,stackable:this.stackable,name:this.name,item:this.cloneItem(),qty:t.chancy(this.qty)}}generateBaseResults(t){const e=this.resultDefinition(t);return new l.A([new i.A(e)])}async applyConditions({rng:t,table:e,looter:n,context:r,result:s=new l.A}){let i=!0;for(const l of this.conditions)if(i=i&&await e.applyCondition(l,{rng:this.getRng(t),looter:n,context:r,result:s}),!i){o.A.d(`Entry: ${this.description} | Condition "${l.function}" stopped this from being added`);break}return i}async roll({rng:t,table:e,looter:n,context:o,result:r=new l.A}){return this.isTable()?await this.rollTable({rng:this.getRng(t),table:e,looter:n,context:o,result:r}):await this.rollItem({rng:this.getRng(t),table:e,looter:n,context:o,result:r})}async rollItem({rng:t,table:e,looter:n,context:r,result:s=new l.A}){return o.A.d(`Entry: ${this.description} | Rolling Item for ${this.id}`,{looter:n,context:r}),await this.processEntryResults(this.generateBaseResults(this.getRng(t)),{rng:this.getRng(t),table:e,looter:n,context:r,result:s}),s}async rollTable({rng:t,table:e,looter:n,context:o,result:r=new l.A}){const s=await this.getItem().borrow(e).roll({looter:n,context:o,result:[],rng:t,n:this.qty});return this.getItem().unborrow(e),await this.processEntryResults(s,{rng:this.getRng(t),table:e,looter:n,context:o,result:r}),r}async processEntryResults(t,{rng:e,table:n,looter:o,context:r,result:s=new l.A}){for(const i of t)await this.processEntryResult(i,{rng:this.getRng(e),table:n,looter:o,context:r,result:s});return t}async processEntryResult(t,{rng:e,table:n,looter:o,context:r,result:s=new l.A}){for(const i of this.functions)await n.applyFunction(i,{rng:this.getRng(e),looted:t,looter:o,context:r,result:s});if(t.qty>0)if(t.stackable)s.push(t);else for(let e=0;e0)if(t.stackable||1===t.qty)s.push(t);else for(let e=0;e{n.d(e,{A:()=>o});class o{id;stackable=!0;name;item;qty=1;constructor({id:t,stackable:e=!0,name:n,item:o,qty:r=1}={}){this.id=t,this.name=n,this.item=o,this.qty=r,this.stackable=e}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}getQty(){return this.qty}setQty(t){this.qty=t}addQty(t){this.qty=this.qty+t}}},219:(t,e,n)=>{n.d(e,{A:()=>o});class o extends Array{constructor(t){t instanceof Array?super(...t):t?super(t):super(),Object.setPrototypeOf(this,Object.create(o.prototype))}merge(t){for(const e of t)this.push(e);return this}merged(t){return new o([...this,...t])}entrySignature(t){const e={};for(const[n,o]of Object.entries(t))"qty"!==n&&(e[n]=o);return JSON.stringify(e)}collapsed(){const t={},e=[];for(const n of this)if(n.stackable){const e=this.entrySignature(n);void 0===t[e]?t[e]=n:t[e].addQty(n.qty)}else e.push(n);return new o([...e,...Object.values(t)])}}},224:(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{tZ:()=>UltraLoot});var _log__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(334),_table__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(784),_table_pool__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(425),_table_pool_entry__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__(50),_rng__WEBPACK_IMPORTED_MODULE_4__=__webpack_require__(629),_package_json__WEBPACK_IMPORTED_MODULE_5__=__webpack_require__(330),_default_functions__WEBPACK_IMPORTED_MODULE_6__=__webpack_require__(663),_default_conditions__WEBPACK_IMPORTED_MODULE_7__=__webpack_require__(494);let fs,isNode=!1;"object"==typeof process&&"object"==typeof process.versions&&void 0!==process.versions.node&&(fs=__webpack_require__(896),isNode=!0);const VERSION_KEY="__version__";class RecursiveTableError extends Error{}class UltraLoot{static version=_package_json__WEBPACK_IMPORTED_MODULE_5__.rE;version=_package_json__WEBPACK_IMPORTED_MODULE_5__.rE;defaultRng;rng;rngConstructor;functions={};conditions={};throwOnMissingFunctions=!0;throwOnMissingConditions=!0;constructor(t){_log__WEBPACK_IMPORTED_MODULE_0__.A.d("UltraLoot initialising"),t&&(this.rng=this.makeRng(t))}registerDefaults(){return this.registerDefaultFunctions(),this.registerDefaultConditions(),this}registerDefaultFunctions(){for(const[t,e]of Object.entries(_default_functions__WEBPACK_IMPORTED_MODULE_6__))this.registerFunction(t,e);return this}registerDefaultConditions(){for(const[t,e]of Object.entries(_default_conditions__WEBPACK_IMPORTED_MODULE_7__))this.registerCondition(t,e);return this}instance(t){return new UltraLoot(t)}setRng(t){if(!this.isRng(t))throw new Error("rng given does not confirm to RngInterface");this.rng=t}getRng(){return this.rng??this.getDefaultRng()}getDefaultRng(){return this.defaultRng??(this.defaultRng=this.makeRng())}setRngConstructor(t){this.rngConstructor=t}getRngConstructor(){return this.rngConstructor??Object.getPrototypeOf(this.rng).constructor}isRng(t){if(void 0===t)return!1;if("object"!=typeof t)return!1;const e=["predictable","hashStr","convertStringToNumber","getSeed","seed","percentage","random","chance","chanceTo","randInt","uniqid","uniqstr","randBetween","normal","chancyInt","chancy","weightedChoice","dice","parseDiceString","clamp","bin","serialize"];let n=!0;for(const o of e)n=n&&"function"==typeof t[o];return n}makeRng(t){if(this.isRng(t))return t;return new(this.rngConstructor??_rng__WEBPACK_IMPORTED_MODULE_4__.A)(t)}registerFunction(t,e){this.functions[t]=e}registerCondition(t,e){this.conditions[t]=e}hasFunction(t){return void 0!==this.functions[t]}hasCondition(t){return void 0!==this.conditions[t]}noThrowOnMissingFunctionsOrConditions(){return this.throwOnMissingFunctions=!1,this.throwOnMissingConditions=!1,this}throwOnMissingFunctionsOrConditions(){return this.throwOnMissingFunctions=!0,this.throwOnMissingConditions=!0,this}functionCheck(t){if(_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`UL | Applying function ${t.function}`),void 0===this.functions[t.function]){const e=`Function ${t.function} has not been defined. Did you forget to register the function with this loot table? UltraLoot.registerFunction(name, function).`;if(this.throwOnMissingFunctions)throw new Error(e);return console.error(e),!1}return!0}conditionCheck(t){if(_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`UL | Applying condition ${t.function}`),void 0===this.conditions[t.function]){const e=`Condition ${t.function} has not been defined. Did you forget to register the function with this loot table? UltraLoot.registerCondition(name, condition_function).`;if(this.throwOnMissingConditions)throw new Error(e);return console.error(e),!1}return!0}applyFunctionSync(t,{rng:e,looted:n,looter:o,context:r,result:s}){if(this.functionCheck(t))return this.functions[t.function]({rng:e,looted:n,looter:o,context:r,result:s,args:Object.assign({},t.args??{},t.arguments??{})})}applyConditionSync(t,{rng:e,looter:n,context:o,result:r}){if(this.conditionCheck(t)){const s=this.conditions[t.function]({rng:e,looter:n,context:o,result:r,args:Object.assign({},t.args??{},t.arguments??{})});if(s instanceof Promise)throw new Error("Cannot return promise from sync condition call");return s}return!0}async applyFunction(t,{rng:e,looted:n,looter:o,context:r,result:s}){if(this.functionCheck(t))return await this.functions[t.function]({rng:e,looted:n,looter:o,context:r,result:s,args:Object.assign({},t.args??{},t.arguments??{})})}async applyCondition(t,{rng:e,looter:n,context:o,result:r}){return!this.conditionCheck(t)||await this.conditions[t.function]({rng:e,looter:n,context:o,result:r,args:Object.assign({},t.args??{},t.arguments??{})})}createTable(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||this.isLootTableDefinition(t)){t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A?_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating table from LootTable"):_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating table from LootTableDefinition"),t.ul=this,t.rng?t.rng=t.rng??this.makeRng(t.rng):t.rng=this.getRng();const e=new _table__WEBPACK_IMPORTED_MODULE_1__.A(t);return e.ultraloot=this,e}if(this.isEasyLootTableDefinition(t)){_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating table from LootTableEasyDefinition"),t.rng?t.rng=t.rng??this.makeRng(t.rng):t.rng=this.getRng();const e=new _table__WEBPACK_IMPORTED_MODULE_1__.A(this.transformEasyToProperLootTableDefinition(t));return e.ultraloot=this,e}throw new Error("Cannot create loot table from these params")}createPool(t){return this.isEasyLootTablePoolDefinition(t)?(_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating pool from LootTablePoolEasyDefinition"),new _table_pool__WEBPACK_IMPORTED_MODULE_2__.A(this.transformEasyToProperLootTablePoolDefinition(t))):(_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating pool from LootTablePoolDefinition"),new _table_pool__WEBPACK_IMPORTED_MODULE_2__.A(t))}createEntry(t){return t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A?new _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A({id:t.id,name:t.name,item:t,qty:1}):new _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A(t)}isLootTableDefinition(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||t instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A||t instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;if(t.pools)for(const e of t.pools)if(!(e instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A))return!1;return"object"==typeof t}isEasyLootTableDefinition(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||t instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A||t instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;if(t.pools)for(const e of t.pools)if(e instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A)return!1;return"object"==typeof t}isEasyLootTablePoolDefinition(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||t instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A||t instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;if(t.entries)for(const e of t.entries)if(e instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;return"object"==typeof t}transformEasyToProperLootTableDefinition(t){const e={rng:this.makeRng(t.rng??this.getRng()),name:t.name,id:t.id,pools:([],[])};if(t.pools)for(const n of t.pools)e.pools.push(this.createPool(n));return e.ul=this,e}transformEasyToProperLootTablePoolDefinition(t){const e=[];for(let n of t.entries??[])this.isEasyLootTableDefinition(n)&&void 0!==n.pools&&Array.isArray(n.pools)&&(n=this.createTable(n)),e.push(n);return{name:t.name,id:t.id,rolls:t.rolls,nulls:t.nulls,template:t.template,conditions:t.conditions,functions:t.functions,entries:e}}pathJoin(t,e="/"){return t.join(e).replace(new RegExp(e+"{1,}","g"),e)}finishWith(t,e){return t.endsWith(e)?t:t+e}finishWithExtension(t,e){if(t.endsWith(e))return t;if(0===t.length)return e;const n=t.split("/").pop().split("\\").pop(),o=n.includes(".")?n.lastIndexOf("."):n.length;return`${t.substring(0,t.length-n.length+o)}.${e.replace(".","")}`}getExtension(t){if(0===t.length)return null;const e=t.split("/").pop().split("\\").pop();if(!e.includes("."))return null;const n=e.lastIndexOf(".");return e.substring(n,e.length)}serialize(t,{includeRng:e=!1,key:n,had:o=new Set}={}){const r={},s={name:t.name,id:t.id,fn:t.fn,pools:([],[])},i=t.filename??this.getRng().uniqstr(6);o.add(t),e&&(s.rng=t.rng?.serialize()??null);for(const n of t.pools??[]){const t={name:n.name,id:n.id,rolls:n.rolls,nulls:n.nulls,conditions:n.conditions,functions:n.functions,entries:[]};for(const s of n.entries??[]){const n={name:s.name,id:s.id};if(s instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A?n.item=s:(n.stackable=s.stackable,n.weight=s.weight,n.item=s.item,n.qty=s.qty,n.conditions=s.conditions,n.functions=s.functions),n.item instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A){const t=n.item.filename??this.getRng().uniqstr(6);if(o.has(n.item))throw new RecursiveTableError("Recursive requirement detected - cannot serialize recursively required tables.");if(void 0===r[t]){n.item.filename=t;const s=this.serialize(n.item,{includeRng:e,key:t,had:o});r[t]=s.tables[t]}n.type="table",n.item=t}t.entries.push(n)}s.pools.push(t)}r[i]=s;return{[VERSION_KEY]:_package_json__WEBPACK_IMPORTED_MODULE_5__.rE,tables:r}}toJson(t,{includeRng:e=!1}={}){return JSON.stringify(this.serialize(t,{includeRng:e}))}async saveTable(t,{path:e="",defaultExtension:n}={}){throw new Error("Not yet implemented.")}async loadTables(t,{path:e="",defaultExtension:n}={}){n=n??this.getExtension(e)??".json";const o=this.finishWith(this.pathJoin([e,t]),n);return isNode?o.startsWith("http")||o.startsWith("file://")?this.loadTablesFromUrl(o,{path:e}):this.loadTablesFromFile(o,{path:e}):this.loadTablesFromUrl(o,{path:e})}async loadTablesFromFile(filename,{path="",defaultExtension}={}){let contents;defaultExtension=defaultExtension??this.getExtension(path)??".json",_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`Reading tables from ${filename}`);const ext=this.getExtension(filename);if(".js"===ext){const cb=await fs.promises.readFile(`${filename}`,"utf8");contents=eval(cb)}else contents=await fs.promises.readFile(filename,"utf8").then((t=>JSON.parse(t))).catch((t=>{if(t instanceof SyntaxError)throw t.message=`There was an error loading file: "${filename}". ${t.message}`,t;throw t}));return this.unserialize(contents)}async loadTablesFromUrl(t,{path:e="",defaultExtension:n}={}){return n=n??this.getExtension(t)??".json",_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`Reading tables from ${t}`),fetch(t).then((t=>t.text())).then((e=>{try{return JSON.parse(e)}catch(e){if(e instanceof SyntaxError)throw e.message=`There was an error loading file: "${t}". ${e.message}`,e;throw e}})).then((t=>this.unserialize(t)))}async loadTable(t,{path:e="",defaultExtension:n}={}){const o=n??this.getExtension(t)??".json",r=this.finishWithExtension(this.pathJoin([e,t]),o);return _log__WEBPACK_IMPORTED_MODULE_0__.A.d("Load Table",{filenameWithPath:this.pathJoin([e,t]),filename:t,defaultExtension:n,ext:o,path:e,fullPath:r}),isNode?r.startsWith("http")||r.startsWith("file://")?this.loadTableFromUrl(r,{path:e,defaultExtension:n}):this.loadTableFromFile(t,{path:e,defaultExtension:n}):this.loadTableFromUrl(r,{path:e,defaultExtension:n})}async loadTableFromFile(filename,{path="",defaultExtension}={}){defaultExtension=defaultExtension??this.getExtension(filename)??".json";const extension=this.getExtension(filename),pj=this.pathJoin([path,filename]);if(!extension){if(fs.existsSync(pj)&&fs.statSync(pj).isFile()){const t=await fs.promises.readFile(pj,"utf8").then((t=>JSON.parse(t))).catch((t=>{if(t instanceof SyntaxError)throw t.message=`There was an error loading file: "${filename}". ${t.message}`,t;throw t}));return this.resolveTable(t,{path,defaultExtension})}const t=new Set([defaultExtension,".json",".js",".cjs",".mjs"]);for(const e of t){const t=this.finishWithExtension(pj,e);if(fs.existsSync(t)&&fs.statSync(t).isFile())return this.loadTableFromFile(this.finishWithExtension(filename,e),{path,defaultExtension})}}if(!fs.existsSync(pj))throw new Error(`Could not find file "${filename}" in path "${path}"`);let contents;if(".js"===extension||".mjs"===extension||".cjs"===extension){const cb=await fs.promises.readFile(`${pj}`,"utf8");contents=eval(cb)}else".json"!==extension&&""!==defaultExtension||(contents=await fs.promises.readFile(pj,"utf8").then((t=>JSON.parse(t))).catch((t=>{if(t instanceof SyntaxError)throw t.message=`There was an error loading file: "${filename}". ${t.message}`,t;throw t})));return this.resolveTable(contents,{path,defaultExtension})}async loadTableFromUrl(t,{path:e="",defaultExtension:n}={}){return n=n??this.getExtension(t)??".json",fetch(t).then((t=>t.text())).then((e=>{try{return JSON.parse(e)}catch(e){if(e instanceof SyntaxError)throw e.message=`There was an error loading file: "${t}". ${e.message}`,e;throw e}})).then((t=>this.resolveTable(t,{path:e,defaultExtension:n})))}async resolveTable(t,{path:e="",defaultExtension:n}={}){for(const o of t.pools??[])for(const t of o.entries??[])"table"===t.type&&(t.item=await this.loadTable(t.item,{path:e,defaultExtension:n})),delete t.type;return this.createTable(t)}unserialize(t){const e={};let n=100;for(;Object.values(t.tables).length>0&&n-- >0;)t:for(const[n,o]of Object.entries(t.tables)){const r=o.rng??null;delete o.rng,_log__WEBPACK_IMPORTED_MODULE_0__.A.v(`Unserializing table ${n}`);for(const n of o.pools??[])for(const o of n.entries??[]){if("table"===o.type){if(void 0===e[o.item]){if(void 0===t.tables[o.item])throw new Error(`Table ${o.item} not present in serialized data`);_log__WEBPACK_IMPORTED_MODULE_0__.A.v(`We didn't have ${o.item} in our results`);continue t}o.item=e[o.item]}delete o.type}e[n]=this.createTable(o),r&&e[n].setRng(this.getRngConstructor().unserialize(r)),delete t.tables[n]}if(0===n)throw new Error("Maximum nested serialized table limit reached (could be a recursive requirement somewhere causing an issue?)");return e}}var __WEBPACK_DEFAULT_EXPORT__=UltraLoot},185:(t,e,n)=>{n.d(e,{_c:()=>r,fE:()=>s,m0:()=>o});const o=(t,e,n)=>{const o=e.split(".").reduce(((t,e)=>void 0!==t?t[e]:t),t);return void 0===o?n:o},r=(t,e,n)=>{const o=e.split(".");let r=t;for(let t=0;t{if(i=!!i,!t)return i;let a=t;if("string"==typeof e&&(a=o(t,e)),void 0!==n)return a=l?a===n:a==n,i?!a:!!a;if((void 0!==r||void 0!==s)&&l&&"number"!=typeof a)return!1;if(null!=a){if(void 0!==r&&parseFloat(a)s)return i;if(void 0!==r||void 0!==s)return!i}return i?!a:!!a}},896:t=>{t.exports=require("fs")},330:t=>{t.exports={rE:"0.1.1"}}},__webpack_module_cache__={};function __webpack_require__(t){var e=__webpack_module_cache__[t];if(void 0!==e)return e.exports;var n=__webpack_module_cache__[t]={exports:{}};return __webpack_modules__[t](n,n.exports,__webpack_require__),n.exports}__webpack_require__.d=(t,e)=>{for(var n in e)__webpack_require__.o(e,n)&&!__webpack_require__.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},__webpack_require__.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),__webpack_require__.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.d(__webpack_exports__,{default:()=>src});var ultraloot=__webpack_require__(224),rng=__webpack_require__(629);class Rng extends rng.U{counter=0;_results=[0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1-Number.EPSILON];constructor(t,e){super(t),e&&(this.results=e)}get results(){return this._results}set results(t){if(t.length<=0)throw new Error("Must provide some fake results.");for(const e of t){if(e<0)throw new Error(`Results must be greater than or equal to 0, got '${e}'`);if(e>=1)throw new Error(`Results must be less than 1, got '${e}'`)}this._results=t,this.reset()}evenSpread(t){const e=[];for(let n=0;n(()=>{"use strict";var __webpack_modules__={494:(t,e,n)=>{n.r(e),n.d(e,{dependContext:()=>o,dependLooter:()=>s});var r=n(185);const o=({context:t,args:e})=>!e||(0,r.fE)(t,e),s=({looter:t,args:e})=>!e||(0,r.fE)(t,e)},663:(t,e,n)=>{n.r(e),n.d(e,{inheritContext:()=>s,inheritLooter:()=>o,setToRandomChoice:()=>i});var r=n(185);const o=({looted:t,looter:e,args:n})=>{(n=n??{}).lootedProperty=n.lootedProperty??n.property,n.looterProperty=n.looterProperty??n.property,n.looterProperty&&n.lootedProperty&&(0,r._c)(t,n.lootedProperty,(0,r.m0)(e,n.looterProperty,n.default))},s=({looted:t,context:e,args:n})=>{(n=n??{}).lootedProperty=n.lootedProperty??n.property,n.contextProperty=n.contextProperty??n.property,n.contextProperty&&n.lootedProperty&&(0,r._c)(t,n.lootedProperty,(0,r.m0)(e,n.contextProperty,n.default))},i=({rng:t,looted:e,args:n})=>{n=n??{};const{property:o,choices:s}=n;o&&e&&s&&(0,r._c)(e,o,t.weightedChoice(s))}},334:(t,e,n)=>{n.d(e,{A:()=>i});let r=!1;r=!1;const o=(...t)=>{};let s={debug:o,v:o,vv:o,vi:o,ve:o,vg:o,vge:o,vgc:o,vt:o,d:o,g:o,ge:o,gc:o,t:o,te:o,time:o,timeEnd:o,group:o,groupEnd:o,groupCollapsed:o,log:o,error:o,table:o,info:o};r&&(s={...s,debug:function(t){r&&t()},d:console.log,g:console.group,ge:console.groupEnd,gc:console.groupCollapsed,group:console.group,groupEnd:console.groupEnd,groupCollapsed:console.groupCollapsed,log:console.log,error:console.error,table:console.table,info:console.info},s={...s,v:console.log,vi:console.info,ve:console.error,vg:console.group,vge:console.groupEnd,vgc:console.groupCollapsed,vt:console.table,t:console.time,te:console.timeEnd,time:console.time,timeEnd:console.timeEnd},s.vv=console.log);const i=s},623:(t,e,n)=>{n.d(e,{Ay:()=>l});const r=(t,e="Assertion failed")=>{if(!t)throw new o(e)};class o extends Error{}class s{#t=[];name="numbers";constructor(t,e="numbers"){this.numbers=t,this.name=e}get numbers(){return this.#t}set numbers(t){for(const e of t)r("number"==typeof e,`Non-number passed to validator ${e}`);this.#t=t}all(t){return this.numbers=t,this}validate(t){return Array.isArray(t)?this.all(t):new i(t)}varname(t){return this.name=t,this}sum(){return this.numbers.reduce(((t,e)=>t+e),0)}sumcloseto(t,e=1e-4,n){return r(Math.abs(this.sum()-t)t,e??`Expected sum of ${this.name} to be greater than ${t}, got ${this.sum()}`),this}sumtolteq(t,e){return r(this.sum()<=t,e??`Expected sum of ${this.name} to be less than or equal to ${t}, got ${this.sum()}`),this}sumtogteq(t,e){return r(this.sum()>=t,e??`Expected sum of ${this.name} to be greater than or equal to ${t}, got ${this.sum()}`),this}int(t){return this.numbers.forEach((e=>a(e).int(t??`Expected every component of ${this.name} to be an integer, got ${e}`))),this}positive(t){return this.numbers.forEach((e=>a(e).positive(t??`Expected every component of ${this.name} to be postiive, got ${e}`))),this}negative(t){return this.numbers.forEach((e=>a(e).negative(t??`Expected every component of ${this.name} to be negative, got ${e}`))),this}between(t,e,n){return this.numbers.forEach((r=>a(r).between(t,e,n??`Expected every component of ${this.name} to be between ${t} and ${e}, got ${r}`))),this}betweenEq(t,e,n){return this.numbers.forEach((r=>a(r).betweenEq(t,e,n??`Expected every component of ${this.name} to be between or equal to ${t} and ${e}, got ${r}`))),this}gt(t,e){return this.numbers.forEach((n=>a(n).gt(t,e??`Expected every component of ${this.name} to be > ${t}, got ${n}`))),this}gteq(t,e){return this.numbers.forEach((n=>a(n).gteq(t,e??`Expected every component of ${this.name} to be >= ${t}, got ${n}`))),this}lt(t,e){return this.numbers.forEach((n=>a(n).lt(t,e??`Expected every component of ${this.name} to be < ${t}, got ${n}`))),this}lteq(t,e){return this.numbers.forEach((n=>a(n).lteq(t,e??`Expected every component of ${this.name} to be <= ${t}, got ${n}`))),this}}class i{#e;name="number";constructor(t=0,e="number"){this.number=t,this.name=e}get number(){return this.#e}set number(t){r("number"==typeof t,`Non-number passed to validator ${t}`),this.#e=t}all(t,e){return new s(t,e??this.name)}assertNumber(t){return r(void 0!==this.number,"No number passed to validator."),!0}varname(t){return this.name=t,this}validate(t){return Array.isArray(t)?this.all(t):(this.number=t,this)}int(t){return this.assertNumber(this.number)&&r(Number.isInteger(this.number),t??`Expected ${this.name} to be an integer, got ${this.number}`),this}positive(t){return this.gt(0,t??`Expected ${this.name} to be positive, got ${this.number}`)}negative(t){return this.lt(0,t??`Expected ${this.name} to be negative, got ${this.number}`)}between(t,e,n){return this.assertNumber(this.number)&&r(this.number>t&&this.number=t&&this.number<=e,n??`Expected ${this.name} to be between or equal to ${t} and ${e}, got ${this.number}`),this}gt(t,e){return this.assertNumber(this.number)&&r(this.number>t,e??`Expected ${this.name} to be greater than ${t}, got ${this.number}`),this}gteq(t,e){return this.assertNumber(this.number)&&r(this.number>=t,e??`Expected ${this.name} to be greater than or equal to ${t}, got ${this.number}`),this}lt(t,e){return this.assertNumber(this.number)&&r(this.number{n.d(e,{Up:()=>y,Ay:()=>E});var r=n(623);class o extends Error{}class s extends Error{}class i{rng;#n=[];constructor(t=[],e){this.entries=t,this.rng=e||new E}copyArray(t){return Array.from(t)}setEntries(t){return this.entries=t,this}getEntries(){return this.#n}set entries(t){this.#n=this.copyArray(t)}get entries(){return this.#n}get length(){return this.#n.length}setRng(t){return this.rng=t,this}getRng(){return this.rng}add(t){this.#n.push(t)}empty(){return this.#n=[],this}isEmpty(){return this.length<=0}draw(){if(0===this.length)throw new o("No more elements left to draw from in pool.");if(1===this.length)return this.#n.splice(0,1)[0];const t=this.rng.randInt(0,this.#n.length-1);return this.#n.splice(t,1)[0]}drawMany(t){if(t<0)throw new Error("Cannot draw < 0 elements from pool");if(0===this.length&&t>0)throw new o("No more elements left to draw from in pool.");if(this.lengththis.size)return this.pop()}pop(){return this.elements.pop()}full(){return this.length>=this.size}empty(){this.elements=[]}get(t){return this.elements[t]}allSame(){return!(this.length>0)||this.elements.every((t=>t===this.elements[0]))}}class l extends Error{}class c extends a{minsequencelength=2;errormessage="Loop detected in input data. Randomness source not random?";constructor(t=1,e=2){if(super(t),this.size>1e4)throw new Error("Cannot detect loops for more than 10000 elements");this.minsequencelength=e}push(t){if(this.detectLoop(),this.elements.push(t),this.elements.length>this.size)return this.pop()}detectLoop(t){this.full()&&(this.allSame()&&this.loopDetected(t),this.hasRepeatingSequence(this.elements,this.minsequencelength)&&this.loopDetected(t))}loopDetected(t){throw new l(t??this.errormessage)}hasRepeatingSequence(t,e){for(let n=0;ne)return!0}return!1}}const h=1e7,u=500,d=5789938451,p=10,g=/^ *([+-]? *[0-9_]*) *[dD] *([0-9_]+) *([+-]? *[0-9_.]*) *$/,f={},m={};class _ extends Error{}function b(t){return"number"==typeof t||!isNaN(parseFloat(t))&&isFinite(t)}class y{#r=0;#o=0;#s=0;#i;#a;#l=["normal","gaussian","boxMuller","irwinHall","bates","batesgaussian","bernoulli","exponential","pareto","poisson","hypergeometric","rademacher","binomial","betaBinomial","beta","gamma","studentsT","wignerSemicircle","kumaraswamy","hermite","chiSquared","rayleigh","logNormal","cauchy","laplace","logistic"];constructor(t){this.setSeed(t)}getSeed(){return this.#r}sameAs(t){return t instanceof y&&(this.#r===t.#r&&this.#i===t.#i)}randomSource(t){return this.#i=t,this}getRandomSource(){return this.#i}setSeed(t){return null==t?this.setSeed(Math.ceil(1e8*Math.random())):("string"==typeof t&&(t=this.convertStringToNumber(t)),this.#r=t,this)}seed(t){return this.setSeed(t),this}serialize(){return{seed:this.#r}}static unserialize(t){const{constructor:e}=Object.getPrototypeOf(this),n=new e(t.seed);return n.setSeed(t.seed),n}predictable(t){const{constructor:e}=Object.getPrototypeOf(this);return new e(t??d)}static predictable(t){return new this(t??d)}hashStr(t){let e,n,r=0;if(0===t.length)return r;for(e=0;eu&&this.shouldThrowOnMaxRecursionsReached())throw new _(`Max recursive calls to rng normal function. This might be as a result of using predictable random numbers, or inappropriate arguments? Args: ${JSON.stringify({mean:t,stddev:e,max:n,min:r,skew:o})}`);let i=this.bates(7);return i=o<0?1-Math.pow(i,Math.pow(2,o)):Math.pow(i,Math.pow(2,-o)),void 0===t&&void 0===e&&void 0!==n&&void 0!==r?this.scaleNorm(i,r,n):(i=10*i-5,void 0===t?(t=0,void 0!==n&&void 0!==r&&(t=(n+r)/2,void 0===e&&(e=Math.abs(n-r)/10)),void 0===e&&(e=.1),i=i*e+t):(void 0===e&&(e=void 0!==n&&void 0!==r?Math.abs(n-r)/10:.1),i=i*e+t),s<=u&&(void 0!==n&&i>n||void 0!==r&&ie&&s++=h)throw new Error("LOOP_MAX reached in poisson - bailing out - possible parameter error, or using non-random source?");return n-1}hypergeometric({N:t=50,K:e=10,n=5,k:o}={}){function s(t){let e=0;for(let n=2;n<=t;n++)e+=Math.log(n);return e}function i(t,e){return s(t)-s(e)-s(t-e)}(0,r.Ay)({N:t}).int().positive(),(0,r.Ay)({K:e}).int().positive().lteq(t),(0,r.Ay)({n}).int().positive().lteq(t),void 0===o&&(o=this.randInt(0,Math.min(e,n))),(0,r.Ay)({k:o}).int().betweenEq(0,Math.min(e,n));const a=i(e,o)+i(t-e,n-o)-i(t,n);return Math.exp(a)}rademacher(){return this._random()<.5?-1:1}binomial({n:t=1,p:e=.5}={}){(0,r.Ay)({n:t}).int().positive(),(0,r.Ay)({p:e}).betweenEq(0,1);let n=0;for(let r=0;r{let n=this._random(),r=this._random();return n=Math.pow(n,1/t),r=Math.pow(r,1/e),n/(n+r)})(t,e);let s=0;for(let t=0;t{let e=0;for(let n=0;n=h)throw new Error("LOOP_MAX reached in beta - bailing out - possible parameter error, or using non-random source?")}return e},o=n(t);return o/(o+n(e))}gamma({shape:t=1,rate:e,scale:n}={}){if((0,r.Ay)({shape:t}).positive(),void 0!==n&&void 0!==e&&e!==1/n)throw new Error("Cannot supply rate and scale");let o,s,i,a,l,u;void 0!==n&&((0,r.Ay)({scale:n}).positive(),e=1/n),void 0===e&&(e=1),e&&(0,r.Ay)({rate:e}).positive();let d=1;const g=t-1/3,f=1/Math.sqrt(9*g);let m=0;o=!0;const _=new c(p);for(;o&&m++=h)throw new Error(`LOOP_MAX reached inside gamma inner loop - bailing out - possible parameter error, or using non-random source? had shape = ${t}, rate = ${e}, scale = ${n}`);d*=Math.pow(d,2),s=Math.pow(l,2),i=1-.331*s*s,a=.5*s+g*(1-d+Math.log(d)),u=this._random(),_.push(u),_.detectLoop(`Loop detected in randomly generated numbers over the last 10 generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${u}`),(u=h)throw new Error(`LOOP_MAX reached inside gamma - bailing out - possible parameter error, or using non-random source? had shape = ${t}, rate = ${e}, scale = ${n}`);return e*g*d}studentsT(t=1){"object"==typeof t&&({nu:t=1}=t),(0,r.Ay)({nu:t}).positive();const e=Math.sqrt(-2*Math.log(this._random()))*Math.cos(2*Math.PI*this._random()),n=this.gamma({shape:t/2,rate:2});return e/Math.sqrt(n/t)}wignerSemicircle(t=1){"object"==typeof t&&({R:t=1}=t),(0,r.Ay)({R:t}).gt(0);const e=2*this._random()*Math.PI;return t*Math.cos(e)}kumaraswamy({alpha:t=.5,beta:e=.5}={}){(0,r.Ay)({alpha:t}).gt(0),(0,r.Ay)({beta:e}).gt(0);const n=this._random();return Math.pow(1-Math.pow(1-n,1/e),1/t)}hermite({lambda1:t=1,lambda2:e=2}={}){(0,r.Ay)({lambda1:t}).gt(0),(0,r.Ay)({lambda2:e}).gt(0);return this.poisson({lambda:t})+this.poisson({lambda:e})}chiSquared(t=1){"object"==typeof t&&({k:t=1}=t),(0,r.Ay)({k:t}).positive().int();let e=0;for(let n=0;n=u){if(this.shouldThrowOnMaxRecursionsReached())throw new _("Max recursions reached in chancy. Usually a case of badly chosen min/max values.");return 0}if(Array.isArray(t))return this.choice(t);if("string"==typeof t)return this.dice(t);if("object"==typeof t){switch(t.type=t.type??"random","random"!==t.type&&"int"!==t.type&&"integer"!==t.type||void 0!==t.min&&void 0===t.max&&(t.max=Number.MAX_SAFE_INTEGER),t.type){case"random":return this.random(t.min,t.max,t.skew);case"int":case"integer":return this.randInt(t.min,t.max,t.skew);case"normal_integer":case"normal_int":return Math.floor(this.normal(t));case"dice":return this.chancyMinMax(this.dice(t.dice??t),t,e);case"rademacher":return this.chancyMinMax(this.rademacher(),t,e);case"normal":case"gaussian":case"boxMuller":case"irwinHall":case"bates":case"batesgaussian":case"bernoulli":case"exponential":case"pareto":case"poisson":case"hypergeometric":case"binomial":case"betaBinomial":case"beta":case"gamma":case"studentsT":case"wignerSemicircle":case"kumaraswamy":case"hermite":case"chiSquared":case"rayleigh":case"logNormal":case"cauchy":case"laplace":case"logistic":return this.chancyMinMax(this[t.type](t),t,e)}throw new Error(`Invalid input type given to chancy: "${t.type}".`)}if("number"==typeof t)return t;throw new Error("Invalid input given to chancy")}chancyMinMax(t,e,n=0){const{min:r,max:o}=e;return n+1>=u&&!this.shouldThrowOnMaxRecursionsReached()?(void 0!==r&&(t=Math.max(r,t)),void 0!==o&&(t=Math.min(o,t)),t):void 0!==r&&to?this.chancy(e,n+1):t}chancyMin(t){const{constructor:e}=Object.getPrototypeOf(this);return e.chancyMin(t)}chancyMax(t){const{constructor:e}=Object.getPrototypeOf(this);return e.chancyMax(t)}static chancyMin(t){if(Array.isArray(t)){for(const e of t)if(!b(e))throw new Error("Cannot pass non-numbers to chancyMin array input");return Math.min(...t)}if("string"==typeof t)return this.diceMin(t);if("number"==typeof t)return t;if("object"==typeof t){switch(t.type=t.type??"random","random"!==t.type&&"integer"!==t.type||void 0!==t.min&&void 0===t.max&&(t.max=Number.MAX_SAFE_INTEGER),t.type){case"dice":return this.diceMin(t.dice);case"normal":case"normal_integer":return t.min??Number.NEGATIVE_INFINITY;case"integer":case"random":return t.min??0;case"boxMuller":case"gaussian":case"batesgaussian":case"studentsT":case"cauchy":case"laplace":case"logistic":return Number.NEGATIVE_INFINITY;case"irwinHall":case"bates":case"bernoulli":case"exponential":case"binomial":case"betaBinomial":case"hermite":case"chiSquared":case"rayleigh":return 0;case"pareto":return t.scale??1;case"poisson":return 1;case"hypergeometric":const{N:e=50,K:n=10,n:r=5}=t;return Math.max(0,r+n-e);case"rademacher":return-1;case"beta":case"gamma":case"kumaraswamy":case"logNormal":return Number.EPSILON;case"wignerSemicircle":return-1*(t.R??10)}throw new Error(`Invalid input type ${t.type}.`)}throw new Error("Invalid input supplied to chancyMin")}static chancyMax(t){if(Array.isArray(t)){for(const e of t)if(!b(e))throw new Error("Cannot pass non-numbers to chancyMax array input");return Math.max(...t)}if("string"==typeof t)return this.diceMax(t);if("number"==typeof t)return t;if("object"==typeof t){switch(t.type=t.type??"random","random"!==t.type&&"integer"!==t.type||void 0!==t.min&&void 0===t.max&&(t.max=Number.MAX_SAFE_INTEGER),t.type){case"dice":return this.diceMax(t.dice);case"normal":case"normal_integer":return t.max??Number.POSITIVE_INFINITY;case"integer":case"random":return t.max??1;case"boxMuller":case"gaussian":case"batesgaussian":case"exponential":case"pareto":case"gamma":case"studentsT":case"chiSquared":case"rayleigh":case"logNormal":case"cauchy":case"laplace":case"logistic":return Number.POSITIVE_INFINITY;case"irwinHall":return t.n??6;case"bates":case"bernoulli":case"rademacher":case"beta":case"kumaraswamy":return 1;case"poisson":case"hermite":return Number.MAX_SAFE_INTEGER;case"hypergeometric":const{K:e=10,n=5}=t;return Math.min(n,e);case"binomial":case"betaBinomial":return t.n??1;case"wignerSemicircle":return t.R??10}throw new Error(`Invalid input type ${t.type}.`)}throw new Error("Invalid input supplied to chancyMax")}choice(t){return this.weightedChoice(t)}weights(t){const e=new Map;return t.forEach((function(t){let n=0;e.has(t)&&(n=e.get(t)),e.set(t,n+1)})),e}weightedChoice(t){let e,n=0;if(Array.isArray(t)){if(0===t.length)return null;if(1===t.length)return t[0];const e=this.weights(t),n=this.weightedChoice(e);return e.clear(),n}if(t instanceof Map){if(0===t.size)return null;if(1===t.size)return t.keys().next().value;t.forEach(((t,e)=>{n+=t}))}else{const r=Object.keys(t);if(0===r.length)return null;if(1===r.length)return r[0];for(e in t){if(t[e]<0)throw new Error("Probability cannot be negative");n+=t[e]}}const r=this._random()*n;let o=0;if(t instanceof Map){for(const[e,n]of t)if(o+=n,r0;)i.dice.push(s*this.randInt(1,o)),r--;return i.total=function(t,...e){return Array.isArray(t)?t.reduce(((t,e)=>t+e),0):e.reduce(((t,e)=>t+e),0)}(i.dice)+n,i}throw new Error("Invalid arguments given to dice")}dice(t,e,n){return this.diceExpanded(t,e,n).total}parseDiceString(t){const{constructor:e}=Object.getPrototypeOf(this);return e.parseDiceString(t)}clamp(t,e,n){return void 0!==n&&(t=t<=n?t:n),void 0!==e&&(t=t>=e?t:e),t}bin(t,e,n,o){(0,r.Ay)({val:t}).gt(n).lt(o);const s=o-n;return Math.round((t-n)/s*(e-1))/(e-1)*s+n}}class w extends y{#c;#r=0;#i;#h=0;constructor(t){super(t),this.#c=4294967295,this.#h=987654321}static predictable(t){return new this(t??d)}serialize(){return{mask:this.getMask(),seed:this.getSeed(),m_z:this.getMz()}}sameAs(t){return t instanceof w&&(this.getRandomSource()===t.getRandomSource()&&this.getSeed()===t.getSeed()&&this.getMask()===t.getMask()&&this.getMz()===t.getMz())}getMask(){return this.#c}getMz(){return this.#h}setMask(t){this.#c=t}setMz(t){this.#h=t}static unserialize(t){const e=new this;return e.setSeed(t.seed),e.setMask(t.mask),e.setMz(t.m_z),e}seed(t){return super.seed(t),this.#h=987654321,this}_next(){this.#h=36969*(65535&this.#h)+(this.#h>>16)&this.#c,this.setSeed(18e3*(65535&this.getSeed())+(this.getSeed()>>16)&this.#c);let t=(this.#h<<16)+this.getSeed()&this.#c;return t/=4294967296,t+.5}}const E=w},784:(t,e,n)=>{n.d(e,{A:()=>a});var r=n(334),o=n(425),s=n(219),i=n(673);class a{name;id;fn;ul;rng;pools=[];functions={};conditions={};borrowed=new Set;constructor({name:t,rng:e,id:n,pools:r=[],fn:o,ul:s}={}){this.name=t,this.pools=r,this.fn=o,this.ul=s,this.rng=e??(s?s.getRng():new i.Ay),this.id=n??this.rng.randomString(6)}registerFunction(t,e){this.functions[t]=e}registerCondition(t,e){this.conditions[t]=e}get filename(){return this.fn??this.id??this.name}set filename(t){this.fn=t}get ultraloot(){return this.ul}set ultraloot(t){this.ul=t}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}borrow(t){return this.borrowed.add(t),this}unborrow(t){return this.borrowed.delete(t),this}getPools(){return this.pools}setRng(t){return this.rng=t,this}rollBasics({rng:t,looter:e,context:n,n:o=1}){const s=t??this.rng,i=s.chancy(o);return r.A.gc(`Table: ${this.description} | Rolling table ${i} times (from chancy(${JSON.stringify(o)}))`,{looter:e,context:n}),[s,i]}rollSync({looter:t,context:e,result:n=new s.A,rng:o,n:i=1}={}){const[a,l]=this.rollBasics({rng:o,n:i,looter:t,context:e});for(const r of this.pools)this.rollPoolSync({n:l,pool:r,rng:a,looter:t,context:e,result:n});return r.A.ge(),n}async roll({looter:t,context:e,result:n=new s.A,rng:o,n:i=1}={}){const[a,l]=this.rollBasics({rng:o,n:i,looter:t,context:e});for(const r of this.pools)await this.rollPool({n:l,pool:r,rng:a,looter:t,context:e,result:n});return r.A.ge(),n}rollPoolSync({pool:t,looter:e,context:n,result:r=new s.A,rng:o,n:i=1}){const a=o??this.rng,l=a.chancy(i);for(let o=0;oe||n.hasFunction(t)),!1)}hasCondition(t){return void 0!==this.conditions[t.function]||Array.from(this.borrowed).reduce(((e,n)=>e||n.hasCondition(t)),!1)}createPool(t){const e=new o.A(t);return this.pools.push(e),e}addPool(t){return t instanceof o.A?this.pools.push(t):this.createPool(t),this}getPotentialDrops(){const t=[];for(const e of this.pools){let n=0;for(const t of e.getEntries())n+=t instanceof a?1:t.weight??1;const r=i.Ay.chancyMax(e.rolls),o=i.Ay.chancyMin(e.rolls),s=i.Ay.chancyMin(e.nulls);for(const l of e.getEntries())if(l instanceof a||l.isTable()){let e,n=1;l instanceof a?(n=1,e=l):l.isTable()&&(n=l.weight??1,e=l.getItem());const i=e.getPotentialDrops();for(const e of i)t.push({entry:e.entry,weight:e.weight/n,min:s>0?0:o*e.min,max:r*e.max})}else t.push({entry:l,weight:l.weight/n,min:s>0?0:o*i.Ay.chancyMin(l.qty),max:r*i.Ay.chancyMax(l.qty)})}return t}async applyFunction(t,{rng:e,looted:n,looter:r,context:o,result:s}){if(void 0!==this.functions[t.function])return await this.functions[t.function]({rng:e,looted:n,looter:r,context:o,result:s,args:t.args});{for(const i of Array.from(this.borrowed))if(i.hasFunction(t))return await i.applyFunction(t,{rng:e,looted:n,looter:r,context:o,result:s});const i=`Function ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerFunction(name, function).`;if(this.ultraloot){if(this.ultraloot.hasFunction(t.function))return await this.ultraloot.applyFunction(t,{rng:e,looted:n,looter:r,context:o,result:s});if(this.ultraloot.throwOnMissingFunctions)throw new Error(i);console.error(i)}else console.error(i)}}async applyCondition(t,{rng:e,looter:n,context:r,result:o}){if(void 0===this.conditions[t.function]){for(const s of Array.from(this.borrowed))if(s.hasCondition(t))return await s.applyCondition(t,{rng:e,looter:n,context:r,result:o});const s=`Condition ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerCondition(name, condition_function).`;if(this.ultraloot){if(this.ultraloot.hasCondition(t.function))return await this.ultraloot.applyCondition(t,{rng:e,looter:n,context:r,result:o});if(this.ultraloot.throwOnMissingConditions)throw new Error(s);return console.error(`CR: ${s}`),!0}return console.error(`CR: ${s}`),!0}return await this.conditions[t.function]({rng:e,looter:n,context:r,result:o,args:t.args})}applyFunctionSync(t,{rng:e,looted:n,looter:r,context:o,result:s}){if(void 0!==this.functions[t.function])return this.functions[t.function]({rng:e,looted:n,looter:r,context:o,result:s,args:t.args});{for(const i of Array.from(this.borrowed))if(i.hasFunction(t))return i.applyFunctionSync(t,{rng:e,looted:n,looter:r,context:o,result:s});const i=`Function ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerFunction(name, function).`;if(this.ultraloot){if(this.ultraloot.hasFunction(t.function))return this.ultraloot.applyFunctionSync(t,{rng:e,looted:n,looter:r,context:o,result:s});if(this.ultraloot.throwOnMissingFunctions)throw new Error(i);console.error(i)}else console.error(i)}}applyConditionSync(t,{rng:e,looter:n,context:r,result:o}){if(void 0===this.conditions[t.function]){for(const s of Array.from(this.borrowed))if(s.hasCondition(t))return s.applyConditionSync(t,{rng:e,looter:n,context:r,result:o});const s=`Condition ${t.function} has not been defined. Did you forget to register the function with this loot table? table.registerCondition(name, condition_function).`;if(this.ultraloot){if(this.ultraloot.hasCondition(t.function))return this.ultraloot.applyConditionSync(t,{rng:e,looter:n,context:r,result:o});if(this.ultraloot.throwOnMissingConditions)throw new Error(s);return console.error(s),!0}return console.error(s),!0}const s=this.conditions[t.function]({rng:e,looter:n,context:r,result:o,args:t.args});if(s instanceof Promise)throw new Error("Cannot return promise from sync condition call");return s}}},425:(t,e,n)=>{n.d(e,{A:()=>c});var r=n(334),o=n(50),s=n(668),i=n(219),a=n(784),l=n(673);class c{name;id;conditions=[];functions=[];rolls=1;nulls=0;entries=[];template={};static NULLKEY="__NULL__fd2a99d2-26c0-4454-a284-34578b94e0f6";constructor({name:t,id:e,conditions:n=[],functions:r=[],rolls:o=1,nulls:s=0,entries:i=[],template:a={}}={}){if(this.name=t,this.conditions=n??[],this.functions=r??[],this.rolls=o,this.nulls=s,this.id=e??(new l.Ay).randomString(6),this.template=a,i)for(const t of i)this.addEntry(t)}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}createEntry(t){const e=new o.A({...this.template??{},...t});return this.entries.push(e),e}addEntry(t,e){return t instanceof a.A&&(t=new o.A({...this.template??{},...e??{},id:t.id,item:t})),t instanceof o.A?this.entries.push(t):this.createEntry(t),this}getEntries(){return this.entries}async roll({rng:t,table:e,looter:n,context:s,result:l=new i.A}){const h=t.chancyInt(this.rolls);r.A.gc(`Pool ${this.description} | Rolling pool ${h} times (from chancy(${JSON.stringify(this.rolls)}))`);const u={};t.chancy(this.nulls)>0&&(u[c.NULLKEY]=t.chancy(this.nulls));for(const o in this.entries){const i=this.entries[o];if(i instanceof a.A)u[o]=1;else{const a=await i.applyConditions({rng:t,table:e,looter:n,context:s,result:l});r.A.vv(`Pool ${this.description} | Result of calling await a.applyConditions was ${JSON.stringify(a)}`),a&&(u[o]=t.chancy(i.weight??1))}}r.A.vv(`Pool ${this.description} | Choices:`,u);const d=new i.A;let p=!0;for(const o of this.conditions){const i=await e.applyCondition(o,{rng:t,looter:n,context:s,result:l});if(r.A.v(`Pool ${this.description} | Testing function "${o.function}" resulted in ${JSON.stringify(i)}`),p=p&&i,!p){r.A.v(`Pool ${this.description} | Function "${o.function}" stopped this from being added`);break}}if(r.A.v(`Pool ${this.description} | After applying conditions, add was ${JSON.stringify(p)}`),p)for(let i=0;i0&&(u[c.NULLKEY]=t.chancy(this.nulls)),this.entries.forEach(((r,o)=>{r instanceof a.A?u[o]=1:r.applyConditionsSync({rng:t,table:e,looter:n,context:s,result:l})&&(u[o]=t.chancy(r.weight??1))})),r.A.vv(`Pool ${this.description} | Choices:`,u);const d=new i.A;let p=!0;for(const o of this.conditions){const i=e.applyConditionSync(o,{rng:t,looter:n,context:s,result:l});if(r.A.v(`Pool ${this.description} | Testing function "${o.function}" resulted in ${JSON.stringify(i)}`),p=p&&i,!p){r.A.v(`Pool ${this.description} | Function "${o.function}" stopped this from being added`);break}}if(r.A.v(`Pool ${this.description} | After applying conditions, add was ${JSON.stringify(p)}`),p)for(let i=0;i0)if(t.stackable)a.push(t);else for(let e=0;e0)if(t.stackable)a.push(t);else for(let e=0;e{n.d(e,{A:()=>l});var r=n(334),o=n(784),s=n(673),i=n(668),a=n(219);class l{id;stackable=!0;unique=!1;name;weight=1;item;qty=1;functions;conditions;rng;constructor({id:t,stackable:e=!0,unique:n=!1,name:r,weight:o=1,item:s,functions:i=[],conditions:a=[],qty:l=1}={}){this.id=t,this.name=r,this.stackable=e,this.unique=n,this.weight=o,this.item=s,this.qty=l,this.functions=i??[],this.conditions=a??[]}getRng(t){return t??this.rng??(this.rng=new s.Ay)}setRng(t){this.rng=t}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}getItem(){return this.item??this.id}deepCloneObject(t){return JSON.parse(JSON.stringify(t))}cloneItem(){return null===this.item?null:"object"==typeof this.item?"function"==typeof this.item.clone?this.item.clone(this.item):this.deepCloneObject(this.item):this.item}isTable(){return this.getItem()instanceof o.A}resultDefinition(t){return{id:this.id,stackable:this.stackable,name:this.name,item:this.cloneItem(),qty:t.chancy(this.qty)}}generateBaseResults(t){const e=this.resultDefinition(t);return new a.A([new i.A(e)])}async applyConditions({rng:t,table:e,looter:n,context:o,result:s=new a.A}){let i=!0;for(const a of this.conditions)if(i=i&&await e.applyCondition(a,{rng:this.getRng(t),looter:n,context:o,result:s}),!i){r.A.d(`Entry: ${this.description} | Condition "${a.function}" stopped this from being added`);break}return i}async roll({rng:t,table:e,looter:n,context:r,result:o=new a.A}){return this.isTable()?await this.rollTable({rng:this.getRng(t),table:e,looter:n,context:r,result:o}):await this.rollItem({rng:this.getRng(t),table:e,looter:n,context:r,result:o})}async rollItem({rng:t,table:e,looter:n,context:o,result:s=new a.A}){return r.A.d(`Entry: ${this.description} | Rolling Item for ${this.id}`,{looter:n,context:o}),await this.processEntryResults(this.generateBaseResults(this.getRng(t)),{rng:this.getRng(t),table:e,looter:n,context:o,result:s}),s}async rollTable({rng:t,table:e,looter:n,context:r,result:o=new a.A}){const s=await this.getItem().borrow(e).roll({looter:n,context:r,result:[],rng:t,n:this.qty});return this.getItem().unborrow(e),await this.processEntryResults(s,{rng:this.getRng(t),table:e,looter:n,context:r,result:o}),o}async processEntryResults(t,{rng:e,table:n,looter:r,context:o,result:s=new a.A}){for(const i of t)await this.processEntryResult(i,{rng:this.getRng(e),table:n,looter:r,context:o,result:s});return t}async processEntryResult(t,{rng:e,table:n,looter:r,context:o,result:s=new a.A}){for(const i of this.functions)await n.applyFunction(i,{rng:this.getRng(e),looted:t,looter:r,context:o,result:s});if(t.qty>0)if(t.stackable)s.push(t);else for(let e=0;e0)if(t.stackable||1===t.qty)s.push(t);else for(let e=0;e{n.d(e,{A:()=>r});class r{id;stackable=!0;name;item;qty=1;constructor({id:t,stackable:e=!0,name:n,item:r,qty:o=1}={}){this.id=t,this.name=n,this.item=r,this.qty=o,this.stackable=e}get description(){return this.describe()}describe(){return this.name?`${this.name} [${this.id}]`:`[${this.id}]`}getQty(){return this.qty}setQty(t){this.qty=t}addQty(t){this.qty=this.qty+t}}},219:(t,e,n)=>{n.d(e,{A:()=>r});class r extends Array{constructor(t){t instanceof Array?super(...t):t?super(t):super(),Object.setPrototypeOf(this,Object.create(r.prototype))}merge(t){for(const e of t)this.push(e);return this}merged(t){return new r([...this,...t])}entrySignature(t){const e={};for(const[n,r]of Object.entries(t))"qty"!==n&&(e[n]=r);return JSON.stringify(e)}collapsed(){const t={},e=[];for(const n of this)if(n.stackable){const e=this.entrySignature(n);void 0===t[e]?t[e]=n:t[e].addQty(n.qty)}else e.push(n);return new r([...e,...Object.values(t)])}}},224:(__unused_webpack_module,__webpack_exports__,__webpack_require__)=>{__webpack_require__.d(__webpack_exports__,{tZ:()=>UltraLoot});var _log__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(334),_table__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(784),_table_pool__WEBPACK_IMPORTED_MODULE_2__=__webpack_require__(425),_table_pool_entry__WEBPACK_IMPORTED_MODULE_3__=__webpack_require__(50),_rng__WEBPACK_IMPORTED_MODULE_4__=__webpack_require__(673),_package_json__WEBPACK_IMPORTED_MODULE_5__=__webpack_require__(330),_default_functions__WEBPACK_IMPORTED_MODULE_6__=__webpack_require__(663),_default_conditions__WEBPACK_IMPORTED_MODULE_7__=__webpack_require__(494);let fs,isNode=!1;"object"==typeof process&&"object"==typeof process.versions&&void 0!==process.versions.node&&(fs=__webpack_require__(896),isNode=!0);const VERSION_KEY="__version__";class RecursiveTableError extends Error{}class UltraLoot{static version=_package_json__WEBPACK_IMPORTED_MODULE_5__.rE;version=_package_json__WEBPACK_IMPORTED_MODULE_5__.rE;defaultRng;rng;rngConstructor;functions={};conditions={};throwOnMissingFunctions=!0;throwOnMissingConditions=!0;constructor(t){_log__WEBPACK_IMPORTED_MODULE_0__.A.d("UltraLoot initialising"),t&&(this.rng=this.makeRng(t))}registerDefaults(){return this.registerDefaultFunctions(),this.registerDefaultConditions(),this}registerDefaultFunctions(){for(const[t,e]of Object.entries(_default_functions__WEBPACK_IMPORTED_MODULE_6__))this.registerFunction(t,e);return this}registerDefaultConditions(){for(const[t,e]of Object.entries(_default_conditions__WEBPACK_IMPORTED_MODULE_7__))this.registerCondition(t,e);return this}instance(t){return new UltraLoot(t)}setRng(t){if(!this.isRng(t))throw new Error("rng given does not confirm to RngInterface");this.rng=t}getRng(){return this.rng??this.getDefaultRng()}getDefaultRng(){return this.defaultRng??(this.defaultRng=this.makeRng())}setRngConstructor(t){this.rngConstructor=t}getRngConstructor(){return this.rngConstructor??Object.getPrototypeOf(this.rng).constructor}isRng(t){if(void 0===t)return!1;if("object"!=typeof t)return!1;const e=["predictable","hashStr","convertStringToNumber","getSeed","seed","percentage","random","chance","chanceTo","randInt","uniqid","randomString","randBetween","normal","chancyInt","chancy","weightedChoice","dice","parseDiceString","clamp","bin","serialize"];let n=!0;for(const r of e)n=n&&"function"==typeof t[r];return n}makeRng(t){if(this.isRng(t))return t;return new(this.rngConstructor??_rng__WEBPACK_IMPORTED_MODULE_4__.Ay)(t)}registerFunction(t,e){this.functions[t]=e}registerCondition(t,e){this.conditions[t]=e}hasFunction(t){return void 0!==this.functions[t]}hasCondition(t){return void 0!==this.conditions[t]}noThrowOnMissingFunctionsOrConditions(){return this.throwOnMissingFunctions=!1,this.throwOnMissingConditions=!1,this}throwOnMissingFunctionsOrConditions(){return this.throwOnMissingFunctions=!0,this.throwOnMissingConditions=!0,this}functionCheck(t){if(_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`UL | Applying function ${t.function}`),void 0===this.functions[t.function]){const e=`Function ${t.function} has not been defined. Did you forget to register the function with this loot table? UltraLoot.registerFunction(name, function).`;if(this.throwOnMissingFunctions)throw new Error(e);return console.error(e),!1}return!0}conditionCheck(t){if(_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`UL | Applying condition ${t.function}`),void 0===this.conditions[t.function]){const e=`Condition ${t.function} has not been defined. Did you forget to register the function with this loot table? UltraLoot.registerCondition(name, condition_function).`;if(this.throwOnMissingConditions)throw new Error(e);return console.error(e),!1}return!0}applyFunctionSync(t,{rng:e,looted:n,looter:r,context:o,result:s}){if(this.functionCheck(t))return this.functions[t.function]({rng:e,looted:n,looter:r,context:o,result:s,args:Object.assign({},t.args??{},t.arguments??{})})}applyConditionSync(t,{rng:e,looter:n,context:r,result:o}){if(this.conditionCheck(t)){const s=this.conditions[t.function]({rng:e,looter:n,context:r,result:o,args:Object.assign({},t.args??{},t.arguments??{})});if(s instanceof Promise)throw new Error("Cannot return promise from sync condition call");return s}return!0}async applyFunction(t,{rng:e,looted:n,looter:r,context:o,result:s}){if(this.functionCheck(t))return await this.functions[t.function]({rng:e,looted:n,looter:r,context:o,result:s,args:Object.assign({},t.args??{},t.arguments??{})})}async applyCondition(t,{rng:e,looter:n,context:r,result:o}){return!this.conditionCheck(t)||await this.conditions[t.function]({rng:e,looter:n,context:r,result:o,args:Object.assign({},t.args??{},t.arguments??{})})}createTable(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||this.isLootTableDefinition(t)){t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A?_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating table from LootTable"):_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating table from LootTableDefinition"),t.ul=this,t.rng?t.rng=t.rng??this.makeRng(t.rng):t.rng=this.getRng();const e=new _table__WEBPACK_IMPORTED_MODULE_1__.A(t);return e.ultraloot=this,e}if(this.isEasyLootTableDefinition(t)){_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating table from LootTableEasyDefinition"),t.rng?t.rng=t.rng??this.makeRng(t.rng):t.rng=this.getRng();const e=new _table__WEBPACK_IMPORTED_MODULE_1__.A(this.transformEasyToProperLootTableDefinition(t));return e.ultraloot=this,e}throw new Error("Cannot create loot table from these params")}createPool(t){return this.isEasyLootTablePoolDefinition(t)?(_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating pool from LootTablePoolEasyDefinition"),new _table_pool__WEBPACK_IMPORTED_MODULE_2__.A(this.transformEasyToProperLootTablePoolDefinition(t))):(_log__WEBPACK_IMPORTED_MODULE_0__.A.vv("Creating pool from LootTablePoolDefinition"),new _table_pool__WEBPACK_IMPORTED_MODULE_2__.A(t))}createEntry(t){return t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A?new _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A({id:t.id,name:t.name,item:t,qty:1}):new _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A(t)}isLootTableDefinition(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||t instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A||t instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;if(t.pools)for(const e of t.pools)if(!(e instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A))return!1;return"object"==typeof t}isEasyLootTableDefinition(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||t instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A||t instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;if(t.pools)for(const e of t.pools)if(e instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A)return!1;return"object"==typeof t}isEasyLootTablePoolDefinition(t){if(t instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A||t instanceof _table_pool__WEBPACK_IMPORTED_MODULE_2__.A||t instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;if(t.entries)for(const e of t.entries)if(e instanceof _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__.A)return!1;return"object"==typeof t}transformEasyToProperLootTableDefinition(t){const e={rng:this.makeRng(t.rng??this.getRng()),name:t.name,id:t.id,pools:([],[])};if(t.pools)for(const n of t.pools)e.pools.push(this.createPool(n));return e.ul=this,e}transformEasyToProperLootTablePoolDefinition(t){const e=[];for(let n of t.entries??[])this.isEasyLootTableDefinition(n)&&void 0!==n.pools&&Array.isArray(n.pools)&&(n=this.createTable(n)),e.push(n);return{name:t.name,id:t.id,rolls:t.rolls,nulls:t.nulls,template:t.template,conditions:t.conditions,functions:t.functions,entries:e}}pathJoin(t,e="/"){return t.join(e).replace(new RegExp(e+"{1,}","g"),e)}finishWith(t,e){return t.endsWith(e)?t:t+e}finishWithExtension(t,e){if(t.endsWith(e))return t;if(0===t.length)return e;const n=t.split("/").pop().split("\\").pop(),r=n.includes(".")?n.lastIndexOf("."):n.length;return`${t.substring(0,t.length-n.length+r)}.${e.replace(".","")}`}getExtension(t){if(0===t.length)return null;const e=t.split("/").pop().split("\\").pop();if(!e.includes("."))return null;const n=e.lastIndexOf(".");return e.substring(n,e.length)}serialize(t,{includeRng:e=!1,key:n,had:r=new Set}={}){const o={},s={name:t.name,id:t.id,fn:t.fn,pools:([],[])},i=t.filename??this.getRng().randomString(6);r.add(t),e&&(s.rng=t.rng?.serialize()??null);for(const n of t.pools??[]){const t={name:n.name,id:n.id,rolls:n.rolls,nulls:n.nulls,conditions:n.conditions,functions:n.functions,entries:[]};for(const s of n.entries??[]){const n={name:s.name,id:s.id};if(s instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A?n.item=s:(n.stackable=s.stackable,n.weight=s.weight,n.item=s.item,n.qty=s.qty,n.conditions=s.conditions,n.functions=s.functions),n.item instanceof _table__WEBPACK_IMPORTED_MODULE_1__.A){const t=n.item.filename??this.getRng().randomString(6);if(r.has(n.item))throw new RecursiveTableError("Recursive requirement detected - cannot serialize recursively required tables.");if(void 0===o[t]){n.item.filename=t;const s=this.serialize(n.item,{includeRng:e,key:t,had:r});o[t]=s.tables[t]}n.type="table",n.item=t}t.entries.push(n)}s.pools.push(t)}o[i]=s;return{[VERSION_KEY]:_package_json__WEBPACK_IMPORTED_MODULE_5__.rE,tables:o}}toJson(t,{includeRng:e=!1}={}){return JSON.stringify(this.serialize(t,{includeRng:e}))}async saveTable(t,{path:e="",defaultExtension:n}={}){throw new Error("Not yet implemented.")}async loadTables(t,{path:e="",defaultExtension:n}={}){n=n??this.getExtension(e)??".json";const r=this.finishWith(this.pathJoin([e,t]),n);return isNode?r.startsWith("http")||r.startsWith("file://")?this.loadTablesFromUrl(r,{path:e}):this.loadTablesFromFile(r,{path:e}):this.loadTablesFromUrl(r,{path:e})}async loadTablesFromFile(filename,{path="",defaultExtension}={}){let contents;defaultExtension=defaultExtension??this.getExtension(path)??".json",_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`Reading tables from ${filename}`);const ext=this.getExtension(filename);if(".js"===ext){const cb=await fs.promises.readFile(`${filename}`,"utf8");contents=eval(cb)}else contents=await fs.promises.readFile(filename,"utf8").then((t=>JSON.parse(t))).catch((t=>{if(t instanceof SyntaxError)throw t.message=`There was an error loading file: "${filename}". ${t.message}`,t;throw t}));return this.unserialize(contents)}async loadTablesFromUrl(t,{path:e="",defaultExtension:n}={}){return n=n??this.getExtension(t)??".json",_log__WEBPACK_IMPORTED_MODULE_0__.A.d(`Reading tables from ${t}`),fetch(t).then((t=>t.text())).then((e=>{try{return JSON.parse(e)}catch(e){if(e instanceof SyntaxError)throw e.message=`There was an error loading file: "${t}". ${e.message}`,e;throw e}})).then((t=>this.unserialize(t)))}async loadTable(t,{path:e="",defaultExtension:n}={}){const r=n??this.getExtension(t)??".json",o=this.finishWithExtension(this.pathJoin([e,t]),r);return _log__WEBPACK_IMPORTED_MODULE_0__.A.d("Load Table",{filenameWithPath:this.pathJoin([e,t]),filename:t,defaultExtension:n,ext:r,path:e,fullPath:o}),isNode?o.startsWith("http")||o.startsWith("file://")?this.loadTableFromUrl(o,{path:e,defaultExtension:n}):this.loadTableFromFile(t,{path:e,defaultExtension:n}):this.loadTableFromUrl(o,{path:e,defaultExtension:n})}async loadTableFromFile(filename,{path="",defaultExtension}={}){defaultExtension=defaultExtension??this.getExtension(filename)??".json";const extension=this.getExtension(filename),pj=this.pathJoin([path,filename]);if(!extension){if(fs.existsSync(pj)&&fs.statSync(pj).isFile()){const t=await fs.promises.readFile(pj,"utf8").then((t=>JSON.parse(t))).catch((t=>{if(t instanceof SyntaxError)throw t.message=`There was an error loading file: "${filename}". ${t.message}`,t;throw t}));return this.resolveTable(t,{path,defaultExtension})}const t=new Set([defaultExtension,".json",".js",".cjs",".mjs"]);for(const e of t){const t=this.finishWithExtension(pj,e);if(fs.existsSync(t)&&fs.statSync(t).isFile())return this.loadTableFromFile(this.finishWithExtension(filename,e),{path,defaultExtension})}}if(!fs.existsSync(pj))throw new Error(`Could not find file "${filename}" in path "${path}"`);let contents;if(".js"===extension||".mjs"===extension||".cjs"===extension){const cb=await fs.promises.readFile(`${pj}`,"utf8");contents=eval(cb)}else".json"!==extension&&""!==defaultExtension||(contents=await fs.promises.readFile(pj,"utf8").then((t=>JSON.parse(t))).catch((t=>{if(t instanceof SyntaxError)throw t.message=`There was an error loading file: "${filename}". ${t.message}`,t;throw t})));return this.resolveTable(contents,{path,defaultExtension})}async loadTableFromUrl(t,{path:e="",defaultExtension:n}={}){return n=n??this.getExtension(t)??".json",fetch(t).then((t=>t.text())).then((e=>{try{return JSON.parse(e)}catch(e){if(e instanceof SyntaxError)throw e.message=`There was an error loading file: "${t}". ${e.message}`,e;throw e}})).then((t=>this.resolveTable(t,{path:e,defaultExtension:n})))}async resolveTable(t,{path:e="",defaultExtension:n}={}){for(const r of t.pools??[])for(const t of r.entries??[])"table"===t.type&&(t.item=await this.loadTable(t.item,{path:e,defaultExtension:n})),delete t.type;return this.createTable(t)}unserialize(t){const e={};let n=100;for(;Object.values(t.tables).length>0&&n-- >0;)t:for(const[n,r]of Object.entries(t.tables)){const o=r.rng??null;delete r.rng,_log__WEBPACK_IMPORTED_MODULE_0__.A.v(`Unserializing table ${n}`);for(const n of r.pools??[])for(const r of n.entries??[]){if("table"===r.type){if(void 0===e[r.item]){if(void 0===t.tables[r.item])throw new Error(`Table ${r.item} not present in serialized data`);_log__WEBPACK_IMPORTED_MODULE_0__.A.v(`We didn't have ${r.item} in our results`);continue t}r.item=e[r.item]}delete r.type}e[n]=this.createTable(r),o&&e[n].setRng(this.getRngConstructor().unserialize(o)),delete t.tables[n]}if(0===n)throw new Error("Maximum nested serialized table limit reached (could be a recursive requirement somewhere causing an issue?)");return e}}var __WEBPACK_DEFAULT_EXPORT__=UltraLoot},185:(t,e,n)=>{n.d(e,{_c:()=>o,fE:()=>s,m0:()=>r});const r=(t,e,n)=>{const r=e.split(".").reduce(((t,e)=>void 0!==t?t[e]:t),t);return void 0===r?n:r},o=(t,e,n)=>{const r=e.split(".");let o=t;for(let t=0;t{if(i=!!i,!t)return i;let l=t;if("string"==typeof e&&(l=r(t,e)),void 0!==n)return l=a?l===n:l==n,i?!l:!!l;if((void 0!==o||void 0!==s)&&a&&"number"!=typeof l)return!1;if(null!=l){if(void 0!==o&&parseFloat(l)s)return i;if(void 0!==o||void 0!==s)return!i}return i?!l:!!l}},896:t=>{t.exports=require("fs")},330:t=>{t.exports={rE:"0.3.0"}}},__webpack_module_cache__={};function __webpack_require__(t){var e=__webpack_module_cache__[t];if(void 0!==e)return e.exports;var n=__webpack_module_cache__[t]={exports:{}};return __webpack_modules__[t](n,n.exports,__webpack_require__),n.exports}__webpack_require__.d=(t,e)=>{for(var n in e)__webpack_require__.o(e,n)&&!__webpack_require__.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:e[n]})},__webpack_require__.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),__webpack_require__.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})};var __webpack_exports__={};__webpack_require__.d(__webpack_exports__,{default:()=>src});var ultraloot=__webpack_require__(224),rng=__webpack_require__(673);class PredictableRng extends rng.Up{counter=0;_results=[0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1-Number.EPSILON];constructor(t,e){super(t),e&&(this.results=e)}get results(){return this._results}set results(t){if(t.length<=0)throw new Error("Must provide some fake results.");for(const e of t){if(e<0)throw new Error(`Results must be greater than or equal to 0, got '${e}'`);if(e>=1)throw new Error(`Results must be less than 1, got '${e}'`)}this._results=t,this.reset()}evenSpread(t){const e=[];for(let n=0;n { /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ A: () => (/* binding */ Rng), -/* harmony export */ U: () => (/* binding */ RngAbstract) +/* harmony export */ Ay: () => (__WEBPACK_DEFAULT_EXPORT__), +/* harmony export */ Bh: () => (/* binding */ ArrayNumberValidator), +/* harmony export */ Ol: () => (/* binding */ NumberValidator), +/* harmony export */ X: () => (/* binding */ NumberValidationError) /* harmony export */ }); -const MAX_RECURSIONS = 100; +/** + * @category Number Validator + */ +const assert = (truthy, msg = 'Assertion failed') => { + if (!truthy) { + throw new NumberValidationError(msg); + } +}; +/** + * @category Number Validator + */ +class NumberValidationError extends Error { +} +/** + * @category Number Validator + */ +class ArrayNumberValidator { + /** + * The numbers to be validated + */ + #numbers = []; + /** + * Descriptive name for this validation + */ + name = 'numbers'; + constructor(numbers, name = 'numbers') { + this.numbers = numbers; + this.name = name; + } + get numbers() { + return this.#numbers; + } + set numbers(numbers) { + for (const number of numbers) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + } + this.#numbers = numbers; + } + /** + * Specify the numbers to validate + */ + all(numbers) { + this.numbers = numbers; + return this; + } + /** + * Specify the numbers to validate + */ + validate(numbers) { + if (!Array.isArray(numbers)) { + return new NumberValidator(numbers); + } + return this.all(numbers); + } + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potatoes = [0, 1]; + * validate(potatoes).varname('potatoes').gt(2); // "Expected every component of potatoes to be > 2, got 0" + */ + varname(name) { + this.name = name; + return this; + } + /** + * Get the sum of our numbers + */ + sum() { + return this.numbers.reduce((a, b) => a + b, 0); + } + /** + * Validates whether the total of all of our numbers is close to sum, with a maximum difference of diff + * @param sum The sum + * @param diff The maximum difference + * @param msg Error message + * @throws {@link NumberValidationError} If they do not sum close to the correct amount + */ + sumcloseto(sum, diff = 0.0001, msg) { + assert(Math.abs(this.sum() - sum) < diff, msg ?? `Expected sum of ${this.name} to be within ${diff} of ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is equal (===) to sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to the correct amount + */ + sumto(sum, msg) { + assert(this.sum() === sum, msg ?? `Expected sum of ${this.name} to be equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is < sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to < sum + */ + sumtolt(sum, msg) { + assert(this.sum() < sum, msg ?? `Expected sum of ${this.name} to be less than ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is > sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to > sum + */ + sumtogt(sum, msg) { + assert(this.sum() > sum, msg ?? `Expected sum of ${this.name} to be greater than ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is <= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to <= sum + */ + sumtolteq(sum, msg) { + assert(this.sum() <= sum, msg ?? `Expected sum of ${this.name} to be less than or equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * Validates whether the total of all of our numbers is >= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to >= sum + */ + sumtogteq(sum, msg) { + assert(this.sum() >= sum, msg ?? `Expected sum of ${this.name} to be greater than or equal to ${sum}, got ${this.sum()}`); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all integers + */ + int(msg) { + this.numbers.forEach(a => validate(a).int(msg ?? `Expected every component of ${this.name} to be an integer, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all positive + */ + positive(msg) { + this.numbers.forEach(a => validate(a).positive(msg ?? `Expected every component of ${this.name} to be postiive, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all negative + */ + negative(msg) { + this.numbers.forEach(a => validate(a).negative(msg ?? `Expected every component of ${this.name} to be negative, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all between from and to + */ + between(from, to, msg) { + this.numbers.forEach(a => validate(a).between(from, to, msg ?? `Expected every component of ${this.name} to be between ${from} and ${to}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all between or equal to from and to + */ + betweenEq(from, to, msg) { + this.numbers.forEach(a => validate(a).betweenEq(from, to, msg ?? `Expected every component of ${this.name} to be between or equal to ${from} and ${to}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all > n + */ + gt(n, msg) { + this.numbers.forEach(a => validate(a).gt(n, msg ?? `Expected every component of ${this.name} to be > ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all >= n + */ + gteq(n, msg) { + this.numbers.forEach(a => validate(a).gteq(n, msg ?? `Expected every component of ${this.name} to be >= ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all < n + */ + lt(n, msg) { + this.numbers.forEach(a => validate(a).lt(n, msg ?? `Expected every component of ${this.name} to be < ${n}, got ${a}`)); + return this; + } + /** + * @throws {@link NumberValidationError} if numbers are not all <= n + */ + lteq(n, msg) { + this.numbers.forEach(a => validate(a).lteq(n, msg ?? `Expected every component of ${this.name} to be <= ${n}, got ${a}`)); + return this; + } +} +/** + * Validate numbers in a fluent fashion. + * + * Each validator method accepts a message as the last parameter + * for customising the error message. + * + * @category Number Validator + * + * @example + * const n = new NumberValidator(); + * n.validate(0).gt(1); // NumberValidationError + * + * @example + * const n = new NumberValidator(); + * const probability = -0.1; + * n.validate(probability).gteq(0, 'Probabilities should always be >= 0'); // NumberValidationError('Probabilities should always be >= 0'). + */ +class NumberValidator { + /** + * The number being tested. + */ + #number; + /** + * The name of the variable being validated - shows up in error messages. + */ + name = 'number'; + constructor(number = 0, name = 'number') { + this.number = number; + this.name = name; + } + get number() { + return this.#number; + } + set number(number) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + this.#number = number; + } + /** + * Returns an ArrayNumberValidator for all the numbers + */ + all(numbers, name) { + return new ArrayNumberValidator(numbers, name ?? this.name); + } + assertNumber(num) { + assert(typeof this.number !== 'undefined', 'No number passed to validator.'); + return true; + } + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potato = 1; + * validate(potato).varname('potato').gt(2); // "Expected potato to be greater than 2, got 1" + * @param {string} name [description] + */ + varname(name) { + this.name = name; + return this; + } + /** + * Specify the number to be validated + */ + validate(number) { + if (Array.isArray(number)) { + return this.all(number); + } + this.number = number; + return this; + } + /** + * Asserts that the number is an integer + * @throws {@link NumberValidationError} if ths number is not an integer + */ + int(msg) { + if (this.assertNumber(this.number)) + assert(Number.isInteger(this.number), msg ?? `Expected ${this.name} to be an integer, got ${this.number}`); + return this; + } + /** + * Asserts that the number is > 0 + * @throws {@link NumberValidationError} if the number is not positive + */ + positive(msg) { + return this.gt(0, msg ?? `Expected ${this.name} to be positive, got ${this.number}`); + } + /** + * Asserts that the number is < 0 + * @throws {@link NumberValidationError} if the number is not negative + */ + negative(msg) { + return this.lt(0, msg ?? `Expected ${this.name} to be negative, got ${this.number}`); + } + /** + * Asserts that the from < number < to + * @throws {@link NumberValidationError} if it is outside or equal to those bounds + */ + between(from, to, msg) { + if (this.assertNumber(this.number)) + assert(this.number > from && this.number < to, msg ?? `Expected ${this.name} to be between ${from} and ${to}, got ${this.number}`); + return this; + } + /** + * Asserts that the from <= number <= to + * @throws {@link NumberValidationError} if it is outside those bounds + */ + betweenEq(from, to, msg) { + if (this.assertNumber(this.number)) + assert(this.number >= from && this.number <= to, msg ?? `Expected ${this.name} to be between or equal to ${from} and ${to}, got ${this.number}`); + return this; + } + /** + * Asserts that number > n + * @throws {@link NumberValidationError} if it is less than or equal to n + */ + gt(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number > n, msg ?? `Expected ${this.name} to be greater than ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number >= n + * @throws {@link NumberValidationError} if it is less than n + */ + gteq(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number >= n, msg ?? `Expected ${this.name} to be greater than or equal to ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number < n + * @throws {@link NumberValidationError} if it is greater than or equal to n + */ + lt(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number < n, msg ?? `Expected ${this.name} to be less than ${n}, got ${this.number}`); + return this; + } + /** + * Asserts that number <= n + * @throws {@link NumberValidationError} if it is greater than n + */ + lteq(n, msg) { + if (this.assertNumber(this.number)) + assert(this.number <= n, msg ?? `Expected ${this.name} to be less than or equal to ${n}, got ${this.number}`); + return this; + } +} +function validate(number) { + if (Array.isArray(number)) { + return new ArrayNumberValidator(number); + } + else if (typeof number === 'object') { + const entries = Object.entries(number); + if (entries.length === 0) { + throw new Error('Empty object provided'); + } + const [name, value] = entries[0]; + return validate(value).varname(name); + } + else { + return new NumberValidator(number); + } +} +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (validate); + + +/***/ }), + +/***/ 673: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + YG: () => (/* binding */ MaxRecursionsError), + Qs: () => (/* binding */ NonRandomRandomError), + Up: () => (/* binding */ RngAbstract), + Ay: () => (/* binding */ src_rng) +}); + +// EXTERNAL MODULE: ./src/number.ts +var src_number = __webpack_require__(623); +;// ./src/rng/pool.ts + +/** + * @category Pool + */ +class PoolEmptyError extends Error { +} +/** + * @category Pool + */ +class PoolNotEnoughElementsError extends Error { +} +/** + * Allows for randomly drawing from a pool of entries without replacement + * @category Pool + */ +class Pool { + rng; + #entries = []; + constructor(entries = [], rng) { + this.entries = entries; + if (rng) { + this.rng = rng; + } + else { + this.rng = new src_rng(); + } + } + copyArray(arr) { + return Array.from(arr); + } + setEntries(entries) { + this.entries = entries; + return this; + } + getEntries() { + return this.#entries; + } + set entries(entries) { + this.#entries = this.copyArray(entries); + } + get entries() { + return this.#entries; + } + get length() { + return this.#entries.length; + } + setRng(rng) { + this.rng = rng; + return this; + } + getRng() { + return this.rng; + } + add(entry) { + this.#entries.push(entry); + } + empty() { + this.#entries = []; + return this; + } + isEmpty() { + return this.length <= 0; + } + /** + * Draw an element from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + */ + draw() { + if (this.length === 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length === 1) { + return this.#entries.splice(0, 1)[0]; + } + const idx = this.rng.randInt(0, this.#entries.length - 1); + return this.#entries.splice(idx, 1)[0]; + } + /** + * Draw n elements from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + * @throws {@link PoolNotEnoughElementsError} if the pool does not have enough elements to draw n values + */ + drawMany(n) { + if (n < 0) { + throw new Error('Cannot draw < 0 elements from pool'); + } + if (this.length === 0 && n > 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length < n) { + throw new PoolNotEnoughElementsError(`Tried to draw ${n} elements from pool with only ${this.length} entries.`); + } + const result = []; + for (let i = 0; i < n; i++) { + const idx = this.rng.randInt(0, this.#entries.length - 1); + result.push(this.#entries.splice(idx, 1)[0]); + } + return result; + } +} + +;// ./src/rng/queue.ts +class Dequeue { + size; + elements = []; + constructor(length = 1) { + if (Array.isArray(length)) { + this.elements = length; + this.size = this.elements.length; + } + else { + this.size = length; + } + } + get length() { + return this.elements.length; + } + push(el) { + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + pop() { + return this.elements.pop(); + } + full() { + return this.length >= this.size; + } + empty() { + this.elements = []; + } + get(i) { + return this.elements[i]; + } + allSame() { + if (this.length > 0) { + return this.elements.every(a => a === this.elements[0]); + } + return true; + } +} +class NumberQueue extends (/* unused pure expression or super */ null && (Dequeue)) { + sum() { + return this.elements.reduce((a, b) => a + b, 0); + } + avg() { + return this.sum() / this.length; + } +} +class LoopDetectedError extends Error { +} +class NonRandomDetector extends Dequeue { + minsequencelength = 2; + errormessage = 'Loop detected in input data. Randomness source not random?'; + constructor(length = 1, minsequencelength = 2) { + super(length); + if (this.size > 10000) { + throw new Error('Cannot detect loops for more than 10000 elements'); + } + this.minsequencelength = minsequencelength; + } + push(el) { + this.detectLoop(); + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + detectLoop(msg) { + if (this.full()) { + if (this.allSame()) { + this.loopDetected(msg); + } + if (this.hasRepeatingSequence(this.elements, this.minsequencelength)) { + this.loopDetected(msg); + } + } + } + loopDetected(msg) { + throw new LoopDetectedError(msg ?? this.errormessage); + } + /** + * Checks if there is a repeating sequence longer than a specified length in an array of numbers. + * + * @param {number[]} arr - The array of numbers. + * @param {number} n - The minimum length of the repeating sequence. + * @returns {boolean} True if a repeating sequence longer than length n is found, otherwise false. + */ + hasRepeatingSequence(arr, n) { + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + let k = 0; + while (j + k < arr.length && arr[i + k] === arr[j + k]) { + k++; + if (k > n) { + return true; + } + } + } + } + return false; + } +} + +;// ./src/rng.ts + + + +/** + * Safeguard against huge loops. If loops unintentionally grow beyond this + * arbitrary limit, bail out.. + */ +const LOOP_MAX = 10000000; +/** + * Safeguard against too much recursion - if a function recurses more than this, + * we know we have a problem. + * + * Max recursion limit is around ~1000 anyway, so would get picked up by interpreter. + */ +const MAX_RECURSIONS = 500; const THROW_ON_MAX_RECURSIONS_REACHED = true; -const diceRe = /^ *([0-9]+) *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; -const diceReNoInit = /^ *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; +const PREDICTABLE_SEED = 5789938451; +const SAMERANDOM_MAX = 10; +const diceRe = /^ *([+-]? *[0-9_]*) *[dD] *([0-9_]+) *([+-]? *[0-9_.]*) *$/; const strToNumberCache = {}; const diceCache = {}; +class MaxRecursionsError extends Error { +} +class NonRandomRandomError extends Error { +} +function sum(numbersFirstArg, ...numbers) { + if (Array.isArray(numbersFirstArg)) { + return numbersFirstArg.reduce((a, b) => a + b, 0); + } + return numbers.reduce((a, b) => a + b, 0); +} +function isNumeric(input) { + return (typeof input === 'number') || (!isNaN(parseFloat(input)) && isFinite(input)); +} +/** + * This abstract class implements most concrete implementations of + * functions, as the only underlying changes are likely to be to the + * uniform random number generation, and how that is handled. + * + * All the typedoc documentation for this has been sharded out to RngInterface + * in a separate file. + */ class RngAbstract { #seed = 0; + #monotonic = 0; + #lastuniqid = 0; + #randFunc; + #shouldThrowOnMaxRecursionsReached; + #distributions = [ + 'normal', + 'gaussian', + 'boxMuller', + 'irwinHall', + 'bates', + 'batesgaussian', + 'bernoulli', + 'exponential', + 'pareto', + 'poisson', + 'hypergeometric', + 'rademacher', + 'binomial', + 'betaBinomial', + 'beta', + 'gamma', + 'studentsT', + 'wignerSemicircle', + 'kumaraswamy', + 'hermite', + 'chiSquared', + 'rayleigh', + 'logNormal', + 'cauchy', + 'laplace', + 'logistic', + ]; constructor(seed) { this.setSeed(seed); } @@ -282,7 +943,17 @@ class RngAbstract { return this.#seed; } sameAs(other) { - return this.#seed === other.#seed; + if (other instanceof RngAbstract) { + return this.#seed === other.#seed && this.#randFunc === other.#randFunc; + } + return false; + } + randomSource(source) { + this.#randFunc = source; + return this; + } + getRandomSource() { + return this.#randFunc; } setSeed(seed) { if (typeof seed !== 'undefined' && seed !== null) { @@ -305,6 +976,10 @@ class RngAbstract { seed: this.#seed, }; } + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized) { const { constructor } = Object.getPrototypeOf(this); const rng = new constructor(serialized.seed); @@ -313,11 +988,15 @@ class RngAbstract { } predictable(seed) { const { constructor } = Object.getPrototypeOf(this); - const newSelf = new constructor(seed); + const newSelf = new constructor(seed ?? PREDICTABLE_SEED); return newSelf; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ static predictable(seed) { - return new this(seed); + return new this(seed ?? PREDICTABLE_SEED); } hashStr(str) { let hash = 0; @@ -341,23 +1020,36 @@ class RngAbstract { return num; } _random() { - return Math.random(); + if (typeof this.#randFunc === 'function') { + return this.#randFunc(); + } + return this._next(); } percentage() { return this.randBetween(0, 100); } + probability() { + return this.randBetween(0, 1); + } random(from = 0, to = 1, skew = 0) { return this.randBetween(from, to, skew); } chance(n, chanceIn = 1) { + (0,src_number/* default */.Ay)({ chanceIn }).positive(); + (0,src_number/* default */.Ay)({ n }).positive(); const chance = n / chanceIn; return this._random() <= chance; } // 500 to 1 chance, for example chanceTo(from, to) { - return this._random() <= (from / (from + to)); + return this.chance(from, from + to); } randInt(from = 0, to = 1, skew = 0) { + (0,src_number/* default */.Ay)({ from }).int(); + (0,src_number/* default */.Ay)({ to }).int(); + if (from === to) { + return from; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -368,14 +1060,21 @@ class RngAbstract { } return Math.floor(rand * ((to + 1) - from)) + from; } - // Not deterministic - uniqid(prefix = '', random = false) { - const sec = Date.now() * 1000 + Math.random() * 1000; + uniqid(prefix = '') { + const now = Date.now() * 1000; + if (this.#lastuniqid === now) { + this.#monotonic++; + } + else { + this.#monotonic = Math.round(this._random() * 100); + } + const sec = now + this.#monotonic; const id = sec.toString(16).replace(/\./g, '').padEnd(14, '0'); - return `${prefix}${id}${random ? `.${Math.trunc(Math.random() * 100000000)}` : ''}`; + this.#lastuniqid = now; + return `${prefix}${id}`; } - // Deterministic - uniqstr(len = 6) { + randomString(len = 6) { + (0,src_number/* default */.Ay)({ len }).gt(0); const str = []; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const alen = 61; @@ -384,7 +1083,10 @@ class RngAbstract { } return str.join(''); } - randBetween(from = 0, to = 1, skew = 0) { + randBetween(from = 0, to, skew = 0) { + if (typeof to === 'undefined') { + to = from + 1; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -396,57 +1098,97 @@ class RngAbstract { return this.scaleNorm(rand, from, to); } scale(number, from, to, min = 0, max = 1) { - if (number > max) - throw new Error(`Number ${number} is greater than max of ${max}`); - if (number < min) - throw new Error(`Number ${number} is less than min of ${min}`); + (0,src_number/* default */.Ay)({ number }).lteq(max); + (0,src_number/* default */.Ay)({ number }).gteq(min); // First we scale the number in the range [0-1) number = (number - min) / (max - min); return this.scaleNorm(number, from, to); } scaleNorm(number, from, to) { - if (number > 1 || number < 0) - throw new Error(`Number must be < 1 and > 0, got ${number}`); + (0,src_number/* default */.Ay)({ number }).betweenEq(0, 1); return (number * (to - from)) + from; } - shouldThrowOnMaxRecursionsReached() { + shouldThrowOnMaxRecursionsReached(val) { + if (typeof val === 'boolean') { + this.#shouldThrowOnMaxRecursionsReached = val; + return this; + } + if (typeof this.#shouldThrowOnMaxRecursionsReached !== 'undefined') { + return this.#shouldThrowOnMaxRecursionsReached; + } return THROW_ON_MAX_RECURSIONS_REACHED; } - // Gaussian number between 0 and 1 - normal({ mean, stddev = 1, max, min, skew = 0 } = {}, depth = 0) { - if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { - throw new Error('Max recursive calls to rng normal function. This might be as a result of using predictable random numbers?'); - } - let num = this.boxMuller(); - num = num / 10.0 + 0.5; // Translate to 0 -> 1 - if (depth > MAX_RECURSIONS) { - num = Math.min(Math.max(num, 0), 1); + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + * @throws {@link MaxRecursionsError} If the function recurses too many times in trying to generate in bounds numbers + */ + normal({ mean, stddev, max, min, skew = 0 } = {}, depth = 0) { + if (typeof min === 'undefined' && typeof max === 'undefined') { + return this.gaussian({ mean, stddev, skew }); } - else { - if (num > 1 || num < 0) { - return this.normal({ mean, stddev, max, min, skew }, depth + 1); // resample between 0 and 1 - } + if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError(`Max recursive calls to rng normal function. This might be as a result of using predictable random numbers, or inappropriate arguments? Args: ${JSON.stringify({ mean, stddev, max, min, skew })}`); } + let num = this.bates(7); if (skew < 0) { num = 1 - (Math.pow(num, Math.pow(2, skew))); } else { num = Math.pow(num, Math.pow(2, -skew)); } + if (typeof mean === 'undefined' && + typeof stddev === 'undefined' && + typeof max !== 'undefined' && + typeof min !== 'undefined') { + // This is a simple scaling of the bates distribution. + return this.scaleNorm(num, min, max); + } + num = (num * 10) - 5; if (typeof mean === 'undefined') { mean = 0; if (typeof max !== 'undefined' && typeof min !== 'undefined') { - num *= max - min; - num += min; + mean = (max + min) / 2; + if (typeof stddev === 'undefined') { + stddev = Math.abs(max - min) / 10; + } } - else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + stddev = 0.1; } + num = num * stddev + mean; } else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + if (typeof max !== 'undefined' && typeof min !== 'undefined') { + stddev = Math.abs(max - min) / 10; + } + else { + stddev = 0.1; + } + } num = num * stddev + mean; } if (depth <= MAX_RECURSIONS && ((typeof max !== 'undefined' && num > max) || (typeof min !== 'undefined' && num < min))) { @@ -465,48 +1207,489 @@ class RngAbstract { } return num; } - // Standard Normal variate using Box-Muller transform. + gaussian({ mean = 0, stddev = 1, skew = 0 } = {}) { + (0,src_number/* default */.Ay)({ stddev }).positive(); + if (skew === 0) { + return this.boxMuller({ mean, stddev }); + } + let num = this.boxMuller({ mean: 0, stddev: 1 }); + num = num / 10.0 + 0.5; // Translate to 0 -> 1 + if (skew < 0) { + num = 1 - (Math.pow(num, Math.pow(2, skew))); + } + else { + num = Math.pow(num, Math.pow(2, -skew)); + } + num = num * 10; + num = num - 5; + num = num * stddev + mean; + return num; + } boxMuller(mean = 0, stddev = 1) { + if (typeof mean === 'object') { + ({ mean = 0, stddev = 1 } = mean); + } + (0,src_number/* default */.Ay)({ stddev }).gteq(0); const u = 1 - this._random(); // Converting [0,1) to (0,1] const v = this._random(); const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); // Transform to the desired mean and standard deviation: return z * stddev + mean; } + irwinHall(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().positive(); + let sum = 0; + for (let i = 0; i < n; i++) { + sum += this._random(); + } + return sum; + } + bates(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().positive(); + return this.irwinHall({ n }) / n; + } + batesgaussian(n = 6) { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + (0,src_number/* default */.Ay)({ n }).int().gt(1); + return (this.irwinHall({ n }) / Math.sqrt(n)) - ((1 / Math.sqrt(1 / n)) / 2); + } + bernoulli(p = 0.5) { + if (typeof p === 'object') { + ({ p = 0.5 } = p); + } + (0,src_number/* default */.Ay)({ p }).lteq(1).gteq(0); + return this._random() < p ? 1 : 0; + } + exponential(rate = 1) { + if (typeof rate === 'object') { + ({ rate = 1 } = rate); + } + (0,src_number/* default */.Ay)({ rate }).gt(0); + return -Math.log(1 - this._random()) / rate; + } + pareto({ shape = 0.5, scale = 1, location = 0 } = {}) { + (0,src_number/* default */.Ay)({ shape }).gteq(0); + (0,src_number/* default */.Ay)({ scale }).positive(); + const u = this._random(); + if (shape !== 0) { + return location + (scale / shape) * (Math.pow(u, -shape) - 1); + } + else { + return location - scale * Math.log(u); + } + } + poisson(lambda = 1) { + if (typeof lambda === 'object') { + ({ lambda = 1 } = lambda); + } + (0,src_number/* default */.Ay)({ lambda }).positive(); + const L = Math.exp(-lambda); + let k = 0; + let p = 1; + let i = 0; + const nq = new NonRandomDetector(SAMERANDOM_MAX, 2); + do { + k++; + const r = this._random(); + nq.push(r); + p *= r; + nq.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the poisson distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${r}`); + } while (p > L && i++ < LOOP_MAX); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in poisson - bailing out - possible parameter error, or using non-random source?'); + } + return k - 1; + } + hypergeometric({ N = 50, K = 10, n = 5, k } = {}) { + (0,src_number/* default */.Ay)({ N }).int().positive(); + (0,src_number/* default */.Ay)({ K }).int().positive().lteq(N); + (0,src_number/* default */.Ay)({ n }).int().positive().lteq(N); + if (typeof k === 'undefined') { + k = this.randInt(0, Math.min(K, n)); + } + (0,src_number/* default */.Ay)({ k }).int().betweenEq(0, Math.min(K, n)); + function logFactorial(x) { + let res = 0; + for (let i = 2; i <= x; i++) { + res += Math.log(i); + } + return res; + } + function logCombination(a, b) { + return logFactorial(a) - logFactorial(b) - logFactorial(a - b); + } + const logProb = logCombination(K, k) + logCombination(N - K, n - k) - logCombination(N, n); + return Math.exp(logProb); + } + rademacher() { + return this._random() < 0.5 ? -1 : 1; + } + binomial({ n = 1, p = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ n }).int().positive(); + (0,src_number/* default */.Ay)({ p }).betweenEq(0, 1); + let successes = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + successes++; + } + } + return successes; + } + betaBinomial({ alpha = 1, beta = 1, n = 1 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).positive(); + (0,src_number/* default */.Ay)({ beta }).positive(); + (0,src_number/* default */.Ay)({ n }).int().positive(); + const bd = (alpha, beta) => { + let x = this._random(); + let y = this._random(); + x = Math.pow(x, 1 / alpha); + y = Math.pow(y, 1 / beta); + return x / (x + y); + }; + const p = bd(alpha, beta); + let k = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + k++; + } + } + return k; + } + beta({ alpha = 0.5, beta = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).positive(); + (0,src_number/* default */.Ay)({ beta }).positive(); + const gamma = (alpha) => { + let x = 0; + for (let i = 0; i < alpha; i++) { + const r = this._random(); + x += -Math.log(r); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in beta - bailing out - possible parameter error, or using non-random source?'); + } + } + return x; + }; + const x = gamma(alpha); + const y = gamma(beta); + return x / (x + y); + } + gamma({ shape = 1, rate, scale } = {}) { + (0,src_number/* default */.Ay)({ shape }).positive(); + if (typeof scale !== 'undefined' && typeof rate !== 'undefined' && rate !== 1 / scale) { + throw new Error('Cannot supply rate and scale'); + } + if (typeof scale !== 'undefined') { + (0,src_number/* default */.Ay)({ scale }).positive(); + rate = 1 / scale; + } + if (typeof rate === 'undefined') { + rate = 1; + } + if (rate) { + (0,src_number/* default */.Ay)({ rate }).positive(); + } + let flg; + let x2; + let v0; + let v1; + let x; + let u; + let v = 1; + const d = shape - 1 / 3; + const c = 1.0 / Math.sqrt(9.0 * d); + let i = 0; + flg = true; + const nq1 = new NonRandomDetector(SAMERANDOM_MAX); + while (flg && i++ < LOOP_MAX) { + let j = 0; + const nq2 = new NonRandomDetector(SAMERANDOM_MAX); + do { + x = this.normal(); + nq2.push(x); + nq2.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul ofthe looped way of generating.`); + v = 1.0 + (c * x); + } while (v <= 0.0 && j++ < LOOP_MAX); + if ((j + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma inner loop - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + v *= Math.pow(v, 2); + x2 = Math.pow(x, 2); + v0 = 1.0 - (0.331 * x2 * x2); + v1 = (0.5 * x2) + (d * (1.0 - v + Math.log(v))); + u = this._random(); + nq1.push(u); + nq1.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${u}`); + if (u < v0 || Math.log(u) < v1) { + flg = false; + } + } + if ((i + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + return rate * d * v; + } + studentsT(nu = 1) { + if (typeof nu === 'object') { + ({ nu = 1 } = nu); + } + (0,src_number/* default */.Ay)({ nu }).positive(); + const normal = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + const chiSquared = this.gamma({ shape: nu / 2, rate: 2 }); + return normal / Math.sqrt(chiSquared / nu); + } + wignerSemicircle(R = 1) { + if (typeof R === 'object') { + ({ R = 1 } = R); + } + (0,src_number/* default */.Ay)({ R }).gt(0); + const theta = this._random() * 2 * Math.PI; + return R * Math.cos(theta); + } + kumaraswamy({ alpha = 0.5, beta = 0.5 } = {}) { + (0,src_number/* default */.Ay)({ alpha }).gt(0); + (0,src_number/* default */.Ay)({ beta }).gt(0); + const u = this._random(); + return Math.pow(1 - Math.pow(1 - u, 1 / beta), 1 / alpha); + } + hermite({ lambda1 = 1, lambda2 = 2 } = {}) { + (0,src_number/* default */.Ay)({ lambda1 }).gt(0); + (0,src_number/* default */.Ay)({ lambda2 }).gt(0); + const x1 = this.poisson({ lambda: lambda1 }); + const x2 = this.poisson({ lambda: lambda2 }); + return x1 + x2; + } + chiSquared(k = 1) { + if (typeof k === 'object') { + ({ k = 1 } = k); + } + (0,src_number/* default */.Ay)({ k }).positive().int(); + let sum = 0; + for (let i = 0; i < k; i++) { + const z = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + sum += z * z; + } + return sum; + } + rayleigh(scale = 1) { + if (typeof scale === 'object') { + ({ scale = 1 } = scale); + } + (0,src_number/* default */.Ay)({ scale }).gt(0); + return scale * Math.sqrt(-2 * Math.log(this._random())); + } + logNormal({ mean = 0, stddev = 1 } = {}) { + (0,src_number/* default */.Ay)({ stddev }).gt(0); + const normal = mean + stddev * Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + return Math.exp(normal); + } + cauchy({ median = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random(); + return median + scale * Math.tan(Math.PI * (u - 0.5)); + } + laplace({ mean = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random() - 0.5; + return mean - scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)); + } + logistic({ mean = 0, scale = 1 } = {}) { + (0,src_number/* default */.Ay)({ scale }).gt(0); + const u = this._random(); + return mean + scale * Math.log(u / (1 - u)); + } + /** + * Returns the support of the given distribution. + * + * @see [Wikipedia - Support (mathematics)](https://en.wikipedia.org/wiki/Support_(mathematics)#In_probability_and_measure_theory) + */ + support(distribution) { + const map = { + random: '[min, max)', + integer: '[min, max]', + normal: '(-INF, INF)', + boxMuller: '(-INF, INF)', + gaussian: '(-INF, INF)', + irwinHall: '[0, n]', + bates: '[0, 1]', + batesgaussian: '(-INF, INF)', + bernoulli: '{0, 1}', + exponential: '[0, INF)', + pareto: '[scale, INF)', + poisson: '{1, 2, 3 ...}', + hypergeometric: '{max(0, n+K-N), ..., min(n, K)}', + rademacher: '{-1, 1}', + binomial: '{0, 1, 2, ..., n}', + betaBinomial: '{0, 1, 2, ..., n}', + beta: '(0, 1)', + gamma: '(0, INF)', + studentsT: '(-INF, INF)', + wignerSemicircle: '[-R; +R]', + kumaraswamy: '(0, 1)', + hermite: '{0, 1, 2, 3, ...}', + chiSquared: '[0, INF)', + rayleigh: '[0, INF)', + logNormal: '(0, INF)', + cauchy: '(-INF, +INF)', + laplace: '(-INF, +INF)', + logistic: '(-INF, +INF)', + }; + return map[distribution]; + } chancyInt(input) { if (typeof input === 'number') { return Math.round(input); } + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyInt'); + } + } + let choice = this.choice(input); + if (typeof choice !== 'number') { + choice = parseFloat(choice); + } + return Math.round(choice); + } if (typeof input === 'object') { - input.type = 'integer'; + const type = input.type ?? 'random'; + if (type === 'random') { + input.type = 'integer'; + } + else if (type === 'normal') { + input.type = 'normal_integer'; + } } - return this.chancy(input); + return Math.round(this.chancy(input)); } - chancy(input) { + chancy(input, depth = 0) { + if (depth >= MAX_RECURSIONS) { + if (this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError('Max recursions reached in chancy. Usually a case of badly chosen min/max values.'); + } + else { + return 0; + } + } + if (Array.isArray(input)) { + return this.choice(input); + } if (typeof input === 'string') { return this.dice(input); } if (typeof input === 'object') { + input.type = input.type ?? 'random'; + if (input.type === 'random' || + input.type === 'int' || + input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; + } + } switch (input.type) { - case 'normal': - return this.normal(input); - break; + case 'random': + return this.random(input.min, input.max, input.skew); + case 'int': + case 'integer': + return this.randInt(input.min, input.max, input.skew); case 'normal_integer': + case 'normal_int': return Math.floor(this.normal(input)); - break; - case 'integer': - return this.randInt(input.min ?? 0, input.max ?? 1, input.skew ?? 0); - break; - default: - return this.random(input.min ?? 0, input.max ?? 1, input.skew ?? 0); + case 'dice': + return this.chancyMinMax(this.dice(input.dice ?? input), input, depth); + case 'rademacher': + return this.chancyMinMax(this.rademacher(), input, depth); + case 'normal': + case 'gaussian': + case 'boxMuller': + case 'irwinHall': + case 'bates': + case 'batesgaussian': + case 'bernoulli': + case 'exponential': + case 'pareto': + case 'poisson': + case 'hypergeometric': + case 'binomial': + case 'betaBinomial': + case 'beta': + case 'gamma': + case 'studentsT': + case 'wignerSemicircle': + case 'kumaraswamy': + case 'hermite': + case 'chiSquared': + case 'rayleigh': + case 'logNormal': + case 'cauchy': + case 'laplace': + case 'logistic': + return this.chancyMinMax(this[input.type](input), input, depth); } + throw new Error(`Invalid input type given to chancy: "${input.type}".`); } if (typeof input === 'number') { return input; } throw new Error('Invalid input given to chancy'); } + chancyMinMax(result, input, depth = 0) { + const { min, max } = input; + if ((depth + 1) >= MAX_RECURSIONS && !this.shouldThrowOnMaxRecursionsReached()) { + if (typeof min !== 'undefined') { + result = Math.max(min, result); + } + if (typeof max !== 'undefined') { + result = Math.min(max, result); + } + // always returns something in bounds. + return result; + } + if (typeof min !== 'undefined' && result < min) { + return this.chancy(input, depth + 1); + } + if (typeof max !== 'undefined' && result > max) { + return this.chancy(input, depth + 1); + } + return result; + } + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + chancyMin(input) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMin(input); + } + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + chancyMax(input) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMax(input); + } + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ static chancyMin(input) { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMin array input'); + } + } + return Math.min(...input); + } if (typeof input === 'string') { return this.diceMin(input); } @@ -514,30 +1697,93 @@ class RngAbstract { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } switch (input.type) { + case 'dice': + return this.diceMin(input.dice); case 'normal': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'normal_integer': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'integer': return input.min ?? 0; - break; - default: + case 'random': return input.min ?? 0; + case 'boxMuller': + return Number.NEGATIVE_INFINITY; + case 'gaussian': + return Number.NEGATIVE_INFINITY; + case 'irwinHall': + return 0; + case 'bates': + return 0; + case 'batesgaussian': + return Number.NEGATIVE_INFINITY; + case 'bernoulli': + return 0; + case 'exponential': + return 0; + case 'pareto': + return input.scale ?? 1; + case 'poisson': + return 1; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { N = 50, K = 10, n = 5 } = input; + return Math.max(0, (n + K - N)); + case 'rademacher': + return -1; + case 'binomial': + return 0; + case 'betaBinomial': + return 0; + case 'beta': + return Number.EPSILON; + case 'gamma': + return Number.EPSILON; + case 'studentsT': + return Number.NEGATIVE_INFINITY; + case 'wignerSemicircle': + return -1 * (input.R ?? 10); + case 'kumaraswamy': + return Number.EPSILON; + case 'hermite': + return 0; + case 'chiSquared': + return 0; + case 'rayleigh': + return 0; + case 'logNormal': + return Number.EPSILON; + case 'cauchy': + return Number.NEGATIVE_INFINITY; + case 'laplace': + return Number.NEGATIVE_INFINITY; + case 'logistic': + return Number.NEGATIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMin'); + throw new Error('Invalid input supplied to chancyMin'); } + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ static chancyMax(input) { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMax array input'); + } + } + return Math.max(...input); + } if (typeof input === 'string') { return this.diceMax(input); } @@ -545,40 +1791,94 @@ class RngAbstract { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } switch (input.type) { + case 'dice': + return this.diceMax(input.dice); case 'normal': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'normal_integer': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'integer': return input.max ?? 1; - break; - default: + case 'random': return input.max ?? 1; + case 'boxMuller': + return Number.POSITIVE_INFINITY; + case 'gaussian': + return Number.POSITIVE_INFINITY; + case 'irwinHall': + return (input.n ?? 6); + case 'bates': + return 1; + case 'batesgaussian': + return Number.POSITIVE_INFINITY; + case 'bernoulli': + return 1; + case 'exponential': + return Number.POSITIVE_INFINITY; + case 'pareto': + return Number.POSITIVE_INFINITY; + case 'poisson': + return Number.MAX_SAFE_INTEGER; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { K = 10, n = 5 } = input; + return Math.min(n, K); + case 'rademacher': + return 1; + case 'binomial': + return (input.n ?? 1); + case 'betaBinomial': + return (input.n ?? 1); + case 'beta': + return 1; + case 'gamma': + return Number.POSITIVE_INFINITY; + case 'studentsT': + return Number.POSITIVE_INFINITY; + case 'wignerSemicircle': + return (input.R ?? 10); + case 'kumaraswamy': + return 1; + case 'hermite': + return Number.MAX_SAFE_INTEGER; + case 'chiSquared': + return Number.POSITIVE_INFINITY; + case 'rayleigh': + return Number.POSITIVE_INFINITY; + case 'logNormal': + return Number.POSITIVE_INFINITY; + case 'cauchy': + return Number.POSITIVE_INFINITY; + case 'laplace': + return Number.POSITIVE_INFINITY; + case 'logistic': + return Number.POSITIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMax'); + throw new Error('Invalid input supplied to chancyMax'); } choice(data) { return this.weightedChoice(data); } - /** - * data format: - * { - * choice1: 1, - * choice2: 2, - * choice3: 3, - * } - */ + weights(data) { + const chances = new Map(); + data.forEach(function (a) { + let init = 0; + if (chances.has(a)) { + init = chances.get(a); + } + chances.set(a, init + 1); + }); + return chances; + } weightedChoice(data) { let total = 0; let id; @@ -590,11 +1890,10 @@ class RngAbstract { if (data.length === 1) { return data[0]; } - const chances = new Map(); - data.forEach(function (a) { - chances.set(a, 1); - }); - return this.weightedChoice(chances); + const chances = this.weights(data); + const result = this.weightedChoice(chances); + chances.clear(); + return result; } if (data instanceof Map) { // Some shortcuts @@ -646,6 +1945,9 @@ class RngAbstract { // random >= total, just return the last id. return id; } + pool(entries) { + return new Pool(entries, this); + } static parseDiceArgs(n = 1, d = 6, plus = 0) { if (n === null || typeof n === 'undefined' || arguments.length <= 0) { throw new Error('Dice expects at least one argument'); @@ -658,114 +1960,200 @@ class RngAbstract { [n, d, plus] = n; } else { - d = n.d; - plus = n.plus; - n = n.n; + if (typeof n.n === 'undefined' && + typeof n.d === 'undefined' && + typeof n.plus === 'undefined') { + throw new Error('Invalid input given to dice related function - dice object must have at least one of n, d or plus properties.'); + } + ({ n = 1, d = 6, plus = 0 } = n); } } + (0,src_number/* default */.Ay)({ n }).int(`Expected n to be an integer, got ${n}`); + (0,src_number/* default */.Ay)({ d }).int(`Expected d to be an integer, got ${d}`); return { n, d, plus }; } parseDiceArgs(n = 1, d = 6, plus = 0) { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceArgs(n); } + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ static parseDiceString(string) { // dice string like 5d10+1 if (!diceCache[string]) { + const trimmed = string.replace(/ +/g, ''); + if (/^[+-]*[\d.]+$/.test(trimmed)) { + return { n: 0, d: 0, plus: parseFloat(trimmed) }; + } if (diceRe.test(string)) { - const result = diceRe.exec(string.replace(/ +/g, '')); + const result = diceRe.exec(trimmed); if (result !== null) { diceCache[string] = { - n: (parseInt(result[1]) / 1 || 1), - d: (parseInt(result[2]) / 1 || 1), - plus: (parseFloat(result[3]) / 1 || 0), + n: parseInt(result[1]), + d: parseInt(result[2]), + plus: parseFloat(result[3]), }; + if (Number.isNaN(diceCache[string].n)) { + diceCache[string].n = 1; + } + if (Number.isNaN(diceCache[string].d)) { + diceCache[string].d = 6; + } + if (Number.isNaN(diceCache[string].plus)) { + diceCache[string].plus = 0; + } } } - else if (diceReNoInit.test(string)) { - const result = diceReNoInit.exec(string.replace(/ +/g, '')); - if (result !== null) { - diceCache[string] = { - n: 1, - d: (parseInt(result[1]) / 1 || 1), - plus: (parseFloat(result[2]) / 1 || 0), - }; - } + if (typeof diceCache[string] === 'undefined') { + throw new Error(`Could not parse dice string ${string}`); } } return diceCache[string]; } + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + diceMax(n, d, plus) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMax(n, d, plus); + } + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + diceMin(n, d, plus) { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMin(n, d, plus); + } + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ static diceMax(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return (n * d) + plus; } + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ static diceMin(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return n + plus; } - dice(n = 1, d = 6, plus = 0) { + diceExpanded(n = 1, d = 6, plus = 0) { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); if (typeof n === 'number') { - let nval = Math.max(n, 1); - const dval = Math.max(d, 1); + let nval = n; + const dval = Math.max(d, 0); if (d === 1) { - return plus + 1; + return { dice: Array(n).fill(d), plus, total: (n * d + plus) }; + } + if (n === 0 || d === 0) { + return { dice: [], plus, total: plus }; } - let sum = plus || 0; + const multiplier = nval < 0 ? -1 : 1; + nval *= multiplier; + const results = { dice: [], plus, total: plus }; while (nval > 0) { - sum += this.randInt(1, dval); + results.dice.push(multiplier * this.randInt(1, dval)); nval--; } - return sum; + results.total = sum(results.dice) + plus; + return results; } throw new Error('Invalid arguments given to dice'); } + dice(n, d, plus) { + return this.diceExpanded(n, d, plus).total; + } + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ parseDiceString(string) { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceString(string); } clamp(number, lower, upper) { - if (upper !== undefined) { + if (typeof upper !== 'undefined') { number = number <= upper ? number : upper; } - if (lower !== undefined) { + if (typeof lower !== 'undefined') { number = number >= lower ? number : lower; } return number; } bin(val, bins, min, max) { + (0,src_number/* default */.Ay)({ val }).gt(min).lt(max); const spread = max - min; return (Math.round(((val - min) / spread) * (bins - 1)) / (bins - 1) * spread) + min; } } +/** + * @category Main Class + */ class Rng extends RngAbstract { #mask; #seed = 0; + #randFunc; #m_z = 0; constructor(seed) { super(seed); this.#mask = 0xffffffff; this.#m_z = 987654321; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ + static predictable(seed) { + return new this(seed ?? PREDICTABLE_SEED); + } serialize() { return { - mask: this.#mask, + mask: this.getMask(), seed: this.getSeed(), - m_z: this.#m_z, + m_z: this.getMz(), }; } sameAs(other) { - const s = other.serialize(); - return this.#seed === s.seed && - this.#mask === s.mask && - this.#m_z === s.m_z; + if (other instanceof Rng) { + return this.getRandomSource() === other.getRandomSource() && + this.getSeed() === other.getSeed() && + this.getMask() === other.getMask() && + this.getMz() === other.getMz(); + } + return false; + } + /** @hidden */ + getMask() { + return this.#mask; + } + /** @hidden */ + getMz() { + return this.#m_z; } + /** @hidden */ + setMask(mask) { + this.#mask = mask; + } + /** @hidden */ + setMz(mz) { + this.#m_z = mz; + } + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ static unserialize(serialized) { const rng = new this(); rng.setSeed(serialized.seed); - rng.#mask = serialized.mask; - rng.#seed = serialized.seed; - rng.#m_z = serialized.m_z; + rng.setMask(serialized.mask); + rng.setMz(serialized.m_z); return rng; } seed(i) { @@ -773,7 +2161,7 @@ class Rng extends RngAbstract { this.#m_z = 987654321; return this; } - _random() { + _next() { this.#m_z = (36969 * (this.#m_z & 65535) + (this.#m_z >> 16)) & this.#mask; this.setSeed((18000 * (this.getSeed() & 65535) + (this.getSeed() >> 16)) & this.#mask); let result = ((this.#m_z << 16) + this.getSeed()) & this.#mask; @@ -781,6 +2169,7 @@ class Rng extends RngAbstract { return result + 0.5; } } +/* harmony default export */ const src_rng = (Rng); /***/ }), @@ -794,7 +2183,7 @@ class Rng extends RngAbstract { /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(334); /* harmony import */ var _table_pool__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(425); /* harmony import */ var _table_pool_entry_results__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(673); @@ -829,8 +2218,8 @@ class LootTable { this.pools = pools; this.fn = fn; this.ul = ul; - this.rng = rng ?? (ul ? ul.getRng() : new _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A()); - this.id = id ?? this.rng.uniqstr(6); + this.rng = rng ?? (ul ? ul.getRng() : new _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay()); + this.id = id ?? this.rng.randomString(6); } // Register a function for use in loot pools registerFunction(name, fn) { @@ -999,9 +2388,9 @@ class LootTable { totalWeight += (entry.weight ?? 1); } } - const rollsMax = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMax(pool.rolls); - const rollsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(pool.rolls); - const nullsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(pool.nulls); + const rollsMax = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMax(pool.rolls); + const rollsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(pool.rolls); + const nullsMin = _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(pool.nulls); for (const entry of pool.getEntries()) { if (entry instanceof LootTable || entry.isTable()) { let table; @@ -1029,8 +2418,8 @@ class LootTable { entries.push({ entry, weight: entry.weight / totalWeight, - min: nullsMin > 0 ? 0 : (rollsMin * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMin(entry.qty)), - max: rollsMax * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .A.chancyMax(entry.qty), + min: nullsMin > 0 ? 0 : (rollsMin * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMin(entry.qty)), + max: rollsMax * _rng__WEBPACK_IMPORTED_MODULE_3__/* ["default"] */ .Ay.chancyMax(entry.qty), }); } } @@ -1181,7 +2570,7 @@ class LootTable { /* harmony import */ var _pool_entry_result__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(668); /* harmony import */ var _pool_entry_results__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(784); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(673); @@ -1207,7 +2596,7 @@ class LootPool { this.functions = functions ?? []; this.rolls = rolls; this.nulls = nulls; - this.id = id ?? (new _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A()).uniqstr(6); + this.id = id ?? (new _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .Ay()).randomString(6); this.template = template; if (entries) { for (const entry of entries) { @@ -1444,7 +2833,7 @@ class LootPool { /* harmony export */ }); /* harmony import */ var _log__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(334); /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(784); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(673); /* harmony import */ var _entry_result__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(668); /* harmony import */ var _entry_results__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(219); @@ -1478,7 +2867,7 @@ class LootTableEntry { this.conditions = conditions ?? []; } getRng(rng) { - return rng ?? this.rng ?? (this.rng = new _rng__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .A()); + return rng ?? this.rng ?? (this.rng = new _rng__WEBPACK_IMPORTED_MODULE_2__/* ["default"] */ .Ay()); } setRng(rng) { this.rng = rng; @@ -1754,7 +3143,7 @@ class LootTableEntryResults extends Array { /* harmony import */ var _table__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(784); /* harmony import */ var _table_pool__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(425); /* harmony import */ var _table_pool_entry__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(50); -/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(629); +/* harmony import */ var _rng__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(673); /* harmony import */ var _package_json__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(330); /* harmony import */ var _default_functions__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(663); /* harmony import */ var _default_conditions__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(494); @@ -1892,7 +3281,7 @@ class UltraLoot { 'chanceTo', 'randInt', 'uniqid', - 'uniqstr', + 'randomString', 'randBetween', 'normal', 'chancyInt', @@ -1914,7 +3303,7 @@ class UltraLoot { if (this.isRng(rng)) { return rng; } - const RngConstructor = this.rngConstructor ?? _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .A; + const RngConstructor = this.rngConstructor ?? _rng__WEBPACK_IMPORTED_MODULE_4__/* ["default"] */ .Ay; return new RngConstructor(rng); } registerFunction(name, fn) { @@ -2288,7 +3677,7 @@ class UltraLoot { pools: [] }; clone.pools = []; - const keyToUse = table.filename ?? this.getRng().uniqstr(6); + const keyToUse = table.filename ?? this.getRng().randomString(6); had.add(table); if (includeRng) { clone.rng = table.rng?.serialize() ?? null; @@ -2320,7 +3709,7 @@ class UltraLoot { entryClone.functions = entry.functions; } if (entryClone.item instanceof _table__WEBPACK_IMPORTED_MODULE_1__/* ["default"] */ .A) { - const subKeyToUse = entryClone.item.filename ?? this.getRng().uniqstr(6); + const subKeyToUse = entryClone.item.filename ?? this.getRng().randomString(6); if (had.has(entryClone.item)) { throw new RecursiveTableError('Recursive requirement detected - cannot serialize recursively required tables.'); } @@ -2715,7 +4104,7 @@ module.exports = __WEBPACK_EXTERNAL_createRequire(import.meta.url)("fs"); /***/ 330: /***/ ((module) => { -module.exports = {"rE":"0.1.1"}; +module.exports = {"rE":"0.3.0"}; /***/ }) @@ -2779,31 +4168,93 @@ var __webpack_exports__ = {}; // EXPORTS __webpack_require__.d(__webpack_exports__, { + Bh: () => (/* reexport */ number/* ArrayNumberValidator */.Bh), tS: () => (/* reexport */ src_table/* default */.A), dl: () => (/* reexport */ entry/* default */.A), uX: () => (/* reexport */ result/* default */.A), jQ: () => (/* reexport */ results/* default */.A), o3: () => (/* reexport */ LootTableManager), DW: () => (/* reexport */ pool/* default */.A), - TU: () => (/* reexport */ Rng), + YG: () => (/* reexport */ rng/* MaxRecursionsError */.YG), + Qs: () => (/* reexport */ rng/* NonRandomRandomError */.Qs), + X: () => (/* reexport */ number/* NumberValidationError */.X), + Ol: () => (/* reexport */ number/* NumberValidator */.Ol), + TU: () => (/* reexport */ PredictableRng), Bc: () => (/* reexport */ ultraloot/* RecursiveTableError */.Bc), - Kd: () => (/* reexport */ rng/* default */.A), - Up: () => (/* reexport */ rng/* RngAbstract */.U), + Kd: () => (/* reexport */ rng/* default */.Ay), + Up: () => (/* reexport */ rng/* RngAbstract */.Up), tZ: () => (/* reexport */ ultraloot/* UltraLoot */.tZ), Ay: () => (/* binding */ src) }); // EXTERNAL MODULE: ./src/ultraloot.ts var ultraloot = __webpack_require__(224); -// EXTERNAL MODULE: ./src/rng.ts -var rng = __webpack_require__(629); +// EXTERNAL MODULE: ./src/number.ts +var number = __webpack_require__(623); +// EXTERNAL MODULE: ./src/rng.ts + 2 modules +var rng = __webpack_require__(673); ;// ./src/rng/predictable.ts /** + * * An Rng type that can be used to give predictable results * for testing purposes, and giving known results. + * + * You can set an array of results that will be returned from called to _next() + * + * Note: To avoid unexpected results when using this in place of regular Rng, it is + * only allowed to make the results spread from [0, 1) + * + * The numbers are returned and cycled, so once you reach the end of the list, it will + * just keep on going. + * + * @category Other Rngs + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0]; + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0, 0.5]; + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.0 + * + * @example + * // The setEvenSpread and evenSpread methods can be used to generate + * // n numbers between [0, 1) with even gaps between + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.setEvenSpread(11); + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.5 + * prng.random(); // 0.6 + * prng.random(); // 0.7 + * prng.random(); // 0.8 + * prng.random(); // 0.9 + * prng.random(); // 0.9999999... + * prng.random(); // 0.0 */ -class Rng extends rng/* RngAbstract */.U { +class PredictableRng extends rng/* RngAbstract */.Up { counter = 0; _results = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; constructor(seed, results) { @@ -2843,14 +4294,18 @@ class Rng extends rng/* RngAbstract */.U { return this; } sameAs(other) { - return this.results.sort().join(',') === other.results.sort().join(',') && - this.counter === other.counter; + if (other instanceof PredictableRng) { + return this.results.join(',') === other.results.join(',') && + this.counter === other.counter && + this.getRandomSource() === other.getRandomSource(); + } + return false; } reset() { this.counter = 0; return this; } - _random() { + _next() { return this.results[this.counter++ % this.results.length]; } } @@ -2930,22 +4385,29 @@ var result = __webpack_require__(668); + + // This provides an easy way of using ultraloot in browser. // It can be instantiated by new UltraLoot() and submodules can be // instantiated by new UltraLoot.LootTable() and whatnot. /* harmony default export */ const src = (ultraloot/* UltraLoot */.tZ); +var __webpack_exports__ArrayNumberValidator = __webpack_exports__.Bh; var __webpack_exports__LootTable = __webpack_exports__.tS; var __webpack_exports__LootTableEntry = __webpack_exports__.dl; var __webpack_exports__LootTableEntryResult = __webpack_exports__.uX; var __webpack_exports__LootTableEntryResults = __webpack_exports__.jQ; var __webpack_exports__LootTableManager = __webpack_exports__.o3; var __webpack_exports__LootTablePool = __webpack_exports__.DW; +var __webpack_exports__MaxRecursionsError = __webpack_exports__.YG; +var __webpack_exports__NonRandomRandomError = __webpack_exports__.Qs; +var __webpack_exports__NumberValidationError = __webpack_exports__.X; +var __webpack_exports__NumberValidator = __webpack_exports__.Ol; var __webpack_exports__PredictableRng = __webpack_exports__.TU; var __webpack_exports__RecursiveTableError = __webpack_exports__.Bc; var __webpack_exports__Rng = __webpack_exports__.Kd; var __webpack_exports__RngAbstract = __webpack_exports__.Up; var __webpack_exports__UltraLoot = __webpack_exports__.tZ; var __webpack_exports__default = __webpack_exports__.Ay; -export { __webpack_exports__LootTable as LootTable, __webpack_exports__LootTableEntry as LootTableEntry, __webpack_exports__LootTableEntryResult as LootTableEntryResult, __webpack_exports__LootTableEntryResults as LootTableEntryResults, __webpack_exports__LootTableManager as LootTableManager, __webpack_exports__LootTablePool as LootTablePool, __webpack_exports__PredictableRng as PredictableRng, __webpack_exports__RecursiveTableError as RecursiveTableError, __webpack_exports__Rng as Rng, __webpack_exports__RngAbstract as RngAbstract, __webpack_exports__UltraLoot as UltraLoot, __webpack_exports__default as default }; +export { __webpack_exports__ArrayNumberValidator as ArrayNumberValidator, __webpack_exports__LootTable as LootTable, __webpack_exports__LootTableEntry as LootTableEntry, __webpack_exports__LootTableEntryResult as LootTableEntryResult, __webpack_exports__LootTableEntryResults as LootTableEntryResults, __webpack_exports__LootTableManager as LootTableManager, __webpack_exports__LootTablePool as LootTablePool, __webpack_exports__MaxRecursionsError as MaxRecursionsError, __webpack_exports__NonRandomRandomError as NonRandomRandomError, __webpack_exports__NumberValidationError as NumberValidationError, __webpack_exports__NumberValidator as NumberValidator, __webpack_exports__PredictableRng as PredictableRng, __webpack_exports__RecursiveTableError as RecursiveTableError, __webpack_exports__Rng as Rng, __webpack_exports__RngAbstract as RngAbstract, __webpack_exports__UltraLoot as UltraLoot, __webpack_exports__default as default }; diff --git a/package.json b/package.json index 8212d05..57fcae7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@manticorp/ultraloot", - "version": "0.2.0", + "version": "0.3.0", "description": "", "main": "dist/ultraloot.cjs", "browser": "dist/ultraloot.cjs", @@ -22,7 +22,7 @@ "dev": "webpack --mode development --config webpack.config.js", "prebuild": "rimraf dist", "build": "webpack --mode production --config webpack.config.js && webpack --mode production --config webpack-esm.config.js", - "ci": "npm t && npx typedoc && copyfiles -f ./dist/*.* ./docs/js && npm run test-build", + "ci": "npm t && npx typedoc && npm run test-build && copyfiles -f ./dist/*.* ./docs/js", "test-build": "npm run build && node test-build-common.cjs && node test-build-module.mjs", "test": "jest --coverage", "test-watch": "jest --coverage --watchAll" diff --git a/src/default/conditions.ts b/src/default/conditions.ts index c8dc6cc..a8409e2 100644 --- a/src/default/conditions.ts +++ b/src/default/conditions.ts @@ -1,4 +1,3 @@ -import { LootTableConditionSignature } from './../table'; import { depend } from './../utils'; diff --git a/src/default/functions.ts b/src/default/functions.ts index 58e7a12..705769a 100644 --- a/src/default/functions.ts +++ b/src/default/functions.ts @@ -1,5 +1,4 @@ -import { LootTableFunctionSignature } from './../table'; -import { RngInterface } from './../rng'; +import { RngInterface } from './../rng/interface'; import LootTableEntryResult from './../table/pool/entry/result'; import { dotSet, diff --git a/src/index.ts b/src/index.ts index 136dc2a..d3838d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ export * from './ultraloot'; +export * from './number'; export * from './rng'; +export * from './rng/interface'; export * from './rng/predictable'; export * from './table'; export * from './table/manager'; @@ -9,7 +11,7 @@ export * from './table/pool/entry/result'; export * from './table/pool/entry/results'; import { UltraLoot } from './ultraloot'; -export { UltraLoot as UltraLoot}; +export { UltraLoot }; export { default as Rng } from './rng'; export { default as PredictableRng } from './rng/predictable'; export { default as LootTable } from './table'; diff --git a/src/number.ts b/src/number.ts new file mode 100644 index 0000000..c944aca --- /dev/null +++ b/src/number.ts @@ -0,0 +1,465 @@ +/** + * @category Number Validator + */ +const assert = (truthy : boolean, msg : string = 'Assertion failed') => { + if (!truthy) { + throw new NumberValidationError(msg); + } +}; + +/** + * @category Number Validator + */ +export class NumberValidationError extends Error {} + +/** + * @category Number Validator + */ +export class ArrayNumberValidator { + /** + * The numbers to be validated + */ + #numbers: number[] = []; + + /** + * Descriptive name for this validation + */ + name: string = 'numbers'; + + constructor (numbers : number[], name = 'numbers') { + this.numbers = numbers; + this.name = name; + } + + get numbers () : number[] { + return this.#numbers; + } + + set numbers (numbers: number[]) { + for (const number of numbers) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + } + this.#numbers = numbers; + } + + /** + * Specify the numbers to validate + */ + all (numbers: number[]): this { + this.numbers = numbers; + return this; + } + + /** + * Specify the numbers to validate + */ + validate (numbers : number | number[]) { + if (!Array.isArray(numbers)) { + return new NumberValidator(numbers); + } + return this.all(numbers); + } + + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potatoes = [0, 1]; + * validate(potatoes).varname('potatoes').gt(2); // "Expected every component of potatoes to be > 2, got 0" + */ + varname (name: string): this { + this.name = name; + return this; + } + + /** + * Get the sum of our numbers + */ + sum (): number { + return this.numbers.reduce((a, b) => a + b, 0); + } + + /** + * Validates whether the total of all of our numbers is close to sum, with a maximum difference of diff + * @param sum The sum + * @param diff The maximum difference + * @param msg Error message + * @throws {@link NumberValidationError} If they do not sum close to the correct amount + */ + sumcloseto (sum: number, diff : number = 0.0001, msg?: string): this { + assert(Math.abs(this.sum() - sum) < diff, msg ?? `Expected sum of ${this.name} to be within ${diff} of ${sum}, got ${this.sum()}`); + return this; + } + + /** + * Validates whether the total of all of our numbers is equal (===) to sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to the correct amount + */ + sumto (sum : number, msg?: string): this { + assert(this.sum() === sum, msg ?? `Expected sum of ${this.name} to be equal to ${sum}, got ${this.sum()}`); + return this; + } + + /** + * Validates whether the total of all of our numbers is < sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to < sum + */ + sumtolt (sum : number, msg?: string): this { + assert(this.sum() < sum, msg ?? `Expected sum of ${this.name} to be less than ${sum}, got ${this.sum()}`); + return this; + } + + /** + * Validates whether the total of all of our numbers is > sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to > sum + */ + sumtogt (sum : number, msg?: string): this { + assert(this.sum() > sum, msg ?? `Expected sum of ${this.name} to be greater than ${sum}, got ${this.sum()}`); + return this; + } + + /** + * Validates whether the total of all of our numbers is <= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to <= sum + */ + sumtolteq (sum : number, msg?: string): this { + assert(this.sum() <= sum, msg ?? `Expected sum of ${this.name} to be less than or equal to ${sum}, got ${this.sum()}`); + return this; + } + + /** + * Validates whether the total of all of our numbers is >= sum + * @param sum The sum + * @param msg Error message + * @throws {@link NumberValidationError} If they do not total to >= sum + */ + sumtogteq (sum : number, msg?: string): this { + assert(this.sum() >= sum, msg ?? `Expected sum of ${this.name} to be greater than or equal to ${sum}, got ${this.sum()}`); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all integers + */ + int (msg?: string): this { + this.numbers.forEach(a => validate(a).int(msg ?? `Expected every component of ${this.name} to be an integer, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all positive + */ + positive (msg?: string): this { + this.numbers.forEach(a => validate(a).positive(msg ?? `Expected every component of ${this.name} to be postiive, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all negative + */ + negative (msg?: string): this { + this.numbers.forEach(a => validate(a).negative(msg ?? `Expected every component of ${this.name} to be negative, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all between from and to + */ + between (from: number, to: number, msg?: string): this { + this.numbers.forEach(a => validate(a).between(from, to, msg ?? `Expected every component of ${this.name} to be between ${from} and ${to}, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all between or equal to from and to + */ + betweenEq (from: number, to: number, msg?: string): this { + this.numbers.forEach(a => validate(a).betweenEq(from, to, msg ?? `Expected every component of ${this.name} to be between or equal to ${from} and ${to}, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all > n + */ + gt (n : number, msg?: string): this { + this.numbers.forEach(a => validate(a).gt(n, msg ?? `Expected every component of ${this.name} to be > ${n}, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all >= n + */ + gteq (n : number, msg?: string): this { + this.numbers.forEach(a => validate(a).gteq(n, msg ?? `Expected every component of ${this.name} to be >= ${n}, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all < n + */ + lt (n : number, msg?: string): this { + this.numbers.forEach(a => validate(a).lt(n, msg ?? `Expected every component of ${this.name} to be < ${n}, got ${a}`)); + return this; + } + + /** + * @throws {@link NumberValidationError} if numbers are not all <= n + */ + lteq (n : number, msg?: string): this { + this.numbers.forEach(a => validate(a).lteq(n, msg ?? `Expected every component of ${this.name} to be <= ${n}, got ${a}`)); + return this; + } +} + +/** + * Validate numbers in a fluent fashion. + * + * Each validator method accepts a message as the last parameter + * for customising the error message. + * + * @category Number Validator + * + * @example + * const n = new NumberValidator(); + * n.validate(0).gt(1); // NumberValidationError + * + * @example + * const n = new NumberValidator(); + * const probability = -0.1; + * n.validate(probability).gteq(0, 'Probabilities should always be >= 0'); // NumberValidationError('Probabilities should always be >= 0'). + */ +export class NumberValidator { + /** + * The number being tested. + */ + #number?: number; + + /** + * The name of the variable being validated - shows up in error messages. + */ + name: string = 'number'; + + constructor (number: number = 0, name = 'number') { + this.number = number; + this.name = name; + } + + get number () : number | undefined { + return this.#number; + } + + set number (number: number | undefined) { + assert(typeof number === 'number', `Non-number passed to validator ${number}`); + this.#number = number; + } + + /** + * Returns an ArrayNumberValidator for all the numbers + */ + all (numbers: number[], name?: string): ArrayNumberValidator { + return new ArrayNumberValidator(numbers, name ?? this.name); + } + + assertNumber (num?: number) : num is number { + assert(typeof this.number !== 'undefined', 'No number passed to validator.'); + return true; + } + + /** + * Pass a string decribing the varname to this to make the error messages + * make more sense in your context. + * + * @example + * + * const potato = 1; + * validate(potato).varname('potato').gt(2); // "Expected potato to be greater than 2, got 1" + * @param {string} name [description] + */ + varname (name: string) { + this.name = name; + return this; + } + + /** + * Specify the number to be validated + */ + validate (number : number | number[]) { + if (Array.isArray(number)) { + return this.all(number); + } + this.number = number; + return this; + } + + /** + * Asserts that the number is an integer + * @throws {@link NumberValidationError} if ths number is not an integer + */ + int (msg?: string): this { + if (this.assertNumber(this.number)) assert(Number.isInteger(this.number), msg ?? `Expected ${this.name} to be an integer, got ${this.number}`); + return this; + } + + /** + * Asserts that the number is > 0 + * @throws {@link NumberValidationError} if the number is not positive + */ + positive (msg?: string): this { + return this.gt(0, msg ?? `Expected ${this.name} to be positive, got ${this.number}`); + } + + /** + * Asserts that the number is < 0 + * @throws {@link NumberValidationError} if the number is not negative + */ + negative (msg?: string): this { + return this.lt(0, msg ?? `Expected ${this.name} to be negative, got ${this.number}`); + } + + /** + * Asserts that the from < number < to + * @throws {@link NumberValidationError} if it is outside or equal to those bounds + */ + between (from: number, to: number, msg?: string) { + if (this.assertNumber(this.number)) assert(this.number > from && this.number < to, msg ?? `Expected ${this.name} to be between ${from} and ${to}, got ${this.number}`); + return this; + } + + /** + * Asserts that the from <= number <= to + * @throws {@link NumberValidationError} if it is outside those bounds + */ + betweenEq (from: number, to: number, msg?: string) { + if (this.assertNumber(this.number)) assert(this.number >= from && this.number <= to, msg ?? `Expected ${this.name} to be between or equal to ${from} and ${to}, got ${this.number}`); + return this; + } + + /** + * Asserts that number > n + * @throws {@link NumberValidationError} if it is less than or equal to n + */ + gt (n : number, msg?: string): this { + if (this.assertNumber(this.number)) assert(this.number > n, msg ?? `Expected ${this.name} to be greater than ${n}, got ${this.number}`); + return this; + } + + /** + * Asserts that number >= n + * @throws {@link NumberValidationError} if it is less than n + */ + gteq (n : number, msg?: string): this { + if (this.assertNumber(this.number)) assert(this.number >= n, msg ?? `Expected ${this.name} to be greater than or equal to ${n}, got ${this.number}`); + return this; + } + + /** + * Asserts that number < n + * @throws {@link NumberValidationError} if it is greater than or equal to n + */ + lt (n : number, msg?: string): this { + if (this.assertNumber(this.number)) assert(this.number < n, msg ?? `Expected ${this.name} to be less than ${n}, got ${this.number}`); + return this; + } + + /** + * Asserts that number <= n + * @throws {@link NumberValidationError} if it is greater than n + */ + lteq (n : number, msg?: string): this { + if (this.assertNumber(this.number)) assert(this.number <= n, msg ?? `Expected ${this.name} to be less than or equal to ${n}, got ${this.number}`); + return this; + } +} + +/** + * Validates a number or an array of numbers, with a fluent interface. + * + * If passed an array, it will return an ArrayNumberValidator + * + * If passed anything else, it will return a NumberValidator + * + * You can pass the things in as a one key object, and it will automatically + * set the name for you. + * + * @category Number Validator + * + * @example + * // Validate single numbers + * validate(2).gt(1).lt(3); // doesn't throw + * validate(3).gt(1).lt(3); // throws + * + * @example + * // Validate in the object fashion so it automatically sets the name + * let myVar = 5; + * validate({ myVar }).gt(10); // throws `Expected myVar to be greater than 10, got 5` + * + * @example + * // Also used with arrays of numbers + * validate([1, 2, 3]).lt(10); // doesn't throw + * validate([1, 2, 3]).sumto(6); // doesn't throw + * validate([1, 2, 3, 4]).sumtolt(9); // throws + * + * @example + * // All single number validations + * validate(1).int(); + * validate(1).positive(); + * validate(-1).negative(); + * validate(1).between(0, 2); + * validate(1).betweenEq(1, 2); + * validate(1).gt(0); + * validate(1).gteq(1); + * validate(1).lt(2); + * validate(1).lteq(1); + * + * @example + * // All array of numbers validations + * validate([1, 2, 3]).sumcloseto(6); + * validate([1, 2, 3.0001]).sumcloseto(6, 0.001); + * validate([1, 2, 3]).sumto(6); + * validate([1, 2, 3]).sumtolt(7); + * validate([1, 2, 3]).sumtogt(5); + * validate([1, 2, 3]).sumtolteq(6); + * validate([1, 2, 3]).sumtogteq(1); + * validate([1, 2, 3]).int(); + * validate([1, 2, 3]).positive(); + * validate([-1, -2, -4]).negative(); + * validate([1, 2, 3]).between(0, 4); + * validate([1, 2, 3]).betweenEq(1, 3); + * validate([1, 2, 3]).gt(0); + * validate([1, 2, 3]).gteq(1); + * validate([1, 2, 3]).lt(4); + * validate([1, 2, 3]).lteq(3); + * + * @see {@link NumberValidator} + * @see {@link ArrayNumberValidator} + * @param {number | number[]} number [description] + */ +function validate (number?: Record) : NumberValidator; +function validate (number?: Record) : ArrayNumberValidator; +function validate (number?: number) : NumberValidator; +function validate (number?: number[]) : ArrayNumberValidator; +function validate (number?: number | number[] | Record | Record) { + if (Array.isArray(number)) { + return new ArrayNumberValidator(number); + } else if (typeof number === 'object') { + const entries = Object.entries(number); + if (entries.length === 0) { + throw new Error('Empty object provided'); + } + const [name, value] = entries[0]; + return validate(value).varname(name); + } else { + return new NumberValidator(number); + } +} +export default validate; diff --git a/src/rng.ts b/src/rng.ts index 1157718..2c5b0cd 100644 --- a/src/rng.ts +++ b/src/rng.ts @@ -1,80 +1,29 @@ -const MAX_RECURSIONS = 100; -const THROW_ON_MAX_RECURSIONS_REACHED = true; - -export interface RandomInterface { - random() : number; -} - -export interface DiceInterface { - n: number; - d: number; - plus: number; -} +import validate from './number'; +import Pool from './rng/pool'; +import { NonRandomDetector } from './rng/queue'; +import { ChancyInterface, DiceInterface, Distribution, Chancy, ChancyNumeric, Seed, Randfunc, RngInterface, RngDistributionsInterface } from './rng/interface'; /** - * @interface - * @prop mean Used for "normal" type chancy results to determine the mean - * @prop stddev Used for "normal" type chancy results to determine the stddev - * @prop min The minimum possible result - * @prop max The maximum possible result - * @prop type The type of result, can be "normal", "normal_int", "integer" or "random" - * @prop power The power factor to pass to the random function - basically skews results one way or the other - * @prop skew Skew to use when using a "normal" or "normal_int" distribution + * Safeguard against huge loops. If loops unintentionally grow beyond this + * arbitrary limit, bail out.. */ -export interface ChancyInterface { - mean?: number; - stddev?: number; - min?: number; - max?: number; - type?: string; - skew?: number; -} +const LOOP_MAX = 10000000; -export type Chancy = ChancyInterface | string | number; - -export type Seed = string | number; - -export interface RngInterface { - predictable(seed?: Seed) : RngInterface; - hashStr(str : string) : string | number; - convertStringToNumber(str : string) : number; - getSeed() : number; - sameAs(other: RngInterface) : boolean; - seed(seed : Seed) : this; - percentage() : number; - random(from? : number, to? : number, skew? : number) : number; - chance(n : number, chanceIn? : number) : boolean; - chanceTo(from : number, to : number) : boolean; - randInt(from? : number, to? : number, skew? : number) : number; - uniqid(prefix?: string, random?: boolean) : string; - uniqstr(len?: number) : string; - randBetween(from : number, to : number, skew : number) : number; - normal(args?: NormalArgs) : number; - chancyInt(input : Chancy) : number; - chancy(input : Chancy) : number; - choice(data : Array) : any; - weightedChoice(data : Record | Array | Map) : any; - dice(n : string | DiceInterface | number, d? : number, plus? : number) : number; - parseDiceString(string : string) : DiceInterface; - clamp(number : number, lower : number, upper : number) : number; - bin(val : number, bins : number, min : number, max : number) : number; - serialize() : any; -} +/** + * Safeguard against too much recursion - if a function recurses more than this, + * we know we have a problem. + * + * Max recursion limit is around ~1000 anyway, so would get picked up by interpreter. + */ +const MAX_RECURSIONS = 500; -export interface RngConstructor { - new (seed?:Seed): RngInterface; - unserialize(rng: any): RngInterface; - chancyMin(input : Chancy) : number; - chancyMax(input : Chancy) : number; - parseDiceString(string : string) : DiceInterface; - diceMin(n : string | DiceInterface | number, d? : number, plus? : number) : number; - diceMax(n : string | DiceInterface | number, d? : number, plus? : number) : number; -} +const THROW_ON_MAX_RECURSIONS_REACHED = true; +const PREDICTABLE_SEED = 5789938451; +const SAMERANDOM_MAX = 10; -const diceRe : RegExp = /^ *([0-9]+) *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; -const diceReNoInit : RegExp = /^ *[dD] *([0-9]+) *([+-]? *[0-9]*) *$/; -const strToNumberCache : Record = {}; -const diceCache : Record = {}; +const diceRe: RegExp = /^ *([+-]? *[0-9_]*) *[dD] *([0-9_]+) *([+-]? *[0-9_.]*) *$/; +const strToNumberCache: Record = {}; +const diceCache: Record = {}; export interface SerializedRng { mask: number, @@ -82,30 +31,90 @@ export interface SerializedRng { m_z: number, } -export type NormalArgs = { - mean?: number, - stddev?: number, - max?: number, - min?: number, - skew?: number, - skewtype?: string, -}; +export class MaxRecursionsError extends Error {} +export class NonRandomRandomError extends Error {} -export abstract class RngAbstract implements RngInterface { +function sum (numbers: number[], ...other: []): number; +function sum (...numbers: number[]): number; +function sum (numbersFirstArg: number | number[], ...numbers: number[]): number { + if (Array.isArray(numbersFirstArg)) { + return numbersFirstArg.reduce((a, b) => a + b, 0); + } + return numbers.reduce((a, b) => a + b, 0); +} + +function isNumeric (input: any) { + return (typeof input === 'number') || (!isNaN(parseFloat(input)) && isFinite(input)); +} + +/** + * This abstract class implements most concrete implementations of + * functions, as the only underlying changes are likely to be to the + * uniform random number generation, and how that is handled. + * + * All the typedoc documentation for this has been sharded out to RngInterface + * in a separate file. + */ +export abstract class RngAbstract implements RngInterface, RngDistributionsInterface { #seed: number = 0; - constructor (seed? : Seed) { + #monotonic: number = 0; + #lastuniqid: number = 0; + #randFunc?: Randfunc | null; + #shouldThrowOnMaxRecursionsReached?: boolean; + #distributions: Distribution[] = [ + 'normal', + 'gaussian', + 'boxMuller', + 'irwinHall', + 'bates', + 'batesgaussian', + 'bernoulli', + 'exponential', + 'pareto', + 'poisson', + 'hypergeometric', + 'rademacher', + 'binomial', + 'betaBinomial', + 'beta', + 'gamma', + 'studentsT', + 'wignerSemicircle', + 'kumaraswamy', + 'hermite', + 'chiSquared', + 'rayleigh', + 'logNormal', + 'cauchy', + 'laplace', + 'logistic', + ]; + + constructor (seed?: Seed) { this.setSeed(seed); } - public getSeed () : number { + public getSeed (): number { return this.#seed; } - public sameAs (other : RngAbstract) : boolean { - return this.#seed === other.#seed; + public sameAs (other: RngInterface): boolean { + if (other instanceof RngAbstract) { + return this.#seed === other.#seed && this.#randFunc === other.#randFunc; + } + return false; + } + + public randomSource (source?: Randfunc | null): this { + this.#randFunc = source; + return this; + } + + public getRandomSource () { + return this.#randFunc; } - protected setSeed (seed? : Seed) : this { + protected setSeed (seed?: Seed): this { if (typeof seed !== 'undefined' && seed !== null) { if (typeof seed === 'string') { seed = this.convertStringToNumber(seed); @@ -122,30 +131,38 @@ export abstract class RngAbstract implements RngInterface { return this; } - public serialize () : any { + public serialize (): any { return { seed: this.#seed, }; } - public static unserialize (serialized : SerializedRng) : RngInterface { + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ + public static unserialize (serialized: SerializedRng): RngInterface { const { constructor } = Object.getPrototypeOf(this); const rng = new constructor(serialized.seed); rng.setSeed(serialized.seed); return rng; } - public predictable (seed? : Seed) : RngInterface { + public predictable (seed?: Seed): RngInterface { const { constructor } = Object.getPrototypeOf(this); - const newSelf : RngInterface = new constructor(seed); + const newSelf: RngInterface = new constructor(seed ?? PREDICTABLE_SEED); return newSelf; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ public static predictable(this: new (seed: Seed) => T, seed: Seed): T { - return new this(seed); + return new this(seed ?? PREDICTABLE_SEED); } - public hashStr (str : string) : number { + protected hashStr (str: string): number { let hash = 0; let i; let chr; @@ -158,7 +175,7 @@ export abstract class RngAbstract implements RngInterface { return hash; } - public convertStringToNumber (str : string) : number { + protected convertStringToNumber (str: string): number { if (strToNumberCache[str]) { return strToNumberCache[str]; } @@ -168,28 +185,52 @@ export abstract class RngAbstract implements RngInterface { } protected _random (): number { - return Math.random(); + if (typeof this.#randFunc === 'function') { + return this.#randFunc(); + } + return this._next(); } - public percentage () : number { + /** + * Internal source of uniformly distributed random numbers between 0 and 1, [0, 1) + * + * Simplest implementation would be Math.random() + */ + protected abstract _next (): number; + + public percentage (): number { return this.randBetween(0, 100); } - public random (from : number = 0, to : number = 1, skew : number = 0) : number { + public probability (): number { + return this.randBetween(0, 1); + } + + public random (from: number = 0, to: number = 1, skew: number = 0): number { return this.randBetween(from, to, skew); } - public chance (n : number, chanceIn : number = 1) : boolean { + public chance (n: number, chanceIn: number = 1): boolean { + validate({ chanceIn }).positive(); + validate({ n }).positive(); + const chance = n / chanceIn; return this._random() <= chance; } // 500 to 1 chance, for example - public chanceTo (from : number, to : number) : boolean { - return this._random() <= (from / (from + to)); + public chanceTo (from: number, to: number): boolean { + return this.chance(from, from + to); } - public randInt (from = 0, to = 1, skew = 0) : number { + public randInt (from: number = 0, to: number = 1, skew: number = 0): number { + validate({ from }).int(); + validate({ to }).int(); + + if (from === to) { + return from; + } + [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -200,16 +241,23 @@ export abstract class RngAbstract implements RngInterface { return Math.floor(rand * ((to + 1) - from)) + from; } - // Not deterministic - public uniqid (prefix : string = '', random : boolean = false) : string { - const sec = Date.now() * 1000 + Math.random() * 1000; + public uniqid (prefix: string = ''): string { + const now = Date.now() * 1000; + if (this.#lastuniqid === now) { + this.#monotonic++; + } else { + this.#monotonic = Math.round(this._random() * 100); + } + const sec = now + this.#monotonic; const id = sec.toString(16).replace(/\./g, '').padEnd(14, '0'); - return `${prefix}${id}${random ? `.${Math.trunc(Math.random() * 100000000)}` : ''}`; + this.#lastuniqid = now; + return `${prefix}${id}`; } - // Deterministic - public uniqstr (len: number = 6) : string { - const str : string[] = []; + public randomString (len: number = 6): string { + validate({ len }).gt(0); + + const str: string[] = []; const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const alen = 61; for (let i = 0; i < len; i++) { @@ -218,7 +266,10 @@ export abstract class RngAbstract implements RngInterface { return str.join(''); } - public randBetween (from : number = 0, to : number = 1, skew : number = 0): number { + public randBetween (from: number = 0, to?: number, skew: number = 0): number { + if (typeof to === 'undefined') { + to = from + 1; + } [from, to] = [Math.min(from, to), Math.max(from, to)]; let rand = this._random(); if (skew < 0) { @@ -230,36 +281,68 @@ export abstract class RngAbstract implements RngInterface { } public scale (number: number, from: number, to: number, min: number = 0, max: number = 1): number { - if (number > max) throw new Error(`Number ${number} is greater than max of ${max}`); - if (number < min) throw new Error(`Number ${number} is less than min of ${min}`); + validate({ number }).lteq(max); + validate({ number }).gteq(min); + // First we scale the number in the range [0-1) number = (number - min) / (max - min); return this.scaleNorm(number, from, to); } public scaleNorm (number: number, from: number, to: number): number { - if (number > 1 || number < 0) throw new Error(`Number must be < 1 and > 0, got ${number}`); + validate({ number }).betweenEq(0, 1); + return (number * (to - from)) + from; } - public shouldThrowOnMaxRecursionsReached (): boolean { + public shouldThrowOnMaxRecursionsReached (): boolean; + public shouldThrowOnMaxRecursionsReached (val: boolean): this; + public shouldThrowOnMaxRecursionsReached (val?: boolean): boolean | this { + if (typeof val === 'boolean') { + this.#shouldThrowOnMaxRecursionsReached = val; + return this; + } + if (typeof this.#shouldThrowOnMaxRecursionsReached !== 'undefined') { + return this.#shouldThrowOnMaxRecursionsReached; + } return THROW_ON_MAX_RECURSIONS_REACHED; } - // Gaussian number between 0 and 1 - public normal ({ mean, stddev = 1, max, min, skew = 0 } : NormalArgs = {}, depth = 0): number { - if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { - throw new Error('Max recursive calls to rng normal function. This might be as a result of using predictable random numbers?'); + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + * @throws {@link MaxRecursionsError} If the function recurses too many times in trying to generate in bounds numbers + */ + public normal ({ mean, stddev, max, min, skew = 0 }: { mean?: number, stddev?: number, max?: number, min?: number, skew?: number } = {}, depth = 0): number { + if (typeof min === 'undefined' && typeof max === 'undefined') { + return this.gaussian({ mean, stddev, skew }); } - let num = this.boxMuller(); - num = num / 10.0 + 0.5; // Translate to 0 -> 1 - if (depth > MAX_RECURSIONS) { - num = Math.min(Math.max(num, 0), 1); - } else { - if (num > 1 || num < 0) { - return this.normal({ mean, stddev, max, min, skew }, depth + 1); // resample between 0 and 1 - } + if (depth > MAX_RECURSIONS && this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError(`Max recursive calls to rng normal function. This might be as a result of using predictable random numbers, or inappropriate arguments? Args: ${JSON.stringify({ mean, stddev, max, min, skew })}`); } + let num = this.bates(7); if (skew < 0) { num = 1 - (Math.pow(num, Math.pow(2, skew))); @@ -267,18 +350,37 @@ export abstract class RngAbstract implements RngInterface { num = Math.pow(num, Math.pow(2, -skew)); } + if ( + typeof mean === 'undefined' && + typeof stddev === 'undefined' && + typeof max !== 'undefined' && + typeof min !== 'undefined' + ) { + // This is a simple scaling of the bates distribution. + return this.scaleNorm(num, min, max); + } + + num = (num * 10) - 5; if (typeof mean === 'undefined') { mean = 0; if (typeof max !== 'undefined' && typeof min !== 'undefined') { - num *= max - min; - num += min; - } else { - num = num * 10; - num = num - 5; + mean = (max + min) / 2; + if (typeof stddev === 'undefined') { + stddev = Math.abs(max - min) / 10; + } + } + if (typeof stddev === 'undefined') { + stddev = 0.1; } + num = num * stddev + mean; } else { - num = num * 10; - num = num - 5; + if (typeof stddev === 'undefined') { + if (typeof max !== 'undefined' && typeof min !== 'undefined') { + stddev = Math.abs(max - min) / 10; + } else { + stddev = 0.1; + } + } num = num * stddev + mean; } @@ -300,8 +402,34 @@ export abstract class RngAbstract implements RngInterface { return num; } - // Standard Normal variate using Box-Muller transform. - public boxMuller (mean : number = 0, stddev : number = 1) : number { + public gaussian ({ mean = 0, stddev = 1, skew = 0 } : { mean?: number, stddev?: number, skew?: number } = {}): number { + validate({ stddev }).positive(); + + if (skew === 0) { + return this.boxMuller({ mean, stddev }); + } + + let num = this.boxMuller({ mean: 0, stddev: 1 }); + num = num / 10.0 + 0.5; // Translate to 0 -> 1 + + if (skew < 0) { + num = 1 - (Math.pow(num, Math.pow(2, skew))); + } else { + num = Math.pow(num, Math.pow(2, -skew)); + } + + num = num * 10; + num = num - 5; + num = num * stddev + mean; + + return num; + } + + public boxMuller (mean: number | { mean?: number, stddev?: number } = 0, stddev: number = 1): number { + if (typeof mean === 'object') { + ({ mean = 0, stddev = 1 } = mean); + } + validate({ stddev }).gteq(0); const u = 1 - this._random(); // Converting [0,1) to (0,1] const v = this._random(); const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); @@ -309,42 +437,474 @@ export abstract class RngAbstract implements RngInterface { return z * stddev + mean; } - public chancyInt (input : Chancy) : number { + public irwinHall (n: number | { n?: number } = 6): number { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + validate({ n }).int().positive(); + let sum = 0; + for (let i = 0; i < n; i++) { + sum += this._random(); + } + return sum; + } + + public bates (n: number | { n?: number } = 6): number { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + validate({ n }).int().positive(); + + return this.irwinHall({ n }) / n; + } + + public batesgaussian (n: number | { n?: number } = 6): number { + if (typeof n === 'object') { + ({ n = 6 } = n); + } + validate({ n }).int().gt(1); + + return (this.irwinHall({ n }) / Math.sqrt(n)) - ((1 / Math.sqrt(1 / n)) / 2); + } + + public bernoulli (p: number | { p?: number } = 0.5): number { + if (typeof p === 'object') { + ({ p = 0.5 } = p); + } + validate({ p }).lteq(1).gteq(0); + + return this._random() < p ? 1 : 0; + } + + public exponential (rate: number | { rate?: number } = 1): number { + if (typeof rate === 'object') { + ({ rate = 1 } = rate); + } + validate({ rate }).gt(0); + return -Math.log(1 - this._random()) / rate; + } + + public pareto ({ shape = 0.5, scale = 1, location = 0 }: { shape?: number, scale?: number, location?: number } = {}): number { + validate({ shape }).gteq(0); + validate({ scale }).positive(); + + const u = this._random(); + if (shape !== 0) { + return location + (scale / shape) * (Math.pow(u, -shape) - 1); + } else { + return location - scale * Math.log(u); + } + } + + public poisson (lambda: number | { lambda?: number } = 1): number { + if (typeof lambda === 'object') { + ({ lambda = 1 } = lambda); + } + validate({ lambda }).positive(); + + const L = Math.exp(-lambda); + let k = 0; + let p = 1; + let i = 0; + const nq = new NonRandomDetector(SAMERANDOM_MAX, 2); + + do { + k++; + const r = this._random(); + nq.push(r); + p *= r; + nq.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the poisson distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${r}`); + } while (p > L && i++ < LOOP_MAX); + + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in poisson - bailing out - possible parameter error, or using non-random source?'); + } + + return k - 1; + } + + public hypergeometric ({ N = 50, K = 10, n = 5, k }: { N?: number, K?: number, n?: number, k?: number } = {}): number { + validate({ N }).int().positive(); + validate({ K }).int().positive().lteq(N); + validate({ n }).int().positive().lteq(N); + + if (typeof k === 'undefined') { + k = this.randInt(0, Math.min(K, n)); + } + validate({ k }).int().betweenEq(0, Math.min(K, n)); + + function logFactorial (x: number): number { + let res = 0; + for (let i = 2; i <= x; i++) { + res += Math.log(i); + } + return res; + } + + function logCombination (a: number, b: number): number { + return logFactorial(a) - logFactorial(b) - logFactorial(a - b); + } + + const logProb = logCombination(K, k) + logCombination(N - K, n - k) - logCombination(N, n); + return Math.exp(logProb); + } + + public rademacher (): -1 | 1 { + return this._random() < 0.5 ? -1 : 1; + } + + public binomial ({ n = 1, p = 0.5 }: { n?: number, p?: number } = {}): number { + validate({ n }).int().positive(); + validate({ p }).betweenEq(0, 1); + + let successes = 0; + for (let i = 0; i < n; i++) { + if (this._random() < p) { + successes++; + } + } + return successes; + } + + public betaBinomial ({ alpha = 1, beta = 1, n = 1 }: { alpha?: number, beta?: number, n?: number } = {}): number { + validate({ alpha }).positive(); + validate({ beta }).positive(); + validate({ n }).int().positive(); + + const bd = (alpha: number, beta: number): number => { + let x = this._random(); + let y = this._random(); + x = Math.pow(x, 1 / alpha); + y = Math.pow(y, 1 / beta); + return x / (x + y); + }; + + const p = bd(alpha, beta); + let k = 0; + + for (let i = 0; i < n; i++) { + if (this._random() < p) { + k++; + } + } + return k; + } + + public beta ({ alpha = 0.5, beta = 0.5 }: { alpha?: number, beta?: number } = {}): number { + validate({ alpha }).positive(); + validate({ beta }).positive(); + + const gamma = (alpha: number): number => { + let x = 0; + for (let i = 0; i < alpha; i++) { + const r = this._random(); + x += -Math.log(r); + if ((i + 1) >= LOOP_MAX) { + throw new Error('LOOP_MAX reached in beta - bailing out - possible parameter error, or using non-random source?'); + } + } + return x; + }; + + const x = gamma(alpha); + const y = gamma(beta); + return x / (x + y); + } + + public gamma ({ shape = 1, rate, scale }: { shape?: number, rate?: number, scale?: number } = {}): number { + validate({ shape }).positive(); + if (typeof scale !== 'undefined' && typeof rate !== 'undefined' && rate !== 1 / scale) { + throw new Error('Cannot supply rate and scale'); + } + if (typeof scale !== 'undefined') { + validate({ scale }).positive(); + rate = 1 / scale; + } + if (typeof rate === 'undefined') { + rate = 1; + } + if (rate) { + validate({ rate }).positive(); + } + let flg; + let x2; + let v0; + let v1; + let x; + let u; + let v = 1; + const d = shape - 1 / 3; + const c = 1.0 / Math.sqrt(9.0 * d); + let i = 0; + + flg = true; + const nq1 = new NonRandomDetector(SAMERANDOM_MAX); + + while (flg && i++ < LOOP_MAX) { + let j = 0; + const nq2 = new NonRandomDetector(SAMERANDOM_MAX); + do { + x = this.normal(); + nq2.push(x); + nq2.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul ofthe looped way of generating.`); + v = 1.0 + (c * x); + } while (v <= 0.0 && j++ < LOOP_MAX); + if ((j + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma inner loop - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + + v *= Math.pow(v, 2); + x2 = Math.pow(x, 2); + + v0 = 1.0 - (0.331 * x2 * x2); + v1 = (0.5 * x2) + (d * (1.0 - v + Math.log(v))); + + u = this._random(); + nq1.push(u); + nq1.detectLoop(`Loop detected in randomly generated numbers over the last ${SAMERANDOM_MAX} generations. This is incompatible with the gamma distribution. Try either using a spread of non-random numbers or fine tune the number to not fall foul of the looped way of generating. Last random number was ${u}`); + if (u < v0 || Math.log(u) < v1) { + flg = false; + } + } + if ((i + 1) >= LOOP_MAX) { + throw new Error(`LOOP_MAX reached inside gamma - bailing out - possible parameter error, or using non-random source? had shape = ${shape}, rate = ${rate}, scale = ${scale}`); + } + return rate * d * v; + } + + public studentsT (nu: number | { nu?: number } = 1): number { + if (typeof nu === 'object') { + ({ nu = 1 } = nu); + } + validate({ nu }).positive(); + + const normal = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + const chiSquared = this.gamma({ shape: nu / 2, rate: 2 }); + return normal / Math.sqrt(chiSquared / nu); + } + + public wignerSemicircle (R: number | { R?: number } = 1): number { + if (typeof R === 'object') { + ({ R = 1 } = R); + } + validate({ R }).gt(0); + + const theta = this._random() * 2 * Math.PI; + return R * Math.cos(theta); + } + + public kumaraswamy ({ alpha = 0.5, beta = 0.5 }: { alpha?: number, beta?: number } = {}): number { + validate({ alpha }).gt(0); + validate({ beta }).gt(0); + + const u = this._random(); + return Math.pow(1 - Math.pow(1 - u, 1 / beta), 1 / alpha); + } + + public hermite ({ lambda1 = 1, lambda2 = 2 }: { lambda1?: number, lambda2?: number } = {}): number { + validate({ lambda1 }).gt(0); + validate({ lambda2 }).gt(0); + + const x1 = this.poisson({ lambda: lambda1 }); + const x2 = this.poisson({ lambda: lambda2 }); + + return x1 + x2; + } + + public chiSquared (k: number | { k?: number } = 1): number { + if (typeof k === 'object') { + ({ k = 1 } = k); + } + validate({ k }).positive().int(); + + let sum = 0; + for (let i = 0; i < k; i++) { + const z = Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + sum += z * z; + } + return sum; + } + + public rayleigh (scale: number | { scale?: number } = 1): number { + if (typeof scale === 'object') { + ({ scale = 1 } = scale); + } + validate({ scale }).gt(0); + return scale * Math.sqrt(-2 * Math.log(this._random())); + } + + public logNormal ({ mean = 0, stddev = 1 } : { mean?: number, stddev?: number } = {}): number { + validate({ stddev }).gt(0); + + const normal = mean + stddev * Math.sqrt(-2.0 * Math.log(this._random())) * Math.cos(2.0 * Math.PI * this._random()); + return Math.exp(normal); + } + + public cauchy ({ median = 0, scale = 1 } : { median?: number, scale?: number } = {}): number { + validate({ scale }).gt(0); + + const u = this._random(); + return median + scale * Math.tan(Math.PI * (u - 0.5)); + } + + public laplace ({ mean = 0, scale = 1 } : { mean?: number, scale?: number } = {}): number { + validate({ scale }).gt(0); + + const u = this._random() - 0.5; + return mean - scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u)); + } + + public logistic ({ mean = 0, scale = 1 } : { mean?: number, scale?: number } = {}): number { + validate({ scale }).gt(0); + + const u = this._random(); + return mean + scale * Math.log(u / (1 - u)); + } + + /** + * Returns the support of the given distribution. + * + * @see [Wikipedia - Support (mathematics)](https://en.wikipedia.org/wiki/Support_(mathematics)#In_probability_and_measure_theory) + */ + public support (distribution: Distribution): string | undefined { + const map : Record = { + random: '[min, max)', + integer: '[min, max]', + normal: '(-INF, INF)', + boxMuller: '(-INF, INF)', + gaussian: '(-INF, INF)', + irwinHall: '[0, n]', + bates: '[0, 1]', + batesgaussian: '(-INF, INF)', + bernoulli: '{0, 1}', + exponential: '[0, INF)', + pareto: '[scale, INF)', + poisson: '{1, 2, 3 ...}', + hypergeometric: '{max(0, n+K-N), ..., min(n, K)}', + rademacher: '{-1, 1}', + binomial: '{0, 1, 2, ..., n}', + betaBinomial: '{0, 1, 2, ..., n}', + beta: '(0, 1)', + gamma: '(0, INF)', + studentsT: '(-INF, INF)', + wignerSemicircle: '[-R; +R]', + kumaraswamy: '(0, 1)', + hermite: '{0, 1, 2, 3, ...}', + chiSquared: '[0, INF)', + rayleigh: '[0, INF)', + logNormal: '(0, INF)', + cauchy: '(-INF, +INF)', + laplace: '(-INF, +INF)', + logistic: '(-INF, +INF)', + }; + return map[distribution]; + } + + public chancyInt (input: Chancy): number { if (typeof input === 'number') { return Math.round(input); } + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyInt'); + } + } + let choice = this.choice(input); + if (typeof choice !== 'number') { + choice = parseFloat(choice); + } + return Math.round(choice); + } if (typeof input === 'object') { - input.type = 'integer'; + const type = input.type ?? 'random'; + if (type === 'random') { + input.type = 'integer'; + } else if (type === 'normal') { + input.type = 'normal_integer'; + } } - return this.chancy(input); + return Math.round(this.chancy(input)); } - public chancy (input : Chancy) : number { + public chancy(input: T[], depth?: number): T; + public chancy (input: ChancyNumeric, depth?: number): number; + public chancy(input: Chancy, depth = 0): any { + if (depth >= MAX_RECURSIONS) { + if (this.shouldThrowOnMaxRecursionsReached()) { + throw new MaxRecursionsError('Max recursions reached in chancy. Usually a case of badly chosen min/max values.'); + } else { + return 0; + } + } + if (Array.isArray(input)) { + return this.choice(input); + } if (typeof input === 'string') { return this.dice(input); } if (typeof input === 'object') { + input.type = input.type ?? 'random'; + + if ( + input.type === 'random' || + input.type === 'int' || + input.type === 'integer' + ) { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; + } + } + switch (input.type) { - case 'normal': - return this.normal(input); - break; - case 'normal_integer': - return Math.floor(this.normal(input)); - break; + case 'random': + return this.random( + input.min, + input.max, + input.skew + ); + case 'int': case 'integer': return this.randInt( - input.min ?? 0, - input.max ?? 1, - input.skew ?? 0 - ); - break; - default: - return this.random( - input.min ?? 0, - input.max ?? 1, - input.skew ?? 0 + input.min, + input.max, + input.skew ); + case 'normal_integer': + case 'normal_int': + return Math.floor(this.normal(input)); + case 'dice': + return this.chancyMinMax(this.dice(input.dice ?? input), input, depth); + case 'rademacher': + return this.chancyMinMax(this.rademacher(), input, depth); + case 'normal': + case 'gaussian': + case 'boxMuller': + case 'irwinHall': + case 'bates': + case 'batesgaussian': + case 'bernoulli': + case 'exponential': + case 'pareto': + case 'poisson': + case 'hypergeometric': + case 'binomial': + case 'betaBinomial': + case 'beta': + case 'gamma': + case 'studentsT': + case 'wignerSemicircle': + case 'kumaraswamy': + case 'hermite': + case 'chiSquared': + case 'rayleigh': + case 'logNormal': + case 'cauchy': + case 'laplace': + case 'logistic': + return this.chancyMinMax(this[input.type](input), input, depth); } + throw new Error(`Invalid input type given to chancy: "${input.type}".`); } if (typeof input === 'number') { return input; @@ -352,7 +912,58 @@ export abstract class RngAbstract implements RngInterface { throw new Error('Invalid input given to chancy'); } - public static chancyMin (input : Chancy) : number { + private chancyMinMax (result: number, input: ChancyInterface, depth : number = 0) { + const { min, max } = input; + if ((depth + 1) >= MAX_RECURSIONS && !this.shouldThrowOnMaxRecursionsReached()) { + if (typeof min !== 'undefined') { + result = Math.max(min, result); + } + if (typeof max !== 'undefined') { + result = Math.min(max, result); + } + // always returns something in bounds. + return result; + } + if (typeof min !== 'undefined' && result < min) { + return this.chancy(input, depth + 1); + } + if (typeof max !== 'undefined' && result > max) { + return this.chancy(input, depth + 1); + } + return result; + } + + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + public chancyMin (input: Chancy): number { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMin(input); + } + + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + public chancyMax (input: Chancy): number { + const { constructor } = Object.getPrototypeOf(this); + return constructor.chancyMax(input); + } + + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + public static chancyMin (input: Chancy): number { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMin array input'); + } + } + return Math.min(...input); + } if (typeof input === 'string') { return this.diceMin(input); } @@ -360,31 +971,96 @@ export abstract class RngAbstract implements RngInterface { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } + switch (input.type) { + case 'dice': + return this.diceMin(input.dice); case 'normal': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'normal_integer': return input.min ?? Number.NEGATIVE_INFINITY; - break; case 'integer': return input.min ?? 0; - break; - default: + case 'random': return input.min ?? 0; + case 'boxMuller': + return Number.NEGATIVE_INFINITY; + case 'gaussian': + return Number.NEGATIVE_INFINITY; + case 'irwinHall': + return 0; + case 'bates': + return 0; + case 'batesgaussian': + return Number.NEGATIVE_INFINITY; + case 'bernoulli': + return 0; + case 'exponential': + return 0; + case 'pareto': + return input.scale ?? 1; + case 'poisson': + return 1; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { N = 50, K = 10, n = 5 } = input; + return Math.max(0, (n + K - N)); + case 'rademacher': + return -1; + case 'binomial': + return 0; + case 'betaBinomial': + return 0; + case 'beta': + return Number.EPSILON; + case 'gamma': + return Number.EPSILON; + case 'studentsT': + return Number.NEGATIVE_INFINITY; + case 'wignerSemicircle': + return -1 * (input.R ?? 10); + case 'kumaraswamy': + return Number.EPSILON; + case 'hermite': + return 0; + case 'chiSquared': + return 0; + case 'rayleigh': + return 0; + case 'logNormal': + return Number.EPSILON; + case 'cauchy': + return Number.NEGATIVE_INFINITY; + case 'laplace': + return Number.NEGATIVE_INFINITY; + case 'logistic': + return Number.NEGATIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMin'); + throw new Error('Invalid input supplied to chancyMin'); } - public static chancyMax (input : Chancy) : number { + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + public static chancyMax (input: Chancy): number { + if (Array.isArray(input)) { + for (const el of input) { + if (!isNumeric(el)) { + throw new Error('Cannot pass non-numbers to chancyMax array input'); + } + } + return Math.max(...input); + } if (typeof input === 'string') { return this.diceMax(input); } @@ -392,43 +1068,100 @@ export abstract class RngAbstract implements RngInterface { return input; } if (typeof input === 'object') { - if (typeof input.type === 'undefined') { - if (typeof input.skew !== 'undefined') { - // Regular random numbers are evenly distributed, so skew - // only makes sense on normal numbers - input.type = 'normal'; + input.type = input.type ?? 'random'; + + if (input.type === 'random' || input.type === 'integer') { + if (typeof input.min !== 'undefined' && typeof input.max === 'undefined') { + input.max = Number.MAX_SAFE_INTEGER; } } + switch (input.type) { + case 'dice': + return this.diceMax(input.dice); case 'normal': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'normal_integer': return input.max ?? Number.POSITIVE_INFINITY; - break; case 'integer': return input.max ?? 1; - break; - default: + case 'random': return input.max ?? 1; + case 'boxMuller': + return Number.POSITIVE_INFINITY; + case 'gaussian': + return Number.POSITIVE_INFINITY; + case 'irwinHall': + return (input.n ?? 6); + case 'bates': + return 1; + case 'batesgaussian': + return Number.POSITIVE_INFINITY; + case 'bernoulli': + return 1; + case 'exponential': + return Number.POSITIVE_INFINITY; + case 'pareto': + return Number.POSITIVE_INFINITY; + case 'poisson': + return Number.MAX_SAFE_INTEGER; + case 'hypergeometric': + // eslint-disable-next-line no-case-declarations + const { K = 10, n = 5 } = input; + return Math.min(n, K); + case 'rademacher': + return 1; + case 'binomial': + return (input.n ?? 1); + case 'betaBinomial': + return (input.n ?? 1); + case 'beta': + return 1; + case 'gamma': + return Number.POSITIVE_INFINITY; + case 'studentsT': + return Number.POSITIVE_INFINITY; + case 'wignerSemicircle': + return (input.R ?? 10); + case 'kumaraswamy': + return 1; + case 'hermite': + return Number.MAX_SAFE_INTEGER; + case 'chiSquared': + return Number.POSITIVE_INFINITY; + case 'rayleigh': + return Number.POSITIVE_INFINITY; + case 'logNormal': + return Number.POSITIVE_INFINITY; + case 'cauchy': + return Number.POSITIVE_INFINITY; + case 'laplace': + return Number.POSITIVE_INFINITY; + case 'logistic': + return Number.POSITIVE_INFINITY; } + throw new Error(`Invalid input type ${input.type}.`); } - throw new Error('Invalid input given to chancyMax'); + throw new Error('Invalid input supplied to chancyMax'); } - public choice (data : Array) : any { + public choice (data: Array): any { return this.weightedChoice(data); } - /** - * data format: - * { - * choice1: 1, - * choice2: 2, - * choice3: 3, - * } - */ - public weightedChoice (data : Record | Array | Map) : any { + public weights (data: Array): Map { + const chances: Map = new Map(); + data.forEach(function (a) { + let init = 0; + if (chances.has(a)) { + init = (chances.get(a) as number); + } + chances.set(a, init + 1); + }); + return chances; + } + + public weightedChoice (data: Record | Array | Map): any { let total = 0; let id; if (Array.isArray(data)) { @@ -439,11 +1172,10 @@ export abstract class RngAbstract implements RngInterface { if (data.length === 1) { return data[0]; } - const chances : Map = new Map(); - data.forEach(function (a) { - chances.set(a, 1); - }); - return this.weightedChoice(chances); + const chances = this.weights(data); + const result = this.weightedChoice(chances); + chances.clear(); + return result; } if (data instanceof Map) { @@ -497,7 +1229,11 @@ export abstract class RngAbstract implements RngInterface { return id; } - protected static parseDiceArgs (n : string | DiceInterface | number | number[] = 1, d: number = 6, plus: number = 0) : DiceInterface { + public pool(entries?: T[]): Pool { + return new Pool(entries, this); + } + + protected static parseDiceArgs (n: string | Partial | number | number[] = 1, d: number = 6, plus: number = 0): DiceInterface { if (n === null || typeof n === 'undefined' || arguments.length <= 0) { throw new Error('Dice expects at least one argument'); } @@ -508,135 +1244,234 @@ export abstract class RngAbstract implements RngInterface { if (Array.isArray(n)) { [n, d, plus] = n; } else { - d = n.d; - plus = n.plus; - n = n.n; + if ( + typeof n.n === 'undefined' && + typeof n.d === 'undefined' && + typeof n.plus === 'undefined' + ) { + throw new Error('Invalid input given to dice related function - dice object must have at least one of n, d or plus properties.'); + } + ({ n = 1, d = 6, plus = 0 } = n); } } + + validate({ n }).int(`Expected n to be an integer, got ${n}`); + validate({ d }).int(`Expected d to be an integer, got ${d}`); + return { n, d, plus }; } - public parseDiceArgs (n : string | DiceInterface | number | number[] = 1, d: number = 6, plus: number = 0) : DiceInterface { + protected parseDiceArgs (n: string | Partial | number | number[] = 1, d: number = 6, plus: number = 0): DiceInterface { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceArgs(n); } - public static parseDiceString (string : string) : DiceInterface { + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ + public static parseDiceString (string: string): DiceInterface { // dice string like 5d10+1 if (!diceCache[string]) { + const trimmed = string.replace(/ +/g, ''); + if (/^[+-]*[\d.]+$/.test(trimmed)) { + return { n: 0, d: 0, plus: parseFloat(trimmed) }; + } if (diceRe.test(string)) { - const result = diceRe.exec(string.replace(/ +/g, '')); + const result = diceRe.exec(trimmed); if (result !== null) { diceCache[string] = { - n: (parseInt(result[1]) / 1 || 1), - d: (parseInt(result[2]) / 1 || 1), - plus: (parseFloat(result[3]) / 1 || 0), - }; - } - } else if (diceReNoInit.test(string)) { - const result = diceReNoInit.exec(string.replace(/ +/g, '')); - if (result !== null) { - diceCache[string] = { - n: 1, - d: (parseInt(result[1]) / 1 || 1), - plus: (parseFloat(result[2]) / 1 || 0), + n: parseInt(result[1]), + d: parseInt(result[2]), + plus: parseFloat(result[3]), }; + if (Number.isNaN(diceCache[string].n)) { + diceCache[string].n = 1; + } + if (Number.isNaN(diceCache[string].d)) { + diceCache[string].d = 6; + } + if (Number.isNaN(diceCache[string].plus)) { + diceCache[string].plus = 0; + } } } + if (typeof diceCache[string] === 'undefined') { + throw new Error(`Could not parse dice string ${string}`); + } } return diceCache[string]; } - public static diceMax (n : string | DiceInterface | number | number[] = 1, d: number = 6, plus: number = 0) : number { + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + public diceMax (n?: string | Partial | number | number[], d?: number, plus?: number): number { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMax(n, d, plus); + } + + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + public diceMin (n?: string | Partial | number | number[], d?: number, plus?: number): number { + const { constructor } = Object.getPrototypeOf(this); + return constructor.diceMin(n, d, plus); + } + + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + public static diceMax (n: string | Partial | number | number[] = 1, d: number = 6, plus: number = 0): number { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return (n * d) + plus; } - public static diceMin (n : string | DiceInterface | number | number[] = 1, d: number = 6, plus: number = 0) : number { + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + public static diceMin (n: string | Partial | number | number[] = 1, d: number = 6, plus: number = 0): number { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); return n + plus; } - public dice (n : string | DiceInterface | number | number[] = 1, d: number = 6, plus: number = 0) : number { + public diceExpanded (n: string | Partial | number | number[] = 1, d: number = 6, plus: number = 0): { dice: number[], plus: number, total: number } { ({ n, d, plus } = this.parseDiceArgs(n, d, plus)); if (typeof n === 'number') { - let nval = Math.max(n, 1); - const dval = Math.max(d, 1); + let nval = n; + const dval = Math.max(d, 0); if (d === 1) { - return plus + 1; + return { dice: Array(n).fill(d), plus, total: (n * d + plus) }; } - let sum = plus || 0; + if (n === 0 || d === 0) { + return { dice: [], plus, total: plus }; + } + const multiplier = nval < 0 ? -1 : 1; + nval *= multiplier; + const results: { dice: number[], plus: number, total: number } = { dice: [], plus, total: plus }; while (nval > 0) { - sum += this.randInt(1, dval); + results.dice.push(multiplier * this.randInt(1, dval)); nval--; } - return sum; + results.total = sum(results.dice) + plus; + return results; } throw new Error('Invalid arguments given to dice'); } - public parseDiceString (string : string) : DiceInterface { + public dice (n?: string | Partial | number | number[], d?: number, plus?: number): number { + return this.diceExpanded(n, d, plus).total; + } + + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ + public parseDiceString (string: string): DiceInterface { const { constructor } = Object.getPrototypeOf(this); return constructor.parseDiceString(string); } - public clamp (number : number, lower : number, upper : number) : number { - if (upper !== undefined) { + public clamp (number: number, lower?: number, upper?: number): number { + if (typeof upper !== 'undefined') { number = number <= upper ? number : upper; } - if (lower !== undefined) { + if (typeof lower !== 'undefined') { number = number >= lower ? number : lower; } return number; } - public bin (val : number, bins : number, min : number, max : number) : number { + public bin (val: number, bins: number, min: number, max: number): number { + validate({ val }).gt(min).lt(max); const spread = max - min; return (Math.round(((val - min) / spread) * (bins - 1)) / (bins - 1) * spread) + min; } } -export default class Rng extends RngAbstract implements RngInterface { +/** + * @category Main Class + */ +class Rng extends RngAbstract implements RngInterface, RngDistributionsInterface { #mask: number; #seed: number = 0; + #randFunc?: Randfunc | null; #m_z: number = 0; - constructor (seed? : Seed) { + constructor (seed?: Seed) { super(seed); this.#mask = 0xffffffff; this.#m_z = 987654321; } + /** + * {@inheritDoc RngInterface.predictable} + * @group Seeding + */ + public static predictable(this: new (seed: Seed) => Rng, seed: Seed): Rng { + return new this(seed ?? PREDICTABLE_SEED); + } + public serialize (): any { return { - mask: this.#mask, + mask: this.getMask(), seed: this.getSeed(), - m_z: this.#m_z, + m_z: this.getMz(), }; } - public sameAs (other: Rng): boolean { - const s = other.serialize(); - return this.#seed === s.seed && - this.#mask === s.mask && - this.#m_z === s.m_z; + public sameAs (other: any): boolean { + if (other instanceof Rng) { + return this.getRandomSource() === other.getRandomSource() && + this.getSeed() === other.getSeed() && + this.getMask() === other.getMask() && + this.getMz() === other.getMz(); + } + return false; + } + + /** @hidden */ + public getMask (): number { + return this.#mask; + } + + /** @hidden */ + public getMz (): number { + return this.#m_z; + } + + /** @hidden */ + public setMask (mask: number): void { + this.#mask = mask; + } + + /** @hidden */ + public setMz (mz: number): void { + this.#m_z = mz; } - public static unserialize (serialized : SerializedRng): Rng { + /** + * {@inheritDoc RngConstructor.unserialize} + * @group Serialization + */ + public static unserialize (serialized: SerializedRng): Rng { const rng = new this(); rng.setSeed(serialized.seed); - rng.#mask = serialized.mask; - rng.#seed = serialized.seed; - rng.#m_z = serialized.m_z; + rng.setMask(serialized.mask); + rng.setMz(serialized.m_z); return rng; } - public seed (i? : Seed): this { + public seed (i?: Seed): this { super.seed(i); this.#m_z = 987654321; return this; } - protected _random (): number { + protected _next (): number { this.#m_z = (36969 * (this.#m_z & 65535) + (this.#m_z >> 16)) & this.#mask; this.setSeed((18000 * (this.getSeed() & 65535) + (this.getSeed() >> 16)) & this.#mask); let result = ((this.#m_z << 16) + this.getSeed()) & this.#mask; @@ -644,3 +1479,5 @@ export default class Rng extends RngAbstract implements RngInterface { return result + 0.5; } } + +export default Rng; diff --git a/src/rng/interface.ts b/src/rng/interface.ts new file mode 100644 index 0000000..1d9b250 --- /dev/null +++ b/src/rng/interface.ts @@ -0,0 +1,1550 @@ +import Pool from './pool'; + +export interface DiceInterface { + n: number; + d: number; + plus: number; +} + +export interface DiceReturnInterface { + dice: number[]; + plus: number; + total: number; +} + +/** + * Distributions allowed for use in chancy + * @type {String} + */ +export type Distribution = 'normal' | 'boxMuller' | 'gaussian' | keyof RngDistributionsInterface; + +/** + * Most of the properties here are used to pass on to the resepective distribution functions. + * + * This basically takes the form: + * + * ```ts + * type ChancyInterface = { + * type?: string, // name of a distribution - must be valid + * min?: number, // always available + * max?: number, // always available + * ...otherArgs, // The relevant args for the distribution named above, all optional + * } + * ``` + * + * Specifying it this way allows TypeScript to perform a sort of validation for objects and + * parameters passed to the chancy function, but will require a bit of maintenance on the user + * end. + * + * @privateRemarks + * + * Unfortunately, this **does** mean we have to maintain call signatures in two places - both + * in the distribution functions, and here, because it's tightly coupled to the way Chancy can + * be called - but this added complexity in development allows for flexibility for the end user. + * + * @see {@link RngInterface.chancy} + */ +export type ChancyInterface = (( + ({ type?: 'random', skew?: number }) | + ({ type: 'integer' | 'int', skew?: number }) | + ({ type: 'dice', dice?: string, n?: number, d?: number, plus?: number }) | + ({ type: 'normal' | 'normal_int' | 'normal_integer', mean?: number, stddev?: number, skew?: number }) | + ({ type: 'bates', n?: number }) | + ({ type: 'batesgaussian', n?: number }) | + ({ type: 'bernoulli', p?: number }) | + ({ type: 'beta', alpha?: number, beta?: number }) | + ({ type: 'betaBinomial', alpha?: number, beta?: number, n?: number }) | + ({ type: 'binomial', n?: number, p?: number }) | + ({ type: 'boxMuller', mean?: number, stddev?: number }) | + ({ type: 'cauchy', median?: number, scale?: number }) | + ({ type: 'chiSquared', k?: number }) | + ({ type: 'exponential', rate?: number }) | + ({ type: 'gamma', shape?: number, rate?: number, scale?: number }) | + ({ type: 'gaussian', mean?: number, stddev?: number, skew?: number }) | + ({ type: 'hermite', lambda1?: number, lambda2?: number }) | + ({ type: 'hypergeometric', N?: number, K?: number, n?: number, k?: number }) | + ({ type: 'irwinHall', n?: number }) | + ({ type: 'kumaraswamy', alpha?: number, beta?: number }) | + ({ type: 'laplace', mean?: number, scale?: number }) | + ({ type: 'logistic', mean?: number, scale?: number }) | + ({ type: 'logNormal', mean?: number, stddev?: number }) | + ({ type: 'pareto', shape?: number, scale?: number, location?: number }) | + ({ type: 'poisson', lambda?: number }) | + ({ type: 'rademacher' }) | + ({ type: 'rayleigh', scale?: number }) | + ({ type: 'studentsT', nu?: number }) | + ({ type: 'wignerSemicircle', R?: number }) +) & { min?: number, max?: number }); + + +/** + * Chancy inputs that lead to strictly numeric results. + * @see {@link RngInterface.chancy} + */ +export type ChancyNumeric = ChancyInterface | string | number | number[]; + +/** + * Special Chancy type - for feeding to the 'chancy' function + * @see {@link RngInterface.chancy} + */ +export type Chancy = ChancyNumeric | any[]; + +/** + * Valid seeds for feeding to the RNG + */ +export type Seed = string | number; + +/** + * Interface for a random function that returns a number, given no arguments + * that is uniformly distributed. + */ +export interface Randfunc { + (): number +} + +/** + * Basic interface required for Rng implementations. + * + * Use this as an interface if you don't need all the advanced distributions + */ +export interface RngInterface { + + /** + * Whether this is the same as another object interfacting RngInterface, i.e. + * they will generate the same next number. + * @param other The thing to compare + */ + sameAs (other: RngInterface): boolean; + + /** + * Seed the random number generator with the given seed. + * + * @group Seeding + * @param {Seed} seed Can be a string or a number + */ + seed (seed: Seed): this; + + /** + * Get the current seed. + * + * Note this may not be the same as the set seed if numbers have been generated + * since its inception. Also, strings are usually transformed to numbers. + * + * @group Seeding + * @return The current seed + */ + getSeed (): number; + + /** + * Whether we are going to throw if max recursions is reached + */ + shouldThrowOnMaxRecursionsReached () : boolean; + + /** + * Sets whether we should throw if max recursions is reached. + */ + shouldThrowOnMaxRecursionsReached (val: boolean) : this; + + /** + * Create a new instance. Will use a globally set seed, so every instance + * returnd by this should generate the same numbers. + * + * @group Seeding + * @return An Rng instance, set to the given seed + */ + predictable (): RngInterface; + + /** + * Create a new instance with the given seed + * + * @group Seeding + * @param seed + * @return An Rng instance, set to the given seed + */ + predictable (seed: Seed): RngInterface; + + /** + * Pass a function that will return uniform random numbers as the source + * of this rng's randomness. + * + * Supersedes and seed setting. + * + * @example + * + * const rng = new Rng(); + * rng.randomSource(() => 1); + * + * assert(rng.random() === 1); // true + * + * @group Seeding + * @param func The random function + */ + randomSource (func?: Randfunc): this; + + /** + * Returns a unique 14 character string. + * + * Highly collision resistant, and strictly incrementing + * + * Useful for using as object IDs or HTML ids (with prefix). + * + * @group Utilities + * @param prefix A prefix to include on the output + * @return a 14 character string + */ + uniqid (prefix?: string): string; + + /** + * Returns a unique string of length len + * + * @group Utilities + * @param len Length of the string to generate + * @return A string of length "len" + */ + randomString (len?: number): string; + + /** + * Scales a number from [min, max] to [from, to]. + * + * Some might call this linear interpolation. + * + * Min and max default to 0 and 1 respectively + * + * @example + * rng.scale(0.5, 100, 200); // 150 + * rng.scale(0, 100, 200); // 100 + * rng.scale(1, 100, 200); // 200 + * rng.scale(5, 100, 200, 0, 10); // 150 + * + * @group Utilities + * @param number The number - must be 0 <= number <= 1 + * @param from The min number to scale to. When number === min, will return from + * @param to The max number to scale to. When number === max, will return to + * @param min The minimum number can take, default 0 + * @param max The maximum number can take, default 1 + * @return A number scaled to the interval [from, to] + */ + scale (number: number, from: number, to: number, min?: number, max?: number): number; + + /** + * Scales a number from [0, 1] to [from, to]. + * + * Some might call this linear interpolation + * + * @example + * rng.scaleNorm(0.5, 100, 200); // 150 + * rng.scaleNorm(0, 100, 200); // 100 + * rng.scaleNorm(1, 100, 200); // 200 + * + * @group Utilities + * @param number The number - must be 0 <= number <= 1 + * @param from The min number to scale to. When number === 0, will return from + * @param to The max number to scale to. When number === 1, will return to + * @return A normal number scaled to the interval [from, to] + */ + scaleNorm (number: number, from: number, to: number): number; + + /** + * Alias of randBetween + * + * @group Random Number Generation + * @see {@link randBetween} + */ + random (from?: number, to?: number, skew?: number): number; + + /** + * @group Random Number Generation + * @return A random number from [0, 1) + */ + randBetween (): number; + + /** + * @group Random Number Generation + * @param from Lower bound, inclusive + * @return A random number from [from, from+1) + */ + randBetween (from?: number): number; + + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randBetween(0, 10); + * // is the same as + * rng.randBetween(10, 0); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, exclusive + * @return A random number from [from, to) + */ + randBetween (from?: number, to?: number): number; + + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randBetween(0, 10, 0); + * // is the same as + * rng.randBetween(10, 0, 0); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, exclusive + * @param skew A number by which the numbers should skew. Negative skews towards from, and positive towards to. + * @return A random number from [from, to) skewed a bit skew direction + */ + randBetween (from: number, to: number, skew: number): number; + + /** + * @group Random Number Generation + * @return A random integer from [0, 1] + */ + randInt (): number; + + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randInt(0, 10); + * // is the same as + * rng.randInt(10, 0); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, inclusive + * @return A random integer from [from, to] + */ + randInt (from?: number, to?: number): number; + + /** + * Note that from and to should be interchangeable. + * + * @example + * + * rng.randInt(0, 10, 1); + * // is the same as + * rng.randInt(10, 0, 1); + * + * @group Random Number Generation + * @param from Lower bound, inclusive + * @param to Upper bound, inclusive + * @param skew A number by which the numbers should skew. Negative skews towards from, and positive towards to. + * @return A random integer from [from, to] skewed a bit in skew direction + */ + randInt (from?: number, to?: number, skew?: number): number; + + /** + * @group Random Number Generation + * @return A percentage [0, 100] + */ + percentage (): number; + + /** + * @group Random Number Generation + * @return A probability [0, 1] + */ + probability (): number; + + /** + * Results of an "n" in "chanceIn" chance of something happening. + * + * @example + * A "1 in 10" chance would be: + * + * ```ts + * rng.chance(1, 10); + * ``` + * + * @group Boolean Results + * @param n Numerator + * @param chanceIn Denominator + * @return Success or not + */ + chance (n: number, chanceIn?: number): boolean; + + /** + * Results of an "from" to "to" chance of something happening. + * + * @example + * A "500 to 1" chance would be: + * + * ```ts + * rng.chanceTo(500, 1); + * ``` + * + * @group Boolean Results + * @param from Left hand side + * @param to Right hand side + * @return Success or not + */ + chanceTo (from: number, to: number): boolean; + + /** + * The chancy function has a very flexible calling pattern. + * + * You can pass it a dice string, an object or a number. + * + * * If passed a dice string, it will do a roll of that dice. + * * If passed a number, it will return that number + * * If passed a config object, it will return a randomly generated number based on that object + * + * The purpose of this is to have easily serialised random signatures that you can pass to a single + * function easily. + * + * All chancy distribution functions (that is, when called with ChancyInterface) can be called with min and + * max parameters, however, it's highly advised to tune your parameters someway else. + * + * Basically, the function will just keep resampling until a figure inside the range is generated. This can + * quickly lead to large recursion depths for out of bounds inputs, at which point an error is thrown. + * + * @example + * + * rng.chancy(1); // returns 1 + * rng.chancy('1d6'); // returns an int between 1 and 6 [1, 6] + * + * @example + * + * rng.chancy({ min: 10 }); // Equivalent to calling rng.random(10, Number.MAX_SAFE_INTEGER) + * rng.chancy({ max: 10 }); // Equivalent to calling rng.random(0, 10) + * rng.chancy({ min: 0, max: 1 }); // Equivalent to calling rng.random(0, 1) + * + * @example + * + * rng.chancy({ type: 'integer', min: 10 }); // Equivalent to calling rng.randInt(10, Number.MAX_SAFE_INTEGER) + * rng.chancy({ type: 'integer', max: 10 }); // Equivalent to calling rng.randInt(0, 10) + * rng.chancy({ type: 'integer', min: 10, max: 20 }); // Equivalent to calling rng.randInt(10, 20) + * + * @example + * + * rng.chancy({ type: 'normal', ...args }); // Equivalent to calling rng.normal(args) + * rng.chancy({ type: 'normal_integer', ...args }); // Equivalent to calling Math.floor(rng.normal(args)) + * + * @example + * + * // You can call any of the 'distribution' type functions with chancy as well. + * rng.chancy({ type: 'boxMuller', ...args }); // Equivalent to calling rng.boxMuller(args) + * rng.chancy({ type: 'bates', ...args }); // Equivalent to calling rng.bates(args) + * rng.chancy({ type: 'exponential', ...args }); // Equivalent to calling rng.exponential(args) + * + * @example + * + * This is your monster file ```monster.json```: + * + * ```json + * { + * "id": "monster", + * "hp": {"min": 1, "max": 6, "type": "integer"}, + * "attack": "1d4" + * } + * ``` + * + * How about a stronger monster, with normally distributed health ```strong_monster.json```: + * + * ```json + * { + * "id": "strong_monster", + * "hp": {"min": 10, "max": 20, "type": "normal_integer"}, + * "attack": "1d6+1" + * } + * ``` + * + * Or something like this for ```boss_monster.json``` which has a fixed HP: + * + * ```json + * { + * "id": "boss_monster", + * "hp": 140, + * "attack": "2d10+4" + * } + * ``` + * + * Then in your code: + * + * ```ts + * import {Rng, Chancy} from GameRng; + * + * const rng = new Rng(); + * + * class Monster { + * hp = 10; + * id; + * attack = '1d4'; + * constructor ({id, hp = 10, attack} : {id: string, hp: Chancy, attack: Chancy} = {}) { + * this.id = options.id; + * this.hp = rng.chancy(hp); + * if (attack) this.attack = attack; + * } + * attack () { + * return rng.chancy(this.attack); + * } + * } + * + * const spec = await fetch('strong_monster.json').then(a => a.json()); + * const monster = new Monster(spec); + * ``` + * + * @see {@link Chancy} for details on input types + * @see {@link RngDistributionsInterface} for details on the different distributions that can be passed to the "type" param + * @group Random Number Generation + * @param input Chancy type input + * @return A randomly generated number or an element from a passed array + */ + chancy (input: ChancyNumeric): number; + chancy(input: T[]): T; + + /** + * Rounds the results of a chancy call so that it's always an integer. + * + * Not _quite_ equivalent to Math.round(rng.chancy(input)) because it will also + * transform {type: 'random'} to {type: 'integer'} which aren't quite the same. + * + * 'random' has a range of [min, max) whereas interger is [min, max] (inclusive of max). + * + * @group Random Number Generation + * @see {@link chancy} + */ + chancyInt (input: Chancy): number; + + /** + * Determines the minimum value a Chancy input can take. + * + * @group Result Prediction + * @param input Chancy input + * @return Minimum value a call to chancy with these args can take + */ + chancyMin (input: Chancy): number; + + /** + * Determines the maximum value a Chancy input can take. + * + * @group Result Prediction + * @param input Chancy input + * @return Maximum value a call to chancy with these args can take + */ + chancyMax (input: Chancy): number; + + /** + * Outputs what the distribution supports in terms of output + */ + support (input: Distribution) : string | undefined; + + /** + * Takes a random choice from an array of values, with equal weight. + * + * @group Choices + * @param data The values to choose from + * @return The random choice from the array + */ + choice (data: Array): any; + + /** + * Given an array, gives a key:weight Map of entries in the array based + * on how many times they appear in the array. + * + * @example + * + * const weights = rng.weights(['a', 'b', 'c', 'a']); + * assert(weights['a'] === 2); + * assert(weights['b'] === 1); + * assert(weights['c'] === 1); + * + * @group Utilities + * @param data The values to choose from + * @return The weights of the array + */ + weights (data: Array): Map; + + /** + * Takes a random key from an object with a key:number pattern + * + * Using a Map allows objects to be specified as keys, can be useful for + * choosing between concrete objects. + * + * @example + * + * Will return: + * + * * 'a' 1/10 of the time + * * 'b' 2/10 of the time + * * 'c' 3/10 of the time + * * 'd' 3/10 of the time + * + * ```ts + * rng.weightedChoice({ + * a: 1, + * b: 2, + * c: 3, + * d: 4 + * }); + * ``` + * + * @example + * + * Will return: + * + * * diamond 1/111 of the time + * * ruby 10/111 of the time + * * pebble 100/111 of the time + * + * ```ts + * const diamond = new Item('diamond'); + * const ruby = new Item('ruby'); + * const pebble = new Item('pebble'); + * + * const choices = new Map(); + * choices.set(diamond, 1); + * choices.set(ruby, 10); + * choices.set(pebble, 100); + * + * rng.weightedChoice(choices); + * ``` + * + * @group Choices + * @see [Map Object - MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) + * @see [Map Object - w3schools](https://www.w3schools.com/js/js_maps.asp) + * @param data The values to choose from + * @return The random choice from the array + */ + weightedChoice (data: Record | Array | Map): any; + + /** + * Returns a Pool with the given entries based on this RNG. + * + * Pools allow you to draw (without replacement) from them. + * + * @example + * + * const pool = rng.pool(['a', 'b', 'c', 'd']); + * + * pool.draw(); // 'b' + * pool.draw(); // 'c' + * pool.draw(); // 'a' + * pool.draw(); // 'd' + * pool.draw(); // PoolEmptyError('No more elements left to draw from in pool.') + * + * @param entries An array of anything + */ + pool(entries?: T[]): Pool; + + /** + * Rolls dice and returns the results in an expanded format. + * + * @example + * rng.diceExpanded('2d6+1'); // returns { dice: [3, 5], plus: 1, total: 9 } + * + * @group Random Number Generation + * @throws Error if the given input is invalid. + */ + diceExpanded (dice: string): DiceReturnInterface; + diceExpanded (options: Partial): DiceReturnInterface; + diceExpanded (dice: number[]): DiceReturnInterface; + diceExpanded (n: number, d: number, plus: number): DiceReturnInterface; + + /** + * Given a string dice representation, roll it + * @example + * + * rng.dice('1d6'); + * rng.dice('3d6+10'); + * rng.dice('1d20-1'); + * rng.dice('d10'); + * + * @group Random Number Generation + * @param dice e.g. 1d6+5 + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice (dice: string): number; + + /** + * Given an object representation of a dice, roll it + * @example + * + * rng.dice({ d: 6 }); + * rng.dice({ n: 3, d: 6, plus: 10 }); + * rng.dice({ n: 1, d: 20, plus: -1 }); + * rng.dice({ n: 1, d: 10 }); + * + * @group Random Number Generation + * @param options {n, d, plus} format of dice roll + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice ({ n, d, plus }: Partial): number; + + /** + * Roll "n" x "d" sided dice and add "plus" + * @group Random Number Generation + * @param options [n, d, plus] format of dice roll + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice ([n, d, plus]: number[]): number; + + /** + * Roll "n" x "d" sided dice and add "plus" + * @group Random Number Generation + * @param n The number of dice to roll + * @param d The number of faces on the dice + * @param plus The number to add at the end + * @return A random roll on the dice + * @throws Error if the given input is invalid. + */ + dice (n: number, d?: number, plus?: number): number; + + /** + * Parses a string representation of a dice and gives the + * object representation {n, d, plus} + * @group Utilities + * @param string String dice representation, e.g. '1d6' + * @returns The dice representation object + * @throws Error if the given input is invalid. + */ + parseDiceString (string: string): DiceInterface; + + /** + * Gives the minimum result of a call to dice with these arguments + * @group Result Prediction + * @see {@link dice} + */ + diceMin (n: string | DiceInterface | number, d?: number, plus?: number): number; + + /** + * Gives the maximum result of a call to dice with these arguments + * @group Result Prediction + * @see {@link dice} + */ + diceMax (n: string | DiceInterface | number, d?: number, plus?: number): number; + + /** + * Clamps a number to lower and upper bounds, inclusive + * + * @example + * rng.clamp(5, 0, 1); // 1 + * rng.clamp(-1, 0, 1); // 0 + * rng.clamp(0.5, 0, 1); // 0.5 + * @group Utilities + */ + clamp (number: number, lower: number, upper: number): number; + + /** + * Gets the bin "val" sits in when between "min" and "max" is separated by "bins" number of bins + * + * This is right aligning, so .5 rounds up + * + * This is useful when wanting only a discrete number values between two endpoints + * + * @example + * + * rng.bin(1.3, 11, 0, 10); // 1 + * rng.bin(4.9, 11, 0, 10); // 5 + * rng.bin(9.9, 11, 0, 10); // 10 + * rng.bin(0.45, 11, 0, 10); // 0 + * rng.bin(0.50, 11, 0, 10); // 1 + * + * @group Utilities + * @param val The value to bin + * @param bins The number of bins + * @param min Minimum value + * @param max Maximum value + * @return The corresponding bin (left aligned) + */ + bin (val: number, bins: number, min: number, max: number): number; + + /** + * Generates a normally distributed number, but with a special clamping and skewing procedure + * that is sometimes useful. + * + * Note that the results of this aren't strictly gaussian normal when min/max are present, + * but for our puposes they should suffice. + * + * Otherwise, without min and max and skew, the results are gaussian normal. + * + * @example + * + * rng.normal({ min: 0, max: 1, stddev: 0.1 }); + * rng.normal({ mean: 0.5, stddev: 0.5 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Random Number Generation + * @param [options] + * @param [options.mean] - The mean value of the distribution + * @param [options.stddev] - Must be > 0 if present + * @param [options.skew] - The skew to apply. -ve = left, +ve = right + * @param [options.min] - Minimum value allowed for the output + * @param [options.max] - Maximum value allowed for the output + * @param [depth] - used internally to track the recursion depth + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + normal (options?: { mean?: number, stddev?: number, max?: number, min?: number, skew?: number }, depth?: number): number; + + /** + * Generates a gaussian normal number, but with a special skewing procedure + * that is sometimes useful. + * + * @example + * + * rng.gaussian({ mean: 0.5, stddev: 0.5, skew: -1 }); + * + * @see [Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Normal_distribution) + * @group Distributions + * @param [options] + * @param [options.mean] + * @param [options.stddev] Must be > 0 + * @param [options.skew] + * @return A normally distributed number + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + gaussian (options?: { mean?: number, stddev?: number, skew?: number }): number; + + /** + * Generates a Gaussian normal value via Box–Muller transform + * + * There are two ways of calling, either with an object with mean and stddev + * as keys, or just with two params, the mean and stddev + * + * Support: [0, 1] + * + * @example + * + * rng.boxMuller({ mean: 0.5, stddev: 1 }); + * rng.boxMuller(0.5, 1); + * + * @group Distributions + * @see [Box–Muller transform - Wikipedia](https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform) + * @param [options] + * @param [options.mean] - The mean of the underlying normal distribution. + * @param [options.stddev] - The standard deviation of the underlying normal distribution. + * @returns A value from the Log-Normal distribution. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + boxMuller (options?: { mean?: number, stddev?: number }): number; + + /** + * Generates a Gaussian normal value via Box–Muller transform + * + * There are two ways of calling, either with an object with mean and stddev + * as keys, or just with two params, the mean and stddev + * + * Support: [0, 1] + * + * @example + * + * rng.boxMuller({ mean: 0.5, stddev: 1 }); + * rng.boxMuller(0.5, 1); + * + * @group Distributions + * @see [Box–Muller transform - Wikipedia](https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform) + * @param [mean] - The mean of the underlying normal distribution. + * @param [stddev] - The standard deviation of the underlying normal distribution. + * @returns A value from the Log-Normal distribution. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + boxMuller (mean?: number, stddev?: number): number; + + /** + * A serialized, storable version of this RNG that can be + * unserialized with unserialize + * + * @group Serialization + */ + serialize (): any; +} + +/** + * A version of RngInterface that includes lots of extra distributions. + * + * Depending on your integration, you may not need this. + * + */ +export interface RngDistributionsInterface { + /** + * Sum of n uniformly distributed values + * + * Support: [0, n] + * + * @example + * + * rng.irwinHall({ n: 6 }); + * rng.irwinHall(6); + * + * @see [Irwin-Hall Distribution - Wikipedia](https://en.wikipedia.org/wiki/Irwin%E2%80%93Hall_distribution) + * @group Distributions + * @param [options] + * @param [options.n]- Number of values to sum + * @returns The sum + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + irwinHall (options?: { n?: number }): number; + + /** + * Sum of n uniformly distributed values + * + * Support: [0, n] + * + * @example + * + * rng.irwinHall({ n: 6 }); + * rng.irwinHall(6); + * + * @see [Irwin-Hall Distribution - Wikipedia](https://en.wikipedia.org/wiki/Irwin%E2%80%93Hall_distribution) + * @group Distributions + * @param n - Number of values to sum + * @returns The sum + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + irwinHall (n?: number): number; + + /** + * Mean of n uniformly distributed values + * + * Support: [0, 1] + * + * @example + * + * rng.bates({ n: 6 }); + * rng.bates(6); + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param [options] + * @param [options.n] - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bates (options?: { n?: number }): number; + + /** + * Mean of n uniformly distributed values + * + * Support: [0, 1] + * + * @example + * + * rng.bates({ n: 6 }); + * rng.bates(6); + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param n - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bates (n?: number): number; + + /** + * A version of the bates distribution that returns gaussian normally distributed results, + * with n acting as a shape parameter. + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param n - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + batesgaussian (n: number): number + + /** + * A version of the bates distribution that returns gaussian normally distributed results, + * with n acting as a shape parameter. + * + * @see [Bates Distribution - Wikipedia](https://en.wikipedia.org/wiki/Bates_distribution) + * @group Distributions + * @param [options] + * @param [options.n] - Number of values to sum + * @returns The mean of n uniform values + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + batesgaussian (options: { n?: number }): number + + // Other distributions + + /** + * Probability that random number is less than p, returns 1 or 0. + * + * Support: {0, 1} + * + * @example + * + * rng.bernoulli({ p: 0.5 }); + * rng.bernoulli(0.5); + * + * @see [Bernoulli distribution - Wikipedia](https://en.wikipedia.org/wiki/Bernoulli_distribution) + * @group Distributions + * @param [options] + * @param [options.p] The probability of success, from [0 to 1], default 0.5 + * @returns 1 or 0, depending on if random number was less than p + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bernoulli (options?: { p?: number }): number; + + /** + * Probability that random number is less than p, returns 1 or 0. + * + * Support: {0, 1} + * + * @example + * + * rng.bernoulli({ p: 0.5 }); + * rng.bernoulli(0.5); + * + * @see [Bernoulli distribution - Wikipedia](https://en.wikipedia.org/wiki/Bernoulli_distribution) + * @group Distributions + * @param [p = 0.5] The probability of success, from [0 to 1], default 0.5 + * @returns 1 or 0, depending on if random number was less than p + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + bernoulli (p?: number): number; + + /** + * This function uses the inverse transform sampling method to generate + * an exponentially distributed random variable. + * + * Support: [0, ∞) + * + * @example + * + * rng.exponential({ rate: 1 }); + * rng.exponential(1); + * + * @param [options = {}] + * @param [options.rate = 1] The rate, must be > 0, default 1 + * @see [Exponential distribution - Wikipedia](https://en.wikipedia.org/wiki/Exponential_distribution) + * @group Distributions + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + exponential (options?: { rate?: number }): number; + + /** + * This function uses the inverse transform sampling method to generate + * an exponentially distributed random variable. + * + * Support: [0, ∞) + * + * @example + * + * rng.exponential({ rate: 1 }); + * rng.exponential(1); + * + * @param [rate = 1] The rate, must be > 0, default 1 + * @see [Exponential distribution - Wikipedia](https://en.wikipedia.org/wiki/Exponential_distribution) + * @group Distributions + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + exponential (rate?: number): number; + + /** + * Generates a value from the Generalized Pareto distribution. + * + * Support: [scale, ∞) + * + * @example + * + * rng.pareto({ shape: 0.5, scale: 1, location: 0 }); + * rng.pareto({ location: 0 }); + * rng.pareto({ scale: 1 }); + * rng.pareto({ shape: 0.5 }); + * + * @see [Pareto distribution - Wikipedia](https://en.wikipedia.org/wiki/Pareto_distribution) + * @group Distributions + * @param [options] + * @param [options.shape=0.5] - The shape parameter, must be >= 0, default 0.5. + * @param [options.scale=1] - The scale parameter, must be positive ( > 0), default 1. + * @param [options.location=0] - The location parameter, default 0. + * @returns A value from the Generalized Pareto distribution, [scale, ∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + pareto (options?: { shape?: number, scale?: number, location?: number }): number; + + /** + * This function uses the fact that the Poisson distribution can be generated using a series of + * random numbers multiplied together until their product is less than e^(-lambda). The number + * of terms needed is the Poisson-distributed random variable. + * + * Support: {1, 2, 3 ...} + * + * @example + * + * rng.poisson({ lambda: 1 }); + * rng.poisson(1); + * + * @see [Poisson distribution - Wikipedia](https://en.wikipedia.org/wiki/Poisson_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.lambda = 1] Control parameter, must be positive, default 1. + * @returns Poisson distributed random number, {1, 2, 3 ...} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + poisson (options?: { lambda?: number }): number; + + /** + * This function uses the fact that the Poisson distribution can be generated using a series of + * random numbers multiplied together until their product is less than e^(-lambda). The number + * of terms needed is the Poisson-distributed random variable. + * + * Support: {1, 2, 3 ...} + * + * @example + * + * rng.poisson({ lambda: 1 }); + * rng.poisson(1); + * + * @see [Poisson distribution - Wikipedia](https://en.wikipedia.org/wiki/Poisson_distribution) + * @group Distributions + * @param [lambda = 1] Control parameter, must be positive, default 1. + * @returns Poisson distributed random number, {1, 2, 3 ...} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + poisson (lambda?: number): number; + + /** + * This function uses combinations to calculate probabilities for the hypergeometric distribution. + * + * The hypergeometric distribution is a discrete probability distribution that describes the probability of k + * successes (random draws for which the object drawn has a specified feature) in + * n draws, without replacement, from a finite population of size N that contains exactly + * K objects with that feature + * + * Support: {max(0, n+K-N), ..., min(n, K)} + * + * @example + * + * rng.hypergeometric({ N: 50, K: 10, n: 5 }); + * rng.hypergeometric({ N: 50, K: 10, n: 5, k: 2 }); + * + * @see [Hypergeometric distribution - Wikipedia](https://en.wikipedia.org/wiki/Hypergeometric_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.N = 50] - The population size, must be positive integer. + * @param [options.K = 10] - The number of successes in the population, must be positive integer lteq N. + * @param [options.n = 5] - The number of draws, must be positive integer lteq N. + * @param [options.k] - The number of observed successes, must be positive integer lteq K and n. + * @returns The probability of exactly k successes in n draws, {max(0, n+K-N), ..., min(n, K)}. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + hypergeometric (options?: { N?: number, K?: number, n?: number, k?: number }): number; + + /** + * Generates a value from the Rademacher distribution. + * + * Support: {-1, 1} + * + * @example + * + * rng.rademacher(); + * + * @see [Rademacher distribution](https://en.wikipedia.org/wiki/Rademacher_distribution) + * @group Distributions + * @returns either -1 or 1 with 50% probability + */ + rademacher (): -1 | 1; + + /** + * Generates a value from the Binomial distribution. + * + * Probability distribution of getting number of successes of n trials of a boolean trial with probability p + * + * Support: {0, 1, 2, ..., n} + * + * @example + * + * rng.binomial({ n = 1, p = 0.5 }); + * rng.binomial({ n = 100, p = 0.1 }); + * + * @see [Binomial distribution - Wikipedia](https://en.wikipedia.org/wiki/Binomial_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.n = 1] - The number of trials, must be positive integer, default 1. + * @param [options.p = 0.6] - The probability of success, must be a number between 0 and 1 inclusive, default 0.5. + * @returns The number of successes, {0, 1, 2, ..., n}. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + binomial (options?: { n?: number, p?: number }): number; + + /** + * Generates a value from the Beta-binomial distribution. + * + * Support: {0, 1, 2, ..., n} + * + * @example + * + * rng.betaBinomial({ alpha = 1, beta = 2, n = 10 }) + * rng.betaBinomial({ n = 100 }) + * + * @see [Beta-binomial distribution - Wikipedia](https://en.wikipedia.org/wiki/Beta-binomial_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.alpha = 1] - The alpha parameter of the Beta distribution, default 1, must be positive. + * @param [options.beta = 1] - The beta parameter of the Beta distribution, default 1, must be positive. + * @param [options.n = 1] - The number of trials, default 1, must be positive integer. + * @returns The number of successes in n trials, {0, 1, 2, ..., n} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + betaBinomial (options?: { alpha?: number, beta?: number, n?: number }): number; + + /** + * Generates a value from the Beta distribution. + * + * Support: (0, 1) + * + * @example + * + * rng.beta({ alpha = 0.5, beta = 0.5 }) + * rng.beta({ alpha = 1, beta = 2 }) + * rng.beta({ beta = 1 }) + * + * @see [Beta distribution - Wikipedia](https://en.wikipedia.org/wiki/Beta_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.alpha = 0.5] - The alpha parameter of the Beta distribution, must be positive, default 0.5. + * @param [options.beta = 0.5] - The beta parameter of the Beta distribution, must be positive, default 0.5. + * @returns A value from the Beta distribution, (0, 1). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + beta (options?: { alpha?: number, beta?: number }): number; + + /** + * Generates a random number from the Gamma distribution. + * + * Support: (0, ∞) + * + * @example + * + * rng.gamma({ shape = 0.5, rate = 0.5 }) + * rng.gamma({ shape = 0.5, scale = 2 }) + * rng.gamma({ shape = 0.5, rate = 0.5, scale = 2 }) // Redundant as scale = 1 / rate + * rng.gamma({ shape = 0.5, rate = 2, scale = 2 }) // Error('Cannot supply scale and rate') + * + * @see [Gamma distribution - Wikipedia](https://en.wikipedia.org/wiki/Gamma_distribution) + * @group Distributions + * @see [stdlib Gamma function](https://github.com/stdlib-js/random-base-gamma/blob/main/lib/gamma.js#L39) + * @param [options = {}] + * @param [options.shape = 1] - The shape parameter, must be postive, default 1. + * @param [options.rate = 1] - The rate parameter, must be postive, default 1. + * @param [options.scale] - The scale parameter, must be postive, ( = 1/rate). + * @returns A random number from the Gamma distribution, from (0, ∞). + * @throws {Error} If both scale and rate are given and are not reciprocals of each other + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + gamma (options?: { shape?: number, rate?: number, scale?: number }): number; + + /** + * Generates a value from the Student's t-distribution. + * + * Support: (-∞, ∞) + * + * @example: + * + * rng.studentsT({ nu: 10 }) + * rng.studentsT(10) + * + * @see [Student's t-distribution - Wikipedia](https://en.wikipedia.org/wiki/Student%27s_t-distribution) + * @group Distributions + * @param [options = {}] + * @param [options.nu = 1] - The degrees of freedom, default 1, must be positive. + * @returns A value from the Student's t-distribution, (-∞, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + studentsT (options?: { nu?: number }): number; + + /** + * Generates a value from the Student's t-distribution. + * + * Support: (-∞, ∞) + * + * @example: + * + * rng.studentsT({ nu: 10 }) + * rng.studentsT(10) + * + * @see [Student's t-distribution - Wikipedia](https://en.wikipedia.org/wiki/Student%27s_t-distribution) + * @group Distributions + * @param [nu = 1] The degrees of freedom, must be positive, default 1 + * @returns A value from the Student's t-distribution, (-∞, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + studentsT (nu?: number): number; + + /** + * Generates a value from the Wigner semicircle distribution. + * + * Support: [-R; +R] + * + * @example: + * + * rng.wignerSemicircle({ R: 1 }) + * rng.wignerSemicircle(1) + * + * @see [Wigner Semicircle Distribution - Wikipedia](https://en.wikipedia.org/wiki/Wigner_semicircle_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.R = 1] - The radius of the semicircle, must be positive, default 1. + * @returns A value from the Wigner semicircle distribution, [-R; +R]. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + wignerSemicircle (options?: { R?: number }): number; + + /** + * Generates a value from the Wigner semicircle distribution. + * + * Support: [-R; +R] + * + * @example: + * + * rng.wignerSemicircle({ R: 1 }) + * rng.wignerSemicircle(1) + * + * @see [Wigner Semicircle Distribution - Wikipedia](https://en.wikipedia.org/wiki/Wigner_semicircle_distribution) + * @group Distributions + * @param [R = 1] - The radius of the semicircle, must be positive, default 1. + * @returns A value from the Wigner semicircle distribution, [-R; +R]. + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + wignerSemicircle (R?: number): number; + + /** + * Generates a value from the Kumaraswamy distribution. + * + * Support: (0, 1) + * + * @example: + * + * rng.kumaraswamy({ alpha: 1, beta: 2 }) + * rng.kumaraswamy({ alpha: 1 }) + * rng.kumaraswamy({ beta: 2 }) + * + * @see [Kumaraswamy Distribution - Wikipedia](https://en.wikipedia.org/wiki/Kumaraswamy_distribution) + * @group Distributions + * @param [options = {}] + * @param [options.alpha = 0.5] - The first shape parameter of the Kumaraswamy distribution, must be positive. + * @param [options.beta = 0.5] - The second shape parameter of the Kumaraswamy distribution, must be positive. + * @returns A value from the Kumaraswamy distribution, (0, 1). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + kumaraswamy (options?: { alpha?: number, beta?: number }): number; + + /** + * Generates a value from the Hermite distribution. + * + * Support: {0, 1, 2, 3, ...} + * + * @example: + * + * rng.hermite({ lambda1: 1, lambda2: 2 }) + * rng.hermite({ lambda1: 1 }) + * rng.hermite({ lambda2: 2 }) + * + * @see [Hermite Distribution - Wikipedia](https://en.wikipedia.org/wiki/Hermite_distribution) + * @group Distributions + * @param [options] + * @param [options.lambda1 = 1] - The mean of the first Poisson process, must be positive. + * @param [options.lambda2 = 2] - The mean of the second Poisson process, must be positive. + * @returns A value from the Hermite distribution, {0, 1, 2, 3, ...} + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + hermite (options?: { lambda1?: number, lambda2?: number }): number; + + /** + * Generates a value from the Chi-squared distribution. + * + * Support: [0, ∞) + * + * @example: + * + * rng.chiSquared({ k: 2 }) + * rng.chiSquared(2) // Equivalent + * + * @see [Chi-squared Distribution - Wikipedia](https://en.wikipedia.org/wiki/Chi-squared_distribution) + * @group Distributions + * @param [options] + * @param [options.k] - The degrees of freedom, must be a postive integer - default 1. + * @returns A value from the Chi-squared distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + chiSquared (options?: { k?: number }): number; + + /** + * Generates a value from the Chi-squared distribution. + * + * Support: [0, ∞) + * + * @example: + * + * rng.chiSquared({ k: 2 }) + * rng.chiSquared(2) // Equivalent + * + * @see [Chi-squared Distribution - Wikipedia](https://en.wikipedia.org/wiki/Chi-squared_distribution) + * @group Distributions + * @param [k = 1] - The degrees of freedom, must be a postive integer - default 1.. + * @returns A value from the Chi-squared distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + chiSquared (k?: number): number; + + /** + * Generates a value from the Rayleigh distribution + * + * Support: [0, ∞) + * + * @example: + * + * rng.rayleigh({ scale: 2 }) + * rng.rayleigh(2) // Equivalent + * + * @see [Rayleigh Distribution - Wikipedia](https://en.wikipedia.org/wiki/Rayleigh_distribution) + * @group Distributions + * @param [options] + * @param [options.scale] - The scale parameter of the Rayleigh distribution, must be > 0 - default 1. + * @returns A value from the Rayleigh distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + rayleigh (options?: { scale?: number }): number; + + /** + * Generates a value from the Rayleigh distribution + * + * Support: [0, ∞) + * + * @example: + * + * rng.rayleigh({ scale: 2 }) + * rng.rayleigh(2) // Equivalent + * + * @see [Rayleigh Distribution - Wikipedia](https://en.wikipedia.org/wiki/Rayleigh_distribution) + * @group Distributions + * @param [scale = 1] - The scale parameter of the Rayleigh distribution, must be > 0 - default 1. + * @returns A value from the Rayleigh distribution [0, ∞). + * @throws {@link NumberValidationError} If the input parameter is not valid. + */ + rayleigh (scale?: number): number; + + /** + * Generates a value from the Log-Normal distribution. + * + * Support: (0, ∞) + * + * @example: + * + * rng.logNormal({ mean: 2, stddev: 1 }) + * rng.logNormal({ mean: 2 }) + * rng.logNormal({ stddev: 1 }) + * + * @see [Log Normal Distribution - Wikipedia](https://en.wikipedia.org/wiki/Log-normal_distribution) + * @group Distributions + * @param [options] + * @param [options.mean] - The mean of the underlying normal distribution - default 0. + * @param [options.stddev] - The standard deviation of the underlying normal distribution, must be positive - default 1. + * @returns A value from the Log-Normal distribution (0, ∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + logNormal (options?: { mean?: number, stddev?: number }): number; + + /** + * Generates a value from the Cauchy distribution. + * + * Support: (-∞, +∞) + * + * @example: + * + * rng.cauchy({ median: 2, scale: 1 }) + * rng.cauchy({ median: 2 }) + * rng.cauchy({ scale: 1 }) + * + * @see [Cauchy Distribution - Wikipedia](https://en.wikipedia.org/wiki/Cauchy_distribution) + * @group Distributions + * @param [options] + * @param [options.median]- The location parameter (median, sometimes called x0) - default 0. + * @param [options.scale]- The scale parameter, must be positive - default 1. + * @returns A value from the Cauchy distribution (-∞, +∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + cauchy (options?: { median?: number, scale?: number }): number; + + /** + * Generates a value from the Laplace distribution. + * + * Support: (-∞, +∞) + * + * @example: + * + * rng.laplace({ mean: 2, scale: 1 }) + * rng.laplace({ mean: 2 }) + * rng.laplace({ scale: 1 }) + * + * @see [Laplace Distribution - Wikipedia](https://en.wikipedia.org/wiki/Laplace_distribution) + * @group Distributions + * @param [options] + * @param [options.mean]- The location parameter (mean) - default 0. + * @param [options.scale]- The scale parameter, must be positive - default 1. + * @returns A value from the Laplace distribution (-∞, +∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + laplace (options?: { mean?: number, scale?: number }): number; + + /** + * Generates a value from the Logistic distribution. + * + * Support: (-∞, +∞) + * + * @example: + * + * rng.logistic({ mean: 2, scale: 1 }) + * rng.logistic({ mean: 2 }) + * rng.logistic({ scale: 1 }) + * + * @see [Laplace Distribution - Wikipedia](https://en.wikipedia.org/wiki/Logistic_distribution) + * @group Distributions + * @param [options] + * @param [options.mean]- The location parameter (mean) - default 0. + * @param [options.scale]- The scale parameter, must be positive - default 1. + * @returns A value from the Logistic distribution (-∞, +∞). + * @throws {@link NumberValidationError} If the input parameters are not valid. + */ + logistic (options?: { mean?: number, scale?: number }): number; +} + +export interface RngConstructor { + new (seed?:Seed): RngInterface; + + predictable(this: new (seed: Seed) => RngConstructor, seed: Seed): RngConstructor; + + /** + * @group Serialization + */ + unserialize (rng: any): any; + + /** + * {@inheritDoc RngInterface.chancyMin} + * @group Result Prediction + */ + chancyMin (input: Chancy): number; + + /** + * {@inheritDoc RngInterface.chancyMax} + * @group Result Prediction + */ + chancyMax (input: Chancy): number; + + /** + * {@inheritDoc RngInterface.parseDiceString} + * @group Utilities + */ + parseDiceString (string: string): DiceInterface; + + /** + * {@inheritDoc RngInterface.diceMin} + * @group Result Prediction + */ + diceMin (n: string | DiceInterface | number, d?: number, plus?: number): number; + + /** + * {@inheritDoc RngInterface.diceMax} + * @group Result Prediction + */ + diceMax (n: string | DiceInterface | number, d?: number, plus?: number): number; +} diff --git a/src/rng/pool.ts b/src/rng/pool.ts new file mode 100644 index 0000000..a20a167 --- /dev/null +++ b/src/rng/pool.ts @@ -0,0 +1,116 @@ +import { RngInterface } from './interface'; +import Rng from './../rng'; + +/** + * @category Pool + */ +export class PoolEmptyError extends Error {} + +/** + * @category Pool + */ +export class PoolNotEnoughElementsError extends Error {} + +/** + * Allows for randomly drawing from a pool of entries without replacement + * @category Pool + */ +export default class Pool { + rng: RngInterface; + #entries: EntryType[] = []; + constructor (entries: EntryType[] = [], rng?: RngInterface) { + this.entries = entries; + if (rng) { + this.rng = rng; + } else { + this.rng = new Rng(); + } + } + + private copyArray(arr: EntryType[]): EntryType[] { + return Array.from(arr); + } + + public setEntries (entries: EntryType[]): this { + this.entries = entries; + return this; + } + + public getEntries (): EntryType[] { + return this.#entries; + } + + set entries (entries: EntryType[]) { + this.#entries = this.copyArray(entries); + } + + get entries (): EntryType[] { + return this.#entries; + } + + get length (): number { + return this.#entries.length; + } + + setRng (rng: RngInterface): this { + this.rng = rng; + return this; + } + + getRng (): RngInterface { + return this.rng; + } + + add (entry: EntryType) { + this.#entries.push(entry); + } + + empty (): this { + this.#entries = []; + return this; + } + + isEmpty (): boolean { + return this.length <= 0; + } + + /** + * Draw an element from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + */ + draw (): EntryType { + if (this.length === 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length === 1) { + return this.#entries.splice(0, 1)[0]; + } + const idx = this.rng.randInt(0, this.#entries.length - 1); + return this.#entries.splice(idx, 1)[0]; + } + + /** + * Draw n elements from the pool, without replacement. + * + * @throws {@link PoolEmptyError} if the pool is empty + * @throws {@link PoolNotEnoughElementsError} if the pool does not have enough elements to draw n values + */ + drawMany (n: number) : EntryType[] { + if (n < 0) { + throw new Error('Cannot draw < 0 elements from pool'); + } + if (this.length === 0 && n > 0) { + throw new PoolEmptyError('No more elements left to draw from in pool.'); + } + if (this.length < n) { + throw new PoolNotEnoughElementsError(`Tried to draw ${n} elements from pool with only ${this.length} entries.`); + } + const result: EntryType[] = []; + for (let i = 0; i < n; i++) { + const idx = this.rng.randInt(0, this.#entries.length - 1); + result.push(this.#entries.splice(idx, 1)[0]); + } + return result; + } +} diff --git a/src/rng/predictable.ts b/src/rng/predictable.ts index e8d125b..3c9c764 100644 --- a/src/rng/predictable.ts +++ b/src/rng/predictable.ts @@ -1,14 +1,70 @@ +import { RngAbstract } from './../rng'; import { - RngAbstract, + RngDistributionsInterface, RngInterface, Seed -} from './../rng'; +} from './interface'; /** + * * An Rng type that can be used to give predictable results * for testing purposes, and giving known results. + * + * You can set an array of results that will be returned from called to _next() + * + * Note: To avoid unexpected results when using this in place of regular Rng, it is + * only allowed to make the results spread from [0, 1) + * + * The numbers are returned and cycled, so once you reach the end of the list, it will + * just keep on going. + * + * @category Other Rngs + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0]; + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * prng.random(); // 0.0 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0, 0.5]; + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * prng.random(); // 0.0 + * prng.random(); // 0.5 + * + * @example + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.0 + * + * @example + * // The setEvenSpread and evenSpread methods can be used to generate + * // n numbers between [0, 1) with even gaps between + * const prng = new PredictableRng(); + * prng.results = [0.0, 0.1, 0.2, 0.3, 0.4]; + * prng.setEvenSpread(11); + * prng.random(); // 0.0 + * prng.random(); // 0.1 + * prng.random(); // 0.2 + * prng.random(); // 0.3 + * prng.random(); // 0.4 + * prng.random(); // 0.5 + * prng.random(); // 0.6 + * prng.random(); // 0.7 + * prng.random(); // 0.8 + * prng.random(); // 0.9 + * prng.random(); // 0.9999999... + * prng.random(); // 0.0 */ -export default class Rng extends RngAbstract implements RngInterface { +export default class PredictableRng extends RngAbstract implements RngInterface, RngDistributionsInterface { public counter = 0; protected _results: number[] = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; constructor (seed? : Seed, results?: number[]) { @@ -52,9 +108,14 @@ export default class Rng extends RngAbstract implements RngInterface { return this; } - public sameAs (other : Rng) : boolean { - return this.results.sort().join(',') === other.results.sort().join(',') && - this.counter === other.counter; + public sameAs (other : any) : boolean { + if (other instanceof PredictableRng) { + return this.results.join(',') === other.results.join(',') && + this.counter === other.counter && + this.getRandomSource() === other.getRandomSource() + ; + } + return false; } public reset () { @@ -62,7 +123,7 @@ export default class Rng extends RngAbstract implements RngInterface { return this; } - protected _random () : number { + protected _next () : number { return this.results[this.counter++ % this.results.length]; } } diff --git a/src/rng/queue.ts b/src/rng/queue.ts new file mode 100644 index 0000000..d846c4b --- /dev/null +++ b/src/rng/queue.ts @@ -0,0 +1,116 @@ +export default class Dequeue { + size: number; + elements: T[] = []; + constructor (length: T[] | number = 1) { + if (Array.isArray(length)) { + this.elements = length; + this.size = this.elements.length; + } else { + this.size = length; + } + } + + get length () { + return this.elements.length; + } + + public push (el: T) : T | undefined { + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + + public pop (): T | undefined { + return this.elements.pop(); + } + + public full (): boolean { + return this.length >= this.size; + } + + public empty (): void { + this.elements = []; + } + + public get (i: number) { + return this.elements[i]; + } + + public allSame (): boolean { + if (this.length > 0) { + return this.elements.every(a => a === this.elements[0]); + } + return true; + } +} + +export class NumberQueue extends Dequeue { + public sum (): number { + return this.elements.reduce((a, b) => a + b, 0); + } + + public avg (): number { + return this.sum() / this.length; + } +} + +class LoopDetectedError extends Error {} + +export class NonRandomDetector extends Dequeue { + minsequencelength: number = 2; + errormessage: string = 'Loop detected in input data. Randomness source not random?'; + + constructor (length: T[] | number = 1, minsequencelength: number = 2) { + super(length); + if (this.size > 10000) { + throw new Error('Cannot detect loops for more than 10000 elements'); + } + this.minsequencelength = minsequencelength; + } + + public push (el: T): T | undefined { + this.detectLoop(); + this.elements.push(el); + if (this.elements.length > this.size) { + return this.pop(); + } + } + + public detectLoop (msg?: string) { + if (this.full()) { + if (this.allSame()) { + this.loopDetected(msg); + } + if (this.hasRepeatingSequence(this.elements, this.minsequencelength)) { + this.loopDetected(msg); + } + } + } + + protected loopDetected (msg?: string) { + throw new LoopDetectedError(msg ?? this.errormessage); + } + + /** + * Checks if there is a repeating sequence longer than a specified length in an array of numbers. + * + * @param {number[]} arr - The array of numbers. + * @param {number} n - The minimum length of the repeating sequence. + * @returns {boolean} True if a repeating sequence longer than length n is found, otherwise false. + */ + public hasRepeatingSequence (arr: T[], n: number): boolean { + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + let k = 0; + while (j + k < arr.length && arr[i + k] === arr[j + k]) { + k++; + if (k > n) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/table.ts b/src/table.ts index 35d55f2..b1ea52a 100644 --- a/src/table.ts +++ b/src/table.ts @@ -4,7 +4,8 @@ import { default as LootTablePool, LootTablePoolDefinition } from './table/pool' import { FunctionDefinition, ConditionDefinition } from './table/pool/entry'; import LootTableEntryResult from './table/pool/entry/result'; import LootTableEntryResults from './table/pool/entry/results'; -import { default as RNG, RngInterface, Chancy } from './rng'; +import RNG from './rng'; +import { RngInterface, ChancyNumeric } from './rng/interface'; /** * Object used when creating a loot table. @@ -55,7 +56,7 @@ export interface TableRollInterface { context?: any, result?: LootTableEntryResults, rng?: RngInterface, - n?: Chancy, + n?: ChancyNumeric, } export interface TablePoolRollInterface { @@ -64,7 +65,7 @@ export interface TablePoolRollInterface { context?: any, result?: LootTableEntryResults, rng?: RngInterface, - n?: Chancy, + n?: ChancyNumeric, } export default class LootTable { @@ -102,7 +103,7 @@ export default class LootTable { this.fn = fn; this.ul = ul; this.rng = rng ?? (ul ? ul.getRng() : new RNG()); - this.id = id ?? this.rng.uniqstr(6); + this.id = id ?? this.rng.randomString(6); } // Register a function for use in loot pools diff --git a/src/table/pool.ts b/src/table/pool.ts index f94f8e4..ac7f325 100644 --- a/src/table/pool.ts +++ b/src/table/pool.ts @@ -3,15 +3,16 @@ import { default as LootTableEntry, LootTableEntryDefinition, ConditionDefinitio import LootTableEntryResult from './pool/entry/result'; import LootTableEntryResults from './pool/entry/results'; import { default as LootTable } from './../table'; -import { default as RNG, RngInterface, Chancy } from './../rng'; +import RNG from './../rng'; +import { RngInterface, ChancyNumeric } from './../rng/interface'; export interface LootTablePoolDefinition { name?: string, id?: string, conditions?: Array, functions?: Array, - rolls?: Chancy, - nulls?: Chancy, + rolls?: ChancyNumeric, + nulls?: ChancyNumeric, entries?: Array, template?: Partial } @@ -21,8 +22,8 @@ export default class LootPool { id?: string; conditions: Array = []; functions: Array = []; - rolls: Chancy = 1; - nulls: Chancy = 0; + rolls: ChancyNumeric = 1; + nulls: ChancyNumeric = 0; entries: Array = []; template: Partial = {}; @@ -46,7 +47,7 @@ export default class LootPool { this.functions = functions ?? []; this.rolls = rolls; this.nulls = nulls; - this.id = id ?? (new RNG()).uniqstr(6); + this.id = id ?? (new RNG()).randomString(6); this.template = template; if (entries) { for (const entry of entries) { diff --git a/src/table/pool/entry.ts b/src/table/pool/entry.ts index 8517627..75437b2 100644 --- a/src/table/pool/entry.ts +++ b/src/table/pool/entry.ts @@ -1,6 +1,7 @@ import log from './../../log'; import LootTable from './../../table'; -import { default as RNG, RngInterface, Chancy } from './../../rng'; +import RNG from './../../rng'; +import { RngInterface, ChancyNumeric } from './../../rng/interface'; import LootTableEntryResult from './entry/result'; import LootTableEntryResults from './entry/results'; @@ -11,7 +12,7 @@ export type LootTableEntryDefinition = { unique?: boolean, weight?: number, item?: any, - qty?: Chancy, + qty?: ChancyNumeric, functions?: Array, conditions?: Array }; @@ -34,7 +35,7 @@ export default class LootTableEntry { name?: string; weight: number = 1; item?: any; - qty: Chancy = 1; + qty: ChancyNumeric = 1; functions: Array; conditions: Array; rng?: RngInterface; diff --git a/src/ultraloot.ts b/src/ultraloot.ts index 6aacd2f..24633c1 100644 --- a/src/ultraloot.ts +++ b/src/ultraloot.ts @@ -4,7 +4,8 @@ import { default as LootTablePool, LootTablePoolDefinition } from './table/pool' import { default as LootTableEntry, LootTableEntryDefinition, FunctionDefinition, ConditionDefinition } from './table/pool/entry'; import LootTableEntryResult from './table/pool/entry/result'; import LootTableEntryResults from './table/pool/entry/results'; -import { default as RNG, Seed, RngInterface, RngConstructor, Chancy } from './rng'; +import RNG from './rng'; +import { Seed, RngInterface, RngConstructor, ChancyNumeric } from './rng/interface'; import { version as CURRENT_VERSION } from './../package.json'; import * as defaultFunctions from './default/functions'; import * as defaultConditions from './default/conditions'; @@ -44,8 +45,8 @@ export type LootTablePoolEasyDefinition = { conditions?: Array, functions?: Array, template?: LootTableEntryDefinition, - rolls?: Chancy, - nulls?: Chancy, + rolls?: ChancyNumeric, + nulls?: ChancyNumeric, entries?: Array, }; @@ -68,8 +69,8 @@ export type LootTablePoolJsonDefinition = { id?: string, conditions?: Array, functions?: Array, - rolls?: Chancy, - nulls?: Chancy, + rolls?: ChancyNumeric, + nulls?: ChancyNumeric, entries: Array, }; @@ -83,7 +84,7 @@ export type LootTableEntryJsonDefinition = { stackable?: boolean, weight?: number, item?: any, - qty?: Chancy, + qty?: ChancyNumeric, functions?: Array, conditions?: Array }; @@ -228,7 +229,7 @@ export class UltraLoot { 'chanceTo', 'randInt', 'uniqid', - 'uniqstr', + 'randomString', 'randBetween', 'normal', 'chancyInt', @@ -690,7 +691,7 @@ export class UltraLoot { }; clone.pools = []; - const keyToUse = table.filename ?? this.getRng().uniqstr(6); + const keyToUse = table.filename ?? this.getRng().randomString(6); had.add(table); if (includeRng) { @@ -726,7 +727,7 @@ export class UltraLoot { } if (entryClone.item instanceof LootTable) { - const subKeyToUse = entryClone.item.filename ?? this.getRng().uniqstr(6); + const subKeyToUse = entryClone.item.filename ?? this.getRng().randomString(6); if (had.has(entryClone.item)) { throw new RecursiveTableError('Recursive requirement detected - cannot serialize recursively required tables.'); } diff --git a/tests/rng.test.ts b/tests/rng.test.ts deleted file mode 100644 index 701c19e..0000000 --- a/tests/rng.test.ts +++ /dev/null @@ -1,577 +0,0 @@ -import { default as Rng } from './../src/rng'; -import { default as PredictableRng } from './../src/rng/predictable'; - -const defaultResultSet = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; -const prng = new PredictableRng(); -const rng = new Rng(); - -describe('testing Rng & predictable Rng', () => { - test('Getting and setting predictable results', () => { - prng.results = [0.5]; - expect(prng.results).toEqual([0.5]); - - prng.results = defaultResultSet; - expect(prng.results).toEqual(defaultResultSet); - }); - - test('Predictable results appear in proper order', () => { - prng.results = defaultResultSet; - expect(prng.random()).toBe(defaultResultSet[0]); - expect(prng.random()).toBe(defaultResultSet[1]); - expect(prng.random()).toBe(defaultResultSet[2]); - expect(prng.random()).toBe(defaultResultSet[3]); - expect(prng.random()).toBe(defaultResultSet[4]); - expect(prng.random()).toBe(defaultResultSet[5]); - expect(prng.random()).toBe(defaultResultSet[6]); - expect(prng.random()).toBe(defaultResultSet[7]); - expect(prng.random()).toBe(defaultResultSet[8]); - expect(prng.random()).toBe(defaultResultSet[9]); - expect(prng.random()).toBe(defaultResultSet[10]); - }); - - test('Predictable reset works', () => { - prng.results = defaultResultSet; - expect(prng.random()).toBe(defaultResultSet[0]); - expect(prng.random()).toBe(defaultResultSet[1]); - expect(prng.random()).toBe(defaultResultSet[2]); - expect(prng.random()).toBe(defaultResultSet[3]); - expect(prng.random()).toBe(defaultResultSet[4]); - expect(prng.random()).toBe(defaultResultSet[5]); - prng.reset(); - expect(prng.random()).toBe(defaultResultSet[0]); - expect(prng.random()).toBe(defaultResultSet[1]); - expect(prng.random()).toBe(defaultResultSet[2]); - expect(prng.random()).toBe(defaultResultSet[3]); - expect(prng.random()).toBe(defaultResultSet[4]); - expect(prng.random()).toBe(defaultResultSet[5]); - }); - - test('Predictable results reset when set', () => { - prng.results = defaultResultSet; - expect(prng.random()).toBe(defaultResultSet[0]); - prng.results = defaultResultSet; - expect(prng.random()).toBe(defaultResultSet[0]); - }); - - test('Predictable even spread', () => { - const numsToTest = [3, 6, 9, 12]; - for (const num of numsToTest) { - prng.setEvenSpread(num); - for (let i = 0; i < num - 1; i++) { - expect(prng.random()).toBe(i / (num - 1)); - } - expect(prng.random()).toBeCloseTo(1); - } - }); - - test('Predictable same as', () => { - const p1 = new PredictableRng(); - const p2 = new PredictableRng(); - - p1.results = [0.1, 0.2, 0.3]; - p2.results = [0.1, 0.2, 0.3]; - - expect(p1.sameAs(p2)).toBeTruthy(); - expect(p2.sameAs(p1)).toBeTruthy(); - }); - - test('Predictable throws when results are empty', () => { - expect(() => { - prng.results = []; - }).toThrow(); - }); - - test('Predictable throws when >= 1', () => { - expect(() => { - prng.results = [2]; - }).toThrow(); - expect(() => { - prng.results = [1]; - }).toThrow(); - }); - - test('Predictable throws when < 0', () => { - expect(() => { - prng.results = [-1]; - }).toThrow(); - }); - - test('Test rng accepts string seed', () => { - expect(() => { - const rng = new Rng('abc'); - rng.seed('def'); - }).not.toThrow(); - }); - - test('Test rng predictable sets seed', () => { - const rng = new Rng(); - const predictableRng = rng.predictable(1234); - - expect(predictableRng.getSeed()).toBe(1234); - - const predictableRngStatic = Rng.predictable(1234); - - expect(predictableRngStatic.getSeed()).toBe(1234); - }); - - test('Test scaling and scale normalizing', () => { - for (let i = 0; i < 100; i++) { - expect(rng.scale(i, 0, 1, 0, 100)).toBeGreaterThanOrEqual(0); - expect(rng.scale(i, 0, 1, 0, 100)).toBeLessThanOrEqual(100); - expect(rng.scale(i, 0, 1, 0, 100)).toBeCloseTo(i / 100); - } - for (let i = 0; i < 1; i += 0.1) { - expect(rng.scaleNorm(i, 0, 100)).toBeGreaterThanOrEqual(0); - expect(rng.scaleNorm(i, 0, 100)).toBeLessThanOrEqual(100); - expect(rng.scaleNorm(i, 0, 100)).toBeCloseTo(i * 100); - } - }); - - test('Test scaling with invalid parameters throws', () => { - expect(() => { - rng.scale(-1, 0, 100, 0, 1); - }).toThrow(); - expect(() => { - rng.scale(10, 0, 100, 0, 1); - }).toThrow(); - expect(() => { - rng.scaleNorm(-1, 0, 100); - }).toThrow(); - expect(() => { - rng.scaleNorm(10, 0, 100); - }).toThrow(); - }); - - test('Random returns 0, 1', () => { - for (let i = 0; i < 10000; i++) { - const randResult = rng.random(); - expect(randResult).toBeGreaterThanOrEqual(0); - expect(randResult).toBeLessThan(1); - } - }); - - test('Random int returns int', () => { - for (let i = 0; i < 100; i++) { - const randResult = rng.randInt(0, 100); - expect(randResult).toBeGreaterThanOrEqual(0); - expect(randResult).toBeLessThanOrEqual(100); - expect(Number.isInteger(randResult)).toBeTruthy(); - } - }); - - test('Get/Set seed', () => { - const orig = new Rng(); - orig.seed(12345); - expect(orig.getSeed()).toBe(12345); - }); - - test('Constructor get/set seed', () => { - const orig = new Rng(12345); - expect(orig.getSeed()).toBe(12345); - }); - - test('Two instances with same seed product same random number', () => { - const a = new Rng(12345); - const b = new Rng(12345); - for (let i = 0; i < 100; i++) { - expect(a.random()).toBe(b.random()); - } - }); - - test('Serialize basic', () => { - const orng = new Rng(56789); - const s = orng.serialize(); - const nrng = Rng.unserialize(s); - expect(nrng.getSeed()).toEqual(orng.getSeed()); - expect(nrng.getSeed()).toEqual(56789); - expect(nrng.serialize()).toEqual(s); - }); - - test('Serialize after random number gen', () => { - const orng = new Rng(56789); - orng.random(); - orng.random(); - orng.random(); - orng.random(); - orng.random(); - const s = orng.serialize(); - const nrng = Rng.unserialize(s); - expect(nrng.getSeed()).toEqual(orng.getSeed()); - expect(nrng.serialize()).toEqual(s); - }); - - test('Serialize and unserialize produces same random numbers', () => { - const orig = new Rng(12345); - orig.random(); - const s = orig.serialize(); - const other = Rng.unserialize(s); - expect(other.getSeed()).toBe(orig.getSeed()); - expect(other.serialize()).toEqual(s); - for (let i = 0; i < 100; i++) { - expect(orig.random()).toBe(other.random()); - } - }); - - test('percentage', () => { - prng.results = defaultResultSet; - expect(prng.percentage()).toBe(defaultResultSet[0] * 100); - expect(prng.percentage()).toBe(defaultResultSet[1] * 100); - expect(prng.percentage()).toBe(defaultResultSet[2] * 100); - expect(prng.percentage()).toBe(defaultResultSet[3] * 100); - expect(prng.percentage()).toBe(defaultResultSet[4] * 100); - expect(prng.percentage()).toBe(defaultResultSet[5] * 100); - expect(prng.percentage()).toBe(defaultResultSet[6] * 100); - expect(prng.percentage()).toBe(defaultResultSet[7] * 100); - expect(prng.percentage()).toBe(defaultResultSet[8] * 100); - expect(prng.percentage()).toBe(defaultResultSet[9] * 100); - expect(prng.percentage()).toBe(defaultResultSet[10] * 100); - }); - - test('chance', () => { - const rng = new Rng(12345); - expect(rng.chance(1, 1)).toBeTruthy(); - const prng = new PredictableRng(12345, [0, 0.5, 1 - Number.EPSILON]); - expect(prng.chance(1, 10)).toBeTruthy(); - expect(prng.chance(1, 10)).toBeFalsy(); - expect(prng.chance(1, 10)).toBeFalsy(); - }); - - test('chanceTo', () => { - const rng = new Rng(12345); - expect(rng.chanceTo(1, 0)).toBeTruthy(); - const prng = new PredictableRng(12345, [0, 0.5, 1 - Number.EPSILON]); - expect(prng.chanceTo(10, 1)).toBeTruthy(); - expect(prng.chanceTo(10, 1)).toBeTruthy(); - expect(prng.chanceTo(10, 1)).toBeFalsy(); - }); - - test('uniqid', () => { - const rng = new Rng(12345); - expect(rng.uniqid()).not.toBe(rng.uniqid()); - }); - - test('uniqstr', () => { - const rng = new Rng(12345); - expect(rng.uniqstr()).not.toBe(rng.uniqstr()); - }); - - test('randBetween', () => { - const rng = new Rng(); - for (let i = 0; i < 100; i++) { - const r = rng.randBetween(1, 100); - expect(r).toBeGreaterThanOrEqual(1); - expect(r).toBeLessThanOrEqual(100); - } - }); - - test('normal', () => { - const results = []; - const mean = 10; - const stddev = 0.1; - for (let i = 0; i < 100000; i++) { - results.push(rng.normal({ mean, stddev })); - } - const sum = results.reduce((a, b) => a + b); - const calcMean = sum / results.length; - const calcStdDev = Math.sqrt(results.reduce((a, b) => a + Math.pow((b - mean), 2), 0) / (results.length - 1)); - - expect(Math.abs(mean - calcMean)).toBeLessThan(stddev / 10); - expect(Math.abs(stddev - calcStdDev)).toBeLessThan(stddev / 10); - - // There is a 1 in 390,682,215,445 chance for each result to be within 7 standard deviations. - // It should be fairly sufficient to test that results are within this +/- 7o window. - // i.e. these will fail only 1/3,906,822 times. - // Using mean = 10 and stddev = 0.1 the window is then from 9.3 to 10.7 - expect(Math.max(...results)).toBeLessThan(mean + (stddev * 7)); - expect(Math.min(...results)).toBeGreaterThan(mean - (stddev * 7)); - }); - - test('Test shouldThrowOnMaxRecursionsReached returns boolean', () => { - const rng = new Rng(); - expect(typeof rng.shouldThrowOnMaxRecursionsReached()).toBe('boolean'); - }); - - test('Test normal throws on max recursions', () => { - const rng = new Rng(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockReturnValue(true); - jest.spyOn(rng, 'boxMuller').mockReturnValue(100); - expect(() => { - rng.normal(); - }).toThrow(); - jest.spyOn(rng, 'boxMuller').mockRestore(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockRestore(); - }); - - /** Please note if this test becomes impossible to maintain, it's not important, get rid */ - test('Test normal throws on max recursions with specifically crafted values...', () => { - const rng = new Rng(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockReturnValue(true); - jest.spyOn(rng, 'boxMuller').mockReturnValue(-0.5); - expect(() => { - rng.normal({ mean: 0.1 - Number.EPSILON, max: 0.1, min: 0.1 - (Number.EPSILON * 2) }); - }).toThrow(); - jest.spyOn(rng, 'boxMuller').mockRestore(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockRestore(); - }); - - test('Test normal does not throw on max recursions when set not to', () => { - const rng = new Rng(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockReturnValue(false); - jest.spyOn(rng, 'boxMuller').mockReturnValue(100); - expect(() => { - rng.normal(); - }).not.toThrow(); - jest.spyOn(rng, 'boxMuller').mockRestore(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockRestore(); - }); - - test('Test normal does not throw on max recursions when set not to with min and max', () => { - const rng = new Rng(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockReturnValue(false); - jest.spyOn(rng, 'boxMuller').mockReturnValue(-0.5); - expect(() => { - rng.normal({ mean: 0.1 - Number.EPSILON, max: 0.1, min: 0.1 - (Number.EPSILON * 2) }); - }).not.toThrow(); - jest.spyOn(rng, 'boxMuller').mockRestore(); - jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockRestore(); - }); - - test('weightedChoice', () => { - prng.results = defaultResultSet; - const choices = { - a: 10, - b: 1 - }; - for (let i = 0; i < 10; i++) expect(prng.weightedChoice(choices)).toBe('a'); - const r1 = prng.weightedChoice(choices); - expect(r1).toBe('b'); - - const stacked = { - a: Math.pow(2, 32), - b: 1 - }; - for (let i = 0; i < 1000; i++) { - expect(rng.weightedChoice(stacked)).toBe('a'); - } - }); - - test('weightedChoice invalid input', () => { - const choices = { - a: -1, - b: 1 - }; - expect(() => { - rng.weightedChoice(choices); - }).toThrow(); - }); - - test('weightedChoice with map', () => { - const map = new Map(); - const ob1 = { name: 'ob1' }; - const ob2 = { name: 'ob2' }; - - map.set(ob1, 10); - map.set(ob2, 1); - - prng.results = defaultResultSet; - for (let i = 0; i < 10; i++) expect(prng.weightedChoice(map)).toBe(ob1); - const r1 = prng.weightedChoice(map); - expect(r1).toBe(ob2); - }); - - test('weightedChoice with array of strings', () => { - const colors = ['red', 'green', 'blue']; - prng.setEvenSpread(3); - expect(prng.weightedChoice(colors)).toBe('red'); - expect(prng.weightedChoice(colors)).toBe('green'); - expect(prng.weightedChoice(colors)).toBe('blue'); - }); - - test('weightedChoice with array of objects', () => { - const ob1 = { name: 'ob1' }; - const ob2 = { name: 'ob2' }; - const choices = [ob1, ob2]; - - prng.setEvenSpread(2); - expect(prng.weightedChoice(choices)).toBe(ob1); - expect(prng.weightedChoice(choices)).toBe(ob2); - }); - - test('parseDiceString', () => { - const expectations = { - '5d6+6': { n: 5, d: 6, plus: 6 }, - '2d12+1': { n: 2, d: 12, plus: 1 }, - d12: { n: 1, d: 12, plus: 0 }, - '5 d 6 + 6': { n: 5, d: 6, plus: 6 }, - }; - for (const [d, exp] of Object.entries(expectations)) { - const result = rng.parseDiceString(d); - expect(result).toEqual(exp); - } - }); - - test('dice', () => { - const buildResults = (n : number) => { - const results = []; - for (let i = 0; i < (n - 1); i++) { - results.push(i / (n - 1)); - } - results.push(1 - Number.EPSILON); - return results; - }; - let n = 6; - prng.results = buildResults(n); - expect(prng.dice('1d6+1')).toBe(2); - expect(prng.dice('1d6+1')).toBe(3); - expect(prng.dice('1d6+1')).toBe(4); - expect(prng.dice('1d6+1')).toBe(5); - expect(prng.dice('1d6+1')).toBe(6); - expect(prng.dice('1d6+1')).toBe(7); - expect(prng.dice('1d6+1')).toBe(2); - - prng.reset(); - expect(prng.dice('1d6+1')).toBe(2); - expect(prng.dice('1d6+1')).toBe(3); - - prng.reset(); - expect(prng.dice([1, 6, 1])).toBe(2); - expect(prng.dice([1, 6, 1])).toBe(3); - - prng.reset(); - expect(prng.dice('2d6+1')).toBe(4); - - prng.reset(); - expect(prng.dice('1d1+1')).toBe(2); - - n = 12; - prng.results = buildResults(n); - expect(prng.dice(`2d${n}+1`)).toBe(4); - - prng.results = [1 - Number.EPSILON]; - expect(prng.dice(`2d${n}+1`)).toBe(25); - - prng.results = [1 - Number.EPSILON]; - expect(prng.dice(`1d${n}`)).toBe(n); - - prng.results = [0.5 - Number.EPSILON, 1 - Number.EPSILON]; - expect(prng.dice(`2d${n}+1`)).toBe(6 + 12 + 1); - - prng.reset(); - expect(prng.dice(`2d${n}-1`)).toBe(6 + 12 - 1); - }); - - test('dice - invalid inputs', () => { - expect(() => { - // @ts-ignore - rng.dice(null); - }).toThrow(); - - expect(() => { - // @ts-ignore - rng.dice(new Set()); - }).toThrow(); - }); - - test('clamp', () => { - expect(rng.clamp(15, 1, 10)).toBe(10); - expect(rng.clamp(-1, 1, 10)).toBe(1); - }); - - test('bin', () => { - expect(rng.bin(1.5 - Number.EPSILON, 11, 0, 10)).toBe(1); - expect(rng.bin(1.5 + Number.EPSILON, 11, 0, 10)).toBe(2); - expect(rng.bin(9.1, 11, 0, 10)).toBe(9); - expect(rng.bin(9.7, 11, 0, 10)).toBe(10); - }); - - test('chancyInt', () => { - const r1 = rng.chancyInt(5.5); - expect(Number.isInteger(r1)).toBeTruthy(); - - const r2 = rng.chancyInt(5); - expect(r2).toBe(5); - expect(Number.isInteger(r2)).toBeTruthy(); - - const r3 = rng.chancyInt('2d6'); - expect(Number.isInteger(r3)).toBeTruthy(); - - const r4 = rng.chancyInt({min: 1, max: 3}); - expect(Number.isInteger(r4)).toBeTruthy(); - }); - - test('chancy', () => { - prng.results = defaultResultSet; - expect(prng.chancy(5)).toBe(5); - expect(prng.chancy({ min: 0, max: 10 })).toBe(0); - expect(prng.chancy({ min: 0, max: 10 })).toBe(1); - expect(prng.chancy({ min: 0, max: 1 })).toBe(0.2); - }); - - test('chancy - normal', () => { - const results = []; - const mean = 0.5; - for (let i = 0; i < 100000; i++) { - results.push(rng.chancy({ min: 0, max: 1, type: 'normal' })); - } - const sum = results.reduce((a, b) => a + b); - const calcMean = sum / results.length; - - expect(Math.abs(mean - calcMean)).toBeLessThan(0.003); - - expect(Math.max(...results)).toBeLessThan(0.9999); - expect(Math.min(...results)).toBeGreaterThan(0.0001); - }); - - test('chancy - normal_integer', () => { - const results = []; - const mean = 50; - for (let i = 0; i < 100000; i++) { - results.push(rng.chancy({ min: 0, max: 100, type: 'normal_integer' })); - } - const sum = results.reduce((a, b) => a + b); - const calcMean = sum / results.length; - - expect(Math.abs(mean - calcMean)).toBeLessThan(1); - }); - - test('dice max', () => { - expect(Rng.diceMax({ n: 1, d: 6, plus: 2 })).toBe(8); - expect(Rng.diceMax('1d6+2')).toBe(8); - expect(Rng.diceMax('2d6+2')).toBe(14); - expect(Rng.diceMax('2d6+5')).toBe(17); - expect(Rng.diceMax({ n: 0, d: 6, plus: 2 })).toBe(2); - }); - - test('dice min', () => { - expect(Rng.diceMin({ n: 1, d: 6, plus: 2 })).toBe(3); - expect(Rng.diceMin('1d6+2')).toBe(3); - expect(Rng.diceMin('2d6+2')).toBe(4); - expect(Rng.diceMin('2d6+5')).toBe(7); - expect(Rng.diceMin({ n: 0, d: 6, plus: 2 })).toBe(2); - expect(Rng.diceMin({ n: 1, d: 6, plus: 0 })).toBe(1); - }); - - test('chancy max', () => { - expect(Rng.chancyMax('1d6+2')).toBe(8); - expect(Rng.chancyMax('2d6+2')).toBe(14); - expect(Rng.chancyMax('2d6+5')).toBe(17); - expect(Rng.chancyMax(5)).toBe(5); - expect(Rng.chancyMax({ min: 0, max: 10 })).toBe(10); - expect(Rng.chancyMax({ mean: 0.5, type: 'normal' })).toBe(Number.POSITIVE_INFINITY); - expect(Rng.chancyMax({ mean: 0.5, max: 10, type: 'normal' })).toBe(10); - }); - - test('chancy min', () => { - expect(Rng.chancyMin('1d6+2')).toBe(3); - expect(Rng.chancyMin('2d6+2')).toBe(4); - expect(Rng.chancyMin('2d6+5')).toBe(7); - expect(Rng.chancyMin(5)).toBe(5); - expect(Rng.chancyMin({ min: 0 })).toBe(0); - expect(Rng.chancyMin({ min: 0, max: 10 })).toBe(0); - expect(Rng.chancyMin({ min: 5 })).toBe(5); - expect(Rng.chancyMin({ min: 5, max: 10 })).toBe(5); - expect(Rng.chancyMin({ mean: 0.5, type: 'normal' })).toBe(Number.NEGATIVE_INFINITY); - expect(Rng.chancyMin({ mean: 0.5, max: 10, type: 'normal' })).toBe(Number.NEGATIVE_INFINITY); - expect(Rng.chancyMin({ mean: 0.5, min: -10, type: 'normal' })).toBe(-10); - }); -}); diff --git a/tests/rng/chancy.test.ts b/tests/rng/chancy.test.ts new file mode 100644 index 0000000..9a416ce --- /dev/null +++ b/tests/rng/chancy.test.ts @@ -0,0 +1,360 @@ +import { default as Rng } from './../../src/rng'; +import { RngInterface, RngDistributionsInterface, ChancyInterface } from './../../src/rng/interface'; +import { default as PredictableRng } from './../../src/rng/predictable'; + +const defaultResultSet = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; +const smruns = 100; +const mdruns = 1000; +const lgruns = 10000; + +describe('Testing Chancy Specifically', () => { + let rng: Rng; + let prng: PredictableRng; + + beforeEach(() => { + rng = new Rng(); + prng = new PredictableRng(); + }); + + describe('Basic functionality tests', () => { + test('should handle array input correctly', () => { + const input = [1, 2, 3, 4, 5]; + jest.spyOn(rng, 'choice').mockReturnValue(3); + const result = rng.chancy(input); + expect(result).toBe(3); + expect(rng.choice).toHaveBeenCalledWith(input); + }); + + test('should handle string input correctly', () => { + const input = '3d6'; + jest.spyOn(rng, 'dice').mockReturnValue(10); + const result = rng.chancy(input); + expect(result).toBe(10); + expect(rng.dice).toHaveBeenCalledWith(input); + }); + + test('should handle integer input correctly', () => { + const input = { type: 'int', min: 1, max: 10 } as const; + jest.spyOn(rng, 'randInt').mockReturnValue(7); + const result = rng.chancy(input); + expect(result).toBe(7); + expect(rng.randInt).toHaveBeenCalledWith(1, 10, undefined); + }); + + test('should handle random input correctly', () => { + const input = { type: 'random', min: 0, max: 1 } as const; + jest.spyOn(rng, 'random').mockReturnValue(0.5); + const result = rng.chancy(input); + expect(result).toBe(0.5); + expect(rng.random).toHaveBeenCalledWith(0, 1, undefined); + }); + + test('should handle skew input correctly', () => { + const input = { type: 'random', min: 0, max: 1, skew: 0.5 } as const; + jest.spyOn(rng, 'random').mockReturnValue(0.7); + const result = rng.chancy(input); + expect(result).toBe(0.7); + expect(rng.random).toHaveBeenCalledWith(0, 1, 0.5); + }); + + test('should handle various distributions', () => { + const distributions = [ + { type: 'normal', mean: 0, stddev: 1 }, + { type: 'gamma', shape: 2, rate: 1 }, + { type: 'exponential', rate: 1 }, + { type: 'beta', alpha: 2, beta: 2 } + ] as const; + + distributions.forEach(dist => { + const methodName = dist.type as keyof (RngInterface & RngDistributionsInterface); + jest.spyOn(rng, methodName).mockReturnValue(1); + const result = rng.chancy(dist); + expect(result).toBe(1); + expect(rng[methodName]).toHaveBeenCalledWith(dist); + }); + }); + + test('should handle number input correctly', () => { + const input = 5; + const result = rng.chancy(input); + expect(result).toBe(5); + }); + + test('should throw error for invalid input', () => { + const input = { type: 'invalid_type' }; + // @ts-ignore + expect(() => rng.chancy(input)).toThrow('Invalid input type given to chancy: "invalid_type".'); + }); + + test('should respect maximum recursion depth', () => { + const input : ChancyInterface = { type: 'random', min: 10, max: 20 }; + // @ts-ignore + jest.spyOn(rng, 'shouldThrowOnMaxRecursionsReached').mockReturnValue(true); + expect(() => rng.chancy(input, 1000)).toThrow(); + }); + + test('should handle specific distributions correctly', () => { + const inputNormal = { type: 'normal', mean: 0, stddev: 1 } as const; + jest.spyOn(rng, 'normal').mockReturnValue(1); + const resultNormal = rng.chancy(inputNormal); + expect(resultNormal).toBe(1); + expect(rng.normal).toHaveBeenCalledWith(inputNormal); + + const inputGamma = { type: 'gamma', shape: 2, rate: 1 } as const; + jest.spyOn(rng, 'gamma').mockReturnValue(3); + const resultGamma = rng.chancy(inputGamma); + expect(resultGamma).toBe(3); + expect(rng.gamma).toHaveBeenCalledWith(inputGamma); + }); + }); + + test('Number just returns that number', () => { + expect(rng.chancy(1)).toBe(1); + expect(rng.chancy(-1)).toBe(-1); + }, 100); + + test('min and max always within bounds', () => { + const minMaxes = [ + [-5, 5], + [0, 1], + [100, 200], + [-10, -5] + ]; + for (const [min, max] of minMaxes) { + for (let i = 0; i < mdruns; i++) { + const r = rng.chancy({ min, max }); + expect(r).toBeGreaterThanOrEqual(min); + expect(r).toBeLessThan(max); + } + } + }, 5000); + + test('chancyInt', () => { + const r1 = rng.chancyInt(5.5); + expect(Number.isInteger(r1)).toBeTruthy(); + + const r2 = rng.chancyInt(5); + expect(r2).toBe(5); + expect(Number.isInteger(r2)).toBeTruthy(); + + const r3 = rng.chancyInt('2d6'); + expect(Number.isInteger(r3)).toBeTruthy(); + + const r4 = rng.chancyInt({ min: 1, max: 3 }); + expect(Number.isInteger(r4)).toBeTruthy(); + }, 100); + + test('chancyInt - with array always produces ints', () => { + expect(Number.isInteger(rng.chancyInt([1.1, 2.2, 3.3]))).toBeTruthy(); + expect(Number.isInteger(rng.chancyInt([-5.5, -10.10, -11.11]))).toBeTruthy(); + expect(rng.chancyInt([1])).toBe(1); + expect(rng.chancyInt([2.2])).toBe(2); + expect(rng.chancyInt([2.7])).toBe(3); + expect(rng.chancyInt([0o777])).toBe(511); + expect(rng.chancyInt([0XA])).toBe(10); + expect(rng.chancyInt([Number.MAX_SAFE_INTEGER])).toBe(Number.MAX_SAFE_INTEGER); + expect(rng.chancyInt([Number.EPSILON])).toBe(0); + expect(rng.chancyInt([Number.POSITIVE_INFINITY])).toBe(Number.POSITIVE_INFINITY); + expect(rng.chancyInt([Number.NEGATIVE_INFINITY])).toBe(Number.NEGATIVE_INFINITY); + expect(rng.chancyInt([0b01010])).toBe(10); + expect(rng.chancyInt([0b10000000000000000000000000000000])).toBe(2_147_483_648); + + expect(rng.chancyInt(['1'])).toBe(1); + expect(rng.chancyInt(['2.2'])).toBe(2); + expect(rng.chancyInt(['2.7'])).toBe(3); + expect(rng.chancyInt(['512e-2'])).toBe(5); + expect(rng.chancyInt(['592e-2'])).toBe(6); + }, 100); + + test('chancyInt - with array of non-numbers throws', () => { + expect(() => rng.chancyInt(['a'])).toThrow(); + expect(() => rng.chancyInt([1, 2, 3, 'a'])).toThrow(); + expect(() => rng.chancyInt(['Number.MAX_VALUE'])).toThrow(); + }, 100); + + test('choice', () => { + const from = ['a', 'b', 'c']; + prng.results = [0]; + + expect(prng.chancy(from)).toBe('a'); + + const r = rng.chancy(from); + expect(from.includes(r)).toBeTruthy(); + expect(['d', 'e', 'f'].includes(rng.chancy(['d', 'e', 'f']))).toBeTruthy(); + }, 100); + + test('choice - invalid args produces invalid results', () => { + const from = ['a', 'b', 'c']; + expect(() => rng.chancyMax(from)).toThrow(); + expect(() => rng.chancyMin(from)).toThrow(); + }, 100); + + test('chancy - basic usage', () => { + prng.results = defaultResultSet; + expect(prng.chancy(5)).toBe(5); + expect(prng.chancy({ min: 0, max: 10 })).toBe(0); + expect(prng.chancy({ min: 0, max: 10 })).toBe(1); + expect(prng.chancy({ min: 0, max: 1 })).toBe(0.2); + }, 100); + + test('chancy - type int / integer', () => { + prng.results = [0.5]; + expect(prng.chancy({ type: 'int', min: 0, max: 10 })).toBe(5); + expect(prng.chancy({ type: 'integer', min: 0, max: 10 })).toBe(5); + }, 100); + + test('chancy - random / int / integer with min but no max produces results', () => { + prng.results = [0.5]; + expect(prng.chancy({ type: 'random', min: 0 })).toMatchInlineSnapshot('4503599627370495.5'); + expect(prng.chancy({ type: 'int', min: 0 })).toMatchInlineSnapshot('4503599627370496'); + expect(prng.chancy({ type: 'integer', min: 0 })).toMatchInlineSnapshot('4503599627370496'); + + expect(rng.chancy({ type: 'random', min: 10 })).toBeGreaterThanOrEqual(10); + expect(rng.chancy({ type: 'int', min: 10 })).toBeGreaterThanOrEqual(10); + expect(rng.chancy({ type: 'integer', min: 10 })).toBeGreaterThanOrEqual(10); + }, 100); + + test('chancy - normal', () => { + const results = []; + const mean = 0.5; + for (let i = 0; i < 100000; i++) { + results.push(rng.chancy({ min: 0, max: 1, type: 'normal' })); + } + const sum = results.reduce((a, b) => a + b); + const calcMean = sum / results.length; + + expect(Math.abs(mean - calcMean)).toBeLessThan(0.003); + + expect(Math.max(...results)).toBeLessThan(0.9999); + expect(Math.min(...results)).toBeGreaterThan(0.0001); + }, 5000); + + test('chancy - normal_integer', () => { + const results = []; + const mean = 50; + for (let i = 0; i < 100000; i++) { + results.push(rng.chancy({ min: 0, max: 100, type: 'normal_integer' })); + } + const sum = results.reduce((a, b) => a + b); + const calcMean = sum / results.length; + + expect(Math.abs(mean - calcMean)).toBeLessThan(1); + }, 1000); + + const distributions = [ + 'normal', + 'gaussian', + 'boxMuller', + 'irwinHall', + 'bates', + 'bernoulli', + 'exponential', + 'pareto', + 'poisson', + 'hypergeometric', + 'rademacher', + 'binomial', + 'betaBinomial', + 'beta', + 'gamma', + 'studentsT', + 'wignerSemicircle', + 'kumaraswamy', + 'hermite', + 'chiSquared', + 'rayleigh', + 'logNormal', + 'cauchy', + 'laplace', + 'logistic', + ]; + + test.each(distributions)('Test all chancy distributions', (type) => { + expect(() => { + // @ts-ignore + rng.chancy({ type }); + }).not.toThrow(); + + expect(() => { + // @ts-ignore + rng.chancyMin({ type }); + }).not.toThrow(); + + expect(() => { + // @ts-ignore + rng.chancyMax({ type }); + }).not.toThrow(); + + rng.randomSource(() => 0.1); + // @ts-ignore + expect(rng.chancy({ type })).toBe(rng[type]()); + }, 1000); + + test('chancy max', () => { + expect(rng.chancyMax('1d6+2')).toBe(8); + expect(Rng.chancyMax('1d6+2')).toBe(8); + expect(Rng.chancyMax('2d6+2')).toBe(14); + expect(Rng.chancyMax('2d6+5')).toBe(17); + expect(Rng.chancyMax(5)).toBe(5); + expect(Rng.chancyMax({ min: 0, max: 10 })).toBe(10); + expect(Rng.chancyMax({})).toBe(1); + expect(Rng.chancyMax({ type: 'random' })).toBe(1); + expect(Rng.chancyMax({ type: 'integer' })).toBe(1); + expect(Rng.chancyMax({ min: 0, type: 'integer' })).toBe(Number.MAX_SAFE_INTEGER); + expect(Rng.chancyMax({ min: 0, type: 'random' })).toBe(Number.MAX_SAFE_INTEGER); + expect(Rng.chancyMax({ min: 0, max: 10, type: 'integer' })).toBe(10); + expect(Rng.chancyMax({ mean: 0.5, type: 'normal' })).toBe(Number.POSITIVE_INFINITY); + expect(Rng.chancyMax({ mean: 0.5, type: 'normal_integer' })).toBe(Number.POSITIVE_INFINITY); + expect(Rng.chancyMax({ mean: 0.5, max: 10, type: 'normal' })).toBe(10); + expect(Rng.chancyMax([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toBe(10); + }, 100); + + test('chancy min', () => { + expect(rng.chancyMin('1d6+2')).toBe(3); + expect(Rng.chancyMin('1d6+2')).toBe(3); + expect(Rng.chancyMin('2d6+2')).toBe(4); + expect(Rng.chancyMin('2d6+5')).toBe(7); + expect(Rng.chancyMin(5)).toBe(5); + expect(Rng.chancyMin({})).toBe(0); + expect(Rng.chancyMin({ type: 'integer' })).toBe(0); + expect(Rng.chancyMin({ min: 0 })).toBe(0); + expect(Rng.chancyMin({ min: 0, max: 10 })).toBe(0); + expect(Rng.chancyMin({ min: 5 })).toBe(5); + expect(Rng.chancyMin({ min: 5, max: 10 })).toBe(5); + expect(Rng.chancyMin({ min: 5, max: 10, type: 'integer' })).toBe(5); + expect(Rng.chancyMin({ mean: 0.5, type: 'normal' })).toBe(Number.NEGATIVE_INFINITY); + expect(Rng.chancyMin({ mean: 0.5, type: 'normal_integer' })).toBe(Number.NEGATIVE_INFINITY); + expect(Rng.chancyMin({ mean: 0.5, max: 10, type: 'normal' })).toBe(Number.NEGATIVE_INFINITY); + expect(Rng.chancyMin({ mean: 0.5, min: -10, type: 'normal' })).toBe(-10); + expect(Rng.chancyMin([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])).toBe(1); + }, 100); + + test('chancy with invalid type throws', () => { + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancy({ type: 'foobar' })).toThrow(); + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancyMin({ type: 'foobar' })).toThrow(); + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancyMax({ type: 'foobar' })).toThrow(); + }, 100); + + test('chancy with invalid input throws', () => { + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancy()).toThrow(); + + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancy(false)).toThrow(); + + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancyMin()).toThrow(); + + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancyMin(false)).toThrow(); + + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancyMax()).toThrow(); + + // @ts-ignore - we are deliberately passing nonsense + expect(() => rng.chancyMax(false)).toThrow(); + }, 100); +}); diff --git a/tests/rng/distributions.test.ts b/tests/rng/distributions.test.ts new file mode 100644 index 0000000..ea5e1aa --- /dev/null +++ b/tests/rng/distributions.test.ts @@ -0,0 +1,750 @@ +import { default as Rng } from './../../src/rng'; +import { default as PredictableRng } from './../../src/rng/predictable'; +import { NumberValidationError } from './../../src/number'; +import { Distribution } from './../../src/rng/interface'; + +const defaultResultSet = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; +const smruns = 100; +const mdruns = 1000; +const lgruns = 10000; + +describe('Testing RNG Distributions specifically', () => { + let rng: Rng; + let prng: PredictableRng; + + beforeEach(() => { + rng = new Rng(); // Replace with your actual constructor + prng = new PredictableRng(); // Replace with your actual constructor + }); + + afterEach(() => { + prng.reset(); + }); + + describe('Basic functionality tests', () => { + beforeEach(() => { + rng = new Rng(); // Replace with your actual constructor + prng = new PredictableRng(); // Replace with your actual constructor + }); + + afterEach(() => { + prng.reset(); + }); + + test('should generate a value from the Irwin-Hall distribution', () => { + const result = rng.irwinHall({ n: 10 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Bates distribution', () => { + const result = rng.bates({ n: 10 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Bates Gaussian distribution', () => { + const result = rng.batesgaussian({ n: 10 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Bernoulli distribution', () => { + const result = rng.bernoulli({ p: 0.5 }); + expect([0, 1]).toContain(result); + }); + + test('should generate a value from the Exponential distribution', () => { + const result = rng.exponential({ rate: 1 }); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test('should generate a value from the Pareto distribution', () => { + const result = rng.pareto({ shape: 1, scale: 1 }); + expect(result).toBeGreaterThanOrEqual(1); + }); + + test('should generate a value from the Poisson distribution', () => { + const result = rng.poisson({ lambda: 2 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Hypergeometric distribution', () => { + const result = rng.hypergeometric({ N: 100, K: 10, n: 10 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Rademacher distribution', () => { + const result = rng.rademacher(); + expect([-1, 1]).toContain(result); + }); + + test('should generate a value from the Binomial distribution', () => { + const result = rng.binomial({ n: 10, p: 0.5 }); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(10); + }); + + test('should generate a value from the Beta-Binomial distribution', () => { + const result = rng.betaBinomial({ alpha: 2, beta: 2, n: 10 }); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(10); + }); + + test('should generate a value from the Beta distribution', () => { + const result = rng.beta({ alpha: 2, beta: 2 }); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(1); + }); + + test('should generate a value from the Gamma distribution', () => { + const result = rng.gamma({ shape: 2, rate: 1 }); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test('should generate a value from the Student\'s t-distribution', () => { + const result = rng.studentsT({ nu: 5 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Wigner Semicircle distribution', () => { + const result = rng.wignerSemicircle({ R: 1 }); + expect(result).toBeGreaterThanOrEqual(-1); + expect(result).toBeLessThanOrEqual(1); + }); + + test('should generate a value from the Kumaraswamy distribution', () => { + const result = rng.kumaraswamy({ alpha: 2, beta: 2 }); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(1); + }); + + test('should generate a value from the Hermite distribution', () => { + const result = rng.hermite({ lambda1: 1, lambda2: 2 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Chi-Squared distribution', () => { + const result = rng.chiSquared({ k: 2 }); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test('should generate a value from the Rayleigh distribution', () => { + const result = rng.rayleigh({ scale: 1 }); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test('should generate a value from the Log-Normal distribution', () => { + const result = rng.logNormal({ mean: 0, stddev: 1 }); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test('should generate a value from the Cauchy distribution', () => { + const result = rng.cauchy({ median: 0, scale: 1 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Laplace distribution', () => { + const result = rng.laplace({ mean: 0, scale: 1 }); + expect(result).toEqual(expect.any(Number)); + }); + + test('should generate a value from the Logistic distribution', () => { + const result = rng.logistic({ mean: 0, scale: 1 }); + expect(result).toEqual(expect.any(Number)); + }); + }); + + test('Getting and setting predictable results', () => { + prng.results = [0.5]; + expect(prng.results).toEqual([0.5]); + + prng.results = defaultResultSet; + expect(prng.results).toEqual(defaultResultSet); + }); + + // Normal Distributions + test('boxMuller', () => { + expect(() => { + prng.boxMuller({ mean: 0.5, stddev: 0 }); + }).not.toThrow(); + + expect(() => { + prng.boxMuller(0.5, 0); + }).not.toThrow(); + + expect(prng.boxMuller({ mean: 0.5, stddev: 0 })).toBe(0.5); + + expect(() => { + prng.boxMuller({ mean: 0.5, stddev: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + prng.boxMuller(0.5, -1); + }).toThrow(NumberValidationError); + + let sum = 0; + for (let i = 0; i < lgruns; i++) { + sum += rng.boxMuller({ mean: 0.5, stddev: 0.1 }); + } + expect(sum / lgruns).toBeCloseTo(0.5); + }); + + // rng.irwinHall({ n }: { n?: number }); + describe('irwinHall', () => { + test('irwinHall - basic usage', () => { + expect(() => { + rng.irwinHall(); + rng.irwinHall(100); + rng.irwinHall({ n: 100 }); + }).not.toThrow(); + }); + + test('irwinHall - invalid input throws', () => { + expect(() => { + rng.irwinHall(-5); + }).toThrow(NumberValidationError); + + expect(() => { + rng.irwinHall({ n: -10 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('bates', () => { + test('bates basic', () => { + expect(() => { + rng.bates(); + rng.bates(5); + rng.bates({ n: 10 }); + }).not.toThrow(); + }); + + test('invalid input throws', () => { + expect(() => { + rng.bates(-5); + }).toThrow(NumberValidationError); + + expect(() => { + rng.bates({ n: -10 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('bernoulli', () => { + test('basic usage', () => { + expect(() => { + rng.bernoulli(); + rng.bernoulli(0.4); + rng.bernoulli({ p: 0.8 }); + }).not.toThrow(); + }); + + test('invalid input throws', () => { + expect(() => { + rng.bernoulli(-1); + }).toThrow(NumberValidationError); + + expect(() => { + rng.bernoulli({ p: 10 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('exponential', () => { + test('basic usage', () => { + expect(() => { + rng.exponential(); + rng.exponential(1); + rng.exponential({ rate: 10 }); + }).not.toThrow(); + }); + }); + + describe('pareto', () => { + test('basic usage', () => { + expect(() => { + rng.pareto(); + rng.pareto({ shape: 1 }); + rng.pareto({ shape: 1, scale: 2 }); + rng.pareto({ shape: 1, location: 5 }); + rng.pareto({ shape: 0.5, scale: 0.1, location: -5 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.pareto({ shape: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.pareto({ scale: -2 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('poisson', () => { + test('basic usage', () => { + expect(() => { + rng.poisson(); + rng.poisson(1); + rng.poisson({ lambda: 10 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.poisson(-1); + }).toThrow(NumberValidationError); + + expect(() => { + rng.poisson({ lambda: -10 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('hypergeometric', () => { + test('basic usage', () => { + expect(() => { + rng.hypergeometric(); + rng.hypergeometric({ N: 100 }); + rng.hypergeometric({ N: 100, K: 50 }); + rng.hypergeometric({ N: 100, K: 50, n: 10 }); + rng.hypergeometric({ N: 100, n: 10 }); + rng.hypergeometric({ N: 100, n: 10, k: 1 }); + rng.hypergeometric({ N: 100, k: 1 }); + rng.hypergeometric({ N: 100, K: 50, n: 10, k: 1 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.hypergeometric({ N: 100, K: 150 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hypergeometric({ N: 100, n: 150 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hypergeometric({ N: 100, K: 50, n: 150 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hypergeometric({ N: 100, K: 50, n: 30, k: 55 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hypergeometric({ N: 100, K: 50, n: 30, k: 45 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hypergeometric({ N: 100, K: 50, k: 55 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('rademacher', () => { + test('basic usage', () => { + expect(() => { + rng.rademacher(); + }).not.toThrow(); + }); + }); + + describe('binomial', () => { + test('basic usage', () => { + expect(() => { + rng.binomial({ n: 100, p: 0.5 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.binomial({ n: 100, p: -0.5 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('betaBinomial', () => { + test('betaBinomial - basic usage', () => { + expect(() => { + rng.betaBinomial(); + rng.betaBinomial({ alpha: 1 }); + rng.betaBinomial({ beta: 1 }); + rng.betaBinomial({ alpha: 3, beta: 2 }); + rng.betaBinomial({ n: 100 }); + rng.betaBinomial({ alpha: 1, n: 1 }); + rng.betaBinomial({ beta: 1, n: 50 }); + rng.betaBinomial({ alpha: 1, beta: 1, n: 999999 }); + }).not.toThrow(); + }); + + test('betaBinomial - invalid arguments throws', () => { + expect(() => { + rng.betaBinomial({ alpha: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.betaBinomial({ beta: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.betaBinomial({ n: 0.5 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.betaBinomial({ n: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.betaBinomial({ n: 0 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('beta', () => { + test('basic usage', () => { + expect(() => { + rng.beta(); + rng.beta({ beta: 1 }); + rng.beta({ alpha: 2 }); + rng.beta({ alpha: 3, beta: 4 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.beta({ alpha: -3, beta: 4 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.beta({ alpha: 3, beta: -4 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.beta({ beta: -4 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.beta({ alpha: 0 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('gamma', () => { + test('basic usage', () => { + expect(() => { + rng.gamma(); + rng.gamma({ shape: 1 }); + rng.gamma({ shape: 2, rate: 2 }); + rng.gamma({ shape: 5, scale: 4 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.gamma({ shape: -1 }); + rng.gamma({ shape: 2, rate: -2 }); + rng.gamma({ shape: 5, scale: -4 }); + }).toThrow(NumberValidationError); + }); + + test('specifying rate and scale compatible doesnt throw', () => { + expect(() => { + rng.gamma({ rate: 2, scale: 1 / 2 }); + }).not.toThrow(); + + expect(() => { + rng.gamma({ rate: 1 / 4, scale: 4 }); + }).not.toThrow(); + }); + + test('specifying rate and scale incompatible throws', () => { + expect(() => { + rng.gamma(); + rng.gamma({ rate: 2, scale: 2 }); + }).toThrow(); + }); + }); + + describe('studentsT', () => { + test('studentsT - basic usage', () => { + expect(() => { + rng.studentsT(); + rng.studentsT(1); + rng.studentsT({ nu: 5 }); + }).not.toThrow(); + }); + + test('studentsT - invalid arguments throws', () => { + expect(() => { + rng.studentsT(-5); + }).toThrow(NumberValidationError); + + expect(() => { + rng.studentsT({ nu: -0.1 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('wignerSemicircle', () => { + test('wignerSemicircle - basic usage', () => { + expect(() => { + rng.wignerSemicircle(); + rng.wignerSemicircle(1); + rng.wignerSemicircle({ R: 10 }); + }).not.toThrow(); + }); + + test('wignerSemicircle - invalid arguments throws', () => { + expect(() => { + rng.wignerSemicircle(-2); + }).toThrow(NumberValidationError); + + expect(() => { + rng.wignerSemicircle({ R: 0 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('kumaraswamy', () => { + test('basic usage', () => { + expect(() => { + rng.kumaraswamy(); + rng.kumaraswamy({ alpha: 10 }); + rng.kumaraswamy({ beta: 10 }); + rng.kumaraswamy({ alpha: 10, beta: 10 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.kumaraswamy({ alpha: -10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.kumaraswamy({ beta: -10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.kumaraswamy({ alpha: 10, beta: -10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.kumaraswamy({ alpha: -10, beta: 10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.kumaraswamy({ alpha: -10, beta: -10 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('hermite', () => { + test('basic usage', () => { + expect(() => { + rng.hermite(); + rng.hermite({ lambda1: 10 }); + rng.hermite({ lambda2: 10 }); + rng.hermite({ lambda1: 10, lambda2: 10 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.hermite({ lambda1: -10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hermite({ lambda2: -10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hermite({ lambda1: 10, lambda2: -10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hermite({ lambda1: -10, lambda2: 10 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.hermite({ lambda1: -10, lambda2: -10 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('chiSquared', () => { + test('chiSquared - basic usage', () => { + expect(() => { + rng.chiSquared(); + rng.chiSquared(10); + rng.chiSquared({ k: 1 }); + }).not.toThrow(); + }); + + test('chiSquared - basic usage', () => { + expect(() => { + rng.chiSquared(-10); + }).toThrow(NumberValidationError); + + expect(() => { + rng.chiSquared({ k: -1 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('rayleigh', () => { + test('basic usage', () => { + expect(() => { + rng.rayleigh(); + rng.rayleigh(1); + rng.rayleigh({ scale: 10 }); + }).not.toThrow(); + }); + + test('basic usage', () => { + expect(() => { + rng.rayleigh(-10); + }).toThrow(NumberValidationError); + + expect(() => { + rng.rayleigh({ scale: -1 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('logNormal', () => { + test('logNormal - basic usage', () => { + expect(() => { + rng.logNormal(); + rng.logNormal({ mean: 0.5 }); + rng.logNormal({ stddev: 0.1 }); + rng.logNormal({ mean: -5, stddev: 0.9 }); + }).not.toThrow(); + }); + + test('logNormal - invalid arguments throws', () => { + expect(() => { + rng.logNormal({ stddev: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.logNormal({ mean: -5, stddev: -0.9 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('cauchy', () => { + test('basic usage', () => { + expect(() => { + rng.cauchy(); + rng.cauchy({ median: 0.5 }); + rng.cauchy({ scale: 0.1 }); + rng.cauchy({ median: -5, scale: 0.9 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.cauchy({ scale: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.cauchy({ median: -5, scale: -0.9 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('laplace', () => { + test('basic usage', () => { + expect(() => { + rng.laplace(); + rng.laplace({ mean: 0.5 }); + rng.laplace({ scale: 0.1 }); + rng.laplace({ mean: -5, scale: 0.9 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.laplace({ scale: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.laplace({ mean: -5, scale: -0.9 }); + }).toThrow(NumberValidationError); + }); + }); + + describe('logistic', () => { + test('basic usage', () => { + expect(() => { + rng.logistic(); + rng.logistic({ mean: 0.5 }); + rng.logistic({ scale: 0.1 }); + rng.logistic({ mean: -5, scale: 0.9 }); + }).not.toThrow(); + }); + + test('invalid arguments throws', () => { + expect(() => { + rng.logistic({ scale: -1 }); + }).toThrow(NumberValidationError); + + expect(() => { + rng.logistic({ mean: -5, scale: -0.9 }); + }).toThrow(NumberValidationError); + }); + }); + + const distributions: Distribution[] = [ + 'normal', + 'gaussian', + 'boxMuller', + 'irwinHall', + 'bates', + 'bernoulli', + 'exponential', + 'pareto', + 'poisson', + 'hypergeometric', + 'rademacher', + 'binomial', + 'betaBinomial', + 'beta', + 'gamma', + 'studentsT', + 'wignerSemicircle', + 'kumaraswamy', + 'hermite', + 'chiSquared', + 'rayleigh', + 'logNormal', + 'cauchy', + 'laplace', + 'logistic', + ]; + + describe.each(distributions)('Test default args are the same for empty object or no args', (distribution: Distribution) => { + test(`Test ${distribution} default args are the same for empty object or no args`, () => { + const rng = new Rng(); + rng.randomSource(() => 0.1); + + // @ts-ignore - otherwise we get errors + expect(rng[distribution]()).toBe(rng[distribution]({})); + }, 1000); + }); + + const distributionsAndRandom : [Distribution, any, number][] = [ + ['beta', { alpha: 100000000 + 1 }, 1 - Number.EPSILON], + ['poisson', { lambda: 100 }, 1 - Number.EPSILON], + ['gamma', { shape: 1 / 2, rate: 2 }, 0.5], + ['studentsT', {}, 0.5], + ]; + + test.each(distributionsAndRandom)('Test non-random throws for sculpted values', (distribution: Distribution, args, num: number) => { + const rng = new PredictableRng(); + rng.results = [num]; + + // @ts-ignore - otherwise we get errors + expect(() => rng[distribution](args)).toThrow(); + }, 1000); +}); diff --git a/tests/rng/numbervalidator.test.ts b/tests/rng/numbervalidator.test.ts new file mode 100644 index 0000000..3c40923 --- /dev/null +++ b/tests/rng/numbervalidator.test.ts @@ -0,0 +1,138 @@ +import { default as validate, NumberValidationError, ArrayNumberValidator, NumberValidator } from './../../src/number'; + +describe('Testing Number Validation', () => { + describe('validate function', () => { + test('validate returns correct validator for array', () => { + expect(validate([1, 2, 3])).toBeInstanceOf(ArrayNumberValidator); + }); + + test('validate returns correct validator for array', () => { + expect(validate(5)).toBeInstanceOf(NumberValidator); + }); + + test('basic suite', () => { + expect(() => { + // Validate single numbers + validate(2).gt(1).lt(3); + + // Also used with arrays of numbers + validate([1, 2, 3]).lt(10); + validate([1, 2, 3]).sumto(6); + + // All single number validations + validate(1).int(); + validate(1).positive(); + validate(-1).negative(); + validate(1).between(0, 2); + validate(1).betweenEq(1, 2); + validate(1).gt(0); + validate(1).gteq(1); + validate(1).lt(2); + validate(1).lteq(1); + + // All array of numbers validations + validate([1, 2, 3]).sumcloseto(6); + validate([1, 2, 3.0001]).sumcloseto(6, 0.001); + validate([1, 2, 3]).sumto(6); + validate([1, 2, 3]).sumtolt(7); + validate([1, 2, 3]).sumtogt(5); + validate([1, 2, 3]).sumtolteq(6); + validate([1, 2, 3]).sumtogteq(1); + validate([1, 2, 3]).int(); + validate([1, 2, 3]).positive(); + validate([-1, -2, -4]).negative(); + validate([1, 2, 3]).between(0, 4); + validate([1, 2, 3]).betweenEq(1, 3); + validate([1, 2, 3]).gt(0); + validate([1, 2, 3]).gteq(1); + validate([1, 2, 3]).lt(4); + validate([1, 2, 3]).lteq(3); + }).not.toThrow(); + }); + + test('Object name setting', () => { + const numvalidator = validate({ my_name: 5 }); + + expect(numvalidator.name).toBe('my_name'); + expect(numvalidator.number).toBe(5); + + const nums = [5, 6]; + const arrvalidator = validate({ my_array: nums }); + + expect(arrvalidator.name).toBe('my_array'); + expect(arrvalidator.numbers).toBe(nums); + }); + + test('fluent chaining', () => { + expect(() => { + validate(1).lteq(1).gt(0).validate(2).lt(3); + }).not.toThrow(); + + expect(() => { + validate(1).gt(0).lt(1).validate(2).lt(3); + }).toThrow(NumberValidationError); + + expect(() => { + validate(1).lteq(1).gt(0).validate([1, 2, 3]).lteq(3); + }).not.toThrow(); + + expect(() => { + validate([1, 2, 3]).gt(0).lt(4).validate([3, 2, 1]).lt(1); + }).toThrow(NumberValidationError); + + expect(() => { + validate([1, 2, 3]).gt(0).lt(4).validate(2).lt(1); + }).toThrow(NumberValidationError); + }); + + test('variable naming', () => { + expect(() => { + validate(1).varname('lambda').lteq(1).gt(0); + }).not.toThrow(); + + expect(() => { + validate(1).varname('lambda').lt(1).gt(0); + }).toThrow(NumberValidationError); + + expect(() => { + validate(1).varname('lambda').lt(1).gt(0); + }).toThrow(/lambda/); + + expect(() => { + validate([1, 2, 3]).varname('lambda').lt(1).gt(0); + }).toThrow(NumberValidationError); + + expect(() => { + validate([1, 2, 3]).varname('lambda').lt(1).gt(0); + }).toThrow(/lambda/); + }); + + test('Validate all', () => { + expect(() => { + validate().all([1, 2, 3]).lt(6); + }).not.toThrow(); + + expect(() => { + validate().all([1, 2, 3]).gt(2); + }).toThrow(NumberValidationError); + }); + + test('Validating nothing doesn\'t work', () => { + expect(() => { + validate().gt(2); + }).toThrow(NumberValidationError); + }); + + test('Trying to validate non-number doesn\'t work', () => { + expect(() => { + // @ts-ignore + validate('a').gt(2); + }).toThrow(NumberValidationError); + + expect(() => { + // @ts-ignore + validate([1, 2, 3, 'a']).gt(2); + }).toThrow(NumberValidationError); + }); + }); +}); diff --git a/tests/rng/pool.test.ts b/tests/rng/pool.test.ts new file mode 100644 index 0000000..7639997 --- /dev/null +++ b/tests/rng/pool.test.ts @@ -0,0 +1,143 @@ +import { PoolEmptyError, PoolNotEnoughElementsError } from './../../src/rng/pool'; +import Rng from './../../src/rng'; +import Pool from './../../src/rng/pool'; + +describe('Testing Pools', () => { + test.concurrent('Basic test', () => { + const pool = new Pool(['a', 'b', 'c', 'd']); + pool.draw(); + pool.draw(); + pool.draw(); + pool.draw(); + expect(() => pool.draw()).toThrow(PoolEmptyError); + }); + + test.concurrent('setEntries, .entries and getEntries', () => { + const pool = new Pool(); + pool.setEntries(['a', 'b', 'c', 'd']); + expect(pool.entries).toContainEqual('a'); + expect(pool.getEntries()).toContainEqual('b'); + expect(pool.getEntries()).toContainEqual('c'); + expect(pool.getEntries()).toContainEqual('d'); + }); + + test.concurrent('setRng', () => { + const pool = new Pool(); + const rng = new Rng(); + pool.setRng(rng); + expect(pool.getRng()).toBe(rng); + }); + + test.concurrent('Basic test, correct order', () => { + const rng = new Rng(); + // This sort or relies on knowing how pool implements + // random numbers - but we can test it + rng.randomSource(() => 0); + const pool = rng.pool(['a', 'b', 'c', 'd']); + + expect(pool.draw()).toBe('a'); + expect(pool.draw()).toBe('b'); + expect(pool.draw()).toBe('c'); + expect(pool.draw()).toBe('d'); + expect(() => pool.draw()).toThrow(PoolEmptyError); + + rng.randomSource(); + }); + + test.concurrent('Test original array is not modified', () => { + const rng = new Rng(); + const src = ['a', 'b', 'c', 'd']; + const pool = rng.pool(src); + + for (let i = 0; i < 4; i++) { + pool.draw(); + } + + expect(() => pool.draw()).toThrow(PoolEmptyError); + expect(src.length).toBe(4); + }); + + test.concurrent('Test everything drawn is from original array', () => { + const rng = new Rng(); + const src = ['a', 'b', 'c', 'd']; + const pool = rng.pool(src); + + for (let i = 0; i < 4; i++) { + expect(src).toContain(pool.draw()); + } + + expect(() => pool.draw()).toThrow(PoolEmptyError); + }); + + test.concurrent('Test with repeated entries', () => { + const rng = new Rng(); + const src = ['a', 'b', 'c', 'd', 'a', 'b', 'c', 'd']; + const pool = rng.pool(src); + + for (let i = 0; i < 8; i++) { + expect(src).toContain(pool.draw()); + } + + expect(() => pool.draw()).toThrow(PoolEmptyError); + }); + + test.concurrent('Test with mixed entry types', () => { + const rng = new Rng(); + const src = ['a', 1, 1.2, 0.1, -1, {}, new Set(), Symbol('foo'), ['another array']]; + const pool = rng.pool(src); + + for (let i = 0; i < src.length; i++) { + expect(src).toContain(pool.draw()); + } + + expect(() => pool.draw()).toThrow(PoolEmptyError); + }); + + test.concurrent('Drawing multiple', () => { + const rng = new Rng(); + const pool = rng.pool(['a', 'b', 'c', 'd']); + + expect(pool.drawMany(2)).toHaveLength(2); + expect(pool.drawMany(2)).toHaveLength(2); + expect(() => pool.drawMany(2)).toThrow(PoolEmptyError); + }); + + test.concurrent('Drawing too many throws error', () => { + const rng = new Rng(); + const pool = rng.pool(['a', 'b', 'c', 'd']); + expect(() => pool.drawMany(5)).toThrow(PoolNotEnoughElementsError); + }); + + test.concurrent('Drawing < 0 throws', () => { + const rng = new Rng(); + const pool = rng.pool(['a', 'b', 'c', 'd']); + expect(() => pool.drawMany(-1)).toThrow(); + }); + + test.concurrent('Returns correct length', () => { + const rng = new Rng(); + const pool = rng.pool(['a', 'b', 'c', 'd']); + expect(pool.length).toBe(4); + }); + + test.concurrent('isEmpty', () => { + const rng = new Rng(); + const pool = rng.pool([]); + expect(pool.isEmpty()).toBeTruthy(); + }); + + test.concurrent('instantiates empty', () => { + const rng = new Rng(); + const pool = rng.pool(); + expect(pool.isEmpty()).toBeTruthy(); + }); + + test.concurrent('empty empties the pool', () => { + const rng = new Rng(); + const pool = rng.pool(['a', 'b', 'c', 'd']); + expect(pool.length).toBe(4); + pool.empty(); + expect(pool.length).toBe(0); + expect(pool.isEmpty()).toBeTruthy(); + }); +}); diff --git a/tests/rng/rng.test.ts b/tests/rng/rng.test.ts new file mode 100644 index 0000000..2d02cd1 --- /dev/null +++ b/tests/rng/rng.test.ts @@ -0,0 +1,885 @@ +import { default as Rng } from './../../src/rng'; +import { default as PredictableRng } from './../../src/rng/predictable'; +import { default as Pool } from './../../src/rng/pool'; + +const defaultResultSet = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1 - Number.EPSILON]; +const smruns = Math.pow(10, 2); +const mdruns = Math.pow(10, 3); +const lgruns = Math.pow(10, 4); +const xlruns = Math.pow(10, 5); + +describe('testing Rng & predictable Rng', () => { + let rng: Rng; + let prng: PredictableRng; + + beforeEach(() => { + rng = new Rng(); // Replace with your actual constructor + prng = new PredictableRng(); // Replace with your actual constructor + }); + + afterEach(() => { + prng.reset(); + }); + + test('Getting and setting predictable results', () => { + prng.results = [0.5]; + expect(prng.results).toEqual([0.5]); + + prng.results = defaultResultSet; + expect(prng.results).toEqual(defaultResultSet); + }); + + test('Predictable results appear in proper order', () => { + prng.results = defaultResultSet; + expect(prng.random()).toBe(defaultResultSet[0]); + expect(prng.random()).toBe(defaultResultSet[1]); + expect(prng.random()).toBe(defaultResultSet[2]); + expect(prng.random()).toBe(defaultResultSet[3]); + expect(prng.random()).toBe(defaultResultSet[4]); + expect(prng.random()).toBe(defaultResultSet[5]); + expect(prng.random()).toBe(defaultResultSet[6]); + expect(prng.random()).toBe(defaultResultSet[7]); + expect(prng.random()).toBe(defaultResultSet[8]); + expect(prng.random()).toBe(defaultResultSet[9]); + expect(prng.random()).toBe(defaultResultSet[10]); + }); + + test('Predictable reset works', () => { + prng.results = defaultResultSet; + expect(prng.random()).toBe(defaultResultSet[0]); + expect(prng.random()).toBe(defaultResultSet[1]); + expect(prng.random()).toBe(defaultResultSet[2]); + expect(prng.random()).toBe(defaultResultSet[3]); + expect(prng.random()).toBe(defaultResultSet[4]); + expect(prng.random()).toBe(defaultResultSet[5]); + prng.reset(); + expect(prng.random()).toBe(defaultResultSet[0]); + expect(prng.random()).toBe(defaultResultSet[1]); + expect(prng.random()).toBe(defaultResultSet[2]); + expect(prng.random()).toBe(defaultResultSet[3]); + expect(prng.random()).toBe(defaultResultSet[4]); + expect(prng.random()).toBe(defaultResultSet[5]); + }); + + test('Predictable results reset when set', () => { + const prng = new PredictableRng(); + + prng.results = defaultResultSet; + expect(prng.random()).toBe(defaultResultSet[0]); + + prng.results = defaultResultSet; + expect(prng.random()).toBe(defaultResultSet[0]); + }); + + test('Predictable even spread', () => { + const numsToTest = [3, 6, 9, 12]; + for (const num of numsToTest) { + prng.setEvenSpread(num); + for (let i = 0; i < num - 1; i++) { + expect(prng.random()).toBe(i / (num - 1)); + } + expect(prng.random()).toBeCloseTo(1); + } + }); + + test('Predictable same as', () => { + const p1 = new PredictableRng(); + const p2 = new PredictableRng(); + + p1.results = [0.1, 0.2, 0.3]; + p2.results = [0.1, 0.2, 0.3]; + + expect(p1.sameAs(p2)).toBeTruthy(); + expect(p2.sameAs(p1)).toBeTruthy(); + }); + + test('rng same as with default args should not be true', () => { + const p1 = new Rng(); + const p2 = new Rng(); + + expect(p1.sameAs(p2)).not.toBeTruthy(); + expect(p2.sameAs(p1)).not.toBeTruthy(); + }); + + test('should initialize with seed', () => { + const seedRng = new Rng('test-seed'); + expect(seedRng.getSeed()).toEqual(expect.any(Number)); + }); + + test('rng same as with same seed should be true', () => { + const p1 = new Rng(); + const p2 = new Rng(); + const p3 = new Rng('abc'); + const p4 = new Rng('abc'); + + expect(p3.sameAs(p4)).toBeTruthy(); + expect(p4.sameAs(p3)).toBeTruthy(); + + p1.seed('123'); + p2.seed('123'); + + expect(p1.sameAs(p2)).toBeTruthy(); + expect(p2.sameAs(p1)).toBeTruthy(); + }); + + test('rng same as with same seed and randomSource should be true', () => { + const p1 = new Rng('abc'); + const p2 = new Rng('abc'); + + const f = () => 1; + p1.randomSource(f); + + expect(p1.sameAs(p2)).not.toBeTruthy(); + expect(p2.sameAs(p1)).not.toBeTruthy(); + + p2.randomSource(f); + + expect(p1.sameAs(p2)).toBeTruthy(); + expect(p2.sameAs(p1)).toBeTruthy(); + }); + + test('rng same as returns false for arbitrary other arguments', () => { + const p1 = new Rng('abc'); + + const f = () => 1; + + expect(p1.sameAs('some string')).toBeFalsy(); + expect(p1.sameAs({})).toBeFalsy(); + expect(p1.sameAs(f)).toBeFalsy(); + expect(p1.sameAs(-1)).toBeFalsy(); + expect(p1.sameAs(10)).toBeFalsy(); + expect(p1.sameAs(new Set())).toBeFalsy(); + expect(p1.sameAs(new PredictableRng())).toBeFalsy(); + }); + + test('Predictable throws when results are empty', () => { + expect(() => { + prng.results = []; + }).toThrow(); + }); + + test('Predictable throws when >= 1', () => { + expect(() => { + prng.results = [2]; + }).toThrow(); + expect(() => { + prng.results = [1]; + }).toThrow(); + }); + + test('Predictable throws when < 0', () => { + expect(() => { + prng.results = [-1]; + }).toThrow(); + }); + + test('Test rng accepts string seed', () => { + expect(() => { + const rng = new Rng('abc'); + rng.seed('def'); + }).not.toThrow(); + }); + + test('Test rng accepts string seed', () => { + expect(() => { + const rng = new Rng('abc'); + rng.seed('def'); + }).not.toThrow(); + }); + + test('Test rng predictable sets seed', () => { + const predictableRng = rng.predictable(1234); + expect(predictableRng.getSeed()).toBe(1234); + + const predictableRngStatic = Rng.predictable(1234); + expect(predictableRngStatic.getSeed()).toBe(1234); + }); + + test('should provide predictable random sequence', () => { + const predictableRng = Rng.predictable('seed'); + const randomNum1 = predictableRng.random(); + const randomNum2 = predictableRng.random(); + const newPredictableRng = Rng.predictable('seed'); + const newRandomNum1 = newPredictableRng.random(); + const newRandomNum2 = newPredictableRng.random(); + expect(randomNum1).toBe(newRandomNum1); + expect(randomNum2).toBe(newRandomNum2); + }); + + test('Test scaling and scale normalizing', () => { + for (let i = 0; i < 100; i++) { + expect(rng.scale(i, 0, 1, 0, 100)).toBeGreaterThanOrEqual(0); + expect(rng.scale(i, 0, 1, 0, 100)).toBeLessThanOrEqual(1); + expect(rng.scale(i, 0, 1, 0, 100)).toBeCloseTo(i / 100); + } + expect(rng.scale(5, 0, 100, 0, 10)).toBe(50); + expect(rng.scale(5, 100, 200, 0, 10)).toBe(150); + for (let i = 0; i < 1; i += 0.01) { + expect(rng.scaleNorm(i, 0, 100)).toBeGreaterThanOrEqual(0); + expect(rng.scaleNorm(i, 0, 100)).toBeLessThanOrEqual(100); + expect(rng.scaleNorm(i, 0, 100)).toBeCloseTo(i * 100); + } + }); + + test('Test scale with minimum args', () => { + for (let i = 0; i < 100; i++) { + expect(rng.scale(i / 100, 0, 100)).toBeGreaterThanOrEqual(0); + expect(rng.scale(i / 100, 0, 100)).toBeLessThanOrEqual(100); + expect(rng.scale(i / 100, 0, 100)).toBeCloseTo(i); + } + + for (let i = 0; i < 100; i++) { + expect(rng.scale(i / 100, 0, 10)).toBeGreaterThanOrEqual(0); + expect(rng.scale(i / 100, 0, 10)).toBeLessThanOrEqual(10); + expect(rng.scale(i / 100, 0, 10)).toBeCloseTo(i / 10); + } + + for (let i = 0; i < 100; i++) { + expect(rng.scale(i / 100, 10, 20)).toBeGreaterThanOrEqual(10); + expect(rng.scale(i / 100, 10, 20)).toBeLessThanOrEqual(20); + expect(rng.scale(i / 100, 10, 20)).toBeCloseTo(10 + i / 10); + } + }); + + test('Test scaling with invalid parameters throws', () => { + expect(() => { + rng.scale(-1, 0, 100, 0, 1); + }).toThrow(); + expect(() => { + rng.scale(10, 0, 100, 0, 1); + }).toThrow(); + expect(() => { + rng.scaleNorm(-1, 0, 100); + }).toThrow(); + expect(() => { + rng.scaleNorm(10, 0, 100); + }).toThrow(); + }); + + test('Random returns 0, 1', () => { + for (let i = 0; i < mdruns; i++) { + const randResult = rng.random(); + expect(randResult).toBeGreaterThanOrEqual(0); + expect(randResult).toBeLessThan(1); + } + }); + + test('should generate a random number between a given range', () => { + for (let i = 0; i < smruns; i++) { + const randomNumber = rng.random(1, 10); + expect(randomNumber).toBeGreaterThanOrEqual(1); + expect(randomNumber).toBeLessThanOrEqual(10); + } + }); + + test('Random int returns int within range', () => { + for (let i = 0; i < smruns; i++) { + const randResult = rng.randInt(0, 100); + expect(randResult).toBeGreaterThanOrEqual(0); + expect(randResult).toBeLessThanOrEqual(100); + expect(Number.isInteger(randResult)).toBeTruthy(); + } + }); + + test('randInt with only from as 1 returns 1', () => { + for (let i = 0; i < smruns; i++) { + const r = rng.randInt(1); + expect(r).toBe(1); + } + }); + + test('randInt with no args returns 0-1', () => { + for (let i = 0; i < smruns; i++) { + const r = rng.randInt(); + expect(r).toBeGreaterThanOrEqual(0); + expect(r).toBeLessThanOrEqual(1); + } + }); + + test('Random int with skew', () => { + prng.results = [0.5]; + expect(prng.randInt(0, 100)).toBe(50); + expect(prng.randInt(0, 100, -1)).toBeLessThan(prng.randInt(0, 100)); + expect(prng.randInt(0, 100, 1)).toBeGreaterThan(prng.randInt(0, 100)); + expect(prng.randInt(0, 100, -2)).toBeLessThan(prng.randInt(0, 100, -1)); + expect(prng.randInt(0, 100, 2)).toBeGreaterThan(prng.randInt(0, 100, 1)); + }); + + test('randBetween', () => { + for (let i = 0; i < smruns; i++) { + const r = rng.randBetween(1, 100); + expect(r).toBeGreaterThanOrEqual(1); + expect(r).toBeLessThanOrEqual(100); + } + }); + + test('randBetween with only from', () => { + for (let i = 0; i < smruns; i++) { + const r = rng.randBetween(1); + expect(r).toBeGreaterThanOrEqual(1); + expect(r).toBeLessThanOrEqual(2); + } + }); + + test('randBetween with no args', () => { + for (let i = 0; i < smruns; i++) { + const r = rng.randBetween(); + expect(r).toBeGreaterThanOrEqual(0); + expect(r).toBeLessThanOrEqual(1); + } + }); + + test('randBetween with skew', () => { + prng.results = [0.5]; + expect(prng.randBetween(0, 100)).toBe(50); + expect(prng.randBetween(0, 100, -1)).toBeLessThan(prng.randInt(0, 100)); + expect(prng.randBetween(0, 100, 1)).toBeGreaterThan(prng.randInt(0, 100)); + expect(prng.randBetween(0, 100, -2)).toBeLessThan(prng.randInt(0, 100, -1)); + expect(prng.randBetween(0, 100, 2)).toBeGreaterThan(prng.randInt(0, 100, 1)); + }); + + test('Get/Set seed', () => { + const orig = new Rng(); + orig.seed(12345); + expect(orig.getSeed()).toBe(12345); + }); + + test('Constructor get/set seed', () => { + const orig = new Rng(12345); + expect(orig.getSeed()).toBe(12345); + }); + + test('Two instances with same seed product same random number', () => { + const a = new Rng(12345); + const b = new Rng(12345); + for (let i = 0; i < 100; i++) { + expect(a.random()).toBe(b.random()); + } + }); + + test('Seeded RNG remains the same over time.', () => { + const seed1 = new Rng('abc'); + const seed2 = new Rng(1234); + const seed3 = new Rng(1_000_000_000_000_000_000); + const seed4 = new Rng(0x00FF00); + expect(seed1.random()).toMatchInlineSnapshot('0.3614412921015173'); + expect(seed2.random()).toMatchInlineSnapshot('0.23745618015527725'); + expect(seed3.random()).toMatchInlineSnapshot('0.23227926436811686'); + expect(seed4.random()).toMatchInlineSnapshot('0.5058698654174805'); + + seed1.seed('abc'); + seed2.seed(1234); + seed3.seed(1_000_000_000_000_000_000); + seed4.seed(0x00FF00); + expect(seed1.random()).toMatchInlineSnapshot('0.3614412921015173'); + expect(seed2.random()).toMatchInlineSnapshot('0.23745618015527725'); + expect(seed3.random()).toMatchInlineSnapshot('0.23227926436811686'); + expect(seed4.random()).toMatchInlineSnapshot('0.5058698654174805'); + }); + + test('Serialize basic', () => { + const orng = new Rng(56789); + const s = orng.serialize(); + const nrng = Rng.unserialize(s); + expect(nrng.getSeed()).toEqual(orng.getSeed()); + expect(nrng.getSeed()).toEqual(56789); + expect(nrng.serialize()).toEqual(s); + }); + + test('should serialize and unserialize RNG state', () => { + const serialized = rng.serialize(); + const newRng = Rng.unserialize(serialized); + expect(newRng).toEqual(expect.any(Rng)); + }); + + test('Serialize after random number gen', () => { + const orng = new Rng(56789); + orng.random(); + orng.random(); + orng.random(); + orng.random(); + orng.random(); + const s = orng.serialize(); + const nrng = Rng.unserialize(s); + expect(nrng.getSeed()).toEqual(orng.getSeed()); + expect(nrng.serialize()).toEqual(s); + expect(nrng.sameAs(orng)); + }); + + test('Serialize and unserialize produces same random numbers', () => { + const orig = new Rng(12345); + orig.random(); + const s = orig.serialize(); + const other = Rng.unserialize(s); + expect(other.getSeed()).toBe(orig.getSeed()); + expect(other.serialize()).toEqual(s); + for (let i = 0; i < 100; i++) { + expect(orig.random()).toBe(other.random()); + } + }); + + test('should calculate probabilities correctly', () => { + const percentage = rng.percentage(); + expect(percentage).toBeGreaterThanOrEqual(0); + expect(percentage).toBeLessThanOrEqual(100); + }); + + test('percentage generates mean of ~50 over long run', () => { + let sum = 0; + for (let i = 0; i < lgruns; i++) { + const r = rng.percentage(); + sum += r; + expect(r).toBeLessThan(100); + expect(r).toBeGreaterThan(0); + } + const mean = sum / lgruns; + expect(Math.round(mean)).toBeGreaterThanOrEqual(49); + expect(Math.round(mean)).toBeLessThanOrEqual(51); + }); + + test('probability generates mean of ~0.5 over long run', () => { + let sum = 0; + for (let i = 0; i < lgruns; i++) { + const r = rng.probability(); + sum += r; + expect(r).toBeLessThan(1); + expect(r).toBeGreaterThan(0); + } + const mean = sum / lgruns; + expect(mean).toBeCloseTo(0.5, 1); + }); + + test('should evaluate chance correctly', () => { + const chance = rng.chance(1, 2); + expect(typeof chance === 'boolean').toBeTruthy(); + }); + + test('chance with predictable RNG gives correct results', () => { + const rng = new Rng(12345); + expect(rng.chance(1, 1)).toBeTruthy(); + const prng = new PredictableRng(12345, [0, 0.5, 1 - Number.EPSILON]); + expect(prng.chance(1, 10)).toBeTruthy(); + expect(prng.chance(1, 10)).toBeFalsy(); + expect(prng.chance(1, 10)).toBeFalsy(); + }); + + test('chance with only default args', () => { + const prng = new PredictableRng(12345, [0, 0.5, 1 - Number.EPSILON]); + expect(prng.chance(0.1)).toBeTruthy(); + expect(prng.chance(0.1)).toBeFalsy(); + expect(prng.chance(0.1)).toBeFalsy(); + }); + + test('chanceTo', () => { + const rng = new Rng(12345); + expect(rng.chanceTo(1, 0)).toBeTruthy(); + const prng = new PredictableRng(12345, [0, 0.5, 1 - Number.EPSILON]); + expect(prng.chanceTo(10, 1)).toBeTruthy(); + expect(prng.chanceTo(10, 1)).toBeTruthy(); + expect(prng.chanceTo(10, 1)).toBeFalsy(); + }); + + test('uniqid should return a string', () => { + const uniqid = rng.uniqid(); + expect(uniqid).toEqual(expect.any(String)); + }); + + test('uniqid should not return the same thing twice for seeded RNG', () => { + const rng = new Rng(12345); + expect(rng.uniqid()).not.toBe(rng.uniqid()); + }); + + test('uniqid should return distinct and incrementing results', async () => { + const strs = []; + for (let i = 0; i <= mdruns; i++) { + strs.push(rng.uniqid()); + } + const strCopy = [...strs].sort(); + const strSet = Array.from(new Set(strs)); + + expect(strCopy).toEqual(strs); + expect(strSet).toEqual(strs); + + return (new Promise((resolve) => { + const strs : any[] = []; + for (let i = 0; i <= smruns; i++) { + strs.push(rng.uniqid()); + } + for (let i = 0; i <= 20; i++) { + setTimeout(() => { + for (let i = 0; i <= smruns; i++) { + strs.push(rng.uniqid()); + } + const strCopy = [...strs].sort(); + const strSet = Array.from(new Set(strs)); + + expect(strCopy).toEqual(strs); + expect(strSet).toEqual(strs); + }, i * 100); + } + setTimeout(() => { + for (let i = 0; i <= smruns; i++) { + strs.push(rng.uniqid()); + } + const strCopy = [...strs].sort(); + const strSet = Array.from(new Set(strs)); + + expect(strCopy).toEqual(strs); + expect(strSet).toEqual(strs); + resolve(null); + }, 3000); + })); + }, 10000); + + test('randomString should return a string', () => { + const randomString = rng.randomString(); + expect(randomString).toEqual(expect.any(String)); + }); + + test('randomString should not return the same thing twice for seeded RNG', () => { + const rng = new Rng(12345); + expect(rng.randomString()).not.toBe(rng.randomString()); + }); + + test('randomString should return string of length n', () => { + expect(rng.randomString(12).length).toBe(12); + }); + + test('normal', () => { + const results = []; + const mean = 10; + const stddev = 0.1; + for (let i = 0; i < 100000; i++) { + results.push(rng.normal({ mean, stddev })); + } + const sum = results.reduce((a, b) => a + b); + const calcMean = sum / results.length; + const calcStdDev = Math.sqrt(results.reduce((a, b) => a + Math.pow((b - mean), 2), 0) / (results.length - 1)); + + expect(Math.abs(mean - calcMean)).toBeLessThan(stddev / 10); + expect(Math.abs(stddev - calcStdDev)).toBeLessThan(stddev / 10); + + // There is a 1 in 390,682,215,445 chance for each result to be within 7 standard deviations. + // It should be fairly sufficient to test that results are within this +/- 7o window. + // i.e. these will fail only 1/3,906,822 times. + // Using mean = 10 and stddev = 0.1 the window is then from 9.3 to 10.7 + expect(Math.max(...results)).toBeLessThan(mean + (stddev * 7)); + expect(Math.min(...results)).toBeGreaterThan(mean - (stddev * 7)); + }); + + test.each([ + undefined, + {}, + { mean: 50 }, + { mean: -50 }, + { stddev: 1 }, + { stddev: 100 }, + { min: 1, max: 100 }, + { min: 1, max: 100, skew: -1 }, + { min: 1, max: 100, skew: 10 }, + { min: 1, max: 100, skew: 1 }, + { skew: 10 }, + { min: -1 }, + { max: 100 } + ])('normal with different number of args doesnt throw', (args) => { + expect(() => { + rng.normal(args); + }).not.toThrow(); + }); + + test('Test shouldThrowOnMaxRecursionsReached returns boolean', () => { + expect(typeof rng.shouldThrowOnMaxRecursionsReached()).toBe('boolean'); + }); + + test('weightedChoice', () => { + prng.results = defaultResultSet; + const choices = { + a: 10, + b: 1 + }; + for (let i = 0; i < 10; i++) expect(prng.weightedChoice(choices)).toBe('a'); + const r1 = prng.weightedChoice(choices); + expect(r1).toBe('b'); + + const stacked = { + a: Math.pow(2, 32), + b: 1 + }; + for (let i = 0; i < 1000; i++) { + expect(rng.weightedChoice(stacked)).toBe('a'); + } + }); + + test('should choose a random element from an array', () => { + const choices = [1, 2, 3, 4, 5]; + const choice = rng.choice(choices); + expect(choices).toContain(choice); + }); + + test('should handle weighted choice from an array', () => { + const choices = [1, 2, 3, 4, 5]; + const weightedChoice = rng.weightedChoice(choices); + expect(choices).toContain(weightedChoice); + }); + + test('weightedChoice invalid input', () => { + const choices = { + a: -1, + b: 1 + }; + expect(() => { + rng.weightedChoice(choices); + }).toThrow(); + }); + + test('weightedChoice with map', () => { + const map = new Map(); + const ob1 = { name: 'ob1' }; + const ob2 = { name: 'ob2' }; + + map.set(ob1, 10); + map.set(ob2, 1); + + prng.results = defaultResultSet; + for (let i = 0; i < 10; i++) expect(prng.weightedChoice(map)).toBe(ob1); + const r1 = prng.weightedChoice(map); + expect(r1).toBe(ob2); + }); + + test('weightedChoice with array of strings', () => { + const colors = ['red', 'green', 'blue']; + prng.setEvenSpread(3); + expect(prng.weightedChoice(colors)).toBe('red'); + expect(prng.weightedChoice(colors)).toBe('green'); + expect(prng.weightedChoice(colors)).toBe('blue'); + }); + + test('weightedChoice with array of objects', () => { + const ob1 = { name: 'ob1' }; + const ob2 = { name: 'ob2' }; + const choices = [ob1, ob2]; + + prng.setEvenSpread(2); + expect(prng.weightedChoice(choices)).toBe(ob1); + expect(prng.weightedChoice(choices)).toBe(ob2); + }); + + test('weightedChoice with empty array produces null', () => { + expect(rng.weightedChoice([])).toBe(null); + expect(rng.weightedChoice({})).toBe(null); + expect(rng.weightedChoice(new Map())).toBe(null); + }); + + test('weightedChoice with single entry returns that entry', () => { + expect(rng.weightedChoice(['a'])).toBe('a'); + expect(rng.weightedChoice({ a: 1 })).toBe('a'); + expect(rng.weightedChoice(new Map([['b', 1]]))).toBe('b'); + }); + + test('weights produces correct output', () => { + const f = () => {}; + const weights = rng.weights(['a', 'a', 'a', 'b', 'b', f, f, 'c']); + expect(weights.get('a')).toBe(3); + expect(weights.get('b')).toBe(2); + expect(weights.get(f)).toBe(2); + expect(weights.get('c')).toBe(1); + }); + + test('parseDiceString', () => { + const expectations = { + '5d6+6': { n: 5, d: 6, plus: 6 }, + '2d12+1': { n: 2, d: 12, plus: 1 }, + d12: { n: 1, d: 12, plus: 0 }, + '5 d 6 + 6': { n: 5, d: 6, plus: 6 }, + 6: { n: 0, d: 0, plus: 6 }, + 1: { n: 0, d: 0, plus: 1 }, + 1.5: { n: 0, d: 0, plus: 1.5 }, + '5d6+1.5': { n: 5, d: 6, plus: 1.5 }, + '5d6-1.5': { n: 5, d: 6, plus: -1.5 }, + '0d6-1.5': { n: 0, d: 6, plus: -1.5 }, + }; + for (const [d, exp] of Object.entries(expectations)) { + const result = rng.parseDiceString(d); + expect(result).toEqual(exp); + } + }); + + test('diceExpanded - some basic results', () => { + let n = 6; + prng.setEvenSpread(n); + for (let i = 1; i <= n; i++) { + const r = prng.diceExpanded(`1d${n}+1`); + expect(r.dice).toEqual([i]); + expect(r.plus).toBe(1); + expect(r.total).toBe(i + 1); + } + + n = 12; + prng.setEvenSpread(n); + + let r = prng.diceExpanded(`2d${n}+5`); + expect(r.dice).toEqual([1, 2]); + expect(r.plus).toBe(5); + expect(r.total).toBe(5 + 3); + + r = prng.diceExpanded(`2d${n}+5`); + expect(r.dice).toEqual([3, 4]); + expect(r.plus).toBe(5); + expect(r.total).toBe(5 + 3 + 4); + }); + + test('dice - some basic results', () => { + let n = 6; + prng.setEvenSpread(6); + expect(prng.dice('1d6+1')).toBe(2); + expect(prng.dice('1d6+1')).toBe(3); + expect(prng.dice('1d6+1')).toBe(4); + expect(prng.dice('1d6+1')).toBe(5); + expect(prng.dice('1d6+1')).toBe(6); + expect(prng.dice('1d6+1')).toBe(7); + expect(prng.dice('1d6+1')).toBe(2); + + prng.reset(); + expect(prng.dice('1d6+1')).toBe(2); + expect(prng.dice('1d6+1')).toBe(3); + + prng.reset(); + expect(prng.dice([1, 6, 1])).toBe(2); + expect(prng.dice([1, 6, 1])).toBe(3); + + prng.reset(); + expect(prng.dice()).toBe(1); + + prng.reset(); + expect(prng.dice('2d6+1')).toBe(4); + + prng.reset(); + expect(prng.dice('1d1+1')).toBe(2); + + n = 12; + prng.setEvenSpread(12); + expect(prng.dice(`2d${n}+1`)).toBe(4); + + prng.results = [1 - Number.EPSILON]; + expect(prng.dice(`2d${n}+1`)).toBe(25); + + prng.results = [1 - Number.EPSILON]; + expect(prng.dice(`1d${n}`)).toBe(n); + + prng.results = [0.5 - Number.EPSILON, 1 - Number.EPSILON]; + expect(prng.dice(`2d${n}+1`)).toBe(6 + 12 + 1); + + prng.reset(); + expect(prng.dice(`2d${n}-1`)).toBe(6 + 12 - 1); + }); + + test('dice - weird combos', () => { + prng.setEvenSpread(6); + prng.reset(); + expect(prng.dice('1d6+0.5')).toBe(1.5); + expect(prng.dice('1d6+0.5')).toBe(2.5); + expect(prng.dice('1d6+0.5')).toBe(3.5); + expect(prng.dice('1d6+0.5')).toBe(4.5); + expect(prng.dice('1d6+0.5')).toBe(5.5); + expect(prng.dice('1d6+0.5')).toBe(6.5); + prng.reset(); + expect(prng.dice('1d6-0.5')).toBe(0.5); + expect(prng.dice('1d6-0.5')).toBe(1.5); + expect(prng.dice('1d6-0.5')).toBe(2.5); + expect(prng.dice('1d6-0.5')).toBe(3.5); + expect(prng.dice('1d6-0.5')).toBe(4.5); + expect(prng.dice('1d6-0.5')).toBe(5.5); + prng.reset(); + expect(prng.dice('0d6-0.5')).toBe(-0.5); + prng.reset(); + expect(prng.dice('0d0-1')).toBe(-1); + }); + + test('dice - negative numbers of dice', () => { + prng.setEvenSpread(6); + expect(prng.dice('-1d6')).toBe(-1); + expect(prng.dice('-1d6')).toBe(-2); + expect(prng.dice('-1d6')).toBe(-3); + expect(prng.dice('-1d6')).toBe(-4); + expect(prng.dice('-1d6')).toBe(-5); + expect(prng.dice('-1d6')).toBe(-6); + expect(prng.dice('-1d6')).toBe(-1); + prng.setEvenSpread(6); + expect(prng.dice('-2d6')).toBe(-3); + }); + + test('dice - invalid inputs', () => { + expect(() => { + // @ts-ignore + rng.dice(null); + }).toThrow(); + + expect(() => { + // @ts-ignore + rng.dice(new Set()); + }).toThrow(); + + expect(() => { + rng.dice('abcdefghijk'); + }).toThrow(); + }); + + test('clamp should clamp a number within a range', () => { + const clampedNumber = rng.clamp(15, 10, 20); + expect(clampedNumber).toBe(15); + + const clampedNumberBelow = rng.clamp(5, 10, 20); + expect(clampedNumberBelow).toBe(10); + + const clampedNumberAbove = rng.clamp(25, 10, 20); + expect(clampedNumberAbove).toBe(20); + + expect(rng.clamp(15, 1, 10)).toBe(10); + expect(rng.clamp(-1, 1, 10)).toBe(1); + }); + + test('bin', () => { + expect(rng.bin(1.5 - Number.EPSILON, 11, 0, 10)).toBe(1); + expect(rng.bin(1.5 + Number.EPSILON, 11, 0, 10)).toBe(2); + expect(rng.bin(9.1, 11, 0, 10)).toBe(9); + expect(rng.bin(9.7, 11, 0, 10)).toBe(10); + }); + + test('dice max', () => { + expect(Rng.diceMax()).toBe(6); + expect(Rng.diceMax({ n: 1, d: 6, plus: 2 })).toBe(8); + expect(Rng.diceMax('1d6+2')).toBe(8); + expect(Rng.diceMax('2d6+2')).toBe(14); + expect(Rng.diceMax('2d6+5')).toBe(17); + expect(Rng.diceMax({ n: 0, d: 6, plus: 2 })).toBe(2); + + expect(rng.diceMax()).toBe(6); + expect(rng.diceMax({ n: 1, d: 6, plus: 2 })).toBe(8); + expect(rng.diceMax('1d6+2')).toBe(8); + expect(rng.diceMax('2d6+2')).toBe(14); + expect(rng.diceMax('2d6+5')).toBe(17); + expect(rng.diceMax({ n: 0, d: 6, plus: 2 })).toBe(2); + }); + + test('dice min', () => { + expect(Rng.diceMin({ n: 1, d: 6, plus: 2 })).toBe(3); + expect(Rng.diceMin('1d6+2')).toBe(3); + expect(Rng.diceMin('2d6+2')).toBe(4); + expect(Rng.diceMin('2d6+5')).toBe(7); + expect(Rng.diceMin({ n: 0, d: 6, plus: 2 })).toBe(2); + expect(Rng.diceMin({ n: 1, d: 6, plus: 0 })).toBe(1); + + expect(rng.diceMin()).toBe(1); + expect(rng.diceMin({ n: 1, d: 6, plus: 2 })).toBe(3); + expect(rng.diceMin('1d6+2')).toBe(3); + expect(rng.diceMin('2d6+2')).toBe(4); + expect(rng.diceMin('2d6+5')).toBe(7); + expect(rng.diceMin({ n: 0, d: 6, plus: 2 })).toBe(2); + expect(rng.diceMin({ n: 1, d: 6, plus: 0 })).toBe(1); + }); + + test('should handle pool of elements', () => { + const pool = rng.pool([1, 2, 3, 4, 5]); + expect(pool).toEqual(expect.any(Pool)); + }); +});