Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simulate osu!mania scores by accuracy and counts #166

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion PerformanceCalculator/Simulate/CatchSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class CatchSimulateCommand : SimulateCommand
protected override int GetMaxCombo(IBeatmap beatmap) => beatmap.HitObjects.Count(h => h is Fruit)
+ beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet));

protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat)
{
var maxCombo = GetMaxCombo(beatmap);
int maxTinyDroplets = beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.NestedHitObjects.OfType<TinyDroplet>().Count());
Expand Down
105 changes: 68 additions & 37 deletions PerformanceCalculator/Simulate/ManiaSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,106 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;

namespace PerformanceCalculator.Simulate
{
[Command(Name = "mania", Description = "Computes the performance (pp) of a simulated osu!mania play.")]
public class ManiaSimulateCommand : SimulateCommand
{
public override int Score
{
get
{
Debug.Assert(score != null);
return score.Value;
}
}

[UsedImplicitly]
[Option(Template = "-s|--score <score>", Description = "Score. An integer 0-1000000.")]
private int? score { get; set; }
[Option(Template = "-a|--accuracy <accuracy>", Description = "Accuracy. Enter as decimal 0-100. Defaults to 100."
+ " Scales hit results as well and is rounded to the nearest possible value for the beatmap.")]
public override double Accuracy { get; } = 100;

[UsedImplicitly]
[Option(CommandOptionType.MultipleValue, Template = "-m|--mod <mod>", Description = "One for each mod. The mods to compute the performance with."
+ " Values: hr, dt, fl, 4k, 5k, etc...")]
public override string[] Mods { get; }

public override Ruleset Ruleset => new ManiaRuleset();
[UsedImplicitly]
[Option(Template = "-X|--misses <misses>", Description = "Number of misses. Defaults to 0.")]
public override int Misses { get; }

public override void Execute()
{
if (score == null)
{
double scoreMultiplier = 1;
[UsedImplicitly]
[Option(Template = "-M|--mehs <mehs>", Description = "Number of mehs (50). Will override accuracy if used. Otherwise is automatically calculated.")]
public override int? Mehs { get; }

// Cap score depending on difficulty adjustment mods (matters for mania).
foreach (var mod in GetMods(Ruleset))
{
if (mod.Type == ModType.DifficultyReduction)
scoreMultiplier *= mod.ScoreMultiplier;
}
[UsedImplicitly]
[Option(Template = "-O|--oks <oks>", Description = "Number of oks (100).")]
public override int? Oks { get; }

score = (int)Math.Round(1000000 * scoreMultiplier);
}
[UsedImplicitly]
[Option(Template = "-G|--goods <goods>", Description = "Number of goods (200).")]
public override int? Goods { get; }

base.Execute();
}
[UsedImplicitly]
[Option(Template = "-GR|--greats <greats>", Description = "Number of greats (300).")]
public override int? Greats { get; }

public override Ruleset Ruleset => new ManiaRuleset();

protected override int GetMaxCombo(IBeatmap beatmap) => 0;

protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat)
{
var totalHits = beatmap.HitObjects.Count;

// Only total number of hits is considered currently, so specifics don't matter
countMiss = Math.Clamp(countMiss, 0, totalHits);

// Populate score with mehs to make this approximation more precise.
// This value can be negative on impossible misscount.
//
// total = ((1/6) * meh + (1/3) * ok + (2/3) * good + great + perfect) / acc
// total = miss + meh + ok + good + great + perfect
//
// miss + (5/6) * meh + (2/3) * ok + (1/3) * good = total - acc * total
// meh = 1.2 * (total - acc * total) - 1.2 * miss - 0.8 * ok - 0.4 * good
countMeh ??= (int)Math.Round((1.2 * (totalHits - totalHits * accuracy)) - (1.2 * countMiss) - (0.8 * (countOk ?? 0)) - (0.4 * (countGood ?? 0)));

// We need to clamp for all values because performance calculator's custom accuracy formula is not invariant to negative counts.
int currentCounts = countMiss;

countMeh = Math.Clamp((int)countMeh, 0, totalHits - currentCounts);
currentCounts += (int)countMeh;

countOk = Math.Clamp(countOk ?? 0, 0, totalHits - currentCounts);
currentCounts += (int)countOk;

countGood = Math.Clamp(countGood ?? 0, 0, totalHits - currentCounts);
currentCounts += (int)countGood;

countGreat = Math.Clamp(countGreat ?? 0, 0, totalHits - currentCounts);

int countPerfect = totalHits - (int)countGreat - (int)countGood - (int)countOk - (int)countMeh - countMiss;

return new Dictionary<HitResult, int>
{
{ HitResult.Perfect, totalHits },
{ HitResult.Great, 0 },
{ HitResult.Ok, 0 },
{ HitResult.Good, 0 },
{ HitResult.Meh, 0 },
{ HitResult.Miss, 0 }
{ HitResult.Perfect, countPerfect },
{ HitResult.Great, (int)countGreat },
{ HitResult.Ok, (int)countOk },
{ HitResult.Good, (int)countGood },
{ HitResult.Meh, (int)countMeh },
{ HitResult.Miss, countMiss }
};
}

protected override double GetAccuracy(Dictionary<HitResult, int> statistics)
{
var countPerfect = statistics[HitResult.Perfect];
var countGreat = statistics[HitResult.Great];
var countGood = statistics[HitResult.Good];
var countOk = statistics[HitResult.Ok];
var countMeh = statistics[HitResult.Meh];
var countMiss = statistics[HitResult.Miss];
var total = countPerfect + countGreat + countGood + countOk + countMeh + countMiss;

return ((countMeh / 6.0) + (countOk / 3.0) + (countGood / 1.5) + countGreat + countPerfect) / total;
}
}
}
6 changes: 3 additions & 3 deletions PerformanceCalculator/Simulate/OsuSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ public class OsuSimulateCommand : SimulateCommand

