1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 17:27:24 +08:00

Refactor further to allow extensibility to other rulesets

This commit is contained in:
Bartłomiej Dach 2023-09-13 10:13:53 +02:00
parent 45751dd1f2
commit 0c22ff2a80
No known key found for this signature in database
2 changed files with 187 additions and 138 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -16,8 +17,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture] [TestFixture]
public partial class TestSceneScoring : ScoringTestScene public partial class TestSceneScoring : ScoringTestScene
{ {
protected override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
protected override IBeatmap CreateBeatmap(int maxCombo) protected override IBeatmap CreateBeatmap(int maxCombo)
{ {
var beatmap = new OsuBeatmap(); var beatmap = new OsuBeatmap();
@ -26,8 +25,117 @@ namespace osu.Game.Rulesets.Osu.Tests
return beatmap; return beatmap;
} }
protected override JudgementResult CreatePerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }; protected override IScoringAlgorithm CreateScoreV1() => new ScoreV1();
protected override JudgementResult CreateNonPerfectJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }; protected override IScoringAlgorithm CreateScoreV2(int maxCombo) => new ScoreV2(maxCombo);
protected override JudgementResult CreateMissJudgementResult() => new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }; 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 };
}
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -31,11 +30,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public abstract partial class ScoringTestScene : OsuTestScene public abstract partial class ScoringTestScene : OsuTestScene
{ {
protected abstract ScoreProcessor CreateScoreProcessor();
protected abstract IBeatmap CreateBeatmap(int maxCombo); protected abstract IBeatmap CreateBeatmap(int maxCombo);
protected abstract JudgementResult CreatePerfectJudgementResult();
protected abstract JudgementResult CreateNonPerfectJudgementResult(); protected abstract IScoringAlgorithm CreateScoreV1();
protected abstract JudgementResult CreateMissJudgementResult(); protected abstract IScoringAlgorithm CreateScoreV2(int maxCombo);
protected abstract ProcessorBasedScoringAlgorithm CreateScoreAlgorithm(IBeatmap beatmap, ScoringMode mode);
private GraphContainer graphs = null!; private GraphContainer graphs = null!;
private SettingsSlider<int> sliderMaxCombo = null!; private SettingsSlider<int> sliderMaxCombo = null!;
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, 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 } 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() private void rerun()
{ {
graphs.Clear(); graphs.Clear();
legend.Clear(); legend.Clear();
runForProcessor("lazer-standardised", colours.Green1, CreateScoreProcessor(), ScoringMode.Standardised, standardisedVisible); runForProcessor("lazer-standardised", colours.Green1, ScoringMode.Standardised, standardisedVisible);
runForProcessor("lazer-classic", colours.Blue1, CreateScoreProcessor(), ScoringMode.Classic, classicVisible); runForProcessor("lazer-classic", colours.Blue1, ScoringMode.Classic, classicVisible);
runScoreV1(); runForAlgorithm(new ScoringAlgorithmInfo
runScoreV2(); {
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(); rescalePlots();
} }
@ -181,119 +189,22 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
private void runScoreV1() private void runForProcessor(string name, Color4 colour, ScoringMode scoringMode, BindableBool visibility)
{
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)
{ {
int maxCombo = sliderMaxCombo.Current.Value; int maxCombo = sliderMaxCombo.Current.Value;
var beatmap = CreateBeatmap(maxCombo); var beatmap = CreateBeatmap(maxCombo);
processor.ApplyBeatmap(beatmap); var algorithm = CreateScoreAlgorithm(beatmap, scoringMode);
runForAlgorithm(new ScoringAlgorithm runForAlgorithm(new ScoringAlgorithmInfo
{ {
Name = name, Name = name,
Colour = colour, Colour = colour,
ApplyHit = () => processor.ApplyResult(CreatePerfectJudgementResult()), Algorithm = algorithm,
ApplyNonPerfect = () => processor.ApplyResult(CreateNonPerfectJudgementResult()),
ApplyMiss = () => processor.ApplyResult(CreateMissJudgementResult()),
GetTotalScore = () => processor.GetDisplayScore(mode),
Visible = visibility Visible = visibility
}); });
} }
private void runForAlgorithm(ScoringAlgorithm scoringAlgorithm) private void runForAlgorithm(ScoringAlgorithmInfo algorithmInfo)
{ {
int maxCombo = sliderMaxCombo.Current.Value; int maxCombo = sliderMaxCombo.Current.Value;
@ -302,43 +213,73 @@ namespace osu.Game.Tests.Visual.Gameplay
for (int i = 0; i < maxCombo; i++) for (int i = 0; i < maxCombo; i++)
{ {
if (graphs.MissLocations.Contains(i)) if (graphs.MissLocations.Contains(i))
scoringAlgorithm.ApplyMiss(); algorithmInfo.Algorithm.ApplyMiss();
else if (graphs.NonPerfectLocations.Contains(i)) else if (graphs.NonPerfectLocations.Contains(i))
scoringAlgorithm.ApplyNonPerfect(); algorithmInfo.Algorithm.ApplyNonPerfect();
else else
scoringAlgorithm.ApplyHit(); algorithmInfo.Algorithm.ApplyHit();
results.Add(scoringAlgorithm.GetTotalScore()); results.Add(algorithmInfo.Algorithm.TotalScore);
} }
LineGraph graph; LineGraph graph;
graphs.Add(graph = new LineGraph graphs.Add(graph = new LineGraph
{ {
Name = scoringAlgorithm.Name, Name = algorithmInfo.Name,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
LineColour = scoringAlgorithm.Colour, LineColour = algorithmInfo.Colour,
Values = results 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 string Name { get; init; } = null!;
public Color4 Colour { get; init; } public Color4 Colour { get; init; }
public Action ApplyHit { get; init; } = () => { }; public IScoringAlgorithm Algorithm { get; init; } = null!;
public Action ApplyNonPerfect { get; init; } = () => { };
public Action ApplyMiss { get; init; } = () => { };
public Func<long> GetTotalScore { get; init; } = null!;
public BindableBool Visible { 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<LineGraph>, IHasCustomTooltip<IEnumerable<LineGraph>> public partial class GraphContainer : Container<LineGraph>, IHasCustomTooltip<IEnumerable<LineGraph>>
{ {
public readonly BindableList<double> MissLocations = new BindableList<double>(); public readonly BindableList<double> MissLocations = new BindableList<double>();
@ -572,12 +513,12 @@ namespace osu.Game.Tests.Visual.Gameplay
private OsuSpriteText descriptionText = null!; private OsuSpriteText descriptionText = null!;
private OsuSpriteText finalScoreText = null!; private OsuSpriteText finalScoreText = null!;
public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph) public LegendEntry(ScoringAlgorithmInfo scoringAlgorithmInfo, LineGraph lineGraph)
{ {
description = scoringAlgorithm.Name; description = scoringAlgorithmInfo.Name;
FinalScore = scoringAlgorithm.GetTotalScore(); FinalScore = scoringAlgorithmInfo.Algorithm.TotalScore;
AccentColour = scoringAlgorithm.Colour; AccentColour = scoringAlgorithmInfo.Colour;
Visible.BindTo(scoringAlgorithm.Visible); Visible.BindTo(scoringAlgorithmInfo.Visible);
this.lineGraph = lineGraph; this.lineGraph = lineGraph;
} }