From f6cb9b81c9acd129d1d93a5c95dbc90c1f312cb1 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 21 Jul 2022 17:53:08 -0400 Subject: [PATCH 01/30] Implement deviation-dependent acc pp/scaling --- .../Difficulty/OsuPerformanceAttributes.cs | 6 + .../Difficulty/OsuPerformanceCalculator.cs | 122 +++++++++++++----- .../osu.Game.Rulesets.Osu.csproj | 6 +- 3 files changed, 99 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index efb3ade220..b791f473d5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -26,6 +26,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty [JsonProperty("effective_miss_count")] public double EffectiveMissCount { get; set; } + [JsonProperty("deviation")] + public double Deviation { get; set; } + + [JsonProperty("speed_deviation")] + public double SpeedDeviation { get; set; } + public override IEnumerable GetAttributesForDisplay() { foreach (var attribute in base.GetAttributesForDisplay()) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index c3b7834009..ea4c2752d8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -6,7 +6,12 @@ using System; using System.Collections.Generic; using System.Linq; +using MathNet.Numerics; +using MathNet.Numerics.RootFinding; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -23,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMiss; private double effectiveMissCount; + private double deviation; + private double speedDeviation; public OsuPerformanceCalculator() : base(new OsuRuleset()) @@ -40,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); effectiveMissCount = calculateEffectiveMissCount(osuAttributes); + deviation = calculateDeviation(score, osuAttributes); + speedDeviation = calculateSpeedDeviation(score, osuAttributes); double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. @@ -76,6 +85,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty Accuracy = accuracyValue, Flashlight = flashlightValue, EffectiveMissCount = effectiveMissCount, + Deviation = deviation, + SpeedDeviation = speedDeviation, Total = totalValue }; } @@ -125,9 +136,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - aimValue *= accuracy; - // It is important to consider accuracy difficulty when scaling with accuracy. - aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + // Scale aim value with deviation + aimValue *= 4169 / 4050.0 * SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); return aimValue; } @@ -163,18 +173,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - // Calculate accuracy assuming the worst case scenario - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; - double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); - - // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); - - // Scale the speed value with # of 50s to punish doubletapping. - speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); + // Scale the speed value with speed deviation + speedValue *= 120.289 / 108 * Math.Pow(SpecialFunctions.Erf(26 / (Math.Sqrt(2) * speedDeviation)), 2); return speedValue; } @@ -184,25 +184,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; - // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. - double betterAccuracyPercentage; - int amountHitObjectsWithAccuracy = attributes.HitCircleCount; - - if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); - else - betterAccuracyPercentage = 0; - - // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points. - if (betterAccuracyPercentage < 0) - betterAccuracyPercentage = 0; - - // Lots of arbitrary values from testing. - // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. - double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; - - // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. - accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); + double accuracyValue = 800 * Math.Exp(-deviation / 4); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -216,6 +198,78 @@ namespace osu.Game.Rulesets.Osu.Difficulty return accuracyValue; } + private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double root2 = Math.Sqrt(2); + + if (totalHits == 0) + return double.PositiveInfinity; + + double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); + double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)) + 1; + double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); + + // Derivative of erf(x) + double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); + + // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, + // which is the same as finding the zero of the derivative of the log-likelihood function. + double logLikelihoodGradient(double u) + { + double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); + double t2 = relevantCountMeh * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow50 * erfPrime(hitWindow50 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow50 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); + double t3 = relevantCountOk * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); + return (t1 + t2 + t3) / (root2 * u * u); + } + + return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); + } + + private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + + double root2 = Math.Sqrt(2); + + if (totalHits == 0) + return double.PositiveInfinity; + + int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); + int okCountOnCircles = Math.Min(countOk, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. + int mehCountOnCircles = Math.Min(countMeh, attributes.HitCircleCount); + int slidersHit = attributes.SliderCount - countMiss; + + // Derivative of erf(x) + double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); + + // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, + // which is the same as finding the zero of the derivative of the log-likelihood function. + double logLikelihoodGradient(double u) + { + double t1 = -hitWindow50 * slidersHit * erfPrime(hitWindow50 / (root2 * u)) / SpecialFunctions.Erf(hitWindow50 / (root2 * u)); + double t2 = -hitWindow300 * greatCountOnCircles * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); + double t3 = mehCountOnCircles * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow50 * erfPrime(hitWindow50 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow50 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); + double t4 = okCountOnCircles * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); + return (t1 + t2 + t3 + t4) / (root2 * u * u); + } + + return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); + } + private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) { if (!score.Mods.Any(h => h is OsuModFlashlight)) diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 98f1e69bd1..fdb17f1621 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -15,4 +15,8 @@ - \ No newline at end of file + + + + + From 791fed9e19b8f1725d62bb2cd5b2b324a65efe50 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 21 Jul 2022 19:25:06 -0400 Subject: [PATCH 02/30] Penalize acc pp with misses + reorder code --- .../Difficulty/OsuPerformanceCalculator.cs | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ea4c2752d8..b8b5a6a1e5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -195,46 +195,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; + accuracyValue *= 1 - (double)countMiss / totalHits; + return accuracyValue; } - private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) - { - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; - - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double root2 = Math.Sqrt(2); - - if (totalHits == 0) - return double.PositiveInfinity; - - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; - double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)) + 1; - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - - // Derivative of erf(x) - double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); - - // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, - // which is the same as finding the zero of the derivative of the log-likelihood function. - double logLikelihoodGradient(double u) - { - double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t2 = relevantCountMeh * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow50 * erfPrime(hitWindow50 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow50 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); - double t3 = relevantCountOk * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); - return (t1 + t2 + t3) / (root2 * u * u); - } - - return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); - } - private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (totalSuccessfulHits == 0) + return double.PositiveInfinity; + var track = new TrackVirtual(10000); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); double clockRate = track.Rate; @@ -245,9 +215,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double root2 = Math.Sqrt(2); - if (totalHits == 0) - return double.PositiveInfinity; - int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); int okCountOnCircles = Math.Min(countOk, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. int mehCountOnCircles = Math.Min(countMeh, attributes.HitCircleCount); @@ -270,6 +237,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); } + private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return double.PositiveInfinity; + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double root2 = Math.Sqrt(2); + + double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); + double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)) + 1; + double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); + + // Derivative of erf(x) + double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); + + // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, + // which is the same as finding the zero of the derivative of the log-likelihood function. + double logLikelihoodGradient(double u) + { + double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); + double t2 = relevantCountMeh * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow50 * erfPrime(hitWindow50 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow50 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); + double t3 = relevantCountOk * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); + return (t1 + t2 + t3) / (root2 * u * u); + } + + return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); + } + private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) { if (!score.Mods.Any(h => h is OsuModFlashlight)) From c4f1de46c1028af69f041666bbfc0f89d60a7e21 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 21 Jul 2022 20:51:01 -0400 Subject: [PATCH 03/30] Fix slidersHit being negative --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index b8b5a6a1e5..67f24ce158 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); int okCountOnCircles = Math.Min(countOk, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. int mehCountOnCircles = Math.Min(countMeh, attributes.HitCircleCount); - int slidersHit = attributes.SliderCount - countMiss; + int slidersHit = Math.Max(0, attributes.SliderCount - countMiss); // Derivative of erf(x) double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); From b82afaca69546ba3a2098e16dc19cb8f63ed3148 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sun, 24 Jul 2022 14:28:18 -0400 Subject: [PATCH 04/30] Remove 50s and change acc formula --- .../Difficulty/OsuPerformanceCalculator.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 67f24ce158..0c3cb72a67 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); - double accuracyValue = computeAccuracyValue(score, osuAttributes); + double accuracyValue = computeAccuracyValue(score); double flashlightValue = computeFlashlightValue(score, osuAttributes); double totalValue = Math.Pow( @@ -179,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty return speedValue; } - private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes) + private double computeAccuracyValue(ScoreInfo score) { if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; - double accuracyValue = 800 * Math.Exp(-deviation / 4); + double accuracyValue = 4407 * Math.Pow(deviation, -1.818); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -210,14 +210,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty double clockRate = track.Rate; double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; double root2 = Math.Sqrt(2); int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); - int okCountOnCircles = Math.Min(countOk, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. - int mehCountOnCircles = Math.Min(countMeh, attributes.HitCircleCount); + int inaccuracies = Math.Min(countOk + countMeh, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. int slidersHit = Math.Max(0, attributes.SliderCount - countMiss); // Derivative of erf(x) @@ -229,12 +227,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty { double t1 = -hitWindow50 * slidersHit * erfPrime(hitWindow50 / (root2 * u)) / SpecialFunctions.Erf(hitWindow50 / (root2 * u)); double t2 = -hitWindow300 * greatCountOnCircles * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t3 = mehCountOnCircles * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow50 * erfPrime(hitWindow50 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow50 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); - double t4 = okCountOnCircles * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); - return (t1 + t2 + t3 + t4) / (root2 * u * u); + double t4 = inaccuracies * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); + return (t1 + t2 + t4) / (root2 * u * u); } - return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); + return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); } private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) @@ -247,7 +244,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double clockRate = track.Rate; double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; double root2 = Math.Sqrt(2); @@ -264,12 +260,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty double logLikelihoodGradient(double u) { double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t2 = relevantCountMeh * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow50 * erfPrime(hitWindow50 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow50 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); - double t3 = relevantCountOk * (-hitWindow100 * erfPrime(hitWindow100 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow100 / (root2 * u))); - return (t1 + t2 + t3) / (root2 * u * u); + double t2 = (relevantCountOk + relevantCountMeh) * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); + return (t1 + t2) / (root2 * u * u); } - return Brent.FindRootExpand(logLikelihoodGradient, 4, 20, 1e-6, expandFactor: 2); + return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); } private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) From d42c62d70faac55e698a5da91fd781a76047f24c Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sat, 6 Aug 2022 21:41:08 -0400 Subject: [PATCH 05/30] Change acc formula; remove constants from acc scaling --- .../Difficulty/OsuPerformanceCalculator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0c3cb72a67..e8a10b211f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } // Scale aim value with deviation - aimValue *= 4169 / 4050.0 * SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); + aimValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); return aimValue; } @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } // Scale the speed value with speed deviation - speedValue *= 120.289 / 108 * Math.Pow(SpecialFunctions.Erf(26 / (Math.Sqrt(2) * speedDeviation)), 2); + speedValue *= Math.Pow(SpecialFunctions.Erf(26 / (Math.Sqrt(2) * speedDeviation)), 2); return speedValue; } @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; - double accuracyValue = 4407 * Math.Pow(deviation, -1.818); + double accuracyValue = 763.087 * Math.Exp(-0.230237 * deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) From c998a7c94f0f9153bb673ea79e5e93751ba7f5ff Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sat, 6 Aug 2022 21:41:19 -0400 Subject: [PATCH 06/30] Buff speed to account for the constant removal --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index a156726f94..02f436a802 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1375; + private double skillMultiplier => 1475; private double strainDecayBase => 0.3; private double currentStrain; From c4094d10b221ba053cdaadcd9c9f8c3fbfe20505 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Tue, 9 Aug 2022 13:37:39 -0400 Subject: [PATCH 07/30] Check for maps with no objects or plays with no hits --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index e8a10b211f..da6046c178 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAccuracyValue(ScoreInfo score) { - if (score.Mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0) return 0.0; double accuracyValue = 763.087 * Math.Exp(-0.230237 * deviation); From e39705b94a865a9ad76478ce04da76ef2f1f2817 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Wed, 10 Aug 2022 20:55:34 -0400 Subject: [PATCH 08/30] Harshen speed scaling for lower UR, ease for higher UR; add comments and rearrange code --- .../Difficulty/OsuPerformanceCalculator.cs | 148 ++++++++++-------- 1 file changed, 80 insertions(+), 68 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index da6046c178..9912ea92b4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } // Scale the speed value with speed deviation - speedValue *= Math.Pow(SpecialFunctions.Erf(26 / (Math.Sqrt(2) * speedDeviation)), 2); + speedValue *= SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)); return speedValue; } @@ -184,6 +184,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0) return 0.0; + // This formula is based on the previous accuracy formula to keep values similar, but it caps SS pp. + // Eventually, this should be changed to a power law to make SS pp uncapped. double accuracyValue = 763.087 * Math.Exp(-0.230237 * deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. @@ -200,73 +202,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty return accuracyValue; } - private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) - { - if (totalSuccessfulHits == 0) - return double.PositiveInfinity; - - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; - - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - - double root2 = Math.Sqrt(2); - - int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); - int inaccuracies = Math.Min(countOk + countMeh, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. - int slidersHit = Math.Max(0, attributes.SliderCount - countMiss); - - // Derivative of erf(x) - double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); - - // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, - // which is the same as finding the zero of the derivative of the log-likelihood function. - double logLikelihoodGradient(double u) - { - double t1 = -hitWindow50 * slidersHit * erfPrime(hitWindow50 / (root2 * u)) / SpecialFunctions.Erf(hitWindow50 / (root2 * u)); - double t2 = -hitWindow300 * greatCountOnCircles * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t4 = inaccuracies * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); - return (t1 + t2 + t4) / (root2 * u * u); - } - - return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); - } - - private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) - { - if (totalSuccessfulHits == 0) - return double.PositiveInfinity; - - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; - - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double root2 = Math.Sqrt(2); - - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; - double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)) + 1; - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - - // Derivative of erf(x) - double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); - - // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, - // which is the same as finding the zero of the derivative of the log-likelihood function. - double logLikelihoodGradient(double u) - { - double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t2 = (relevantCountOk + relevantCountMeh) * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); - return (t1 + t2) / (root2 * u * u); - } - - return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); - } - private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) { if (!score.Mods.Any(h => h is OsuModFlashlight)) @@ -315,6 +250,83 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, comboBasedMissCount); } + /// + /// Estimates the player's tap deviation based on the OD, number of circles and sliders, and number of 300s, 100s, 50s, and misses, + /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings + /// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming, + /// and 50s are grouped with 100s since they are usually due to misreading. Inaccuracies are capped to the number of circles in the map. + /// + private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return double.PositiveInfinity; + + // Create a new track to properly calculate the hit windows of 100s and 50s. + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + + double root2 = Math.Sqrt(2); + + int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); + int inaccuracies = Math.Min(countOk + countMeh, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. + int slidersHit = Math.Max(0, attributes.SliderCount - countMiss); + + double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); // Derivative of erf(x). + + // To find the deviation, we have to maximize the log-likelihood function, + // which is the same as finding the zero of the derivative of the log-likelihood function. + double logLikelihoodGradient(double u) + { + double t1 = -hitWindow50 * slidersHit * erfPrime(hitWindow50 / (root2 * u)) / SpecialFunctions.Erf(hitWindow50 / (root2 * u)); + double t2 = -hitWindow300 * greatCountOnCircles * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); + double t4 = inaccuracies * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); + return (t1 + t2 + t4) / (root2 * u * u); + } + + return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); + } + + /// + /// Does the same as , but only for notes and inaccuracies that are relevant to speed difficulty. + /// Treats all difficulty speed notes as circles, so this method can sometimes return a lower deviation than . + /// + private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return double.PositiveInfinity; + + var track = new TrackVirtual(10000); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double root2 = Math.Sqrt(2); + + double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); + double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)) + 1; + double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); + + // Derivative of erf(x) + double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); + + // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, + // which is the same as finding the zero of the derivative of the log-likelihood function. + double logLikelihoodGradient(double u) + { + double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); + double t2 = (relevantCountOk + relevantCountMeh) * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); + return (t1 + t2) / (root2 * u * u); + } + + return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); + } + private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; From 0b3f8687cfe2d190c5cac07def4c4d6c6dc6b69d Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 11 Aug 2022 14:06:46 -0400 Subject: [PATCH 09/30] Add master changes --- .../Difficulty/OsuPerformanceCalculator.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 9912ea92b4..11cbd54e26 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { + public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; + private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -50,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty deviation = calculateDeviation(score, osuAttributes); speedDeviation = calculateSpeedDeviation(score, osuAttributes); - double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + double multiplier = PERFORMANCE_BASE_MULTIPLIER; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. if (score.Mods.Any(m => m is OsuModNoFail)) multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); @@ -60,10 +62,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) { - // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. - effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits); + // https://www.desmos.com/calculator/bc9eybdthb + // we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0 + // this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11) + double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0); + double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0); - multiplier *= 0.6; + // As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it. + effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } double aimValue = computeAimValue(score, osuAttributes); @@ -114,7 +120,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); else if (attributes.ApproachRate < 8.0) - approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate); + approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate); + + if (score.Mods.Any(h => h is OsuModRelax)) + approachRateFactor = 0.0; aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. @@ -144,6 +153,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (score.Mods.Any(h => h is OsuModRelax)) + return 0.0; + double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + From 64da4884d0665a5a641e4ee12350a5baa6270c79 Mon Sep 17 00:00:00 2001 From: 02naitsirk Date: Wed, 24 Aug 2022 11:46:50 -0400 Subject: [PATCH 10/30] Revert master multiplier buffs --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 11cbd54e26..98e72c4aae 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; + public const double PERFORMANCE_BASE_MULTIPLIER = 1.12; private double accuracy; private int scoreMaxCombo; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 38e0e5b677..9b1fbf9a2e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55; + private double skillMultiplier => 23.25; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); From 1a8a500196461b023a10d58a838def13300c85e8 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Tue, 30 Aug 2022 15:49:20 -0400 Subject: [PATCH 11/30] Scale flashlight with deviation --- .../Difficulty/OsuPerformanceCalculator.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 98e72c4aae..0b88a94756 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -236,10 +236,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); - // Scale the flashlight value with accuracy _slightly_. - flashlightValue *= 0.5 + accuracy / 2.0; - // It is important to also consider accuracy difficulty when doing that. - flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; + // Scale the flashlight value with deviation + flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); return flashlightValue; } From 498ce7edea77c4877dd6a98f8aff3760dd32db58 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sun, 30 Oct 2022 23:30:13 -0400 Subject: [PATCH 12/30] Multiply skill multipliers by highest old acc scaling --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 38e0e5b677..be08a629d6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 23.55; + private double skillMultiplier => 24; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 2bdfd05572..d7cb6b054d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1475; + private double skillMultiplier => 1480; private double strainDecayBase => 0.3; private double currentStrain; From 716bd0d105b6f02ce716b5354b837d3c99f3d0e1 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sun, 30 Oct 2022 23:30:36 -0400 Subject: [PATCH 13/30] Simplify deviation calculation and change acc pp formula --- .../Difficulty/OsuPerformanceCalculator.cs | 71 ++++++++----------- 1 file changed, 29 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a363acf966..aa9a44517e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Linq; using MathNet.Numerics; -using MathNet.Numerics.RootFinding; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Difficulty; @@ -140,7 +139,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - // Scale aim value with deviation aimValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); return aimValue; @@ -191,9 +189,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0) return 0.0; - // This formula is based on the previous accuracy formula to keep values similar, but it caps SS pp. - // Eventually, this should be changed to a power law to make SS pp uncapped. - double accuracyValue = 763.087 * Math.Exp(-0.230237 * deviation); + double accuracyValue = 95 * Math.Pow(8 / deviation, 1.5); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -204,8 +200,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; - accuracyValue *= 1 - (double)countMiss / totalHits; - return accuracyValue; } @@ -263,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (totalSuccessfulHits == 0) return double.PositiveInfinity; - // Create a new track to properly calculate the hit windows of 100s and 50s. + // Create a new track to properly calculate the hit windows of 50s. var track = new TrackVirtual(10000); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); double clockRate = track.Rate; @@ -271,62 +265,55 @@ namespace osu.Game.Rulesets.Osu.Difficulty double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double root2 = Math.Sqrt(2); + int greatCountOnCircles = attributes.HitCircleCount - countOk - countMeh - countMiss; - int greatCountOnCircles = Math.Max(0, attributes.HitCircleCount - countOk - countMeh - countMiss); - int inaccuracies = Math.Min(countOk + countMeh, attributes.HitCircleCount) + 1; // Add one 100 to process SS scores. - int slidersHit = Math.Max(0, attributes.SliderCount - countMiss); + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction / bayesian prior. + double greatProbabilityCircle = Math.Max(0, greatCountOnCircles / (attributes.HitCircleCount + 1.0)); + double greatProbabilitySlider; - double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); // Derivative of erf(x). - - // To find the deviation, we have to maximize the log-likelihood function, - // which is the same as finding the zero of the derivative of the log-likelihood function. - double logLikelihoodGradient(double u) + if (greatCountOnCircles < 0) { - double t1 = -hitWindow50 * slidersHit * erfPrime(hitWindow50 / (root2 * u)) / SpecialFunctions.Erf(hitWindow50 / (root2 * u)); - double t2 = -hitWindow300 * greatCountOnCircles * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t4 = inaccuracies * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); - return (t1 + t2 + t4) / (root2 * u * u); + int nonCircleMisses = -greatCountOnCircles; + greatProbabilitySlider = Math.Max(0, (attributes.SliderCount - nonCircleMisses) / (attributes.SliderCount + 1.0)); + } + else + { + greatProbabilitySlider = attributes.SliderCount / (attributes.SliderCount + 1.0); } - return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); + if (greatProbabilityCircle == 0 && greatProbabilitySlider == 0) + return double.PositiveInfinity; + + double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); + double deviationOnSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilitySlider)); + + return Math.Min(deviationOnCircles, deviationOnSliders); } /// /// Does the same as , but only for notes and inaccuracies that are relevant to speed difficulty. - /// Treats all difficulty speed notes as circles, so this method can sometimes return a lower deviation than . + /// Treats all difficult speed notes as circles, so this method can sometimes return a lower deviation than . + /// This is fine though, since this method is only used to scale speed pp. /// private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) { if (totalSuccessfulHits == 0) return double.PositiveInfinity; - var track = new TrackVirtual(10000); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double root2 = Math.Sqrt(2); double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)) + 1; - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - // Derivative of erf(x) - double erfPrime(double x) => 2 / Math.Sqrt(Math.PI) * Math.Exp(-x * x); + if (relevantCountGreat == 0) + return double.PositiveInfinity; - // Let f(x) = erf(x). To find the deviation, we have to maximize the log-likelihood function, - // which is the same as finding the zero of the derivative of the log-likelihood function. - double logLikelihoodGradient(double u) - { - double t1 = -hitWindow300 * relevantCountGreat * erfPrime(hitWindow300 / (root2 * u)) / SpecialFunctions.Erf(hitWindow300 / (root2 * u)); - double t2 = (relevantCountOk + relevantCountMeh) * (-hitWindow50 * erfPrime(hitWindow50 / (root2 * u)) + hitWindow300 * erfPrime(hitWindow300 / (root2 * u))) / (SpecialFunctions.Erfc(hitWindow300 / (root2 * u)) - SpecialFunctions.Erfc(hitWindow50 / (root2 * u))); - return (t1 + t2) / (root2 * u * u); - } + double greatProbability = relevantCountGreat / (attributes.SpeedNoteCount + 1); + double deviationOnSpeedCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbability)); - return Brent.FindRootExpand(logLikelihoodGradient, 3, 20, 1e-6, expandFactor: 2); + return deviationOnSpeedCircles; } private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); From 90b36b6fab51f1be20933b8b9f72acb87ee957a9 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sun, 30 Oct 2022 23:36:48 -0400 Subject: [PATCH 14/30] Set base pp multiplier to 1.14 --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0d28631430..42a7ff76a7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public const double PERFORMANCE_BASE_MULTIPLIER = 1.12; + public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; private double accuracy; private int scoreMaxCombo; From 176196d5c8b4c7d0c53124504a571b131483d74c Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Wed, 16 Nov 2022 21:44:36 -0500 Subject: [PATCH 15/30] Test --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 42a7ff76a7..5a97450328 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0) return 0.0; - double accuracyValue = 95 * Math.Pow(8 / deviation, 1.5); + double accuracyValue = 477.793 * Math.Exp(-0.197612 * deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) From db1e2f3f571eb72d39d1bd723e119671572ddf84 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Mon, 10 Jul 2023 21:44:31 -0400 Subject: [PATCH 16/30] Test (LB, no length AR scale) --- .../Difficulty/OsuPerformanceCalculator.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 5a97450328..7236124f6c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); - double accuracyValue = computeAccuracyValue(score); + double accuracyValue = computeAccuracyValue(score, osuAttributes); double flashlightValue = computeFlashlightValue(score, osuAttributes); double totalValue = Math.Pow( @@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; - aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + aimValue *= 1.0 + approachRateFactor; if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); @@ -139,8 +139,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - aimValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); - return aimValue; } @@ -165,7 +163,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. + speedValue *= 1.0 + approachRateFactor; if (score.Mods.Any(m => m is OsuModBlinds)) { @@ -184,12 +182,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty return speedValue; } - private double computeAccuracyValue(ScoreInfo score) + private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes) { if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0) return 0.0; - double accuracyValue = 477.793 * Math.Exp(-0.197612 * deviation); + double od = attributes.OverallDifficulty; + double n = attributes.HitCircleCount; + + double liveLengthBonus = Math.Min(1.15, Math.Pow(n / 1000, 0.3)); // Should eventually be removed. + + // Some fancy stuff to ensure SS values stay the same. + double scaling = -Math.Sqrt(2) * SpecialFunctions.ErfInv(n / (n + 1)) * Math.Log(Math.Pow(1.52163, od - 40.0 / 3) * liveLengthBonus) / (80 - 6 * od); + + // Accuracy pp formula that's roughly the same as live. + double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * Math.Exp(-scaling * deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) From c7a769e47474da78e8a73f308ad3e48470c872d0 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Tue, 11 Jul 2023 16:10:04 -0400 Subject: [PATCH 17/30] Fix scaling + don't change SR --- .../Difficulty/OsuPerformanceCalculator.cs | 18 +++++++++++------- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 2 +- .../Difficulty/Skills/Speed.cs | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 7236124f6c..eee77f3b30 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -139,6 +139,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } + aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. + return aimValue; } @@ -179,24 +181,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with speed deviation speedValue *= SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)); + speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. + return speedValue; } private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0) + int hitCircleCount = attributes.HitCircleCount; + + if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0 || hitCircleCount == 0) return 0.0; - double od = attributes.OverallDifficulty; - double n = attributes.HitCircleCount; - - double liveLengthBonus = Math.Min(1.15, Math.Pow(n / 1000, 0.3)); // Should eventually be removed. + double liveLengthBonus = Math.Min(1.15, Math.Pow(hitCircleCount / 1000.0, 0.3)); // Should eventually be removed. + double threshold = 1000 * Math.Pow(1.15, 1 / 0.3); // Number of objects until length bonus caps. // Some fancy stuff to ensure SS values stay the same. - double scaling = -Math.Sqrt(2) * SpecialFunctions.ErfInv(n / (n + 1)) * Math.Log(Math.Pow(1.52163, od - 40.0 / 3) * liveLengthBonus) / (80 - 6 * od); + double scaling = Math.Sqrt(2) * Math.Log(1.52163) * SpecialFunctions.ErfInv(1 / (1 + 1 / Math.Min(hitCircleCount, threshold))) / 6; // Accuracy pp formula that's roughly the same as live. - double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * Math.Exp(-scaling * deviation); + double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index be08a629d6..38e0e5b677 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; - private double skillMultiplier => 24; + private double skillMultiplier => 23.55; private double strainDecayBase => 0.15; private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index d7cb6b054d..efe0e136bf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// public class Speed : OsuStrainSkill { - private double skillMultiplier => 1480; + private double skillMultiplier => 1375; private double strainDecayBase => 0.3; private double currentStrain; From df8f7e968fb40ba9944aeff0dcc7adad47eb0eb9 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Wed, 12 Jul 2023 14:40:02 -0400 Subject: [PATCH 18/30] Normalize to OD 10 test (with AR length bonus, live aim and speed scaling) --- .../Difficulty/OsuPerformanceCalculator.cs | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index eee77f3b30..06d8e18ae9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; + private const double od_to_normalize_into = 10; + private const double normalized_hit_window300 = 80 - 6 * od_to_normalize_into; + private const double normalized_hit_window100 = 140 - 8 * od_to_normalize_into; + private const double normalized_hit_window50 = 200 - 10 * od_to_normalize_into; + private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -119,7 +124,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; - aimValue *= 1.0 + approachRateFactor; + aimValue *= 1.0 + approachRateFactor * lengthBonus; if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); @@ -139,7 +144,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. + aimValue *= 0.98 + Math.Pow(od_to_normalize_into, 2) / 2500; + + double deviationRoot2 = Math.Sqrt(2) * deviation; + double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); + + aimValue *= accuracyOnNormalizedOd; return aimValue; } @@ -165,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - speedValue *= 1.0 + approachRateFactor; + speedValue *= 1.0 + approachRateFactor * lengthBonus; if (score.Mods.Any(m => m is OsuModBlinds)) { @@ -178,10 +190,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - // Scale the speed value with speed deviation - speedValue *= SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)); + double deviationRoot2 = Math.Sqrt(2) * deviation; + double speedDeviationRoot2 = Math.Sqrt(2) * speedDeviation; - speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. + double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); + + double speedAccuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / speedDeviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / speedDeviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / speedDeviationRoot2); + + speedValue *= (0.95 + Math.Pow(od_to_normalize_into, 2) / 750) * Math.Pow((accuracyOnNormalizedOd + speedAccuracyOnNormalizedOd) / 2, (14.5 - Math.Max(8, od_to_normalize_into)) / 2); return speedValue; } @@ -194,13 +214,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty return 0.0; double liveLengthBonus = Math.Min(1.15, Math.Pow(hitCircleCount / 1000.0, 0.3)); // Should eventually be removed. - double threshold = 1000 * Math.Pow(1.15, 1 / 0.3); // Number of objects until length bonus caps. - // Some fancy stuff to ensure SS values stay the same. - double scaling = Math.Sqrt(2) * Math.Log(1.52163) * SpecialFunctions.ErfInv(1 / (1 + 1 / Math.Min(hitCircleCount, threshold))) / 6; + double deviationRoot2 = Math.Sqrt(2) * deviation; + double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); - // Accuracy pp formula that's roughly the same as live. - double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * deviation); + // Accuracy pp formula that's the same as live. + double accuracyValue = 2.83 * Math.Pow(1.52163, od_to_normalize_into) * liveLengthBonus * Math.Pow(accuracyOnNormalizedOd, 24); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -231,8 +252,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); - // Scale the flashlight value with deviation - flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); + flashlightValue *= 0.98 + Math.Pow(od_to_normalize_into, 2) / 2500; + + double deviationRoot2 = Math.Sqrt(2) * deviation; + double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + + 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); + + flashlightValue *= 0.5 + accuracyOnNormalizedOd / 2.0; return flashlightValue; } From cb03df22f8d1e1535456039ce2e19f127722baca Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 3 Aug 2023 15:47:01 -0400 Subject: [PATCH 19/30] Revert "Normalize to OD 10 test (with AR length bonus, live aim and speed scaling)" This reverts commit df8f7e968fb40ba9944aeff0dcc7adad47eb0eb9. --- .../Difficulty/OsuPerformanceCalculator.cs | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 06d8e18ae9..eee77f3b30 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -21,11 +21,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; - private const double od_to_normalize_into = 10; - private const double normalized_hit_window300 = 80 - 6 * od_to_normalize_into; - private const double normalized_hit_window100 = 140 - 8 * od_to_normalize_into; - private const double normalized_hit_window50 = 200 - 10 * od_to_normalize_into; - private double accuracy; private int scoreMaxCombo; private int countGreat; @@ -124,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; - aimValue *= 1.0 + approachRateFactor * lengthBonus; + aimValue *= 1.0 + approachRateFactor; if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); @@ -144,14 +139,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - aimValue *= 0.98 + Math.Pow(od_to_normalize_into, 2) / 2500; - - double deviationRoot2 = Math.Sqrt(2) * deviation; - double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); - - aimValue *= accuracyOnNormalizedOd; + aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. return aimValue; } @@ -177,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - speedValue *= 1.0 + approachRateFactor * lengthBonus; + speedValue *= 1.0 + approachRateFactor; if (score.Mods.Any(m => m is OsuModBlinds)) { @@ -190,18 +178,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - double deviationRoot2 = Math.Sqrt(2) * deviation; - double speedDeviationRoot2 = Math.Sqrt(2) * speedDeviation; + // Scale the speed value with speed deviation + speedValue *= SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)); - double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); - - double speedAccuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / speedDeviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / speedDeviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / speedDeviationRoot2); - - speedValue *= (0.95 + Math.Pow(od_to_normalize_into, 2) / 750) * Math.Pow((accuracyOnNormalizedOd + speedAccuracyOnNormalizedOd) / 2, (14.5 - Math.Max(8, od_to_normalize_into)) / 2); + speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. return speedValue; } @@ -214,14 +194,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty return 0.0; double liveLengthBonus = Math.Min(1.15, Math.Pow(hitCircleCount / 1000.0, 0.3)); // Should eventually be removed. + double threshold = 1000 * Math.Pow(1.15, 1 / 0.3); // Number of objects until length bonus caps. - double deviationRoot2 = Math.Sqrt(2) * deviation; - double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); + // Some fancy stuff to ensure SS values stay the same. + double scaling = Math.Sqrt(2) * Math.Log(1.52163) * SpecialFunctions.ErfInv(1 / (1 + 1 / Math.Min(hitCircleCount, threshold))) / 6; - // Accuracy pp formula that's the same as live. - double accuracyValue = 2.83 * Math.Pow(1.52163, od_to_normalize_into) * liveLengthBonus * Math.Pow(accuracyOnNormalizedOd, 24); + // Accuracy pp formula that's roughly the same as live. + double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -252,14 +231,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); - flashlightValue *= 0.98 + Math.Pow(od_to_normalize_into, 2) / 2500; - - double deviationRoot2 = Math.Sqrt(2) * deviation; - double accuracyOnNormalizedOd = 2.0 / 3 * SpecialFunctions.Erf(normalized_hit_window300 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window100 / deviationRoot2) + - 1.0 / 6 * SpecialFunctions.Erf(normalized_hit_window50 / deviationRoot2); - - flashlightValue *= 0.5 + accuracyOnNormalizedOd / 2.0; + // Scale the flashlight value with deviation + flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); return flashlightValue; } From b9eda3e0db927e9f5cde173acab9dbb4564d1fcd Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 3 Aug 2023 21:09:51 -0400 Subject: [PATCH 20/30] Improve deviation calc, make deviation nullable --- .../Difficulty/OsuPerformanceAttributes.cs | 2 +- .../Difficulty/OsuPerformanceCalculator.cs | 78 +++++++++++++------ 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs index b791f473d5..6f92c92ce9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceAttributes.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double EffectiveMissCount { get; set; } [JsonProperty("deviation")] - public double Deviation { get; set; } + public double? Deviation { get; set; } [JsonProperty("speed_deviation")] public double SpeedDeviation { get; set; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index eee77f3b30..2fc21e9311 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMiss; private double effectiveMissCount; - private double deviation; + private double? deviation; private double speedDeviation; public OsuPerformanceCalculator() @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { int hitCircleCount = attributes.HitCircleCount; - if (score.Mods.Any(h => h is OsuModRelax) || totalSuccessfulHits == 0 || hitCircleCount == 0) + if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) return 0.0; double liveLengthBonus = Math.Min(1.15, Math.Pow(hitCircleCount / 1000.0, 0.3)); // Should eventually be removed. @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double scaling = Math.Sqrt(2) * Math.Log(1.52163) * SpecialFunctions.ErfInv(1 / (1 + 1 / Math.Min(hitCircleCount, threshold))) / 6; // Accuracy pp formula that's roughly the same as live. - double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * deviation); + double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * (double)deviation); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (!score.Mods.Any(h => h is OsuModFlashlight)) + if (!score.Mods.Any(h => h is OsuModFlashlight) || deviation == null) return 0.0; double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0; @@ -232,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); // Scale the flashlight value with deviation - flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * deviation)); + flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * (double)deviation)); return flashlightValue; } @@ -261,44 +261,72 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming, /// and 50s are grouped with 100s since they are usually due to misreading. Inaccuracies are capped to the number of circles in the map. /// - private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + private double? calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) { if (totalSuccessfulHits == 0) - return double.PositiveInfinity; + return null; // Create a new track to properly calculate the hit windows of 50s. - var track = new TrackVirtual(10000); + var track = new TrackVirtual(1); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); double clockRate = track.Rate; double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - int greatCountOnCircles = attributes.HitCircleCount - countOk - countMeh - countMiss; + int circleCount = attributes.HitCircleCount; + int missCountCircles = Math.Min(countMiss, circleCount); + int mehCountCircles = Math.Min(countMeh, circleCount - missCountCircles); + int okCountCircles = Math.Min(countOk, circleCount - missCountCircles - mehCountCircles); + int greatCountCircles = Math.Max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles); - // The probability that a player hits a circle is unknown, but we can estimate it to be - // the number of greats on circles divided by the number of circles, and then add one - // to the number of circles as a bias correction / bayesian prior. - double greatProbabilityCircle = Math.Max(0, greatCountOnCircles / (attributes.HitCircleCount + 1.0)); - double greatProbabilitySlider; + // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, + // compute the deviation on circles. - if (greatCountOnCircles < 0) + if (greatCountCircles > 0) { - int nonCircleMisses = -greatCountOnCircles; - greatProbabilitySlider = Math.Max(0, (attributes.SliderCount - nonCircleMisses) / (attributes.SliderCount + 1.0)); - } - else - { - greatProbabilitySlider = attributes.SliderCount / (attributes.SliderCount + 1.0); + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + + double greatProbabilityCircle = greatCountCircles / (circleCount - missCountCircles - mehCountCircles + 1.0); + + // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. + // Begin with the normal distribution first. + + double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); + deviationOnCircles *= Math.Sqrt(1 - Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviationOnCircles, 2)) + / (deviationOnCircles * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviationOnCircles)))); + + // Then compute the variance for 50s. + double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; + + // Find the total deviation. + deviationOnCircles = Math.Sqrt(((greatCountCircles + okCountCircles) * Math.Pow(deviationOnCircles, 2) + mehCountCircles * mehVariance) / (greatCountCircles + okCountCircles + mehCountCircles)); + + return deviationOnCircles; } - if (greatProbabilityCircle == 0 && greatProbabilitySlider == 0) - return double.PositiveInfinity; + // If there are more non-300s than there are circles, compute the deviation on sliders instead. + // Here, all that matters is whether or not the slider was missed, since it is impossible + // to get a 100 or 50 on a slider by mis-tapping it. - double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); + int sliderCount = attributes.SliderCount; + int missCountSliders = Math.Min(sliderCount, countMiss - missCountCircles); + int greatCountSliders = sliderCount - missCountSliders; + + // We only get here if nothing was hit. In this case, there is no estimate for deviation. + // Note that this is never negative, so checking if this is only equal to 0 makes sense. + if (greatCountSliders == 0) + { + return null; + } + + double greatProbabilitySlider = greatCountSliders / (sliderCount + 1.0); double deviationOnSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilitySlider)); - return Math.Min(deviationOnCircles, deviationOnSliders); + return deviationOnSliders; } /// From e22f5a669ee5844d411084653e34d4a3d2d5ba71 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 3 Aug 2023 21:34:21 -0400 Subject: [PATCH 21/30] Change comments and whitespace --- .../Difficulty/OsuPerformanceCalculator.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 2fc21e9311..edb564606f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -258,15 +258,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// /// Estimates the player's tap deviation based on the OD, number of circles and sliders, and number of 300s, 100s, 50s, and misses, /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings - /// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming, - /// and 50s are grouped with 100s since they are usually due to misreading. Inaccuracies are capped to the number of circles in the map. + /// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming. + /// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution. /// private double? calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) { if (totalSuccessfulHits == 0) return null; - // Create a new track to properly calculate the hit windows of 50s. + // Create a new track to properly calculate the hit windows of 100s and 50s. var track = new TrackVirtual(1); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); double clockRate = track.Rate; @@ -283,18 +283,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, // compute the deviation on circles. - if (greatCountCircles > 0) { // The probability that a player hits a circle is unknown, but we can estimate it to be // the number of greats on circles divided by the number of circles, and then add one // to the number of circles as a bias correction. - double greatProbabilityCircle = greatCountCircles / (circleCount - missCountCircles - mehCountCircles + 1.0); // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. // Begin with the normal distribution first. - double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); deviationOnCircles *= Math.Sqrt(1 - Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviationOnCircles, 2)) / (deviationOnCircles * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviationOnCircles)))); @@ -311,7 +308,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty // If there are more non-300s than there are circles, compute the deviation on sliders instead. // Here, all that matters is whether or not the slider was missed, since it is impossible // to get a 100 or 50 on a slider by mis-tapping it. - int sliderCount = attributes.SliderCount; int missCountSliders = Math.Min(sliderCount, countMiss - missCountCircles); int greatCountSliders = sliderCount - missCountSliders; From 16963a5cf20987dab0c8c222e2d6f38fba8a1b19 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Tue, 8 Aug 2023 14:08:50 -0400 Subject: [PATCH 22/30] Scale acc pp with miss count --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index bc0360b2fd..64ac83bf6d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -209,6 +209,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; + accuracyValue *= Math.Pow(0.97, Math.Max(0, effectiveMissCount - 1)); // Penalize accuracy pp after the first miss. + return accuracyValue; } From 251760dd9f73bff2a6b9402407c895e85ecafcb4 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Mon, 16 Oct 2023 12:03:30 -0400 Subject: [PATCH 23/30] Consider 50s in speedDeviation, remove some nerfs --- .../Difficulty/Evaluators/SpeedEvaluator.cs | 4 -- .../Difficulty/OsuPerformanceCalculator.cs | 44 +++++++++++++++---- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs index 2df383aaa8..a733d381f5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/SpeedEvaluator.cs @@ -46,10 +46,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators doubletapness = Math.Pow(speedRatio, 1 - windowRatio); } - // Cap deltatime to the OD 300 hitwindow. - // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. - strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1); - // derive speedBonus for calculation double speedBonus = 1.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 64ac83bf6d..b0fe96dc64 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -209,8 +209,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; - accuracyValue *= Math.Pow(0.97, Math.Max(0, effectiveMissCount - 1)); // Penalize accuracy pp after the first miss. - return accuracyValue; } @@ -335,18 +333,48 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (totalSuccessfulHits == 0) return double.PositiveInfinity; - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + // Create a new track to properly calculate the hit windows of 100s and 50s. + var track = new TrackVirtual(1); + score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); + double clockRate = track.Rate; + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + + // Calculate accuracy assuming the worst case scenario + double speedNoteCount = attributes.SpeedNoteCount; double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); + double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); + double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); + double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh)); - if (relevantCountGreat == 0) - return double.PositiveInfinity; + // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, + // compute the deviation on circles. + if (relevantCountGreat > 0) + { + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + double greatProbabilityCircle = relevantCountGreat / (speedNoteCount - relevantCountMiss - relevantCountMeh + 1.0); - double greatProbability = relevantCountGreat / (attributes.SpeedNoteCount + 1); - double deviationOnSpeedCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbability)); + // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. + // Begin with the normal distribution first. + double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); + deviationOnCircles *= Math.Sqrt(1 - Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviationOnCircles, 2)) + / (deviationOnCircles * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviationOnCircles)))); - return deviationOnSpeedCircles; + // Then compute the variance for 50s. + double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; + + // Find the total deviation. + deviationOnCircles = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviationOnCircles, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); + + return deviationOnCircles; + } + + return double.PositiveInfinity; } private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); From 7bd61db424aa93ce04aa4ab810e97d238b780384 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 19 Oct 2023 23:04:39 -0400 Subject: [PATCH 24/30] Aggressive test --- .../Difficulty/OsuPerformanceCalculator.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index b0fe96dc64..88b5a617f3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (score.Mods.Any(h => h is OsuModRelax)) + if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) return 0.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; @@ -177,9 +177,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty } // Scale the speed value with speed deviation - speedValue *= SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)); + double accOd10Speed = 2.0 / 3 * SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)) + + 1.0 / 6 * SpecialFunctions.Erf(60 / (Math.Sqrt(2) * speedDeviation)) + + 1.0 / 6 * SpecialFunctions.Erf(100 / (Math.Sqrt(2) * speedDeviation)); speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. + speedValue *= Math.Pow(accOd10Speed, 2); return speedValue; } @@ -191,14 +194,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) return 0.0; - double liveLengthBonus = Math.Min(1.15, Math.Pow(hitCircleCount / 1000.0, 0.3)); // Should eventually be removed. - double threshold = 1000 * Math.Pow(1.15, 1 / 0.3); // Number of objects until length bonus caps. - - // Some fancy stuff to ensure SS values stay the same. - double scaling = Math.Sqrt(2) * Math.Log(1.52163) * SpecialFunctions.ErfInv(1 / (1 + 1 / Math.Min(hitCircleCount, threshold))) / 6; - - // Accuracy pp formula that's roughly the same as live. - double accuracyValue = 2.83 * Math.Pow(1.52163, 40.0 / 3) * liveLengthBonus * Math.Exp(-scaling * (double)deviation); + double accuracyValue = 75 * Math.Pow(7.5 / (double)deviation, 2); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) From 078df533fbdee6a7536da8715f66dc6f3253b7be Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Sat, 21 Oct 2023 13:05:47 -0400 Subject: [PATCH 25/30] Anti-raketap speed curve --- .../Difficulty/OsuPerformanceCalculator.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 88b5a617f3..ec077f9f9f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -176,13 +176,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - // Scale the speed value with speed deviation - double accOd10Speed = 2.0 / 3 * SpecialFunctions.Erf(20 / (Math.Sqrt(2) * speedDeviation)) - + 1.0 / 6 * SpecialFunctions.Erf(60 / (Math.Sqrt(2) * speedDeviation)) - + 1.0 / 6 * SpecialFunctions.Erf(100 / (Math.Sqrt(2) * speedDeviation)); - speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. - speedValue *= Math.Pow(accOd10Speed, 2); + + // Scale the speed value with speed deviation. + // Constants obtained with regression. + speedValue *= Math.Exp(1 - Math.Cosh(Math.Pow(speedDeviation / 18.8, 1.9))); return speedValue; } From 04d1f535e911ab0c99c644689ac7172518e7284b Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 9 Nov 2023 12:24:37 -0500 Subject: [PATCH 26/30] Make SNC not depend on distance; change summation --- .../Difficulty/Skills/Speed.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 40aac013ab..e2a13d3177 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -22,10 +22,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; private double currentRhythm; + private double currentStrainNoDistance; + protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; - private readonly List objectStrains = new List(); + private readonly List objectStrainsNoDistance = new List(); public Speed(Mod[] mods) : base(mods) @@ -41,26 +43,34 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; + // Disregard distance when computing the number of speed notes. + double travelDistance = current.Index > 0 ? ((OsuDifficultyHitObject)current.Previous(0)).TravelDistance : 0; + double distance = Math.Min(125, travelDistance + ((OsuDifficultyHitObject)current).MinimumJumpDistance); + + currentStrainNoDistance *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); + currentStrainNoDistance += SpeedEvaluator.EvaluateDifficultyOf(current) / (1 + Math.Pow(distance / 125, 3.5)) * skillMultiplier; + currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; + double totalStrainNoDistance = currentStrainNoDistance * currentRhythm; - objectStrains.Add(totalStrain); + objectStrainsNoDistance.Add(totalStrainNoDistance); return totalStrain; } public double RelevantNoteCount() { - if (objectStrains.Count == 0) + if (objectStrainsNoDistance.Count == 0) return 0; - double maxStrain = objectStrains.Max(); + double maxStrain = objectStrainsNoDistance.Max(); if (maxStrain == 0) return 0; - return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); + return objectStrainsNoDistance.Sum(strain => strain / maxStrain); } } } From e09beebe1de2fc64538606495d8597c36f9e3353 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 9 Nov 2023 12:25:02 -0500 Subject: [PATCH 27/30] Scale aim with deviation, change speed scaling a bit --- .../Difficulty/OsuPerformanceCalculator.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ec077f9f9f..2836af6b58 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); - double accuracyValue = computeAccuracyValue(score, osuAttributes); + double accuracyValue = computeAccuracyValue(score); double flashlightValue = computeFlashlightValue(score, osuAttributes); double totalValue = Math.Pow( @@ -96,6 +96,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { + if (deviation == null) + return 0; + double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + @@ -138,6 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. + aimValue *= 1 / (1 + Math.Pow((double)deviation / 30, 4)); // Scale the aim value with deviation. return aimValue; } @@ -177,18 +181,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty } speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. - - // Scale the speed value with speed deviation. - // Constants obtained with regression. - speedValue *= Math.Exp(1 - Math.Cosh(Math.Pow(speedDeviation / 18.8, 1.9))); + speedValue *= 1 / (1 + Math.Pow(speedDeviation / 20, 4)); // Scale the speed value with speed deviation. return speedValue; } - private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes) + private double computeAccuracyValue(ScoreInfo score) { - int hitCircleCount = attributes.HitCircleCount; - if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) return 0.0; From 5bae5d09ff599830ac6eea3351306804a6f9ba08 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Fri, 16 Feb 2024 12:30:26 -0500 Subject: [PATCH 28/30] Scale acc pp with an upper bound on deviation --- .../Difficulty/OsuPerformanceCalculator.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 2836af6b58..26ecb0ad0a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) return 0.0; - double accuracyValue = 75 * Math.Pow(7.5 / (double)deviation, 2); + double accuracyValue = 121 * Math.Pow(7.5 / (double)deviation, 2); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -247,7 +247,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } /// - /// Estimates the player's tap deviation based on the OD, number of circles and sliders, and number of 300s, 100s, 50s, and misses, + /// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders, and the hit judgements, /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings /// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming. /// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution. @@ -276,14 +276,18 @@ namespace osu.Game.Rulesets.Osu.Difficulty // compute the deviation on circles. if (greatCountCircles > 0) { - // The probability that a player hits a circle is unknown, but we can estimate it to be - // the number of greats on circles divided by the number of circles, and then add one - // to the number of circles as a bias correction. - double greatProbabilityCircle = greatCountCircles / (circleCount - missCountCircles - mehCountCircles + 1.0); + double n = circleCount - missCountCircles - mehCountCircles; + const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + + // Proportion of greats hit on circles, ignoring misses and 50s. + double p = greatCountCircles / n; + + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. - // Begin with the normal distribution first. - double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); + // Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than: + double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); deviationOnCircles *= Math.Sqrt(1 - Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviationOnCircles, 2)) / (deviationOnCircles * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviationOnCircles)))); From aeb7ecf07de96819eb25128c360306690e7fe450 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Wed, 14 Aug 2024 21:52:27 -0400 Subject: [PATCH 29/30] Not bad I think --- .../Difficulty/OsuPerformanceCalculator.cs | 175 ++++++++++-------- .../Difficulty/Skills/Speed.cs | 21 +-- 2 files changed, 99 insertions(+), 97 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ffc9582e13..fdb60879f2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public class OsuPerformanceCalculator : PerformanceCalculator { - public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; + public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. private double accuracy; private int scoreMaxCombo; @@ -27,8 +27,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMiss; private double effectiveMissCount; - private double? deviation; - private double speedDeviation; + + private double hitWindow300; + private double deviation, speedDeviation; public OsuPerformanceCalculator() : base(new OsuRuleset()) @@ -46,10 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); effectiveMissCount = calculateEffectiveMissCount(osuAttributes); - deviation = calculateDeviation(score, osuAttributes); - speedDeviation = calculateSpeedDeviation(score, osuAttributes); - double multiplier = PERFORMANCE_BASE_MULTIPLIER; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + double multiplier = PERFORMANCE_BASE_MULTIPLIER; if (score.Mods.Any(m => m is OsuModNoFail)) multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount); @@ -69,10 +68,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits); } + double clockRate = getClockRate(score); + hitWindow300 = 80 - 6 * osuAttributes.OverallDifficulty; + + deviation = calculateDeviation(score, osuAttributes); + speedDeviation = calculateSpeedDeviation(score, osuAttributes); + double aimValue = computeAimValue(score, osuAttributes); double speedValue = computeSpeedValue(score, osuAttributes); double accuracyValue = computeAccuracyValue(score); double flashlightValue = computeFlashlightValue(score, osuAttributes); + double totalValue = Math.Pow( Math.Pow(aimValue, 1.1) + @@ -96,8 +102,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (deviation == null) - return 0; + if (deviation == double.PositiveInfinity) + return 0.0; double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; @@ -120,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (score.Mods.Any(h => h is OsuModRelax)) approachRateFactor = 0.0; - aimValue *= 1.0 + approachRateFactor; + aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. if (score.Mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate); @@ -140,15 +146,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. - aimValue *= 1 / (1 + Math.Pow((double)deviation / 30, 4)); // Scale the aim value with deviation. + double prod = Math.Sqrt(attributes.AimDifficulty * attributes.SpeedDifficulty) * deviation; + aimValue *= Math.Pow(SpecialFunctions.Erf(130 / prod), 1.5); + aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11.1 SS stays the same. return aimValue; } private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) + if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == double.PositiveInfinity) return 0.0; double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; @@ -167,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (attributes.ApproachRate > 10.33) approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33); - speedValue *= 1.0 + approachRateFactor; + speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR. if (score.Mods.Any(m => m is OsuModBlinds)) { @@ -180,18 +187,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. - speedValue *= 1 / (1 + Math.Pow(speedDeviation / 20, 4)); // Scale the speed value with speed deviation. + double prod = attributes.SpeedDifficulty * speedDeviation; + speedValue *= Math.Pow(SpecialFunctions.Erf(67.5 / prod), 1.5); + speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11.1 SS stays the same. return speedValue; } private double computeAccuracyValue(ScoreInfo score) { - if (score.Mods.Any(h => h is OsuModRelax) || deviation == null) + if (score.Mods.Any(h => h is OsuModRelax)) return 0.0; - double accuracyValue = 121 * Math.Pow(7.5 / (double)deviation, 2); + double accuracyValue = 120 * Math.Pow(7.5 / deviation, 2); // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (score.Mods.Any(m => m is OsuModBlinds)) @@ -207,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes) { - if (!score.Mods.Any(h => h is OsuModFlashlight) || deviation == null) + if (!score.Mods.Any(h => h is OsuModFlashlight) || deviation == double.PositiveInfinity) return 0.0; double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0; @@ -223,7 +231,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); // Scale the flashlight value with deviation - flashlightValue *= SpecialFunctions.Erf(50 / (Math.Sqrt(2) * (double)deviation)); + flashlightValue *= SpecialFunctions.Erf(35 / deviation); + flashlightValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. return flashlightValue; } @@ -247,20 +256,72 @@ namespace osu.Game.Rulesets.Osu.Difficulty } /// - /// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders, and the hit judgements, - /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings - /// will always return the same deviation. Sliders are treated as circles with a 50 hit window. Misses are ignored because they are usually due to misaiming. - /// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution. + /// Using estimates player's deviation on speed notes, assuming worst-case. + /// Treats all speed notes as hit circles. This is not good way to do this, but fixing this is impossible under the limitation of current speed pp. + /// If score was set with slideracc - tries to remove mistaps on sliders from total mistaps. /// - private double? calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + /// + /// Does the same as , but only for notes and inaccuracies that are relevant to speed difficulty. + /// Treats all difficult speed notes as circles, so this method can sometimes return a lower deviation than . + /// This is fine though, since this method is only used to scale speed pp. + /// + private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) { if (totalSuccessfulHits == 0) - return null; + return double.PositiveInfinity; // Create a new track to properly calculate the hit windows of 100s and 50s. - var track = new TrackVirtual(1); - score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; + double clockRate = getClockRate(score); + + double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; + double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + + // Calculate accuracy assuming the worst case scenario + double speedNoteCount = attributes.SpeedNoteCount; + double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; + double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); + double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); + double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); + double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh)); + + // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, + // compute the deviation on circles. + if (relevantCountGreat > 0) + { + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + double greatProbabilityCircle = relevantCountGreat / (speedNoteCount - relevantCountMiss - relevantCountMeh + 1.0); + + // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. + // Begin with the normal distribution first. + double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); + + // Then compute the variance for 50s. + double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; + + // Find the total deviation. + deviationOnCircles = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviationOnCircles, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); + + return deviationOnCircles; + } + + return double.PositiveInfinity; + } + + /// + /// Estimates the player's tap deviation based on the OD, given number of 300s, 100s, 50s and misses, + /// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings + /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. + /// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution. + /// + private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return double.PositiveInfinity; + + double clockRate = getClockRate(score); double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; @@ -288,8 +349,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. // Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than: double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - deviationOnCircles *= Math.Sqrt(1 - Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviationOnCircles, 2)) - / (deviationOnCircles * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviationOnCircles)))); // Then compute the variance for 50s. double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; @@ -311,7 +370,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Note that this is never negative, so checking if this is only equal to 0 makes sense. if (greatCountSliders == 0) { - return null; + return double.PositiveInfinity; } double greatProbabilitySlider = greatCountSliders / (sliderCount + 1.0); @@ -320,62 +379,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty return deviationOnSliders; } - /// - /// Does the same as , but only for notes and inaccuracies that are relevant to speed difficulty. - /// Treats all difficult speed notes as circles, so this method can sometimes return a lower deviation than . - /// This is fine though, since this method is only used to scale speed pp. - /// - private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + private static double getClockRate(ScoreInfo score) { - if (totalSuccessfulHits == 0) - return double.PositiveInfinity; - - // Create a new track to properly calculate the hit windows of 100s and 50s. var track = new TrackVirtual(1); score.Mods.OfType().ForEach(m => m.ApplyToTrack(track)); - double clockRate = track.Rate; - - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - - // Calculate accuracy assuming the worst case scenario - double speedNoteCount = attributes.SpeedNoteCount; - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; - double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh)); - - // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, - // compute the deviation on circles. - if (relevantCountGreat > 0) - { - // The probability that a player hits a circle is unknown, but we can estimate it to be - // the number of greats on circles divided by the number of circles, and then add one - // to the number of circles as a bias correction. - double greatProbabilityCircle = relevantCountGreat / (speedNoteCount - relevantCountMiss - relevantCountMeh + 1.0); - - // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. - // Begin with the normal distribution first. - double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); - deviationOnCircles *= Math.Sqrt(1 - Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviationOnCircles, 2)) - / (deviationOnCircles * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviationOnCircles)))); - - // Then compute the variance for 50s. - double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; - - // Find the total deviation. - deviationOnCircles = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviationOnCircles, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); - - return deviationOnCircles; - } - - return double.PositiveInfinity; + return track.Rate; } - private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); + private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countOk + countMeh; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index e2a13d3177..c7c46f7265 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -22,12 +22,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double currentStrain; private double currentRhythm; - private double currentStrainNoDistance; - protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; - private readonly List objectStrainsNoDistance = new List(); + private readonly List objectStrains = new List(); public Speed(Mod[] mods) : base(mods) @@ -43,34 +41,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier; - // Disregard distance when computing the number of speed notes. - double travelDistance = current.Index > 0 ? ((OsuDifficultyHitObject)current.Previous(0)).TravelDistance : 0; - double distance = Math.Min(125, travelDistance + ((OsuDifficultyHitObject)current).MinimumJumpDistance); - - currentStrainNoDistance *= strainDecay(((OsuDifficultyHitObject)current).StrainTime); - currentStrainNoDistance += SpeedEvaluator.EvaluateDifficultyOf(current) / (1 + Math.Pow(distance / 125, 3.5)) * skillMultiplier; - currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); double totalStrain = currentStrain * currentRhythm; - double totalStrainNoDistance = currentStrainNoDistance * currentRhythm; - - objectStrainsNoDistance.Add(totalStrainNoDistance); + objectStrains.Add(totalStrain); return totalStrain; } public double RelevantNoteCount() { - if (objectStrainsNoDistance.Count == 0) + if (objectStrains.Count == 0) return 0; - double maxStrain = objectStrainsNoDistance.Max(); + double maxStrain = objectStrains.Max(); if (maxStrain == 0) return 0; - return objectStrainsNoDistance.Sum(strain => strain / maxStrain); + return objectStrains.Sum(strain => strain / maxStrain); } } } From c64ad60cb15b78779bbbb04699e3e21a58e71f89 Mon Sep 17 00:00:00 2001 From: 02Naitsirk Date: Thu, 29 Aug 2024 20:55:52 -0400 Subject: [PATCH 30/30] Merge like half of givi's changes to compare --- .../Difficulty/OsuPerformanceCalculator.cs | 235 +++++++++++------- .../Difficulty/Skills/Speed.cs | 2 +- 2 files changed, 145 insertions(+), 92 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index fdb60879f2..f63f62062b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double effectiveMissCount; - private double hitWindow300; + private double hitWindow300, hitWindow100, hitWindow50; private double deviation, speedDeviation; public OsuPerformanceCalculator() @@ -70,8 +70,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty double clockRate = getClockRate(score); hitWindow300 = 80 - 6 * osuAttributes.OverallDifficulty; + hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - deviation = calculateDeviation(score, osuAttributes); + deviation = calculateTotalDeviation(score, osuAttributes); speedDeviation = calculateSpeedDeviation(score, osuAttributes); double aimValue = computeAimValue(score, osuAttributes); @@ -146,9 +148,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= sliderNerfFactor; } - double prod = Math.Sqrt(attributes.AimDifficulty * attributes.SpeedDifficulty) * deviation; - aimValue *= Math.Pow(SpecialFunctions.Erf(130 / prod), 1.5); - aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11.1 SS stays the same. + // Apply antirake nerf + double totalAntiRakeMultiplier = calculateTotalRakeNerf(attributes); + aimValue *= totalAntiRakeMultiplier; + + // Scale the aim value with adjusted deviation + double adjustedDeviation = deviation * calculateDeviationArAdjust(attributes.ApproachRate); + aimValue *= SpecialFunctions.Erf(33 / (Math.Sqrt(2) * adjustedDeviation)); + aimValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. return aimValue; } @@ -187,9 +194,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); } - double prod = attributes.SpeedDifficulty * speedDeviation; - speedValue *= Math.Pow(SpecialFunctions.Erf(67.5 / prod), 1.5); - speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11.1 SS stays the same. + // Apply antirake nerf + double speedAntiRakeMultiplier = calculateSpeedRakeNerf(attributes); + speedValue *= speedAntiRakeMultiplier; + + double adjustedSpeedDeviation = speedDeviation * calculateDeviationArAdjust(attributes.ApproachRate); + speedValue *= SpecialFunctions.Erf(22 / (Math.Sqrt(2) * adjustedSpeedDeviation * Math.Max(1, Math.Pow(attributes.SpeedDifficulty / 4.5, 1.2)))); + speedValue *= 0.95 + Math.Pow(100.0 / 9, 2) / 750; // OD 11 SS stays the same. return speedValue; } @@ -230,8 +241,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) + (totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0); - // Scale the flashlight value with deviation - flashlightValue *= SpecialFunctions.Erf(35 / deviation); + // Scale the flashlight value with adjusted deviation + double adjustedDeviation = deviation * calculateDeviationArAdjust(attributes.ApproachRate); + flashlightValue *= SpecialFunctions.Erf(55 / (Math.Sqrt(2) * adjustedDeviation)); flashlightValue *= 0.98 + Math.Pow(100.0 / 9, 2) / 2500; // OD 11 SS stays the same. return flashlightValue; @@ -255,6 +267,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty return Math.Max(countMiss, comboBasedMissCount); } + /// + /// Using estimates player's deviation on accuracy objects. + /// Returns deviation for circles and sliders if score was set with slideracc. + /// Returns the min between deviation of circles and deviation on circles and sliders (assuming slider hits are 50s), if score was set without slideracc. + /// + private double calculateTotalDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + { + if (totalSuccessfulHits == 0) + return double.PositiveInfinity; + + int accuracyObjectCount = attributes.HitCircleCount; + + // Assume worst case: all mistakes was on accuracy objects + int relevantCountMiss = Math.Min(countMiss, accuracyObjectCount); + int relevantCountMeh = Math.Min(countMeh, accuracyObjectCount - relevantCountMiss); + int relevantCountOk = Math.Min(countOk, accuracyObjectCount - relevantCountMiss - relevantCountMeh); + int relevantCountGreat = Math.Max(0, accuracyObjectCount - relevantCountMiss - relevantCountMeh - relevantCountOk); + + // Calculate deviation on accuracy objects + double deviation = calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); + + // If score was set without slider accuracy - also compute deviation with sliders + // Assume that all hits was 50s + int totalCountWithSliders = attributes.HitCircleCount + attributes.SliderCount; + int missCountWithSliders = Math.Min(totalCountWithSliders, countMiss); + int hitCountWithSliders = totalCountWithSliders - missCountWithSliders; + + double hitProbabilityWithSliders = hitCountWithSliders / (totalCountWithSliders + 1.0); + double deviationWithSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(hitProbabilityWithSliders)); + + // Min is needed for edgecase maps with 1 circle and 999 sliders, as deviation on sliders can be lower in this case + return Math.Min(deviation, deviationWithSliders); + } + /// /// Using estimates player's deviation on speed notes, assuming worst-case. /// Treats all speed notes as hit circles. This is not good way to do this, but fixing this is impossible under the limitation of current speed pp. @@ -265,49 +311,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// Treats all difficult speed notes as circles, so this method can sometimes return a lower deviation than . /// This is fine though, since this method is only used to scale speed pp. /// + /// + /// Using estimates player's deviation on speed notes, assuming worst-case. + /// Treats all speed notes as hit circles. This is not good way to do this, but fixing this is impossible under the limitation of current speed pp. + /// If score was set with slideracc - tries to remove mistaps on sliders from total mistaps. + /// private double calculateSpeedDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) { if (totalSuccessfulHits == 0) return double.PositiveInfinity; - // Create a new track to properly calculate the hit windows of 100s and 50s. - double clockRate = getClockRate(score); - - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - // Calculate accuracy assuming the worst case scenario double speedNoteCount = attributes.SpeedNoteCount; - double relevantTotalDiff = totalHits - attributes.SpeedNoteCount; - double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff); - double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat)); - double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk)); - double relevantCountMiss = Math.Max(0, countMiss - Math.Max(0, relevantTotalDiff - countGreat - countOk - countMeh)); - // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, - // compute the deviation on circles. - if (relevantCountGreat > 0) - { - // The probability that a player hits a circle is unknown, but we can estimate it to be - // the number of greats on circles divided by the number of circles, and then add one - // to the number of circles as a bias correction. - double greatProbabilityCircle = relevantCountGreat / (speedNoteCount - relevantCountMiss - relevantCountMeh + 1.0); + // Assume worst case: all mistakes was on speed notes + double relevantCountMiss = Math.Min(countMiss, speedNoteCount); + double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss); + double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh); + double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk); - // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. - // Begin with the normal distribution first. - double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilityCircle)); - - // Then compute the variance for 50s. - double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; - - // Find the total deviation. - deviationOnCircles = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviationOnCircles, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); - - return deviationOnCircles; - } - - return double.PositiveInfinity; + // Calculate and return deviation on speed notes + return calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss); } /// @@ -316,69 +340,98 @@ namespace osu.Game.Rulesets.Osu.Difficulty /// will always return the same deviation. Misses are ignored because they are usually due to misaiming. /// 300s and 100s are assumed to follow a normal distribution, whereas 50s are assumed to follow a uniform distribution. /// - private double calculateDeviation(ScoreInfo score, OsuDifficultyAttributes attributes) + private double calculateDeviation(double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss) { - if (totalSuccessfulHits == 0) + if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) return double.PositiveInfinity; - double clockRate = getClockRate(score); + double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss; - double hitWindow300 = 80 - 6 * attributes.OverallDifficulty; - double hitWindow100 = (140 - 8 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; - double hitWindow50 = (200 - 10 * ((80 - hitWindow300 * clockRate) / 6)) / clockRate; + //// The probability that a player hits a circle is unknown, but we can estimate it to be + //// the number of greats on circles divided by the number of circles, and then add one + //// to the number of circles as a bias correction. + double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh); + const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). - int circleCount = attributes.HitCircleCount; - int missCountCircles = Math.Min(countMiss, circleCount); - int mehCountCircles = Math.Min(countMeh, circleCount - missCountCircles); - int okCountCircles = Math.Min(countOk, circleCount - missCountCircles - mehCountCircles); - int greatCountCircles = Math.Max(0, circleCount - missCountCircles - mehCountCircles - okCountCircles); + // Proportion of greats hit on circles, ignoring misses and 50s. + double p = relevantCountGreat / n; - // Assume 100s, 50s, and misses happen on circles. If there are less non-300s on circles than 300s, - // compute the deviation on circles. - if (greatCountCircles > 0) - { - double n = circleCount - missCountCircles - mehCountCircles; - const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). + // We can be 99% confident that p is at least this value. + double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); - // Proportion of greats hit on circles, ignoring misses and 50s. - double p = greatCountCircles / n; + // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. + // Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than: + double deviation = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); - // We can be 99% confident that p is at least this value. - double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); + double adjustFor100 = Math.Sqrt(2 / Math.PI) * hitWindow100 * Math.Exp(-0.5 * Math.Pow(hitWindow100 / deviation, 2)) + / (deviation * SpecialFunctions.Erf(hitWindow100 / (Math.Sqrt(2) * deviation))); - // Compute the deviation assuming 300s and 100s are normally distributed, and 50s are uniformly distributed. - // Begin with 300s and 100s first. Ignoring 50s, we can be 99% confident that the deviation is not higher than: - double deviationOnCircles = hitWindow300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); + deviation *= Math.Sqrt(1 - adjustFor100); - // Then compute the variance for 50s. - double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; + // Value deviation approach as greatCount approaches 0 + double limitValue = hitWindow100 / Math.Sqrt(3); - // Find the total deviation. - deviationOnCircles = Math.Sqrt(((greatCountCircles + okCountCircles) * Math.Pow(deviationOnCircles, 2) + mehCountCircles * mehVariance) / (greatCountCircles + okCountCircles + mehCountCircles)); + // If precision is not enough to compute true deviation - use limit value + if (pLowerBound == 0 || adjustFor100 >= 1 || deviation > limitValue) + deviation = limitValue; - return deviationOnCircles; - } + // Then compute the variance for 50s. + double mehVariance = (hitWindow50 * hitWindow50 + hitWindow100 * hitWindow50 + hitWindow100 * hitWindow100) / 3; - // If there are more non-300s than there are circles, compute the deviation on sliders instead. - // Here, all that matters is whether or not the slider was missed, since it is impossible - // to get a 100 or 50 on a slider by mis-tapping it. - int sliderCount = attributes.SliderCount; - int missCountSliders = Math.Min(sliderCount, countMiss - missCountCircles); - int greatCountSliders = sliderCount - missCountSliders; + // Find the total deviation. + deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh)); - // We only get here if nothing was hit. In this case, there is no estimate for deviation. - // Note that this is never negative, so checking if this is only equal to 0 makes sense. - if (greatCountSliders == 0) - { - return double.PositiveInfinity; - } - - double greatProbabilitySlider = greatCountSliders / (sliderCount + 1.0); - double deviationOnSliders = hitWindow50 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(greatProbabilitySlider)); - - return deviationOnSliders; + return deviation; } + // Calculates multiplier for speed accounting for rake based on the deviation and speed difficulty + // https://www.desmos.com/calculator/puc1mzdtfv + private double calculateSpeedRakeNerf(OsuDifficultyAttributes attributes) + { + // Base speed value + double speedValue = 4 * Math.Pow(attributes.SpeedDifficulty, 3); + + // Starting from this pp amount - penalty will be applied + double abusePoint = 100 + 260 * Math.Pow(22 / speedDeviation, 5.8); + + if (speedValue <= abusePoint) + return 1.0; + + // Use log curve to make additional rise in difficulty unimpactful. Rescale values to make curve have correct steepness + const double scale = 50; + double adjustedSpeedValue = scale * (Math.Log((speedValue - abusePoint) / scale + 1) + abusePoint / scale); + + double speedRakeNerf = adjustedSpeedValue / speedValue; + + return Math.Min(speedRakeNerf, calculateTotalRakeNerf(attributes)); + } + + // Calculates multiplier for total pp accounting for rake based on the deviation and sliderless aim and speed difficulty + private double calculateTotalRakeNerf(OsuDifficultyAttributes attributes) + { + // Use adjusted deviation to not nerf EZHT aim maps + double adjustedDeviation = deviation * calculateDeviationArAdjust(attributes.ApproachRate); + + // Base values + double aimNoSlidersValue = 4 * Math.Pow(attributes.AimDifficulty * attributes.SliderFactor, 3); + double speedValue = 4 * Math.Pow(attributes.SpeedDifficulty, 3); + double totalValue = Math.Pow(Math.Pow(aimNoSlidersValue, 1.1) + Math.Pow(speedValue, 1.1), 1 / 1.1); + + // Starting from this pp amount - penalty will be applied + double abusePoint = 200 + 600 * Math.Pow(22 / adjustedDeviation, 4.2); + + if (totalValue <= abusePoint) + return 1.0; + + // Use relax penalty after the point to make values grow slower but still noticeably + double adjustedTotalValue = abusePoint + Math.Pow(0.9, 3) * (totalValue - abusePoint); + + return adjustedTotalValue / totalValue; + } + + // Bonus for low AR to account for the fact that it's more difficult to get low UR on low AR + private static double calculateDeviationArAdjust(double AR) => 0.475 + 0.7 / (1.0 + Math.Pow(1.73, 7.9 - AR)); + private static double getClockRate(ScoreInfo score) { var track = new TrackVirtual(1); diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index c7c46f7265..db58cb0d42 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (maxStrain == 0) return 0; - return objectStrains.Sum(strain => strain / maxStrain); + return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 8.0 - 4.0)))); } } }