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:
parent
4b94bdd1a5
commit
2462dd6dc4
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
84
Source/Collections/Variegator.Test.cs
Normal file
84
Source/Collections/Variegator.Test.cs
Normal 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
|
283
Source/Collections/Variegator.cs
Normal file
283
Source/Collections/Variegator.cs
Normal 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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user