1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 00:02:54 +08:00

Revamp score processing to once more unify scoring methods

This commit is contained in:
smoogipooo 2017-09-12 21:05:50 +09:00
parent 0b94939474
commit abab2a4878
7 changed files with 61 additions and 300 deletions

View File

@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Catch.Scoring
protected override void OnNewJudgement(Judgement judgement)
{
base.OnNewJudgement(judgement);
}
}
}

View File

@ -15,32 +15,6 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor<ManiaHitObject>
{
/// <summary>
/// The maximum score achievable.
/// Does _not_ include bonus score - for bonus score see <see cref="bonusScore"/>.
/// </summary>
private const int max_score = 1000000;
/// <summary>
/// The amount of the score attributed to combo.
/// </summary>
private const double combo_portion_max = max_score * 0.2;
/// <summary>
/// The amount of the score attributed to accuracy.
/// </summary>
private const double accuracy_portion_max = max_score * 0.8;
/// <summary>
/// The factor used to determine relevance of combos.
/// </summary>
private const double combo_base = 4;
/// <summary>
/// The combo value at which hit objects result in the max score possible.
/// </summary>
private const int combo_relevance_cap = 400;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
@ -116,41 +90,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
/// </summary>
private double hpMultiplier = 1;
/// <summary>
/// The cumulative combo portion of the score.
/// </summary>
private double comboScore => combo_portion_max * comboPortion / maxComboPortion;
/// <summary>
/// The cumulative accuracy portion of the score.
/// </summary>
private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 4) * totalHits / maxTotalHits;
/// <summary>
/// The cumulative bonus score.
/// This is added on top of <see cref="max_score"/>, thus the total score can exceed <see cref="max_score"/>.
/// </summary>
private double bonusScore;
/// <summary>
/// The <see cref="comboPortion"/> achieved by a perfect playthrough.
/// </summary>
private double maxComboPortion;
/// <summary>
/// The portion of the score dedicated to combo.
/// </summary>
private double comboPortion;
/// <summary>
/// The <see cref="totalHits"/> achieved by a perfect playthrough.
/// </summary>
private int maxTotalHits;
/// <summary>
/// The total hits.
/// </summary>
private int totalHits;
protected override double ComboPortion => 0.2f;
protected override double AccuracyPortion => 0.8f;
public ManiaScoreProcessor()
{
@ -199,27 +140,21 @@ namespace osu.Game.Rulesets.Mania.Scoring
Reset();
}
maxTotalHits = totalHits;
maxComboPortion = comboPortion;
}
protected override void OnNewJudgement(Judgement judgement)
{
base.OnNewJudgement(judgement);
bool isTick = judgement is HoldNoteTickJudgement;
if (isTick)
{
if (judgement.IsHit)
{
Health.Value += hpMultiplier * hp_increase_tick;
bonusScore += judgement.NumericResult;
}
}
else
{
totalHits++;
switch (judgement.Result)
{
case HitResult.Miss:
@ -241,35 +176,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
Health.Value += hpMultiplier * hp_increase_perfect;
break;
}
if (judgement.IsHit)
{
// A factor that is applied to make higher combos more relevant
double comboRelevance = Math.Min(Math.Max(0.5, Math.Log(Combo.Value, combo_base)), Math.Log(combo_relevance_cap, combo_base));
comboPortion += judgement.NumericResult * comboRelevance;
}
}
int scoreForAccuracy = 0;
int maxScoreForAccuracy = 0;
foreach (var j in Judgements)
{
var maniaJudgement = (ManiaJudgement)j;
scoreForAccuracy += maniaJudgement.NumericAccuracyResult;
maxScoreForAccuracy += maniaJudgement.MaxNumericAccuracyResult;
}
Accuracy.Value = (double)scoreForAccuracy / maxScoreForAccuracy;
TotalScore.Value = comboScore + accuracyScore + bonusScore;
}
base.Reset();
bonusScore = 0;
comboPortion = 0;
totalHits = 0;
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
@ -20,6 +19,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>(ScoringMode.Exponential);
protected override double ComboPortion => 0.7;
protected override double AccuracyPortion => 0.3;
public OsuScoreProcessor()
{
}
@ -31,17 +33,12 @@ namespace osu.Game.Rulesets.Osu.Scoring
private float hpDrainRate;
private int totalAccurateJudgements;
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
private readonly Dictionary<ComboResult, int> comboResultCounts = new Dictionary<ComboResult, int>();
private double comboMaxScore;
protected override void ComputeTargets(Beatmap<OsuHitObject> beatmap)
{
hpDrainRate = beatmap.BeatmapInfo.Difficulty.DrainRate;
totalAccurateJudgements = beatmap.HitObjects.Count;
foreach (var unused in beatmap.HitObjects)
{
@ -70,6 +67,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
protected override void OnNewJudgement(Judgement judgement)
{
base.OnNewJudgement(judgement);
var osuJudgement = (OsuJudgement)judgement;
if (judgement.Result != HitResult.None)
@ -100,46 +99,6 @@ namespace osu.Game.Rulesets.Osu.Scoring
Health.Value -= hpDrainRate * 0.04;
break;
}
calculateScore();
}
private void calculateScore()
{
int baseScore = 0;
double comboScore = 0;
int baseMaxScore = 0;
foreach (var j in Judgements)
{
baseScore += j.NumericResult;
baseMaxScore += j.MaxNumericResult;
comboScore += j.NumericResult * (1 + Combo.Value / 10d);
}
Accuracy.Value = (double)baseScore / baseMaxScore;
if (comboScore > comboMaxScore)
comboMaxScore = comboScore;
if (baseScore == 0)
TotalScore.Value = 0;
else
{
// temporary to make scoring feel more like score v1 without being score v1.
float exponentialFactor = Mode.Value == ScoringMode.Exponential ? (float)Judgements.Count / 100 : 1;
TotalScore.Value =
(int)
(
exponentialFactor *
700000 * comboScore / comboMaxScore +
300000 * Math.Pow(Accuracy.Value, 10) * ((double)Judgements.Count / totalAccurateJudgements) +
0 /* bonusScore */
);
}
}
public enum ScoringMode

View File

@ -20,10 +20,5 @@ namespace osu.Game.Rulesets.Taiko.Judgements
return 200;
}
}
protected override int NumericResultForAccuracy(HitResult result)
{
return 0;
}
}
}

