mirror of
https://github.com/ppy/osu.git
synced 2025-01-30 00:03:08 +08:00
Punish speed PP for scores with high deviation (#30907)
This commit is contained in:
parent
db58ec8645
commit
b21c6457b1
@ -62,21 +62,33 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("overall_difficulty")]
|
||||
public double OverallDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
[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>
|
||||
[JsonProperty("ok_hit_window")]
|
||||
public double OkHitWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for a MEH hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
[JsonProperty("meh_hit_window")]
|
||||
public double MehHitWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||
/// </summary>
|
||||
@ -107,6 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
|
||||
if (ShouldSerializeFlashlightDifficulty())
|
||||
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
|
||||
@ -117,6 +130,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||
|
||||
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||
yield return (ATTRIB_ID_MEH_HIT_WINDOW, MehHitWindow);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@ -128,12 +144,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||
MehHitWindow = values[ATTRIB_ID_MEH_HIT_WINDOW];
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
|
@ -99,6 +99,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
double hitWindowOk = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||
double hitWindowMeh = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
@ -114,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
GreatHitWindow = hitWindowGreat,
|
||||
OkHitWindow = hitWindowOk,
|
||||
MehHitWindow = hitWindowMeh,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCirclesCount,
|
||||
|
@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
[JsonProperty("speed_deviation")]
|
||||
public double? SpeedDeviation { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
|
@ -9,6 +9,7 @@ using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@ -40,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
/// </summary>
|
||||
private double effectiveMissCount;
|
||||
|
||||
private double? speedDeviation;
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
@ -110,10 +113,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
speedDeviation = calculateSpeedDeviation(osuAttributes);
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(aimValue, 1.1) +
|
||||
@ -129,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
SpeedDeviation = speedDeviation,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
@ -198,7 +205,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) || speedDeviation == null)
|
||||
return 0.0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
@ -230,6 +237,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
}
|
||||
|
||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||
speedValue *= speedHighDeviationMultiplier;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
@ -240,9 +250,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(Math.Max(0, attributes.OverallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
|
||||
@ -310,12 +317,116 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
||||
/// Treats all speed notes as hit circles.
|
||||
/// </summary>
|
||||
private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (totalSuccessfulHits == 0)
|
||||
return null;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double speedNoteCount = attributes.SpeedNoteCount;
|
||||
speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
|
||||
|
||||
// Assume worst case: all mistakes were 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);
|
||||
|
||||
return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs 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.
|
||||
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
||||
/// </summary>
|
||||
private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
|
||||
{
|
||||
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
||||
return null;
|
||||
|
||||
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
|
||||
|
||||
double hitWindowGreat = attributes.GreatHitWindow;
|
||||
double hitWindowOk = attributes.OkHitWindow;
|
||||
double hitWindowMeh = attributes.MehHitWindow;
|
||||
|
||||
// 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).
|
||||
|
||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
||||
double p = relevantCountGreat / 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 greats and oks are normally distributed, and mehs are uniformly distributed.
|
||||
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
||||
double deviation = hitWindowGreat / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
|
||||
double randomValue = Math.Sqrt(2 / Math.PI) * hitWindowOk * Math.Exp(-0.5 * Math.Pow(hitWindowOk / deviation, 2))
|
||||
/ (deviation * SpecialFunctions.Erf(hitWindowOk / (Math.Sqrt(2) * deviation)));
|
||||
|
||||
deviation *= Math.Sqrt(1 - randomValue);
|
||||
|
||||
// Value deviation approach as greatCount approaches 0
|
||||
double limitValue = hitWindowOk / Math.Sqrt(3);
|
||||
|
||||
// If precision is not enough to compute true deviation - use limit value
|
||||
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
|
||||
deviation = limitValue;
|
||||
|
||||
// Then compute the variance for mehs.
|
||||
double mehVariance = (hitWindowMeh * hitWindowMeh + hitWindowOk * hitWindowMeh + hitWindowOk * hitWindowOk) / 3;
|
||||
|
||||
// Find the total deviation.
|
||||
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||
|
||||
return deviation;
|
||||
}
|
||||
|
||||
// Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
|
||||
// https://www.desmos.com/calculator/dmogdhzofn
|
||||
private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (speedDeviation == null)
|
||||
return 0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||
double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
|
||||
|
||||
if (speedValue <= excessSpeedDifficultyCutoff)
|
||||
return 1.0;
|
||||
|
||||
const double scale = 50;
|
||||
double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
|
||||
|
||||
// 200 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
|
||||
double lerp = 1 - Math.Clamp((speedDeviation.Value - 20) / (24 - 20), 0, 1);
|
||||
adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
|
||||
|
||||
return adjustedSpeedValue / speedValue;
|
||||
}
|
||||
|
||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||
// to make it more punishing on maps with lower amount of hard sections.
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||
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;
|
||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
|
||||
protected const int ATTRIB_ID_MONO_STAMINA_FACTOR = 29;
|
||||
protected const int ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT = 31;
|
||||
protected const int ATTRIB_ID_MEH_HIT_WINDOW = 33;
|
||||
|
||||
/// <summary>
|
||||
/// The mods which were applied to the beatmap.
|
||||
|
Loading…
Reference in New Issue
Block a user