Initial implementation of ScoreV2

This commit is contained in:
Dan Balasescu 2023-05-09 19:33:33 +09:00
parent 5afe57033d
commit 3c3c812ed6
15 changed files with 300 additions and 361 deletions

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor();
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this);

View File

@ -1,17 +1,87 @@
// 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.
using System;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Scoring
{
public partial class CatchScoreProcessor : ScoreProcessor
{
public CatchScoreProcessor()
: base(new CatchRuleset())
private const int combo_cap = 200;
private const double combo_base = 4;
protected override double ClassicScoreMultiplier => 28;
private double tinyDropletScale;
private int maximumTinyDroplets;
private int hitTinyDroplets;
public CatchScoreProcessor(Ruleset ruleset)
: base(ruleset)
{
}
protected override double ClassicScoreMultiplier => 28;
protected override double ComputeTotalScore()
{
double fruitHitsRatio = maximumTinyDroplets == 0 ? 0 : (double)hitTinyDroplets / maximumTinyDroplets;
const int tiny_droplets_portion = 400000;
return
(int)Math.Round
((
((1000000 - tiny_droplets_portion) + tiny_droplets_portion * (1 - tinyDropletScale)) * ComboPortion / MaxComboPortion +
tiny_droplets_portion * tinyDropletScale * fruitHitsRatio +
BonusPortion
) * ScoreMultiplier);
}
protected override void AddScoreChange(JudgementResult result)
{
var change = computeScoreChange(result);
ComboPortion += change.combo;
BonusPortion += change.bonus;
hitTinyDroplets += change.tinyDropletHits;
}
protected override void RemoveScoreChange(JudgementResult result)
{
var change = computeScoreChange(result);
ComboPortion -= change.combo;
BonusPortion -= change.bonus;
hitTinyDroplets -= change.tinyDropletHits;
}
private (double combo, double bonus, int tinyDropletHits) computeScoreChange(JudgementResult result)
{
if (result.HitObject is TinyDroplet)
return (0, 0, 1);
if (result.Type.IsBonus())
return (0, Judgement.ToNumericResult(result.Type), 0);
return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(combo_cap, combo_base)), 0, 0);
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);
if (storeResults)
{
maximumTinyDroplets = hitTinyDroplets;
if (maximumTinyDroplets + MaxBasicJudgements == 0)
tinyDropletScale = 0;
else
tinyDropletScale = (double)maximumTinyDroplets / (maximumTinyDroplets + MaxBasicJudgements);
}
hitTinyDroplets = 0;
}
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Mania
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableManiaRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime);

View File

@ -1,23 +1,54 @@
// 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.
#nullable disable
using System;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Scoring
{
internal partial class ManiaScoreProcessor : ScoreProcessor
public partial class ManiaScoreProcessor : ScoreProcessor
{
public ManiaScoreProcessor()
: base(new ManiaRuleset())
private const double combo_base = 4;
protected override double ClassicScoreMultiplier => 16;
public ManiaScoreProcessor(Ruleset ruleset)
: base(ruleset)
{
}
protected override double DefaultAccuracyPortion => 0.99;
protected override double ComputeTotalScore()
{
return
(int)Math.Round
((
200000 * ComboPortion / MaxComboPortion +
800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
BonusPortion
) * ScoreMultiplier);
}
protected override double DefaultComboPortion => 0.01;
protected override void AddScoreChange(JudgementResult result)
{
var change = computeScoreChange(result);
ComboPortion += change.combo;
BonusPortion += change.bonus;
}
protected override double ClassicScoreMultiplier => 16;
protected override void RemoveScoreChange(JudgementResult result)
{
var change = computeScoreChange(result);
ComboPortion -= change.combo;
BonusPortion -= change.bonus;
}
private (double combo, double bonus) computeScoreChange(JudgementResult result)
{
if (result.Type.IsBonus())
return (0, Judgement.ToNumericResult(result.Type));
return (Judgement.ToNumericResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0);
}
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor();
public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(this);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this);

View File

@ -1,38 +1,29 @@
// 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.
#nullable disable
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using System;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public partial class OsuScoreProcessor : ScoreProcessor
{
public OsuScoreProcessor()
: base(new OsuRuleset())
protected override double ClassicScoreMultiplier => 36;
public OsuScoreProcessor(Ruleset ruleset)
: base(ruleset)
{
}
protected override double ClassicScoreMultiplier => 36;
protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
protected override double ComputeTotalScore()
{
switch (hitObject)
{
case HitCircle:
return new OsuHitCircleJudgementResult(hitObject, judgement);
default:
return new OsuJudgementResult(hitObject, judgement);
}
return
(int)Math.Round
((
700000 * ComboPortion / MaxComboPortion +
300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
BonusPortion
) * ScoreMultiplier);
}
}
}

