From 31cf00e3b83f04aee8c2d28325239f0b1060431d Mon Sep 17 00:00:00 2001 From: Poyo Date: Sun, 25 Feb 2018 23:52:38 -0800 Subject: [PATCH] Implement mania star difficulty calculation --- .../ManiaDifficultyCalculator.cs | 124 +++++++++++++++++- .../Objects/ManiaHitObjectDifficulty.cs | 114 ++++++++++++++++ .../osu.Game.Rulesets.Mania.csproj | 1 + 3 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 75a8543548..62d2929f27 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -4,18 +4,140 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using System; using System.Collections.Generic; namespace osu.Game.Rulesets.Mania { public class ManiaDifficultyCalculator : DifficultyCalculator { + private const double star_scaling_factor = 0.018; + + /// + /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size strain_step. + /// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain. + /// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage. + /// + protected const double strain_step = 400; + + /// + /// The weighting of each strain value decays to this number * it's previous value + /// + protected const double decay_weight = 0.9; + + /// + /// HitObjects are stored as a member variable. + /// + private readonly List difficultyHitObjects = new List(); + public ManiaDifficultyCalculator(Beatmap beatmap) : base(beatmap) { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public override double Calculate(Dictionary categoryDifficulty = null) + { + // Fill our custom DifficultyHitObject class, that carries additional information + difficultyHitObjects.Clear(); + + int columnCount = (Beatmap as ManiaBeatmap).TotalColumns; + + foreach (var hitObject in Beatmap.HitObjects) + difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount)); + + // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. + difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); + + if (!calculateStrainValues()) + return 0; + + double starRating = calculateDifficulty() * star_scaling_factor; + + if (categoryDifficulty != null) + { + categoryDifficulty.Add("Strain", starRating); + // categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); + } + + return starRating; + } + + private bool calculateStrainValues() + { + // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. + using (List.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator()) + { + if (!hitObjectsEnumerator.MoveNext()) + return false; + + ManiaHitObjectDifficulty current = hitObjectsEnumerator.Current; + + // First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject. + while (hitObjectsEnumerator.MoveNext()) + { + var next = hitObjectsEnumerator.Current; + next?.CalculateStrains(current, TimeRate); + current = next; + } + + return true; + } + } + + private double calculateDifficulty() + { + double actualStrainStep = strain_step * TimeRate; + + // Find the highest strain value within each strain step + List highestStrains = new List(); + double intervalEndTime = actualStrainStep; + double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval + + ManiaHitObjectDifficulty previousHitObject = null; + foreach (var hitObject in difficultyHitObjects) + { + // While we are beyond the current interval push the currently available maximum to our strain list + while (hitObject.BaseHitObject.StartTime > intervalEndTime) + { + highestStrains.Add(maximumStrain); + + // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay + // until the beginning of the next interval. + if (previousHitObject == null) + { + maximumStrain = 0; + } + else + { + double individualDecay = Math.Pow(ManiaHitObjectDifficulty.INDIVIDUAL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); + double overallDecay = Math.Pow(ManiaHitObjectDifficulty.OVERALL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); + maximumStrain = previousHitObject.IndividualStrain * individualDecay + previousHitObject.OverallStrain * overallDecay; + } + + // Go to the next time interval + intervalEndTime += actualStrainStep; + } + + // Obtain maximum strain + double strain = hitObject.IndividualStrain + hitObject.OverallStrain; + maximumStrain = Math.Max(strain, maximumStrain); + + previousHitObject = hitObject; + } + + // Build the weighted sum over the highest strains for each interval + double difficulty = 0; + double weight = 1; + highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. + + foreach (double strain in highestStrains) + { + difficulty += weight * strain; + weight *= decay_weight; + } + + return difficulty; + } protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, beatmap); } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs new file mode 100644 index 0000000000..e8b47092f6 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs @@ -0,0 +1,114 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Types; +using System; + +namespace osu.Game.Rulesets.Mania.Objects +{ + class ManiaHitObjectDifficulty + { + /// + /// Factor by how much individual / overall strain decays per second. + /// + /// + /// These values are results of tweaking a lot and taking into account general feedback. + /// + internal const double INDIVIDUAL_DECAY_BASE = 0.125; + internal const double OVERALL_DECAY_BASE = 0.30; + + internal ManiaHitObject BaseHitObject; + + private int beatmapColumnCount; + + + private double endTime; + private double[] heldUntil; + + /// + /// Measures jacks or more generally: repeated presses of the same button + /// + private double[] individualStrains; + + internal double IndividualStrain + { + get + { + return individualStrains[BaseHitObject.Column]; + } + + set + { + individualStrains[BaseHitObject.Column] = value; + } + } + + /// + /// Measures note density in a way + /// + internal double OverallStrain = 1; + + public ManiaHitObjectDifficulty(ManiaHitObject baseHitObject, int columnCount) + { + BaseHitObject = baseHitObject; + + endTime = (baseHitObject as IHasEndTime)?.EndTime ?? baseHitObject.StartTime; + + beatmapColumnCount = columnCount; + heldUntil = new double[beatmapColumnCount]; + individualStrains = new double[beatmapColumnCount]; + + for (int i = 0; i < beatmapColumnCount; ++i) + { + individualStrains[i] = 0; + heldUntil[i] = 0; + } + } + + internal void CalculateStrains(ManiaHitObjectDifficulty previousHitObject, double timeRate) + { + // TODO: Factor in holds + double addition = 1.0; + double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; + double individualDecay = Math.Pow(INDIVIDUAL_DECAY_BASE, timeElapsed / 1000); + double overallDecay = Math.Pow(OVERALL_DECAY_BASE, timeElapsed / 1000); + + double holdFactor = 1.0; // Factor to all additional strains in case something else is held + double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + + // Fill up the heldUntil array + for (int i = 0; i < beatmapColumnCount; ++i) + { + heldUntil[i] = previousHitObject.heldUntil[i]; + + // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... + if (BaseHitObject.StartTime < heldUntil[i] && endTime > heldUntil[i]) + { + holdAddition = 1.0; + } + + // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 + if (endTime == heldUntil[i]) + { + holdAddition = 0; + } + + // We give a slight bonus to everything if something is held meanwhile + if (heldUntil[i] > endTime) + { + holdFactor = 1.25; + } + + // Decay individual strains + individualStrains[i] = previousHitObject.individualStrains[i] * individualDecay; + } + + heldUntil[BaseHitObject.Column] = endTime; + + // Increase individual strain in own column + IndividualStrain += (2.0/* + (double)SpeedMania.Column / 8.0*/) * holdFactor; + + OverallStrain = previousHitObject.OverallStrain * overallDecay + (addition + holdAddition) * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index a2e21e2053..b9c62cf40b 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -114,6 +114,7 @@ +