Skip to content

Commit

Permalink
Merge pull request #26630 from peppy/s-rank-change
Browse files Browse the repository at this point in the history
Change S rank to require no miss
  • Loading branch information
peppy authored Jan 24, 2024
2 parents 871683a + 6c169e3 commit b272d34
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 273 deletions.
2 changes: 1 addition & 1 deletion osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ protected override double GetComboScoreChange(JudgementResult result)
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
}

public override ScoreRank RankFromAccuracy(double accuracy)
public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
{
if (accuracy == accuracy_cutoff_x)
return ScoreRank.X;
Expand Down
18 changes: 18 additions & 0 deletions osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;

namespace osu.Game.Rulesets.Osu.Scoring
{
Expand All @@ -14,6 +16,22 @@ public OsuScoreProcessor()
{
}

public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
{
ScoreRank rank = base.RankFromScore(accuracy, results);

switch (rank)
{
case ScoreRank.S:
case ScoreRank.X:
if (results.GetValueOrDefault(HitResult.Miss) > 0)
rank = ScoreRank.A;
break;
}

return rank;
}

protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
}
Expand Down
18 changes: 18 additions & 0 deletions osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Scoring;

namespace osu.Game.Rulesets.Taiko.Scoring
{
Expand Down Expand Up @@ -33,6 +35,22 @@ protected override double GetComboScoreChange(JudgementResult result)
* strongScaleValue(result);
}

public override ScoreRank RankFromScore(double accuracy, IReadOnlyDictionary<HitResult, int> results)
{
ScoreRank rank = base.RankFromScore(accuracy, results);

switch (rank)
{
case ScoreRank.S:
case ScoreRank.X:
if (results.GetValueOrDefault(HitResult.Miss) > 0)
rank = ScoreRank.A;
break;
}

return rank;
}

public override int GetBaseScoreForResult(HitResult result)
{
switch (result)
Expand Down
72 changes: 61 additions & 11 deletions osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
Expand All @@ -23,6 +22,7 @@
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
Expand Down Expand Up @@ -59,14 +59,14 @@ public void TestDecodeManiaReplay()
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);

Assert.AreEqual(829_931, score.ScoreInfo.TotalScore);
Assert.AreEqual(829_931, score.ScoreInfo.LegacyTotalScore);
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);

Assert.IsTrue(score.ScoreInfo.Mods.Any(m => m is ManiaModClassic));
Assert.IsTrue(score.ScoreInfo.APIMods.Any(m => m.Acronym == "CL"));
Assert.IsTrue(score.ScoreInfo.ModsJson.Contains("CL"));

Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001));
Assert.That((2 * 300d + 1 * 200) / (3 * 305d), Is.EqualTo(score.ScoreInfo.Accuracy).Within(0.0001));
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);

Assert.That(score.Replay.Frames, Is.Not.Empty);
Expand Down Expand Up @@ -252,7 +252,49 @@ public void TestSoloScoreData()
}

[Test]
public void AccuracyAndRankOfStableScorePreserved()
public void AccuracyOfStableScoreRecomputed()
{
var memoryStream = new MemoryStream();

// local partial implementation of legacy score encoder
// this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION`
// and we want to emulate a stable score here
using (var sw = new SerializationWriter(memoryStream, true))
{
sw.Write((byte)3); // ruleset id (mania).
// mania is used intentionally as it is the only ruleset wherein default accuracy calculation is changed in lazer
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
sw.Write("username"); // irrelevant to this test
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
sw.Write((ushort)1); // count300
sw.Write((ushort)0); // count100
sw.Write((ushort)0); // count50
sw.Write((ushort)198); // countGeki (perfects / "rainbow 300s" in mania)
sw.Write((ushort)0); // countKatu
sw.Write((ushort)1); // countMiss
sw.Write(12345678); // total score, irrelevant to this test
sw.Write((ushort)1000); // max combo, irrelevant to this test
sw.Write(false); // full combo, irrelevant to this test
sw.Write((int)LegacyMods.Hidden); // mods
sw.Write(string.Empty); // hp graph, irrelevant
sw.Write(DateTime.Now); // date, irrelevant
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
sw.Write((long)1234); // legacy online ID, irrelevant
}

memoryStream.Seek(0, SeekOrigin.Begin);
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);

Assert.Multiple(() =>
{
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 305 + 300) / (200 * 305)));
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
});
}

