1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 06:17:23 +08:00

Implement PerformanceCalculator testcase

This commit is contained in:
smoogipoo 2017-11-17 12:35:23 +09:00
parent 38fe95d94a
commit 1e023f0419
7 changed files with 336 additions and 67 deletions

View File

@ -14,6 +14,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu
{
@ -114,6 +115,8 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
public override string Description => "osu!";
public override SettingsSubsection CreateSettings() => new OsuSettings();

View File

@ -0,0 +1,189 @@
// 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 System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuPerformanceCalculator : PerformanceCalculator<OsuHitObject>
{
private readonly int countHitCircles;
private readonly int beatmapMaxCombo;
private Mod[] mods;
private double accuracy;
private int scoreMaxCombo;
private int count300;
private int count100;
private int count50;
private int countMiss;
public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
beatmapMaxCombo = Beatmap.HitObjects.Count();
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.RepeatCount + s.Ticks.Count());
}
public override double Calculate(Dictionary<string, string> categoryRatings = null)
{
mods = Score.Mods;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
count300 = Convert.ToInt32(Score.Statistics["300"]);
count100 = Convert.ToInt32(Score.Statistics["100"]);
count50 = Convert.ToInt32(Score.Statistics["50"]);
countMiss = Convert.ToInt32(Score.Statistics["x"]);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => m is OsuModRelax || m is OsuModAutopilot || m is OsuModAutoplay))
return 0;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is OsuModNoFail))
multiplier *= 0.90f;
if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 0.95f;
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1f) +
Math.Pow(speedValue, 1.1f) +
Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f
) * multiplier;
if (categoryRatings != null)
{
categoryRatings.Add("Aim", aimValue.ToString("0.00"));
categoryRatings.Add("Speed", speedValue.ToString("0.00"));
categoryRatings.Add("Accuracy", accuracyValue.ToString("0.00"));
}
return totalValue;
}
private double computeAimValue()
{
double aimValue = Math.Pow(5.0f * Math.Max(1.0f, double.Parse(Attributes["Aim"]) / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
// Longer maps are worth more
double LengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
aimValue *= LengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
aimValue *= Math.Pow(0.97f, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
double approachRate = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate;
double approachRateFactor = 1.0f;
if (approachRate > 10.33f)
approachRateFactor += 0.45f * (approachRate - 10.33f);
else if (approachRate < 8.0f)
{
// HD is worth more with lower ar!
if (mods.Any(h => h is OsuModHidden))
approachRateFactor += 0.02f * (8.0f - approachRate);
else
approachRateFactor += 0.01f * (8.0f - approachRate);
}
aimValue *= approachRateFactor;
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.18f;
if (mods.Any(h => h is OsuModFlashlight))
{
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
aimValue *= 1.45f * LengthBonus;
}
// Scale the aim value with accuracy _slightly_
aimValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that
aimValue *= 0.98f + (Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500);
return aimValue;
}
private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0f * Math.Max(1.0f, double.Parse(Attributes["Speed"]) / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
// Longer maps are worth more
speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
speedValue *= Math.Pow(0.97f, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
// Scale the speed value with accuracy _slightly_
speedValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that
speedValue *= 0.98f + (Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500);
return speedValue;
}
private double computeAccuracyValue()
{
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
double betterAccuracyPercentage = 0;
int amountHitObjectsWithAccuracy = countHitCircles;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6);
else
betterAccuracyPercentage = 0;
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points
if (betterAccuracyPercentage < 0)
betterAccuracyPercentage = 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));
if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.02f;
if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02f;
return accuracyValue;
}
private double totalHits => count300 + count100 + count50 + countMiss;
private double totalSuccessfulHits => count300 + count100 + count50;
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
}
}

View File

@ -89,6 +89,7 @@
<Compile Include="UI\Cursor\CursorTrail.cs" />
<Compile Include="UI\Cursor\GameplayCursor.cs" />
<Compile Include="UI\OsuSettings.cs" />
<Compile Include="Scoring\OsuPerformanceCalculator.cs" />
<Compile Include="Scoring\OsuScoreProcessor.cs" />
<Compile Include="UI\OsuRulesetContainer.cs" />
<Compile Include="UI\OsuPlayfield.cs" />

View File

@ -49,6 +49,8 @@ namespace osu.Game.Rulesets
public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null);
public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null;
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_question_circle };
public abstract string Description { get; }

View File

