Implement dynamic previous hitobject retention for Skill class

There is no reason we should be limiting skills to knowing only the previous 2 objects. This originally existed as an angle implementation detail of the original pp+ codebase which made its way here, but didn't get used in the same way.
This commit is contained in:
Samuel Cattini-Schultz 2021-02-06 15:48:14 +11:00
parent eb1e850f99
commit fe66b84bed
5 changed files with 284 additions and 209 deletions

View File

@ -1,115 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class LimitedCapacityStackTest
{
private const int capacity = 3;
private LimitedCapacityStack<int> stack;
[SetUp]
public void Setup()
{
stack = new LimitedCapacityStack<int>(capacity);
}
[Test]
public void TestEmptyStack()
{
Assert.AreEqual(0, stack.Count);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
int unused = stack[0];
});
int count = 0;
foreach (var unused in stack)
count++;
Assert.AreEqual(0, count);
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
public void TestInRangeElements(int count)
{
// e.g. 0 -> 1 -> 2
for (int i = 0; i < count; i++)
stack.Push(i);
Assert.AreEqual(count, stack.Count);
// e.g. 2 -> 1 -> 0 (reverse order)
for (int i = 0; i < stack.Count; i++)
Assert.AreEqual(count - 1 - i, stack[i]);
// e.g. indices 3, 4, 5, 6 (out of range)
for (int i = stack.Count; i < stack.Count + capacity; i++)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
int unused = stack[i];
});
}
}
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestOverflowElements(int count)
{
// e.g. 0 -> 1 -> 2 -> 3
for (int i = 0; i < count; i++)
stack.Push(i);
Assert.AreEqual(capacity, stack.Count);
// e.g. 3 -> 2 -> 1 (reverse order)
for (int i = 0; i < stack.Count; i++)
Assert.AreEqual(count - 1 - i, stack[i]);
// e.g. indices 3, 4, 5, 6 (out of range)
for (int i = stack.Count; i < stack.Count + capacity; i++)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
int unused = stack[i];
});
}
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(5)]
[TestCase(6)]
public void TestEnumerator(int count)
{
// e.g. 0 -> 1 -> 2 -> 3
for (int i = 0; i < count; i++)
stack.Push(i);
int enumeratorCount = 0;
int expectedValue = count - 1;
foreach (var item in stack)
{
Assert.AreEqual(expectedValue, item);
enumeratorCount++;
expectedValue--;
}
Assert.AreEqual(stack.Count, enumeratorCount);
}
}
}

View File

@ -0,0 +1,143 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Rulesets.Difficulty.Utils;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class ReverseQueueTest
{
private ReverseQueue<char> queue;
[SetUp]
public void Setup()
{
queue = new ReverseQueue<char>(4);
}
[Test]
public void TestEmptyQueue()
{
Assert.AreEqual(0, queue.Count);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
char unused = queue[0];
});
int count = 0;
foreach (var unused in queue)
count++;
Assert.AreEqual(0, count);
}
[Test]
public void TestEnqueue()
{
// Assert correct values and reverse index after enqueueing
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
Assert.AreEqual('c', queue[0]);
Assert.AreEqual('b', queue[1]);
Assert.AreEqual('a', queue[2]);
// Assert correct values and reverse index after enqueueing beyond initial capacity of 4
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
Assert.AreEqual('f', queue[0]);
Assert.AreEqual('e', queue[1]);
Assert.AreEqual('d', queue[2]);
Assert.AreEqual('c', queue[3]);
Assert.AreEqual('b', queue[4]);
Assert.AreEqual('a', queue[5]);
}
[Test]
public void TestDequeue()
{
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
// Assert correct item return and no longer in queue after dequeueing
Assert.AreEqual('a', queue[5]);
var dequeuedItem = queue.Dequeue();
Assert.AreEqual('a', dequeuedItem);
Assert.AreEqual(5, queue.Count);
Assert.AreEqual('f', queue[0]);
Assert.AreEqual('b', queue[4]);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
char unused = queue[5];
});
// Assert correct state after enough enqueues and dequeues to wrap around array (queue.start = 0 again)
queue.Enqueue('g');
queue.Enqueue('h');
queue.Enqueue('i');
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
queue.Dequeue();
Assert.AreEqual(1, queue.Count);
Assert.AreEqual('i', queue[0]);
}
[Test]
public void TestClear()
{
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
// Assert queue is empty after clearing
queue.Clear();
Assert.AreEqual(0, queue.Count);
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
char unused = queue[0];
});
}
[Test]
public void TestEnumerator()
{
queue.Enqueue('a');
queue.Enqueue('b');
queue.Enqueue('c');
queue.Enqueue('d');
queue.Enqueue('e');
queue.Enqueue('f');
char[] expectedValues = { 'f', 'e', 'd', 'c', 'b', 'a' };
int expectedValueIndex = 0;
// Assert items are enumerated in correct order
foreach (var item in queue)
{
Assert.AreEqual(expectedValues[expectedValueIndex], item);
expectedValueIndex++;
}
}
}
}

View File

