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
This commit is contained in:
Markus Ewald 2013-06-27 16:05:54 +00:00
parent 4b94bdd1a5
commit 2462dd6dc4
6 changed files with 410 additions and 9 deletions

View File

@ -38,6 +38,16 @@
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Source\Cloning\CloneFactoryTest.cs" />
<Compile Include="Source\Cloning\ExpressionTreeCloner.cs" />
<Compile Include="Source\Cloning\ExpressionTreeCloner.FieldBased.cs">
<DependentUpon>ExpressionTreeCloner.cs</DependentUpon>
</Compile>
<Compile Include="Source\Cloning\ExpressionTreeCloner.PropertyBased.cs">
<DependentUpon>ExpressionTreeCloner.cs</DependentUpon>
</Compile>
<Compile Include="Source\Cloning\ExpressionTreeCloner.Test.cs">
<DependentUpon>ExpressionTreeCloner.cs</DependentUpon>
</Compile>
<Compile Include="Source\Cloning\IStateCopier.cs" />
<Compile Include="Source\Cloning\ReflectionCloner.cs" />
<Compile Include="Source\Cloning\ReflectionCloner.Test.cs">
@ -101,6 +111,10 @@
<Compile Include="Source\Collections\ObservableList.Test.cs">
<DependentUpon>ObservableList.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\ObservableSet.cs" />
<Compile Include="Source\Collections\ObservableSet.Test.cs">
<DependentUpon>ObservableSet.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\PairPriorityQueue.cs" />
<Compile Include="Source\Collections\PairPriorityQueue.Test.cs">
<DependentUpon>PairPriorityQueue.cs</DependentUpon>
@ -137,6 +151,10 @@
<Compile Include="Source\Collections\ReadOnlyList.Test.cs">
<DependentUpon>ReadOnlyList.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\ReadOnlySet.cs" />
<Compile Include="Source\Collections\ReadOnlySet.Test.cs">
<DependentUpon>ReadOnlySet.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\ReverseComparer.cs" />
<Compile Include="Source\Collections\ReverseComparer.Test.cs">
<DependentUpon>ReverseComparer.cs</DependentUpon>
@ -148,6 +166,10 @@
<Compile Include="Source\Collections\TransformingReadOnlyCollection.Test.cs">
<DependentUpon>TransformingReadOnlyCollection.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\Variegator.cs" />
<Compile Include="Source\Collections\Variegator.Test.cs">
<DependentUpon>TransformingReadOnlyCollection.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\WeakCollection.cs" />
<Compile Include="Source\Collections\WeakCollection.Interfaces.cs">
<DependentUpon>WeakCollection.cs</DependentUpon>
@ -185,6 +207,10 @@
<Compile Include="Source\Parsing\CommandLine.Parser.cs">
<DependentUpon>CommandLine.cs</DependentUpon>
</Compile>
<Compile Include="Source\Parsing\ParserHelper.cs" />
<Compile Include="Source\Parsing\ParserHelper.Test.cs">
<DependentUpon>ParserHelper.cs</DependentUpon>
</Compile>
<Compile Include="Source\PropertyChangedEventArgsHelper.cs" />
<Compile Include="Source\PropertyChangedEventArgsHelper.Test.cs">
<DependentUpon>PropertyChangedEventArgsHelper.cs</DependentUpon>
@ -253,6 +279,11 @@
<DependentUpon>XmlHelper.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Include="..\Foundation.snk">
<Link>Foundation.snk</Link>
</None>
</ItemGroup>
<ItemGroup>
<Content Include="Documents\CommandLine.txt" />
<Content Include="Documents\DoubleConverter.txt" />

View File

@ -186,6 +186,10 @@
<Compile Include="Source\Collections\TransformingReadOnlyCollection.Test.cs">
<DependentUpon>TransformingReadOnlyCollection.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\Variegator.cs" />
<Compile Include="Source\Collections\Variegator.Test.cs">
<DependentUpon>TransformingReadOnlyCollection.cs</DependentUpon>
</Compile>
<Compile Include="Source\Collections\WeakCollection.cs" />
<Compile Include="Source\Collections\WeakCollection.Interfaces.cs">
<DependentUpon>WeakCollection.cs</DependentUpon>

View File

@ -290,7 +290,7 @@ namespace Nuclex.Support.Collections {
}
}
/// <summary>Number of elements contained in the Dictionary</summary>
/// <summary>Number of elements contained in the multi dictionary</summary>
public int Count {
get { return this.count; }
}

View File