protected override int GetMaxCombo(IBeatmap beatmap) => beatmap.GetMaxCombo();

protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? _)
{
int countGreat;

var totalResultCount = beatmap.HitObjects.Count;

int countGreat;

if (countMeh != null || countGood != null)
{
countGreat = totalResultCount - (countGood ?? 0) - (countMeh ?? 0) - countMiss;
Expand Down
10 changes: 8 additions & 2 deletions PerformanceCalculator/Simulate/SimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ public abstract class SimulateCommand : ProcessorCommand
[UsedImplicitly]
public virtual int? Mehs { get; }

[UsedImplicitly]
public virtual int? Oks { get; }

[UsedImplicitly]
public virtual int? Goods { get; }

[UsedImplicitly]
public virtual int? Greats { get; }

[UsedImplicitly]
[Option(Template = "-j|--json", Description = "Output results as JSON.")]
public bool OutputJson { get; }
Expand All @@ -73,7 +79,7 @@ public override void Execute()

var beatmapMaxCombo = GetMaxCombo(beatmap);
var maxCombo = Combo ?? (int)Math.Round(PercentCombo / 100 * beatmapMaxCombo);
var statistics = GenerateHitResults(Accuracy / 100, beatmap, Misses, Mehs, Goods);
var statistics = GenerateHitResults(Accuracy / 100, beatmap, Misses, Mehs, Oks, Goods, Greats);
var score = Score;
var accuracy = GetAccuracy(statistics);

Expand Down Expand Up @@ -182,7 +188,7 @@ protected Mod[] GetMods(Ruleset ruleset)

protected abstract int GetMaxCombo(IBeatmap beatmap);

protected abstract Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood);
protected abstract Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat);

protected virtual double GetAccuracy(Dictionary<HitResult, int> statistics) => 0;

Expand Down
2 changes: 1 addition & 1 deletion PerformanceCalculator/Simulate/TaikoSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class TaikoSimulateCommand : SimulateCommand

protected override int GetMaxCombo(IBeatmap beatmap) => beatmap.HitObjects.OfType<Hit>().Count();

protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? _)
{
var totalResultCount = GetMaxCombo(beatmap);

Expand Down
54 changes: 34 additions & 20 deletions PerformanceCalculatorGUI/RulesetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ public static int AdjustManiaScore(int score, IReadOnlyList<Mod> mods)
return (int)Math.Round(1000000 * scoreMultiplier);
}

