Skip to content

Refactor of osu!taiko difficulty calculation code #31636

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

Merged
merged 23 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fa20bc6
Remove `EffectiveBPMPreprocessor`
tsunyoku Jan 21, 2025
dbe3688
Refactor `ColourEvaluator`
tsunyoku Jan 21, 2025
9919179
Format `ReadingEvaluator`
tsunyoku Jan 21, 2025
b8c79d5
Refactor `StaminaEvaluator`
tsunyoku Jan 21, 2025
ef88677
Add xmldoc to explain `IHasInterval.Interval`
tsunyoku Jan 21, 2025
20a76d8
Rename rhythm preprocessing objects to be clearer with intent
tsunyoku Jan 21, 2025
e0882d2
Make `rescale` a static method
tsunyoku Jan 21, 2025
764b000
Fix typo in `ColourEvaluator`
tsunyoku Jan 21, 2025
1c4bc6d
Revert `Precision.DefinitelyBigger` usage
tsunyoku Jan 21, 2025
14c68bc
Replace weird `IntervalGroupedHitObjects` inheritance layer
tsunyoku Jan 21, 2025
2c0d6b1
Fix incorrect namespace
tsunyoku Jan 22, 2025
753e9ef
Keep old behaviour of `double.PositiveInfinity` being the default for…
tsunyoku Jan 22, 2025
8f17a44
Remove unused default value
tsunyoku Jan 23, 2025
a7aa553
Fix incorrect `startTime` calculation
tsunyoku Jan 26, 2025
13c956c
Account for floating point errors
tsunyoku Jan 26, 2025
71b89c3
Rename class, rename children to hit objects and groups, make fields …
tsunyoku Jan 27, 2025
f3c17f1
Use correct English
tsunyoku Jan 27, 2025
fa844b0
Rename `Colour` / `Rhythm` related fields and classes
peppy Feb 5, 2025
709ad02
Simplify `TaikoRhythmData`'s ratio computation
peppy Feb 5, 2025
fc93390
Remove unused `HitObjectInterval`
peppy Feb 5, 2025
3254831
Tidy up xmldoc and remove another unused field
peppy Feb 5, 2025
8447679
Initial tidy-up pass on `IntervalGroupingUtils`
peppy Feb 5, 2025
40ea7ff
Add better documentation for interval change code
peppy Feb 5, 2025
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
41 changes: 13 additions & 28 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,8 @@

namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class ColourEvaluator
public static class ColourEvaluator
{
/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
/// </summary>
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
{
return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
}

/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="AlternatingMonoPattern"/>.
/// </summary>
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
{
return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
}

/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="RepeatingHitPatterns"/>.
/// </summary>
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
{
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}

/// <summary>
/// Calculates a consistency penalty based on the number of consecutive consistent intervals,
/// considering the delta time between each colour sequence.
Expand Down Expand Up @@ -89,18 +65,27 @@ public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
double difficulty = 0.0d;

if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
difficulty += evaluateMonoStreakDifficulty(colour.MonoStreak);

if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
difficulty += evaluateAlternatingMonoPatternDifficulty(colour.AlternatingMonoPattern);

if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
difficulty += evaluateRepeatingHitPatternsDifficulty(colour.RepeatingHitPattern);

double consistencyPenalty = consistentRatioPenalty(taikoObject);
difficulty *= consistencyPenalty;

return difficulty;
}

private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) =>
DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5;

private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) =>
DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent);

private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
// High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);

double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty) * DifficultyCalculationUtils.Logistic
(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
* DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));

return midVelocityDifficulty + highVelocityDifficulty;
}
Expand Down
38 changes: 19 additions & 19 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/RhythmEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,32 @@ public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double
double samePattern = 0;
double intervalPenalty = 0;

if (rhythm.SameRhythmHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmHitObjects
if (rhythm.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
{
sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmHitObjects, hitWindow);
intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmHitObjects, hitWindow);
sameRhythm += 10.0 * evaluateDifficultyOf(rhythm.SameRhythmGroupedHitObjects, hitWindow);
intervalPenalty = repeatedIntervalPenalty(rhythm.SameRhythmGroupedHitObjects, hitWindow);
}

if (rhythm.SamePatterns?.FirstHitObject == hitObject) // Difficulty for SamePatterns
samePattern += 1.15 * ratioDifficulty(rhythm.SamePatterns.IntervalRatio);
if (rhythm.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
samePattern += 1.15 * ratioDifficulty(rhythm.SamePatternsGroupedHitObjects.IntervalRatio);

difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;

return difficulty;
}

private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow)
private static double evaluateDifficultyOf(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow)
{
double intervalDifficulty = ratioDifficulty(sameRhythmHitObjects.HitObjectIntervalRatio);
double? previousInterval = sameRhythmHitObjects.Previous?.HitObjectInterval;
double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio);
double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval;

intervalDifficulty *= repeatedIntervalPenalty(sameRhythmHitObjects, hitWindow);
intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow);