View File

@ -1,23 +1,63 @@
// 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.
#nullable disable
using System;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Scoring
{
internal partial class TaikoScoreProcessor : ScoreProcessor
public partial class TaikoScoreProcessor : ScoreProcessor
{
public TaikoScoreProcessor()
: base(new TaikoRuleset())
private const double combo_base = 4;
protected override double ClassicScoreMultiplier => 22;
public TaikoScoreProcessor(Ruleset ruleset)
: base(ruleset)
{
}
protected override double DefaultAccuracyPortion => 0.75;
protected override double ComputeTotalScore()
{
return
(int)Math.Round
((
250000 * ComboPortion / MaxComboPortion +
750000 * Math.Pow(Accuracy.Value, 3.6) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
BonusPortion
) * ScoreMultiplier);
}
protected override double DefaultComboPortion => 0.25;
protected override void AddScoreChange(JudgementResult result)
{
var change = computeScoreChange(result);
BonusPortion += change.bonus;
ComboPortion += change.combo;
}
protected override double ClassicScoreMultiplier => 22;
protected override void RemoveScoreChange(JudgementResult result)
{
var change = computeScoreChange(result);
BonusPortion -= change.bonus;
ComboPortion -= change.combo;
}
private (double combo, double bonus) computeScoreChange(JudgementResult result)
{
double hitValue = Judgement.ToNumericResult(result.Type);
if (result.HitObject is StrongNestedHitObject strong)
{
double strongBonus = strong.Parent is DrumRollTick ? 3 : 7;
hitValue *= strongBonus;
}
if (result.Type.IsBonus())
return (0, hitValue);
return (hitValue * Math.Min(Math.Max(0.5, Math.Log(result.ComboAtJudgement, combo_base)), Math.Log(400, combo_base)), 0);
}
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor();
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor();

View File

@ -157,7 +157,8 @@ namespace osu.Game.Online.Spectator
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
// Todo:
// TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, scoreInfo);
}
protected override void Dispose(bool isDisposing)

View File

@ -66,7 +66,9 @@ namespace osu.Game.Rulesets.Difficulty
// calculate total score
ScoreProcessor scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = perfectPlay.Mods;
perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
// Todo:
// perfectPlay.TotalScore = scoreProcessor.ComputeScore(ScoringMode.Standardised, perfectPlay);
// compute rank achieved
// default to SS, then adjust the rank with mods

View File

@ -65,7 +65,9 @@ namespace osu.Game.Rulesets.Mods
scoreProcessor.PopulateScore(score);
score.Statistics[result.Type]++;
return scoreProcessor.ComputeAccuracy(score);
// Todo:
return 0;
// return scoreProcessor.ComputeAccuracy(score);
}
}
}

View File

@ -232,7 +232,7 @@ namespace osu.Game.Rulesets
/// Creates a <see cref="ScoreProcessor"/> for this <see cref="Ruleset"/>.
/// </summary>
/// <returns>The score processor.</returns>
public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
public virtual ScoreProcessor CreateScoreProcessor() => new DefaultScoreProcessor(this);
/// <summary>
/// Creates a <see cref="HealthProcessor"/> for this <see cref="Ruleset"/>.
@ -381,4 +381,23 @@ namespace osu.Game.Rulesets
/// </summary>
public virtual RulesetSetupSection? CreateEditorSetupSection() => null;
}
public partial class DefaultScoreProcessor : ScoreProcessor
{
public DefaultScoreProcessor(Ruleset ruleset)
: base(ruleset)
{
}
protected override double ComputeTotalScore()
{
return
(int)Math.Round
((
700000 * ComboPortion / MaxComboPortion +
300000 * Math.Pow(Accuracy.Value, 10) * ((double)CurrentBasicJudgements / MaxBasicJudgements) +
BonusPortion
) * ScoreMultiplier);
}
}
}

View File

