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

Clean up API #5

Merged
merged 6 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 4 additions & 18 deletions Difficalcy.Catch.Tests/CatchCalculatorServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class CatchCalculatorServiceTest : CalculatorServiceTest<CatchScore, Catc
[InlineData(4.0505463516206195d, 164.5770866821372d, "diffcalc-test", 0)]
[InlineData(5.1696411260785498d, 291.43480971713944d, "diffcalc-test", 64)]
public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods)
=> base.TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new CatchScore { BeatmapId = beatmapId, Mods = mods });
=> TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new CatchScore { BeatmapId = beatmapId, Mods = mods });

[Fact]
public void TestAllParameters()
Expand All @@ -24,23 +24,9 @@ public void TestAllParameters()
Mods = 80, // HR, DT
Combo = 100,
Misses = 5,
TinyDroplets = 200,
Droplets = 3,
LargeDroplets = 18,
SmallDroplets = 200,
};
base.TestGetCalculationReturnsCorrectValues(5.739025024925009d, 241.19384779497875d, score);
}

[Fact]
public void TestAccuracyParameter()
{
var score = new CatchScore
{
BeatmapId = "diffcalc-test",
Mods = 80, // HR, DT
Accuracy = 0.9583333333333334,
Combo = 100,
Misses = 5,
};
base.TestGetCalculationReturnsCorrectValues(5.739025024925009d, 241.19384779497875d, score);
TestGetCalculationReturnsCorrectValues(5.739025024925009d, 241.19384779497875d, score);
}
}
2 changes: 2 additions & 0 deletions Difficalcy.Catch/Models/CatchCalculation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ namespace Difficalcy.Catch.Models
{
public record CatchCalculation : Calculation<CatchDifficulty, CatchPerformance>
{
public double Accuracy { get; init; }
public double Combo { get; init; }
}
}
29 changes: 24 additions & 5 deletions Difficalcy.Catch/Models/CatchScore.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Difficalcy.Models;

namespace Difficalcy.Catch.Models
{
public record CatchScore : Score
public record CatchScore : Score, IValidatableObject
{
public double? Accuracy { get; init; }
[Range(0, int.MaxValue)]
public int? Combo { get; init; }
public int? Misses { get; init; }
public int? TinyDroplets { get; init; }
public int? Droplets { get; init; }

/// <summary>
/// The number of fruit and large droplet misses.
/// </summary>
[Range(0, int.MaxValue)]
public int Misses { get; init; } = 0; // fruit + large droplet misses

[Range(0, int.MaxValue)]
public int? SmallDroplets { get; init; }

[Range(0, int.MaxValue)]
public int? LargeDroplets { get; init; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Misses > 0 && Combo is null)
{
yield return new ValidationResult("Combo must be specified if Misses are greater than 0.", [nameof(Combo)]);
}
}
}
}
116 changes: 54 additions & 62 deletions Difficalcy.Catch/Services/CatchCalculatorService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@

namespace Difficalcy.Catch.Services
{
public class CatchCalculatorService : CalculatorService<CatchScore, CatchDifficulty, CatchPerformance, CatchCalculation>
public class CatchCalculatorService(ICache cache, IBeatmapProvider beatmapProvider) : CalculatorService<CatchScore, CatchDifficulty, CatchPerformance, CatchCalculation>(cache)
{
private readonly IBeatmapProvider _beatmapProvider;
private CatchRuleset CatchRuleset { get; } = new CatchRuleset();

public override CalculatorInfo Info
Expand All @@ -39,57 +38,44 @@ public override CalculatorInfo Info
}
}

public CatchCalculatorService(ICache cache, IBeatmapProvider beatmapProvider) : base(cache)
{
_beatmapProvider = beatmapProvider;
}

protected override async Task EnsureBeatmap(string beatmapId)
{
await _beatmapProvider.EnsureBeatmap(beatmapId);
await beatmapProvider.EnsureBeatmap(beatmapId);
}

protected override (object, string) CalculateDifficultyAttributes(CatchScore score)
{
var workingBeatmap = getWorkingBeatmap(score.BeatmapId);
var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)(score.Mods ?? 0)).ToArray();
var workingBeatmap = GetWorkingBeatmap(score.BeatmapId);
var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray();

var difficultyCalculator = CatchRuleset.CreateDifficultyCalculator(workingBeatmap);
var difficultyAttributes = difficultyCalculator.Calculate(mods) as CatchDifficultyAttributes;

// Serialising anonymous object with same names because some properties can't be serialised, and the built-in JsonProperty fields aren't on all required fields
return (difficultyAttributes, JsonSerializer.Serialize(new
{
StarRating = difficultyAttributes.StarRating,
MaxCombo = difficultyAttributes.MaxCombo,
ApproachRate = difficultyAttributes.ApproachRate
difficultyAttributes.StarRating,
difficultyAttributes.MaxCombo,
difficultyAttributes.ApproachRate
}));
}

