1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 21:27:24 +08:00

Add xmldoc to taiko difficulty calculation code

This commit is contained in:
Bartłomiej Dach 2020-08-22 19:34:16 +02:00
parent 8ace7df0fd
commit a080774799
7 changed files with 281 additions and 51 deletions

View File

@ -8,11 +8,32 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
/// <summary>
/// Detects special hit object patterns which are easier to hit using special techniques
/// than normally assumed in the fully-alternating play style.
/// </summary>
/// <remarks>
/// This component detects two basic types of patterns, leveraged by the following techniques:
/// <list>
/// <item>Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.</item>
/// <item>TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.</item>
/// </list>
/// </remarks>
public class StaminaCheeseDetector
{
/// <summary>
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
/// </summary>
private const int roll_min_repetitions = 12;
/// <summary>
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
/// </summary>
private const int tl_min_repetitions = 16;
/// <summary>
/// The list of all <see cref="TaikoDifficultyHitObject"/>s in the map.
/// </summary>
private readonly List<TaikoDifficultyHitObject> hitObjects;
public StaminaCheeseDetector(List<TaikoDifficultyHitObject> hitObjects)
@ -20,16 +41,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
this.hitObjects = hitObjects;
}
/// <summary>
/// Finds and marks all objects in <see cref="hitObjects"/> that special difficulty-reducing techiques apply to
/// with the <see cref="TaikoDifficultyHitObject.StaminaCheese"/> flag.
/// </summary>
public void FindCheese()
{
findRolls(3);
findRolls(4);
findTlTap(0, HitType.Rim);
findTlTap(1, HitType.Rim);
findTlTap(0, HitType.Centre);
findTlTap(1, HitType.Centre);
}
/// <summary>
/// Finds and marks all sequences hittable using a roll.
/// </summary>
/// <param name="patternLength">The length of a single repeating pattern to consider (triplets/quadruplets).</param>
private void findRolls(int patternLength)
{
var history = new LimitedCapacityQueue<TaikoDifficultyHitObject>(2 * patternLength);
@ -56,6 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
}
}
/// <summary>
/// Determines whether the objects stored in <paramref name="history"/> contain a repetition of a pattern of length <paramref name="patternLength"/>.
/// </summary>
private static bool containsPatternRepeat(LimitedCapacityQueue<TaikoDifficultyHitObject> history, int patternLength)
{
for (int j = 0; j < patternLength; j++)
@ -67,6 +100,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
return true;
}
/// <summary>
/// Finds and marks all sequences hittable using a TL tap.
/// </summary>
/// <param name="parity">Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.</param>
/// <param name="type">The type of hit to check for TL taps.</param>
private void findTlTap(int parity, HitType type)
{
int tlLength = -2;
@ -85,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
}
}
/// <summary>
/// Marks all objects from index <paramref name="start"/> up until <paramref name="end"/> (exclusive) as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
/// </summary>
private void markObjectsAsCheese(int start, int end)
{
for (int i = start; i < end; ++i)

View File

@ -9,27 +9,61 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
/// <summary>
/// Represents a single hit object in taiko difficulty calculation.
/// </summary>
public class TaikoDifficultyHitObject : DifficultyHitObject
{
/// <summary>
/// The rhythm required to hit this hit object.
/// </summary>
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
/// <summary>
/// The hit type of this hit object.
/// </summary>
public readonly HitType? HitType;
/// <summary>
/// The index of the object in the beatmap.
/// </summary>
public readonly int ObjectIndex;
/// <summary>
/// Whether the object should carry a penalty due to being hittable using special techniques
/// making it easier to do so.
/// </summary>
public bool StaminaCheese;
/// <summary>
/// Creates a new difficulty hit object.
/// </summary>
/// <param name="hitObject">The gameplay <see cref="HitObject"/> associated with this difficulty object.</param>
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
/// <param name="objectIndex">The index of the object in the beatmap.</param>
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex)
: base(hitObject, lastObject, clockRate)
{
var currentHit = hitObject as Hit;
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
Rhythm = getClosestRhythm(DeltaTime / prevLength);
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
HitType = currentHit?.Type;
ObjectIndex = objectIndex;
}
/// <summary>
/// List of most common rhythm changes in taiko maps.
/// </summary>
/// <remarks>
/// The general guidelines for the values are:
/// <list type="bullet">
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
/// </list>
/// </remarks>
private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
{
new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
@ -37,13 +71,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6),
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
};
private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio)
=> common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
/// <summary>
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
/// </summary>
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
/// <param name="clockRate">The rate of the gameplay clock.</param>
private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
{
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
double ratio = DeltaTime / prevLength;
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
}
}
}

View File

