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; }
}