using System.Security.Cryptography;
using Core.Annotations;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Utils;
// TODO: Finish porting this class
[Injectable(InjectionType.Singleton)]
public class RandomUtil
{
private readonly ILogger _logger;
public RandomUtil
(
ILogger logger
)
{
_logger = logger;
}
public readonly Random Random = new();
///
/// The IEEE-754 standard for double-precision floating-point numbers limits the number of digits (including both
/// integer + fractional parts) to about 15–17 significant digits. 15 is a safe upper bound, so we'll use that.
///
public const int MaxSignificantDigits = 15;
///
/// Generates a random integer between the specified minimum and maximum values, inclusive.
///
/// The minimum value (inclusive).
/// The maximum value (inclusive).
/// A random integer between the specified minimum and maximum values.
public int GetInt(int min, int max)
{
// Prevents a potential integer overflow.
if (max == int.MaxValue) max -= 1;
// maxVal is exclusive of the passed value, so add 1
return max > min ? Random.Next(min, max + 1) : min;
}
///
/// 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.
///
/// The upper bound (exclusive) for the random integer generation.
/// A random integer between 1 and max - 1, or 1 if max is less than or equal to 1.
public int GetIntEx(int max)
{
return max > 2 ? Random.Next(1, max - 1) : 1;
}
///
/// Generates a random floating-point number within the specified range ~6-9 digits (4 bytes).
///
/// The minimum value of the range (inclusive).
/// The maximum value of the range (exclusive).
/// A random floating-point number between `min` (inclusive) and `max` (exclusive).
public float GetFloat(float min, float max)
{
return (float)GetSecureRandomNumber() * (max - min) + min;
}
///
/// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes).
///
/// The minimum value of the range (inclusive).
/// The maximum value of the range (exclusive).
/// A random floating-point number between `min` (inclusive) and `max` (exclusive).
public double GetDouble(double min, double max)
{
return GetSecureRandomNumber() * (max - min) + min;
}
///
/// Generates a random boolean value.
///
/// A random boolean value, where the probability of `true` and `false` is approximately equal.
public bool GetBool()
{
return GetSecureRandomNumber() < 0.5;
}
///
/// Calculates the percentage of a given number and returns the result.
///
/// The percentage to calculate.
/// The number to calculate the percentage of.
/// The number of decimal places to round the result to (default is 2).
/// The calculated percentage of the given number, rounded to the specified number of decimal places.
public float GetPercentOfValue(float percent, float number, int toFixed = 2)
{
var num = percent * number / 100;
return (float)Math.Round(num, toFixed);
}
///
/// Reduces a given number by a specified percentage.
///
/// The original number to be reduced.
/// The percentage by which to reduce the number.
/// The reduced number after applying the percentage reduction.
public float ReduceValueByPercent(float number, float percentage)
{
var reductionAmount = number * percentage / 100;
return number - reductionAmount;
}
///
/// Determines if a random event occurs based on the given chance percentage.
///
/// The percentage chance (0-100) that the event will occur.
/// `true` if the event occurs, `false` otherwise.
public bool GetChance100(double chancePercent)
{
chancePercent = Math.Clamp(chancePercent, 0f, 100f);
return GetIntEx(100) <= chancePercent;
}
///
/// Returns a random string from the provided collection of strings.
///
/// This method is separate from GetCollectionValue so we can use a generic inference with GetCollectionValue.
///
/// The collection of strings to select a random value from.
/// A randomly selected string from the array.
public string GetStringCollectionValue(IEnumerable collection)
{
return collection.ElementAt(GetInt(0, collection.Count() - 1));
}
///
/// Returns a random type T from the provided collection of type T.
///
/// The collection to get the random element from
/// The type of elements in the collection.
/// A random element from the collection.
/// This was formerly getArrayValue() in the node server
public T GetCollectionValue(IEnumerable collection)
{
return collection.ElementAt(GetInt(0, collection.Count() - 1));
}
///
/// Gets a random key from the given dictionary
///
/// The dictionary from which to retrieve a key.
/// Type of key
/// Type of Value
/// A random TKey representing one of the keys of the dictionary.
public TKey GetKey(Dictionary dictionary) where TKey : notnull
{
return GetCollectionValue(dictionary.Keys);
}
///
/// Gets a random val from the given dictionary
///
/// The dictionary from which to retrieve a value.
/// Type of key
/// Type of Value
/// A random TVal representing one of the values of the dictionary.
public TVal GetVal(Dictionary dictionary) where TKey : notnull
{
return GetCollectionValue(dictionary.Values);
}
///
/// Generates a normally distributed random number using the Box-Muller transform.
///
/// The mean (μ) of the normal distribution.
/// The standard deviation (σ) of the normal distribution.
/// The current attempt count to generate a valid number (default is 0).
/// A normally distributed random number.
///
/// 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 double GetNormallyDistributedRandomNumber(double mean, double sigma, int attempt = 0)
{
double u, v;
do
{
u = GetSecureRandomNumber();
} while (u == 0);
do
{
v = GetSecureRandomNumber();
} while (v == 0);
// Apply the Box-Muller transform
var w = Math.Sqrt(-2.0 * Math.Log(u)) * Math.Cos(2.0 * Math.PI * v);
var valueDrawn = mean + w * sigma;
// Check if the generated value is valid
if (valueDrawn < 0)
return attempt > 100
? GetDouble(0.01f, mean * 2f)
: GetNormallyDistributedRandomNumber(mean, sigma, attempt + 1);
return valueDrawn;
}
///
/// Generates a random integer between the specified range.
///
/// The lower bound of the range (inclusive).
/// The upper bound of the range (exclusive). If not provided, the range will be from 0 to `low`.
/// A random integer within the specified range.
public int RandInt(int low, int? high = null)
{
// Return a random integer from 0 to low if high is not provided
if (high is null) return Random.Next(0, low);
// Return low directly when low and high are equal
return low == high
? low
: Random.Next(low, (int)high);
}
///
/// Generates a random number between two given values with optional precision.
///
/// The first value to determine the range.
/// The second value to determine the range. If not provided, 0 is used.
///
/// The number of decimal places to round the result to. Must be a positive integer between 0
/// and MaxSignificantDigits(15), inclusive. If not provided, precision is determined by the input values.
///
///
public double RandNum(double val1, double val2 = 0, byte? precision = null)
{
if (!double.IsFinite(val1) || !double.IsFinite(val2)) throw new ArgumentException("RandNum() parameters 'value1' and 'value2' must be finite numbers.");
// Determine the range
var min = Math.Min(val1, val2);
var max = Math.Max(val1, val2);
// Validate and adjust precision
if (precision is not null)
{
if (precision > MaxSignificantDigits)
throw new ArgumentOutOfRangeException(
nameof(precision), "Must be less than 16");
// Calculate the number of whole-number digits in the maximum absolute value of the range
var maxAbsoluteValue = Math.Max(Math.Abs(min), Math.Abs(max));
var wholeNumberDigits = (int)Math.Floor(Math.Log10(maxAbsoluteValue)) + 1;
var maxAllowedPrecision = Math.Max(0, MaxSignificantDigits - wholeNumberDigits);
if (precision > maxAllowedPrecision)
throw new ArgumentException(
$"RandNum() precision of {precision} exceeds the allowable precision ({maxAllowedPrecision}) for the given values."
);
}
var result = GetSecureRandomNumber() * (max - min) + min;
// Determine effective precision
var maxPrecision = Math.Max(GetNumberPrecision(val1), GetNumberPrecision(val2));
var effectivePrecision = precision ?? maxPrecision;
var factor = Math.Pow(2, effectivePrecision);
return Math.Round(result * factor) / factor;
}
///
/// Draws a specified number of random elements from a given list.
///
/// The list to draw elements from.
/// The number of elements to draw. Defaults to 1.
/// Whether to draw with replacement. Defaults to true.
/// The type of elements in the list.
/// A List containing the drawn elements.
public List DrawRandomFromList(List originalList, int count = 1, bool replacement = true)
{
throw new NotImplementedException("ICloneable needs implemented on types before this can be written");
}
///
/// Draws a specified number of random keys from a given dictionary.
///
/// The dictionary from which to draw keys.
/// The number of keys to draw. Defaults to 1.
/// Whether to draw with replacement. Defaults to true.
/// The type of elements in keys
/// The type of elements in values
/// A list of randomly drawn keys from the dictionary.
public List DrawRandomFromDict(Dictionary dict, int count = 1, bool replacement = true) where TKey : notnull
{
throw new NotImplementedException("ICloneable needs implemented on types before this can be written");
}
///
/// Generates a biased random number within a specified range.
///
/// The minimum value of the range (inclusive).
/// The maximum value of the range (inclusive).
/// The bias shift to apply to the random number generation.
/// The number of iterations to use for generating a Gaussian random number.
/// A biased random number within the specified range.
public double GetBiasedRandomNumber(double min, double max, double shift, double n)
{
/**
* 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:
* 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.
*
* 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.
*
* 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/
*/
if (max < min)
{
_logger.Error($"Invalid argument, Bounded random number generation max is smaller than min({max} < {min}");
return -1;
}
if (n < 1)
{
_logger.Error($"Invalid argument, 'n' must be 1 or greater(received {n})");
return -1;
}
if (min == max)
{
return min;
}
if (shift > max - min)
{
/**
* If a rolled number is out of bounds (due to bias being applied), we roll it again.
* 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.
* Shifting even further drops the success chance very rapidly - so we want to warn against that
**/
_logger.Warning(
"Bias shift for random number generation is greater than the range of available numbers. This will have a severe performance impact");
_logger.Warning($"min-> {min}; max-> {max}; shift-> {shift}");
}
var biasedMin = shift >= 0 ? min - shift : min;
var biasedMax = shift < 0 ? max + shift : max;
double num;
do
{
num = GetBoundedGaussian(biasedMin, biasedMax, n);
} while (num < min || num > max);
return num;
}
private double GetBoundedGaussian(double start, double end, double n)
{
return Math.Round(start + GetGaussianRandom(n) * (end - start + 1));
}
private double GetGaussianRandom(double n)
{
var rand = 0d;
for (var i = 0; i < n; i += 1)
{
rand += GetSecureRandomNumber();
}
return rand / n;
}
///
/// Shuffles a list in place using the Fisher-Yates algorithm.
///
/// The list to shuffle.
/// The type of elements in the list.
/// The shuffled list.
public List Shuffle(List originalList)
{
var currentIndex = originalList.Count;
while (currentIndex != 0)
{
var randomIndex = GetInt(0, currentIndex);
currentIndex--;
// Swap it with the current element.
(originalList[currentIndex], originalList[randomIndex]) = (originalList[randomIndex], originalList[currentIndex]);
}
return originalList;
}
///
/// 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).
///
/// A secure random number between 0 (inclusive) and 1 (exclusive).
private static double GetSecureRandomNumber()
{
var buffer = new byte[6];
using var rng = RandomNumberGenerator.Create();
// Fill buffer with random bytes
rng.GetBytes(buffer);
var integer = 0;
for (var i = 0; i < 6; i++) integer = (integer << 8) | buffer[i];
const ulong maxInt = 1UL << 48;
return (double)Math.Abs(integer) / maxInt;
}
///
/// Determines the number of decimal places in a number.
///
/// The number to analyze.
/// The number of decimal places, or 0 if none exist.
public int GetNumberPrecision(double num)
{
var parts = num.ToString($"G{MaxSignificantDigits}").Split('.');
return parts.Length > 1
? parts[1].Length
: 0;
}
public T GetArrayValue(IEnumerable list)
{
var rand = new Random();
return list.ElementAt(rand.Next(0, list.Count()));
}
}