From d61388880364b45b49ac037c9d04bd0427fcf80c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 14:50:02 +0900 Subject: [PATCH 01/39] Add initial changes --- .../Preprocessing/StaminaCheeseDetector.cs | 95 ++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 30 +++- .../TaikoDifficultyHitObjectRhythm.cs | 124 +++++++++++++++ .../Difficulty/Skills/Colour.cs | 144 ++++++++++++++++++ .../Difficulty/Skills/SpeedInvariantRhythm.cs | 133 ++++++++++++++++ .../Difficulty/Skills/Stamina.cs | 103 +++++++++++++ .../Difficulty/Skills/Strain.cs | 95 ------------ .../Difficulty/TaikoDifficultyCalculator.cs | 99 +++++++++++- .../Difficulty/TaikoPerformanceCalculator.cs | 10 +- 9 files changed, 726 insertions(+), 107 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs new file mode 100644 index 0000000000..ffdf4cb82a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class StaminaCheeseDetector + { + + private const int roll_min_repetitions = 12; + private const int tl_min_repetitions = 16; + + private List hitObjects; + + public void FindCheese(List difficultyHitObjects) + { + this.hitObjects = difficultyHitObjects; + findRolls(3); + findRolls(4); + findTLTap(0, true); + findTLTap(1, true); + findTLTap(0, false); + findTLTap(1, false); + } + + private void findRolls(int patternLength) + { + List history = new List(); + + int repititionStart = 0; + + for (int i = 0; i < hitObjects.Count; i++) + { + history.Add(hitObjects[i]); + if (history.Count < 2 * patternLength) continue; + if (history.Count > 2 * patternLength) history.RemoveAt(0); + + bool isRepeat = true; + for (int j = 0; j < patternLength; j++) + { + if (history[j].IsKat != history[j + patternLength].IsKat) + { + isRepeat = false; + } + } + + if (!isRepeat) + { + repititionStart = i - 2 * patternLength; + } + + int repeatedLength = i - repititionStart; + + if (repeatedLength >= roll_min_repetitions) + { + // Console.WriteLine("Found Roll Cheese.\tStart: " + repititionStart + "\tEnd: " + i); + for (int j = repititionStart; j < i; j++) + { + (hitObjects[i]).StaminaCheese = true; + } + } + + } + } + + private void findTLTap(int parity, bool kat) + { + int tl_length = -2; + for (int i = parity; i < hitObjects.Count; i += 2) + { + if (kat == hitObjects[i].IsKat) + { + tl_length += 2; + } + else + { + tl_length = -2; + } + + if (tl_length >= tl_min_repetitions) + { + // Console.WriteLine("Found TL Cheese.\tStart: " + (i - tl_length) + "\tEnd: " + i); + for (int j = i - tl_length; j < i; j++) + { + (hitObjects[i]).StaminaCheese = true; + } + } + } + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 6807142327..abad494e62 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -10,11 +11,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public class TaikoDifficultyHitObject : DifficultyHitObject { public readonly bool HasTypeChange; + public readonly bool HasTimingChange; + public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly bool IsKat; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) + public bool StaminaCheese = false; + + public readonly int RhythmID; + + public readonly double NoteLength; + + public readonly int n; + private int counter = 0; + + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) : base(hitObject, lastObject, clockRate) { - HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type; + NoteLength = DeltaTime; + double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; + Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); + RhythmID = Rhythm.ID; + HasTypeChange = lastObject is RimHit != hitObject is RimHit; + IsKat = lastObject is RimHit; + HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); + + n = counter; + counter++; } + + public const int CONST_RHYTHM_ID = 0; + + } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs new file mode 100644 index 0000000000..74b3d285aa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -0,0 +1,124 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class TaikoDifficultyHitObjectRhythm + { + + private static TaikoDifficultyHitObjectRhythm[] commonRhythms; + private static TaikoDifficultyHitObjectRhythm constRhythm; + private static int constRhythmID; + + public int ID = 0; + public readonly double Difficulty; + private readonly double ratio; + + private static void initialiseCommonRhythms() + { + + /* + + ALCHYRS CODE + + If (change < 0.48) Then 'sometimes gaps are slightly different due to position rounding + Return 0.65 'This number increases value of anything that more than doubles speed. Affects doubles. + ElseIf (change < 0.52) Then + Return 0.5 'speed doubling - this one affects pretty much every map other than stream maps + ElseIf change <= 0.9 Then + Return 1.0 'This number increases value of 1/4 -> 1/6 and other weird rhythms. + ElseIf change < 0.95 Then + Return 0.25 '.9 + ElseIf change > 1.95 Then + Return 0.3 'half speed or more - this affects pretty much every map + ElseIf change > 1.15 Then + Return 0.425 'in between - this affects (mostly) 1/6 -> 1/4 + ElseIf change > 1.05 Then + Return 0.15 '.9, small speed changes + + */ + + + commonRhythms = new TaikoDifficultyHitObjectRhythm[] + { + new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + 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(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) + }; + + for (int i = 0; i < commonRhythms.Length; i++) + { + commonRhythms[i].ID = i; + } + + constRhythmID = 0; + constRhythm = commonRhythms[constRhythmID]; + + } + + public bool IsRepeat() + { + return ID == constRhythmID; + } + + public static bool IsRepeat(int id) + { + return id == constRhythmID; + } + + public bool IsSpeedup() + { + return ratio < 1.0; + } + + public bool IsLargeSpeedup() + { + return ratio < 0.49; + } + + private TaikoDifficultyHitObjectRhythm(double ratio, double difficulty) + { + this.ratio = ratio; + this.Difficulty = difficulty; + } + + private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) + { + this.ratio = ((double)numerator) / ((double)denominator); + this.Difficulty = difficulty; + } + + // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms + public static TaikoDifficultyHitObjectRhythm GetClosest(double ratio) + { + if (commonRhythms == null) + { + initialiseCommonRhythms(); + } + + TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; + double closestDistance = Double.MaxValue; + + foreach (TaikoDifficultyHitObjectRhythm r in commonRhythms) + { + if (Math.Abs(r.ratio - ratio) < closestDistance) + { + closestRhythm = r; + closestDistance = Math.Abs(r.ratio - ratio); + } + } + + return closestRhythm; + + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs new file mode 100644 index 0000000000..6ed826f345 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -0,0 +1,144 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Colour : Skill + { + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.3; + + private ColourSwitch lastColourSwitch = ColourSwitch.None; + private int sameColourCount = 1; + + private int[] previousDonLengths = {0, 0}, previousKatLengths = {0, 0}; + private int sameTypeCount = 1; + // TODO: make this smarter (dont initialise with "Don") + private bool previousIsKat = false; + + protected override double StrainValueOf(DifficultyHitObject current) + { + return StrainValueOfNew(current); + } + + protected double StrainValueOfNew(DifficultyHitObject current) + { + + double returnVal = 0.0; + double returnMultiplier = 1.0; + + if (previousIsKat != ((TaikoDifficultyHitObject) current).IsKat) + { + returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); + + if (previousIsKat) + { + if (sameTypeCount % 2 == previousDonLengths[0] % 2) + { + returnMultiplier *= 0.8; + } + + if (previousKatLengths[0] == sameTypeCount) + { + returnMultiplier *= 0.525; + } + + if (previousKatLengths[1] == sameTypeCount) + { + returnMultiplier *= 0.75; + } + + previousKatLengths[1] = previousKatLengths[0]; + previousKatLengths[0] = sameTypeCount; + } + else + { + if (sameTypeCount % 2 == previousKatLengths[0] % 2) + { + returnMultiplier *= 0.8; + } + + if (previousDonLengths[0] == sameTypeCount) + { + returnMultiplier *= 0.525; + } + + if (previousDonLengths[1] == sameTypeCount) + { + returnMultiplier *= 0.75; + } + + previousDonLengths[1] = previousDonLengths[0]; + previousDonLengths[0] = sameTypeCount; + } + + + sameTypeCount = 1; + previousIsKat = ((TaikoDifficultyHitObject) current).IsKat; + + } + + else + { + sameTypeCount += 1; + } + + return Math.Min(1.25, returnVal) * returnMultiplier; + } + + protected double StrainValueOfOld(DifficultyHitObject current) + { + + double addition = 0; + + // We get an extra addition if we are not a slider or spinner + if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + { + if (hasColourChange(current)) + addition = 0.75; + } + else + { + lastColourSwitch = ColourSwitch.None; + sameColourCount = 1; + } + + return addition; + } + + + private bool hasColourChange(DifficultyHitObject current) + { + var taikoCurrent = (TaikoDifficultyHitObject) current; + + if (!taikoCurrent.HasTypeChange) + { + sameColourCount++; + return false; + } + + var oldColourSwitch = lastColourSwitch; + var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; + + lastColourSwitch = newColourSwitch; + sameColourCount = 1; + + // We only want a bonus if the parity of the color switch changes + return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; + } + + private enum ColourSwitch + { + None, + Even, + Odd + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs new file mode 100644 index 0000000000..b48cfc675f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Rhythm : Skill + { + + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0; + private const double strain_decay = 0.96; + private double currentStrain = 0.0; + + private readonly List ratioObjectHistory = new List(); + private int ratioHistoryLength = 0; + private const int ratio_history_max_length = 8; + + private int rhythmLength = 0; + + // Penalty for repeated sequences of rhythm changes + private double repititionPenalty(double timeSinceRepititionMS) + { + double t = Math.Atan(timeSinceRepititionMS / 3000) / (Math.PI / 2); + return t; + } + + private double repititionPenalty(int notesSince) + { + double t = notesSince * 150; + t = Math.Atan(t / 3000) / (Math.PI / 2); + return t; + } + + // Penalty for short patterns + // Must be low to buff maps like wizodmiot + // Must not be too low for maps like inverse world + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } + + // Penalty for notes so slow that alting is not necessary. + private double speedPenalty(double noteLengthMS) + { + if (noteLengthMS < 80) return 1; + if (noteLengthMS < 160) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 300) return 0.6; + return 0.0; + } + + // Penalty for the first rhythm change in a pattern + private const double first_burst_penalty = 0.1; + private bool prevIsSpeedup = true; + + protected override double StrainValueOf(DifficultyHitObject dho) + { + currentStrain *= strain_decay; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) dho; + rhythmLength += 1; + if (!currentHO.HasTimingChange) + { + return 0.0; + } + + double objectDifficulty = currentHO.Rhythm.Difficulty; + + // find repeated ratios + + ratioObjectHistory.Add(currentHO); + ratioHistoryLength += 1; + if (ratioHistoryLength > ratio_history_max_length) + { + ratioObjectHistory.RemoveAt(0); + ratioHistoryLength -= 1; + } + + for (int l = 2; l <= ratio_history_max_length / 2; l++) + { + for (int start = ratioHistoryLength - l - 1; start >= 0; start--) + { + bool samePattern = true; + for (int i = 0; i < l; i++) + { + if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) + { + samePattern = false; + } + } + + if (samePattern) // Repitition found! + { + int notesSince = currentHO.n - ratioObjectHistory[start].n; + objectDifficulty *= repititionPenalty(notesSince); + break; + } + } + } + + + if (currentHO.Rhythm.IsSpeedup()) + { + objectDifficulty *= 1; + if (currentHO.Rhythm.IsLargeSpeedup()) objectDifficulty *= 1; + if (prevIsSpeedup) objectDifficulty *= 1; + + prevIsSpeedup = true; + } + else + { + prevIsSpeedup = false; + } + + objectDifficulty *= patternLengthPenalty(rhythmLength); + objectDifficulty *= speedPenalty(currentHO.NoteLength); + + rhythmLength = 0; + + currentStrain += objectDifficulty; + return currentStrain; + + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs new file mode 100644 index 0000000000..349f4c29fa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -0,0 +1,103 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Stamina : Skill + { + + private int hand; + private int noteNumber = 0; + + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.4; + // i only add strain every second note so its kind of like using 0.16 + + private readonly int maxHistoryLength = 2; + private List noteDurationHistory = new List(); + + private List lastHitObjects = new List(); + + private double offhandObjectDuration = double.MaxValue; + + // Penalty for tl tap or roll + private double cheesePenalty(double last2NoteDuration) + { + if (last2NoteDuration > 125) return 1; + if (last2NoteDuration < 100) return 0.6; + + return 0.6 + (last2NoteDuration - 100) * 0.016; + } + + private double speedBonus(double last2NoteDuration) + { + // note that we are only looking at every 2nd note, so a 300bpm stream has a note duration of 100ms. + if (last2NoteDuration >= 200) return 0; + double bonus = 200 - last2NoteDuration; + bonus *= bonus; + return bonus / 100000; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + noteNumber += 1; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) current; + + if (noteNumber % 2 == hand) + { + lastHitObjects.Add(currentHO); + noteDurationHistory.Add(currentHO.NoteLength + offhandObjectDuration); + + if (noteNumber == 1) + return 1; + + if (noteDurationHistory.Count > maxHistoryLength) + noteDurationHistory.RemoveAt(0); + + double shortestRecentNote = min(noteDurationHistory); + double bonus = 0; + bonus += speedBonus(shortestRecentNote); + + double objectStaminaStrain = 1 + bonus; + if (currentHO.StaminaCheese) objectStaminaStrain *= cheesePenalty(currentHO.NoteLength + offhandObjectDuration); + + return objectStaminaStrain; + } + + offhandObjectDuration = currentHO.NoteLength; + return 0; + } + + private static double min(List l) + { + double minimum = double.MaxValue; + + foreach (double d in l) + { + if (d < minimum) + minimum = d; + } + return minimum; + } + + public Stamina(bool rightHand) + { + hand = 0; + if (rightHand) + { + hand = 1; + } + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs deleted file mode 100644 index c6fe273b50..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Strain : Skill - { - private const double rhythm_change_base_threshold = 0.2; - private const double rhythm_change_base = 2.0; - - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; - - private ColourSwitch lastColourSwitch = ColourSwitch.None; - - private int sameColourCount = 1; - - protected override double StrainValueOf(DifficultyHitObject current) - { - double addition = 1; - - // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) - { - if (hasColourChange(current)) - addition += 0.75; - - if (hasRhythmChange(current)) - addition += 1; - } - else - { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; - } - - double additionFactor = 1; - - // Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50 - if (current.DeltaTime < 50) - additionFactor = 0.4 + 0.6 * current.DeltaTime / 50; - - return additionFactor * addition; - } - - private bool hasRhythmChange(DifficultyHitObject current) - { - // We don't want a division by zero if some random mapper decides to put two HitObjects at the same time. - if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0) - return false; - - double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime); - - if (timeElapsedRatio >= 8) - return false; - - double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0; - - return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold; - } - - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 32d49ea39c..68da0f0e02 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; @@ -19,39 +20,121 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.04125; + + private const double rhythmSkillMultiplier = 0.15; + private const double colourSkillMultiplier = 0.01; + private const double staminaSkillMultiplier = 0.02; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { } + private double readingPenalty(double staminaDifficulty) + { + return Math.Max(0, 1 - staminaDifficulty / 14); + // return 1; + } + + private double norm(double p, double v1, double v2, double v3) + { + return Math.Pow( + Math.Pow(v1, p) + + Math.Pow(v2, p) + + Math.Pow(v3, p) + , 1 / p); + } + + private double rescale(double sr) + { + if (sr <= 1) return sr; + sr -= 1; + sr = 1.5 * Math.Pow(sr, 0.76); + sr += 1; + return sr; + } + + private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + { + + double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; + double readingPenalty = this.readingPenalty(staminaRating); + + + double difficulty = 0; + double weight = 1; + List peaks = new List(); + for (int i = 0; i < colour.StrainPeaks.Count; i++) + { + double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier * readingPenalty; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; + peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); + } + foreach (double strain in peaks.OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; + double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; + double readingPenalty = this.readingPenalty(staminaRating); + + double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier * readingPenalty; + double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; + double combinedRating = combinedDifficulty(skills[0], skills[1], skills[2], skills[3]); + + // Console.WriteLine("colour\t" + colourRating); + // Console.WriteLine("rhythm\t" + rhythmRating); + // Console.WriteLine("stamina\t" + staminaRating); + double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); + // Console.WriteLine("combinedRating\t" + combinedRating); + // Console.WriteLine("separatedRating\t" + separatedRating); + double starRating = 1.4 * separatedRating + 0.5 * combinedRating; + starRating = rescale(starRating); + HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); return new TaikoDifficultyAttributes { - StarRating = skills.Single().DifficultyValue() * star_scaling_factor, + StarRating = starRating, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; + } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - for (int i = 1; i < beatmap.HitObjects.Count; i++) - yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate); + List taikoDifficultyHitObjects = new List(); + for (int i = 2; i < beatmap.HitObjects.Count; i++) + { + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); + } + new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); + for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) + yield return taikoDifficultyHitObjects[i]; } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() }; + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + { + new Colour(), + new Rhythm(), + new Stamina(true), + new Stamina(false), + }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] { @@ -60,5 +143,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty new TaikoModEasy(), new TaikoModHardRock(), }; + + /* + protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) + => taikoCalculate(beatmap, mods, clockRate); + */ + } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 3a0fb64622..70249db0f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public override double Calculate(Dictionary categoryDifficulty = null) { mods = Score.Mods; - countGreat = Score.Statistics[HitResult.Great]; - countGood = Score.Statistics[HitResult.Good]; - countMeh = Score.Statistics[HitResult.Meh]; - countMiss = Score.Statistics[HitResult.Miss]; + countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); // Don't count scores made with supposedly unranked mods if (mods.Any(m => !m.Ranked)) @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; // Longer maps are worth more - double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); strainValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available From 779af48802ad135529b9e60e0d9df58871fc03f8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 14:53:42 +0900 Subject: [PATCH 02/39] Resolve errors + auto-format --- .../Preprocessing/StaminaCheeseDetector.cs | 7 ++----- .../Preprocessing/TaikoDifficultyHitObject.cs | 10 +++++----- .../Difficulty/Skills/Colour.cs | 16 ++++++---------- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 9 ++++----- .../Difficulty/Skills/Stamina.cs | 11 ++++------- .../Difficulty/TaikoDifficultyCalculator.cs | 9 ++++----- 6 files changed, 25 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index ffdf4cb82a..4f645d7e51 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -1,15 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class StaminaCheeseDetector { - private const int roll_min_repetitions = 12; private const int tl_min_repetitions = 16; @@ -39,6 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (history.Count > 2 * patternLength) history.RemoveAt(0); bool isRepeat = true; + for (int j = 0; j < patternLength; j++) { if (history[j].IsKat != history[j + patternLength].IsKat) @@ -62,13 +60,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing (hitObjects[i]).StaminaCheese = true; } } - } } private void findTLTap(int parity, bool kat) { int tl_length = -2; + for (int i = parity; i < hitObjects.Count; i += 2) { if (kat == hitObjects[i].IsKat) @@ -90,6 +88,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index abad494e62..42c23a3d14 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -27,12 +26,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) : base(hitObject, lastObject, clockRate) { + var lastHit = lastObject as Hit; + var currentHit = hitObject as Hit; + NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); RhythmID = Rhythm.ID; - HasTypeChange = lastObject is RimHit != hitObject is RimHit; - IsKat = lastObject is RimHit; + HasTypeChange = lastHit?.Type != currentHit?.Type; + IsKat = lastHit?.Type == HitType.Rim; HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); n = counter; @@ -40,7 +42,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } public const int CONST_RHYTHM_ID = 0; - - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 6ed826f345..8b3cc0bb8f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -17,8 +17,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private ColourSwitch lastColourSwitch = ColourSwitch.None; private int sameColourCount = 1; - private int[] previousDonLengths = {0, 0}, previousKatLengths = {0, 0}; + private int[] previousDonLengths = { 0, 0 }, previousKatLengths = { 0, 0 }; + private int sameTypeCount = 1; + // TODO: make this smarter (dont initialise with "Don") private bool previousIsKat = false; @@ -29,11 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected double StrainValueOfNew(DifficultyHitObject current) { - double returnVal = 0.0; double returnMultiplier = 1.0; - if (previousIsKat != ((TaikoDifficultyHitObject) current).IsKat) + if (previousIsKat != ((TaikoDifficultyHitObject)current).IsKat) { returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); @@ -78,10 +79,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills previousDonLengths[0] = sameTypeCount; } - sameTypeCount = 1; - previousIsKat = ((TaikoDifficultyHitObject) current).IsKat; - + previousIsKat = ((TaikoDifficultyHitObject)current).IsKat; } else @@ -94,7 +93,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected double StrainValueOfOld(DifficultyHitObject current) { - double addition = 0; // We get an extra addition if we are not a slider or spinner @@ -112,10 +110,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return addition; } - private bool hasColourChange(DifficultyHitObject current) { - var taikoCurrent = (TaikoDifficultyHitObject) current; + var taikoCurrent = (TaikoDifficultyHitObject)current; if (!taikoCurrent.HasTypeChange) { @@ -139,6 +136,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills Even, Odd } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index b48cfc675f..cdd1d2d5d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Rhythm : Skill { - protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0; private const double strain_decay = 0.96; @@ -64,8 +63,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { currentStrain *= strain_decay; - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) dho; + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)dho; rhythmLength += 1; + if (!currentHO.HasTimingChange) { return 0.0; @@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills ratioObjectHistory.Add(currentHO); ratioHistoryLength += 1; + if (ratioHistoryLength > ratio_history_max_length) { ratioObjectHistory.RemoveAt(0); @@ -88,6 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills for (int start = ratioHistoryLength - l - 1; start >= 0; start--) { bool samePattern = true; + for (int i = 0; i < l; i++) { if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) @@ -105,7 +107,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } } - if (currentHO.Rhythm.IsSpeedup()) { objectDifficulty *= 1; @@ -126,8 +127,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills currentStrain += objectDifficulty; return currentStrain; - } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 349f4c29fa..1ecca886df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -1,24 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private int hand; private int noteNumber = 0; protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.4; // i only add strain every second note so its kind of like using 0.16 @@ -51,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { noteNumber += 1; - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) current; + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; if (noteNumber % 2 == hand) { @@ -87,17 +83,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (d < minimum) minimum = d; } + return minimum; } public Stamina(bool rightHand) { hand = 0; + if (rightHand) { hand = 1; } } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 68da0f0e02..26e92a1ea1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythmSkillMultiplier = 0.15; private const double colourSkillMultiplier = 0.01; private const double staminaSkillMultiplier = 0.02; @@ -56,14 +55,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; double readingPenalty = this.readingPenalty(staminaRating); - double difficulty = 0; double weight = 1; List peaks = new List(); + for (int i = 0; i < colour.StrainPeaks.Count; i++) { double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier * readingPenalty; @@ -71,6 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } + foreach (double strain in peaks.OrderByDescending(d => d)) { difficulty += strain * weight; @@ -113,16 +112,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; - } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); + for (int i = 2; i < beatmap.HitObjects.Count; i++) { taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); } + new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) yield return taikoDifficultyHitObjects[i]; @@ -148,6 +148,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) => taikoCalculate(beatmap, mods, clockRate); */ - } } From b0ed39f32baafa5de158cf95b7dc025bf6ce4d6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 14:57:47 +0900 Subject: [PATCH 03/39] Do not use statics --- .../Preprocessing/TaikoDifficultyHitObject.cs | 6 +- .../TaikoDifficultyHitObjectRhythm.cs | 67 +++++++------------ .../Difficulty/Skills/Colour.cs | 5 +- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 6 +- .../Difficulty/TaikoDifficultyCalculator.cs | 3 +- 5 files changed, 36 insertions(+), 51 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 42c23a3d14..75b1b3e268 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int n; private int counter = 0; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, TaikoDifficultyHitObjectRhythm rhythm) : base(hitObject, lastObject, clockRate) { var lastHit = lastObject as Hit; @@ -31,11 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); + Rhythm = rhythm.GetClosest(NoteLength / prevLength); RhythmID = Rhythm.ID; HasTypeChange = lastHit?.Type != currentHit?.Type; IsKat = lastHit?.Type == HitType.Rim; - HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); + HasTimingChange = !rhythm.IsRepeat(RhythmID); n = counter; counter++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 74b3d285aa..8a6f0e5bfe 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -7,18 +7,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObjectRhythm { - - private static TaikoDifficultyHitObjectRhythm[] commonRhythms; - private static TaikoDifficultyHitObjectRhythm constRhythm; - private static int constRhythmID; + private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms; + private readonly TaikoDifficultyHitObjectRhythm constRhythm; + private int constRhythmID; public int ID = 0; public readonly double Difficulty; private readonly double ratio; - private static void initialiseCommonRhythms() + public bool IsRepeat() { + return ID == constRhythmID; + } + public bool IsRepeat(int id) + { + return id == constRhythmID; + } + + public bool IsSpeedup() + { + return ratio < 1.0; + } + + public bool IsLargeSpeedup() + { + return ratio < 0.49; + } + + public TaikoDifficultyHitObjectRhythm() + { /* ALCHYRS CODE @@ -40,8 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing */ - - commonRhythms = new TaikoDifficultyHitObjectRhythm[] + commonRhythms = new[] { new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), @@ -61,33 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing constRhythmID = 0; constRhythm = commonRhythms[constRhythmID]; - - } - - public bool IsRepeat() - { - return ID == constRhythmID; - } - - public static bool IsRepeat(int id) - { - return id == constRhythmID; - } - - public bool IsSpeedup() - { - return ratio < 1.0; - } - - public bool IsLargeSpeedup() - { - return ratio < 0.49; - } - - private TaikoDifficultyHitObjectRhythm(double ratio, double difficulty) - { - this.ratio = ratio; - this.Difficulty = difficulty; } private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) @@ -97,13 +87,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms - public static TaikoDifficultyHitObjectRhythm GetClosest(double ratio) + public TaikoDifficultyHitObjectRhythm GetClosest(double ratio) { - if (commonRhythms == null) - { - initialiseCommonRhythms(); - } - TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; double closestDistance = Double.MaxValue; @@ -117,8 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } return closestRhythm; - } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 8b3cc0bb8f..da255dcdd7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private ColourSwitch lastColourSwitch = ColourSwitch.None; private int sameColourCount = 1; - private int[] previousDonLengths = { 0, 0 }, previousKatLengths = { 0, 0 }; + private readonly int[] previousDonLengths = { 0, 0 }; + private readonly int[] previousKatLengths = { 0, 0 }; private int sameTypeCount = 1; // TODO: make this smarter (dont initialise with "Don") - private bool previousIsKat = false; + private bool previousIsKat; protected override double StrainValueOf(DifficultyHitObject current) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index cdd1d2d5d0..2d99bac7a9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0; private const double strain_decay = 0.96; - private double currentStrain = 0.0; + private double currentStrain; private readonly List ratioObjectHistory = new List(); - private int ratioHistoryLength = 0; + private int ratioHistoryLength; private const int ratio_history_max_length = 8; - private int rhythmLength = 0; + private int rhythmLength; // Penalty for repeated sequences of rhythm changes private double repititionPenalty(double timeSinceRepititionMS) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 26e92a1ea1..6e1fae01ee 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -117,10 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); + var rhythm = new TaikoDifficultyHitObjectRhythm(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); From 9461097b0070c15f275c69ea67be7df638e80208 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 22 May 2020 20:50:21 +0900 Subject: [PATCH 04/39] Update with latest changes --- .../Difficulty/Skills/Colour.cs | 198 ++++++++---------- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 8 +- .../Difficulty/TaikoDifficultyAttributes.cs | 4 + .../Difficulty/TaikoDifficultyCalculator.cs | 28 +-- 4 files changed, 114 insertions(+), 124 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index da255dcdd7..bd94c8aa65 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -12,130 +14,108 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public class Colour : Skill { protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; + protected override double StrainDecayBase => 0.4; - private ColourSwitch lastColourSwitch = ColourSwitch.None; - private int sameColourCount = 1; + private bool prevIsKat = false; - private readonly int[] previousDonLengths = { 0, 0 }; - private readonly int[] previousKatLengths = { 0, 0 }; + private int currentMonoLength = 1; + private List monoHistory = new List(); + private readonly int mono_history_max_length = 5; + private int monoHistoryLength = 0; - private int sameTypeCount = 1; + private double sameParityPenalty() + { + return 0.0; + } - // TODO: make this smarter (dont initialise with "Don") - private bool previousIsKat; + private double repititionPenalty(int notesSince) + { + double d = notesSince; + return Math.Atan(d / 30) / (Math.PI / 2); + } + + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.25 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } protected override double StrainValueOf(DifficultyHitObject current) { - return StrainValueOfNew(current); - } + double objectDifficulty = 0.0; - protected double StrainValueOfNew(DifficultyHitObject current) - { - double returnVal = 0.0; - double returnMultiplier = 1.0; - - if (previousIsKat != ((TaikoDifficultyHitObject)current).IsKat) - { - returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); - - if (previousIsKat) - { - if (sameTypeCount % 2 == previousDonLengths[0] % 2) - { - returnMultiplier *= 0.8; - } - - if (previousKatLengths[0] == sameTypeCount) - { - returnMultiplier *= 0.525; - } - - if (previousKatLengths[1] == sameTypeCount) - { - returnMultiplier *= 0.75; - } - - previousKatLengths[1] = previousKatLengths[0]; - previousKatLengths[0] = sameTypeCount; - } - else - { - if (sameTypeCount % 2 == previousKatLengths[0] % 2) - { - returnMultiplier *= 0.8; - } - - if (previousDonLengths[0] == sameTypeCount) - { - returnMultiplier *= 0.525; - } - - if (previousDonLengths[1] == sameTypeCount) - { - returnMultiplier *= 0.75; - } - - previousDonLengths[1] = previousDonLengths[0]; - previousDonLengths[0] = sameTypeCount; - } - - sameTypeCount = 1; - previousIsKat = ((TaikoDifficultyHitObject)current).IsKat; - } - - else - { - sameTypeCount += 1; - } - - return Math.Min(1.25, returnVal) * returnMultiplier; - } - - protected double StrainValueOfOld(DifficultyHitObject current) - { - double addition = 0; - - // We get an extra addition if we are not a slider or spinner if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) { - if (hasColourChange(current)) - addition = 0.75; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; + + if (currentHO.IsKat == prevIsKat) + { + currentMonoLength += 1; + } + + else + { + + objectDifficulty = 1.0; + + if (monoHistoryLength > 0 && (monoHistory[monoHistoryLength - 1] + currentMonoLength) % 2 == 0) + { + objectDifficulty *= sameParityPenalty(); + } + + monoHistory.Add(currentMonoLength); + monoHistoryLength += 1; + + if (monoHistoryLength > mono_history_max_length) + { + monoHistory.RemoveAt(0); + monoHistoryLength -= 1; + } + + for (int l = 2; l <= mono_history_max_length / 2; l++) + { + for (int start = monoHistoryLength - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistoryLength - l + i]) + { + samePattern = false; + } + } + + if (samePattern) // Repitition found! + { + int notesSince = 0; + for (int i = start; i < monoHistoryLength; i++) notesSince += monoHistory[i]; + objectDifficulty *= repititionPenalty(notesSince); + break; + } + } + } + + currentMonoLength = 1; + prevIsKat = currentHO.IsKat; + + } + } - else + + /* + string path = @"out.txt"; + using (StreamWriter sw = File.AppendText(path)) { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; + if (((TaikoDifficultyHitObject)current).IsKat) sw.WriteLine("k " + Math.Min(1.25, returnVal) * returnMultiplier); + else sw.WriteLine("d " + Math.Min(1.25, returnVal) * returnMultiplier); } + */ - return addition; + return objectDifficulty; } - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index 2d99bac7a9..28198612b2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -49,9 +49,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Penalty for notes so slow that alting is not necessary. private double speedPenalty(double noteLengthMS) { + if (noteLengthMS < 80) return 1; - if (noteLengthMS < 160) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 300) return 0.6; + // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 210) return 0.6; + + currentStrain = 0.0; return 0.0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 75d3807bba..783f1ba696 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { + public double StaminaStrain; + public double RhythmStrain; + public double ColourStrain; + public double ApproachRate; public double GreatHitWindow; public int MaxCombo; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6e1fae01ee..2a6fa81a57 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,10 +29,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - private double readingPenalty(double staminaDifficulty) + private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { - return Math.Max(0, 1 - staminaDifficulty / 14); - // return 1; + return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } private double norm(double p, double v1, double v2, double v3) @@ -48,15 +47,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { if (sr <= 1) return sr; sr -= 1; - sr = 1.5 * Math.Pow(sr, 0.76); + sr = 1.6 * Math.Pow(sr, 0.7); sr += 1; return sr; } - private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double combinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; - double readingPenalty = this.readingPenalty(staminaRating); double difficulty = 0; double weight = 1; @@ -64,9 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colour.StrainPeaks.Count; i++) { - double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier * readingPenalty; + double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier; double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -85,11 +82,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; - double readingPenalty = this.readingPenalty(staminaRating); - - double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier * readingPenalty; + double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier; double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; - double combinedRating = combinedDifficulty(skills[0], skills[1], skills[2], skills[3]); + + double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); + staminaRating *= staminaPenalty; + + double combinedRating = combinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); // Console.WriteLine("colour\t" + colourRating); // Console.WriteLine("rhythm\t" + rhythmRating); @@ -107,6 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { StarRating = starRating, Mods = mods, + StaminaStrain = staminaRating, + RhythmStrain = rhythmRating, + ColourStrain = colourRating, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), From 5852a37eb7499ac3969def1845fd3e2115f3236d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 24 May 2020 11:48:56 +0900 Subject: [PATCH 05/39] Update with latest changes --- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 1 - .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index 28198612b2..dd90463113 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Penalty for notes so slow that alting is not necessary. private double speedPenalty(double noteLengthMS) { - if (noteLengthMS < 80) return 1; // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 2a6fa81a57..dc2b68e0ca 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { + if (colorDifficulty <= 0) return 0.79 - 0.25; return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } @@ -123,7 +124,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 2; i < beatmap.HitObjects.Count; i++) { - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); + // Check for negative durations + if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); From 68027fcc2c46dacfbfda5a64eb5745d40eae6c81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Jun 2020 16:30:26 +0900 Subject: [PATCH 06/39] Update with latest changes --- .../Preprocessing/StaminaCheeseDetector.cs | 37 +++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 28 ++-- .../TaikoDifficultyHitObjectRhythm.cs | 100 +----------- .../Difficulty/Skills/Colour.cs | 153 +++++++++--------- .../Difficulty/Skills/Rhythm.cs | 115 +++++++++++++ .../Difficulty/Skills/SpeedInvariantRhythm.cs | 135 ---------------- .../Difficulty/Skills/Stamina.cs | 79 ++++----- .../Difficulty/TaikoDifficultyCalculator.cs | 71 ++++---- .../Difficulty/TaikoPerformanceCalculator.cs | 4 - 9 files changed, 295 insertions(+), 427 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 4f645d7e51..b52dad5198 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -14,25 +14,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public void FindCheese(List difficultyHitObjects) { - this.hitObjects = difficultyHitObjects; + hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); - findTLTap(0, true); - findTLTap(1, true); - findTLTap(0, false); - findTLTap(1, false); + findTlTap(0, true); + findTlTap(1, true); + findTlTap(0, false); + findTlTap(1, false); } private void findRolls(int patternLength) { List history = new List(); - int repititionStart = 0; + int repetitionStart = 0; for (int i = 0; i < hitObjects.Count; i++) { history.Add(hitObjects[i]); if (history.Count < 2 * patternLength) continue; + if (history.Count > 2 * patternLength) history.RemoveAt(0); bool isRepeat = true; @@ -47,43 +48,41 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!isRepeat) { - repititionStart = i - 2 * patternLength; + repetitionStart = i - 2 * patternLength; } - int repeatedLength = i - repititionStart; + int repeatedLength = i - repetitionStart; if (repeatedLength >= roll_min_repetitions) { - // Console.WriteLine("Found Roll Cheese.\tStart: " + repititionStart + "\tEnd: " + i); - for (int j = repititionStart; j < i; j++) + for (int j = repetitionStart; j < i; j++) { - (hitObjects[i]).StaminaCheese = true; + hitObjects[i].StaminaCheese = true; } } } } - private void findTLTap(int parity, bool kat) + private void findTlTap(int parity, bool kat) { - int tl_length = -2; + int tlLength = -2; for (int i = parity; i < hitObjects.Count; i += 2) { if (kat == hitObjects[i].IsKat) { - tl_length += 2; + tlLength += 2; } else { - tl_length = -2; + tlLength = -2; } - if (tl_length >= tl_min_repetitions) + if (tlLength >= tl_min_repetitions) { - // Console.WriteLine("Found TL Cheese.\tStart: " + (i - tl_length) + "\tEnd: " + i); - for (int j = i - tl_length; j < i; j++) + for (int j = i - tlLength; j < i; j++) { - (hitObjects[i]).StaminaCheese = true; + hitObjects[i].StaminaCheese = true; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 75b1b3e268..cd45db2119 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -1,6 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -9,38 +12,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObject : DifficultyHitObject { - public readonly bool HasTypeChange; - public readonly bool HasTimingChange; public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly bool IsKat; public bool StaminaCheese = false; - public readonly int RhythmID; - public readonly double NoteLength; - public readonly int n; - private int counter = 0; + public readonly int N; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, TaikoDifficultyHitObjectRhythm rhythm) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int n, IEnumerable commonRhythms) : base(hitObject, lastObject, clockRate) { - var lastHit = lastObject as Hit; var currentHit = hitObject as Hit; NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = rhythm.GetClosest(NoteLength / prevLength); - RhythmID = Rhythm.ID; - HasTypeChange = lastHit?.Type != currentHit?.Type; - IsKat = lastHit?.Type == HitType.Rim; - HasTimingChange = !rhythm.IsRepeat(RhythmID); + Rhythm = getClosestRhythm(NoteLength / prevLength, commonRhythms); + IsKat = currentHit?.Type == HitType.Rim; - n = counter; - counter++; + N = n; } - public const int CONST_RHYTHM_ID = 0; + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) + { + return commonRhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 8a6f0e5bfe..0ad885d9bd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -1,107 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; - namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObjectRhythm { - private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms; - private readonly TaikoDifficultyHitObjectRhythm constRhythm; - private int constRhythmID; - - public int ID = 0; public readonly double Difficulty; - private readonly double ratio; + public readonly double Ratio; + public readonly bool IsRepeat; - public bool IsRepeat() + public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty, bool isRepeat) { - return ID == constRhythmID; - } - - public bool IsRepeat(int id) - { - return id == constRhythmID; - } - - public bool IsSpeedup() - { - return ratio < 1.0; - } - - public bool IsLargeSpeedup() - { - return ratio < 0.49; - } - - public TaikoDifficultyHitObjectRhythm() - { - /* - - ALCHYRS CODE - - If (change < 0.48) Then 'sometimes gaps are slightly different due to position rounding - Return 0.65 'This number increases value of anything that more than doubles speed. Affects doubles. - ElseIf (change < 0.52) Then - Return 0.5 'speed doubling - this one affects pretty much every map other than stream maps - ElseIf change <= 0.9 Then - Return 1.0 'This number increases value of 1/4 -> 1/6 and other weird rhythms. - ElseIf change < 0.95 Then - Return 0.25 '.9 - ElseIf change > 1.95 Then - Return 0.3 'half speed or more - this affects pretty much every map - ElseIf change > 1.15 Then - Return 0.425 'in between - this affects (mostly) 1/6 -> 1/4 - ElseIf change > 1.05 Then - Return 0.15 '.9, small speed changes - - */ - - commonRhythms = new[] - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - 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(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - for (int i = 0; i < commonRhythms.Length; i++) - { - commonRhythms[i].ID = i; - } - - constRhythmID = 0; - constRhythm = commonRhythms[constRhythmID]; - } - - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) - { - this.ratio = ((double)numerator) / ((double)denominator); - this.Difficulty = difficulty; - } - - // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms - public TaikoDifficultyHitObjectRhythm GetClosest(double ratio) - { - TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; - double closestDistance = Double.MaxValue; - - foreach (TaikoDifficultyHitObjectRhythm r in commonRhythms) - { - if (Math.Abs(r.ratio - ratio) < closestDistance) - { - closestRhythm = r; - closestDistance = Math.Abs(r.ratio - ratio); - } - } - - return closestRhythm; + Ratio = numerator / (double)denominator; + Difficulty = difficulty; + IsRepeat = isRepeat; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index bd94c8aa65..7c1623c54e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -16,106 +15,100 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - private bool prevIsKat = false; + private NoteColour prevNoteColour = NoteColour.None; private int currentMonoLength = 1; - private List monoHistory = new List(); - private readonly int mono_history_max_length = 5; - private int monoHistoryLength = 0; + private readonly List monoHistory = new List(); + private const int mono_history_max_length = 5; private double sameParityPenalty() { return 0.0; } - private double repititionPenalty(int notesSince) + private double repetitionPenalty(int notesSince) { - double d = notesSince; - return Math.Atan(d / 30) / (Math.PI / 2); + double n = notesSince; + return Math.Min(1.0, 0.032 * n); } - private double patternLengthPenalty(int patternLength) + private double repetitionPenalties() { - double shortPatternPenalty = Math.Min(0.25 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); + double penalty = 1.0; + + monoHistory.Add(currentMonoLength); + + if (monoHistory.Count > mono_history_max_length) + monoHistory.RemoveAt(0); + + for (int l = 2; l <= mono_history_max_length / 2; l++) + { + for (int start = monoHistory.Count - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) + { + samePattern = false; + } + } + + if (samePattern) // Repetition found! + { + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; + } + } + } + + return penalty; } protected override double StrainValueOf(DifficultyHitObject current) { - double objectDifficulty = 0.0; - - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) { - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; - - if (currentHO.IsKat == prevIsKat) - { - currentMonoLength += 1; - } - - else - { - - objectDifficulty = 1.0; - - if (monoHistoryLength > 0 && (monoHistory[monoHistoryLength - 1] + currentMonoLength) % 2 == 0) - { - objectDifficulty *= sameParityPenalty(); - } - - monoHistory.Add(currentMonoLength); - monoHistoryLength += 1; - - if (monoHistoryLength > mono_history_max_length) - { - monoHistory.RemoveAt(0); - monoHistoryLength -= 1; - } - - for (int l = 2; l <= mono_history_max_length / 2; l++) - { - for (int start = monoHistoryLength - l - 1; start >= 0; start--) - { - bool samePattern = true; - - for (int i = 0; i < l; i++) - { - if (monoHistory[start + i] != monoHistory[monoHistoryLength - l + i]) - { - samePattern = false; - } - } - - if (samePattern) // Repitition found! - { - int notesSince = 0; - for (int i = start; i < monoHistoryLength; i++) notesSince += monoHistory[i]; - objectDifficulty *= repititionPenalty(notesSince); - break; - } - } - } - - currentMonoLength = 1; - prevIsKat = currentHO.IsKat; - - } - + prevNoteColour = NoteColour.None; + return 0.0; } - /* - string path = @"out.txt"; - using (StreamWriter sw = File.AppendText(path)) - { - if (((TaikoDifficultyHitObject)current).IsKat) sw.WriteLine("k " + Math.Min(1.25, returnVal) * returnMultiplier); - else sw.WriteLine("d " + Math.Min(1.25, returnVal) * returnMultiplier); - } - */ + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - return objectDifficulty; + double objectStrain = 0.0; + + NoteColour noteColour = hitObject.IsKat ? NoteColour.Ka : NoteColour.Don; + + if (noteColour == NoteColour.Don && prevNoteColour == NoteColour.Ka || + noteColour == NoteColour.Ka && prevNoteColour == NoteColour.Don) + { + objectStrain = 1.0; + + if (monoHistory.Count < 2) + objectStrain = 0.0; + else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + objectStrain *= sameParityPenalty(); + + objectStrain *= repetitionPenalties(); + currentMonoLength = 1; + } + else + { + currentMonoLength += 1; + } + + prevNoteColour = noteColour; + return objectStrain; } + private enum NoteColour + { + Don, + Ka, + None + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs new file mode 100644 index 0000000000..c3e6ee4d12 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Rhythm : Skill + { + protected override double SkillMultiplier => 10; + protected override double StrainDecayBase => 0; + private const double strain_decay = 0.96; + private double currentStrain; + + private readonly List rhythmHistory = new List(); + private const int rhythm_history_max_length = 8; + + private int notesSinceRhythmChange; + + private double repetitionPenalty(int notesSince) + { + return Math.Min(1.0, 0.032 * notesSince); + } + + // Finds repetitions and applies penalties + private double repetitionPenalties(TaikoDifficultyHitObject hitobject) + { + double penalty = 1; + + rhythmHistory.Add(hitobject); + + if (rhythmHistory.Count > rhythm_history_max_length) + rhythmHistory.RemoveAt(0); + + for (int l = 2; l <= rhythm_history_max_length / 2; l++) + { + for (int start = rhythmHistory.Count - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) + { + samePattern = false; + } + } + + if (samePattern) // Repetition found! + { + int notesSince = hitobject.N - rhythmHistory[start].N; + penalty *= repetitionPenalty(notesSince); + break; + } + } + } + + return penalty; + } + + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } + + // Penalty for notes so slow that alternating is not necessary. + private double speedPenalty(double noteLengthMs) + { + if (noteLengthMs < 80) return 1; + if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs); + + currentStrain = 0.0; + notesSinceRhythmChange = 0; + return 0.0; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + if (!(current.BaseObject is Hit)) + { + currentStrain = 0.0; + notesSinceRhythmChange = 0; + return 0.0; + } + + currentStrain *= strain_decay; + + TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; + notesSinceRhythmChange += 1; + + if (hitobject.Rhythm.IsRepeat) + { + return 0.0; + } + + double objectStrain = hitobject.Rhythm.Difficulty; + + objectStrain *= repetitionPenalties(hitobject); + objectStrain *= patternLengthPenalty(notesSinceRhythmChange); + objectStrain *= speedPenalty(hitobject.NoteLength); + + notesSinceRhythmChange = 0; + + currentStrain += objectStrain; + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs deleted file mode 100644 index dd90463113..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Rhythm : Skill - { - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0; - private const double strain_decay = 0.96; - private double currentStrain; - - private readonly List ratioObjectHistory = new List(); - private int ratioHistoryLength; - private const int ratio_history_max_length = 8; - - private int rhythmLength; - - // Penalty for repeated sequences of rhythm changes - private double repititionPenalty(double timeSinceRepititionMS) - { - double t = Math.Atan(timeSinceRepititionMS / 3000) / (Math.PI / 2); - return t; - } - - private double repititionPenalty(int notesSince) - { - double t = notesSince * 150; - t = Math.Atan(t / 3000) / (Math.PI / 2); - return t; - } - - // Penalty for short patterns - // Must be low to buff maps like wizodmiot - // Must not be too low for maps like inverse world - private double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - // Penalty for notes so slow that alting is not necessary. - private double speedPenalty(double noteLengthMS) - { - if (noteLengthMS < 80) return 1; - // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 210) return 0.6; - - currentStrain = 0.0; - return 0.0; - } - - // Penalty for the first rhythm change in a pattern - private const double first_burst_penalty = 0.1; - private bool prevIsSpeedup = true; - - protected override double StrainValueOf(DifficultyHitObject dho) - { - currentStrain *= strain_decay; - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)dho; - rhythmLength += 1; - - if (!currentHO.HasTimingChange) - { - return 0.0; - } - - double objectDifficulty = currentHO.Rhythm.Difficulty; - - // find repeated ratios - - ratioObjectHistory.Add(currentHO); - ratioHistoryLength += 1; - - if (ratioHistoryLength > ratio_history_max_length) - { - ratioObjectHistory.RemoveAt(0); - ratioHistoryLength -= 1; - } - - for (int l = 2; l <= ratio_history_max_length / 2; l++) - { - for (int start = ratioHistoryLength - l - 1; start >= 0; start--) - { - bool samePattern = true; - - for (int i = 0; i < l; i++) - { - if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) - { - samePattern = false; - } - } - - if (samePattern) // Repitition found! - { - int notesSince = currentHO.n - ratioObjectHistory[start].n; - objectDifficulty *= repititionPenalty(notesSince); - break; - } - } - } - - if (currentHO.Rhythm.IsSpeedup()) - { - objectDifficulty *= 1; - if (currentHO.Rhythm.IsLargeSpeedup()) objectDifficulty *= 1; - if (prevIsSpeedup) objectDifficulty *= 1; - - prevIsSpeedup = true; - } - else - { - prevIsSpeedup = false; - } - - objectDifficulty *= patternLengthPenalty(rhythmLength); - objectDifficulty *= speedPenalty(currentHO.NoteLength); - - rhythmLength = 0; - - currentStrain += objectDifficulty; - return currentStrain; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 1ecca886df..29c1c3c322 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -2,91 +2,78 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private int hand; - private int noteNumber = 0; + private readonly int hand; protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.4; - // i only add strain every second note so its kind of like using 0.16 - private readonly int maxHistoryLength = 2; - private List noteDurationHistory = new List(); - - private List lastHitObjects = new List(); + private const int max_history_length = 2; + private readonly List notePairDurationHistory = new List(); private double offhandObjectDuration = double.MaxValue; // Penalty for tl tap or roll - private double cheesePenalty(double last2NoteDuration) + private double cheesePenalty(double notePairDuration) { - if (last2NoteDuration > 125) return 1; - if (last2NoteDuration < 100) return 0.6; + if (notePairDuration > 125) return 1; + if (notePairDuration < 100) return 0.6; - return 0.6 + (last2NoteDuration - 100) * 0.016; + return 0.6 + (notePairDuration - 100) * 0.016; } - private double speedBonus(double last2NoteDuration) + private double speedBonus(double notePairDuration) { - // note that we are only looking at every 2nd note, so a 300bpm stream has a note duration of 100ms. - if (last2NoteDuration >= 200) return 0; - double bonus = 200 - last2NoteDuration; + if (notePairDuration >= 200) return 0; + + double bonus = 200 - notePairDuration; bonus *= bonus; return bonus / 100000; } protected override double StrainValueOf(DifficultyHitObject current) { - noteNumber += 1; - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; - - if (noteNumber % 2 == hand) + if (!(current.BaseObject is Hit)) { - lastHitObjects.Add(currentHO); - noteDurationHistory.Add(currentHO.NoteLength + offhandObjectDuration); + return 0.0; + } - if (noteNumber == 1) + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; + + if (hitObject.N % 2 == hand) + { + double objectStrain = 1; + + if (hitObject.N == 1) return 1; - if (noteDurationHistory.Count > maxHistoryLength) - noteDurationHistory.RemoveAt(0); + notePairDurationHistory.Add(hitObject.NoteLength + offhandObjectDuration); - double shortestRecentNote = min(noteDurationHistory); - double bonus = 0; - bonus += speedBonus(shortestRecentNote); + if (notePairDurationHistory.Count > max_history_length) + notePairDurationHistory.RemoveAt(0); - double objectStaminaStrain = 1 + bonus; - if (currentHO.StaminaCheese) objectStaminaStrain *= cheesePenalty(currentHO.NoteLength + offhandObjectDuration); + double shortestRecentNote = notePairDurationHistory.Min(); + objectStrain += speedBonus(shortestRecentNote); - return objectStaminaStrain; + if (hitObject.StaminaCheese) + objectStrain *= cheesePenalty(hitObject.NoteLength + offhandObjectDuration); + + return objectStrain; } - offhandObjectDuration = currentHO.NoteLength; + offhandObjectDuration = hitObject.NoteLength; return 0; } - private static double min(List l) - { - double minimum = double.MaxValue; - - foreach (double d in l) - { - if (d < minimum) - minimum = d; - } - - return minimum; - } - public Stamina(bool rightHand) { hand = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index dc2b68e0ca..789fd7c63b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,9 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythmSkillMultiplier = 0.15; - private const double colourSkillMultiplier = 0.01; - private const double staminaSkillMultiplier = 0.02; + private const double rhythm_skill_multiplier = 0.014; + private const double colour_skill_multiplier = 0.01; + private const double stamina_skill_multiplier = 0.02; + + private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = + { + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0, true), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3, false), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5, false), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3, false), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35, false), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6, false), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4, false), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5, false), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7, false) + }; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -32,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { if (colorDifficulty <= 0) return 0.79 - 0.25; + return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } @@ -46,25 +60,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double rescale(double sr) { - if (sr <= 1) return sr; - sr -= 1; - sr = 1.6 * Math.Pow(sr, 0.7); - sr += 1; - return sr; + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); } - private double combinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double locallyCombinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double difficulty = 0; double weight = 1; List peaks = new List(); for (int i = 0; i < colour.StrainPeaks.Count; i++) { - double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier; - double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier * staminaPenalty; + double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -82,21 +93,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; - double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier; - double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; + double colourRating = skills[0].DifficultyValue() * colour_skill_multiplier; + double rhythmRating = skills[1].DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * stamina_skill_multiplier; double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); staminaRating *= staminaPenalty; - double combinedRating = combinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); - - // Console.WriteLine("colour\t" + colourRating); - // Console.WriteLine("rhythm\t" + rhythmRating); - // Console.WriteLine("stamina\t" + staminaRating); + double combinedRating = locallyCombinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); - // Console.WriteLine("combinedRating\t" + combinedRating); - // Console.WriteLine("separatedRating\t" + separatedRating); double starRating = 1.4 * separatedRating + 0.5 * combinedRating; starRating = rescale(starRating); @@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty RhythmStrain = rhythmRating, ColourStrain = colourRating, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, + GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; @@ -120,18 +125,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); - var rhythm = new TaikoDifficultyHitObjectRhythm(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { // Check for negative durations if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); + { + taikoDifficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i, commonRhythms + ) + ); + } } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); - for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) - yield return taikoDifficultyHitObjects[i]; + foreach (var hitobject in taikoDifficultyHitObjects) + yield return hitobject; } protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] @@ -149,10 +159,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty new TaikoModEasy(), new TaikoModHardRock(), }; - - /* - protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) - => taikoCalculate(beatmap, mods, clockRate); - */ } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9585a6a369..e6dd9f5084 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -78,10 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available strainValue *= Math.Pow(0.985, countMiss); - // Combo scaling - if (Attributes.MaxCombo > 0) - strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0); - if (mods.Any(m => m is ModHidden)) strainValue *= 1.025; From c44ac9104f77d72d3917c47005a3c7f7b849c72e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jul 2020 14:19:43 +0900 Subject: [PATCH 07/39] Fix post-merge error --- .../Difficulty/Skills/Strain.cs | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs deleted file mode 100644 index 2c1885ae1a..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Strain : Skill - { - private const double rhythm_change_base_threshold = 0.2; - private const double rhythm_change_base = 2.0; - - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; - - private ColourSwitch lastColourSwitch = ColourSwitch.None; - - private int sameColourCount = 1; - - protected override double StrainValueOf(DifficultyHitObject current) - { - double addition = 1; - - // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000) - { - if (hasColourChange(current)) - addition += 0.75; - - if (hasRhythmChange(current)) - addition += 1; - } - else - { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; - } - - double additionFactor = 1; - - // Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50 - if (current.DeltaTime < 50) - additionFactor = 0.4 + 0.6 * current.DeltaTime / 50; - - return additionFactor * addition; - } - - private bool hasRhythmChange(DifficultyHitObject current) - { - // We don't want a division by zero if some random mapper decides to put two HitObjects at the same time. - if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0) - return false; - - double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime); - - if (timeElapsedRatio >= 8) - return false; - - double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0; - - return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold; - } - - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } - } -} From 4c00c11541c18a07d32e2332bfce515bce4d5c8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 20:53:14 +0900 Subject: [PATCH 08/39] Remove unnecessary change --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index e6dd9f5084..b9d95a6ba6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; // Longer maps are worth more - double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); + double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); strainValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available From c1a4f2e6afd823b9e36c73f76f16ea4445da0a88 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 20:53:50 +0900 Subject: [PATCH 09/39] Update expected SR in test --- .../TaikoDifficultyCalculatorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index e7b6d8615b..2d51e82bc4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.9811338051242915d, "diffcalc-test")] - [TestCase(2.9811338051242915d, "diffcalc-test-strong")] + [TestCase(2.2905937546434592d, "diffcalc-test")] + [TestCase(2.2905937546434592d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name); From d2a03f1146dc8257c1ffec299272edeff3f05d03 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 00:59:22 +0900 Subject: [PATCH 10/39] Refactor TaikoDifficultyHitObject --- .../Preprocessing/TaikoDifficultyHitObject.cs | 13 ++++++------- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 4 ++-- .../Difficulty/Skills/Stamina.cs | 10 +++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index cd45db2119..d0f621f4ad 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -17,21 +17,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public bool StaminaCheese = false; - public readonly double NoteLength; + public readonly int ObjectIndex; - public readonly int N; - - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int n, IEnumerable commonRhythms) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex, + IEnumerable commonRhythms) : base(hitObject, lastObject, clockRate) { var currentHit = hitObject as Hit; - NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = getClosestRhythm(NoteLength / prevLength, commonRhythms); + + Rhythm = getClosestRhythm(DeltaTime / prevLength, commonRhythms); IsKat = currentHit?.Type == HitType.Rim; - N = n; + ObjectIndex = objectIndex; } private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index c3e6ee4d12..31dc93a6b2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (samePattern) // Repetition found! { - int notesSince = hitobject.N - rhythmHistory[start].N; + int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; penalty *= repetitionPenalty(notesSince); break; } @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills objectStrain *= repetitionPenalties(hitobject); objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitobject.NoteLength); + objectStrain *= speedPenalty(hitobject.DeltaTime); notesSinceRhythmChange = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 29c1c3c322..c9a691a2aa 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - if (hitObject.N % 2 == hand) + if (hitObject.ObjectIndex % 2 == hand) { double objectStrain = 1; - if (hitObject.N == 1) + if (hitObject.ObjectIndex == 1) return 1; - notePairDurationHistory.Add(hitObject.NoteLength + offhandObjectDuration); + notePairDurationHistory.Add(hitObject.DeltaTime + offhandObjectDuration); if (notePairDurationHistory.Count > max_history_length) notePairDurationHistory.RemoveAt(0); @@ -65,12 +65,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills objectStrain += speedBonus(shortestRecentNote); if (hitObject.StaminaCheese) - objectStrain *= cheesePenalty(hitObject.NoteLength + offhandObjectDuration); + objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration); return objectStrain; } - offhandObjectDuration = hitObject.NoteLength; + offhandObjectDuration = hitObject.DeltaTime; return 0; } From 5010d2044a8b53ed8475dbfde17286485bd64872 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 01:35:56 +0900 Subject: [PATCH 11/39] Replace IsKat with HitType --- .../Preprocessing/StaminaCheeseDetector.cs | 15 ++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 4 +- .../Difficulty/Skills/Colour.cs | 87 +++++++++---------- 3 files changed, 49 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index b52dad5198..b53bc66f39 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { @@ -17,10 +18,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); - findTlTap(0, true); - findTlTap(1, true); - findTlTap(0, false); - findTlTap(1, false); + findTlTap(0, HitType.Rim); + findTlTap(1, HitType.Rim); + findTlTap(0, HitType.Centre); + findTlTap(1, HitType.Centre); } private void findRolls(int patternLength) @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing for (int j = 0; j < patternLength; j++) { - if (history[j].IsKat != history[j + patternLength].IsKat) + if (history[j].HitType != history[j + patternLength].HitType) { isRepeat = false; } @@ -63,13 +64,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } - private void findTlTap(int parity, bool kat) + private void findTlTap(int parity, HitType type) { int tlLength = -2; for (int i = parity; i < hitObjects.Count; i += 2) { - if (kat == hitObjects[i].IsKat) + if (hitObjects[i].HitType == type) { tlLength += 2; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index d0f621f4ad..817e974fe8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public class TaikoDifficultyHitObject : DifficultyHitObject { public readonly TaikoDifficultyHitObjectRhythm Rhythm; - public readonly bool IsKat; + public readonly HitType? HitType; public bool StaminaCheese = false; @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; Rhythm = getClosestRhythm(DeltaTime / prevLength, commonRhythms); - IsKat = currentHit?.Type == HitType.Rim; + HitType = currentHit?.Type; ObjectIndex = objectIndex; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 7c1623c54e..a348c25331 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -12,26 +12,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Colour : Skill { + private const int mono_history_max_length = 5; + protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - private NoteColour prevNoteColour = NoteColour.None; + private HitType? previousHitType; private int currentMonoLength = 1; private readonly List monoHistory = new List(); - private const int mono_history_max_length = 5; + + protected override double StrainValueOf(DifficultyHitObject current) + { + if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) + { + previousHitType = null; + return 0.0; + } + + var taikoCurrent = (TaikoDifficultyHitObject)current; + + double objectStrain = 0.0; + + if (taikoCurrent.HitType != null && previousHitType != null && taikoCurrent.HitType != previousHitType) + { + objectStrain = 1.0; + + if (monoHistory.Count < 2) + objectStrain = 0.0; + else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + objectStrain *= sameParityPenalty(); + + objectStrain *= repetitionPenalties(); + currentMonoLength = 1; + } + else + { + currentMonoLength += 1; + } + + previousHitType = taikoCurrent.HitType; + return objectStrain; + } private double sameParityPenalty() { return 0.0; } - private double repetitionPenalty(int notesSince) - { - double n = notesSince; - return Math.Min(1.0, 0.032 * n); - } - private double repetitionPenalties() { double penalty = 1.0; @@ -68,47 +96,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return penalty; } - protected override double StrainValueOf(DifficultyHitObject current) + private double repetitionPenalty(int notesSince) { - if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) - { - prevNoteColour = NoteColour.None; - return 0.0; - } - - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - - double objectStrain = 0.0; - - NoteColour noteColour = hitObject.IsKat ? NoteColour.Ka : NoteColour.Don; - - if (noteColour == NoteColour.Don && prevNoteColour == NoteColour.Ka || - noteColour == NoteColour.Ka && prevNoteColour == NoteColour.Don) - { - objectStrain = 1.0; - - if (monoHistory.Count < 2) - objectStrain = 0.0; - else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) - objectStrain *= sameParityPenalty(); - - objectStrain *= repetitionPenalties(); - currentMonoLength = 1; - } - else - { - currentMonoLength += 1; - } - - prevNoteColour = noteColour; - return objectStrain; - } - - private enum NoteColour - { - Don, - Ka, - None + double n = notesSince; + return Math.Min(1.0, 0.032 * n); } } } From 8ded5925ff0fbf2dcdbf4e00146898009ab556e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 13:47:35 +0900 Subject: [PATCH 12/39] Xmldoc colour strain --- .../Difficulty/Skills/Colour.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index a348c25331..db445c7d27 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -19,7 +19,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private HitType? previousHitType; + /// + /// Length of the current mono pattern. + /// private int currentMonoLength = 1; + + /// + /// List of the last most recent mono patterns, with the most recent at the end of the list. + /// private readonly List monoHistory = new List(); protected override double StrainValueOf(DifficultyHitObject current) @@ -36,12 +43,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (taikoCurrent.HitType != null && previousHitType != null && taikoCurrent.HitType != previousHitType) { + // The colour has changed. objectStrain = 1.0; if (monoHistory.Count < 2) + { + // There needs to be at least two streaks to determine a strain. objectStrain = 0.0; + } else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + { + // The last streak in the history is guaranteed to be a different type to the current streak. + // If the total number of notes in the two streaks is even, apply a penalty. objectStrain *= sameParityPenalty(); + } objectStrain *= repetitionPenalties(); currentMonoLength = 1; @@ -55,11 +70,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return objectStrain; } - private double sameParityPenalty() - { - return 0.0; - } + /// + /// The penalty to apply when the total number of notes in the two most recent colour streaks is even. + /// + private double sameParityPenalty() => 0.0; + /// + /// The penalty to apply due to the length of repetition in colour streaks. + /// private double repetitionPenalties() { double penalty = 1.0; @@ -96,10 +114,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return penalty; } - private double repetitionPenalty(int notesSince) - { - double n = notesSince; - return Math.Min(1.0, 0.032 * n); - } + private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } From cd2280b5bf8947f29d379da16cb8826eff637fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 15:18:52 +0200 Subject: [PATCH 13/39] Fix cheese indexing bug --- .../Difficulty/Preprocessing/StaminaCheeseDetector.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index b53bc66f39..29e631e515 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Objects; @@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { for (int j = repetitionStart; j < i; j++) { - hitObjects[i].StaminaCheese = true; + hitObjects[j].StaminaCheese = true; } } } @@ -81,9 +82,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength >= tl_min_repetitions) { - for (int j = i - tlLength; j < i; j++) + for (int j = Math.Max(0, i - tlLength); j < i; j++) { - hitObjects[i].StaminaCheese = true; + hitObjects[j].StaminaCheese = true; } } } From 9fb494d5d3067bb39081222cb3f39d5b2dc9ba74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 15:24:30 +0200 Subject: [PATCH 14/39] Eliminate unnecessary loop --- .../Difficulty/Skills/Colour.cs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index db445c7d27..e93893d894 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private double repetitionPenalties() { + const int l = 2; double penalty = 1.0; monoHistory.Add(currentMonoLength); @@ -87,27 +88,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (monoHistory.Count > mono_history_max_length) monoHistory.RemoveAt(0); - for (int l = 2; l <= mono_history_max_length / 2; l++) + for (int start = monoHistory.Count - l - 1; start >= 0; start--) { - for (int start = monoHistory.Count - l - 1; start >= 0; start--) + bool samePattern = true; + + for (int i = 0; i < l; i++) { - bool samePattern = true; - - for (int i = 0; i < l; i++) + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) { - if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) - { - samePattern = false; - } + samePattern = false; } + } - if (samePattern) // Repetition found! - { - int notesSince = 0; - for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; - penalty *= repetitionPenalty(notesSince); - break; - } + if (samePattern) // Repetition found! + { + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; } } From 6c759f31f175f784f447f1ef0d21e20460429ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:13:18 +0200 Subject: [PATCH 15/39] Add and use limited capacity queue --- .../Preprocessing/StaminaCheeseDetector.cs | 10 +- .../Difficulty/Skills/Colour.cs | 9 +- .../Difficulty/Skills/Rhythm.cs | 9 +- .../Difficulty/Skills/Stamina.cs | 9 +- .../NonVisual/LimitedCapacityQueueTest.cs | 98 +++++++++++++++ .../Difficulty/Utils/LimitedCapacityQueue.cs | 114 ++++++++++++++++++ 6 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 29e631e515..c6317ff195 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing @@ -27,16 +28,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing private void findRolls(int patternLength) { - List history = new List(); + var history = new LimitedCapacityQueue(2 * patternLength); int repetitionStart = 0; for (int i = 0; i < hitObjects.Count; i++) { - history.Add(hitObjects[i]); - if (history.Count < 2 * patternLength) continue; - - if (history.Count > 2 * patternLength) history.RemoveAt(0); + history.Enqueue(hitObjects[i]); + if (!history.Full) + continue; bool isRepeat = true; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index e93893d894..e9e0930a9a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// List of the last most recent mono patterns, with the most recent at the end of the list. /// - private readonly List monoHistory = new List(); + private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); protected override double StrainValueOf(DifficultyHitObject current) { @@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills const int l = 2; double penalty = 1.0; - monoHistory.Add(currentMonoLength); - - if (monoHistory.Count > mono_history_max_length) - monoHistory.RemoveAt(0); + monoHistory.Enqueue(currentMonoLength); for (int start = monoHistory.Count - l - 1; start >= 0; start--) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 31dc93a6b2..caf1acccf4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private const double strain_decay = 0.96; private double currentStrain; - private readonly List rhythmHistory = new List(); + private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); private const int rhythm_history_max_length = 8; private int notesSinceRhythmChange; @@ -32,10 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { double penalty = 1; - rhythmHistory.Add(hitobject); - - if (rhythmHistory.Count > rhythm_history_max_length) - rhythmHistory.RemoveAt(0); + rhythmHistory.Enqueue(hitobject); for (int l = 2; l <= rhythm_history_max_length / 2; l++) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index c9a691a2aa..430a553113 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainDecayBase => 0.4; private const int max_history_length = 2; - private readonly List notePairDurationHistory = new List(); + private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); private double offhandObjectDuration = double.MaxValue; @@ -56,10 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (hitObject.ObjectIndex == 1) return 1; - notePairDurationHistory.Add(hitObject.DeltaTime + offhandObjectDuration); - - if (notePairDurationHistory.Count > max_history_length) - notePairDurationHistory.RemoveAt(0); + notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration); double shortestRecentNote = notePairDurationHistory.Min(); objectStrain += speedBonus(shortestRecentNote); diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs new file mode 100644 index 0000000000..52463dd7eb --- /dev/null +++ b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Rulesets.Difficulty.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class LimitedCapacityQueueTest + { + private const int capacity = 3; + + private LimitedCapacityQueue queue; + + [SetUp] + public void SetUp() + { + queue = new LimitedCapacityQueue(capacity); + } + + [Test] + public void TestEmptyQueue() + { + Assert.AreEqual(0, queue.Count); + + Assert.Throws(() => _ = queue[0]); + + Assert.Throws(() => _ = queue.Dequeue()); + + int count = 0; + foreach (var _ in queue) + count++; + + Assert.AreEqual(0, count); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void TestBelowCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + Assert.AreEqual(count, queue.Count); + + for (int i = 0; i < count; ++i) + Assert.AreEqual(i, queue[i]); + + int j = 0; + foreach (var item in queue) + Assert.AreEqual(j++, item); + + for (int i = queue.Count; i < queue.Count + capacity; i++) + Assert.Throws(() => _ = queue[i]); + } + + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void TestEnqueueAtFullCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + Assert.AreEqual(capacity, queue.Count); + + for (int i = 0; i < queue.Count; ++i) + Assert.AreEqual(count - capacity + i, queue[i]); + + int j = count - capacity; + foreach (var item in queue) + Assert.AreEqual(j++, item); + + for (int i = queue.Count; i < queue.Count + capacity; i++) + Assert.Throws(() => _ = queue[i]); + } + + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void TestDequeueAtFullCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + for (int i = 0; i < capacity; ++i) + { + Assert.AreEqual(count - capacity + i, queue.Dequeue()); + Assert.AreEqual(2 - i, queue.Count); + } + + Assert.Throws(() => queue.Dequeue()); + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs new file mode 100644 index 0000000000..0f014e8a8c --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + /// + /// An indexed queue with limited capacity. + /// Respects first-in-first-out insertion order. + /// + public class LimitedCapacityQueue : IEnumerable + { + /// + /// The number of elements in the queue. + /// + public int Count { get; private set; } + + /// + /// Whether the queue is full (adding any new items will cause removing existing ones). + /// + public bool Full => Count == capacity; + + private readonly T[] array; + private readonly int capacity; + + // Markers tracking the queue's first and last element. + private int start, end; + + /// + /// Constructs a new + /// + /// The number of items the queue can hold. + public LimitedCapacityQueue(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity)); + + this.capacity = capacity; + array = new T[capacity]; + start = 0; + end = -1; + } + + /// + /// Removes an item from the front of the . + /// + /// The item removed from the front of the queue. + public T Dequeue() + { + if (Count == 0) + throw new InvalidOperationException("Queue is empty."); + + var result = array[start]; + start = (start + 1) % capacity; + Count--; + return result; + } + + /// + /// Adds an item to the back of the . + /// If the queue is holding elements at the point of addition, + /// the item at the front of the queue will be removed. + /// + /// The item to be added to the back of the queue. + public void Enqueue(T item) + { + end = (end + 1) % capacity; + if (Count == capacity) + start = (start + 1) % capacity; + else + Count++; + array[end] = item; + } + + /// + /// Retrieves the item at the given index in the queue. + /// + /// + /// The index of the item to retrieve. + /// The item with index 0 is at the front of the queue + /// (it was added the earliest). + /// + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return array[(start + index) % capacity]; + } + } + + /// + /// Enumerates the queue from its start to its end. + /// + public IEnumerator GetEnumerator() + { + if (Count == 0) + yield break; + + for (int i = 0; i < Count; i++) + yield return array[(start + i) % capacity]; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} From 292d38362c504196b7593042fc8dcba2104a20af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:18:36 +0200 Subject: [PATCH 16/39] De-nest cheese detection logic --- .../Preprocessing/StaminaCheeseDetector.cs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index c6317ff195..e1dad70d90 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -38,33 +38,34 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!history.Full) continue; - bool isRepeat = true; - - for (int j = 0; j < patternLength; j++) - { - if (history[j].HitType != history[j + patternLength].HitType) - { - isRepeat = false; - } - } - - if (!isRepeat) + if (!containsPatternRepeat(history, patternLength)) { repetitionStart = i - 2 * patternLength; + continue; } int repeatedLength = i - repetitionStart; + if (repeatedLength < roll_min_repetitions) + continue; - if (repeatedLength >= roll_min_repetitions) + for (int j = repetitionStart; j < i; j++) { - for (int j = repetitionStart; j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + hitObjects[j].StaminaCheese = true; } } } + private static bool containsPatternRepeat(LimitedCapacityQueue history, int patternLength) + { + for (int j = 0; j < patternLength; j++) + { + if (history[j].HitType != history[j + patternLength].HitType) + return false; + } + + return true; + } + private void findTlTap(int parity, HitType type) { int tlLength = -2; @@ -72,20 +73,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing for (int i = parity; i < hitObjects.Count; i += 2) { if (hitObjects[i].HitType == type) - { tlLength += 2; - } else - { tlLength = -2; - } - if (tlLength >= tl_min_repetitions) + if (tlLength < tl_min_repetitions) + continue; + + for (int j = Math.Max(0, i - tlLength); j < i; j++) { - for (int j = Math.Max(0, i - tlLength); j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + hitObjects[j].StaminaCheese = true; } } } From ff44437706bc5eb34582329db869c2031772954e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:29:51 +0200 Subject: [PATCH 17/39] Extract method for marking cheese --- .../Preprocessing/StaminaCheeseDetector.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index e1dad70d90..3f952d8317 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -48,10 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (repeatedLength < roll_min_repetitions) continue; - for (int j = repetitionStart; j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + markObjectsAsCheese(repetitionStart, i); } } @@ -80,11 +77,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength < tl_min_repetitions) continue; - for (int j = Math.Max(0, i - tlLength); j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + markObjectsAsCheese(Math.Max(0, i - tlLength), i); } } + + private void markObjectsAsCheese(int start, int end) + { + for (int i = start; i < end; ++i) + hitObjects[i].StaminaCheese = true; + } } } From f22050c9759648521ea2bd05f51b3932e718eb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:31:10 +0200 Subject: [PATCH 18/39] Remove unnecessary initialiser --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 817e974fe8..81b304af13 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly HitType? HitType; - public bool StaminaCheese = false; + public bool StaminaCheese; public readonly int ObjectIndex; From c6a640db55aa386b8acc05fb788c08cfee76aafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:34:44 +0200 Subject: [PATCH 19/39] Remove superfluous IsRepeat field --- .../TaikoDifficultyHitObjectRhythm.cs | 4 +--- .../Difficulty/Skills/Rhythm.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 18 +++++++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 0ad885d9bd..9c22eff22a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -7,13 +7,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public readonly double Difficulty; public readonly double Ratio; - public readonly bool IsRepeat; - public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty, bool isRepeat) + public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) { Ratio = numerator / (double)denominator; Difficulty = difficulty; - IsRepeat = isRepeat; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index caf1acccf4..483e94cd70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; notesSinceRhythmChange += 1; - if (hitobject.Rhythm.IsRepeat) + if (hitobject.Rhythm.Difficulty == 0.0) { return 0.0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 789fd7c63b..7a9f1765ae 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -26,15 +26,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0, true), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3, false), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5, false), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3, false), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35, false), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6, false), - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4, false), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5, false), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7, false) + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + 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(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) }; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) From 00ae456f0879342fb3bc55e8be717585b7ef5e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:39:03 +0200 Subject: [PATCH 20/39] Remove unnecessary null check --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index e9e0930a9a..2a72f884d1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double objectStrain = 0.0; - if (taikoCurrent.HitType != null && previousHitType != null && taikoCurrent.HitType != previousHitType) + if (previousHitType != null && taikoCurrent.HitType != previousHitType) { // The colour has changed. objectStrain = 1.0; From d7ff3d77eb538b598e9878fa3cd814daf15fc499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:44:41 +0200 Subject: [PATCH 21/39] Slightly optimise and de-branch repetition pattern recognition --- .../Difficulty/Skills/Colour.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 2a72f884d1..dd8b536afc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -87,28 +87,29 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills for (int start = monoHistory.Count - l - 1; start >= 0; start--) { - bool samePattern = true; + if (!isSamePattern(start, l)) + continue; - for (int i = 0; i < l; i++) - { - if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) - { - samePattern = false; - } - } - - if (samePattern) // Repetition found! - { - int notesSince = 0; - for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; - penalty *= repetitionPenalty(notesSince); - break; - } + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; } return penalty; } + private bool isSamePattern(int start, int l) + { + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) + return false; + } + + return true; + } + private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } From ce0e5cf9a168ce86b5ea176a4767d4929aa6f211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:47:36 +0200 Subject: [PATCH 22/39] Slightly optimise and de-branch rhythm pattern recognition --- .../Difficulty/Skills/Rhythm.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 483e94cd70..4c06deb5c0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -38,28 +38,29 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { for (int start = rhythmHistory.Count - l - 1; start >= 0; start--) { - bool samePattern = true; + if (!samePattern(start, l)) + continue; - for (int i = 0; i < l; i++) - { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) - { - samePattern = false; - } - } - - if (samePattern) // Repetition found! - { - int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; - penalty *= repetitionPenalty(notesSince); - break; - } + int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; + penalty *= repetitionPenalty(notesSince); + break; } } return penalty; } + private bool samePattern(int start, int l) + { + for (int i = 0; i < l; i++) + { + if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) + return false; + } + + return true; + } + private double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); From 80e4c157279d5a58b873d14fedf3ea3b26ad7bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:50:16 +0200 Subject: [PATCH 23/39] Use Math.Clamp --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 4c06deb5c0..f6ef6470ed 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); return Math.Min(shortPatternPenalty, longPatternPenalty); } From c827e215069bde7be1747524f6fc9a9e755d61d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:51:19 +0200 Subject: [PATCH 24/39] Extract helper method to reset rhythm strain --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index f6ef6470ed..6bb2eaf06a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -74,8 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (noteLengthMs < 80) return 1; if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs); - currentStrain = 0.0; - notesSinceRhythmChange = 0; + resetRhythmStrain(); return 0.0; } @@ -83,8 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { if (!(current.BaseObject is Hit)) { - currentStrain = 0.0; - notesSinceRhythmChange = 0; + resetRhythmStrain(); return 0.0; } @@ -109,5 +107,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills currentStrain += objectStrain; return currentStrain; } + + private void resetRhythmStrain() + { + currentStrain = 0.0; + notesSinceRhythmChange = 0; + } } } From 51d41515ef857386cb89467b8777a8429ab9c072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:54:20 +0200 Subject: [PATCH 25/39] Simplify expression with ternary --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 430a553113..13510290f7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -73,12 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public Stamina(bool rightHand) { - hand = 0; - - if (rightHand) - { - hand = 1; - } + hand = rightHand ? 1 : 0; } } } From cb5ea6aa9a54d26d454456837fc1b15dbb7f7819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:59:28 +0200 Subject: [PATCH 26/39] Generalise p-norm function --- .../Difficulty/TaikoDifficultyCalculator.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7a9f1765ae..aa21df0228 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -49,13 +49,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } - private double norm(double p, double v1, double v2, double v3) + private double norm(double p, params double[] values) { - return Math.Pow( - Math.Pow(v1, p) + - Math.Pow(v2, p) + - Math.Pow(v3, p) - , 1 / p); + return Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } private double rescale(double sr) From 27f97973ee188bf77c6a10248aa88ddad6057989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 20:14:00 +0200 Subject: [PATCH 27/39] Add more proper typing to skills --- .../Difficulty/TaikoDifficultyCalculator.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index aa21df0228..d3ff0b95ee 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -61,7 +61,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 10.43 * Math.Log(sr / 8 + 1); } - private double locallyCombinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double locallyCombinedDifficulty( + double staminaPenalty, Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft) { double difficulty = 0; double weight = 1; @@ -71,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; + double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -89,14 +90,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - double colourRating = skills[0].DifficultyValue() * colour_skill_multiplier; - double rhythmRating = skills[1].DifficultyValue() * rhythm_skill_multiplier; - double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * stamina_skill_multiplier; + var colour = (Colour)skills[0]; + var rhythm = (Rhythm)skills[1]; + var staminaRight = (Stamina)skills[2]; + var staminaLeft = (Stamina)skills[3]; + + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier; double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); staminaRating *= staminaPenalty; - double combinedRating = locallyCombinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); + double combinedRating = locallyCombinedDifficulty(staminaPenalty, colour, rhythm, staminaRight, staminaLeft); double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); double starRating = 1.4 * separatedRating + 0.5 * combinedRating; starRating = rescale(starRating); From ec99fcd7ab2d7ee681677cb4cb082727448f3afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:10:31 +0200 Subject: [PATCH 28/39] Avoid passing down rhythm list every time --- .../Preprocessing/TaikoDifficultyHitObject.cs | 23 ++++++++++++++----- .../Difficulty/TaikoDifficultyCalculator.cs | 15 +----------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 81b304af13..e52f616371 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; @@ -19,23 +18,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int ObjectIndex; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex, - IEnumerable commonRhythms) + 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, commonRhythms); + Rhythm = getClosestRhythm(DeltaTime / prevLength); HitType = currentHit?.Type; ObjectIndex = objectIndex; } - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) + private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = { - return commonRhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + 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(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) + }; + + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio) + { + return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index d3ff0b95ee..961a2dfcda 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -24,19 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private const double colour_skill_multiplier = 0.01; private const double stamina_skill_multiplier = 0.02; - private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - 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(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -135,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { taikoDifficultyHitObjects.Add( new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i, commonRhythms + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i ) ); } From cb3fef76161d8cddc1bf73f99953cb3175e33fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:15:08 +0200 Subject: [PATCH 29/39] Inline same parity penalty --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index dd8b536afc..0453882f45 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -54,8 +54,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) { // The last streak in the history is guaranteed to be a different type to the current streak. - // If the total number of notes in the two streaks is even, apply a penalty. - objectStrain *= sameParityPenalty(); + // If the total number of notes in the two streaks is even, nullify this object's strain. + objectStrain = 0.0; } objectStrain *= repetitionPenalties(); @@ -70,11 +70,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return objectStrain; } - /// - /// The penalty to apply when the total number of notes in the two most recent colour streaks is even. - /// - private double sameParityPenalty() => 0.0; - /// /// The penalty to apply due to the length of repetition in colour streaks. /// From bcf3cd56574338f9ac2005854e1803a704abc439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:24:51 +0200 Subject: [PATCH 30/39] Remove unnecessary yield iteration --- .../Difficulty/TaikoDifficultyCalculator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 961a2dfcda..7a99abdac6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -129,8 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); - foreach (var hitobject in taikoDifficultyHitObjects) - yield return hitobject; + return taikoDifficultyHitObjects; } protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] From 7e2bef3b9fce0fffb8feb2fa0a4293a1714343bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:34:08 +0200 Subject: [PATCH 31/39] Split conditional for readability --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7a99abdac6..cbbef6e957 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -118,7 +118,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 2; i < beatmap.HitObjects.Count; i++) { // Check for negative durations - if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) + var currentAfterLast = beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime; + var lastAfterSecondLast = beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime; + + if (currentAfterLast && lastAfterSecondLast) { taikoDifficultyHitObjects.Add( new TaikoDifficultyHitObject( From 8ace7df0fde9c2480a0f68ba06165e04fd18529d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:51:35 +0200 Subject: [PATCH 32/39] Reorder members for better readability --- .../Preprocessing/StaminaCheeseDetector.cs | 10 +- .../Preprocessing/TaikoDifficultyHitObject.cs | 7 +- .../Difficulty/Skills/Colour.cs | 14 +- .../Difficulty/Skills/Rhythm.cs | 68 ++++--- .../Difficulty/Skills/Stamina.cs | 38 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 181 +++++++++--------- 6 files changed, 158 insertions(+), 160 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 3f952d8317..ef5f4433bf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -13,11 +13,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing private const int roll_min_repetitions = 12; private const int tl_min_repetitions = 16; - private List hitObjects; + private readonly List hitObjects; - public void FindCheese(List difficultyHitObjects) + public StaminaCheeseDetector(List hitObjects) + { + this.hitObjects = hitObjects; + } + + public void FindCheese() { - hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); findTlTap(0, HitType.Rim); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e52f616371..5f7f8040c7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -13,11 +13,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly HitType? HitType; + public readonly int ObjectIndex; public bool StaminaCheese; - public readonly int ObjectIndex; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex) : base(hitObject, lastObject, clockRate) { @@ -45,8 +44,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing }; private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio) - { - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } + => common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 0453882f45..1adbf272b3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -12,11 +12,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Colour : Skill { - private const int mono_history_max_length = 5; - protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + private const int mono_history_max_length = 5; + + /// + /// List of the last most recent mono patterns, with the most recent at the end of the list. + /// + private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); + private HitType? previousHitType; /// @@ -24,11 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private int currentMonoLength = 1; - /// - /// List of the last most recent mono patterns, with the most recent at the end of the list. - /// - private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); - protected override double StrainValueOf(DifficultyHitObject current) { if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 6bb2eaf06a..f37a4c3f65 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -14,17 +14,43 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { protected override double SkillMultiplier => 10; protected override double StrainDecayBase => 0; - private const double strain_decay = 0.96; - private double currentStrain; - private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); + private const double strain_decay = 0.96; private const int rhythm_history_max_length = 8; + private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); + + private double currentStrain; private int notesSinceRhythmChange; - private double repetitionPenalty(int notesSince) + protected override double StrainValueOf(DifficultyHitObject current) { - return Math.Min(1.0, 0.032 * notesSince); + if (!(current.BaseObject is Hit)) + { + resetRhythmStrain(); + return 0.0; + } + + currentStrain *= strain_decay; + + TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; + notesSinceRhythmChange += 1; + + if (hitobject.Rhythm.Difficulty == 0.0) + { + return 0.0; + } + + double objectStrain = hitobject.Rhythm.Difficulty; + + objectStrain *= repetitionPenalties(hitobject); + objectStrain *= patternLengthPenalty(notesSinceRhythmChange); + objectStrain *= speedPenalty(hitobject.DeltaTime); + + notesSinceRhythmChange = 0; + + currentStrain += objectStrain; + return currentStrain; } // Finds repetitions and applies penalties @@ -61,6 +87,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return true; } + private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); + private double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); @@ -78,36 +106,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0.0; } - protected override double StrainValueOf(DifficultyHitObject current) - { - if (!(current.BaseObject is Hit)) - { - resetRhythmStrain(); - return 0.0; - } - - currentStrain *= strain_decay; - - TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; - notesSinceRhythmChange += 1; - - if (hitobject.Rhythm.Difficulty == 0.0) - { - return 0.0; - } - - double objectStrain = hitobject.Rhythm.Difficulty; - - objectStrain *= repetitionPenalties(hitobject); - objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitobject.DeltaTime); - - notesSinceRhythmChange = 0; - - currentStrain += objectStrain; - return currentStrain; - } - private void resetRhythmStrain() { currentStrain = 0.0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 13510290f7..3fd21b5e6d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -12,32 +12,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private readonly int hand; - protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; private const int max_history_length = 2; + + private readonly int hand; private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); private double offhandObjectDuration = double.MaxValue; - // Penalty for tl tap or roll - private double cheesePenalty(double notePairDuration) + public Stamina(bool rightHand) { - if (notePairDuration > 125) return 1; - if (notePairDuration < 100) return 0.6; - - return 0.6 + (notePairDuration - 100) * 0.016; - } - - private double speedBonus(double notePairDuration) - { - if (notePairDuration >= 200) return 0; - - double bonus = 200 - notePairDuration; - bonus *= bonus; - return bonus / 100000; + hand = rightHand ? 1 : 0; } protected override double StrainValueOf(DifficultyHitObject current) @@ -71,9 +58,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0; } - public Stamina(bool rightHand) + // Penalty for tl tap or roll + private double cheesePenalty(double notePairDuration) { - hand = rightHand ? 1 : 0; + if (notePairDuration > 125) return 1; + if (notePairDuration < 100) return 0.6; + + return 0.6 + (notePairDuration - 100) * 0.016; + } + + private double speedBonus(double notePairDuration) + { + if (notePairDuration >= 200) return 0; + + double bonus = 200 - notePairDuration; + bonus *= bonus; + return bonus / 100000; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index cbbef6e957..8e0cb2a094 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,87 +29,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { - if (colorDifficulty <= 0) return 0.79 - 0.25; + new Colour(), + new Rhythm(), + new Stamina(true), + new Stamina(false), + }; - return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; - } - - private double norm(double p, params double[] values) + protected override Mod[] DifficultyAdjustmentMods => new Mod[] { - 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); - } - - private double locallyCombinedDifficulty( - double staminaPenalty, Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft) - { - double difficulty = 0; - double weight = 1; - List peaks = new List(); - - for (int i = 0; i < colour.StrainPeaks.Count; i++) - { - double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; - peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); - } - - foreach (double strain in peaks.OrderByDescending(d => d)) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; - } - - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) - { - if (beatmap.HitObjects.Count == 0) - return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - - var colour = (Colour)skills[0]; - var rhythm = (Rhythm)skills[1]; - var staminaRight = (Stamina)skills[2]; - var staminaLeft = (Stamina)skills[3]; - - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; - double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; - double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier; - - double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); - staminaRating *= staminaPenalty; - - double combinedRating = locallyCombinedDifficulty(staminaPenalty, colour, rhythm, staminaRight, staminaLeft); - double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); - double starRating = 1.4 * separatedRating + 0.5 * combinedRating; - starRating = rescale(starRating); - - HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); - - return new TaikoDifficultyAttributes - { - StarRating = starRating, - Mods = mods, - StaminaStrain = staminaRating, - RhythmStrain = rhythmRating, - ColourStrain = colourRating, - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, - MaxCombo = beatmap.HitObjects.Count(h => h is Hit), - Skills = skills - }; - } + new TaikoModDoubleTime(), + new TaikoModHalfTime(), + new TaikoModEasy(), + new TaikoModHardRock(), + }; protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { @@ -131,24 +65,89 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } } - new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); + new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese(); return taikoDifficultyHitObjects; } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { - new Colour(), - new Rhythm(), - new Stamina(true), - new Stamina(false), - }; + if (beatmap.HitObjects.Count == 0) + return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - protected override Mod[] DifficultyAdjustmentMods => new Mod[] + var colour = (Colour)skills[0]; + var rhythm = (Rhythm)skills[1]; + var staminaRight = (Stamina)skills[2]; + var staminaLeft = (Stamina)skills[3]; + + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier; + + double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); + staminaRating *= staminaPenalty; + + double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty); + double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); + double starRating = 1.4 * separatedRating + 0.5 * combinedRating; + starRating = rescale(starRating); + + HitWindows hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + return new TaikoDifficultyAttributes + { + StarRating = starRating, + Mods = mods, + StaminaStrain = staminaRating, + RhythmStrain = rhythmRating, + ColourStrain = colourRating, + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future + GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, + MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + Skills = skills + }; + } + + private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { - new TaikoModDoubleTime(), - new TaikoModHalfTime(), - new TaikoModEasy(), - new TaikoModHardRock(), - }; + if (colorDifficulty <= 0) return 0.79 - 0.25; + + 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); + } + + private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty) + { + double difficulty = 0; + double weight = 1; + List peaks = new List(); + + for (int i = 0; i < colour.StrainPeaks.Count; i++) + { + double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; + peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); + } + + foreach (double strain in peaks.OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } } } From a0807747995823ed520f7beba6a39ddbb4580f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 19:34:16 +0200 Subject: [PATCH 33/39] Add xmldoc to taiko difficulty calculation code --- .../Preprocessing/StaminaCheeseDetector.cs | 41 +++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 57 ++++++++-- .../TaikoDifficultyHitObjectRhythm.cs | 17 +++ .../Difficulty/Skills/Colour.cs | 34 ++++-- .../Difficulty/Skills/Rhythm.cs | 100 +++++++++++++----- .../Difficulty/Skills/Stamina.cs | 36 ++++++- .../Difficulty/TaikoDifficultyCalculator.cs | 47 +++++--- 7 files changed, 281 insertions(+), 51 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index ef5f4433bf..5187d101ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -8,11 +8,32 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { + /// + /// Detects special hit object patterns which are easier to hit using special techniques + /// than normally assumed in the fully-alternating play style. + /// + /// + /// This component detects two basic types of patterns, leveraged by the following techniques: + /// + /// Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand. + /// TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between. + /// + /// public class StaminaCheeseDetector { + /// + /// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll. + /// private const int roll_min_repetitions = 12; + + /// + /// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap. + /// private const int tl_min_repetitions = 16; + /// + /// The list of all s in the map. + /// private readonly List hitObjects; public StaminaCheeseDetector(List hitObjects) @@ -20,16 +41,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing this.hitObjects = hitObjects; } + /// + /// Finds and marks all objects in that special difficulty-reducing techiques apply to + /// with the flag. + /// public void FindCheese() { findRolls(3); findRolls(4); + findTlTap(0, HitType.Rim); findTlTap(1, HitType.Rim); findTlTap(0, HitType.Centre); findTlTap(1, HitType.Centre); } + /// + /// Finds and marks all sequences hittable using a roll. + /// + /// The length of a single repeating pattern to consider (triplets/quadruplets). private void findRolls(int patternLength) { var history = new LimitedCapacityQueue(2 * patternLength); @@ -56,6 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } + /// + /// Determines whether the objects stored in contain a repetition of a pattern of length . + /// private static bool containsPatternRepeat(LimitedCapacityQueue history, int patternLength) { for (int j = 0; j < patternLength; j++) @@ -67,6 +100,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing return true; } + /// + /// Finds and marks all sequences hittable using a TL tap. + /// + /// Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked. + /// The type of hit to check for TL taps. private void findTlTap(int parity, HitType type) { int tlLength = -2; @@ -85,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } + /// + /// Marks all objects from index up until (exclusive) as . + /// private void markObjectsAsCheese(int start, int end) { for (int i = start; i < end; ++i) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 5f7f8040c7..ae33c184d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -9,27 +9,61 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { + /// + /// Represents a single hit object in taiko difficulty calculation. + /// public class TaikoDifficultyHitObject : DifficultyHitObject { + /// + /// The rhythm required to hit this hit object. + /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + + /// + /// The hit type of this hit object. + /// public readonly HitType? HitType; + + /// + /// The index of the object in the beatmap. + /// public readonly int ObjectIndex; + /// + /// Whether the object should carry a penalty due to being hittable using special techniques + /// making it easier to do so. + /// public bool StaminaCheese; + /// + /// Creates a new difficulty hit object. + /// + /// The gameplay associated with this difficulty object. + /// The gameplay preceding . + /// The gameplay preceding . + /// The rate of the gameplay clock. Modified by speed-changing mods. + /// The index of the object in the beatmap. 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; } + /// + /// List of most common rhythm changes in taiko maps. + /// + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// 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(); + /// + /// Returns the closest rhythm change from required to hit this object. + /// + /// The gameplay preceding this one. + /// The gameplay preceding . + /// The rate of the gameplay clock. + 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(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 9c22eff22a..b6dc69a380 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -3,11 +3,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { + /// + /// Represents a rhythm change in a taiko map. + /// public class TaikoDifficultyHitObjectRhythm { + /// + /// The difficulty multiplier associated with this rhythm change. + /// public readonly double Difficulty; + + /// + /// The ratio of current to previous for the rhythm change. + /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. + /// public readonly double Ratio; + /// + /// Creates an object representing a rhythm change. + /// + /// The numerator for . + /// The denominator for + /// The difficulty multiplier associated with this rhythm change. public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) { Ratio = numerator / (double)denominator; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 1adbf272b3..9fad83c6a1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -10,18 +10,28 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + /// + /// Calculates the colour coefficient of taiko difficulty. + /// public class Colour : Skill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + /// + /// Maximum number of entries to keep in . + /// private const int mono_history_max_length = 5; /// - /// List of the last most recent mono patterns, with the most recent at the end of the list. + /// Queue with the lengths of the last most recent mono (single-colour) patterns, + /// with the most recent value at the end of the queue. /// private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); + /// + /// The of the last object hit before the one being considered. + /// private HitType? previousHitType; /// @@ -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 /// 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) + /// + /// Determines whether the last patterns have repeated in the history + /// of single-colour note sequences, starting from . + /// + 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; } + /// + /// Calculates the strain penalty for a colour pattern repetition. + /// + /// The number of notes since the last repetition of the pattern. private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index f37a4c3f65..5569b27ad5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -10,64 +10,97 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + /// + /// Calculates the rhythm coefficient of taiko difficulty. + /// public class Rhythm : Skill { protected override double SkillMultiplier => 10; protected override double StrainDecayBase => 0; + /// + /// The note-based decay for rhythm strain. + /// + /// + /// is not used here, as it's time- and not note-based. + /// private const double strain_decay = 0.96; + + /// + /// Maximum number of entries in . + /// private const int rhythm_history_max_length = 8; + /// + /// Contains the last changes in note sequence rhythms. + /// private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); + /// + /// Contains the rolling rhythm strain. + /// Used to apply per-note decay. + /// private double currentStrain; + + /// + /// Number of notes since the last rhythm change has taken place. + /// 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) + /// + /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes. + /// + /// + /// Repetitions of more recent patterns are associated with a higher penalty. + /// + /// The current hit object being considered. + 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) + /// + /// Determines whether the rhythm change pattern starting at is a repeat of any of the + /// . + /// + 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); + /// + /// Calculates a single rhythm repetition penalty. + /// + /// Number of notes since the last repetition of a rhythm change. + private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); - private double patternLengthPenalty(int patternLength) + /// + /// Calculates a penalty based on the number of notes since the last rhythm change. + /// Both rare and frequent rhythm changes are penalised. + /// + /// Number of notes since the last rhythm change. + 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) + /// + /// Calculates a penalty for objects that do not require alternating hands. + /// + /// Time (in milliseconds) since the last hit object. + 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() + /// + /// Resets the rolling strain value and counter. + /// + private void resetRhythmAndStrain() { currentStrain = 0.0; notesSinceRhythmChange = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 3fd21b5e6d..0b61eb9930 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -10,18 +10,45 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + /// + /// Calculates the stamina coefficient of taiko difficulty. + /// + /// + /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit). + /// public class Stamina : Skill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + /// + /// Maximum number of entries to keep in . + /// private const int max_history_length = 2; + /// + /// The index of the hand this instance is associated with. + /// + /// + /// 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. + /// private readonly int hand; + + /// + /// Stores the last durations between notes hit with the hand indicated by . + /// private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); + /// + /// Stores the of the last object that was hit by the other hand. + /// private double offhandObjectDuration = double.MaxValue; + /// + /// Creates a skill. + /// + /// Whether this instance is performing calculations for the right hand. 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 + /// + /// Applies a penalty for hit objects marked with . + /// + /// The duration between the current and previous note hit using the hand indicated by . 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; } + /// + /// Applies a speed bonus dependent on the time since the last hit performed using this hand. + /// + /// The duration between the current and previous note hit using the hand indicated by . private double speedBonus(double notePairDuration) { if (notePairDuration >= 200) return 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 8e0cb2a094..ef43fc6d1e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -108,6 +108,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty }; } + /// + /// Calculates the penalty for the stamina skill for maps with low colour difficulty. + /// + /// + /// 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. + /// 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); - } + /// + /// Returns the p-norm of an n-dimensional vector. + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// + /// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map. + /// + /// + /// 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). + /// private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty) { - double difficulty = 0; - double weight = 1; List peaks = new List(); 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; } + + /// + /// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars. + /// + /// The raw star rating value before re-scaling. + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } } } From 5afe9b73d2736c6563826916c1056fcdf285bf17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 21:27:08 +0200 Subject: [PATCH 34/39] Fix invalid cref --- .../Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index b6dc69a380..ea6a224094 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -14,7 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly double Difficulty; /// - /// The ratio of current to previous for the rhythm change. + /// The ratio of current + /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// public readonly double Ratio; From 7c9fae55ad8b5aa2706f29800f91aacca11eedca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 22:50:58 +0200 Subject: [PATCH 35/39] Hopefully fix off-by-one errors --- .../Preprocessing/StaminaCheeseDetector.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 5187d101ac..d07bff4369 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Objects; @@ -64,7 +63,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { var history = new LimitedCapacityQueue(2 * patternLength); - int repetitionStart = 0; + // for convenience, we're tracking the index of the item *before* our suspected repeat's start, + // as that index can be simply subtracted from the current index to get the number of elements in between + // without off-by-one errors + int indexBeforeLastRepeat = -1; for (int i = 0; i < hitObjects.Count; i++) { @@ -74,15 +76,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!containsPatternRepeat(history, patternLength)) { - repetitionStart = i - 2 * patternLength; + // we're setting this up for the next iteration, hence the +1. + // right here this index will point at the queue's front (oldest item), + // but that item is about to be popped next loop with an enqueue. + indexBeforeLastRepeat = i - history.Count + 1; continue; } - int repeatedLength = i - repetitionStart; + int repeatedLength = i - indexBeforeLastRepeat; if (repeatedLength < roll_min_repetitions) continue; - markObjectsAsCheese(repetitionStart, i); + markObjectsAsCheese(i, repeatedLength); } } @@ -119,17 +124,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength < tl_min_repetitions) continue; - markObjectsAsCheese(Math.Max(0, i - tlLength), i); + markObjectsAsCheese(i, tlLength); } } /// - /// Marks all objects from index up until (exclusive) as . + /// Marks elements counting backwards from as . /// - private void markObjectsAsCheese(int start, int end) + private void markObjectsAsCheese(int end, int count) { - for (int i = start; i < end; ++i) - hitObjects[i].StaminaCheese = true; + for (int i = 0; i < count; ++i) + hitObjects[end - i].StaminaCheese = true; } } } From 74eea8900bc2015bdb5f33a7e9f8e4f504bc8ce4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Sep 2020 20:00:57 +0900 Subject: [PATCH 36/39] Remove unnecessary check for negative durations --- .../Difficulty/TaikoDifficultyCalculator.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ef43fc6d1e..e5485db4df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -51,18 +51,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 2; i < beatmap.HitObjects.Count; i++) { - // Check for negative durations - var currentAfterLast = beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime; - var lastAfterSecondLast = beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime; - - if (currentAfterLast && lastAfterSecondLast) - { - taikoDifficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i - ) - ); - } + taikoDifficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i + ) + ); } new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese(); From 6e5c5ab9015e9b98ab52e344d38e5f97ffe57d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Sep 2020 18:22:49 +0200 Subject: [PATCH 37/39] Fix invalid initial value of currentMonoLength --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 9fad83c6a1..ecd74f54ed 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Length of the current mono pattern. /// - private int currentMonoLength = 1; + private int currentMonoLength; protected override double StrainValueOf(DifficultyHitObject current) { From a350802158d7413c9bdb37319b47d92efb17a1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Sep 2020 19:21:16 +0200 Subject: [PATCH 38/39] Fix wrong mono streak length handling in corner case --- .../Difficulty/Skills/Colour.cs | 7 ++++++- .../NonVisual/LimitedCapacityQueueTest.cs | 21 +++++++++++++++++++ .../Difficulty/Utils/LimitedCapacityQueue.cs | 9 ++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index ecd74f54ed..32421ee00a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -45,7 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // 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; + monoHistory.Clear(); + + var currentHit = current.BaseObject as Hit; + currentMonoLength = currentHit != null ? 1 : 0; + previousHitType = currentHit?.Type; + return 0.0; } diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs index 52463dd7eb..a04415bc7f 100644 --- a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs +++ b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs @@ -94,5 +94,26 @@ namespace osu.Game.Tests.NonVisual Assert.Throws(() => queue.Dequeue()); } + + [Test] + public void TestClearQueue() + { + queue.Enqueue(3); + queue.Enqueue(5); + Assert.AreEqual(2, queue.Count); + + queue.Clear(); + Assert.AreEqual(0, queue.Count); + Assert.Throws(() => _ = queue[0]); + + queue.Enqueue(7); + Assert.AreEqual(1, queue.Count); + Assert.AreEqual(7, queue[0]); + Assert.Throws(() => _ = queue[1]); + + queue.Enqueue(9); + Assert.AreEqual(2, queue.Count); + Assert.AreEqual(9, queue[1]); + } } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs index 0f014e8a8c..bc0eb8af88 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs @@ -40,8 +40,17 @@ namespace osu.Game.Rulesets.Difficulty.Utils this.capacity = capacity; array = new T[capacity]; + Clear(); + } + + /// + /// Removes all elements from the . + /// + public void Clear() + { start = 0; end = -1; + Count = 0; } /// From 64b1a009efb5f80f5c78faae44682b5895c2fd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Sep 2020 20:56:55 +0200 Subject: [PATCH 39/39] Adjust diffcalc test case to pass --- .../TaikoDifficultyCalculatorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 2d51e82bc4..71b3c23b50 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.2905937546434592d, "diffcalc-test")] - [TestCase(2.2905937546434592d, "diffcalc-test-strong")] + [TestCase(2.2867022617692685d, "diffcalc-test")] + [TestCase(2.2867022617692685d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name);