@ -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 {
/// <summary>Unit Test for the Variegator multi dictionary</summary>
[TestFixture]
public class VariegatorTest {
/// <summary>
/// Tests whether the default constructor of the reverse comparer works
/// </summary>
[Test]
public void InstancesCanBeCreated() {
new Variegator<int, string>();
}
/// <summary>
/// Verifies that querying for a missing value leads to an exception being thrown
/// </summary>
[Test]
public void QueryingMissingValueThrowsException() {
var variegator = new Variegator<int, string>();
Assert.Throws<KeyNotFoundException>(
() => {
variegator.Get(123);
}
);
}
/// <summary>
/// Verifies that the variegator resolves ambiguous matches according to its design
/// </summary>
[Test]
public void AmbiguityResolvesToLeastRecentValue() {
var variegator = new Variegator<int, string>();
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

View File

@ -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 {
// ------------------------------------------------------------------------------------------- //
/// <summary>Randomly selects between different options, trying to avoid repetition</summary>
/// <typeparam name="TKey">Type of keys through which values can be looked up</typeparam>
/// <typeparam name="TValue">Type of values provided by the variegator</typeparam>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// A typical usage would be to set up a mapping between situations and dialogue lines.
/// Upon calling <see cref="Get(TKey)" /> 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.
/// </para>
/// </remarks>
public class Variegator<TKey, TValue> {
/// <summary>Initializes a new variegator</summary>
/// <param name="historyLength">
/// How far into the past the variegator will look to avoid repetition
/// </param>
public Variegator(int historyLength = 64) {
this.historyLength = historyLength;
this.history = new TValue[historyLength];
this.values = new MultiDictionary<TKey, TValue>();
this.randomNumberGenerator = new Random();
}
/// <summary>Removes all entries from the variegator</summary>
/// <remarks>
/// 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.
/// </remarks>
public void Clear() {
freeHistory();
this.historyFull = false;
this.historyTailIndex = 0;
}
/// <summary>Checks whether the variegator is empty</summary>
/// <returns>True if there are no entries in the variegator</returns>
public bool IsEmpty {
get { return (Count == 0); }
}
/// <summary>Returns the number of values in the variegator</summary>
/// <returns>The number of values stored in the variegator</returns>
/// <remarks>
/// 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.
/// </remarks>
public int Count {
get { return ((System.Collections.ICollection)this.values).Count; }
}
/// <summary>
/// Insert a new value that can be returned when requesting the specified key
/// </summary>
/// <param name="key">Key of the value that will be inserted</param>
/// <param name="value">Value that will be inserted under the provided key</param>
public void Add(TKey key, TValue value) {
this.values.Add(key, value);
}
/// <summary>Retrieves a random value associated with the specified key</summary>
/// <param name="key">For for which a value will be looked up</param>
/// <returns>A random value associated with the specified key</returns>
public TValue Get(TKey key) {
ISet<TValue> candidates = new HashSet<TValue>();
{
ICollection<TValue> 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<TValue>;
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;
}
/// <summary>Retrieves a random value associated with one of the specified keys</summary>
/// <param name="keys">Keys that will be considered</param>
/// <remarks>
/// 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.
/// </remarks>
public TValue Get(params TKey[] keys) {
ISet<TValue> candidates = new HashSet<TValue>();
for(int index = 0; index < keys.Length; ++index) {
ICollection<TValue> 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<TValue>;
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;
}
/// <summary>Picks amongst the values in a set</summary>
/// <param name="candidates">
/// Set containing the candidats values to consider. Will be destroyed.
/// </param>
/// <returns>The least recently used candidate value or a random one</returns>
private TValue destructivePickCandidateValue(ISet<TValue> candidates) {
removeRecentlyUsedValues(candidates);
switch(candidates.Count) {
case 0: {
throw new InvalidOperationException("No values mapped to this key");
}
case 1: {
using(IEnumerator<TValue> 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<TValue> 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"
);
}
}
}
/// <summary>Adds a recently used value to the history</summary>
/// <param name="value">Value that will be added to the history</param>
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;
}
}
/// <summary>Removes all values that are in the recent use list from a set</summary>
/// <param name="candidates">Set from which recently used values are removed</param>
/// <remarks>
/// Stops removing values when there's only 1 value left in the set
/// </remarks>
private void removeRecentlyUsedValues(ISet<TValue> 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;
}
}
}
}
}
/// <summary>Frees all memory used by the individual history entries</summary>
/// <remarks>
/// The history array itself is kept alive and the tail index + full flag will
/// not be reset.
/// </remarks>
private void freeHistory() {
Array.Clear(this.history, 0, this.historyLength);
}
/// <summary>Stores the entries the variegator can select from by their keys</summary>
private IMultiDictionary<TKey, TValue> values;
/// <summary>Random number generator that will be used to pick random values</summary>
private Random randomNumberGenerator;
/// <summary>Number of entries in the recently used list</summary>
private int historyLength;
/// <summary>Array containing the most recently provided values</summary>
private TValue[] history;
/// <summary>Index of the tail in the recently used value array</summary>
private int historyTailIndex;
/// <summary>Whether the recently used value history is at capacity</summary>
private bool historyFull;
}
} // namespace Nuclex { namespace Support { namespace Collections

View File

@ -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 {
/// <summary>Length of the partial stream</summary>
private long length;
}
} // namespace Nuclex.Support.IO