From 6c759f31f175f784f447f1ef0d21e20460429ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:13:18 +0200 Subject: [PATCH] Add and use limited capacity queue --- .../Preprocessing/StaminaCheeseDetector.cs | 10 +- .../Difficulty/Skills/Colour.cs | 9 +- .../Difficulty/Skills/Rhythm.cs | 9 +- .../Difficulty/Skills/Stamina.cs | 9 +- .../NonVisual/LimitedCapacityQueueTest.cs | 98 +++++++++++++++ .../Difficulty/Utils/LimitedCapacityQueue.cs | 114 ++++++++++++++++++ 6 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 29e631e515..c6317ff195 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing @@ -27,16 +28,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing private void findRolls(int patternLength) { - List history = new List(); + var history = new LimitedCapacityQueue(2 * patternLength); int repetitionStart = 0; for (int i = 0; i < hitObjects.Count; i++) { - history.Add(hitObjects[i]); - if (history.Count < 2 * patternLength) continue; - - if (history.Count > 2 * patternLength) history.RemoveAt(0); + history.Enqueue(hitObjects[i]); + if (!history.Full) + continue; bool isRepeat = true; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index e93893d894..e9e0930a9a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// List of the last most recent mono patterns, with the most recent at the end of the list. /// - private readonly List monoHistory = new List(); + private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); protected override double StrainValueOf(DifficultyHitObject current) { @@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills const int l = 2; double penalty = 1.0; - monoHistory.Add(currentMonoLength); - - if (monoHistory.Count > mono_history_max_length) - monoHistory.RemoveAt(0); + monoHistory.Enqueue(currentMonoLength); for (int start = monoHistory.Count - l - 1; start >= 0; start--) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 31dc93a6b2..caf1acccf4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private const double strain_decay = 0.96; private double currentStrain; - private readonly List rhythmHistory = new List(); + private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); private const int rhythm_history_max_length = 8; private int notesSinceRhythmChange; @@ -32,10 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { double penalty = 1; - rhythmHistory.Add(hitobject); - - if (rhythmHistory.Count > rhythm_history_max_length) - rhythmHistory.RemoveAt(0); + rhythmHistory.Enqueue(hitobject); for (int l = 2; l <= rhythm_history_max_length / 2; l++) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index c9a691a2aa..430a553113 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainDecayBase => 0.4; private const int max_history_length = 2; - private readonly List notePairDurationHistory = new List(); + private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); private double offhandObjectDuration = double.MaxValue; @@ -56,10 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (hitObject.ObjectIndex == 1) return 1; - notePairDurationHistory.Add(hitObject.DeltaTime + offhandObjectDuration); - - if (notePairDurationHistory.Count > max_history_length) - notePairDurationHistory.RemoveAt(0); + notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration); double shortestRecentNote = notePairDurationHistory.Min(); objectStrain += speedBonus(shortestRecentNote); diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs new file mode 100644 index 0000000000..52463dd7eb --- /dev/null +++ b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . 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 LimitedCapacityQueueTest + { + private const int capacity = 3; + + private LimitedCapacityQueue queue; + + [SetUp] + public void SetUp() + { + queue = new LimitedCapacityQueue(capacity); + } + + [Test] + public void TestEmptyQueue() + { + Assert.AreEqual(0, queue.Count); + + Assert.Throws(() => _ = queue[0]); + + Assert.Throws(() => _ = queue.Dequeue()); + + int count = 0; + foreach (var _ in queue) + count++; + + Assert.AreEqual(0, count); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void TestBelowCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + Assert.AreEqual(count, queue.Count); + + for (int i = 0; i < count; ++i) + Assert.AreEqual(i, queue[i]); + + int j = 0; + foreach (var item in queue) + Assert.AreEqual(j++, item); + + for (int i = queue.Count; i < queue.Count + capacity; i++) + Assert.Throws(() => _ = queue[i]); + } + + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void TestEnqueueAtFullCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + Assert.AreEqual(capacity, queue.Count); + + for (int i = 0; i < queue.Count; ++i) + Assert.AreEqual(count - capacity + i, queue[i]); + + int j = count - capacity; + foreach (var item in queue) + Assert.AreEqual(j++, item); + + for (int i = queue.Count; i < queue.Count + capacity; i++) + Assert.Throws(() => _ = queue[i]); + } + + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void TestDequeueAtFullCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + for (int i = 0; i < capacity; ++i) + { + Assert.AreEqual(count - capacity + i, queue.Dequeue()); + Assert.AreEqual(2 - i, queue.Count); + } + + Assert.Throws(() => queue.Dequeue()); + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs new file mode 100644 index 0000000000..0f014e8a8c --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . 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 +{ + /// + /// An indexed queue with limited capacity. + /// Respects first-in-first-out insertion order. + /// + public class LimitedCapacityQueue : IEnumerable + { + /// + /// The number of elements in the queue. + /// + public int Count { get; private set; } + + /// + /// Whether the queue is full (adding any new items will cause removing existing ones). + /// + public bool Full => Count == capacity; + + private readonly T[] array; + private readonly int capacity; + + // Markers tracking the queue's first and last element. + private int start, end; + + /// + /// Constructs a new + /// + /// The number of items the queue can hold. + public LimitedCapacityQueue(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity)); + + this.capacity = capacity; + array = new T[capacity]; + start = 0; + end = -1; + } + + /// + /// Removes an item from the front of the . + /// + /// The item removed from the front of the queue. + public T Dequeue() + { + if (Count == 0) + throw new InvalidOperationException("Queue is empty."); + + var result = array[start]; + start = (start + 1) % capacity; + Count--; + return result; + } + + /// + /// Adds an item to the back of the . + /// If the queue is holding elements at the point of addition, + /// the item at the front of the queue will be removed. + /// + /// The item to be added to the back of the queue. + public void Enqueue(T item) + { + end = (end + 1) % capacity; + if (Count == capacity) + start = (start + 1) % capacity; + else + Count++; + array[end] = item; + } + + /// + /// Retrieves the item at the given index in the queue. + /// + /// + /// The index of the item to retrieve. + /// The item with index 0 is at the front of the queue + /// (it was added the earliest). + /// + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return array[(start + index) % capacity]; + } + } + + /// + /// Enumerates the queue from its start to its end. + /// + public IEnumerator GetEnumerator() + { + if (Count == 0) + yield break; + + for (int i = 0; i < Count; i++) + yield return array[(start + i) % capacity]; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +}