From 1269e53c13b5244448c1734c79867b8c12c36cf3 Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Sat, 2 Oct 2021 17:41:22 +0100 Subject: [PATCH 1/5] Refactor skills to use composition rater than inheritance for easier changes --- .../Difficulty/Skills/Movement.cs | 4 +- .../Difficulty/Skills/Strain.cs | 19 ++-- .../Preprocessing/OsuDifficultyHitObject.cs | 3 + .../Difficulty/Skills/Aim.cs | 4 +- .../Difficulty/Skills/Flashlight.cs | 3 +- .../Difficulty/Skills/OsuStrainSkill.cs | 8 +- .../Difficulty/Skills/Speed.cs | 3 +- .../Difficulty/Skills/Colour.cs | 3 +- .../Difficulty/Skills/Rhythm.cs | 20 ++-- .../Difficulty/Skills/Stamina.cs | 3 +- .../Difficulty/Skills/StrainDecaySkill.cs | 44 ++++----- .../Rulesets/Difficulty/Skills/StrainSkill.cs | 92 +++---------------- .../Utils/DecayingStrainSections.cs | 27 ++++++ .../Difficulty/Utils/DecayingValue.cs | 41 +++++++++ .../Difficulty/Utils/StrainSections.cs | 87 ++++++++++++++++++ 15 files changed, 227 insertions(+), 134 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index cfb3fe40bee7..c9989543d61b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -16,11 +16,9 @@ public class Movement : StrainDecaySkill private const double direction_change_bonus = 21.0; protected override double SkillMultiplier => 900; - protected override double StrainDecayBase => 0.2; protected override double DecayWeight => 0.94; - protected override int SectionLength => 750; protected readonly float HalfCatcherWidth; @@ -34,7 +32,7 @@ public class Movement : StrainDecaySkill private readonly double catcherSpeedMultiplier; public Movement(Mod[] mods, float halfCatcherWidth, double clockRate) - : base(mods) + : base(mods, strainDecayBase: 0.2, sectionLength: 750) { HalfCatcherWidth = halfCatcherWidth; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 01d930d5857d..fa4707d5614d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -5,19 +5,20 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; + using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Difficulty.Skills { - public class Strain : StrainDecaySkill + public class Strain : StrainSkill { + protected override double SkillMultiplier => 1; + private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 1; - private readonly double[] holdEndTimes; private readonly double[] individualStrains; @@ -32,7 +33,7 @@ public Strain(Mod[] mods, int totalColumns) overallStrain = 1; } - protected override double StrainValueOf(DifficultyHitObject current) + protected override double StrainValueAt(DifficultyHitObject current) { var maniaCurrent = (ManiaDifficultyHitObject)current; var endTime = maniaCurrent.EndTime; @@ -68,12 +69,12 @@ protected override double StrainValueOf(DifficultyHitObject current) overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor; - return individualStrain + overallStrain - CurrentStrain; + return individualStrain + overallStrain; } - protected override double CalculateInitialStrain(double offset) - => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base) - + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base); + protected override double StrainAtTime(double time) + => applyDecay(individualStrain, time - Previous[0].StartTime, individual_decay_base) + + applyDecay(overallStrain, time - Previous[0].StartTime, overall_decay_base); private double applyDecay(double value, double deltaTime, double decayBase) => value * Math.Pow(decayBase, deltaTime / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 8e8f9bc06eb4..f6362049606c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -40,6 +41,8 @@ public class OsuDifficultyHitObject : DifficultyHitObject private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastObject; + + public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, HitObject lastObject, double clockRate) : base(hitObject, lastObject, clockRate) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 16a18cbcb90c..84ebc56a3a40 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -18,12 +18,12 @@ public class Aim : OsuStrainSkill private const double timing_threshold = 107; public Aim(Mod[] mods) - : base(mods) + : base(mods: mods, strainDecayBase: 0.15) { } protected override double SkillMultiplier => 26.25; - protected override double StrainDecayBase => 0.15; + protected override double StrainValueOf(DifficultyHitObject current) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index abd900a80d53..cece5b724364 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -15,12 +15,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public class Flashlight : OsuStrainSkill { public Flashlight(Mod[] mods) - : base(mods) + : base(mods, strainDecayBase: 0.15) { } protected override double SkillMultiplier => 0.15; - protected override double StrainDecayBase => 0.15; protected override double DecayWeight => 1.0; protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 7bcd867a9c28..cd418568d634 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; + using osu.Game.Rulesets.Mods; using System.Linq; using osu.Framework.Utils; @@ -28,8 +30,10 @@ public abstract class OsuStrainSkill : StrainDecaySkill /// protected virtual double DifficultyMultiplier => 1.06; - protected OsuStrainSkill(Mod[] mods) - : base(mods) + + + protected OsuStrainSkill(Mod[] mods, double strainDecayBase, int sectionLength = 400) + : base(mods, strainDecayBase, sectionLength) { } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 9364b110484b..e9f28135b36a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -22,7 +22,6 @@ public class Speed : OsuStrainSkill private const double pi_over_2 = Math.PI / 2; protected override double SkillMultiplier => 1400; - protected override double StrainDecayBase => 0.3; protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; @@ -32,7 +31,7 @@ public class Speed : OsuStrainSkill private readonly double greatWindow; public Speed(Mod[] mods, double hitWindowGreat) - : base(mods) + : base(mods, strainDecayBase: 0.3) { greatWindow = hitWindowGreat; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 0c17ca66b977..be6401dc40d9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public class Colour : StrainDecaySkill { protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.4; /// /// Maximum number of entries to keep in . @@ -41,7 +40,7 @@ public class Colour : StrainDecaySkill private int currentMonoLength; public Colour(Mod[] mods) - : base(mods) + : base(mods, strainDecayBase: 0.4) { } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 973e55f4b489..6e7045a726c7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -2,6 +2,7 @@ // 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; @@ -14,17 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Calculates the rhythm coefficient of taiko difficulty. /// - public class Rhythm : StrainDecaySkill + public class Rhythm : StrainSkill { protected override double SkillMultiplier => 10; - protected override double StrainDecayBase => 0; /// /// The note-based decay for rhythm strain. /// - /// - /// is not used here, as it's time- and not note-based. - /// private const double strain_decay = 0.96; /// @@ -48,12 +45,23 @@ public class Rhythm : StrainDecaySkill /// private int notesSinceRhythmChange; + public Rhythm(Mod[] mods) : base(mods) { } - protected override double StrainValueOf(DifficultyHitObject current) + public override double DifficultyValue() + { + return sections.ExponentialWeightedSum(); + } + + protected override double StrainAtTime(double time) + { + return 0; + } + + protected override double StrainValueAt(DifficultyHitObject current) { // drum rolls and swells are exempt. if (!(current.BaseObject is Hit)) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 54cf233d69bb..9648d93cee52 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public class Stamina : StrainDecaySkill { protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.4; /// /// Maximum number of entries to keep in . @@ -52,7 +51,7 @@ public class Stamina : StrainDecaySkill /// Mods for use in skill calculations. /// Whether this instance is performing calculations for the right hand. public Stamina(Mod[] mods, bool rightHand) - : base(mods) + : base(mods, strainDecayBase: 0.4) { hand = rightHand ? 1 : 0; } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 73bab31e8278..5295aa8d42ab 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -2,7 +2,10 @@ // 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.Utils; + using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty.Skills @@ -11,44 +14,33 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// Used to processes strain values of s, keep track of strain levels caused by the processed objects /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. /// - public abstract class StrainDecaySkill : StrainSkill + public abstract class StrainDecaySkill : Skill { - /// - /// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other. - /// - protected abstract double SkillMultiplier { get; } + protected virtual double DecayWeight => 0.9; - /// - /// Determines how quickly strain decays for the given skill. - /// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second. - /// - protected abstract double StrainDecayBase { get; } + protected abstract double SkillMultiplier { get; } - /// - /// The current strain level. - /// - protected double CurrentStrain { get; private set; } = 1; + protected readonly DecayingStrainSections strain; - protected StrainDecaySkill(Mod[] mods) + protected StrainDecaySkill(Mod[] mods, double strainDecayBase, int sectionLength=400) : base(mods) { + strain = new DecayingStrainSections(strainDecayBase, sectionLength); } - protected override double CalculateInitialStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime); - - protected override double StrainValueAt(DifficultyHitObject current) + protected override void Process(DifficultyHitObject hitObject) { - CurrentStrain *= strainDecay(current.DeltaTime); - CurrentStrain += StrainValueOf(current) * SkillMultiplier; + strain.AddStrain(hitObject.StartTime, SkillMultiplier*StrainValueOf(hitObject)); + } + + protected abstract double StrainValueOf(DifficultyHitObject hitObject); - return CurrentStrain; + public override double DifficultyValue() + { + return strain.sections.ExponentialWeightedSum(DecayWeight); } - /// - /// Calculates the strain value of a . This value is affected by previously processed objects. - /// - protected abstract double StrainValueOf(DifficultyHitObject current); + public IEnumerable GetCurrentStrainPeaks() => strain.sections.GetCurrentStrainPeaks(); - private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 0880f1b08e97..4f947660b726 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty.Skills @@ -15,100 +16,35 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public abstract class StrainSkill : Skill { - /// - /// The weight by which each strain value decays. - /// protected virtual double DecayWeight => 0.9; - /// - /// The length of each strain section. - /// - protected virtual int SectionLength => 400; + protected abstract double SkillMultiplier { get; } - private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. + protected readonly StrainSections sections; - private double currentSectionEnd; - - private readonly List strainPeaks = new List(); - - protected StrainSkill(Mod[] mods) + protected StrainSkill(Mod[] mods, int sectionLength = 400) : base(mods) { + sections = new StrainSections(StrainAtTime, sectionLength); } - /// - /// Returns the strain value at . This value is calculated with or without respect to previous objects. - /// - protected abstract double StrainValueAt(DifficultyHitObject current); - - /// - /// Process a and update current strain values accordingly. - /// - protected sealed override void Process(DifficultyHitObject current) - { - // The first object doesn't generate a strain, so we begin with an incremented section end - if (Previous.Count == 0) - currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength; - - while (current.StartTime > currentSectionEnd) - { - saveCurrentPeak(); - startNewSectionFrom(currentSectionEnd); - currentSectionEnd += SectionLength; - } - - currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); - } - - /// - /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. - /// - private void saveCurrentPeak() - { - strainPeaks.Add(currentSectionPeak); - } - - /// - /// Sets the initial strain level for a new section. - /// - /// The beginning of the new section in milliseconds. - private void startNewSectionFrom(double time) + protected override void Process(DifficultyHitObject hitObject) { - // The maximum strain of the new section is not zero by default - // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. - currentSectionPeak = CalculateInitialStrain(time); + sections.UpdateTime(hitObject.StartTime); + sections.UpdateStrainPeak(SkillMultiplier*StrainValueAt(hitObject)); } - /// - /// Retrieves the peak strain at a point in time. - /// - /// The time to retrieve the peak strain at. - /// The peak strain. - protected abstract double CalculateInitialStrain(double time); + protected abstract double StrainValueAt(DifficultyHitObject hitObject); + protected abstract double StrainAtTime(double time); - /// - /// Returns a live enumerable of the peak strains for each section of the beatmap, - /// including the peak of the current section. - /// - public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); - /// - /// Returns the calculated difficulty value representing all s that have been processed up to this point. - /// public override double DifficultyValue() { - double difficulty = 0; - double weight = 1; + return sections.ExponentialWeightedSum(DecayWeight); + } - // Difficulty is the weighted sum of the highest strains from every section. - // We're sorting from highest to lowest strain. - foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d)) - { - difficulty += strain * weight; - weight *= DecayWeight; - } + public IEnumerable GetCurrentStrainPeaks() => sections.GetCurrentStrainPeaks(); - return difficulty; - } } + } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs new file mode 100644 index 000000000000..090b7b3e87f7 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + public class DecayingStrainSections + { + + public DecayingValue strain; + public StrainSections sections; + + public DecayingStrainSections(double strainDecayBase, int sectionLength=400) + { + strain = new DecayingValue(strainDecayBase); + sections = new StrainSections(strain.ValueAtTime, sectionLength); + } + + public double AddStrain(double time, double strainIncrease) + { + sections.UpdateTime(time); + strain.IncrementValue(time, strainIncrease); + sections.UpdateStrainPeak(strain.Value); + return strain.Value; + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs new file mode 100644 index 000000000000..55754faed237 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + public class DecayingValue + { + public DecayingValue(double exponentialBase) + { + k = Math.Log(exponentialBase)/1000; + } + + private double k; + + public double CurrentTime { get; private set; } = 0; + public double Value { get; set; } = 0; // why was this 1 before? it's making tests fail by ~0.001 sr + + public double ValueAtTime(double time) + { + double deltaTime = time - CurrentTime; + return Value * Math.Exp(k * deltaTime); + } + + public double UpdateTime(double time) + { + Value = ValueAtTime(time); + CurrentTime = time; + return Value; + } + + + public double IncrementValue(double time, double valueIncrease) + { + UpdateTime(time); + Value += valueIncrease; + + return Value; + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs b/osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs new file mode 100644 index 000000000000..5f2c94c7c813 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + public class StrainSections + { + /// + /// The length of each strain section. + /// + private readonly int sectionLength; + + private Func strainValueAtTime; + + private double currentSectionPeak = 1; // Why 1? + + private double currentSectionEnd; + + private readonly List strainPeaks = new List(); + + public StrainSections(Func strainValueAtTime, int sectionLength = 400) + { + this.sectionLength = sectionLength; + this.strainValueAtTime = strainValueAtTime; + } + + /// + /// Save strain peaks up to given time + /// + public void UpdateTime(double time) + { + // The first object doesn't generate a strain, so we begin with an incremented section end + if (currentSectionEnd == 0) + currentSectionEnd = Math.Ceiling(time / sectionLength) * sectionLength; + + while (time > currentSectionEnd) + { + saveCurrentPeak(); + currentSectionPeak = strainValueAtTime(currentSectionEnd); + currentSectionEnd += sectionLength; + } + + } + + /// + /// Update current strain value, setting currentSectionPeak if necessary + /// + public void UpdateStrainPeak(double strain) + { + currentSectionPeak = Math.Max(strain, currentSectionPeak); + } + + /// + /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. + /// + private void saveCurrentPeak() + { + strainPeaks.Add(currentSectionPeak); + } + + /// + /// Returns a live enumerable of the peak strains for each section of the beatmap, + /// including the peak of the current section. + /// + public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); + + /// + /// Returns the calculated difficulty value representing all strains that have been processed up to this point. + /// + public double ExponentialWeightedSum(double decayWeight=0.9) + { + double difficulty = 0; + double weight = 1; + + // Difficulty is the weighted sum of the highest strains from every section. + // We're sorting from highest to lowest strain. + foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= decayWeight; + } + + return difficulty; + } + } +} From 3c420cecd23db9435ca9831f20993ac89bc133be Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Thu, 7 Oct 2021 08:59:49 +0100 Subject: [PATCH 2/5] further refactoring on skills --- .../Difficulty/Skills/Movement.cs | 4 +- .../Difficulty/Skills/Strain.cs | 2 - .../Preprocessing/OsuDifficultyHitObject.cs | 3 - .../Difficulty/Skills/Aim.cs | 1 - .../Difficulty/Skills/Flashlight.cs | 2 +- .../Difficulty/Skills/OsuStrainSkill.cs | 16 +--- .../Difficulty/Skills/Rhythm.cs | 7 -- osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 2 +- .../Difficulty/Skills/StrainDecaySkill.cs | 22 ++--- .../Rulesets/Difficulty/Skills/StrainSkill.cs | 28 +++--- .../Difficulty/Utils/DecayingStrainPeaks.cs | 32 +++++++ .../Utils/DecayingStrainSections.cs | 27 ------ .../Difficulty/Utils/DecayingValue.cs | 28 ++++-- .../Rulesets/Difficulty/Utils/SectionPeaks.cs | 78 +++++++++++++++++ .../Difficulty/Utils/StrainSections.cs | 87 ------------------- osu.Game/Rulesets/Difficulty/Utils/Utils.cs | 36 ++++++++ 16 files changed, 189 insertions(+), 186 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs delete mode 100644 osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs delete mode 100644 osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/Utils.cs diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index c9989543d61b..c9bf5955594c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -16,9 +16,7 @@ public class Movement : StrainDecaySkill private const double direction_change_bonus = 21.0; protected override double SkillMultiplier => 900; - - protected override double DecayWeight => 0.94; - + protected override double DifficultySumWeight => 0.94; protected readonly float HalfCatcherWidth; diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index fa4707d5614d..c5b07285cfcc 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -5,8 +5,6 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Difficulty.Utils; - using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index f6362049606c..8e8f9bc06eb4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Osu.Difficulty.Skills; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osuTK; @@ -41,8 +40,6 @@ public class OsuDifficultyHitObject : DifficultyHitObject private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastObject; - - public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, HitObject lastObject, double clockRate) : base(hitObject, lastObject, clockRate) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 84ebc56a3a40..d15e49fb77af 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -24,7 +24,6 @@ public Aim(Mod[] mods) protected override double SkillMultiplier => 26.25; - protected override double StrainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index cece5b724364..10b4842578ce 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -20,7 +20,7 @@ public Flashlight(Mod[] mods) } protected override double SkillMultiplier => 0.15; - protected override double DecayWeight => 1.0; + protected override double DifficultySumWeight => 1.0; protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations. protected override double StrainValueOf(DifficultyHitObject current) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index cd418568d634..c81fc9950a30 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; - using osu.Game.Rulesets.Mods; using System.Linq; using osu.Framework.Utils; @@ -30,8 +29,6 @@ public abstract class OsuStrainSkill : StrainDecaySkill /// protected virtual double DifficultyMultiplier => 1.06; - - protected OsuStrainSkill(Mod[] mods, double strainDecayBase, int sectionLength = 400) : base(mods, strainDecayBase, sectionLength) { @@ -39,9 +36,6 @@ protected OsuStrainSkill(Mod[] mods, double strainDecayBase, int sectionLength = public override double DifficultyValue() { - double difficulty = 0; - double weight = 1; - List strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList(); // We are reducing the highest strains first to account for extreme difficulty spikes @@ -51,15 +45,7 @@ public override double DifficultyValue() strains[i] *= Interpolation.Lerp(ReducedStrainBaseline, 1.0, scale); } - // Difficulty is the weighted sum of the highest strains from every section. - // We're sorting from highest to lowest strain. - foreach (double strain in strains.OrderByDescending(d => d)) - { - difficulty += strain * weight; - weight *= DecayWeight; - } - - return difficulty * DifficultyMultiplier; + return strains.SortedExponentialWeightedSum(DifficultySumWeight) * DifficultyMultiplier; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 6e7045a726c7..992b7d9da9ed 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -2,7 +2,6 @@ // 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; @@ -45,17 +44,11 @@ public class Rhythm : StrainSkill /// private int notesSinceRhythmChange; - public Rhythm(Mod[] mods) : base(mods) { } - public override double DifficultyValue() - { - return sections.ExponentialWeightedSum(); - } - protected override double StrainAtTime(double time) { return 0; diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 9f0fb987a73f..98a72bf2c5dd 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills { /// - /// A bare minimal abstract skill for fully custom skill implementations. + /// Process the in a map and produce a difficulty value. /// public abstract class Skill { diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 5295aa8d42ab..60ccb8d6905d 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -1,46 +1,42 @@ // 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 osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; - using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty.Skills { /// - /// Used to processes strain values of s, keep track of strain levels caused by the processed objects - /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. + /// Convenience base class for a making use of /// public abstract class StrainDecaySkill : Skill { - protected virtual double DecayWeight => 0.9; + protected virtual double DifficultySumWeight => 0.9; protected abstract double SkillMultiplier { get; } - protected readonly DecayingStrainSections strain; + protected readonly DecayingStrainPeaks Strain; - protected StrainDecaySkill(Mod[] mods, double strainDecayBase, int sectionLength=400) + protected StrainDecaySkill(Mod[] mods, double strainDecayBase, int sectionLength = 400) : base(mods) { - strain = new DecayingStrainSections(strainDecayBase, sectionLength); + Strain = new DecayingStrainPeaks(strainDecayBase, sectionLength); } - protected override void Process(DifficultyHitObject hitObject) + protected sealed override void Process(DifficultyHitObject hitObject) { - strain.AddStrain(hitObject.StartTime, SkillMultiplier*StrainValueOf(hitObject)); + Strain.IncrementStrainAtTime(hitObject.StartTime, SkillMultiplier * StrainValueOf(hitObject)); } protected abstract double StrainValueOf(DifficultyHitObject hitObject); public override double DifficultyValue() { - return strain.sections.ExponentialWeightedSum(DecayWeight); + return Strain.StrainPeaks.SortedExponentialWeightedSum(DifficultySumWeight); } - public IEnumerable GetCurrentStrainPeaks() => strain.sections.GetCurrentStrainPeaks(); - + public IEnumerable GetCurrentStrainPeaks() => Strain.StrainPeaks; } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 4f947660b726..7b6822294f3d 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.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.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Mods; @@ -11,40 +9,36 @@ namespace osu.Game.Rulesets.Difficulty.Skills { /// - /// Used to processes strain values of s, keep track of strain levels caused by the processed objects - /// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects. + /// Convenience base class for a making use of /// public abstract class StrainSkill : Skill { - protected virtual double DecayWeight => 0.9; + protected virtual double DifficultySumWeight => 0.9; protected abstract double SkillMultiplier { get; } - protected readonly StrainSections sections; + protected readonly SectionPeaks StrainPeaks; protected StrainSkill(Mod[] mods, int sectionLength = 400) : base(mods) { - sections = new StrainSections(StrainAtTime, sectionLength); - } - - protected override void Process(DifficultyHitObject hitObject) - { - sections.UpdateTime(hitObject.StartTime); - sections.UpdateStrainPeak(SkillMultiplier*StrainValueAt(hitObject)); + StrainPeaks = new SectionPeaks(StrainAtTime, sectionLength); } protected abstract double StrainValueAt(DifficultyHitObject hitObject); protected abstract double StrainAtTime(double time); + protected override void Process(DifficultyHitObject hitObject) + { + StrainPeaks.AdvanceTime(hitObject.StartTime); + StrainPeaks.UpdateValue(SkillMultiplier * StrainValueAt(hitObject)); + } public override double DifficultyValue() { - return sections.ExponentialWeightedSum(DecayWeight); + return StrainPeaks.ExponentialWeightedSum(DifficultySumWeight); } - public IEnumerable GetCurrentStrainPeaks() => sections.GetCurrentStrainPeaks(); - + public IEnumerable GetCurrentStrainPeaks() => StrainPeaks; } - } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs new file mode 100644 index 000000000000..a2495ebe14cd --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + /// + /// Store the of a strain. + /// + public class DecayingStrainPeaks + { + public DecayingValue CurrentStrain; + public SectionPeaks StrainPeaks; + + public DecayingStrainPeaks(double strainDecayBase, int sectionLength = 400) + { + CurrentStrain = new DecayingValue(strainDecayBase); + StrainPeaks = new SectionPeaks(CurrentStrain.ValueAtTime, sectionLength); + } + + /// + /// Advances time, increments and updates . + /// + /// The current strain value. + public double IncrementStrainAtTime(double time, double strainIncrease) + { + StrainPeaks.AdvanceTime(time); + CurrentStrain.IncrementValueAtTime(time, strainIncrease); + StrainPeaks.UpdateValue(CurrentStrain.Value); + return CurrentStrain.Value; + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs deleted file mode 100644 index 090b7b3e87f7..000000000000 --- a/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainSections.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace osu.Game.Rulesets.Difficulty.Utils -{ - public class DecayingStrainSections - { - - public DecayingValue strain; - public StrainSections sections; - - public DecayingStrainSections(double strainDecayBase, int sectionLength=400) - { - strain = new DecayingValue(strainDecayBase); - sections = new StrainSections(strain.ValueAtTime, sectionLength); - } - - public double AddStrain(double time, double strainIncrease) - { - sections.UpdateTime(time); - strain.IncrementValue(time, strainIncrease); - sections.UpdateStrainPeak(strain.Value); - return strain.Value; - } - } -} diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs index 55754faed237..42680b6261af 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs @@ -1,20 +1,31 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; namespace osu.Game.Rulesets.Difficulty.Utils { + /// + /// Represents a value that exponentially decays over time + /// public class DecayingValue { + /// The value will decay by this multiplier in one second public DecayingValue(double exponentialBase) { - k = Math.Log(exponentialBase)/1000; + k = Math.Log(exponentialBase) / 1000; } - private double k; + private readonly double k; + + /// + /// Current time in milliseconds + /// + public double CurrentTime { get; private set; } - public double CurrentTime { get; private set; } = 0; - public double Value { get; set; } = 0; // why was this 1 before? it's making tests fail by ~0.001 sr + // why was this 1 before? it's making tests fail by ~0.001 sr. + // It makes the whole system work weird, need to initialize to 1 at the time of the first hit object, but process doesn't get called for that + public double Value { get; set; } public double ValueAtTime(double time) { @@ -29,8 +40,7 @@ public double UpdateTime(double time) return Value; } - - public double IncrementValue(double time, double valueIncrease) + public double IncrementValueAtTime(double time, double valueIncrease) { UpdateTime(time); Value += valueIncrease; diff --git a/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs b/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs new file mode 100644 index 000000000000..601998250aa3 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs @@ -0,0 +1,78 @@ +// 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; +using System.Linq; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + /// + /// Container for storing the peak value for consecutive intervals of a time series + /// + public class SectionPeaks : IEnumerable + { + /// + /// The duration of each interval. + /// + private readonly int sectionLength; + + /// + /// Calculate the value at a time point in the future, assuming nothing else happens + /// Used to calculate the peak at the beginning of each section + /// + private readonly Func valueAtTime; + + private double currentSectionPeak; // Why was this 1? + private double currentSectionEnd; + private readonly List strainPeaks = new List(); + + public SectionPeaks(Func valueAtTime, int sectionLength = 400) + { + this.sectionLength = sectionLength; + this.valueAtTime = valueAtTime; + } + + /// + /// Save strain peaks up to given time + /// + public void AdvanceTime(double time) + { + // On the first call, start with incremented currentSectionEnd. This avoids storing peaks before the first section has begun. + if (currentSectionEnd == 0) + currentSectionEnd = Math.Ceiling(time / sectionLength) * sectionLength; + + while (time > currentSectionEnd) + { + saveCurrentPeak(); + currentSectionPeak = valueAtTime(currentSectionEnd); + currentSectionEnd += sectionLength; + } + } + + /// + /// Update current value, setting if necessary + /// + public void UpdateValue(double value) + { + currentSectionPeak = Math.Max(value, currentSectionPeak); + } + + /// + /// Saves the current peak value to the list of peaks. + /// + private void saveCurrentPeak() + { + strainPeaks.Add(currentSectionPeak); + } + + /// + /// Enumerating yields the peak strains for each section of the beatmap, + /// including the peak of the current section. + /// + public IEnumerator GetEnumerator() => strainPeaks.Append(currentSectionPeak).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs b/osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs deleted file mode 100644 index 5f2c94c7c813..000000000000 --- a/osu.Game/Rulesets/Difficulty/Utils/StrainSections.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace osu.Game.Rulesets.Difficulty.Utils -{ - public class StrainSections - { - /// - /// The length of each strain section. - /// - private readonly int sectionLength; - - private Func strainValueAtTime; - - private double currentSectionPeak = 1; // Why 1? - - private double currentSectionEnd; - - private readonly List strainPeaks = new List(); - - public StrainSections(Func strainValueAtTime, int sectionLength = 400) - { - this.sectionLength = sectionLength; - this.strainValueAtTime = strainValueAtTime; - } - - /// - /// Save strain peaks up to given time - /// - public void UpdateTime(double time) - { - // The first object doesn't generate a strain, so we begin with an incremented section end - if (currentSectionEnd == 0) - currentSectionEnd = Math.Ceiling(time / sectionLength) * sectionLength; - - while (time > currentSectionEnd) - { - saveCurrentPeak(); - currentSectionPeak = strainValueAtTime(currentSectionEnd); - currentSectionEnd += sectionLength; - } - - } - - /// - /// Update current strain value, setting currentSectionPeak if necessary - /// - public void UpdateStrainPeak(double strain) - { - currentSectionPeak = Math.Max(strain, currentSectionPeak); - } - - /// - /// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty. - /// - private void saveCurrentPeak() - { - strainPeaks.Add(currentSectionPeak); - } - - /// - /// Returns a live enumerable of the peak strains for each section of the beatmap, - /// including the peak of the current section. - /// - public IEnumerable GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak); - - /// - /// Returns the calculated difficulty value representing all strains that have been processed up to this point. - /// - public double ExponentialWeightedSum(double decayWeight=0.9) - { - double difficulty = 0; - double weight = 1; - - // Difficulty is the weighted sum of the highest strains from every section. - // We're sorting from highest to lowest strain. - foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d)) - { - difficulty += strain * weight; - weight *= decayWeight; - } - - return difficulty; - } - } -} diff --git a/osu.Game/Rulesets/Difficulty/Utils/Utils.cs b/osu.Game/Rulesets/Difficulty/Utils/Utils.cs new file mode 100644 index 000000000000..0d8642948ac7 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/Utils.cs @@ -0,0 +1,36 @@ +// 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; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + public static class Utils + { + /// + /// Calculates the sum of (values[i] * weight^i) + /// + public static double ExponentialWeightedSum(this IEnumerable values, double weight) + { + double cumulativeWeight = 1; + double total = 0; + + foreach (double value in values) + { + total += value * cumulativeWeight; + cumulativeWeight *= weight; + } + + return total; + } + + /// + /// Calculates the sum of (values[i] * weight^i) after sorting values + /// + public static double SortedExponentialWeightedSum(this IEnumerable values, double weight) + { + return values.OrderByDescending(x => x).ExponentialWeightedSum(weight); + } + } +} From 566075861a0f5b4261be4b1d1b8c53d8eb719ce1 Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Sat, 9 Oct 2021 00:58:30 +0100 Subject: [PATCH 3/5] fix StrainSkill, further refactor --- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 2 -- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 4 ++-- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index c5b07285cfcc..80cf500bcc85 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -12,8 +12,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { public class Strain : StrainSkill { - protected override double SkillMultiplier => 1; - private const double individual_decay_base = 0.125; private const double overall_decay_base = 0.30; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 992b7d9da9ed..61957eb3dbad 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// public class Rhythm : StrainSkill { - protected override double SkillMultiplier => 10; + private const double skill_multiplier = 10; /// /// The note-based decay for rhythm strain. @@ -84,7 +84,7 @@ protected override double StrainValueAt(DifficultyHitObject current) notesSinceRhythmChange = 0; currentStrain += objectStrain; - return currentStrain; + return skill_multiplier * currentStrain; } /// diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 7b6822294f3d..2a81697ebf30 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -15,8 +15,6 @@ public abstract class StrainSkill : Skill { protected virtual double DifficultySumWeight => 0.9; - protected abstract double SkillMultiplier { get; } - protected readonly SectionPeaks StrainPeaks; protected StrainSkill(Mod[] mods, int sectionLength = 400) @@ -31,12 +29,12 @@ protected StrainSkill(Mod[] mods, int sectionLength = 400) protected override void Process(DifficultyHitObject hitObject) { StrainPeaks.AdvanceTime(hitObject.StartTime); - StrainPeaks.UpdateValue(SkillMultiplier * StrainValueAt(hitObject)); + StrainPeaks.UpdateValue(StrainValueAt(hitObject)); } public override double DifficultyValue() { - return StrainPeaks.ExponentialWeightedSum(DifficultySumWeight); + return StrainPeaks.SortedExponentialWeightedSum(DifficultySumWeight); } public IEnumerable GetCurrentStrainPeaks() => StrainPeaks; From 672dd40e5978446f6dd041fe4fd9b7a20882413b Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Sat, 9 Oct 2021 13:49:03 +0100 Subject: [PATCH 4/5] address review comments --- .../Difficulty/Skills/Rhythm.cs | 5 +---- .../Rulesets/Difficulty/Skills/StrainDecaySkill.cs | 10 ++++++++++ osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 14 ++++++++++++-- .../Difficulty/Utils/DecayingStrainPeaks.cs | 6 +++--- .../Rulesets/Difficulty/Utils/DecayingValue.cs | 13 +++++++------ osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs | 4 ++-- 6 files changed, 35 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 61957eb3dbad..5f4a77ec781a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -49,10 +49,7 @@ public Rhythm(Mod[] mods) { } - protected override double StrainAtTime(double time) - { - return 0; - } + protected override double StrainAtTime(double time) => 0; protected override double StrainValueAt(DifficultyHitObject current) { diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 60ccb8d6905d..c072286360dc 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -13,8 +13,14 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public abstract class StrainDecaySkill : Skill { + /// + /// The weight for the exponential sum of strains which produces the final difficulty value + /// protected virtual double DifficultySumWeight => 0.9; + /// + /// Scales the value of to produce a final strain. + /// protected abstract double SkillMultiplier { get; } protected readonly DecayingStrainPeaks Strain; @@ -30,6 +36,10 @@ protected sealed override void Process(DifficultyHitObject hitObject) Strain.IncrementStrainAtTime(hitObject.StartTime, SkillMultiplier * StrainValueOf(hitObject)); } + /// + /// Returns a strain increment representing the difficulty of the . + /// This will be scaled by and added onto the current strain. + /// protected abstract double StrainValueOf(DifficultyHitObject hitObject); public override double DifficultyValue() diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 2a81697ebf30..f92f2d981093 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -9,10 +9,13 @@ namespace osu.Game.Rulesets.Difficulty.Skills { /// - /// Convenience base class for a making use of + /// Convenience base class for a making use of /// public abstract class StrainSkill : Skill { + /// + /// The weight for the exponential sum of strains which produces the final difficulty value + /// protected virtual double DifficultySumWeight => 0.9; protected readonly SectionPeaks StrainPeaks; @@ -23,13 +26,20 @@ protected StrainSkill(Mod[] mods, int sectionLength = 400) StrainPeaks = new SectionPeaks(StrainAtTime, sectionLength); } + /// + /// Calculates the total strain value at the time of the + /// protected abstract double StrainValueAt(DifficultyHitObject hitObject); + + /// + /// Calculates the total strain value at section boundaries. + /// protected abstract double StrainAtTime(double time); protected override void Process(DifficultyHitObject hitObject) { StrainPeaks.AdvanceTime(hitObject.StartTime); - StrainPeaks.UpdateValue(StrainValueAt(hitObject)); + StrainPeaks.SetValueAtCurrentTime(StrainValueAt(hitObject)); } public override double DifficultyValue() diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs index a2495ebe14cd..03adee0df68c 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingStrainPeaks.cs @@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Difficulty.Utils /// public class DecayingStrainPeaks { - public DecayingValue CurrentStrain; - public SectionPeaks StrainPeaks; + public readonly DecayingValue CurrentStrain; + public readonly SectionPeaks StrainPeaks; public DecayingStrainPeaks(double strainDecayBase, int sectionLength = 400) { @@ -25,7 +25,7 @@ public double IncrementStrainAtTime(double time, double strainIncrease) { StrainPeaks.AdvanceTime(time); CurrentStrain.IncrementValueAtTime(time, strainIncrease); - StrainPeaks.UpdateValue(CurrentStrain.Value); + StrainPeaks.SetValueAtCurrentTime(CurrentStrain.Value); return CurrentStrain.Value; } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs index 42680b6261af..0bc9a23f2950 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; namespace osu.Game.Rulesets.Difficulty.Utils { @@ -13,24 +14,24 @@ public class DecayingValue /// The value will decay by this multiplier in one second public DecayingValue(double exponentialBase) { - k = Math.Log(exponentialBase) / 1000; + decayRate = Math.Log(exponentialBase) / 1000; } - private readonly double k; + private readonly double decayRate; /// /// Current time in milliseconds /// public double CurrentTime { get; private set; } - // why was this 1 before? it's making tests fail by ~0.001 sr. - // It makes the whole system work weird, need to initialize to 1 at the time of the first hit object, but process doesn't get called for that - public double Value { get; set; } + public double Value { get; private set; } public double ValueAtTime(double time) { + Debug.Assert(time >= CurrentTime); + double deltaTime = time - CurrentTime; - return Value * Math.Exp(k * deltaTime); + return Value * Math.Exp(decayRate * deltaTime); } public double UpdateTime(double time) diff --git a/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs b/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs index 601998250aa3..3307ff46a973 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs @@ -24,7 +24,7 @@ public class SectionPeaks : IEnumerable /// private readonly Func valueAtTime; - private double currentSectionPeak; // Why was this 1? + private double currentSectionPeak; private double currentSectionEnd; private readonly List strainPeaks = new List(); @@ -54,7 +54,7 @@ public void AdvanceTime(double time) /// /// Update current value, setting if necessary /// - public void UpdateValue(double value) + public void SetValueAtCurrentTime(double value) { currentSectionPeak = Math.Max(value, currentSectionPeak); } From 3d11e420df48546fca039c3f16907a1b51484905 Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Wed, 20 Oct 2021 00:03:36 +0100 Subject: [PATCH 5/5] update StrainSkill documentation --- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index f92f2d981093..7f3434c508b0 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -32,7 +32,7 @@ protected StrainSkill(Mod[] mods, int sectionLength = 400) protected abstract double StrainValueAt(DifficultyHitObject hitObject); /// - /// Calculates the total strain value at section boundaries. + /// Calculate the strain value at a point in time in between hit objects. /// protected abstract double StrainAtTime(double time);