[Test]
public void RankOfStableScoreUsesLazerDefinitions()
{
var memoryStream = new MemoryStream();

Expand All @@ -266,12 +308,12 @@ public void AccuracyAndRankOfStableScorePreserved()
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
sw.Write("username"); // irrelevant to this test
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
sw.Write((ushort)198); // count300
sw.Write((ushort)195); // count300
sw.Write((ushort)1); // count100
sw.Write((ushort)0); // count50
sw.Write((ushort)4); // count50
sw.Write((ushort)0); // countGeki
sw.Write((ushort)0); // countKatu
sw.Write((ushort)1); // countMiss
sw.Write((ushort)0); // countMiss
sw.Write(12345678); // total score, irrelevant to this test
sw.Write((ushort)1000); // max combo, irrelevant to this test
sw.Write(false); // full combo, irrelevant to this test
Expand All @@ -287,13 +329,13 @@ public void AccuracyAndRankOfStableScorePreserved()

Assert.Multiple(() =>
{
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300)));
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
// In stable this would be an A because there are over 1% 50s. But that's not a thing in lazer.
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
});
}

[Test]
public void AccuracyAndRankOfLazerScorePreserved()
public void AccuracyRankAndTotalScoreOfLazerScorePreserved()
{
var ruleset = new OsuRuleset().RulesetInfo;

Expand Down Expand Up @@ -321,8 +363,10 @@ public void AccuracyAndRankOfLazerScorePreserved()

Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.TotalScore, Is.EqualTo(284_537));
Assert.That(decodedAfterEncode.ScoreInfo.LegacyTotalScore, Is.Null);
Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30)));
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
});
}

Expand Down Expand Up @@ -415,6 +459,12 @@ public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_V
Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
},
// needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die
// when trying to recompute total score.
HitObjects =
{
new HitCircle()
}
});
}
Expand Down
26 changes: 1 addition & 25 deletions osu.Game.Tests/Database/BackgroundDataStoreProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
Expand Down Expand Up @@ -210,31 +211,6 @@ public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001));
}

[Test]
public void TestNonLegacyScoreNotSubjectToUpgrades()
{
ScoreInfo scoreInfo = null!;
TestBackgroundDataStoreProcessor processor = null!;

AddStep("Add score which requires upgrade (and has beatmap)", () =>
{
Realm.Write(r =>
{
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
{
TotalScoreVersion = 30000005,
LegacyTotalScore = 123456,
});
});
});

AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);

AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005));
}

public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
{
protected override int TimeToSleepDuringGameplay => 10;
Expand Down
19 changes: 11 additions & 8 deletions osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
Expand Down Expand Up @@ -96,6 +97,14 @@ private ScoreInfo createScore(double accuracy, Ruleset ruleset)
{
var scoreProcessor = ruleset.CreateScoreProcessor();

var statistics = new Dictionary<HitResult, int>
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 },
};

return new ScoreInfo
{
User = new APIUser
Expand All @@ -109,15 +118,9 @@ private ScoreInfo createScore(double accuracy, Ruleset ruleset)
TotalScore = 2845370,
Accuracy = accuracy,
MaxCombo = 999,
Rank = scoreProcessor.RankFromAccuracy(accuracy),
Rank = scoreProcessor.RankFromScore(accuracy, statistics),
Date = DateTimeOffset.Now,
Statistics =
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 },
}
Statistics = statistics,
};
}
}
Expand Down
21 changes: 12 additions & 9 deletions osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
Expand Down Expand Up @@ -71,15 +72,16 @@ public void TestScaling()

private int onlineScoreID = 1;

[TestCase(1, ScoreRank.X)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.2, ScoreRank.D)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
[TestCase(1, ScoreRank.X, 0)]
[TestCase(0.9999, ScoreRank.S, 0)]
[TestCase(0.975, ScoreRank.S, 0)]
[TestCase(0.975, ScoreRank.A, 1)]
[TestCase(0.925, ScoreRank.A, 5)]
[TestCase(0.85, ScoreRank.B, 9)]
[TestCase(0.75, ScoreRank.C, 11)]
[TestCase(0.5, ScoreRank.D, 21)]
[TestCase(0.2, ScoreRank.D, 51)]
public void TestResultsWithPlayer(double accuracy, ScoreRank rank, int missCount)
{
TestResultsScreen screen = null;

Expand All @@ -91,6 +93,7 @@ public void TestResultsWithPlayer(double accuracy, ScoreRank rank)
score.HitEvents = TestSceneStatisticsPanel.CreatePositionDistributedHitEvents();
score.Accuracy = accuracy;
score.Rank = rank;
score.Statistics[HitResult.Miss] = missCount;
return screen = createResultsScreen(score);
});
Expand Down
Loading

0 comments on commit b272d34

Please sign in to comment.