@ -40,7 +40,14 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// <summary>
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
/// </summary>
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
protected readonly ReverseQueue<DifficultyHitObject> Previous;
/// <summary>
/// Soft capacity of the <see cref="Previous"/> queue.
/// <see cref="Previous"/> will automatically resize if it exceeds capacity, but will do so at a very slight performance impact.
/// The actual capacity will be set to this value + 1 to allow for storage of the current object before the next can be processed.
/// </summary>
protected virtual int PreviousCollectionSoftCapacity => 1;
/// <summary>
/// The current strain level.
@ -61,6 +68,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
protected Skill(Mod[] mods)
{
this.mods = mods;
Previous = new ReverseQueue<DifficultyHitObject>(PreviousCollectionSoftCapacity + 1);
}
/// <summary>
@ -68,12 +76,33 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// </summary>
public void Process(DifficultyHitObject current)
{
RemoveExtraneousHistory(current);
CurrentStrain *= strainDecay(current.DeltaTime);
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
Previous.Push(current);
AddToHistory(current);
}
/// <summary>
/// Remove objects from <see cref="Previous"/> that are no longer needed for calculations from the current object onwards.
/// </summary>
/// <param name="current">The <see cref="DifficultyHitObject"/> to be processed.</param>
protected virtual void RemoveExtraneousHistory(DifficultyHitObject current)
{
while (Previous.Count > 1)
Previous.Dequeue();
}
/// <summary>
/// Add the current <see cref="DifficultyHitObject"/> to the <see cref="Previous"/> queue (if required).
/// </summary>
/// <param name="current">The <see cref="DifficultyHitObject"/> that was just processed.</param>
protected virtual void AddToHistory(DifficultyHitObject current)
{
Previous.Enqueue(current);
}
/// <summary>

View File

@ -1,92 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Difficulty.Utils
{
/// <summary>
/// An indexed stack with limited depth. Indexing starts at the top of the stack.
/// </summary>
public class LimitedCapacityStack<T> : IEnumerable<T>
{
/// <summary>
/// The number of elements in the stack.
/// </summary>
public int Count { get; private set; }
private readonly T[] array;
private readonly int capacity;
private int marker; // Marks the position of the most recently added item.
/// <summary>
/// Constructs a new <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="capacity">The number of items the stack can hold.</param>
public LimitedCapacityStack(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
this.capacity = capacity;
array = new T[capacity];
marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
}
/// <summary>
/// Retrieves the item at an index in the stack.
/// </summary>
/// <param name="i">The index of the item to retrieve. The top of the stack is returned at index 0.</param>
public T this[int i]
{
get
{
if (i < 0 || i > Count - 1)
throw new ArgumentOutOfRangeException(nameof(i));
i += marker;
if (i > capacity - 1)
i -= capacity;
return array[i];
}
}
/// <summary>
/// Pushes an item to this <see cref="LimitedCapacityStack{T}"/>.
/// </summary>
/// <param name="item">The item to push.</param>
public void Push(T item)
{
// Overwrite the oldest item instead of shifting every item by one with every addition.
if (marker == 0)
marker = capacity - 1;
else
--marker;
array[marker] = item;
if (Count < capacity)
++Count;
}
/// <summary>
/// Returns an enumerator which enumerates items in the history starting from the most recently added one.
/// </summary>
public IEnumerator<T> GetEnumerator()
{
for (int i = marker; i < capacity; ++i)
yield return array[i];
if (Count == capacity)
{
for (int i = 0; i < marker; ++i)
yield return array[i];
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@ -0,0 +1,110 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Difficulty.Utils
{
/// <summary>
/// An indexed queue where items are indexed beginning from the end instead of the start.
/// </summary>
public class ReverseQueue<T> : IEnumerable<T>
{
/// <summary>
/// The number of elements in the <see cref="ReverseQueue{T}"/>.
/// </summary>
public int Count { get; private set; }
private T[] items;
private int capacity;
private int start;
public ReverseQueue(int initialCapacity)
{
if (initialCapacity <= 0)
throw new ArgumentOutOfRangeException(nameof(initialCapacity));
items = new T[initialCapacity];
capacity = initialCapacity;
start = 0;
Count = 0;
}
/// <summary>
/// Retrieves the item at an index in the <see cref="ReverseQueue{T}"/>.
/// </summary>
/// <param name="index">The index of the item to retrieve. The most recently enqueued item is at index 0.</param>
public T this[int index]
{
get
{
if (index < 0 || index > Count - 1)
throw new ArgumentOutOfRangeException(nameof(index));
int reverseIndex = Count - 1 - index;
return items[(start + reverseIndex) % capacity];
}
}
/// <summary>
/// Enqueues an item to this <see cref="ReverseQueue{T}"/>.
/// </summary>
/// <param name="item">The item to enqueue.</param>
public void Enqueue(T item)
{
if (Count == capacity)
{
// Double the buffer size
var buffer = new T[capacity * 2];
// Copy items to new queue
for (int i = 0; i < Count; i++)
{
buffer[i] = items[(start + i) % capacity];
}
// Replace array with new buffer
items = buffer;
capacity *= 2;
start = 0;
}
items[(start + Count) % capacity] = item;
Count++;
}
/// <summary>
/// Dequeues an item from the <see cref="ReverseQueue{T}"/> and returns it.
/// </summary>
/// <returns>The item dequeued from the <see cref="ReverseQueue{T}"/>.</returns>
public T Dequeue()
{
var item = items[start];
start = (start + 1) % capacity;
Count--;
return item;
}
/// <summary>
/// Clears the <see cref="ReverseQueue{T}"/> of all items.
/// </summary>
public void Clear()
{
start = 0;
Count = 0;
}
/// <summary>
/// Returns an enumerator which enumerates items in the <see cref="ReverseQueue{T}"/> starting from the most recently enqueued one.
/// </summary>
public IEnumerator<T> GetEnumerator()
{
for (int i = Count - 1; i >= 0; i--)
yield return items[(start + i) % capacity];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}