From 4019683f6ce6231c3cf04d437adccc8d48e1b651 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 May 2018 01:24:53 +0900 Subject: [PATCH 1/5] Implement osu!mania performance calculation --- .../Difficulty/ManiaDifficultyCalculator.cs | 3 +- .../Difficulty/ManiaPerformanceCalculator.cs | 122 ++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 + 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ac45130fd8..8458e63fca 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -60,7 +60,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty double starRating = calculateDifficulty() * star_scaling_factor; - categoryDifficulty?.Add("Strain", starRating); + if (categoryDifficulty != null) + categoryDifficulty["Strain"] = starRating; return starRating; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs new file mode 100644 index 0000000000..1704d924f8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -0,0 +1,122 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Difficulty +{ + public class ManiaPerformanceCalculator : PerformanceCalculator + { + private Mod[] mods; + private int countGeki; + private int countKatu; + private int count300; + private int count100; + private int count50; + private int countMiss; + + public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + mods = Score.Mods; + countGeki = Convert.ToInt32(Score.Statistics[HitResult.Perfect]); + countKatu = Convert.ToInt32(Score.Statistics[HitResult.Ok]); + count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]); + count100 = Convert.ToInt32(Score.Statistics[HitResult.Good]); + count50 = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); + + if (mods.Any(m => !m.Ranked)) + return 0; + + // Custom multipliers for NoFail + double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (mods.Any(m => m is ModNoFail)) + multiplier *= 0.9; + if (mods.Any(m => m is ModEasy)) + multiplier *= 0.5; + + double strainValue = computeStrainValue(); + double accValue = computeAccuracyValue(); + double totalValue = + Math.Pow( + Math.Pow(strainValue, 1.1) + + Math.Pow(accValue, 1.1), 1.0 / 1.1 + ) * multiplier; + + if (categoryDifficulty != null) + { + categoryDifficulty["Strain"] = strainValue; + categoryDifficulty["Accuracy"] = accValue; + + } + + return totalValue; + } + + private double computeStrainValue() + { + IEnumerable scoreIncreaseMods = new ManiaRuleset().GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Score after being scaled by non-difficulty-increasing mods + double scaledScore = Score.TotalScore; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; + + // Obtain strain difficulty + double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.0825) - 4.0, 3.0) / 110000.0; + + // Longer maps are worth more + strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + + if (scaledScore <= 500000) + strainValue = 0; + else if (scaledScore <= 600000) + strainValue *= (scaledScore - 500000) / 100000 * 0.3; + else if (scaledScore <= 700000) + strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.35; + else if (scaledScore <= 800000) + strainValue *= 0.65 + (scaledScore - 700000) / 100000 * 0.20; + else if (scaledScore <= 900000) + strainValue *= 0.85 + (scaledScore - 800000) / 100000 * 0.1; + else + strainValue *= 0.95 + (scaledScore - 900000) / 100000 * 0.05; + + return strainValue; + } + + private double computeAccuracyValue() + { + double hitWindow300 = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + if (hitWindow300 <= 0) + return 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(150.0 / hitWindow300 * Math.Pow(Score.Accuracy, 16), 1.8) * 2.5; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + + return accuracyValue; + } + + private double totalHits => countGeki + countKatu + count300 + count100 + count50 + countMiss; + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b1702de537..02ecb3afda 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -18,6 +18,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania { @@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Mania { public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap); public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); + public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score); public override IEnumerable ConvertLegacyMods(LegacyMods mods) { From 1fdc77d5791cd03af2e69fe62b1a68c57e1589c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 May 2018 01:34:07 +0900 Subject: [PATCH 2/5] Update with the rebalance changes --- .../Difficulty/ManiaPerformanceCalculator.cs | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 1704d924f8..3337d4df75 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -14,6 +14,10 @@ namespace osu.Game.Rulesets.Mania.Difficulty public class ManiaPerformanceCalculator : PerformanceCalculator { private Mod[] mods; + + // Score after being scaled by non-difficulty-increasing mods + private double scaledScore; + private int countGeki; private int countKatu; private int count300; @@ -29,6 +33,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty public override double Calculate(Dictionary categoryDifficulty = null) { mods = Score.Mods; + scaledScore = Score.TotalScore; countGeki = Convert.ToInt32(Score.Statistics[HitResult.Perfect]); countKatu = Convert.ToInt32(Score.Statistics[HitResult.Ok]); count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]); @@ -39,8 +44,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (mods.Any(m => !m.Ranked)) return 0; - // Custom multipliers for NoFail - double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + IEnumerable scoreIncreaseMods = new ManiaRuleset().GetModsFor(ModType.DifficultyIncrease); + + double scoreMultiplier = 1.0; + foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) + scoreMultiplier *= m.ScoreMultiplier; + + // Scale score up, so it's comparable to other keymods + scaledScore *= 1.0 / scoreMultiplier; + + // Arbitrary initial value for scaling pp in order to standardize distributions across game modes. + // The specific number has no intrinsic meaning and can be adjusted as needed. + double multiplier = 0.8; if (mods.Any(m => m is ModNoFail)) multiplier *= 0.9; @@ -48,7 +63,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty multiplier *= 0.5; double strainValue = computeStrainValue(); - double accValue = computeAccuracyValue(); + double accValue = computeAccuracyValue(strainValue); double totalValue = Math.Pow( Math.Pow(strainValue, 1.1) + @@ -67,20 +82,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeStrainValue() { - IEnumerable scoreIncreaseMods = new ManiaRuleset().GetModsFor(ModType.DifficultyIncrease); - - double scoreMultiplier = 1.0; - foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) - scoreMultiplier *= m.ScoreMultiplier; - - // Score after being scaled by non-difficulty-increasing mods - double scaledScore = Score.TotalScore; - - // Scale score up, so it's comparable to other keymods - scaledScore *= 1.0 / scoreMultiplier; - // Obtain strain difficulty - double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.0825) - 4.0, 3.0) / 110000.0; + double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0; // Longer maps are worth more strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0); @@ -90,18 +93,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty else if (scaledScore <= 600000) strainValue *= (scaledScore - 500000) / 100000 * 0.3; else if (scaledScore <= 700000) - strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.35; + strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25; else if (scaledScore <= 800000) - strainValue *= 0.65 + (scaledScore - 700000) / 100000 * 0.20; + strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20; else if (scaledScore <= 900000) - strainValue *= 0.85 + (scaledScore - 800000) / 100000 * 0.1; + strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15; else - strainValue *= 0.95 + (scaledScore - 900000) / 100000 * 0.05; + strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1; return strainValue; } - private double computeAccuracyValue() + private double computeAccuracyValue(double strainValue) { double hitWindow300 = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; if (hitWindow300 <= 0) @@ -109,10 +112,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Pow(150.0 / hitWindow300 * Math.Pow(Score.Accuracy, 16), 1.8) * 2.5; + double accuracyValue = Math.Max(0.0, 0.2 - (hitWindow300 - 34) * 0.006667) + * strainValue + * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); // Bonus for many hitcircles - it's harder to keep good accuracy up for longer - accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); + // accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); return accuracyValue; } From ed902d9325eb6c7435e106224bd517a08ab9aa4c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 May 2018 01:36:28 +0900 Subject: [PATCH 3/5] Cleanup --- osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 3337d4df75..024362f5d2 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -74,7 +74,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty { categoryDifficulty["Strain"] = strainValue; categoryDifficulty["Accuracy"] = accValue; - } return totalValue; From de63a1b578d422a1c39b6212018ed3448c806ec5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 May 2018 01:43:58 +0900 Subject: [PATCH 4/5] Remove construction of new ruleset --- .../Difficulty/ManiaPerformanceCalculator.cs | 2 +- osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 024362f5d2..f5f815d9fb 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty if (mods.Any(m => !m.Ranked)) return 0; - IEnumerable scoreIncreaseMods = new ManiaRuleset().GetModsFor(ModType.DifficultyIncrease); + IEnumerable scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease); double scoreMultiplier = 1.0; foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m))) diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index 9fd7a0156d..07d9c80061 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Difficulty private readonly Dictionary attributes = new Dictionary(); protected IDictionary Attributes => attributes; + protected readonly Ruleset Ruleset; protected readonly IBeatmap Beatmap; protected readonly Score Score; @@ -23,9 +24,9 @@ namespace osu.Game.Rulesets.Difficulty protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) { - Score = score; - + Ruleset = ruleset; Beatmap = beatmap; + Score = score; var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); diffCalc.Calculate(attributes); From edbb3a5a57448e1bde365f7df4a7265d8610c07a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 May 2018 12:44:11 +0900 Subject: [PATCH 5/5] Rename to use new hit result namings --- .../Difficulty/ManiaPerformanceCalculator.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index f5f815d9fb..e6e3028d62 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -18,11 +18,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty // Score after being scaled by non-difficulty-increasing mods private double scaledScore; - private int countGeki; - private int countKatu; - private int count300; - private int count100; - private int count50; + private int countPerfect; + private int countGreat; + private int countGood; + private int countOk; + private int countMeh; private int countMiss; public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score) @@ -34,11 +34,11 @@ namespace osu.Game.Rulesets.Mania.Difficulty { mods = Score.Mods; scaledScore = Score.TotalScore; - countGeki = Convert.ToInt32(Score.Statistics[HitResult.Perfect]); - countKatu = Convert.ToInt32(Score.Statistics[HitResult.Ok]); - count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]); - count100 = Convert.ToInt32(Score.Statistics[HitResult.Good]); - count50 = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]); + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); if (mods.Any(m => !m.Ranked)) @@ -105,13 +105,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty private double computeAccuracyValue(double strainValue) { - double hitWindow300 = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; - if (hitWindow300 <= 0) + double hitWindowGreat = (Beatmap.HitObjects.First().HitWindows.Great / 2 - 0.5) / TimeRate; + if (hitWindowGreat <= 0) return 0; // Lots of arbitrary values from testing. // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Max(0.0, 0.2 - (hitWindow300 - 34) * 0.006667) + double accuracyValue = Math.Max(0.0, 0.2 - (hitWindowGreat - 34) * 0.006667) * strainValue * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1); @@ -121,6 +121,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty return accuracyValue; } - private double totalHits => countGeki + countKatu + count300 + count100 + count50 + countMiss; + private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss; } }