RandomUtil fixes

This commit is contained in:
clodan
2025-01-29 11:47:42 +00:00
parent 5c30fa1152
commit 7c060fdf86
9 changed files with 81 additions and 120 deletions
@@ -515,7 +515,7 @@ public class RepeatableQuestGenerator(
);
// Be fair, don't var the items be more expensive than the reward
var multi = _randomUtil.GetFloat((float)0.5, 1);
var multi = _randomUtil.GetDouble(0.5, 1);
var roublesBudget = Math.Floor(
(double)(_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) * multi)
);
@@ -264,7 +264,7 @@ public class RepeatableQuestRewardGenerator(
return Math.Floor(
effectiveDifficulty *
_mathUtil.Interp1(pmcLevel, levelsConfig, xpConfig) *
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
_randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ??
0
);
}
@@ -276,7 +276,7 @@ public class RepeatableQuestRewardGenerator(
return Math.Ceiling(
effectiveDifficulty *
_mathUtil.Interp1(pmcLevel, levelsConfig, gpCoinConfig) *
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
_randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ??
0
);
}
@@ -289,7 +289,7 @@ public class RepeatableQuestRewardGenerator(
100 *
effectiveDifficulty *
_mathUtil.Interp1(pmcLevel, levelsConfig, reputationConfig) *
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
_randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ??
0
) /
100;
@@ -307,7 +307,7 @@ public class RepeatableQuestRewardGenerator(
return Math.Floor(
effectiveDifficulty *
_mathUtil.Interp1(pmcLevel, levelsConfig, roublesConfig) *
_randomUtil.GetFloat((float)(1 - rewardSpreadConfig), (float)(1 + rewardSpreadConfig)) ??
_randomUtil.GetDouble((double)(1 - rewardSpreadConfig), (double)(1 + rewardSpreadConfig)) ??
0
);
}
+2 -2
View File
@@ -124,7 +124,7 @@ public class RepairHelper(
? armorMaterialSettings.MaxRepairKitDegradation
: armorMaterialSettings.MaxRepairDegradation;
var duraLossPercent = _randomUtil.GetFloat((float)minMultiplier, (float)maxMultiplier);
var duraLossPercent = _randomUtil.GetDouble((double)minMultiplier, (double)maxMultiplier);
var duraLossMultipliedByTraderMultiplier = duraLossPercent * armorMax * traderQualityMultipler;
return Math.Round(duraLossMultipliedByTraderMultiplier, 2);
@@ -153,7 +153,7 @@ public class RepairHelper(
maxRepairDeg = itemProps.MaxRepairDegradation;
}
var duraLossPercent = _randomUtil.GetFloat((float)minRepairDeg, (float)maxRepairDeg);
var duraLossPercent = _randomUtil.GetDouble((double)minRepairDeg, (double)maxRepairDeg);
var duraLossMultipliedByTraderMultiplier = duraLossPercent * weaponMax * traderQualityMultipler;
return Math.Round(duraLossMultipliedByTraderMultiplier, 2);
+1 -3
View File
@@ -582,9 +582,7 @@ public class TraderHelper(
var traderBuyBackPricePercent = traderBase.LoyaltyLevels.FirstOrDefault().BuyPriceCoefficient;
var itemHandbookPrice = _handbookHelper.GetTemplatePrice(tpl);
var priceTraderBuysItemAt = Math.Round(
_randomUtil.GetPercentOfValue(traderBuyBackPricePercent ?? 0, itemHandbookPrice ?? 0)
);
var priceTraderBuysItemAt = _randomUtil.GetPercentOfValue(traderBuyBackPricePercent ?? 0, itemHandbookPrice ?? 0, 0);
// Price from this trader is higher than highest found, update
if (priceTraderBuysItemAt > highestPrice)
@@ -126,9 +126,9 @@ public class CircleOfCultistService(
private double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings)
{
// Get a randomised value to multiply the sacrificed rouble cost by
var rewardAmountMultiplier = _randomUtil.GetFloat(
(float)cultistCircleSettings.RewardPriceMultiplerMinMax.Min,
(float)cultistCircleSettings.RewardPriceMultiplerMinMax.Max
var rewardAmountMultiplier = _randomUtil.GetDouble(
(double)cultistCircleSettings.RewardPriceMultiplerMinMax.Min,
(double)cultistCircleSettings.RewardPriceMultiplerMinMax.Max
);
// Adjust value generated by the players hideout management skill
+1 -3
View File
@@ -529,9 +529,7 @@ public class RepairService(
Rarity = bonusRarityName,
BuffType = bonusTypeName,
Value = bonusValue,
ThresholdDurability = Math.Round(
_randomUtil.GetPercentOfValue(bonusThresholdPercent, item.Upd.Repairable.Durability.Value)
)
ThresholdDurability = _randomUtil.GetPercentOfValue(bonusThresholdPercent, item.Upd.Repairable.Durability.Value, 0)
};
}
+41 -87
View File
@@ -11,6 +11,8 @@ namespace Core.Utils;
public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
{
public readonly Random Random = new();
private const int DecimalPointRandomPrecision = 6;
private static readonly int DecimalPointRandomPrecisionMultiplier = (int) Math.Pow(10, DecimalPointRandomPrecision);
/// <summary>
/// The IEEE-754 standard for double-precision floating-point numbers limits the number of digits (including both
@@ -22,40 +24,20 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// Generates a random integer between the specified minimum and maximum values, inclusive.
/// </summary>
/// <param name="min">The minimum value (inclusive).</param>
/// <param name="max">The maximum value (inclusive).</param>
/// <param name="max">The maximum value (optional).</param>
/// <param name="exclusive">If max is exclusive or not.</param>
/// <returns>A random integer between the specified minimum and maximum values.</returns>
public int GetInt(int min, int max)
public int GetInt(int min, int max = int.MaxValue, bool exclusive = false)
{
// Prevents a potential integer overflow.
if (max == int.MaxValue) max -= 1;
if (exclusive && max == int.MaxValue)
{
max -= 1;
}
// maxVal is exclusive of the passed value, so add 1
return max > min ? Random.Next(min, max + 1) : min;
return max > min ? Random.Next(min, exclusive ? max : max + 1) : min;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="max">The upper bound (exclusive) for the random integer generation.</param>
/// <returns>A random integer between 1 and max - 1, or 1 if max is less than or equal to 1.</returns>
public int GetIntEx(int max)
{
return max > 2 ? Random.Next(1, max - 1) : 1;
}
/// <summary>
/// Generates a random floating-point number within the specified range ~6-9 digits (4 bytes).
/// </summary>
/// <param name="min">The minimum value of the range (inclusive).</param>
/// <param name="max">The maximum value of the range (exclusive).</param>
/// <returns>A random floating-point number between `min` (inclusive) and `max` (exclusive).</returns>
public float GetFloat(float min, float max)
{
return (float)GetSecureRandomNumber() * (max - min) + min;
}
/// <summary>
/// Generates a random floating-point number within the specified range ~15-17 digits (8 bytes).
/// </summary>
@@ -64,7 +46,13 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// <returns>A random floating-point number between `min` (inclusive) and `max` (exclusive).</returns>
public double GetDouble(double min, double max)
{
return GetSecureRandomNumber() * (max - min) + min;
var realMin = (long) (min * DecimalPointRandomPrecisionMultiplier);
var realMax = (long) (max * DecimalPointRandomPrecisionMultiplier);
return Math.Round(
Random.NextInt64(realMin, realMax) / (double)DecimalPointRandomPrecisionMultiplier,
DecimalPointRandomPrecision
);
}
/// <summary>
@@ -73,7 +61,7 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// <returns>A random boolean value, where the probability of `true` and `false` is approximately equal.</returns>
public bool GetBool()
{
return GetSecureRandomNumber() < 0.5;
return Random.Next(0, 2) == 1;
}
/// <summary>
@@ -83,11 +71,11 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// <param name="number">The number to calculate the percentage of.</param>
/// <param name="toFixed">The number of decimal places to round the result to (default is 2).</param>
/// <returns>The calculated percentage of the given number, rounded to the specified number of decimal places.</returns>
public float GetPercentOfValue(double percent, double number, int toFixed = 2)
public double GetPercentOfValue(double percent, double number, int toFixed = 2)
{
var num = percent * number / 100;
return (float)Math.Round(num, toFixed);
return Math.Round(num, toFixed);
}
/// <summary>
@@ -110,9 +98,9 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// <returns>`true` if the event occurs, `false` otherwise.</returns>
public bool GetChance100(double? chancePercent)
{
chancePercent = Math.Clamp(chancePercent ?? 0, 0f, 100f);
chancePercent = Math.Clamp(chancePercent ?? 0, 0D, 100D);
return GetIntEx(100) <= chancePercent;
return GetInt(0, 100) <= chancePercent;
}
/// <summary>
@@ -196,7 +184,7 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
// Check if the generated value is valid
if (valueDrawn < 0)
return attempt > 100
? GetDouble(0.01f, mean * 2f)
? GetDouble(0.01D, mean * 2D)
: GetNormallyDistributedRandomNumber(mean, sigma, attempt + 1);
return valueDrawn;
@@ -229,42 +217,20 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// and MaxSignificantDigits(15), inclusive. If not provided, precision is determined by the input values.
/// </param>
/// <returns></returns>
public double RandNum(double val1, double val2 = 0, byte? precision = null)
public double RandNum(double val1, double val2 = 0, int precision = DecimalPointRandomPrecision)
{
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;
var realPrecision = (long) Math.Pow(10, precision);
var minInt = (long) (min * realPrecision);
var maxInt = (long) (max * realPrecision);
return Math.Round(Random.NextInt64(minInt, maxInt) / (double) realPrecision, precision);
}
/// <summary>
@@ -436,23 +402,9 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// produce a floating-point number in the range [0, 1).
/// </summary>
/// <returns>A secure random number between 0 (inclusive) and 1 (exclusive).</returns>
private static double GetSecureRandomNumber()
private double GetSecureRandomNumber()
{
var buffer = new byte[6]; // 48 bits
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(buffer);
}
// Convert byte array to unsigned long
ulong value = 0;
for (var i = 0; i < buffer.Length; i++)
{
value |= (ulong)buffer[i] << (8 * (buffer.Length - 1 - i));
}
const ulong maxInteger = 281474976710656; // 2^48
return (double)value / maxInteger;
return Random.NextSingle();
}
/// <summary>
@@ -462,11 +414,13 @@ public class RandomUtil(ISptLogger<RandomUtil> _logger, ICloner _cloner)
/// <returns>The number of decimal places, or 0 if none exist.</returns>
public int GetNumberPrecision(double num)
{
var parts = num.ToString($"G{MaxSignificantDigits}").Split('.');
return parts.Length > 1
? parts[1].Length
: 0;
var factor = 0;
while (num % 1 > double.Epsilon)
{
num *= 10D;
factor++;
}
return factor;
}
public T? GetArrayValue<T>(IEnumerable<T> list)
+1 -1
View File
@@ -14,7 +14,7 @@ public class Test
public void Setup()
{
var importer = new ImporterUtil(new MockLogger<ImporterUtil>(), new FileUtil(), new JsonUtil());
var importer = new ImporterUtil(new MockLogger<ImporterUtil>(), new FileUtil(new MockLogger<FileUtil>()), new JsonUtil());
var loadTask = importer.LoadRecursiveAsync<Templates>("./TestAssets/");
loadTask.Wait();
_templates = loadTask.Result;
+27 -16
View File
@@ -12,8 +12,8 @@ public sealed class RandomUtilTests
[TestMethod]
public void GetIntTest()
{
// Run 100 test cases
for (var i = 0; i < 100; i++)
// Run 10000 test cases
for (var i = 0; i < 10000; i++)
{
var result = _randomUtil.GetInt(0, 10);
@@ -27,10 +27,10 @@ public sealed class RandomUtilTests
[TestMethod]
public void GetIntExTest()
{
// Run 100 test cases
for (var i = 0; i < 100; i++)
// Run 10000 test cases
for (var i = 0; i < 10000; i++)
{
var result = _randomUtil.GetIntEx(10);
var result = _randomUtil.GetInt(1, 10, true);
if (result < 1 || result > 9)
{
@@ -40,16 +40,16 @@ public sealed class RandomUtilTests
}
[TestMethod]
public void GetFloatTest()
public void GetDoubleTest()
{
// Run 100 test cases
for (var i = 0; i < 100; i++)
// Run 10000 test cases
for (var i = 0; i < 10000; i++)
{
var result = _randomUtil.GetFloat(0f, 10f);
var result = _randomUtil.GetDouble(0D, 10D);
if (result < 0f || result >= 9f)
if (result is < 0d or >= 10d)
{
Assert.Fail($"GetFloat(0f, 10f) out of range. Expected range [0.0f, 9.999f] but was {result}.");
Assert.Fail($"GetDouble(0d, 10d) out of range. Expected range [0.0d, 9.999d] but was {result}.");
}
}
}
@@ -135,11 +135,11 @@ public sealed class RandomUtilTests
[TestMethod]
public void RandNumTest()
{
for (var i = 0; i < 100; i++)
for (var i = 0; i < 10000; i++)
{
var result = _randomUtil.RandNum(0, 10);
var result = _randomUtil.RandNum(0, 10, 15);
if (result < 0 || result > 9)
if (result < 0 || result >= 10)
{
Assert.Fail($"RandNum(0, 10) out of range. Expected range [0, 9.999d] but was {result}.");
}
@@ -150,11 +150,11 @@ public sealed class RandomUtilTests
}
}
for (var i = 0; i < 100; i++)
for (var i = 0; i < 10000; i++)
{
var result = _randomUtil.RandNum(10);
if (result < 0 || result > 9)
if (result < 0 || result >= 10)
{
Assert.Fail($"RandNum(10) out of range. Expected range [0, 9.999d] but was {result}.");
}
@@ -182,4 +182,15 @@ public sealed class RandomUtilTests
result.SequenceEqual(orig),
$"Shuffle test failed. Expected: {string.Join(", ", orig)}, but got {string.Join(", ", result)}");
}
[TestMethod]
[DataRow(0.1, 1)]
[DataRow(0.0001, 4)]
[DataRow(0, 0)]
[DataRow(10000000, 0)]
[DataRow(0.000_000_000_000_1D, 13)]
public void GetNumberPrecision_WithDoubles_ReturnsDecimalPoints(double value, int decimalPoints)
{
Assert.AreEqual(decimalPoints, _randomUtil.GetNumberPrecision(value));
}
}