#region CPL License /* Nuclex Native Framework Copyright (C) 2002-2013 Nuclex Development Labs This library is free software; you can redistribute it and/or modify it under the terms of the IBM Common Public License as published by the IBM Corporation; either version 1.0 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the IBM Common Public License for more details. You should have received a copy of the IBM Common Public License along with this library */ #endregion // CPL License using System; using System.Collections.Generic; 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 a values by their keys. Unlike a multimap, it will /// avoid handing out a previously provided value again. /// /// /// 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 /// /// How far into the past the variegator will look to avoid repetition /// public Variegator(int historyLength = 64) { 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 { namespace Support { namespace Collections