diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..dfdde0a325 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneScoring.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Judgements; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Scoring; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + public TestSceneScoring() + : base(supportsNonPerfectJudgements: false) + { + } + + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new CatchBeatmap(); + for (int i = 0; i < maxCombo; ++i) + beatmap.HitObjects.Add(new Fruit()); + return beatmap; + } + + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1 { ScoreMultiplier = { BindTarget = scoreMultiplier } }; + + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new CatchProcessorBasedScoringAlgorithm(beatmap, mode); + + [Test] + public void TestBasicScenarios() + { + AddStep("set up score multiplier", () => + { + scoreMultiplier.BindValueChanged(_ => Rerun()); + }); + AddStep("set max combo to 100", () => MaxCombo.Value = 100); + AddStep("set perfect score", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + }); + AddStep("set score with misses", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + MissLocations.AddRange(new[] { 24d, 49 }); + }); + AddSliderStep("adjust score multiplier", 0, 10, (int)scoreMultiplier.Default, multiplier => scoreMultiplier.Value = multiplier); + } + + private const int base_great = 300; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); + + public void ApplyHit() => applyHitV1(base_great); + + public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + public void ApplyMiss() => applyHitV1(0); + + private void applyHitV1(int baseScore) + { + if (baseScore == 0) + { + currentCombo = 0; + return; + } + + TotalScore += baseScore; + + // combo multiplier + // ReSharper disable once PossibleLossOfFraction + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value)); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + + private readonly double comboPortionMax; + + private const double combo_base = 4; + private const int combo_cap = 200; + + public ScoreV2(int maxCombo) + { + for (int i = 0; i < maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + + public void ApplyNonPerfect() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + private void applyHitV2(int baseScore) + { + comboPortion += baseScore * Math.Min(Math.Max(0.5, Math.Log(++currentCombo, combo_base)), Math.Log(combo_cap, combo_base)); + } + + public void ApplyMiss() + { + currentCombo = 0; + } + + public long TotalScore + => (int)Math.Round(1000000 * comboPortion / comboPortionMax); // vast simplification, as we're not doing ticks here. + } + + private class CatchProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public CatchProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + + protected override JudgementResult CreatePerfectJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Great }; + + protected override JudgementResult CreateNonPerfectJudgementResult() => throw new NotSupportedException("catch does not have \"non-perfect\" judgements."); + + protected override JudgementResult CreateMissJudgementResult() => new CatchJudgementResult(new Fruit(), new CatchJudgement()) { Type = HitResult.Miss }; + } + } +} diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index c213a17185..de4688a6fe 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -40,6 +40,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected BindableList NonPerfectLocations => graphs.NonPerfectLocations; protected BindableList MissLocations => graphs.MissLocations; + private readonly bool supportsNonPerfectJudgements; + private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; private SettingsCheckbox scaleToMax = null!; @@ -54,11 +56,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuColour colours { get; set; } = null!; + protected ScoringTestScene(bool supportsNonPerfectJudgements = true) + { + this.supportsNonPerfectJudgements = supportsNonPerfectJudgements; + } + [SetUpSteps] public void SetUpSteps() { AddStep("setup tests", () => { + OsuTextFlowContainer clickExplainer; + Children = new Drawable[] { new Box @@ -79,7 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay { new Drawable[] { - graphs = new GraphContainer + graphs = new GraphContainer(supportsNonPerfectJudgements) { RelativeSizeAxes = Axes.Both, }, @@ -120,11 +129,10 @@ namespace osu.Game.Tests.Visual.Gameplay LabelText = "Rescale plots to 100%", Current = { Value = true, Default = true } }, - new OsuTextFlowContainer + clickExplainer = new OsuTextFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = "Left click to add miss\nRight click to add OK", Margin = new MarginPadding { Top = 20 } } } @@ -134,6 +142,10 @@ namespace osu.Game.Tests.Visual.Gameplay } }; + clickExplainer.AddParagraph("Left click to add miss"); + if (supportsNonPerfectJudgements) + clickExplainer.AddParagraph("Right click to add OK"); + sliderMaxCombo.Current.BindValueChanged(_ => Rerun()); scaleToMax.Current.BindValueChanged(_ => Rerun()); @@ -286,6 +298,8 @@ namespace osu.Game.Tests.Visual.Gameplay public partial class GraphContainer : Container, IHasCustomTooltip> { + private readonly bool supportsNonPerfectJudgements; + public readonly BindableList MissLocations = new BindableList(); public readonly BindableList NonPerfectLocations = new BindableList(); @@ -300,8 +314,9 @@ namespace osu.Game.Tests.Visual.Gameplay public int CurrentHoverCombo { get; private set; } - public GraphContainer() + public GraphContainer(bool supportsNonPerfectJudgements) { + this.supportsNonPerfectJudgements = supportsNonPerfectJudgements; InternalChild = new Container { RelativeSizeAxes = Axes.Both, @@ -432,7 +447,7 @@ namespace osu.Game.Tests.Visual.Gameplay { if (e.Button == MouseButton.Left) MissLocations.Add(CurrentHoverCombo); - else + else if (supportsNonPerfectJudgements) NonPerfectLocations.Add(CurrentHoverCombo); return true;