@ -3,11 +3,28 @@
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
{
/// <summary>
/// Represents a rhythm change in a taiko map.
/// </summary>
public class TaikoDifficultyHitObjectRhythm
{
/// <summary>
/// The difficulty multiplier associated with this rhythm change.
/// </summary>
public readonly double Difficulty;
/// <summary>
/// The ratio of current <see cref="TaikoDifficultyHitObject.DeltaTime"/> to previous <see cref="TaikoDifficultyHitObject.DeltaTime"/> for the rhythm change.
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
/// </summary>
public readonly double Ratio;
/// <summary>
/// Creates an object representing a rhythm change.
/// </summary>
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
{
Ratio = numerator / (double)denominator;

View File

@ -10,18 +10,28 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the colour coefficient of taiko difficulty.
/// </summary>
public class Colour : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
/// <summary>
/// Maximum number of entries to keep in <see cref="monoHistory"/>.
/// </summary>
private const int mono_history_max_length = 5;
/// <summary>
/// List of the last <see cref="mono_history_max_length"/> most recent mono patterns, with the most recent at the end of the list.
/// Queue with the lengths of the last <see cref="mono_history_max_length"/> most recent mono (single-colour) patterns,
/// with the most recent value at the end of the queue.
/// </summary>
private readonly LimitedCapacityQueue<int> monoHistory = new LimitedCapacityQueue<int>(mono_history_max_length);
/// <summary>
/// The <see cref="HitType"/> of the last object hit before the one being considered.
/// </summary>
private HitType? previousHitType;
/// <summary>
@ -31,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
protected override double StrainValueOf(DifficultyHitObject current)
{
// changing from/to a drum roll or a swell does not constitute a colour change.
// hits spaced more than a second apart are also exempt from colour strain.
if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
{
previousHitType = null;
@ -75,14 +87,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// </summary>
private double repetitionPenalties()
{
const int l = 2;
const int most_recent_patterns_to_compare = 2;
double penalty = 1.0;
monoHistory.Enqueue(currentMonoLength);
for (int start = monoHistory.Count - l - 1; start >= 0; start--)
for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
{
if (!isSamePattern(start, l))
if (!isSamePattern(start, most_recent_patterns_to_compare))
continue;
int notesSince = 0;
@ -94,17 +106,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
return penalty;
}
private bool isSamePattern(int start, int l)
/// <summary>
/// Determines whether the last <paramref name="mostRecentPatternsToCompare"/> patterns have repeated in the history
/// of single-colour note sequences, starting from <paramref name="start"/>.
/// </summary>
private bool isSamePattern(int start, int mostRecentPatternsToCompare)
{
for (int i = 0; i < l; i++)
for (int i = 0; i < mostRecentPatternsToCompare; i++)
{
if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i])
if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
return false;
}
return true;
}
/// <summary>
/// Calculates the strain penalty for a colour pattern repetition.
/// </summary>
/// <param name="notesSince">The number of notes since the last repetition of the pattern.</param>
private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
}
}

View File

@ -10,64 +10,97 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the rhythm coefficient of taiko difficulty.
/// </summary>
public class Rhythm : Skill
{
protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0;
/// <summary>
/// The note-based decay for rhythm strain.
/// </summary>
/// <remarks>
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
/// </remarks>
private const double strain_decay = 0.96;
/// <summary>
/// Maximum number of entries in <see cref="rhythmHistory"/>.
/// </summary>
private const int rhythm_history_max_length = 8;
/// <summary>
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
/// </summary>
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
/// <summary>
/// Contains the rolling rhythm strain.
/// Used to apply per-note decay.
/// </summary>
private double currentStrain;
/// <summary>
/// Number of notes since the last rhythm change has taken place.
/// </summary>
private int notesSinceRhythmChange;
protected override double StrainValueOf(DifficultyHitObject current)
{
// drum rolls and swells are exempt.
if (!(current.BaseObject is Hit))
{
resetRhythmStrain();
resetRhythmAndStrain();
return 0.0;
}
currentStrain *= strain_decay;
TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current;
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
notesSinceRhythmChange += 1;
if (hitobject.Rhythm.Difficulty == 0.0)
// rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
if (hitObject.Rhythm.Difficulty == 0.0)
{
return 0.0;
}
double objectStrain = hitobject.Rhythm.Difficulty;
double objectStrain = hitObject.Rhythm.Difficulty;
objectStrain *= repetitionPenalties(hitobject);
objectStrain *= repetitionPenalties(hitObject);
objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
objectStrain *= speedPenalty(hitobject.DeltaTime);
objectStrain *= speedPenalty(hitObject.DeltaTime);
// careful - needs to be done here since calls above read this value
notesSinceRhythmChange = 0;
currentStrain += objectStrain;
return currentStrain;
}
// Finds repetitions and applies penalties
private double repetitionPenalties(TaikoDifficultyHitObject hitobject)
/// <summary>
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
/// </summary>
/// <remarks>
/// Repetitions of more recent patterns are associated with a higher penalty.
/// </remarks>
/// <param name="hitObject">The current hit object being considered.</param>
private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
{
double penalty = 1;
rhythmHistory.Enqueue(hitobject);
rhythmHistory.Enqueue(hitObject);
for (int l = 2; l <= rhythm_history_max_length / 2; l++)
for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
{
for (int start = rhythmHistory.Count - l - 1; start >= 0; start--)
for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
{
if (!samePattern(start, l))
if (!samePattern(start, mostRecentPatternsToCompare))
continue;
int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex;
int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex;
penalty *= repetitionPenalty(notesSince);
break;
}
@ -76,37 +109,56 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
return penalty;
}
private bool samePattern(int start, int l)
/// <summary>
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
/// <paramref name="mostRecentPatternsToCompare"/>.
/// </summary>
private bool samePattern(int start, int mostRecentPatternsToCompare)
{
for (int i = 0; i < l; i++)
for (int i = 0; i < mostRecentPatternsToCompare; i++)
{
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm)
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
return false;
}
return true;
}
private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
/// <summary>
/// Calculates a single rhythm repetition penalty.
/// </summary>
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
private double patternLengthPenalty(int patternLength)
/// <summary>
/// Calculates a penalty based on the number of notes since the last rhythm change.
/// Both rare and frequent rhythm changes are penalised.
/// </summary>
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
private static double patternLengthPenalty(int patternLength)
{
double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
return Math.Min(shortPatternPenalty, longPatternPenalty);
}
// Penalty for notes so slow that alternating is not necessary.
private double speedPenalty(double noteLengthMs)
/// <summary>
/// Calculates a penalty for objects that do not require alternating hands.
/// </summary>
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
private double speedPenalty(double deltaTime)
{
if (noteLengthMs < 80) return 1;
if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs);
if (deltaTime < 80) return 1;
if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
resetRhythmStrain();
resetRhythmAndStrain();
return 0.0;
}
private void resetRhythmStrain()
/// <summary>
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
/// </summary>
private void resetRhythmAndStrain()
{
currentStrain = 0.0;
notesSinceRhythmChange = 0;

View File

@ -10,18 +10,45 @@ using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the stamina coefficient of taiko difficulty.
/// </summary>
/// <remarks>
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
/// </remarks>
public class Stamina : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4;
/// <summary>
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
/// </summary>
private const int max_history_length = 2;
/// <summary>
/// The index of the hand this <see cref="Stamina"/> instance is associated with.
/// </summary>
/// <remarks>
/// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
/// This naturally translates onto index offsets of the objects in the map.
/// </remarks>
private readonly int hand;
/// <summary>
/// Stores the last <see cref="max_history_length"/> durations between notes hit with the hand indicated by <see cref="hand"/>.
/// </summary>
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
/// <summary>
/// Stores the <see cref="DifficultyHitObject.DeltaTime"/> of the last object that was hit by the <i>other</i> hand.
/// </summary>
private double offhandObjectDuration = double.MaxValue;
/// <summary>
/// Creates a <see cref="Stamina"/> skill.
/// </summary>
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
public Stamina(bool rightHand)
{
hand = rightHand ? 1 : 0;
@ -58,7 +85,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
return 0;
}
// Penalty for tl tap or roll
/// <summary>
/// Applies a penalty for hit objects marked with <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
/// </summary>
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
private double cheesePenalty(double notePairDuration)
{
if (notePairDuration > 125) return 1;
@ -67,6 +97,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
return 0.6 + (notePairDuration - 100) * 0.016;
}
/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this hand.
/// </summary>
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
private double speedBonus(double notePairDuration)
{
if (notePairDuration >= 200) return 0;

View File

@ -108,6 +108,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
};
}
/// <summary>
/// Calculates the penalty for the stamina skill for maps with low colour difficulty.
/// </summary>
/// <remarks>
/// Some maps (especially converts) can be easy to read despite a high note density.
/// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
/// </remarks>
private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
{
if (colorDifficulty <= 0) return 0.79 - 0.25;
@ -115,22 +122,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
}
private double norm(double p, params double[] values)
{
return Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
}
private double rescale(double sr)
{
if (sr < 0) return sr;
return 10.43 * Math.Log(sr / 8 + 1);
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
/// <summary>
/// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
{
double difficulty = 0;
double weight = 1;
List<double> peaks = new List<double>();
for (int i = 0; i < colour.StrainPeaks.Count; i++)
@ -141,6 +148,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderByDescending(d => d))
{
difficulty += strain * weight;
@ -149,5 +159,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return difficulty;
}
/// <summary>
/// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
/// </summary>
/// <param name="sr">The raw star rating value before re-scaling.</param>
private double rescale(double sr)
{
if (sr < 0) return sr;
return 10.43 * Math.Log(sr / 8 + 1);
}
}
}