@ -4,11 +4,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Localisation;
@ -20,8 +18,10 @@ using osu.Game.Scoring;
namespace osu.Game.Rulesets.Scoring
{
public partial class ScoreProcessor : JudgementProcessor
public abstract partial class ScoreProcessor : JudgementProcessor
{
protected const double MAX_SCORE = 1000000;
private const double accuracy_cutoff_x = 1;
private const double accuracy_cutoff_s = 0.95;
private const double accuracy_cutoff_a = 0.9;
@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Scoring
private const double accuracy_cutoff_c = 0.7;
private const double accuracy_cutoff_d = 0;
private const double max_score = 1000000;
/// <summary>
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
/// </summary>
@ -89,16 +87,6 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public IReadOnlyList<HitEvent> HitEvents => hitEvents;
/// <summary>
/// The default portion of <see cref="max_score"/> awarded for hitting <see cref="HitObject"/>s accurately. Defaults to 30%.
/// </summary>
protected virtual double DefaultAccuracyPortion => 0.3;
/// <summary>
/// The default portion of <see cref="max_score"/> awarded for achieving a high combo. Default to 70%.
/// </summary>
protected virtual double DefaultComboPortion => 0.7;
/// <summary>
/// An arbitrary multiplier to scale scores in the <see cref="ScoringMode.Classic"/> scoring mode.
/// </summary>
@ -109,8 +97,45 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public readonly Ruleset Ruleset;
private readonly double accuracyPortion;
private readonly double comboPortion;
/// <summary>
/// The sum of all basic judgements at the current time.
/// </summary>
private double currentBasicScore;
/// <summary>
/// The maximum sum of basic judgements at the current time.
/// </summary>
private double currentMaxBasicScore;
/// <summary>
/// The total count of basic judgements in the beatmap.
/// </summary>
protected int MaxBasicJudgements { get; private set; }
/// <summary>
/// The current count of basic judgements by the player.
/// </summary>
protected int CurrentBasicJudgements { get; private set; }
/// <summary>
/// The current combo score.
/// </summary>
protected double ComboPortion { get; set; }
/// <summary>
/// The maximum achievable combo score.
/// </summary>
protected double MaxComboPortion { get; private set; }
/// <summary>
/// The current bonus score.
/// </summary>
protected double BonusPortion { get; set; }
/// <summary>
/// The total score multiplier.
/// </summary>
protected double ScoreMultiplier { get; private set; } = 1;
public Dictionary<HitResult, int> MaximumStatistics
{
@ -123,27 +148,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
private ScoringValues maximumScoringValues;
/// <summary>
/// Scoring values for the current play assuming all perfect hits.
/// </summary>
/// <remarks>
/// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
/// </remarks>
private ScoringValues currentMaximumScoringValues;
/// <summary>
/// Scoring values for the current play.
/// </summary>
private ScoringValues currentScoringValues;
/// <summary>
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
/// Only populated via <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoreInfo)"/> or <see cref="ResetFromReplayFrame"/>.
/// </summary>
private HitResult? maxBasicResult;
private bool beatmapApplied;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
@ -152,18 +156,10 @@ namespace osu.Game.Rulesets.Scoring
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject? lastHitObject;
private double scoreMultiplier = 1;
public ScoreProcessor(Ruleset ruleset)
protected ScoreProcessor(Ruleset ruleset)
{
Ruleset = ruleset;
accuracyPortion = DefaultAccuracyPortion;
comboPortion = DefaultComboPortion;
if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion))
throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1.");
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
{
@ -175,10 +171,10 @@ namespace osu.Game.Rulesets.Scoring
Mode.ValueChanged += _ => updateScore();
Mods.ValueChanged += mods =>
{
scoreMultiplier = 1;
ScoreMultiplier = 1;
foreach (var m in mods.NewValue)
scoreMultiplier *= m.ScoreMultiplier;
ScoreMultiplier *= m.ScoreMultiplier;
updateScore();
};
@ -200,10 +196,6 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
// Always update the maximum scoring values.
applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
if (!result.Type.IsScorable())
return;
@ -212,8 +204,13 @@ namespace osu.Game.Rulesets.Scoring
else if (result.Type.BreaksCombo())
Combo.Value = 0;
applyResult(result.Type, ref currentScoringValues);
currentScoringValues.MaxCombo = HighestCombo.Value;
if (result.Type.IsBasic())
CurrentBasicJudgements++;
currentMaxBasicScore += Judgement.ToNumericResult(result.Judgement.MaxResult);
currentBasicScore += Judgement.ToNumericResult(result.Type);
AddScoreChange(result);
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
@ -221,20 +218,6 @@ namespace osu.Game.Rulesets.Scoring
updateScore();
}
private static void applyResult(HitResult result, ref ScoringValues scoringValues)
{
if (!result.IsScorable())
return;
if (result.IsBonus())
scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
else
scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.IsBasic())
scoringValues.CountBasicHitObjects++;
}
/// <summary>
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
/// </summary>
@ -253,15 +236,16 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
// Always update the maximum scoring values.
revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
if (!result.Type.IsScorable())
return;
revertResult(result.Type, ref currentScoringValues);
currentScoringValues.MaxCombo = HighestCombo.Value;
if (result.Type.IsBasic())
CurrentBasicJudgements--;
currentMaxBasicScore -= Judgement.ToNumericResult(result.Judgement.MaxResult);
currentBasicScore -= Judgement.ToNumericResult(result.Type);
RemoveScoreChange(result);
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
@ -270,111 +254,31 @@ namespace osu.Game.Rulesets.Scoring
updateScore();
}
private static void revertResult(HitResult result, ref ScoringValues scoringValues)
protected virtual void AddScoreChange(JudgementResult result)
{
if (!result.IsScorable())
return;
if (result.IsBonus())
scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
if (result.Type.IsBonus())
BonusPortion += Judgement.ToNumericResult(result.Type);
else
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
ComboPortion += Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d);
}
if (result.IsBasic())
scoringValues.CountBasicHitObjects--;
protected virtual void RemoveScoreChange(JudgementResult result)
{
if (result.Type.IsBonus())
BonusPortion -= Judgement.ToNumericResult(result.Type);
else
ComboPortion -= Judgement.ToNumericResult(result.Type) * (1 + result.ComboAtJudgement / 10d);
}
private void updateScore()
{
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
MinimumAccuracy.Value = maximumScoringValues.BaseScore > 0 ? (double)currentScoringValues.BaseScore / maximumScoringValues.BaseScore : 0;
MaximumAccuracy.Value = maximumScoringValues.BaseScore > 0
? (double)(currentScoringValues.BaseScore + (maximumScoringValues.BaseScore - currentMaximumScoringValues.BaseScore)) / maximumScoringValues.BaseScore
: 1;
TotalScore.Value = computeScore(Mode.Value, currentScoringValues, maximumScoringValues);
Accuracy.Value = currentMaxBasicScore > 0 ? currentBasicScore / currentMaxBasicScore : 1;
// Todo: Classic/Standardised
TotalScore.Value = (long)Math.Round(ComputeTotalScore());
}
/// <summary>
/// Computes the accuracy of a given <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The score's accuracy.</returns>
[Pure]
public double ComputeAccuracy(ScoreInfo scoreInfo)
{
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
return maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
}
/// <summary>
/// Computes the total score of a given <see cref="ScoreInfo"/>.
/// </summary>
/// <remarks>
/// Does not require <see cref="JudgementProcessor.ApplyBeatmap"/> to have been called before use.
/// </remarks>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
[Pure]
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo, out var current, out var maximum);
return computeScore(mode, current, maximum);
}
/// <summary>
/// Computes the total score from scoring values.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="current">The current scoring values.</param>
/// <param name="maximum">The maximum scoring values.</param>
/// <returns>The total score computed from the given scoring values.</returns>
[Pure]
private long computeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
{
double accuracyRatio = maximum.BaseScore > 0 ? (double)current.BaseScore / maximum.BaseScore : 1;
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
}
/// <summary>
/// Computes the total score from individual scoring components.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
/// <param name="accuracyRatio">The accuracy percentage achieved by the player.</param>
/// <param name="comboRatio">The portion of the max combo achieved by the player.</param>
/// <param name="bonusScore">The total bonus score.</param>
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
/// <returns>The total score computed from the given scoring component ratios.</returns>
[Pure]
public long ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, long bonusScore, int totalBasicHitObjects)
{
double accuracyScore = accuracyPortion * accuracyRatio;
double comboScore = comboPortion * comboRatio;
double rawScore = (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
switch (mode)
{
default:
case ScoringMode.Standardised:
return (long)Math.Round(rawScore);
case ScoringMode.Classic:
// This gives a similar feeling to osu!stable scoring (ScoreV1) while keeping classic scoring as only a constant multiple of standardised scoring.
// The invariant is important to ensure that scores don't get re-ordered on leaderboards between the two scoring modes.
double scaledRawScore = rawScore / max_score;
return (long)Math.Round(Math.Pow(scaledRawScore * Math.Max(1, totalBasicHitObjects), 2) * ClassicScoreMultiplier);
}
}
protected abstract double ComputeTotalScore();
/// <summary>
/// Resets this ScoreProcessor to a default state.
@ -389,7 +293,8 @@ namespace osu.Game.Rulesets.Scoring
if (storeResults)
{
maximumScoringValues = currentScoringValues;
MaxComboPortion = ComboPortion;
MaxBasicJudgements = CurrentBasicJudgements;
maximumResultCounts.Clear();
maximumResultCounts.AddRange(scoreResultCounts);
@ -397,8 +302,11 @@ namespace osu.Game.Rulesets.Scoring
scoreResultCounts.Clear();
currentScoringValues = default;
currentMaximumScoringValues = default;
currentBasicScore = 0;
currentMaxBasicScore = 0;
CurrentBasicJudgements = 0;
ComboPortion = 0;
BonusPortion = 0;
TotalScore.Value = 0;
Accuracy.Value = 1;
@ -406,6 +314,9 @@ namespace osu.Game.Rulesets.Scoring
Rank.Disabled = false;
Rank.Value = ScoreRank.X;
HighestCombo.Value = 0;
currentBasicScore = 0;
currentMaxBasicScore = 0;
}
/// <summary>
@ -428,7 +339,7 @@ namespace osu.Game.Rulesets.Scoring
score.MaximumStatistics[result] = maximumResultCounts.GetValueOrDefault(result);
// Populate total score after everything else.
score.TotalScore = ComputeScore(ScoringMode.Standardised, score);
score.TotalScore = TotalScore.Value;
}
/// <summary>
@ -452,12 +363,6 @@ namespace osu.Game.Rulesets.Scoring
if (frame.Header == null)
return;
extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
currentScoringValues.BaseScore = current.BaseScore;
currentScoringValues.MaxCombo = frame.Header.MaxCombo;
currentMaximumScoringValues.BaseScore = maximum.BaseScore;
currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
Combo.Value = frame.Header.Combo;
HighestCombo.Value = frame.Header.MaxCombo;
@ -469,105 +374,6 @@ namespace osu.Game.Rulesets.Scoring
OnResetFromReplayFrame?.Invoke();
}
#region ScoringValue extraction
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values through external means.
/// <para>
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
/// <see cref="computeScore(osu.Game.Rulesets.Scoring.ScoringMode,ScoringValues,ScoringValues)"/>.
/// </para>
/// </remarks>
/// <param name="scoreInfo">The score to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
private void extractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
{
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
current.MaxCombo = scoreInfo.MaxCombo;
if (scoreInfo.MaximumStatistics.Count > 0)
extractScoringValues(scoreInfo.MaximumStatistics, out _, out maximum);
}
/// <summary>
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
/// </summary>
/// <remarks>
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
/// <list type="bullet">
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item>
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
/// </list>
/// Consumers are expected to more accurately fill in the above values (especially the current <see cref="ScoringValues.MaxCombo"/>) via external means (e.g. <see cref="ScoreInfo"/>).
/// </remarks>
/// <param name="statistics">The hit statistics to extract scoring values from.</param>
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
[Pure]
private void extractScoringValues(IReadOnlyDictionary<HitResult, int> statistics, out ScoringValues current, out ScoringValues maximum)
{
current = default;
maximum = default;
foreach ((HitResult result, int count) in statistics)
{
if (!result.IsScorable())
continue;
if (result.IsBonus())
current.BonusScore += count * Judgement.ToNumericResult(result);
if (result.AffectsAccuracy())
{
// The maximum result of this judgement if it wasn't a miss.
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
HitResult maxResult;
switch (result)
{
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
maxResult = HitResult.LargeTickHit;
break;
case HitResult.SmallTickHit:
case HitResult.SmallTickMiss:
maxResult = HitResult.SmallTickHit;
break;
default:
maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
break;
}
current.BaseScore += count * Judgement.ToNumericResult(result);
maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
}
if (result.AffectsCombo())
maximum.MaxCombo += count;
if (result.IsBasic())
{
current.CountBasicHitObjects += count;
maximum.CountBasicHitObjects += count;
}
}
}
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@ -629,32 +435,6 @@ namespace osu.Game.Rulesets.Scoring
}
#endregion
/// <summary>
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>
private struct ScoringValues
{
/// <summary>
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BaseScore;
/// <summary>
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
/// </summary>
public long BonusScore;
/// <summary>
/// The highest achieved combo.
/// </summary>
public int MaxCombo;
/// <summary>
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
/// </summary>
public int CountBasicHitObjects;
}
}
public enum ScoringMode

View File

@ -115,7 +115,9 @@ namespace osu.Game.Scoring
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
return scoreProcessor.ComputeScore(mode, score);
// Todo:
return 0;
// return scoreProcessor.ComputeScore(mode, score);
}
/// <summary>

View File

@ -67,7 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
// Todo:
// Score.ScoreInfo.TotalScore = ScoreProcessor.ComputeScore(ScoringMode.Standardised, Score.ScoreInfo);
}
protected override void Dispose(bool isDisposing)