protected override CatchDifficulty GetDifficultyFromDifficultyAttributes(object difficultyAttributes)
{
var catchDifficultyAttributes = (CatchDifficultyAttributes)difficultyAttributes;
return new CatchDifficulty()
{
Total = catchDifficultyAttributes.StarRating
};
}

protected override object DeserialiseDifficultyAttributes(string difficultyAttributesJson)
{
return JsonSerializer.Deserialize<CatchDifficultyAttributes>(difficultyAttributesJson);
}

protected override CatchPerformance CalculatePerformance(CatchScore score, object difficultyAttributes)
protected override CatchCalculation CalculatePerformance(CatchScore score, object difficultyAttributes)
{
var workingBeatmap = getWorkingBeatmap(score.BeatmapId);
var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)(score.Mods ?? 0)).ToArray();
var catchDifficultyAttributes = (CatchDifficultyAttributes)difficultyAttributes;

var workingBeatmap = GetWorkingBeatmap(score.BeatmapId);
var mods = CatchRuleset.ConvertFromLegacyMods((LegacyMods)score.Mods).ToArray();
var beatmap = workingBeatmap.GetPlayableBeatmap(CatchRuleset.RulesetInfo, mods);

var hitResultCount = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet));
var combo = score.Combo ?? hitResultCount;
var statistics = determineHitResults(score.Accuracy ?? 1, hitResultCount, beatmap, score.Misses ?? 0, score.TinyDroplets, score.Droplets);
var accuracy = calculateAccuracy(statistics);
var combo = score.Combo ?? beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet));
var statistics = GetHitResults(beatmap, score.Misses, score.LargeDroplets, score.SmallDroplets);
var accuracy = CalculateAccuracy(statistics);

var scoreInfo = new ScoreInfo(beatmap.BeatmapInfo, CatchRuleset.RulesetInfo)
{
Expand All @@ -100,65 +86,71 @@ protected override CatchPerformance CalculatePerformance(CatchScore score, objec
};

var performanceCalculator = CatchRuleset.CreatePerformanceCalculator();
var performanceAttributes = performanceCalculator.Calculate(scoreInfo, (CatchDifficultyAttributes)difficultyAttributes) as CatchPerformanceAttributes;

return new CatchPerformance()
{
Total = performanceAttributes.Total
};
}
var performanceAttributes = performanceCalculator.Calculate(scoreInfo, catchDifficultyAttributes) as CatchPerformanceAttributes;

protected override CatchCalculation GetCalculation(CatchDifficulty difficulty, CatchPerformance performance)
{
return new CatchCalculation()
{
Difficulty = difficulty,
Performance = performance
Difficulty = GetDifficultyFromDifficultyAttributes(catchDifficultyAttributes),
Performance = GetPerformanceFromPerformanceAttributes(performanceAttributes),
Accuracy = accuracy,
Combo = combo
};
}

private CalculatorWorkingBeatmap getWorkingBeatmap(string beatmapId)
private CalculatorWorkingBeatmap GetWorkingBeatmap(string beatmapId)
{
using var beatmapStream = _beatmapProvider.GetBeatmapStream(beatmapId);
using var beatmapStream = beatmapProvider.GetBeatmapStream(beatmapId);
return new CalculatorWorkingBeatmap(CatchRuleset, beatmapStream, beatmapId);
}

private Dictionary<HitResult, int> determineHitResults(double targetAccuracy, int hitResultCount, IBeatmap beatmap, int countMiss, int? countTinyDroplets, int? countDroplet)
private static Dictionary<HitResult, int> GetHitResults(IBeatmap beatmap, int countMiss, int? countDroplet, int? countTinyDroplet)
{
// Adapted from https://github.com/ppy/osu-tools/blob/cf5410b04f4e2d1ed2c50c7263f98c8fc5f928ab/PerformanceCalculator/Simulate/CatchSimulateCommand.cs#L58-L86
int maxTinyDroplets = beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.NestedHitObjects.OfType<TinyDroplet>().Count());
int maxDroplets = beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.NestedHitObjects.OfType<Droplet>().Count()) - maxTinyDroplets;
int maxFruits = beatmap.HitObjects.OfType<Fruit>().Count() + 2 * beatmap.HitObjects.OfType<JuiceStream>().Count() + beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.RepeatCount);
var maxTinyDroplets = beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.NestedHitObjects.OfType<TinyDroplet>().Count());
var maxDroplets = beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.NestedHitObjects.OfType<Droplet>().Count()) - maxTinyDroplets;
var maxFruits = beatmap.HitObjects.OfType<Fruit>().Count() + 2 * beatmap.HitObjects.OfType<JuiceStream>().Count() + beatmap.HitObjects.OfType<JuiceStream>().Sum(s => s.RepeatCount);

// Either given or max value minus misses
int countDroplets = countDroplet ?? Math.Max(0, maxDroplets - countMiss);
var countDroplets = countDroplet ?? maxDroplets;
var countTinyDroplets = countTinyDroplet ?? maxTinyDroplets;

