using System.Text.Json.Serialization; using SPTarkov.Server.Core.Extensions; using SPTarkov.Server.Core.Utils.Cloners; namespace SPTarkov.Server.Core.Utils.Collections; /// /// Array of ProbabilityObjectArray which allow to randomly draw of the contained objects /// based on the relative probability of each of its elements. /// The probabilities of the contained element is not required to be normalized. /// Example: /// po = new ProbabilityObjectArray( /// new ProbabilityObject("a", 5), /// new ProbabilityObject("b", 1), /// new ProbabilityObject("c", 1) /// ); /// res = po.draw(10000); /// // count the elements which should be distributed according to the relative probabilities /// res.filter(x => x==="b").reduce((sum, x) => sum + 1 , 0) /// /// /// public class ProbabilityObjectArray : List> { private readonly ICloner _cloner; public ProbabilityObjectArray(ICloner cloner, ICollection>? items = null) : base(items ?? []) { _cloner = cloner; } /// /// Calculates the normalized cumulative probability of the ProbabilityObjectArray's elements normalized to 1 /// /// The relative probability values of which to calculate the normalized cumulative sum /// Cumulative Sum normalized to 1 public IEnumerable CumulativeProbability(IEnumerable probValues) { var sum = probValues.Sum(); var probCumsum = probValues.CumulativeSum(); probCumsum = probCumsum.Product(1D / sum); return probCumsum; } /// /// Filter What is inside ProbabilityObjectArray /// /// /// Filtered results public ProbabilityObjectArray Filter(Predicate> predicate) { var result = new ProbabilityObjectArray(_cloner, new List>()); foreach (var probabilityObject in this) { if (predicate.Invoke(probabilityObject)) { result.Add(probabilityObject); } } return result; } /// /// Deep clone this ProbabilityObjectArray /// /// Deep Copy of ProbabilityObjectArray public ProbabilityObjectArray Clone() { var clone = _cloner.Clone(this); var probabilityObjects = new ProbabilityObjectArray(_cloner, new List>()); probabilityObjects.AddRange(clone); return probabilityObjects; } /// /// Drop an element from the ProbabilityObjectArray /// /// The key of the element to drop /// ProbabilityObjectArray without the dropped element public ProbabilityObjectArray Drop(K key) { return (ProbabilityObjectArray)this.Where(r => !r.Key?.Equals(key) ?? false); } /// /// Return the data field of an element of the ProbabilityObjectArray /// /// The key of the element whose data shall be retrieved /// Stored data object public V? Data(K key) { var element = this.FirstOrDefault(r => r.Key?.Equals(key) ?? false); return element == null ? default : element.Data; } /// /// Get the relative probability of an element by its key /// Example: /// po = new ProbabilityObjectArray(new ProbabilityObject("a", 5), new ProbabilityObject("b", 1)) /// po.maxProbability() // returns 5 /// /// Key of element whose relative probability shall be retrieved /// The relative probability public double? Probability(K key) { var element = this.FirstOrDefault(r => r.Key.Equals(key)); return element?.RelativeProbability; } /// /// Get the maximum relative probability out of a ProbabilityObjectArray /// Example: /// po = new ProbabilityObjectArray(new ProbabilityObject("a", 5), new ProbabilityObject("b", 1)) /// po.maxProbability() // returns 5 /// /// the maximum value of all relative probabilities in this ProbabilityObjectArray public double MaxProbability() { return this.Max(x => x.RelativeProbability).Value; } /// /// Get the minimum relative probability out of a ProbabilityObjectArray /// * Example: /// po = new ProbabilityObjectArray(new ProbabilityObject("a", 5), new ProbabilityObject("b", 1)) /// po.minProbability() // returns 1 /// /// the minimum value of all relative probabilities in this ProbabilityObjectArray public double MinProbability() { return this.Min(x => x.RelativeProbability.Value); } /// ///Draw random element of the ProbabilityObject N times to return an array of N keys /// Keeps chosen element in place /// Chosen items can be duplicates /// /// The number of times we want to draw /// Collection consisting of N random keys for this ProbabilityObjectArray public List Draw(int itemCountToDraw = 1) { if (Count == 0) { // Nothing in pool return []; } var cumulativeProbabilities = CumulativeProbability(this.Select(x => x.RelativeProbability.Value)).ToList(); // Init results collection var results = new List(itemCountToDraw); // Loop until we've picked to desired item count for (var i = 0; i < itemCountToDraw; i++) { var rand = Random.Shared.NextDouble(); var randomIndex = cumulativeProbabilities.FindIndex(probability => probability >= rand); if (randomIndex == -1) { continue; } results.Add(this[randomIndex].Key); } return results; } /// ///Draw random element of the ProbabilityObject N times to return an array of N keys /// Removes drawn elements /// /// The number of times we want to draw /// List of keys which shall be replaced even if drawing without replacement /// Collection consisting of N random keys for this ProbabilityObjectArray public List DrawAndRemove(int itemCountToDraw = 1, List? neverRemoveWhitelist = null) { if (Count == 0) { // Nothing in pool return []; } var availableItems = this.Select(x => (x.Key, Weight: x.RelativeProbability.Value)).ToList(); // Calculate total weighting of all items combined var totalWeight = availableItems.Sum(x => x.Weight); // Init results collection var drawnKeys = new List(itemCountToDraw); // Loop until we have drawn to desired count or pool is empty for (var i = 0; i < itemCountToDraw && availableItems.Any(); i++) { // Get value between 0 and 1 to act as a target to aim for var randomTarget = Random.Shared.NextDouble() * totalWeight; // Set default index to start var chosenIndex = -1; // Find element related to random target (greedy) for (var j = 0; j < availableItems.Count; j++) { // Subtract weight of item from above chosen value randomTarget -= availableItems[j].Weight; if (randomTarget <= 0) { // Item falls within 'slice' of desired target, // item has weight that eclipses accumulated weight of randomTarget chosenIndex = j; break; } } // If index not found choose the last element chosenIndex = (chosenIndex == -1) ? availableItems.Count - 1 : chosenIndex; // Get chosen item via index and add to results var chosenItem = availableItems[chosenIndex]; drawnKeys.Add(chosenItem.Key); // Only remove item if it's not in whitelist if (neverRemoveWhitelist is null || !neverRemoveWhitelist.Contains(chosenItem.Key)) { // Reduce total weight value by items weight + Remove item from pool totalWeight -= chosenItem.Weight; availableItems.RemoveAt(chosenIndex); } } return drawnKeys; } } /// /// A ProbabilityObject which is use as an element to the ProbabilityObjectArray array /// It contains a key, the relative probability as well as optional data. /// /// /// public class ProbabilityObject { public ProbabilityObject() { } /// /// constructor for the ProbabilityObject /// /// The key of the element /// The relative probability of this element /// Optional data attached to the element public ProbabilityObject(K key, double? relativeProbability, V? data) { Key = key; RelativeProbability = relativeProbability; Data = data; } [JsonPropertyName("key")] public K? Key { get; set; } /// /// Weighting of key compared to other ProbabilityObjects /// [JsonPropertyName("relativeProbability")] public double? RelativeProbability { get; set; } [JsonPropertyName("data")] public V? Data { get; set; } }