1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

Merge pull request #24924 from bdach/update-classic-scoring

Update classic scoring formula to closer match stable score V1
This commit is contained in:
Dan Balasescu 2023-09-28 20:19:23 +09:00 committed by GitHub
commit db9113b38e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 27 additions and 34 deletions

View File

@ -45,9 +45,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.Meh, 116_667)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, 233_338)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 0)]
[TestCase(ScoringMode.Classic, HitResult.Ok, 2)]
[TestCase(ScoringMode.Classic, HitResult.Great, 36)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 11_670)]
[TestCase(ScoringMode.Classic, HitResult.Ok, 23_341)]
[TestCase(ScoringMode.Classic, HitResult.Great, 100_033)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
scoreProcessor.ApplyBeatmap(beatmap);
@ -84,17 +84,17 @@ namespace osu.Game.Tests.Rulesets.Scoring
[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, 4)]
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 15)]
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 53)]
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 140)]
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 140)]
[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, 30_398)]
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 49_546)]
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 49_546)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 11)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 54_189)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 9)]
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 36)]
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 36)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 49_289)]
[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)
{
var minResult = new TestJudgement(hitResult).MinResult;

View File

@ -27,44 +27,37 @@ namespace osu.Game.Scoring.Legacy
.DefaultIfEmpty(0)
.Sum();
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
double scaledRawScore = score / ScoreProcessor.MAX_SCORE;
return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, maxBasicJudgements), 2) * getStandardisedToClassicMultiplier(rulesetId));
return convertStandardisedToClassic(rulesetId, score, maxBasicJudgements);
}
/// <summary>
/// Returns a ballpark multiplier which gives a similar "feel" for how large scores should get when displayed in "classic" mode.
/// Returns a ballpark "classic" score which gives a similar "feel" to stable.
/// This is different per ruleset to match the different algorithms used in the scoring implementation.
/// </summary>
private static double getStandardisedToClassicMultiplier(int rulesetId)
/// <remarks>
/// The coefficients chosen here were determined by a least-squares fit performed over all beatmaps
/// with the goal of minimising the relative error of maximum possible base score (without bonus).
/// The constant coefficients (100000, 1 / 10d) - while being detrimental to the least-squares fit - are forced,
/// so that every 10 points in standardised mode converts to at least 1 point in classic mode.
/// This is done to account for bonus judgements in a way that does not reorder scores.
/// </remarks>
private static long convertStandardisedToClassic(int rulesetId, long standardisedTotalScore, int objectCount)
{
double multiplier;
switch (rulesetId)
{
// For non-legacy rulesets, just go with the same as the osu! ruleset.
// This is arbitrary, but at least allows the setting to do something to the score.
default:
case 0:
multiplier = 36;
break;
return (long)Math.Round((objectCount * objectCount * 32.57 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE);
case 1:
multiplier = 22;
break;
return (long)Math.Round((objectCount * 1109 + 100000) * standardisedTotalScore / ScoreProcessor.MAX_SCORE);
case 2:
multiplier = 28;
break;
return (long)Math.Round(Math.Pow(standardisedTotalScore / ScoreProcessor.MAX_SCORE * objectCount, 2) * 21.62 + standardisedTotalScore / 10d);
case 3:
multiplier = 16;
break;
default:
return standardisedTotalScore;
}
return multiplier;
}
public static int? GetCountGeki(this ScoreInfo scoreInfo)