#region Apache License 2.0 /* Nuclex .NET Framework Copyright (C) 2002-2024 Markus Ewald / Nuclex Development Labs Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ #endregion // Apache License 2.0 using System; using System.Collections.Generic; #if !NO_SETS namespace Nuclex.Support.Collections { /// Randomly selects between different options, trying to avoid repetition /// Type of keys through which values can be looked up /// Type of values provided by the variegator /// /// /// This class is useful wherever randomness is involved in a game: picking random /// actions for an NPC to execute, selecting different songs to play, displaying /// different dialogue and more. /// /// /// In principle, it works like a multimap, associating keys with a number of values /// and allowing you to look up values by their keys. Unlike a multimap, it will try /// to avoid handing out a previously provided value again as long as possible. /// /// /// A typical usage would be to set up a mapping between situations and dialogue lines. /// Upon calling with the situation 'detected-player-stealing', /// the variegator would return a random (but not recently used) value which in this case /// might contain a commentary an NPC might make upon encountering that situation. /// Other NPCs requesting dialogue lines for the same situation would receive different /// random commentary for as long as long as available data allows. /// /// public class Variegator { /// Initializes a new variegator using the default history length public Variegator() : this(64) {} /// Initializes a new variegator /// /// How far into the past the variegator will look to avoid repetition /// public Variegator(int historyLength) { this.historyLength = historyLength; this.history = new TValue[historyLength]; this.values = new MultiDictionary(); this.randomNumberGenerator = new Random(); } /// Removes all entries from the variegator /// /// This is mainly useful if you are storing smart pointers to values of substantial /// size (eg. audio clips instead of just resource proxies or paths) and need to /// reclaim memory. /// public void Clear() { freeHistory(); this.historyFull = false; this.historyTailIndex = 0; } /// Checks whether the variegator is empty /// True if there are no entries in the variegator public bool IsEmpty { get { return (Count == 0); } } /// Returns the number of values in the variegator /// The number of values stored in the variegator /// /// If the same value is added with different keys (a situation that doesn't make /// sense because such reuse should be covered by specifying multiple keys in /// a query), it will be counted multiple times. /// public int Count { get { return ((System.Collections.ICollection)this.values).Count; } } /// /// Insert a new value that can be returned when requesting the specified key /// /// Key of the value that will be inserted /// Value that will be inserted under the provided key public void Add(TKey key, TValue value) { this.values.Add(key, value); } /// Retrieves a random value associated with the specified key /// For for which a value will be looked up /// A random value associated with the specified key public TValue Get(TKey key) { ISet candidates = new HashSet(); { ICollection valueRange = this.values[key]; // If possible access the values by index because it's faster and produces less // garbage, otherwise fall back to using an enumerator var indexableValueRange = valueRange as IList; if(indexableValueRange == null) { foreach(TValue value in valueRange) { candidates.Add(value); } } else { for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) { candidates.Add(indexableValueRange[valueIndex]); } } } TValue result = destructivePickCandidateValue(candidates); addRecentlyUsedValue(result); return result; } /// Retrieves a random value associated with one of the specified keys /// Keys that will be considered /// /// In many cases, you have generic situations (such as 'detected-player-stealing', /// 'observed-hostile-action') and specified situations (such as /// 'detected-player-stealing-from-beggar', 'observed-hostile-action-on-cop') /// where a values from both pools should be considered. This method allows you /// to specify any number of keys, creating a greater set of values the variegator /// can pick between. /// public TValue Get(params TKey[] keys) { ISet candidates = new HashSet(); for(int index = 0; index < keys.Length; ++index) { ICollection valueRange = this.values[keys[index]]; // If possible access the values by index because it's faster and produces less // garbage, otherwise fall back to using an enumerator var indexableValueRange = valueRange as IList; if(indexableValueRange == null) { foreach(TValue value in valueRange) { candidates.Add(value); } } else { for(int valueIndex = 0; valueIndex < indexableValueRange.Count; ++valueIndex) { candidates.Add(indexableValueRange[valueIndex]); } } } TValue result = destructivePickCandidateValue(candidates); addRecentlyUsedValue(result); return result; } /// Picks amongst the values in a set /// /// Set containing the candidats values to consider. Will be destroyed. /// /// The least recently used candidate value or a random one private TValue destructivePickCandidateValue(ISet candidates) { removeRecentlyUsedValues(candidates); switch(candidates.Count) { case 0: { throw new InvalidOperationException("No values mapped to this key"); } case 1: { using(IEnumerator enumerator = candidates.GetEnumerator()) { enumerator.MoveNext(); // We can be sure this one returns true return enumerator.Current; } } default: { int index = this.randomNumberGenerator.Next(candidates.Count); using(IEnumerator enumerator = candidates.GetEnumerator()) { do { --index; enumerator.MoveNext(); // We can be sure this one returns true } while(index >= 0); return enumerator.Current; } throw new InvalidOperationException( "ISet.Count was off or random number generator malfunctioned" ); } } } /// Adds a recently used value to the history /// Value that will be added to the history private void addRecentlyUsedValue(TValue value) { if(this.historyTailIndex == this.historyLength) { this.historyFull = true; this.history[0] = value; this.historyTailIndex = 1; } else { this.history[this.historyTailIndex] = value; ++this.historyTailIndex; } } /// Removes all values that are in the recent use list from a set /// Set from which recently used values are removed /// /// Stops removing values when there's only 1 value left in the set /// private void removeRecentlyUsedValues(ISet candidates) { if(candidates.Count <= 1) { return; } if(this.historyFull) { // History buffer has wrapped around int index = this.historyTailIndex; while(index > 0) { --index; if(candidates.Remove(this.history[index])) { if(candidates.Count <= 1) { return; } } } index = this.historyLength; while(index > this.historyTailIndex) { --index; if(candidates.Remove(this.history[index])) { if(candidates.Count <= 1) { return; } } } } else { // History buffer was not full yet int index = this.historyTailIndex; while(index > 0) { --index; if(candidates.Remove(this.history[index])) { if(candidates.Count <= 1) { return; } } } } } /// Frees all memory used by the individual history entries /// /// The history array itself is kept alive and the tail index + full flag will /// not be reset. /// private void freeHistory() { Array.Clear(this.history, 0, this.historyLength); } /// Stores the entries the variegator can select from by their keys private IMultiDictionary values; /// Random number generator that will be used to pick random values private Random randomNumberGenerator; /// Number of entries in the recently used list private int historyLength; /// Array containing the most recently provided values private TValue[] history; /// Index of the tail in the recently used value array private int historyTailIndex; /// Whether the recently used value history is at capacity private bool historyFull; } } // namespace Nuclex.Support.Collections #endif // !NO_SETS