mirror of
https://github.com/ppy/osu.git
synced 2024-11-13 16:13:34 +08:00
Refactor further to allow extensibility to other rulesets
This commit is contained in:
parent
45751dd1f2
commit
0c22ff2a80
@ -1,6 +1,7 @@
|
||||
// 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;
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
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<int> 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<long> 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<LineGraph>, IHasCustomTooltip<IEnumerable<LineGraph>>
|
||||
{
|
||||
public readonly BindableList<double> MissLocations = new BindableList<double>();
|
||||
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user