diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index ad6f01881b..103831822c 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -22,7 +22,8 @@ public void TestDistributedHits() var unstableRate = new UnstableRate(events); - Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value, 10 * Math.Sqrt(10))); + Assert.IsNotNull(unstableRate.Value); + Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); } [Test] diff --git a/osu.Game/Rulesets/Scoring/HitEventExtensions.cs b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs new file mode 100644 index 0000000000..f645b12483 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/HitEventExtensions.cs @@ -0,0 +1,37 @@ +// 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.Generic; +using System.Linq; + +namespace osu.Game.Rulesets.Scoring +{ + public static class HitEventExtensions + { + /// + /// Calculates the "unstable rate" for a sequence of s. + /// + /// + /// A non-null value if unstable rate could be calculated, + /// and if unstable rate cannot be calculated due to being empty. + /// + public static double? CalculateUnstableRate(this IEnumerable hitEvents) + { + double[] timeOffsets = hitEvents.Where(affectsUnstableRate).Select(ev => ev.TimeOffset).ToArray(); + return 10 * standardDeviation(timeOffsets); + } + + private static bool affectsUnstableRate(HitEvent e) => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit(); + + private static double? standardDeviation(double[] timeOffsets) + { + if (timeOffsets.Length == 0) + return null; + + double mean = timeOffsets.Average(); + double squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); + return Math.Sqrt(squares / timeOffsets.Length); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs index 89ae4f8a21..4dc154fb9d 100644 --- a/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs +++ b/osu.Game/Screens/Play/HUD/UnstableRateCounter.cs @@ -1,9 +1,6 @@ // 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.Generic; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -29,8 +26,6 @@ public class UnstableRateCounter : RollingCounter, ISkinnableDrawable private const float alpha_when_invalid = 0.3f; private readonly Bindable valid = new Bindable(); - private readonly List hitOffsets = new List(); - [Resolved] private ScoreProcessor scoreProcessor { get; set; } @@ -54,41 +49,20 @@ protected override void LoadComplete() { base.LoadComplete(); - scoreProcessor.NewJudgement += onJudgementAdded; - scoreProcessor.JudgementReverted += onJudgementReverted; - } - - private void onJudgementAdded(JudgementResult judgement) - { - if (!changesUnstableRate(judgement)) return; - - hitOffsets.Add(judgement.TimeOffset); + scoreProcessor.NewJudgement += updateDisplay; + scoreProcessor.JudgementReverted += updateDisplay; updateDisplay(); } - private void onJudgementReverted(JudgementResult judgement) - { - if (judgement.FailedAtJudgement || !changesUnstableRate(judgement)) return; - - hitOffsets.RemoveAt(hitOffsets.Count - 1); - updateDisplay(); - } + private void updateDisplay(JudgementResult _) => Scheduler.AddOnce(updateDisplay); private void updateDisplay() { - // At Count = 0, we get NaN, While we are allowing count = 1, it will be 0 since average = offset. - if (hitOffsets.Count > 0) - { - double mean = hitOffsets.Average(); - double squares = hitOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); - Current.Value = (int)(Math.Sqrt(squares / hitOffsets.Count) * 10); - valid.Value = true; - } - else - { - Current.Value = 0; - valid.Value = false; - } + double? unstableRate = scoreProcessor.HitEvents.CalculateUnstableRate(); + + valid.Value = unstableRate != null; + if (unstableRate != null) + Current.Value = (int)unstableRate.Value; } protected override IHasText CreateText() => new TextComponent @@ -102,8 +76,8 @@ protected override void Dispose(bool isDisposing) if (scoreProcessor == null) return; - scoreProcessor.NewJudgement -= onJudgementAdded; - scoreProcessor.JudgementReverted -= onJudgementReverted; + scoreProcessor.NewJudgement -= updateDisplay; + scoreProcessor.JudgementReverted -= updateDisplay; } private class TextComponent : CompositeDrawable, IHasText diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index cd2b292547..0d23490f40 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -1,9 +1,7 @@ // 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.Generic; -using System.Linq; using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Ranking.Statistics @@ -11,7 +9,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Displays the unstable rate statistic for a given play. /// - public class UnstableRate : SimpleStatisticItem + public class UnstableRate : SimpleStatisticItem { /// /// Creates and computes an statistic. @@ -20,21 +18,9 @@ public class UnstableRate : SimpleStatisticItem public UnstableRate(IEnumerable hitEvents) : base("Unstable Rate") { - double[] timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()) - .Select(ev => ev.TimeOffset).ToArray(); - Value = 10 * standardDeviation(timeOffsets); + Value = hitEvents.CalculateUnstableRate(); } - private static double standardDeviation(double[] timeOffsets) - { - if (timeOffsets.Length == 0) - return double.NaN; - - double mean = timeOffsets.Average(); - double squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); - return Math.Sqrt(squares / timeOffsets.Length); - } - - protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2"); + protected override string DisplayValue(double? value) => value == null ? "(not available)" : value.Value.ToString(@"N2"); } }