// Max value minus whatever misses are left. Negative if impossible missCount
int countFruits = maxFruits - (countMiss - (maxDroplets - countDroplets));
var countDropletMiss = maxDroplets - countDroplets;
var fruitMisses = countMiss - countDropletMiss;
var countFruit = maxFruits - fruitMisses;

// Either given or the max amount of hit objects with respect to accuracy minus the already calculated fruits and drops.
// Negative if accuracy not feasable with missCount.
int countTinyDroplet = countTinyDroplets ?? (int)Math.Round(targetAccuracy * (hitResultCount + maxTinyDroplets)) - countFruits - countDroplets;

// Whatever droplets are left
int countTinyMisses = maxTinyDroplets - countTinyDroplet;
var countTinyDropletMiss = maxTinyDroplets - countTinyDroplets;

return new Dictionary<HitResult, int>
{
{ HitResult.Great, countFruits },
{ HitResult.Great, countFruit },
{ HitResult.LargeTickHit, countDroplets },
{ HitResult.SmallTickHit, countTinyDroplet },
{ HitResult.SmallTickMiss, countTinyMisses },
{ HitResult.Miss, countMiss }
{ HitResult.SmallTickHit, countTinyDroplets },
{ HitResult.Miss, countMiss }, // fruit + large droplet misses
// { HitResult.LargeTickMiss, countDropletMiss }, // included in misses for legacy compatibility
{ HitResult.SmallTickMiss, countTinyDropletMiss },
};
}

private double calculateAccuracy(Dictionary<HitResult, int> statistics)
private static double CalculateAccuracy(Dictionary<HitResult, int> statistics)
{
double hits = statistics[HitResult.Great] + statistics[HitResult.LargeTickHit] + statistics[HitResult.SmallTickHit];
double total = hits + statistics[HitResult.Miss] + statistics[HitResult.SmallTickMiss];

return hits / total;
}

private static CatchDifficulty GetDifficultyFromDifficultyAttributes(CatchDifficultyAttributes difficultyAttributes)
{
return new CatchDifficulty()
{
Total = difficultyAttributes.StarRating
};
}

private static CatchPerformance GetPerformanceFromPerformanceAttributes(CatchPerformanceAttributes performanceAttributes)
{
return new CatchPerformance()
{
Total = performanceAttributes.Total
};
}
}
}
19 changes: 2 additions & 17 deletions Difficalcy.Mania.Tests/ManiaCalculatorServiceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class ManiaCalculatorServiceTest : CalculatorServiceTest<ManiaScore, Mani
[InlineData(2.3493769750220914d, 45.76140071089439d, "diffcalc-test", 0)]
[InlineData(2.797245912537965d, 68.79984443279172d, "diffcalc-test", 64)]
public void Test(double expectedDifficultyTotal, double expectedPerformanceTotal, string beatmapId, int mods)
=> base.TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new ManiaScore { BeatmapId = beatmapId, Mods = mods });
=> TestGetCalculationReturnsCorrectValues(expectedDifficultyTotal, expectedPerformanceTotal, new ManiaScore { BeatmapId = beatmapId, Mods = mods });

[Fact]
public void TestAllParameters()
Expand All @@ -28,21 +28,6 @@ public void TestAllParameters()
Goods = 2,
Greats = 1,
};
base.TestGetCalculationReturnsCorrectValues(2.797245912537965d, 43.17076331130473d, score);
}

[Fact]
public void TestAccuracyParameter()
{
var score = new ManiaScore
{
BeatmapId = "diffcalc-test",
Mods = 64, // DT
Accuracy = 0.9271523178807947,
Misses = 5,
};
// expected pp is slightly higher than above test because there is a different solution to
// the hitresult distribution that yields the same accuracy but gives slightly higher pp
base.TestGetCalculationReturnsCorrectValues(2.797245912537965d, 43.455530879321245d, score);
TestGetCalculationReturnsCorrectValues(2.797245912537965d, 43.17076331130473d, score);
}
}
1 change: 1 addition & 0 deletions Difficalcy.Mania/Models/ManiaCalculation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ namespace Difficalcy.Mania.Models
{
public record ManiaCalculation : Calculation<ManiaDifficulty, ManiaPerformance>
{
public double Accuracy { get; init; }
}
}
21 changes: 15 additions & 6 deletions Difficalcy.Mania/Models/ManiaScore.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
using System.ComponentModel.DataAnnotations;
using Difficalcy.Models;

namespace Difficalcy.Mania.Models
{
public record ManiaScore : Score
{
public double? Accuracy { get; init; }
public int? Misses { get; init; }
public int? Mehs { get; init; }
public int? Oks { get; init; }
public int? Goods { get; init; }
public int? Greats { get; init; }
[Range(0, int.MaxValue)]
public int Misses { get; init; } = 0;

[Range(0, int.MaxValue)]
public int Mehs { get; init; } = 0;

[Range(0, int.MaxValue)]
public int Oks { get; init; } = 0;

[Range(0, int.MaxValue)]
public int Goods { get; init; } = 0;

[Range(0, int.MaxValue)]
public int Greats { get; init; } = 0;
}
}
Loading
Loading