diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index cfb3fe40bee7..c9bf5955594c 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -16,11 +16,7 @@ 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 override double DifficultySumWeight => 0.94; protected readonly float HalfCatcherWidth; @@ -34,7 +30,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..80cf500bcc85 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -10,14 +10,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills { - public class Strain : StrainDecaySkill + public class Strain : StrainSkill { 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 +29,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 +65,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/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index d8f4aa1229f2..d852eac597e7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -65,7 +65,7 @@ private double strainValueOf(DifficultyHitObject current) private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); + protected override double StrainAtTime(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); protected override double StrainValueAt(DifficultyHitObject current) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index e3abe7d7008b..8e39cbbe5d24 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -21,7 +21,7 @@ public Flashlight(Mod[] mods) private double skillMultiplier => 0.15; private double strainDecayBase => 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. private double currentStrain = 1; @@ -66,7 +66,7 @@ private double strainValueOf(DifficultyHitObject current) private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); + protected override double StrainAtTime(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); protected override double StrainValueAt(DifficultyHitObject current) { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index e47edc37cca9..4392a821fb06 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -4,6 +4,7 @@ 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,16 +29,13 @@ public abstract class OsuStrainSkill : StrainSkill /// protected virtual double DifficultyMultiplier => 1.06; - protected OsuStrainSkill(Mod[] mods) - : base(mods) + protected OsuStrainSkill(Mod[] mods, int sectionLength = 400) + : base(mods, 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 @@ -47,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.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index cae6b8e01cbf..8bb8dbc62046 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -161,7 +161,7 @@ private double strainValueOf(DifficultyHitObject current) private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime); + protected override double StrainAtTime(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime); protected override double StrainValueAt(DifficultyHitObject current) { 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..5f4a77ec781a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -14,17 +14,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; + private const double skill_multiplier = 10; /// /// 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; /// @@ -53,7 +49,9 @@ public Rhythm(Mod[] mods) { } - protected override double StrainValueOf(DifficultyHitObject current) + protected override double StrainAtTime(double time) => 0; + + protected override double StrainValueAt(DifficultyHitObject current) { // drum rolls and swells are exempt. if (!(current.BaseObject is Hit)) @@ -83,7 +81,7 @@ protected override double StrainValueOf(DifficultyHitObject current) notesSinceRhythmChange = 0; currentStrain += objectStrain; - return currentStrain; + return skill_multiplier * currentStrain; } /// 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/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 d8babf2f3224..c072286360dc 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -1,54 +1,52 @@ // 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 : 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. + /// The weight for the exponential sum of strains which produces the final difficulty value /// - protected abstract double SkillMultiplier { get; } + protected virtual double DifficultySumWeight => 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. + /// Scales the value of to produce a final strain. /// - protected abstract double StrainDecayBase { get; } + protected abstract double SkillMultiplier { get; } - /// - /// The current strain level. - /// - protected double CurrentStrain { get; private set; } + protected readonly DecayingStrainPeaks Strain; - protected StrainDecaySkill(Mod[] mods) + protected StrainDecaySkill(Mod[] mods, double strainDecayBase, int sectionLength = 400) : base(mods) { + Strain = new DecayingStrainPeaks(strainDecayBase, sectionLength); } - protected override double CalculateInitialStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime); - - protected override double StrainValueAt(DifficultyHitObject current) + protected sealed override void Process(DifficultyHitObject hitObject) { - CurrentStrain *= strainDecay(current.DeltaTime); - CurrentStrain += StrainValueOf(current) * SkillMultiplier; - - return CurrentStrain; + Strain.IncrementStrainAtTime(hitObject.StartTime, SkillMultiplier * StrainValueOf(hitObject)); } /// - /// Calculates the strain value of a . This value is affected by previously processed objects. + /// 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 current); + protected abstract double StrainValueOf(DifficultyHitObject hitObject); + + public override double DifficultyValue() + { + return Strain.StrainPeaks.SortedExponentialWeightedSum(DifficultySumWeight); + } - private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000); + public IEnumerable GetCurrentStrainPeaks() => Strain.StrainPeaks; } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index bbd2f079aaae..7f3434c508b0 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -1,114 +1,52 @@ // 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; 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 { /// - /// The weight by which each strain value decays. + /// The weight for the exponential sum of strains which produces the final difficulty value /// - protected virtual double DecayWeight => 0.9; + protected virtual double DifficultySumWeight => 0.9; - /// - /// The length of each strain section. - /// - protected virtual int SectionLength => 400; - - private double currentSectionPeak; // We also keep track of the peak strain level in the current section. - - private double currentSectionEnd; + protected readonly SectionPeaks StrainPeaks; - private readonly List strainPeaks = new List(); - - protected StrainSkill(Mod[] mods) + protected StrainSkill(Mod[] mods, int sectionLength = 400) : base(mods) { + StrainPeaks = new SectionPeaks(StrainAtTime, sectionLength); } /// - /// Returns the strain value at . This value is calculated with or without respect to previous objects. + /// Calculates the total strain value at the time of the /// - protected abstract double StrainValueAt(DifficultyHitObject current); + protected abstract double StrainValueAt(DifficultyHitObject hitObject); /// - /// Process a and update current strain values accordingly. + /// Calculate the strain value at a point in time in between hit objects. /// - 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; - } + protected abstract double StrainAtTime(double time); - 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() + protected override void Process(DifficultyHitObject hitObject) { - strainPeaks.Add(currentSectionPeak); + StrainPeaks.AdvanceTime(hitObject.StartTime); + StrainPeaks.SetValueAtCurrentTime(StrainValueAt(hitObject)); } - /// - /// Sets the initial strain level for a new section. - /// - /// The beginning of the new section in milliseconds. - private void startNewSectionFrom(double time) - { - // 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); - } - - /// - /// 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); - - /// - /// 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; - - // 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; + return StrainPeaks.SortedExponentialWeightedSum(DifficultySumWeight); } + + 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..03adee0df68c --- /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 readonly DecayingValue CurrentStrain; + public readonly 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.SetValueAtCurrentTime(CurrentStrain.Value); + return CurrentStrain.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..0bc9a23f2950 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/DecayingValue.cs @@ -0,0 +1,52 @@ +// 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.Diagnostics; + +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) + { + decayRate = Math.Log(exponentialBase) / 1000; + } + + private readonly double decayRate; + + /// + /// Current time in milliseconds + /// + public double CurrentTime { get; private set; } + + public double Value { get; private set; } + + public double ValueAtTime(double time) + { + Debug.Assert(time >= CurrentTime); + + double deltaTime = time - CurrentTime; + return Value * Math.Exp(decayRate * deltaTime); + } + + public double UpdateTime(double time) + { + Value = ValueAtTime(time); + CurrentTime = time; + return Value; + } + + public double IncrementValueAtTime(double time, double valueIncrease) + { + UpdateTime(time); + Value += valueIncrease; + + return Value; + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs b/osu.Game/Rulesets/Difficulty/Utils/SectionPeaks.cs new file mode 100644 index 000000000000..3307ff46a973 --- /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; + 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 SetValueAtCurrentTime(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/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); + } + } +}