// If a previous interval exists and there are multiple hit objects in the sequence:
if (previousInterval != null && sameRhythmHitObjects.Children.Count > 1)
if (previousInterval != null && sameRhythmGroupedHitObjects.Children.Count > 1)
{
double expectedDurationFromPrevious = (double)previousInterval * sameRhythmHitObjects.Children.Count;
double durationDifference = sameRhythmHitObjects.Duration - expectedDurationFromPrevious;
double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.Children.Count;
double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious;

if (durationDifference > 0)
{
Expand All @@ -64,7 +64,7 @@ private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObj

// Penalise patterns that can be hit within a single hit window.
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
sameRhythmHitObjects.Duration / hitWindow,
sameRhythmGroupedHitObjects.Duration / hitWindow,
midpointOffset: 0.6,
multiplier: 1,
maxValue: 1);
Expand All @@ -75,20 +75,20 @@ private static double evaluateDifficultyOf(SameRhythmHitObjects sameRhythmHitObj
/// <summary>
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
/// </summary>
private static double repeatedIntervalPenalty(SameRhythmHitObjects sameRhythmHitObjects, double hitWindow, double threshold = 0.1)
private static double repeatedIntervalPenalty(SameRhythmGroupedHitObjects sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
{
double longIntervalPenalty = sameInterval(sameRhythmHitObjects, 3);
double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3);

double shortIntervalPenalty = sameRhythmHitObjects.Children.Count < 6
? sameInterval(sameRhythmHitObjects, 4)
double shortIntervalPenalty = sameRhythmGroupedHitObjects.Children.Count < 6
? sameInterval(sameRhythmGroupedHitObjects, 4)
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.

// The duration penalty is based on hit object duration relative to hitWindow.
double durationPenalty = Math.Max(1 - sameRhythmHitObjects.Duration * 2 / hitWindow, 0.5);
double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5);

return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;

double sameInterval(SameRhythmHitObjects startObject, int intervalCount)
double sameInterval(SameRhythmGroupedHitObjects startObject, int intervalCount)
{
List<double?> intervals = new List<double?>();
var currentObject = startObject;
Expand Down
54 changes: 27 additions & 27 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,34 @@

namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class StaminaEvaluator
public static class StaminaEvaluator
{
/// <summary>
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
/// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
if (current.BaseObject is not Hit)
{
return 0.0;
}

// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
// available fingers.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);

double objectStrain = 0.5; // Add a base strain to all objects
if (taikoPrevious == null) return objectStrain;

if (previousMono != null)
objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);

return objectStrain;
}

/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
/// </summary>
Expand Down Expand Up @@ -44,31 +70,5 @@ private static int availableFingersFor(TaikoDifficultyHitObject hitObject)

return 8;
}

/// <summary>
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
/// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
/// </summary>
public static double EvaluateDifficultyOf(DifficultyHitObject current)
{
if (current.BaseObject is not Hit)
{
return 0.0;
}

// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
// available fingers.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);

double objectStrain = 0.5; // Add a base strain to all objects
if (taikoPrevious == null) return objectStrain;

if (previousMono != null)
objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);

return objectStrain;
}
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using System.Linq;

namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
{
/// <summary>
/// Represents <see cref="SameRhythmGroupedHitObjects"/> grouped by their <see cref="SameRhythmGroupedHitObjects.StartTime"/>'s interval.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This xmldoc doesn't do much for me without reading into how it works. Can it be explained in less code-y terms?

For example, does every grouping in this class have the same rhythm?

Copy link
Member

@Lawtrohux Lawtrohux Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It represents a group of hit objects grouped by the same spacing between their start times (interval). This then is calculated as a singular rhythmic group or segment. Every grouping in this class will be grouped by having the same interval, with the changes garnering the difficulty between them.

/// </summary>
public class SamePatternsGroupedHitObjects
{
public IReadOnlyList<SameRhythmGroupedHitObjects> Children { get; }

public SamePatternsGroupedHitObjects? Previous { get; }

/// <summary>
/// The <see cref="SameRhythmGroupedHitObjects.Interval"/> between children <see cref="SameRhythmGroupedHitObjects"/> within this group.
/// If there is only one child, this will have the value of the first child's <see cref="SameRhythmGroupedHitObjects.Interval"/>.
/// </summary>
public double ChildrenInterval => Children.Count > 1 ? Children[1].Interval : Children[0].Interval;

/// <summary>
/// The ratio of <see cref="ChildrenInterval"/> between this and the previous <see cref="SamePatternsGroupedHitObjects"/>. In the
/// case where there is no previous <see cref="SamePatternsGroupedHitObjects"/>, this will have a value of 1.
/// </summary>
public double IntervalRatio => ChildrenInterval / Previous?.ChildrenInterval ?? 1.0d;

public TaikoDifficultyHitObject FirstHitObject => Children[0].FirstHitObject;

public IEnumerable<TaikoDifficultyHitObject> AllHitObjects => Children.SelectMany(child => child.Children);

public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List<SameRhythmGroupedHitObjects> children)
{
Previous = previous;
Children = children;
}
}
}
Loading
Loading