View File

@ -8,19 +8,8 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoJudgement : Judgement
{
/// <summary>
/// The result value for the accuracy portion of the score.
/// </summary>
public int ResultNumericForAccuracy => Result == HitResult.Miss ? 0 : NumericResultForAccuracy(Result);
/// <summary>
/// The maximum result value for the accuracy portion of the score.
/// </summary>
public int MaxResultValueForAccuracy => NumericResultForAccuracy(HitResult.Great);
/// <summary>
/// Computes the numeric result value for the combo portion of the score.
/// For the accuracy portion of the score (including accuracy percentage), see <see cref="NumericResultForAccuracy(HitResult)"/>.
/// </summary>
/// <param name="result">The result to compute the value for.</param>
/// <returns>The numeric result value.</returns>
@ -36,24 +25,5 @@ namespace osu.Game.Rulesets.Taiko.Judgements
return 300;
}
}
/// <summary>
/// Computes the numeric result value for the accuracy portion of the score.
/// For the combo portion of the score, see <see cref="NumericResultFor(HitResult)"/>.
/// </summary>
/// <param name="result">The result to compute the value for.</param>
/// <returns>The numeric result value.</returns>
protected virtual int NumericResultForAccuracy(HitResult result)
{
switch (result)
{
default:
return 0;
case HitResult.Good:
return 150;
case HitResult.Great:
return 300;
}
}
}
}

View File

