From 45751dd1f2cf5610daf4d9cb966347fb453ef570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 Sep 2023 14:05:58 +0200 Subject: [PATCH 1/3] Minimum viable changes for ruleset-specific scoring test scenes --- .../TestSceneScoring.cs | 33 ++ .../Tests/Visual/Gameplay/ScoringTestScene.cs | 556 +++++++++--------- 2 files changed, 311 insertions(+), 278 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs rename osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs => osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs (53%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs new file mode 100644 index 0000000000..5dbfc8d3a1 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -0,0 +1,33 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual.Gameplay; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public partial class TestSceneScoring : ScoringTestScene + { + protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + + protected override IBeatmap CreateBeatmap(int maxCombo) + { + var beatmap = new OsuBeatmap(); + for (int i = 0; i < maxCombo; i++) + beatmap.HitObjects.Add(new HitCircle()); + return beatmap; + } + + protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs similarity index 53% rename from osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs rename to osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 5db05ea67c..ecf55419a2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -14,15 +14,13 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring.Legacy; using osuTK; @@ -31,8 +29,14 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public partial class TestSceneScoring : OsuTestScene + public abstract partial class ScoringTestScene : OsuTestScene { + protected abstract ScoreProcessor CreateScoreProcessor(); + protected abstract IBeatmap CreateBeatmap(int maxCombo); + protected abstract JudgementResult CreatePerfectJudgementResult(); + protected abstract JudgementResult CreateNonPerfectJudgementResult(); + protected abstract JudgementResult CreateMissJudgementResult(); + private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; private SettingsCheckbox scaleToMax = null!; @@ -152,8 +156,8 @@ namespace osu.Game.Tests.Visual.Gameplay graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", colours.Green1, new OsuScoreProcessor(), ScoringMode.Standardised, standardisedVisible); - runForProcessor("lazer-classic", colours.Blue1, new OsuScoreProcessor(), ScoringMode.Classic, classicVisible); + runForProcessor("lazer-standardised", colours.Green1, CreateScoreProcessor(), ScoringMode.Standardised, standardisedVisible); + runForProcessor("lazer-classic", colours.Blue1, CreateScoreProcessor(), ScoringMode.Classic, classicVisible); runScoreV1(); runScoreV2(); @@ -274,20 +278,16 @@ namespace osu.Game.Tests.Visual.Gameplay private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode, BindableBool visibility) { int maxCombo = sliderMaxCombo.Current.Value; - - var beatmap = new OsuBeatmap(); - for (int i = 0; i < maxCombo; i++) - beatmap.HitObjects.Add(new HitCircle()); - + var beatmap = CreateBeatmap(maxCombo); processor.ApplyBeatmap(beatmap); runForAlgorithm(new ScoringAlgorithm { Name = name, Colour = colour, - ApplyHit = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }), - ApplyNonPerfect = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), - ApplyMiss = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), + ApplyHit = () => processor.ApplyResult(CreatePerfectJudgementResult()), + ApplyNonPerfect = () => processor.ApplyResult(CreateNonPerfectJudgementResult()), + ApplyMiss = () => processor.ApplyResult(CreateMissJudgementResult()), GetTotalScore = () => processor.GetDisplayScore(mode), Visible = visibility }); @@ -327,310 +327,310 @@ namespace osu.Game.Tests.Visual.Gameplay AccentColour = scoringAlgorithm.Colour, }); } - } - public class ScoringAlgorithm - { - public string Name { get; init; } = null!; - public Color4 Colour { get; init; } - public Action ApplyHit { get; init; } = () => { }; - public Action ApplyNonPerfect { get; init; } = () => { }; - public Action ApplyMiss { get; init; } = () => { }; - public Func GetTotalScore { get; init; } = null!; - public BindableBool Visible { get; init; } = null!; - } - - public partial class GraphContainer : Container, IHasCustomTooltip> - { - public readonly BindableList MissLocations = new BindableList(); - public readonly BindableList NonPerfectLocations = new BindableList(); - - public Bindable MaxCombo = new Bindable(); - - protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - - private readonly Box hoverLine; - - private readonly Container missLines; - private readonly Container verticalGridLines; - - public int CurrentHoverCombo { get; private set; } - - public GraphContainer() + private class ScoringAlgorithm { - InternalChild = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - Colour = OsuColour.Gray(0.1f), - RelativeSizeAxes = Axes.Both, - }, - verticalGridLines = new Container - { - RelativeSizeAxes = Axes.Both, - }, - hoverLine = new Box - { - Colour = Color4.Yellow, - RelativeSizeAxes = Axes.Y, - Origin = Anchor.TopCentre, - Alpha = 0, - Width = 1, - }, - missLines = new Container - { - Alpha = 0.6f, - RelativeSizeAxes = Axes.Both, - }, - Content, - } - }; - - MissLocations.BindCollectionChanged((_, _) => updateMissLocations()); - NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations()); - - MaxCombo.BindValueChanged(_ => - { - updateMissLocations(); - updateVerticalGridLines(); - }, true); + public string Name { get; init; } = null!; + public Color4 Colour { get; init; } + public Action ApplyHit { get; init; } = () => { }; + public Action ApplyNonPerfect { get; init; } = () => { }; + public Action ApplyMiss { get; init; } = () => { }; + public Func GetTotalScore { get; init; } = null!; + public BindableBool Visible { get; init; } = null!; } - private void updateVerticalGridLines() + public partial class GraphContainer : Container, IHasCustomTooltip> { - verticalGridLines.Clear(); + public readonly BindableList MissLocations = new BindableList(); + public readonly BindableList NonPerfectLocations = new BindableList(); - for (int i = 0; i < MaxCombo.Value; i++) + public Bindable MaxCombo = new Bindable(); + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + private readonly Box hoverLine; + + private readonly Container missLines; + private readonly Container verticalGridLines; + + public int CurrentHoverCombo { get; private set; } + + public GraphContainer() { - if (i % 100 == 0) + InternalChild = new Container { - verticalGridLines.AddRange(new Drawable[] + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { new Box { - Colour = OsuColour.Gray(0.2f), - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)i / MaxCombo.Value, + Colour = OsuColour.Gray(0.1f), + RelativeSizeAxes = Axes.Both, }, - new OsuSpriteText + verticalGridLines = new Container { - RelativePositionAxes = Axes.X, - X = (float)i / MaxCombo.Value, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = $"{i:#,0}", - Rotation = -30, - Y = -20, - } + RelativeSizeAxes = Axes.Both, + }, + hoverLine = new Box + { + Colour = Color4.Yellow, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.TopCentre, + Alpha = 0, + Width = 1, + }, + missLines = new Container + { + Alpha = 0.6f, + RelativeSizeAxes = Axes.Both, + }, + Content, + } + }; + + MissLocations.BindCollectionChanged((_, _) => updateMissLocations()); + NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations()); + + MaxCombo.BindValueChanged(_ => + { + updateMissLocations(); + updateVerticalGridLines(); + }, true); + } + + private void updateVerticalGridLines() + { + verticalGridLines.Clear(); + + for (int i = 0; i < MaxCombo.Value; i++) + { + if (i % 100 == 0) + { + verticalGridLines.AddRange(new Drawable[] + { + new Box + { + Colour = OsuColour.Gray(0.2f), + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)i / MaxCombo.Value, + }, + new OsuSpriteText + { + RelativePositionAxes = Axes.X, + X = (float)i / MaxCombo.Value, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = $"{i:#,0}", + Rotation = -30, + Y = -20, + } + }); + } + } + } + + private void updateMissLocations() + { + missLines.Clear(); + + foreach (int miss in MissLocations) + { + missLines.Add(new Box + { + Colour = Color4.Red, + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)miss / MaxCombo.Value, + }); + } + + foreach (int miss in NonPerfectLocations) + { + missLines.Add(new Box + { + Colour = Color4.Orange, + Origin = Anchor.TopCentre, + Width = 1, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + X = (float)miss / MaxCombo.Value, }); } } - } - private void updateMissLocations() - { - missLines.Clear(); - - foreach (int miss in MissLocations) + protected override bool OnHover(HoverEvent e) { - missLines.Add(new Box - { - Colour = Color4.Red, - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)miss / MaxCombo.Value, - }); + hoverLine.Show(); + return base.OnHover(e); } - foreach (int miss in NonPerfectLocations) + protected override void OnHoverLost(HoverLostEvent e) { - missLines.Add(new Box - { - Colour = Color4.Orange, - Origin = Anchor.TopCentre, - Width = 1, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - X = (float)miss / MaxCombo.Value, - }); + hoverLine.Hide(); + base.OnHoverLost(e); } - } - protected override bool OnHover(HoverEvent e) - { - hoverLine.Show(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hoverLine.Hide(); - base.OnHoverLost(e); - } - - protected override bool OnMouseMove(MouseMoveEvent e) - { - CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value); - - hoverLine.X = e.MousePosition.X; - return base.OnMouseMove(e); - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.Button == MouseButton.Left) - MissLocations.Add(CurrentHoverCombo); - else - NonPerfectLocations.Add(CurrentHoverCombo); - - return true; - } - - private GraphTooltip? tooltip; - - public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); - - public IEnumerable TooltipContent => Content; - - public partial class GraphTooltip : CompositeDrawable, ITooltip> - { - private readonly GraphContainer graphContainer; - - private readonly OsuTextFlowContainer textFlow; - - public GraphTooltip(GraphContainer graphContainer) + protected override bool OnMouseMove(MouseMoveEvent e) { - this.graphContainer = graphContainer; - AutoSizeAxes = Axes.Both; + CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value); - Masking = true; - CornerRadius = 10; + hoverLine.X = e.MousePosition.X; + return base.OnMouseMove(e); + } - InternalChildren = new Drawable[] + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.Button == MouseButton.Left) + MissLocations.Add(CurrentHoverCombo); + else + NonPerfectLocations.Add(CurrentHoverCombo); + + return true; + } + + private GraphTooltip? tooltip; + + public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); + + public IEnumerable TooltipContent => Content; + + public partial class GraphTooltip : CompositeDrawable, ITooltip> + { + private readonly GraphContainer graphContainer; + + private readonly OsuTextFlowContainer textFlow; + + public GraphTooltip(GraphContainer graphContainer) { - new Box + this.graphContainer = graphContainer; + AutoSizeAxes = Axes.Both; + + Masking = true; + CornerRadius = 10; + + InternalChildren = new Drawable[] { - Colour = OsuColour.Gray(0.15f), - RelativeSizeAxes = Axes.Both, + new Box + { + Colour = OsuColour.Gray(0.15f), + RelativeSizeAxes = Axes.Both, + }, + textFlow = new OsuTextFlowContainer + { + Colour = Color4.White, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + } + }; + } + + private int? lastContentCombo; + + public void SetContent(IEnumerable content) + { + int relevantCombo = graphContainer.CurrentHoverCombo; + + if (lastContentCombo == relevantCombo) + return; + + lastContentCombo = relevantCombo; + textFlow.Clear(); + + textFlow.AddParagraph($"At combo {relevantCombo}:"); + + foreach (var graph in content) + { + if (graph.Alpha == 0) continue; + + float valueAtHover = graph.Values.ElementAt(relevantCombo); + float ofTotal = valueAtHover / graph.Values.Last(); + + textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour); + } + } + + public void Move(Vector2 pos) => this.MoveTo(pos); + } + } + + private partial class LegendEntry : OsuClickableContainer, IHasAccentColour + { + public Color4 AccentColour { get; set; } + + public BindableBool Visible { get; } = new BindableBool(true); + + public readonly long FinalScore; + + private readonly string description; + private readonly LineGraph lineGraph; + + private OsuSpriteText descriptionText = null!; + private OsuSpriteText finalScoreText = null!; + + public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) + { + description = scoringAlgorithm.Name; + FinalScore = scoringAlgorithm.GetTotalScore(); + AccentColour = scoringAlgorithm.Colour; + Visible.BindTo(scoringAlgorithm.Visible); + + this.lineGraph = lineGraph; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X; + AutoSizeAxes = Content.AutoSizeAxes = Axes.Y; + + Children = new Drawable[] + { + descriptionText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, }, - textFlow = new OsuTextFlowContainer + finalScoreText = new OsuSpriteText { - Colour = Color4.White, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(10), + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Default.With(fixedWidth: true) } }; } - private int? lastContentCombo; - - public void SetContent(IEnumerable content) + protected override void LoadComplete() { - int relevantCombo = graphContainer.CurrentHoverCombo; - - if (lastContentCombo == relevantCombo) - return; - - lastContentCombo = relevantCombo; - textFlow.Clear(); - - textFlow.AddParagraph($"At combo {relevantCombo}:"); - - foreach (var graph in content) - { - if (graph.Alpha == 0) continue; - - float valueAtHover = graph.Values.ElementAt(relevantCombo); - float ofTotal = valueAtHover / graph.Values.Last(); - - textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour); - } + base.LoadComplete(); + Visible.BindValueChanged(_ => updateState(), true); + Action = Visible.Toggle; } - public void Move(Vector2 pos) => this.MoveTo(pos); - } - } - - public partial class LegendEntry : OsuClickableContainer, IHasAccentColour - { - public Color4 AccentColour { get; set; } - - public BindableBool Visible { get; } = new BindableBool(true); - - public readonly long FinalScore; - - private readonly string description; - private readonly LineGraph lineGraph; - - private OsuSpriteText descriptionText = null!; - private OsuSpriteText finalScoreText = null!; - - public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) - { - description = scoringAlgorithm.Name; - FinalScore = scoringAlgorithm.GetTotalScore(); - AccentColour = scoringAlgorithm.Colour; - Visible.BindTo(scoringAlgorithm.Visible); - - this.lineGraph = lineGraph; - } - - [BackgroundDependencyLoader] - private void load() - { - RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X; - AutoSizeAxes = Content.AutoSizeAxes = Axes.Y; - - Children = new Drawable[] + protected override bool OnHover(HoverEvent e) { - descriptionText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - finalScoreText = new OsuSpriteText - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Font = OsuFont.Default.With(fixedWidth: true) - } - }; - } + updateState(); + return true; + } - protected override void LoadComplete() - { - base.LoadComplete(); - Visible.BindValueChanged(_ => updateState(), true); - Action = Visible.Toggle; - } + protected override void OnHoverLost(HoverLostEvent e) + { + updateState(); + base.OnHoverLost(e); + } - protected override bool OnHover(HoverEvent e) - { - updateState(); - return true; - } + private void updateState() + { + Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour; - protected override void OnHoverLost(HoverLostEvent e) - { - updateState(); - base.OnHoverLost(e); - } - - private void updateState() - { - Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour; - - descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}"; - finalScoreText.Text = FinalScore.ToString("#,0"); - lineGraph.Alpha = Visible.Value ? 1 : 0; + descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}"; + finalScoreText.Text = FinalScore.ToString("#,0"); + lineGraph.Alpha = Visible.Value ? 1 : 0; + } } } } From 0c22ff2a8093efe5754f060dbab424af81571c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 10:13:53 +0200 Subject: [PATCH 2/3] Refactor further to allow extensibility to other rulesets --- .../TestSceneScoring.cs | 118 +++++++++- .../Tests/Visual/Gameplay/ScoringTestScene.cs | 207 +++++++----------- 2 files changed, 187 insertions(+), 138 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs index 5dbfc8d3a1..36485b93ab 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -1,6 +1,7 @@ // 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.Game.Beatmaps; using osu.Game.Rulesets.Judgements; @@ -16,8 +17,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public partial class TestSceneScoring : ScoringTestScene { - protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); - protected override IBeatmap CreateBeatmap(int maxCombo) { var beatmap = new OsuBeatmap(); @@ -26,8 +25,117 @@ namespace osu.Game.Rulesets.Osu.Tests return beatmap; } - protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; - protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; - protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(); + protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo); + protected override ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode) => new OsuProcessorBasedScoringAlgorithm(beatmap, mode); + + private const int base_great = 300; + private const int base_ok = 100; + + private class ScoreV1 : IScoringAlgorithm + { + private int currentCombo; + + // this corresponds to stable's `ScoreMultiplier`. + // value is chosen arbitrarily, towards the upper range. + private const float score_multiplier = 4; + + public void ApplyHit() => applyHitV1(base_great); + public void ApplyNonPerfect() => applyHitV1(base_ok); + 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 * score_multiplier)); + + currentCombo++; + } + + public long TotalScore { get; private set; } + } + + private class ScoreV2 : IScoringAlgorithm + { + private int currentCombo; + private double comboPortion; + private double currentBaseScore; + private double maxBaseScore; + private int currentHits; + + private readonly double comboPortionMax; + private readonly int maxCombo; + + public ScoreV2(int maxCombo) + { + this.maxCombo = maxCombo; + + for (int i = 0; i < this.maxCombo; i++) + ApplyHit(); + + comboPortionMax = comboPortion; + + currentCombo = 0; + comboPortion = 0; + currentBaseScore = 0; + maxBaseScore = 0; + currentHits = 0; + } + + public void ApplyHit() => applyHitV2(base_great); + public void ApplyNonPerfect() => applyHitV2(base_ok); + + private void applyHitV2(int baseScore) + { + maxBaseScore += base_great; + currentBaseScore += baseScore; + comboPortion += baseScore * (1 + ++currentCombo / 10.0); + + currentHits++; + } + + public void ApplyMiss() + { + currentHits++; + maxBaseScore += base_great; + currentCombo = 0; + } + + public long TotalScore + { + get + { + double accuracy = currentBaseScore / maxBaseScore; + + return (int)Math.Round + ( + 700000 * comboPortion / comboPortionMax + + 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) + ); + } + } + } + + private class OsuProcessorBasedScoringAlgorithm : ProcessorBasedScoringAlgorithm + { + public OsuProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + : base(beatmap, mode) + { + } + + protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); + protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; + protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; + protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; + } } } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index ecf55419a2..331f1bb9aa 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -1,7 +1,6 @@ // 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 System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -31,11 +30,11 @@ namespace osu.Game.Tests.Visual.Gameplay { public abstract partial class ScoringTestScene : OsuTestScene { - protected abstract ScoreProcessor CreateScoreProcessor(); protected abstract IBeatmap CreateBeatmap(int maxCombo); - protected abstract JudgementResult CreatePerfectJudgementResult(); - protected abstract JudgementResult CreateNonPerfectJudgementResult(); - protected abstract JudgementResult CreateMissJudgementResult(); + + protected abstract IScoringAlgorithm CreateScoreV1(); + protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo); + protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode); private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; @@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Text = $"Left click to add miss\nRight click to add OK/{base_ok}", + Text = "Left click to add miss\nRight click to add OK", Margin = new MarginPadding { Top = 20 } } } @@ -148,19 +147,28 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private const int base_great = 300; - private const int base_ok = 100; - private void rerun() { graphs.Clear(); legend.Clear(); - runForProcessor("lazer-standardised", colours.Green1, CreateScoreProcessor(), ScoringMode.Standardised, standardisedVisible); - runForProcessor("lazer-classic", colours.Blue1, CreateScoreProcessor(), ScoringMode.Classic, classicVisible); + runForProcessor("lazer-standardised", colours.Green1, ScoringMode.Standardised, standardisedVisible); + runForProcessor("lazer-classic", colours.Blue1, ScoringMode.Classic, classicVisible); - runScoreV1(); - runScoreV2(); + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV1 (classic)", + Colour = colours.Purple1, + Algorithm = CreateScoreV1(), + Visible = scoreV1Visible + }); + runForAlgorithm(new ScoringAlgorithmInfo + { + Name = "ScoreV2", + Colour = colours.Red1, + Algorithm = CreateScoreV2(sliderMaxCombo.Current.Value), + Visible = scoreV2Visible + }); rescalePlots(); } @@ -181,119 +189,22 @@ namespace osu.Game.Tests.Visual.Gameplay } } - private void runScoreV1() - { - int totalScore = 0; - int currentCombo = 0; - - void applyHitV1(int baseScore) - { - if (baseScore == 0) - { - currentCombo = 0; - return; - } - - // this corresponds to stable's `ScoreMultiplier`. - // value is chosen arbitrarily, towards the upper range. - const float score_multiplier = 4; - - totalScore += baseScore; - - // combo multiplier - // ReSharper disable once PossibleLossOfFraction - totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier)); - - currentCombo++; - } - - runForAlgorithm(new ScoringAlgorithm - { - Name = "ScoreV1 (classic)", - Colour = colours.Purple1, - ApplyHit = () => applyHitV1(base_great), - ApplyNonPerfect = () => applyHitV1(base_ok), - ApplyMiss = () => applyHitV1(0), - GetTotalScore = () => totalScore, - Visible = scoreV1Visible - }); - } - - private void runScoreV2() - { - int maxCombo = sliderMaxCombo.Current.Value; - - int currentCombo = 0; - double comboPortion = 0; - double currentBaseScore = 0; - double maxBaseScore = 0; - int currentHits = 0; - - for (int i = 0; i < maxCombo; i++) - applyHitV2(base_great); - - double comboPortionMax = comboPortion; - - currentCombo = 0; - comboPortion = 0; - currentBaseScore = 0; - maxBaseScore = 0; - currentHits = 0; - - void applyHitV2(int baseScore) - { - maxBaseScore += base_great; - currentBaseScore += baseScore; - comboPortion += baseScore * (1 + ++currentCombo / 10.0); - - currentHits++; - } - - runForAlgorithm(new ScoringAlgorithm - { - Name = "ScoreV2", - Colour = colours.Red1, - ApplyHit = () => applyHitV2(base_great), - ApplyNonPerfect = () => applyHitV2(base_ok), - ApplyMiss = () => - { - currentHits++; - maxBaseScore += base_great; - currentCombo = 0; - }, - GetTotalScore = () => - { - double accuracy = currentBaseScore / maxBaseScore; - - return (int)Math.Round - ( - 700000 * comboPortion / comboPortionMax + - 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) - ); - }, - Visible = scoreV2Visible - }); - } - - private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode, BindableBool visibility) + private void runForProcessor(string name, Color4 colour, ScoringMode scoringMode, BindableBool visibility) { int maxCombo = sliderMaxCombo.Current.Value; var beatmap = CreateBeatmap(maxCombo); - processor.ApplyBeatmap(beatmap); + var algorithm = CreateScoreAlgorithm(beatmap, scoringMode); - runForAlgorithm(new ScoringAlgorithm + runForAlgorithm(new ScoringAlgorithmInfo { Name = name, Colour = colour, - ApplyHit = () => processor.ApplyResult(CreatePerfectJudgementResult()), - ApplyNonPerfect = () => processor.ApplyResult(CreateNonPerfectJudgementResult()), - ApplyMiss = () => processor.ApplyResult(CreateMissJudgementResult()), - GetTotalScore = () => processor.GetDisplayScore(mode), + Algorithm = algorithm, Visible = visibility }); } - private void runForAlgorithm(ScoringAlgorithm scoringAlgorithm) + private void runForAlgorithm(ScoringAlgorithmInfo algorithmInfo) { int maxCombo = sliderMaxCombo.Current.Value; @@ -302,43 +213,73 @@ namespace osu.Game.Tests.Visual.Gameplay for (int i = 0; i < maxCombo; i++) { if (graphs.MissLocations.Contains(i)) - scoringAlgorithm.ApplyMiss(); + algorithmInfo.Algorithm.ApplyMiss(); else if (graphs.NonPerfectLocations.Contains(i)) - scoringAlgorithm.ApplyNonPerfect(); + algorithmInfo.Algorithm.ApplyNonPerfect(); else - scoringAlgorithm.ApplyHit(); + algorithmInfo.Algorithm.ApplyHit(); - results.Add(scoringAlgorithm.GetTotalScore()); + results.Add(algorithmInfo.Algorithm.TotalScore); } LineGraph graph; graphs.Add(graph = new LineGraph { - Name = scoringAlgorithm.Name, + Name = algorithmInfo.Name, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - LineColour = scoringAlgorithm.Colour, + LineColour = algorithmInfo.Colour, Values = results }); - legend.Add(new LegendEntry(scoringAlgorithm, graph) + legend.Add(new LegendEntry(algorithmInfo, graph) { - AccentColour = scoringAlgorithm.Colour, + AccentColour = algorithmInfo.Colour, }); } - private class ScoringAlgorithm + private class ScoringAlgorithmInfo { public string Name { get; init; } = null!; public Color4 Colour { get; init; } - public Action ApplyHit { get; init; } = () => { }; - public Action ApplyNonPerfect { get; init; } = () => { }; - public Action ApplyMiss { get; init; } = () => { }; - public Func GetTotalScore { get; init; } = null!; + public IScoringAlgorithm Algorithm { get; init; } = null!; public BindableBool Visible { get; init; } = null!; } + protected interface IScoringAlgorithm + { + void ApplyHit(); + void ApplyNonPerfect(); + void ApplyMiss(); + + long TotalScore { get; } + } + + protected abstract class ProcessorBasedScoringAlgorithm : IScoringAlgorithm + { + protected abstract ScoreProcessor CreateScoreProcessor(); + protected abstract JudgementResult CreatePerfectJudgementResult(); + protected abstract JudgementResult CreateNonPerfectJudgementResult(); + protected abstract JudgementResult CreateMissJudgementResult(); + + private readonly ScoreProcessor scoreProcessor; + private readonly ScoringMode mode; + + protected ProcessorBasedScoringAlgorithm(IBeatmap beatmap, ScoringMode mode) + { + this.mode = mode; + scoreProcessor = CreateScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + } + + public void ApplyHit() => scoreProcessor.ApplyResult(CreatePerfectJudgementResult()); + public void ApplyNonPerfect() => scoreProcessor.ApplyResult(CreateNonPerfectJudgementResult()); + public void ApplyMiss() => scoreProcessor.ApplyResult(CreateMissJudgementResult()); + + public long TotalScore => scoreProcessor.GetDisplayScore(mode); + } + public partial class GraphContainer : Container, IHasCustomTooltip> { public readonly BindableList MissLocations = new BindableList(); @@ -572,12 +513,12 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuSpriteText descriptionText = null!; private OsuSpriteText finalScoreText = null!; - public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) + public LegendEntry(ScoringAlgorithmInfo scoringAlgorithmInfo, LineGraph lineGraph) { - description = scoringAlgorithm.Name; - FinalScore = scoringAlgorithm.GetTotalScore(); - AccentColour = scoringAlgorithm.Colour; - Visible.BindTo(scoringAlgorithm.Visible); + description = scoringAlgorithmInfo.Name; + FinalScore = scoringAlgorithmInfo.Algorithm.TotalScore; + AccentColour = scoringAlgorithmInfo.Colour; + Visible.BindTo(scoringAlgorithmInfo.Visible); this.lineGraph = lineGraph; } From ebdc501e5b6f3dbea0af959ca4fdbb96e3b3ba13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Sep 2023 14:19:49 +0200 Subject: [PATCH 3/3] Add example scenarios and configurable score multiplier --- .../TestSceneScoring.cs | 45 ++++++++++++++++--- .../Tests/Visual/Gameplay/ScoringTestScene.cs | 22 +++++---- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs index 36485b93ab..bb09328ab7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneScoring.cs @@ -3,6 +3,7 @@ using System; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Beatmaps; @@ -17,6 +18,12 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public partial class TestSceneScoring : ScoringTestScene { + private Bindable scoreMultiplier { get; } = new BindableDouble + { + Default = 4, + Value = 4 + }; + protected override IBeatmap CreateBeatmap(int maxCombo) { var beatmap = new OsuBeatmap(); @@ -25,10 +32,40 @@ namespace osu.Game.Rulesets.Osu.Tests return beatmap; } - protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1(); + 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 OsuProcessorBasedScoringAlgorithm(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 }); + }); + AddStep("set score with misses and OKs", () => + { + NonPerfectLocations.Clear(); + MissLocations.Clear(); + + NonPerfectLocations.AddRange(new[] { 9d, 19, 29, 39, 59, 69, 79, 89, 99 }); + 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 const int base_ok = 100; @@ -36,9 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests { private int currentCombo; - // this corresponds to stable's `ScoreMultiplier`. - // value is chosen arbitrarily, towards the upper range. - private const float score_multiplier = 4; + public BindableDouble ScoreMultiplier { get; } = new BindableDouble(); public void ApplyHit() => applyHitV1(base_great); public void ApplyNonPerfect() => applyHitV1(base_ok); @@ -56,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Tests // combo multiplier // ReSharper disable once PossibleLossOfFraction - TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier)); + TotalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * ScoreMultiplier.Value)); currentCombo++; } diff --git a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs index 331f1bb9aa..c213a17185 100644 --- a/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs +++ b/osu.Game/Tests/Visual/Gameplay/ScoringTestScene.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -13,6 +12,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -36,6 +36,10 @@ namespace osu.Game.Tests.Visual.Gameplay protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo); protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode); + protected Bindable MaxCombo => sliderMaxCombo.Current; + protected BindableList NonPerfectLocations => graphs.NonPerfectLocations; + protected BindableList MissLocations => graphs.MissLocations; + private GraphContainer graphs = null!; private SettingsSlider sliderMaxCombo = null!; private SettingsCheckbox scaleToMax = null!; @@ -50,8 +54,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuColour colours { get; set; } = null!; - [Test] - public void TestBasic() + [SetUpSteps] + public void SetUpSteps() { AddStep("setup tests", () => { @@ -130,24 +134,24 @@ namespace osu.Game.Tests.Visual.Gameplay } }; - sliderMaxCombo.Current.BindValueChanged(_ => rerun()); - scaleToMax.Current.BindValueChanged(_ => rerun()); + sliderMaxCombo.Current.BindValueChanged(_ => Rerun()); + scaleToMax.Current.BindValueChanged(_ => Rerun()); standardisedVisible.BindValueChanged(_ => rescalePlots()); classicVisible.BindValueChanged(_ => rescalePlots()); scoreV1Visible.BindValueChanged(_ => rescalePlots()); scoreV2Visible.BindValueChanged(_ => rescalePlots()); - graphs.MissLocations.BindCollectionChanged((_, __) => rerun()); - graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun()); + graphs.MissLocations.BindCollectionChanged((_, __) => Rerun()); + graphs.NonPerfectLocations.BindCollectionChanged((_, __) => Rerun()); graphs.MaxCombo.BindTo(sliderMaxCombo.Current); - rerun(); + Rerun(); }); } - private void rerun() + protected void Rerun() { graphs.Clear(); legend.Clear();