@ -0,0 +1,35 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Scoring
{
public abstract class PerformanceCalculator
{
public abstract double Calculate(Dictionary<string, string> categoryDifficulty = null);
}
public abstract class PerformanceCalculator<TObject> : PerformanceCalculator
where TObject : HitObject
{
private readonly Dictionary<string, string> attributes = new Dictionary<string, string>();
protected IDictionary<string, string> Attributes => attributes;
protected readonly Beatmap<TObject> Beatmap;
protected readonly Score Score;
public PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
{
Beatmap = CreateBeatmapConverter().Convert(beatmap);
Score = score;
var diffCalc = ruleset.CreateDifficultyCalculator(beatmap);
diffCalc.Calculate(attributes);
}
protected abstract BeatmapConverter<TObject> CreateBeatmapConverter();
}
}

View File

@ -14,9 +14,12 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.Music;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Tests.Visual
{
@ -24,30 +27,51 @@ namespace osu.Game.Tests.Visual
{
public TestCasePerformancePoints(Ruleset ruleset)
{
Children = new Drawable[]
Child = new FillFlowContainer
{
new Container
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 300,
Children = new Drawable[]
new Container
{
new Box
RelativeSizeAxes = Axes.Both,
Width = 0.25f,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new ScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapList(ruleset)
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new ScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapList(ruleset)
}
}
}
},
new PpDisplay(ruleset)
},
new Container
{
RelativeSizeAxes = Axes.Both,
Width = 0.75f,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new ScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
Child = new ScoreList { RelativeSizeAxes = Axes.Both }
}
}
},
}
};
}
@ -144,70 +168,84 @@ namespace osu.Game.Tests.Visual
}
}
private class PpDisplay : CompositeDrawable
private class ScoreList : CompositeDrawable
{
private readonly Container<OsuSpriteText> strainsContainer;
private readonly OsuSpriteText totalPp;
private readonly FillFlowContainer<ScoreDisplay> scores;
private APIAccess api;
private readonly Ruleset ruleset;
public PpDisplay(Ruleset ruleset)
public ScoreList()
{
this.ruleset = ruleset;
RelativeSizeAxes = Axes.Y;
Width = 400;
InternalChildren = new Drawable[]
InternalChild = scores = new FillFlowContainer<ScoreDisplay>
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.2f
},
totalPp = new OsuSpriteText { TextSize = 18 },
new Container
{
AutoSizeAxes = Axes.Both,
Y = 26,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.2f,
},
strainsContainer = new FillFlowContainer<OsuSpriteText>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5)
}
}
}
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4)
};
}
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame)
private void load(OsuGameBase osuGame, APIAccess api)
{
this.api = api;
osuGame.Beatmap.ValueChanged += beatmapChanged;
}
private void beatmapChanged(WorkingBeatmap beatmap)
private GetScoresRequest lastRequest;
private void beatmapChanged(WorkingBeatmap newBeatmap)
{
var diffCalculator = ruleset.CreateDifficultyCalculator(beatmap.Beatmap);
lastRequest?.Cancel();
scores.Clear();
var strains = new Dictionary<string, string>();
double pp = diffCalculator.Calculate(strains);
lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo);
lastRequest.Success += res => res.Scores.ForEach(s => scores.Add(new ScoreDisplay(s, newBeatmap.Beatmap)));
api.Queue(lastRequest);
}
totalPp.Text = $"Total PP: {pp.ToString("n2")}";
private class ScoreDisplay : CompositeDrawable
{
private readonly OsuSpriteText playerName;
private readonly GridContainer attributeGrid;
strainsContainer.Clear();
foreach (var kvp in strains)
strainsContainer.Add(new OsuSpriteText { Text = $"{kvp.Key} : {kvp.Value}" });
private readonly Score score;
private readonly Beatmap beatmap;
public ScoreDisplay(Score score, Beatmap beatmap)
{
this.score = score;
this.beatmap = beatmap;
RelativeSizeAxes = Axes.X;
Height = 16;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[] { playerName = new OsuSpriteText() },
new Drawable[] { attributeGrid = new GridContainer { RelativeSizeAxes = Axes.Both } }
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 0.75f),
new Dimension(GridSizeMode.Relative, 0.25f)
}
};
}
[BackgroundDependencyLoader]
private void load()
{
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
var calculator = ruleset.CreatePerformanceCalculator(beatmap, score);
if (calculator == null)
return;
var attributes = new Dictionary<string, string>();
double performance = calculator.Calculate(attributes);
playerName.Text = $"{score.PP} | {performance.ToString("0.00")} | {score.PP / performance}";
// var attributeRow =
}
}
}
}

View File

@ -620,6 +620,7 @@
<Compile Include="Rulesets\Ruleset.cs" />
<Compile Include="Rulesets\RulesetInfo.cs" />
<Compile Include="Rulesets\RulesetStore.cs" />
<Compile Include="Rulesets\Scoring\PerformanceCalculator.cs" />
<Compile Include="Rulesets\Scoring\Score.cs" />
<Compile Include="Rulesets\Scoring\ScoreProcessor.cs" />
<Compile Include="Rulesets\Scoring\ScoreRank.cs" />