Skip to content

Commit

Permalink
Merge pull request #20963 from Natelytle/taikostatacc
Browse files Browse the repository at this point in the history
Statistical accuracy PP and difficulty scaling for the osu!taiko ruleset
  • Loading branch information
bdach authored Oct 7, 2024
2 parents 6d3eec3 + 84d6467 commit 6608d05
Show file tree
Hide file tree
Showing 6 changed files with 781 additions and 10 deletions.
11 changes: 11 additions & 0 deletions osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,23 @@ public class TaikoDifficultyAttributes : DifficultyAttributes
[JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; }

/// <summary>
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
/// </summary>
/// <remarks>
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
/// </remarks>
[JsonProperty("ok_hit_window")]
public double OkHitWindow { get; set; }

public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{
foreach (var v in base.ToDatabaseAttributes())
yield return v;

yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
}

public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
Expand All @@ -58,6 +68,7 @@ public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> val

StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
ColourDifficulty = colourRating,
PeakDifficulty = combinedRating,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class TaikoPerformanceAttributes : PerformanceAttributes
[JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; }

[JsonProperty("estimated_unstable_rate")]
public double? EstimatedUnstableRate { get; set; }

public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{
foreach (var attribute in base.GetAttributesForDisplay())
Expand Down
81 changes: 71 additions & 10 deletions osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Scoring;
using osu.Game.Utils;

namespace osu.Game.Rulesets.Taiko.Difficulty
{
Expand All @@ -18,7 +19,7 @@ public class TaikoPerformanceCalculator : PerformanceCalculator
private int countOk;
private int countMeh;
private int countMiss;
private double accuracy;
private double? estimatedUnstableRate;

private double effectiveMissCount;

Expand All @@ -35,7 +36,7 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
accuracy = customAccuracy;
estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;

// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0)
Expand Down Expand Up @@ -65,6 +66,7 @@ protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo s
Difficulty = difficultyValue,
Accuracy = accuracyValue,
EffectiveMissCount = effectiveMissCount,
EstimatedUnstableRate = estimatedUnstableRate,
Total = totalValue
};
}
Expand All @@ -85,35 +87,94 @@ private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes
difficultyValue *= 1.025;

if (score.Mods.Any(m => m is ModHardRock))
difficultyValue *= 1.050;
difficultyValue *= 1.10;

if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.050 * lengthBonus;

return difficultyValue * Math.Pow(accuracy, 2.0);
if (estimatedUnstableRate == null)
return 0;

return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUnstableRate.Value)), 2.0);
}

private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
{
if (attributes.GreatHitWindow <= 0)
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
return 0;

double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;

double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;

// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus);
accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus);

return accuracyValue;
}

/// <summary>
/// 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.
/// </summary>
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
{
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
return null;

double h300 = attributes.GreatHitWindow;
double h100 = attributes.OkHitWindow;

const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).

// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
double? calcDeviationGreatWindow()
{
if (countGreat == 0) return null;

double n = totalHits;

// Proportion of greats hit.
double p = countGreat / 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);

// We can be 99% confident that the deviation is not higher than:
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
}

// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
double? calcDeviationGoodWindow()
{
if (totalSuccessfulHits == 0) return null;

double n = totalHits;

// Proportion of greats + goods hit.
double p = totalSuccessfulHits / 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);

// We can be 99% confident that the deviation is not higher than:
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
}

double? deviationGreatWindow = calcDeviationGreatWindow();
double? deviationGoodWindow = calcDeviationGoodWindow();

if (deviationGreatWindow is null)
return deviationGoodWindow;

return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
}

private int totalHits => countGreat + countOk + countMeh + countMiss;

private int totalSuccessfulHits => countGreat + countOk + countMeh;

private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
}
}
1 change: 1 addition & 0 deletions osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class DifficultyAttributes
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23;
protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25;
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;

/// <summary>
/// The mods which were applied to the beatmap.
Expand Down
Loading

0 comments on commit 6608d05

Please sign in to comment.