#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