@ -15,27 +15,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
internal class TaikoScoreProcessor : ScoreProcessor<TaikoHitObject>
{
/// <summary>
/// The maximum score achievable.
/// Does _not_ include bonus score - for bonus score see <see cref="bonusScore"/>.
/// </summary>
private const int max_score = 1000000;
/// <summary>
/// The amount of the score attributed to combo.
/// </summary>
private const double combo_portion_max = max_score * 0.2;
/// <summary>
/// The amount of the score attributed to accuracy.
/// </summary>
private const double accuracy_portion_max = max_score * 0.8;
/// <summary>
/// The factor used to determine relevance of combos.
/// </summary>
private const double combo_base = 4;
/// <summary>
/// The HP awarded by a <see cref="HitResult.Great"/> hit.
/// </summary>
@ -76,40 +55,16 @@ namespace osu.Game.Rulesets.Taiko.Scoring
/// <summary>
/// Taiko fails at the end of the map if the player has not half-filled their HP bar.
/// </summary>
public override bool HasFailed => totalHits == maxTotalHits && Health.Value <= 0.5;
public override bool HasFailed => Hits == MaxHits && Health.Value <= 0.5;
/// <summary>
/// The cumulative combo portion of the score.
/// </summary>
private double comboScore => combo_portion_max * comboPortion / maxComboPortion;
/// <summary>
/// The cumulative accuracy portion of the score.
/// </summary>
private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 3.6) * totalHits / maxTotalHits;
/// <summary>
/// The cumulative bonus score.
/// This is added on top of <see cref="max_score"/>, thus the total score can exceed <see cref="max_score"/>.
/// </summary>
private double bonusScore;
/// <summary>
/// The multiple of the original score added to the combo portion of the score
/// for correctly hitting a strong hit object with both keys.
/// </summary>
private double strongHitScale;
protected override double ComboPortion => 0.2;
protected override double AccuracyPortion => 0.8;
private double hpIncreaseTick;
private double hpIncreaseGreat;
private double hpIncreaseGood;
private double hpIncreaseMiss;
private double maxComboPortion;
private double comboPortion;
private int maxTotalHits;
private int totalHits;
public TaikoScoreProcessor()
{
}
@ -128,13 +83,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
hpIncreaseGood = hpMultiplierNormal * hp_hit_good;
hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max);
var strongHits = beatmap.HitObjects.FindAll(o => o is Hit && o.IsStrong);
// This is a linear function that awards:
// 10 times bonus points for hitting a strong hit object with both keys with 30 strong hit objects in the map
// 3 times bonus points for hitting a strong hit object with both keys with 120 strong hit objects in the map
strongHitScale = -7d / 90d * MathHelper.Clamp(strongHits.Count, 30, 120) + 111d / 9d;
foreach (var obj in beatmap.HitObjects)
{
if (obj is Hit)
@ -163,46 +111,14 @@ namespace osu.Game.Rulesets.Taiko.Scoring
AddJudgement(new TaikoJudgement { Result = HitResult.Great });
}
}
maxTotalHits = totalHits;
maxComboPortion = comboPortion;
}
protected override void OnNewJudgement(Judgement judgement)
{
bool isStrong = judgement is TaikoStrongHitJudgement;
base.OnNewJudgement(judgement);
bool isTick = judgement is TaikoDrumRollTickJudgement;
// Don't consider ticks and strong hits as a type of hit that counts towards map completion
if (!isTick && !isStrong)
totalHits++;
// Apply score changes
if (judgement.IsHit)
{
double baseValue = judgement.NumericResult;
if (isStrong)
{
// Add increased score for the previous judgement by hitting a strong hit object with the second key
var prevJudgement = Judgements[Judgements.Count - 1];
baseValue = prevJudgement.NumericResult * strongHitScale;
}
// Add score to portions
if (judgement is TaikoDrumRollTickJudgement)
bonusScore += baseValue;
else
{
// A relevance factor that needs to be applied to make higher combos more relevant
// Value is capped at 400 combo
double comboRelevance = Math.Min(Math.Log(400, combo_base), Math.Max(0.5, Math.Log(Combo.Value, combo_base)));
comboPortion += baseValue * comboRelevance;
}
}
// Apply HP changes
switch (judgement.Result)
{
@ -221,20 +137,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
Health.Value += hpIncreaseGreat;
break;
}
int scoreForAccuracy = 0;
int maxScoreForAccuracy = 0;
foreach (var j in Judgements)
{
var taikoJudgement = (TaikoJudgement)j;
scoreForAccuracy += taikoJudgement.ResultNumericForAccuracy;
maxScoreForAccuracy += taikoJudgement.MaxResultValueForAccuracy;
}
Accuracy.Value = (double)scoreForAccuracy / maxScoreForAccuracy;
TotalScore.Value = comboScore + accuracyScore + bonusScore;
}
protected override void Reset()
@ -242,10 +144,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
base.Reset();
Health.Value = 0;
bonusScore = 0;
comboPortion = 0;
totalHits = 0;
}
}
}

View File

@ -138,15 +138,21 @@ namespace osu.Game.Rulesets.Scoring
public abstract class ScoreProcessor<TObject> : ScoreProcessor
where TObject : HitObject
{
private const double max_score = 1000000;
/// <summary>
/// All judgements held by this ScoreProcessor.
/// </summary>
protected readonly List<Judgement> Judgements = new List<Judgement>();
private int maxHits;
private int maxCombo;
protected virtual double ComboPortion => 0.5f;
protected virtual double AccuracyPortion => 0.5f;
private int hits;
protected int MaxHits { get; private set; }
protected int Hits { get; private set; }
private double maxComboScore;
private double comboScore;
protected ScoreProcessor()
{
@ -160,8 +166,8 @@ namespace osu.Game.Rulesets.Scoring
ComputeTargets(rulesetContainer.Beatmap);
maxCombo = HighestCombo;
maxHits = hits;
maxComboScore = comboScore;
MaxHits = Hits;
Reset();
}
@ -191,28 +197,53 @@ namespace osu.Game.Rulesets.Scoring
}
}
if (judgement.AffectsCombo && judgement.IsHit)
hits++;
Judgements.Add(judgement);
OnNewJudgement(judgement);
NotifyNewJudgement(judgement);
UpdateFailed();
}
protected virtual void OnNewJudgement(Judgement judgement)
{
double bonusScore = 0;
if (judgement.AffectsCombo)
{
switch (judgement.Result)
{
case HitResult.None:
break;
case HitResult.Miss:
Combo.Value = 0;
break;
default:
Combo.Value++;
break;
}
comboScore += judgement.NumericResult;
}
else if (judgement.IsHit)
bonusScore += judgement.NumericResult;
if (judgement.AffectsAccuracy && judgement.IsHit)
Hits++;
TotalScore.Value =
max_score * (ComboPortion * comboScore / maxComboScore
+ AccuracyPortion * Hits / MaxHits)
+ bonusScore;
}
protected override void Reset()
{
base.Reset();
Judgements.Clear();
}
/// <summary>
/// Updates any values that need post-processing. Invoked when a new judgement has occurred.
/// </summary>
/// <param name="judgement">The judgement that triggered this calculation.</param>
protected abstract void OnNewJudgement(Judgement judgement);
Hits = 0;
comboScore = 0;
}
}
}