public static Dictionary<HitResult, int> GenerateHitResultsForRuleset(RulesetInfo ruleset, double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
public static Dictionary<HitResult, int> GenerateHitResultsForRuleset(RulesetInfo ruleset, double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat)
{
return ruleset.OnlineID switch
{
0 => generateOsuHitResults(accuracy, beatmap, countMiss, countMeh, countGood),
1 => generateTaikoHitResults(accuracy, beatmap, countMiss, countGood),
2 => generateCatchHitResults(accuracy, beatmap, countMiss, countMeh, countGood),
3 => generateManiaHitResults(accuracy, beatmap, countMiss),
3 => generateManiaHitResults(accuracy, beatmap, countMiss, countMeh, countOk, countGood, countGreat),
_ => throw new ArgumentException("Invalid ruleset ID provided.")
};
}
Expand Down Expand Up @@ -210,31 +210,45 @@ private static Dictionary<HitResult, int> generateCatchHitResults(double accurac
};
}

private static Dictionary<HitResult, int> generateManiaHitResults(double accuracy, IBeatmap beatmap, int countMiss)
private static Dictionary<HitResult, int> generateManiaHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countOk, int? countGood, int? countGreat)
{
var totalResultCount = beatmap.HitObjects.Count;
var totalHits = beatmap.HitObjects.Count;

countMiss = Math.Clamp(countMiss, 0, totalHits);

// Populate score with mehs to make this approximation more precise.
// This value can be negative on impossible misscount.
//
// total = ((1/6) * meh + (1/3) * ok + (2/3) * good + great + perfect) / acc
// total = miss + meh + ok + good + great + perfect
//
// miss + (5/6) * meh + (2/3) * ok + (1/3) * good = total - acc * total
// meh = 1.2 * (total - acc * total) - 1.2 * miss - 0.8 * ok - 0.4 * good
countMeh ??= (int)Math.Round((1.2 * (totalHits - totalHits * accuracy)) - (1.2 * countMiss) - (0.8 * (countOk ?? 0)) - (0.4 * (countGood ?? 0)));

// We need to clamp for all values because performance calculator's custom accuracy formula is not invariant to negative counts.
int currentCounts = countMiss;

countMeh = Math.Clamp((int)countMeh, 0, totalHits - currentCounts);
currentCounts += (int)countMeh;

countOk = Math.Clamp(countOk ?? 0, 0, totalHits - currentCounts);
currentCounts += (int)countOk;

// Let Great=6, Good=2, Meh=1, Miss=0. The total should be this.
var targetTotal = (int)Math.Round(accuracy * totalResultCount * 6);
countGood = Math.Clamp(countGood ?? 0, 0, totalHits - currentCounts);
currentCounts += (int)countGood;

// Start by assuming every non miss is a meh
// This is how much increase is needed by greats and goods
var delta = targetTotal - (totalResultCount - countMiss);
countGreat = Math.Clamp(countGreat ?? 0, 0, totalHits - currentCounts);

// Each great increases total by 5 (great-meh=5)
int countGreat = delta / 5;
// Each good increases total by 1 (good-meh=1). Covers remaining difference.
int countGood = delta % 5;
// Mehs are left over. Could be negative if impossible value of amountMiss chosen
int countMeh = totalResultCount - countGreat - countGood - countMiss;
int countPerfect = totalHits - (int)countGreat - (int)countGood - (int)countOk - (int)countMeh - countMiss;

return new Dictionary<HitResult, int>
{
{ HitResult.Perfect, countGreat },
{ HitResult.Great, 0 },
{ HitResult.Good, countGood },
{ HitResult.Ok, 0 },
{ HitResult.Meh, countMeh },
{ HitResult.Perfect, countPerfect },
{ HitResult.Great, (int)countGreat },
{ HitResult.Ok, (int)countOk },
{ HitResult.Good, (int)countGood },
{ HitResult.Meh, (int)countMeh },
{ HitResult.Miss, countMiss }
};
}
Expand Down
Loading