From 2462dd6dc4d87f5af83af57b82317875d610bf60 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Thu, 27 Jun 2013 16:05:54 +0000 Subject: [PATCH] Added a C# port of my new Variegator collection, a MultiDictionary that resolves ambiguities by picking a random value or providing the least recently used one, very useful for returning varying responses git-svn-id: file:///srv/devel/repo-conversion/nusu@286 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (mono-3.5-unity).csproj | 31 +++ Nuclex.Support (net-4.0).csproj | 4 + Source/Collections/MultiDictionary.cs | 2 +- Source/Collections/Variegator.Test.cs | 84 ++++++++ Source/Collections/Variegator.cs | 283 +++++++++++++++++++++++++ Source/IO/PartialStream.cs | 15 +- 6 files changed, 410 insertions(+), 9 deletions(-) create mode 100644 Source/Collections/Variegator.Test.cs create mode 100644 Source/Collections/Variegator.cs diff --git a/Nuclex.Support (mono-3.5-unity).csproj b/Nuclex.Support (mono-3.5-unity).csproj index 1575366..12d2d99 100644 --- a/Nuclex.Support (mono-3.5-unity).csproj +++ b/Nuclex.Support (mono-3.5-unity).csproj @@ -38,6 +38,16 @@ + + + ExpressionTreeCloner.cs + + + ExpressionTreeCloner.cs + + + ExpressionTreeCloner.cs + @@ -101,6 +111,10 @@ ObservableList.cs + + + ObservableSet.cs + PairPriorityQueue.cs @@ -137,6 +151,10 @@ ReadOnlyList.cs + + + ReadOnlySet.cs + ReverseComparer.cs @@ -148,6 +166,10 @@ TransformingReadOnlyCollection.cs + + + TransformingReadOnlyCollection.cs + WeakCollection.cs @@ -185,6 +207,10 @@ CommandLine.cs + + + ParserHelper.cs + PropertyChangedEventArgsHelper.cs @@ -253,6 +279,11 @@ XmlHelper.cs + + + Foundation.snk + + diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj index 4fe8546..e01352e 100644 --- a/Nuclex.Support (net-4.0).csproj +++ b/Nuclex.Support (net-4.0).csproj @@ -186,6 +186,10 @@ TransformingReadOnlyCollection.cs + + + TransformingReadOnlyCollection.cs + WeakCollection.cs diff --git a/Source/Collections/MultiDictionary.cs b/Source/Collections/MultiDictionary.cs index 2b7d50f..b24a279 100644 --- a/Source/Collections/MultiDictionary.cs +++ b/Source/Collections/MultiDictionary.cs @@ -290,7 +290,7 @@ namespace Nuclex.Support.Collections { } } - /// Number of elements contained in the Dictionary + /// Number of elements contained in the multi dictionary public int Count { get { return this.count; } } diff --git a/Source/Collections/Variegator.Test.cs b/Source/Collections/Variegator.Test.cs new file mode 100644 index 0000000..7f0086e --- /dev/null +++ b/Source/Collections/Variegator.Test.cs @@ -0,0 +1,84 @@ +#region CPL License +/* +Nuclex 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 + +using System; +using System.Collections.Generic; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Support.Collections { + + /// Unit Test for the Variegator multi dictionary + [TestFixture] + public class VariegatorTest { + + /// + /// Tests whether the default constructor of the reverse comparer works + /// + [Test] + public void InstancesCanBeCreated() { + new Variegator(); + } + + /// + /// Verifies that querying for a missing value leads to an exception being thrown + /// + [Test] + public void QueryingMissingValueThrowsException() { + var variegator = new Variegator(); + Assert.Throws( + () => { + variegator.Get(123); + } + ); + } + + /// + /// Verifies that the variegator resolves ambiguous matches according to its design + /// + [Test] + public void AmbiguityResolvesToLeastRecentValue() { + var variegator = new Variegator(); + variegator.Add(1, "one"); + variegator.Add(1, "eins"); + + string first = variegator.Get(1); + string second = variegator.Get(1); + + // The variegator should have selected the first value by random and then + // returned the other value on the second query + Assert.AreNotEqual(first, second); + + // Now the variegator should return the first value again because it is + // the least recently used value + Assert.AreEqual(first, variegator.Get(1)); + + // Repeating the query, the second should be returned again because now + // it has become the least recently used value + Assert.AreEqual(second, variegator.Get(1)); + } + + } + +} // namespace Nuclex.Support.Collections + +#endif // UNITTEST diff --git a/Source/Collections/Variegator.cs b/Source/Collections/Variegator.cs new file mode 100644 index 0000000..4e03563 --- /dev/null +++ b/Source/Collections/Variegator.cs @@ -0,0 +1,283 @@ +#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; +using System.Text; + +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 diff --git a/Source/IO/PartialStream.cs b/Source/IO/PartialStream.cs index 7083f47..50724cf 100644 --- a/Source/IO/PartialStream.cs +++ b/Source/IO/PartialStream.cs @@ -43,18 +43,18 @@ namespace Nuclex.Support.IO { throw new ArgumentException("Start index must not be less than 0", "start"); } - if(!stream.CanSeek) { - if(start != 0) { - throw new ArgumentException( - "The only valid start for unseekable streams is 0", "start" - ); - } - } else { + if(stream.CanSeek) { if(start + length > stream.Length) { throw new ArgumentException( "Partial stream exceeds end of full stream", "length" ); } + } else { + if(start != 0) { + throw new ArgumentException( + "The only valid start for unseekable streams is 0", "start" + ); + } } this.stream = stream; @@ -258,7 +258,6 @@ namespace Nuclex.Support.IO { /// Length of the partial stream private long length; - } } // namespace Nuclex.Support.IO