Skip to content

Commit

Permalink
Merge pull request #5 from Syriiin/clean-up-api
Browse files Browse the repository at this point in the history
Clean up API
  • Loading branch information
Syriiin authored Apr 29, 2024
2 parents ef38497 + 73b10f2 commit 3723adf
Show file tree
Hide file tree
Showing 27 changed files with 476 additions and 1,556 deletions.
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

0 comments on commit 3723adf

Please sign in to comment.