mirror of
https://github.com/sp-tarkov/server.git
synced 2025-02-12 17:30:42 -05:00
Better Random (#972)
This started as a small update. This pull request includes updates to the `RandomUtil` class to improve the security and accuracy of random number generation by utilizing the `node:crypto` module. Additionally, it enhances the documentation and refactors several methods for clarity and performance. Includes tests, both before and after the changes. The only *possible* breaking change is that now the `getKey`, `getKeyValue`, and `drawRandomFromDict` methods are typed with generics instead of `any`. I tried to keep them as *generic* as possible, but we should probably include this in a BE to ensure it doesn't break anything. No further changes are planned--Saved as a draft because it's late and I still want to review. --------- Co-authored-by: Chomp <27521899+chompDev@users.noreply.github.com>
This commit is contained in:
parent
9ef8206517
commit
c071702851
@ -69,5 +69,17 @@
|
|||||||
"formatter": {
|
"formatter": {
|
||||||
"trailingCommas": "none"
|
"trailingCommas": "none"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"include": ["tests/*"],
|
||||||
|
"linter": {
|
||||||
|
"rules": {
|
||||||
|
"suspicious": {
|
||||||
|
"noExplicitAny": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import * as crypto from "node:crypto";
|
||||||
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||||
import { MathUtil } from "@spt/utils/MathUtil";
|
import { MathUtil } from "@spt/utils/MathUtil";
|
||||||
import { ICloner } from "@spt/utils/cloners/ICloner";
|
import { ICloner } from "@spt/utils/cloners/ICloner";
|
||||||
@ -194,33 +195,90 @@ export class RandomUtil {
|
|||||||
@inject("PrimaryLogger") protected logger: ILogger,
|
@inject("PrimaryLogger") protected logger: ILogger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a secure random number between 0 (inclusive) and 1 (exclusive).
|
||||||
|
*
|
||||||
|
* This method uses the `crypto` module to generate a 48-bit random integer,
|
||||||
|
* which is then divided by the maximum possible 48-bit integer value to
|
||||||
|
* produce a floating-point number in the range [0, 1).
|
||||||
|
*
|
||||||
|
* @returns A secure random number between 0 (inclusive) and 1 (exclusive).
|
||||||
|
*/
|
||||||
|
private getSecureRandomNumber(): number {
|
||||||
|
const buffer = crypto.randomBytes(6); // 48 bits
|
||||||
|
const integer = buffer.readUIntBE(0, 6);
|
||||||
|
const maxInteger = 281474976710656; // 2^48
|
||||||
|
return integer / maxInteger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random integer between the specified minimum and maximum values, inclusive.
|
||||||
|
*
|
||||||
|
* @param min - The minimum value (inclusive).
|
||||||
|
* @param max - The maximum value (inclusive).
|
||||||
|
* @returns A random integer between the specified minimum and maximum values.
|
||||||
|
*/
|
||||||
public getInt(min: number, max: number): number {
|
public getInt(min: number, max: number): number {
|
||||||
const minimum = Math.ceil(min);
|
const minimum = Math.ceil(min);
|
||||||
const maximum = Math.floor(max);
|
const maximum = Math.floor(max);
|
||||||
return maximum > minimum ? Math.floor(Math.random() * (maximum - minimum + 1) + minimum) : minimum;
|
if (maximum > minimum) {
|
||||||
|
// randomInt is exclusive of the max value, so add 1
|
||||||
|
return crypto.randomInt(minimum, maximum + 1);
|
||||||
|
}
|
||||||
|
return minimum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random integer between 1 (inclusive) and the specified maximum value (exclusive).
|
||||||
|
* If the maximum value is less than or equal to 1, it returns 1.
|
||||||
|
*
|
||||||
|
* @param max - The upper bound (exclusive) for the random integer generation.
|
||||||
|
* @returns A random integer between 1 and max - 1, or 1 if max is less than or equal to 1.
|
||||||
|
*/
|
||||||
public getIntEx(max: number): number {
|
public getIntEx(max: number): number {
|
||||||
return max > 1 ? Math.floor(Math.random() * (max - 2) + 1) : 1;
|
return max > 2 ? crypto.randomInt(1, max - 1) : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random floating-point number within the specified range.
|
||||||
|
*
|
||||||
|
* @param min - The minimum value of the range (inclusive).
|
||||||
|
* @param max - The maximum value of the range (exclusive).
|
||||||
|
* @returns A random floating-point number between `min` (inclusive) and `max` (exclusive).
|
||||||
|
*/
|
||||||
public getFloat(min: number, max: number): number {
|
public getFloat(min: number, max: number): number {
|
||||||
return Math.random() * (max - min) + min;
|
const random = this.getSecureRandomNumber();
|
||||||
|
return random * (max - min) + min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random boolean value.
|
||||||
|
*
|
||||||
|
* @returns A random boolean value, where the probability of `true` and `false` is approximately equal.
|
||||||
|
*/
|
||||||
public getBool(): boolean {
|
public getBool(): boolean {
|
||||||
return Math.random() < 0.5;
|
const random = this.getSecureRandomNumber();
|
||||||
|
return random < 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the percentage of a given number and returns the result.
|
||||||
|
*
|
||||||
|
* @param percent - The percentage to calculate.
|
||||||
|
* @param number - The number to calculate the percentage of.
|
||||||
|
* @param toFixed - The number of decimal places to round the result to (default is 2).
|
||||||
|
* @returns The calculated percentage of the given number, rounded to the specified number of decimal places.
|
||||||
|
*/
|
||||||
public getPercentOfValue(percent: number, number: number, toFixed = 2): number {
|
public getPercentOfValue(percent: number, number: number, toFixed = 2): number {
|
||||||
return Number.parseFloat(((percent * number) / 100).toFixed(toFixed));
|
return Number.parseFloat(((percent * number) / 100).toFixed(toFixed));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduce a value by a percentage
|
* Reduces a given number by a specified percentage.
|
||||||
* @param number Value to reduce
|
*
|
||||||
* @param percentage Percentage to reduce value by
|
* @param number - The original number to be reduced.
|
||||||
* @returns Reduced value
|
* @param percentage - The percentage by which to reduce the number.
|
||||||
|
* @returns The reduced number after applying the percentage reduction.
|
||||||
*/
|
*/
|
||||||
public reduceValueByPercent(number: number, percentage: number): number {
|
public reduceValueByPercent(number: number, percentage: number): number {
|
||||||
const reductionAmount = number * (percentage / 100);
|
const reductionAmount = number * (percentage / 100);
|
||||||
@ -228,46 +286,87 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if number passes a check out of 100
|
* Determines if a random event occurs based on the given chance percentage.
|
||||||
* @param chancePercent value check needs to be above
|
*
|
||||||
* @returns true if value passes check
|
* @param chancePercent - The percentage chance (0-100) that the event will occur.
|
||||||
|
* @returns `true` if the event occurs, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
public getChance100(chancePercent: number): boolean {
|
public getChance100(chancePercent: number): boolean {
|
||||||
return this.getIntEx(100) <= chancePercent;
|
return this.getIntEx(100) <= chancePercent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Its better to keep this method separated from getArrayValue so we can use generic inferance on getArrayValue
|
/**
|
||||||
|
* Returns a random string from the provided array of strings.
|
||||||
|
*
|
||||||
|
* This method is separate from getArrayValue so we can use a generic inferance with getArrayValue.
|
||||||
|
*
|
||||||
|
* @param arr - The array of strings to select a random value from.
|
||||||
|
* @returns A randomly selected string from the array.
|
||||||
|
*/
|
||||||
public getStringArrayValue(arr: string[]): string {
|
public getStringArrayValue(arr: string[]): string {
|
||||||
return arr[this.getInt(0, arr.length - 1)];
|
return arr[this.getInt(0, arr.length - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a random element from the provided array.
|
||||||
|
*
|
||||||
|
* @template T - The type of elements in the array.
|
||||||
|
* @param arr - The array from which to select a random element.
|
||||||
|
* @returns A random element from the array.
|
||||||
|
*/
|
||||||
public getArrayValue<T>(arr: T[]): T {
|
public getArrayValue<T>(arr: T[]): T {
|
||||||
return arr[this.getInt(0, arr.length - 1)];
|
return arr[this.getInt(0, arr.length - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a random key from the given object.
|
||||||
|
*
|
||||||
|
* @param node - The object from which to retrieve a key.
|
||||||
|
* @returns A string representing one of the keys of the node object.
|
||||||
|
*
|
||||||
|
* TODO: v3.11 - This method is not type-safe and should be refactored to use a more specific type:
|
||||||
|
* https://github.com/sp-tarkov/server/pull/972/commits/f2b8efe211d95f71aec0a4bc84f4542335433412
|
||||||
|
*/
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Used to allow for a broad range of types.
|
||||||
public getKey(node: any): string {
|
public getKey(node: any): string {
|
||||||
return this.getArrayValue(Object.keys(node));
|
return this.getArrayValue(Object.keys(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the value associated with a key from the given node object.
|
||||||
|
*
|
||||||
|
* @param node - An object with string keys and any type of values.
|
||||||
|
* @returns The value associated with the key obtained from the node.
|
||||||
|
*
|
||||||
|
* TODO: v3.11 - This method is not type-safe and should be refactored to use a more specific type:
|
||||||
|
* https://github.com/sp-tarkov/server/pull/972/commits/f2b8efe211d95f71aec0a4bc84f4542335433412
|
||||||
|
*/
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Used to allow for a broad range of types.
|
||||||
public getKeyValue(node: { [x: string]: any }): any {
|
public getKeyValue(node: { [x: string]: any }): any {
|
||||||
return node[this.getKey(node)];
|
return node[this.getKey(node)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a normally distributed random number
|
* Generates a normally distributed random number using the Box-Muller transform.
|
||||||
* Uses the Box-Muller transform
|
*
|
||||||
* @param {number} mean Mean of the normal distribution
|
* @param mean - The mean (μ) of the normal distribution.
|
||||||
* @param {number} sigma Standard deviation of the normal distribution
|
* @param sigma - The standard deviation (σ) of the normal distribution.
|
||||||
* @returns {number} The value drawn
|
* @param attempt - The current attempt count to generate a valid number (default is 0).
|
||||||
|
* @returns A normally distributed random number.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This function uses the Box-Muller transform to generate a normally distributed random number.
|
||||||
|
* If the generated number is less than 0, it will recursively attempt to generate a valid number up to 100 times.
|
||||||
|
* If it fails to generate a valid number after 100 attempts, it will return a random float between 0.01 and twice the mean.
|
||||||
*/
|
*/
|
||||||
public getNormallyDistributedRandomNumber(mean: number, sigma: number, attempt = 0): number {
|
public getNormallyDistributedRandomNumber(mean: number, sigma: number, attempt = 0): number {
|
||||||
let u = 0;
|
let u = 0;
|
||||||
let v = 0;
|
let v = 0;
|
||||||
while (u === 0) {
|
while (u === 0) {
|
||||||
u = Math.random(); // Converting [0,1) to (0,1)
|
u = this.getSecureRandomNumber();
|
||||||
}
|
}
|
||||||
while (v === 0) {
|
while (v === 0) {
|
||||||
v = Math.random();
|
v = this.getSecureRandomNumber();
|
||||||
}
|
}
|
||||||
const w = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
const w = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||||||
const valueDrawn = mean + w * sigma;
|
const valueDrawn = mean + w * sigma;
|
||||||
@ -283,36 +382,42 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw Random integer low inclusive, high exclusive
|
* Generates a random integer between the specified range.
|
||||||
* if high is not set we draw from 0 to low (exclusive)
|
*
|
||||||
* @param {integer} low Lower bound inclusive, when high is not set, this is high
|
* @param low - The lower bound of the range (inclusive).
|
||||||
* @param {integer} high Higher bound exclusive
|
* @param high - The upper bound of the range (exclusive). If not provided, the range will be from 0 to `low`.
|
||||||
* @returns {integer} The random integer in [low, high)
|
* @returns A random integer within the specified range.
|
||||||
*/
|
*/
|
||||||
public randInt(low: number, high?: number): number {
|
public randInt(low: number, high?: number): number {
|
||||||
if (high) {
|
if (typeof high !== "undefined") {
|
||||||
return low + Math.floor(Math.random() * (high - low));
|
return crypto.randomInt(low, high);
|
||||||
}
|
}
|
||||||
|
return crypto.randomInt(0, low);
|
||||||
return Math.floor(Math.random() * low);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a random element of the provided list N times to return an array of N random elements
|
* Draws a specified number of random elements from a given list.
|
||||||
* Drawing can be with or without replacement
|
*
|
||||||
* @param {array} list The array we want to draw randomly from
|
* @template T - The type of elements in the list.
|
||||||
* @param {integer} count The number of times we want to draw
|
* @param originalList - The list to draw elements from.
|
||||||
* @param {boolean} replacement Draw with or without replacement from the input array(default true)
|
* @param count - The number of elements to draw. Defaults to 1.
|
||||||
* @return {array} Array consisting of N random elements
|
* @param replacement - Whether to draw with replacement. Defaults to true.
|
||||||
|
* @returns An array containing the drawn elements.
|
||||||
*/
|
*/
|
||||||
public drawRandomFromList<T>(originalList: Array<T>, count = 1, replacement = true): Array<T> {
|
public drawRandomFromList<T>(originalList: Array<T>, count = 1, replacement = true): Array<T> {
|
||||||
let list = originalList;
|
let list = originalList;
|
||||||
|
let drawCount = count;
|
||||||
|
|
||||||
if (!replacement) {
|
if (!replacement) {
|
||||||
list = this.cloner.clone(originalList);
|
list = this.cloner.clone(originalList);
|
||||||
|
// Adjust drawCount to avoid drawing more elements than available
|
||||||
|
if (drawCount > list.length) {
|
||||||
|
drawCount = list.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: T[] = [];
|
const results: T[] = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < drawCount; i++) {
|
||||||
const randomIndex = this.randInt(list.length);
|
const randomIndex = this.randInt(list.length);
|
||||||
if (replacement) {
|
if (replacement) {
|
||||||
results.push(list[randomIndex]);
|
results.push(list[randomIndex]);
|
||||||
@ -324,33 +429,48 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a random (top level) element of the provided dictionary N times to return an array of N random dictionary keys
|
* Draws a specified number of random keys from a given dictionary.
|
||||||
* Drawing can be with or without replacement
|
*
|
||||||
* @param {any} dict The dictionary we want to draw randomly from
|
* @param dict - The dictionary from which to draw keys.
|
||||||
* @param {integer} count The number of times we want to draw
|
* @param count - The number of keys to draw. Defaults to 1.
|
||||||
* @param {boolean} replacement Draw with ot without replacement from the input dict
|
* @param replacement - Whether to draw with replacement. Defaults to true.
|
||||||
* @return {array} Array consisting of N random keys of the dictionary
|
* @returns An array of randomly drawn keys from the dictionary.
|
||||||
|
*
|
||||||
|
* TODO: v3.11 - This method is not type-safe and should be refactored to use a more specific type:
|
||||||
|
* https://github.com/sp-tarkov/server/pull/972/commits/f2b8efe211d95f71aec0a4bc84f4542335433412
|
||||||
*/
|
*/
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: Used to allow for a broad range of types.
|
||||||
public drawRandomFromDict(dict: any, count = 1, replacement = true): any[] {
|
public drawRandomFromDict(dict: any, count = 1, replacement = true): any[] {
|
||||||
const keys = Object.keys(dict);
|
const keys = Object.keys(dict);
|
||||||
const randomKeys = this.drawRandomFromList(keys, count, replacement);
|
const randomKeys = this.drawRandomFromList(keys, count, replacement);
|
||||||
return randomKeys;
|
return randomKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a biased random number within a specified range.
|
||||||
|
*
|
||||||
|
* @param min - The minimum value of the range (inclusive).
|
||||||
|
* @param max - The maximum value of the range (inclusive).
|
||||||
|
* @param shift - The bias shift to apply to the random number generation.
|
||||||
|
* @param n - The number of iterations to use for generating a Gaussian random number.
|
||||||
|
* @returns A biased random number within the specified range.
|
||||||
|
* @throws Will throw if `max` is less than `min` or if `n` is less than 1.
|
||||||
|
*/
|
||||||
public getBiasedRandomNumber(min: number, max: number, shift: number, n: number): number {
|
public getBiasedRandomNumber(min: number, max: number, shift: number, n: number): number {
|
||||||
/* To whoever tries to make sense of this, please forgive me - I tried my best at explaining what goes on here.
|
/**
|
||||||
* This function generates a random number based on a gaussian distribution with an option to add a bias via shifting.
|
* This function generates a random number based on a gaussian distribution with an option to add a bias via shifting.
|
||||||
*
|
*
|
||||||
* Here's an example graph of how the probabilities can be distributed:
|
* Here's an example graph of how the probabilities can be distributed:
|
||||||
* https://www.boost.org/doc/libs/1_49_0/libs/math/doc/sf_and_dist/graphs/normal_pdf.png
|
* https://www.boost.org/doc/libs/1_49_0/libs/math/doc/sf_and_dist/graphs/normal_pdf.png
|
||||||
|
*
|
||||||
* Our parameter 'n' is sort of like σ (sigma) in the example graph.
|
* Our parameter 'n' is sort of like σ (sigma) in the example graph.
|
||||||
*
|
*
|
||||||
* An 'n' of 1 means all values are equally likely. Increasing 'n' causes numbers near the edge to become less likely.
|
* An 'n' of 1 means all values are equally likely. Increasing 'n' causes numbers near the edge to become less likely.
|
||||||
* By setting 'shift' to whatever 'max' is, we can make values near 'min' very likely, while values near 'max' become extremely unlikely.
|
* By setting 'shift' to whatever 'max' is, we can make values near 'min' very likely, while values near 'max' become extremely unlikely.
|
||||||
*
|
*
|
||||||
* Here's a place where you can play around with the 'n' and 'shift' values to see how the distribution changes:
|
* Here's a place where you can play around with the 'n' and 'shift' values to see how the distribution changes:
|
||||||
* http://jsfiddle.net/e08cumyx/ */
|
* http://jsfiddle.net/e08cumyx/
|
||||||
|
*/
|
||||||
if (max < min) {
|
if (max < min) {
|
||||||
throw {
|
throw {
|
||||||
name: "Invalid arguments",
|
name: "Invalid arguments",
|
||||||
@ -367,13 +487,14 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shift > max - min) {
|
if (shift > max - min) {
|
||||||
/* If a rolled number is out of bounds (due to bias being applied), we simply roll it again.
|
/**
|
||||||
|
* If a rolled number is out of bounds (due to bias being applied), we simply roll it again.
|
||||||
* As the shifting increases, the chance of rolling a number within bounds decreases.
|
* As the shifting increases, the chance of rolling a number within bounds decreases.
|
||||||
* A shift that is equal to the available range only has a 50% chance of rolling correctly, theoretically halving performance.
|
* A shift that is equal to the available range only has a 50% chance of rolling correctly, theoretically halving performance.
|
||||||
* Shifting even further drops the success chance very rapidly - so we want to warn against that */
|
* Shifting even further drops the success chance very rapidly - so we want to warn against that
|
||||||
|
**/
|
||||||
this.logger.warning(
|
this.logger.warning(
|
||||||
"Bias shift for random number generation is greater than the range of available numbers.\nThis can have a very severe performance impact!",
|
"Bias shift for random number generation is greater than the range of available numbers. This can have a very severe performance impact!",
|
||||||
);
|
);
|
||||||
this.logger.info(`min -> ${min}; max -> ${max}; shift -> ${shift}`);
|
this.logger.info(`min -> ${min}; max -> ${max}; shift -> ${shift}`);
|
||||||
}
|
}
|
||||||
@ -382,7 +503,7 @@ export class RandomUtil {
|
|||||||
let rand = 0;
|
let rand = 0;
|
||||||
|
|
||||||
for (let i = 0; i < n; i += 1) {
|
for (let i = 0; i < n; i += 1) {
|
||||||
rand += Math.random();
|
rand += this.getSecureRandomNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
return rand / n;
|
return rand / n;
|
||||||
@ -404,9 +525,11 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fisher-Yates shuffle an array
|
* Shuffles an array in place using the Fisher-Yates algorithm.
|
||||||
* @param array Array to shuffle
|
*
|
||||||
* @returns Shuffled array
|
* @template T - The type of elements in the array.
|
||||||
|
* @param array - The array to shuffle.
|
||||||
|
* @returns The shuffled array.
|
||||||
*/
|
*/
|
||||||
public shuffle<T>(array: Array<T>): Array<T> {
|
public shuffle<T>(array: Array<T>): Array<T> {
|
||||||
let currentIndex = array.length;
|
let currentIndex = array.length;
|
||||||
@ -415,7 +538,7 @@ export class RandomUtil {
|
|||||||
// While there remain elements to shuffle.
|
// While there remain elements to shuffle.
|
||||||
while (currentIndex !== 0) {
|
while (currentIndex !== 0) {
|
||||||
// Pick a remaining element.
|
// Pick a remaining element.
|
||||||
randomIndex = Math.floor(Math.random() * currentIndex);
|
randomIndex = crypto.randomInt(0, currentIndex);
|
||||||
currentIndex--;
|
currentIndex--;
|
||||||
|
|
||||||
// And swap it with the current element.
|
// And swap it with the current element.
|
||||||
@ -426,18 +549,13 @@ export class RandomUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rolls for a probability based on chance
|
* Rolls for a chance probability and returns whether the roll is successful.
|
||||||
* @param number Probability Chance as float (0-1)
|
*
|
||||||
* @returns If roll succeed or not
|
* @param probabilityChance - The probability chance to roll for, represented as a number between 0 and 1.
|
||||||
* @example
|
* @returns `true` if the random number is less than or equal to the probability chance, otherwise `false`.
|
||||||
* rollForChanceProbability(0.25); // returns true 25% probability
|
|
||||||
*/
|
*/
|
||||||
public rollForChanceProbability(probabilityChance: number): boolean {
|
public rollForChanceProbability(probabilityChance: number): boolean {
|
||||||
const maxRoll = 9999;
|
const random = this.getSecureRandomNumber();
|
||||||
|
return random <= probabilityChance;
|
||||||
// Roll a number between 0 and 1
|
|
||||||
const rolledChance = this.getInt(0, maxRoll) / 10000;
|
|
||||||
|
|
||||||
return rolledChance <= probabilityChance;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
663
project/tests/utils/RandomUtil.test.ts
Normal file
663
project/tests/utils/RandomUtil.test.ts
Normal file
@ -0,0 +1,663 @@
|
|||||||
|
import "reflect-metadata";
|
||||||
|
import { RandomUtil } from "@spt/utils/RandomUtil";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("RandomUtil", () => {
|
||||||
|
let randomUtil: RandomUtil;
|
||||||
|
let mockCloner: any;
|
||||||
|
let mockLogger: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockCloner = {
|
||||||
|
clone: vi.fn((obj) => JSON.parse(JSON.stringify(obj))),
|
||||||
|
};
|
||||||
|
mockLogger = {
|
||||||
|
warning: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
};
|
||||||
|
randomUtil = new RandomUtil(mockCloner, mockLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getInt", () => {
|
||||||
|
it("should return an integer between min and max inclusive when min < max", () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 5;
|
||||||
|
const result = randomUtil.getInt(min, max);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(Math.ceil(min));
|
||||||
|
expect(result).toBeLessThanOrEqual(Math.floor(max));
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle floating-point min and max values", () => {
|
||||||
|
const min = 1.2;
|
||||||
|
const max = 5.8;
|
||||||
|
const result = randomUtil.getInt(min, max);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(Math.ceil(min)); // 2
|
||||||
|
expect(result).toBeLessThanOrEqual(Math.floor(max)); // 5
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return min when min and max are equal", () => {
|
||||||
|
const min = 3;
|
||||||
|
const max = 3;
|
||||||
|
const result = randomUtil.getInt(min, max);
|
||||||
|
|
||||||
|
expect(result).toBe(Math.ceil(min));
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return min when max is less than min", () => {
|
||||||
|
const min = 5;
|
||||||
|
const max = 3;
|
||||||
|
const result = randomUtil.getInt(min, max);
|
||||||
|
|
||||||
|
expect(result).toBe(Math.ceil(min));
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle negative min and max values", () => {
|
||||||
|
const min = -5;
|
||||||
|
const max = -1;
|
||||||
|
const result = randomUtil.getInt(min, max);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(Math.ceil(min));
|
||||||
|
expect(result).toBeLessThanOrEqual(Math.floor(max));
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getIntEx", () => {
|
||||||
|
it("should return an integer between 1 and max - 2 inclusive when max > 1", () => {
|
||||||
|
const max = 10;
|
||||||
|
const result = randomUtil.getIntEx(max);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(result).toBeLessThanOrEqual(max - 2);
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 1 when max is less than or equal to 1", () => {
|
||||||
|
const maxValues = [1, 0, -5];
|
||||||
|
for (const max of maxValues) {
|
||||||
|
const result = randomUtil.getIntEx(max);
|
||||||
|
expect(result).toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle edge case when max is 2", () => {
|
||||||
|
const max = 2;
|
||||||
|
const result = randomUtil.getIntEx(max);
|
||||||
|
|
||||||
|
expect(result).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getFloat", () => {
|
||||||
|
it("should return a float between min and max", () => {
|
||||||
|
const min = 1.5;
|
||||||
|
const max = 5.5;
|
||||||
|
const result = randomUtil.getFloat(min, max);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(min);
|
||||||
|
expect(result).toBeLessThan(max);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle negative min and max values", () => {
|
||||||
|
const min = -5.5;
|
||||||
|
const max = -1.1;
|
||||||
|
const result = randomUtil.getFloat(min, max);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(min);
|
||||||
|
expect(result).toBeLessThan(max);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return min when min equals max", () => {
|
||||||
|
const min = 3.3;
|
||||||
|
const max = 3.3;
|
||||||
|
const result = randomUtil.getFloat(min, max);
|
||||||
|
|
||||||
|
expect(result).toBe(min);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBool", () => {
|
||||||
|
it("should return a boolean value", () => {
|
||||||
|
const result = randomUtil.getBool();
|
||||||
|
expect(typeof result).toBe("boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true when Math.random is less than 0.5", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.4);
|
||||||
|
const result = randomUtil.getBool();
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when getSecureRandomNumber returns 0.5", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.5);
|
||||||
|
const result = randomUtil.getBool();
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPercentOfValue", () => {
|
||||||
|
it("should calculate the correct percentage of a number", () => {
|
||||||
|
const percent = 25;
|
||||||
|
const number = 200;
|
||||||
|
const result = randomUtil.getPercentOfValue(percent, number);
|
||||||
|
|
||||||
|
expect(result).toBe(50.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle decimal percentages and numbers", () => {
|
||||||
|
const percent = 12.5;
|
||||||
|
const number = 80.4;
|
||||||
|
const result = randomUtil.getPercentOfValue(percent, number);
|
||||||
|
|
||||||
|
expect(result).toBeCloseTo(10.05, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should respect the toFixed parameter", () => {
|
||||||
|
const percent = 33.3333;
|
||||||
|
const number = 100;
|
||||||
|
const result = randomUtil.getPercentOfValue(percent, number, 4);
|
||||||
|
|
||||||
|
expect(result).toBe(33.3333);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("reduceValueByPercent", () => {
|
||||||
|
it("should reduce the value by the given percentage", () => {
|
||||||
|
const number = 200;
|
||||||
|
const percentage = 25;
|
||||||
|
const result = randomUtil.reduceValueByPercent(number, percentage);
|
||||||
|
|
||||||
|
expect(result).toBe(150);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle decimal percentages", () => {
|
||||||
|
const number = 100;
|
||||||
|
const percentage = 12.5;
|
||||||
|
const result = randomUtil.reduceValueByPercent(number, percentage);
|
||||||
|
|
||||||
|
expect(result).toBe(87.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same number when percentage is 0", () => {
|
||||||
|
const number = 100;
|
||||||
|
const percentage = 0;
|
||||||
|
const result = randomUtil.reduceValueByPercent(number, percentage);
|
||||||
|
|
||||||
|
expect(result).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getChance100", () => {
|
||||||
|
it("should return true if random number is less than or equal to chancePercent", () => {
|
||||||
|
vi.spyOn(randomUtil, "getIntEx").mockReturnValue(50);
|
||||||
|
|
||||||
|
const chancePercent = 60;
|
||||||
|
const result = randomUtil.getChance100(chancePercent);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(randomUtil.getIntEx).toHaveBeenCalledWith(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if random number is greater than chancePercent", () => {
|
||||||
|
vi.spyOn(randomUtil, "getIntEx").mockReturnValue(70);
|
||||||
|
|
||||||
|
const chancePercent = 60;
|
||||||
|
const result = randomUtil.getChance100(chancePercent);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getStringArrayValue", () => {
|
||||||
|
it("should return a value from the array", () => {
|
||||||
|
const arr = ["apple", "banana", "cherry"];
|
||||||
|
const result = randomUtil.getStringArrayValue(arr);
|
||||||
|
|
||||||
|
expect(arr).toContain(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle single-element arrays", () => {
|
||||||
|
const arr = ["only"];
|
||||||
|
const result = randomUtil.getStringArrayValue(arr);
|
||||||
|
|
||||||
|
expect(result).toBe("only");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return predictable value when getInt is mocked", () => {
|
||||||
|
vi.spyOn(randomUtil, "getInt").mockReturnValue(1);
|
||||||
|
|
||||||
|
const arr = ["first", "second", "third"];
|
||||||
|
const result = randomUtil.getStringArrayValue(arr);
|
||||||
|
|
||||||
|
expect(result).toBe("second");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getArrayValue", () => {
|
||||||
|
it("should return a value from the array of numbers", () => {
|
||||||
|
const arr = [10, 20, 30, 40];
|
||||||
|
const result = randomUtil.getArrayValue(arr);
|
||||||
|
|
||||||
|
expect(arr).toContain(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a value from the array of objects", () => {
|
||||||
|
const arr = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
||||||
|
const result = randomUtil.getArrayValue(arr);
|
||||||
|
|
||||||
|
expect(arr).toContain(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return predictable value when getInt is mocked", () => {
|
||||||
|
vi.spyOn(randomUtil, "getInt").mockReturnValue(2);
|
||||||
|
|
||||||
|
const arr = ["a", "b", "c", "d"];
|
||||||
|
const result = randomUtil.getArrayValue(arr);
|
||||||
|
|
||||||
|
expect(result).toBe("c");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getKey", () => {
|
||||||
|
it("should return a key from the object", () => {
|
||||||
|
const obj = { a: 1, b: 2, c: 3 };
|
||||||
|
const result = randomUtil.getKey(obj);
|
||||||
|
|
||||||
|
expect(Object.keys(obj)).toContain(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle single-key objects", () => {
|
||||||
|
const obj = { onlyKey: "value" };
|
||||||
|
const result = randomUtil.getKey(obj);
|
||||||
|
|
||||||
|
expect(result).toBe("onlyKey");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty objects", () => {
|
||||||
|
const obj = {};
|
||||||
|
const result = randomUtil.getKey(obj);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle objects with integer keys", () => {
|
||||||
|
const obj = { 1: "a", 2: "b", 3: "c" };
|
||||||
|
const result = randomUtil.getKey(obj);
|
||||||
|
|
||||||
|
expect(Object.keys(obj)).toContain(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return predictable key when getArrayValue is mocked", () => {
|
||||||
|
vi.spyOn(randomUtil, "getArrayValue").mockReturnValue("b");
|
||||||
|
|
||||||
|
const obj = { a: 1, b: 2, c: 3 };
|
||||||
|
const result = randomUtil.getKey(obj);
|
||||||
|
|
||||||
|
expect(result).toBe("b");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getKeyValue", () => {
|
||||||
|
it("should return a value from the object", () => {
|
||||||
|
vi.spyOn(randomUtil, "getKey").mockReturnValue("b");
|
||||||
|
|
||||||
|
const obj = { a: 1, b: 2, c: 3 };
|
||||||
|
const result = randomUtil.getKeyValue(obj);
|
||||||
|
|
||||||
|
expect(result).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle objects with complex values", () => {
|
||||||
|
vi.spyOn(randomUtil, "getKey").mockReturnValue("key2");
|
||||||
|
|
||||||
|
const obj = { key1: "value1", key2: { nested: true }, key3: [1, 2, 3] };
|
||||||
|
const result = randomUtil.getKeyValue(obj);
|
||||||
|
|
||||||
|
expect(result).toEqual({ nested: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getNormallyDistributedRandomNumber", () => {
|
||||||
|
it("should return a number close to the mean", () => {
|
||||||
|
const mean = 50;
|
||||||
|
const sigma = 5;
|
||||||
|
const result = randomUtil.getNormallyDistributedRandomNumber(mean, sigma);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(typeof result).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not return negative numbers", () => {
|
||||||
|
const mean = 5;
|
||||||
|
const sigma = 10;
|
||||||
|
const result = randomUtil.getNormallyDistributedRandomNumber(mean, sigma);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle high attempt counts", () => {
|
||||||
|
let callCount = 0;
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockImplementation(() => {
|
||||||
|
callCount++;
|
||||||
|
// Alternate between u and v
|
||||||
|
if (callCount % 2 === 1) {
|
||||||
|
// u values
|
||||||
|
return 0.00001;
|
||||||
|
}
|
||||||
|
// v values
|
||||||
|
return 0.5;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mean = 5;
|
||||||
|
const sigma = 2;
|
||||||
|
const result = randomUtil.getNormallyDistributedRandomNumber(mean, sigma);
|
||||||
|
|
||||||
|
// Each attempt increases callCount by 2 (once for u, once for v)
|
||||||
|
const attempts = callCount / 2;
|
||||||
|
|
||||||
|
// Ensure that the method attempted multiple times
|
||||||
|
expect(attempts).toBeGreaterThan(100);
|
||||||
|
|
||||||
|
// The result should be from the fallback value
|
||||||
|
expect(result).toBeGreaterThanOrEqual(0.01);
|
||||||
|
expect(result).toBeLessThanOrEqual(mean * 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a fallback value after many attempts", () => {
|
||||||
|
let callCount = 0;
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockImplementation(() => {
|
||||||
|
// Alternate between u and v
|
||||||
|
const value = callCount % 2 === 0 ? 0.0000001 : 0.5;
|
||||||
|
callCount++;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock getFloat to return a predictable value
|
||||||
|
vi.spyOn(randomUtil, "getFloat").mockReturnValue(7.77);
|
||||||
|
|
||||||
|
const mean = 5;
|
||||||
|
const sigma = 2;
|
||||||
|
const result = randomUtil.getNormallyDistributedRandomNumber(mean, sigma, 101);
|
||||||
|
|
||||||
|
expect(result).toBe(7.77);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("randInt", () => {
|
||||||
|
it("should return an integer between low and high - 1", () => {
|
||||||
|
const low = 5;
|
||||||
|
const high = 10;
|
||||||
|
const result = randomUtil.randInt(low, high);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(low);
|
||||||
|
expect(result).toBeLessThan(high);
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an integer between 0 and low - 1 when high is not provided", () => {
|
||||||
|
const low = 5;
|
||||||
|
const result = randomUtil.randInt(low);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(result).toBeLessThan(low);
|
||||||
|
expect(Number.isInteger(result)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle negative values", () => {
|
||||||
|
const low = -10;
|
||||||
|
const high = -5;
|
||||||
|
const result = randomUtil.randInt(low, high);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(low);
|
||||||
|
expect(result).toBeLessThan(high);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("drawRandomFromList", () => {
|
||||||
|
it("should draw elements with replacement", () => {
|
||||||
|
const list = [1, 2, 3, 4, 5];
|
||||||
|
const count = 3;
|
||||||
|
const result = randomUtil.drawRandomFromList(list, count, true);
|
||||||
|
|
||||||
|
expect(result.length).toBe(count);
|
||||||
|
for (const item of result) {
|
||||||
|
expect(list).toContain(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should draw elements without replacement", () => {
|
||||||
|
mockCloner.clone.mockImplementation((obj) => [...obj]);
|
||||||
|
|
||||||
|
const list = [1, 2, 3, 4, 5];
|
||||||
|
const count = 3;
|
||||||
|
const result = randomUtil.drawRandomFromList(list, count, false);
|
||||||
|
|
||||||
|
expect(result.length).toBe(count);
|
||||||
|
expect(new Set(result).size).toBe(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone the original list when drawing without replacement", () => {
|
||||||
|
const list = [1, 2, 3];
|
||||||
|
randomUtil.drawRandomFromList(list, 2, false);
|
||||||
|
|
||||||
|
expect(mockCloner.clone).toHaveBeenCalledWith(list);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle count greater than list length without replacement", () => {
|
||||||
|
const list = [1, 2, 3];
|
||||||
|
const count = 5;
|
||||||
|
const result = randomUtil.drawRandomFromList(list, count, false);
|
||||||
|
|
||||||
|
expect(result.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should adjust count when count exceeds list length without replacement", () => {
|
||||||
|
const list = [10, 20, 30];
|
||||||
|
const count = 4; // Count exceeds list length
|
||||||
|
const result = randomUtil.drawRandomFromList(list, count, false);
|
||||||
|
|
||||||
|
// The result should contain all elements from the list without duplicates
|
||||||
|
expect(result.length).toBe(3); // Should be adjusted to list length
|
||||||
|
expect(new Set(result).size).toBe(3);
|
||||||
|
|
||||||
|
// Ensure that the result contains all elements from the original list
|
||||||
|
expect(result.sort()).toEqual(list.sort());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("drawRandomFromDict", () => {
|
||||||
|
it("should draw keys from the dictionary with replacement", () => {
|
||||||
|
const dict = { a: 1, b: 2, c: 3 };
|
||||||
|
const count = 2;
|
||||||
|
const result = randomUtil.drawRandomFromDict(dict, count, true);
|
||||||
|
|
||||||
|
expect(result.length).toBe(count);
|
||||||
|
for (const key of result) {
|
||||||
|
expect(Object.keys(dict)).toContain(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should draw keys without replacement", () => {
|
||||||
|
const dict = { a: 1, b: 2, c: 3 };
|
||||||
|
const count = 2;
|
||||||
|
const result = randomUtil.drawRandomFromDict(dict, count, false);
|
||||||
|
|
||||||
|
expect(result.length).toBe(count);
|
||||||
|
expect(new Set(result).size).toBe(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle single-key dictionaries", () => {
|
||||||
|
const dict = { onlyKey: 1 };
|
||||||
|
const count = 2;
|
||||||
|
const result = randomUtil.drawRandomFromDict(dict, count, false);
|
||||||
|
|
||||||
|
expect(result.length).toBe(1);
|
||||||
|
expect(result).toEqual(["onlyKey"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle dictionaries with integer keys", () => {
|
||||||
|
const dict = { 1: "a", 2: "b", 3: "c" };
|
||||||
|
const count = 2;
|
||||||
|
const result = randomUtil.drawRandomFromDict(dict, count, false);
|
||||||
|
|
||||||
|
expect(result.length).toBe(count);
|
||||||
|
for (const key of result) {
|
||||||
|
expect(Object.keys(dict)).toContain(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle count greater than number of keys without replacement", () => {
|
||||||
|
const dict = { a: 1, b: 2 };
|
||||||
|
const count = 3;
|
||||||
|
const result = randomUtil.drawRandomFromDict(dict, count, false);
|
||||||
|
|
||||||
|
expect(result.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should adjust count when count exceeds number of keys without replacement", () => {
|
||||||
|
const dict = { a: 1, b: 2, c: 3 };
|
||||||
|
const count = 5; // Count exceeds number of keys
|
||||||
|
const result = randomUtil.drawRandomFromDict(dict, count, false);
|
||||||
|
|
||||||
|
// The result should contain all keys without duplicates
|
||||||
|
expect(result.length).toBe(3); // Should be adjusted to number of keys
|
||||||
|
expect(new Set(result).size).toBe(3);
|
||||||
|
|
||||||
|
// Ensure that the result contains all keys from the dictionary
|
||||||
|
const dictKeys = Object.keys(dict);
|
||||||
|
expect(result.sort()).toEqual(dictKeys.sort());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getBiasedRandomNumber", () => {
|
||||||
|
it("should return a number within the range", () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 10;
|
||||||
|
const shift = 2;
|
||||||
|
const n = 2;
|
||||||
|
const result = randomUtil.getBiasedRandomNumber(min, max, shift, n);
|
||||||
|
|
||||||
|
expect(result).toBeGreaterThanOrEqual(min);
|
||||||
|
expect(result).toBeLessThanOrEqual(max);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when max < min", () => {
|
||||||
|
expect(() => randomUtil.getBiasedRandomNumber(10, 5, 0, 2)).toThrowError(
|
||||||
|
"Bounded random number generation max is smaller than min (5 < 10)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when n < 1", () => {
|
||||||
|
expect(() => randomUtil.getBiasedRandomNumber(1, 10, 0, 0)).toThrowError(
|
||||||
|
"'n' must be 1 or greater (received 0)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log warning when shift is greater than range", () => {
|
||||||
|
const min = 1;
|
||||||
|
const max = 5;
|
||||||
|
const shift = 10;
|
||||||
|
const n = 2;
|
||||||
|
randomUtil.getBiasedRandomNumber(min, max, shift, n);
|
||||||
|
|
||||||
|
expect(mockLogger.warning).toHaveBeenCalled();
|
||||||
|
expect(mockLogger.info).toHaveBeenCalledWith(`min -> ${min}; max -> ${max}; shift -> ${shift}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return predictable result when getSecureRandomNumber is mocked", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.5);
|
||||||
|
|
||||||
|
const min = 1;
|
||||||
|
const max = 10;
|
||||||
|
const shift = 0;
|
||||||
|
const n = 2; // n affects how many times getSecureRandomNumber is summed/averaged
|
||||||
|
|
||||||
|
// With getSecureRandomNumber always returning 0.5,
|
||||||
|
// gaussianRandom(n) = 0.5 no matter what
|
||||||
|
// boundedGaussian(start, end, n) = round(start + 0.5 * (end - start + 1))
|
||||||
|
|
||||||
|
// For shift = 0:
|
||||||
|
// biasedMin = min = 1
|
||||||
|
// biasedMax = max = 10
|
||||||
|
// boundedGaussian(1, 10, 2) = round(1 + 0.5*(10 - 1 + 1)) = round(1 + 0.5*10) = round(1+5) = 6
|
||||||
|
|
||||||
|
// The loop ensures num is within [min, max], and since 6 is within [1,10], it returns 6 immediately.
|
||||||
|
const result = randomUtil.getBiasedRandomNumber(min, max, shift, n);
|
||||||
|
|
||||||
|
expect(result).toBe(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("shuffle", () => {
|
||||||
|
it("should shuffle the array", () => {
|
||||||
|
const array = [1, 2, 3, 4, 5];
|
||||||
|
const shuffled = randomUtil.shuffle([...array]);
|
||||||
|
|
||||||
|
expect(shuffled).toHaveLength(array.length);
|
||||||
|
expect(shuffled).not.toEqual(array);
|
||||||
|
expect(shuffled.sort()).toEqual(array.sort());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty arrays", () => {
|
||||||
|
const array: any[] = [];
|
||||||
|
const shuffled = randomUtil.shuffle(array);
|
||||||
|
|
||||||
|
expect(shuffled).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the same array when array length is 1", () => {
|
||||||
|
const array = [1];
|
||||||
|
const shuffled = randomUtil.shuffle(array);
|
||||||
|
|
||||||
|
expect(shuffled).toEqual([1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rollForChanceProbability", () => {
|
||||||
|
it("should return true when rolled chance is less than or equal to probabilityChance", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.5);
|
||||||
|
|
||||||
|
const probabilityChance = 0.6;
|
||||||
|
const result = randomUtil.rollForChanceProbability(probabilityChance);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when rolled chance is greater than probabilityChance", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.7);
|
||||||
|
|
||||||
|
const probabilityChance = 0.6;
|
||||||
|
const result = randomUtil.rollForChanceProbability(probabilityChance);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle probabilityChance of 0", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.1);
|
||||||
|
|
||||||
|
const probabilityChance = 0;
|
||||||
|
const result = randomUtil.rollForChanceProbability(probabilityChance);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle probabilityChance of 1", () => {
|
||||||
|
vi.spyOn(randomUtil as any, "getSecureRandomNumber").mockReturnValue(0.99);
|
||||||
|
|
||||||
|
const probabilityChance = 1;
|
||||||
|
const result = randomUtil.rollForChanceProbability(probabilityChance);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user