mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 12:22:56 +08:00
Merge pull request #24166 from Zyfarok/scorev3
Modify osu! standardised scoring to introduce a combo exponent
This commit is contained in:
commit
987fe9322e
@ -74,6 +74,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
simulateHit(obj, ref attributes);
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
@ -74,6 +74,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
simulateHit(obj, ref attributes);
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
attributes.MaxCombo = combo;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@ -17,12 +16,5 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
|
||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 700000 * comboProgress
|
||||
+ 300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress
|
||||
+ bonusPortion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
simulateHit(obj, ref attributes);
|
||||
|
||||
attributes.BonusScoreRatio = legacyBonusScore == 0 ? 0 : (double)standardisedBonusScore / legacyBonusScore;
|
||||
attributes.BonusScore = legacyBonusScore;
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
@ -45,11 +45,11 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, 83_398)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, 168_724)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 11_670)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 23_341)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, 8_343)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, 16_878)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, 100_033)]
|
||||
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
||||
{
|
||||
@ -75,27 +75,27 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
|
||||
/// </remarks>
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 79_333)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 158_667)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 317_626)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 34_734)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 69_925)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 154_499)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 541_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 493_652)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 492_894)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 326_963)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)]
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 7_975)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15_949)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 31_928)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 3_492)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 7_029)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 15_530)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 32_867)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 32_867)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 54_189)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 49_365)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 49_289)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 32_696)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 100_003)]
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 100_015)]
|
||||
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Database
|
||||
if (score.IsLegacyScore)
|
||||
return false;
|
||||
|
||||
if (score.TotalScoreVersion > 30000002)
|
||||
if (score.TotalScoreVersion > 30000004)
|
||||
return false;
|
||||
|
||||
// Recalculate the old-style standardised score to see if this was an old lazer score.
|
||||
@ -249,14 +249,15 @@ namespace osu.Game.Database
|
||||
int maximumLegacyAccuracyScore = attributes.AccuracyScore;
|
||||
long maximumLegacyComboScore = (long)Math.Round(attributes.ComboScore * legacyModMultiplier);
|
||||
double maximumLegacyBonusRatio = attributes.BonusScoreRatio;
|
||||
long maximumLegacyBonusScore = attributes.BonusScore;
|
||||
|
||||
// The part of total score that doesn't include bonus.
|
||||
double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy;
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
double comboProportion =
|
||||
((double)score.LegacyTotalScore - legacyAccScore) / (maximumLegacyComboScore + maximumLegacyBonusScore);
|
||||
|
||||
// We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||
|
||||
// The combo proportion is calculated as a proportion of maximumLegacyBaseScore.
|
||||
double comboProportion = Math.Min(1, (double)score.LegacyTotalScore / maximumLegacyBaseScore);
|
||||
|
||||
// The bonus proportion makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
double bonusProportion = Math.Max(0, ((long)score.LegacyTotalScore - maximumLegacyBaseScore) * maximumLegacyBonusRatio);
|
||||
|
||||
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
|
||||
@ -264,9 +265,92 @@ namespace osu.Game.Database
|
||||
switch (score.Ruleset.OnlineID)
|
||||
{
|
||||
case 0:
|
||||
if (score.MaxCombo == 0 || score.Accuracy == 0)
|
||||
{
|
||||
return (long)Math.Round((
|
||||
0
|
||||
+ 500000 * Math.Pow(score.Accuracy, 5)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
}
|
||||
|
||||
// Assumptions:
|
||||
// - sliders and slider ticks are uniformly distributed in the beatmap, and thus can be ignored without losing much precision.
|
||||
// We thus consider a map of hit-circles only, which gives objectCount == maximumCombo.
|
||||
// - the Ok/Meh hit results are uniformly spread in the score, and thus can be ignored without losing much precision.
|
||||
// We simplify and consider each hit result to have the same hit value of `300 * score.Accuracy`
|
||||
// (which represents the average hit value over the entire play),
|
||||
// which allows us to isolate the accuracy multiplier.
|
||||
|
||||
// This is a very ballpark estimate of the maximum magnitude of the combo portion in score V1.
|
||||
// It is derived by assuming a full combo play and summing up the contribution to combo portion from each individual object.
|
||||
// Because each object's combo contribution is proportional to the current combo at the time of judgement,
|
||||
// this can be roughly represented by summing / integrating f(combo) = combo.
|
||||
// All mod- and beatmap-dependent multipliers and constants are not included here,
|
||||
// as we will only be using the magnitude of this to compute ratios.
|
||||
int maximumLegacyCombo = attributes.MaxCombo;
|
||||
double maximumAchievableComboPortionInScoreV1 = Math.Pow(maximumLegacyCombo, 2);
|
||||
// Similarly, estimate the maximum magnitude of the combo portion in standardised score.
|
||||
// Roughly corresponds to integrating f(combo) = combo ^ COMBO_EXPONENT (omitting constants)
|
||||
double maximumAchievableComboPortionInStandardisedScore = Math.Pow(maximumLegacyCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double comboPortionInScoreV1 = maximumAchievableComboPortionInScoreV1 * comboProportion / score.Accuracy;
|
||||
|
||||
// This is - roughly - how much score, in the combo portion, the longest combo on this particular play would gain in score V1.
|
||||
double comboPortionFromLongestComboInScoreV1 = Math.Pow(score.MaxCombo, 2);
|
||||
// Same for standardised score.
|
||||
double comboPortionFromLongestComboInStandardisedScore = Math.Pow(score.MaxCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
// Calculate how many times the longest combo the user has achieved in the play can repeat
|
||||
// without exceeding the combo portion in score V1 as achieved by the player.
|
||||
// This is a pessimistic estimate; it intentionally does not operate on object count and uses only score instead.
|
||||
double maximumOccurrencesOfLongestCombo = Math.Floor(comboPortionInScoreV1 / comboPortionFromLongestComboInScoreV1);
|
||||
double comboPortionFromRepeatedLongestCombosInScoreV1 = maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInScoreV1;
|
||||
|
||||
double remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromRepeatedLongestCombosInScoreV1;
|
||||
// `remainingComboPortionInScoreV1` is in the "score ballpark" realm, which means it's proportional to combo squared.
|
||||
// To convert that back to a raw combo length, we need to take the square root...
|
||||
double remainingCombo = Math.Sqrt(remainingComboPortionInScoreV1);
|
||||
// ...and then based on that raw combo length, we calculate how much this last combo is worth in standardised score.
|
||||
double remainingComboPortionInStandardisedScore = Math.Pow(remainingCombo, 1 + ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double lowerEstimateOfComboPortionInStandardisedScore
|
||||
= maximumOccurrencesOfLongestCombo * comboPortionFromLongestComboInStandardisedScore
|
||||
+ remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Compute approximate upper estimate new score for that play.
|
||||
// This time, divide the remaining combo among remaining objects equally to achieve longest possible combo lengths.
|
||||
// There is no rigorous proof that doing this will yield a correct upper bound, but it seems to work out in practice.
|
||||
remainingComboPortionInScoreV1 = comboPortionInScoreV1 - comboPortionFromLongestComboInScoreV1;
|
||||
double remainingCountOfObjectsGivingCombo = maximumLegacyCombo - score.MaxCombo - score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
// Because we assumed all combos were equal, `remainingComboPortionInScoreV1`
|
||||
// can be approximated by n * x^2, wherein n is the assumed number of equal combos,
|
||||
// and x is the assumed length of every one of those combos.
|
||||
// The remaining count of objects giving combo is, using those terms, equal to n * x.
|
||||
// Therefore, dividing the two will result in x, i.e. the assumed length of the remaining combos.
|
||||
double lengthOfRemainingCombos = remainingCountOfObjectsGivingCombo > 0
|
||||
? remainingComboPortionInScoreV1 / remainingCountOfObjectsGivingCombo
|
||||
: 0;
|
||||
// In standardised scoring, each combo yields a score proportional to combo length to the power 1 + COMBO_EXPONENT.
|
||||
// Using the symbols introduced above, that would be x ^ 1.5 per combo, n times (because there are n assumed equal-length combos).
|
||||
// However, because `remainingCountOfObjectsGivingCombo` - using the symbols introduced above - is assumed to be equal to n * x,
|
||||
// we can skip adding the 1 and just multiply by x ^ 0.5.
|
||||
remainingComboPortionInStandardisedScore = remainingCountOfObjectsGivingCombo * Math.Pow(lengthOfRemainingCombos, ScoreProcessor.COMBO_EXPONENT);
|
||||
|
||||
double upperEstimateOfComboPortionInStandardisedScore = comboPortionFromLongestComboInStandardisedScore + remainingComboPortionInStandardisedScore;
|
||||
|
||||
// Approximate by combining lower and upper estimates.
|
||||
// As the lower-estimate is very pessimistic, we use a 30/70 ratio
|
||||
// and cap it with 1.2 times the middle-point to avoid overestimates.
|
||||
double estimatedComboPortionInStandardisedScore = Math.Min(
|
||||
0.3 * lowerEstimateOfComboPortionInStandardisedScore + 0.7 * upperEstimateOfComboPortionInStandardisedScore,
|
||||
1.2 * (lowerEstimateOfComboPortionInStandardisedScore + upperEstimateOfComboPortionInStandardisedScore) / 2
|
||||
);
|
||||
|
||||
double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore;
|
||||
|
||||
return (long)Math.Round((
|
||||
700000 * comboProportion
|
||||
+ 300000 * Math.Pow(score.Accuracy, 10)
|
||||
500000 * newComboScoreProportion * score.Accuracy
|
||||
+ 500000 * Math.Pow(score.Accuracy, 5)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
|
||||
case 1:
|
||||
|
@ -19,5 +19,15 @@ namespace osu.Game.Rulesets.Scoring.Legacy
|
||||
/// A ratio of standardised score to legacy score for the bonus part of total score.
|
||||
/// </summary>
|
||||
public double BonusScoreRatio;
|
||||
|
||||
/// <summary>
|
||||
/// The bonus portion of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
public int BonusScore;
|
||||
|
||||
/// <summary>
|
||||
/// The max combo of the legacy (ScoreV1) total score.
|
||||
/// </summary>
|
||||
public int MaxCombo;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
{
|
||||
public partial class ScoreProcessor : JudgementProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The exponent applied to combo in the default implementation of <see cref="GetComboScoreChange"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a custom implementation overrides <see cref="GetComboScoreChange"/> this may not be relevant.
|
||||
/// </remarks>
|
||||
public const double COMBO_EXPONENT = 0.5;
|
||||
|
||||
public const double MAX_SCORE = 1000000;
|
||||
|
||||
private const double accuracy_cutoff_x = 1;
|
||||
@ -293,7 +301,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected virtual double GetBonusScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type);
|
||||
|
||||
protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Type) * (1 + result.ComboAfterJudgement / 10d);
|
||||
protected virtual double GetComboScoreChange(JudgementResult result) => Judgement.ToNumericResult(result.Judgement.MaxResult) * Math.Pow(result.ComboAfterJudgement, COMBO_EXPONENT);
|
||||
|
||||
protected virtual void ApplyScoreChange(JudgementResult result)
|
||||
{
|
||||
@ -317,8 +325,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 700000 * comboProgress +
|
||||
300000 * Math.Pow(Accuracy.Value, 10) * accuracyProgress +
|
||||
return 500000 * Accuracy.Value * comboProgress +
|
||||
500000 * Math.Pow(Accuracy.Value, 5) * accuracyProgress +
|
||||
bonusPortion;
|
||||
}
|
||||
|
||||
|
@ -31,9 +31,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm. Legacy scores on this version are candidate to Score V1 -> V2 conversion.</description></item>
|
||||
/// <item><description>30000003: First version after converting legacy total score to standardised.</description></item>
|
||||
/// <item><description>30000004: Fixed mod multipliers during legacy score conversion. Reconvert all scores.</description></item>
|
||||
/// <item><description>30000005: Introduce combo exponent in the osu! gamemode. Reconvert all scores.</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public const int LATEST_VERSION = 30000004;
|
||||
public const int LATEST_VERSION = 30000005;
|
||||
|
||||
/// <summary>
|
||||
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.
|
||||
|
Loading…
Reference in New Issue
Block a user