using SptCommon.Annotations; using Core.Models.Spt.Helper; using Core.Models.Utils; namespace Core.Helpers; [Injectable] public class WeightedRandomHelper( ISptLogger _logger ) { /// /// Choose an item from the passed in array based on the weightings of each /// /// Items and weights to use /// Chosen item from array public T GetWeightedValue(Dictionary values) where T : notnull { var itemKeys = values.Keys.ToList(); var weights = values.Values.ToList(); var chosenItem = WeightedRandom(itemKeys, weights); return chosenItem.Item; } /// /// Picks the random item based on its weight. /// The items with higher weight will be picked more often (with a higher probability). /// /// For example: /// - items = ['banana', 'orange', 'apple'] /// - weights = [0, 0.2, 0.8] /// - weightedRandom(items, weights) in 80% of cases will return 'apple', in 20% of cases will return /// 'orange' and it will never return 'banana' (because probability of picking the banana is 0%) /// /// /// List of items /// List of weights /// Dictionary with item and index public WeightedRandomResult WeightedRandom(List items, List weights) { if (items.Count == 0) _logger.Error("Items must not be empty"); if (weights.Count == 0) _logger.Error("Item weights must not be empty"); if (items.Count != weights.Count) _logger.Error("Items and weight inputs must be of the same length"); // Preparing the cumulative weights list. List cumulativeWeights = []; for (var i = 0; i < weights.Count; i++) cumulativeWeights.Add((int)weights[i] + (i > 0 ? cumulativeWeights[i - 1] : 0)); // Getting the random number in a range of [0...sum(weights)] var maxCumulativeWeight = cumulativeWeights[cumulativeWeights.Count - 1]; var randomNumber = maxCumulativeWeight * new Random().NextDouble(); // Picking the random item based on its weight. for (var itemIndex = 0; itemIndex < items.Count; itemIndex++) if (cumulativeWeights[itemIndex] >= randomNumber) return new WeightedRandomResult() { Item = items[itemIndex], Index = itemIndex }; throw new InvalidOperationException("No item was picked."); } /// /// Find the greated common divisor of all weights and use it on the passed in dictionary /// /// Values to reduce public void ReduceWeightValues(Dictionary weightedDict) { // No values, nothing to reduce if (weightedDict.Count == 0) return; // Only one value, set to 1 and exit if (weightedDict.Count == 1) { var kvp = weightedDict.FirstOrDefault(); weightedDict[kvp.Key] = 1; return; } var weights = weightedDict.Values.ToList(); var commonDivisor = CommonDivisor(weights); // No point in dividing by 1 if (commonDivisor == 1) return; foreach (var kvp in weightedDict) weightedDict[kvp.Key] /= commonDivisor; } /** * Get the common divisor between all values in the passed in list and returns it */ protected double CommonDivisor(List numbers) { var result = numbers[0]; for (var i = 1; i < numbers.Count; i++) result = Gcd(result, numbers[i]); return result; } protected double Gcd(double a, double b) { var x = a; var y = b; while (y != 0) { var temp = y; y = x % y; x = temp; } return x; } }