Skip to content

Commit

Permalink
Merge pull request #19571 from vunyunt/colour-encoding-2
Browse files Browse the repository at this point in the history
Rewrite of the colour skill & refactoring of difficulty calculation within osu!taiko
  • Loading branch information
smoogipoo authored Aug 25, 2022
2 parents 804a752 + fb9bb2d commit 136dcee
Show file tree
Hide file tree
Showing 16 changed files with 717 additions and 329 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ public class TaikoDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";

[TestCase(1.9971301024093662d, 200, "diffcalc-test")]
[TestCase(1.9971301024093662d, 200, "diffcalc-test-strong")]
[TestCase(3.1098944660126882d, 200, "diffcalc-test")]
[TestCase(3.1098944660126882d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);

[TestCase(3.1645810961313674d, 200, "diffcalc-test")]
[TestCase(3.1645810961313674d, 200, "diffcalc-test-strong")]
[TestCase(4.0974106752474251d, 200, "diffcalc-test")]
[TestCase(4.0974106752474251d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());

Expand Down
67 changes: 67 additions & 0 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/ColourEvaluator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;

namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class ColourEvaluator
{
/// <summary>
/// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
/// </summary>
/// <param name="val">The input value.</param>
/// <param name="center">The center of the sigmoid, where the largest gradient occurs and value is equal to middle.</param>
/// <param name="width">The radius of the sigmoid, outside of which values are near the minimum/maximum.</param>
/// <param name="middle">The middle of the sigmoid output.</param>
/// <param name="height">The height of the sigmoid output. This will be equal to max value - min value.</param>
private static double sigmoid(double val, double center, double width, double middle, double height)
{
double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
return sigmoid * (height / 2) + middle;
}

/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
/// </summary>
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
{
return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
}

/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="AlternatingMonoPattern"/>.
/// </summary>
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
{
return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
}

/// <summary>
/// Evaluate the difficulty of the first note of a <see cref="RepeatingHitPatterns"/>.
/// </summary>
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
{
return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1));
}

public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
{
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
double difficulty = 0.0d;

if (colour.MonoStreak != null) // Difficulty for MonoStreak
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
if (colour.AlternatingMonoPattern != null) // Difficulty for AlternatingMonoPattern
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
if (colour.RepeatingHitPattern != null) // Difficulty for RepeatingHitPattern
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);

return difficulty;
}
}
}
53 changes: 53 additions & 0 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/StaminaEvaluator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;

namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public class StaminaEvaluator
{
/// <summary>
/// Applies a speed bonus dependent on the time since the last hit performed using this key.
/// </summary>
/// <param name="interval">The interval between the current and previous note hit using the same key.</param>
private static double speedBonus(double interval)
{
// Cap to 600bpm 1/4, 25ms note interval, 50ms key interval
// Interval will be capped at a very small value to avoid infinite/negative speed bonuses.
// TODO - This is a temporary measure as we need to implement methods of detecting playstyle-abuse of SpeedBonus.
interval = Math.Max(interval, 50);

return 30 / interval;
}

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

// Find the previous hit object hit by the current key, which is two notes of the same colour prior.
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(1);

if (keyPrevious == null)
{
// There is no previous hit object hit by the current key
return 0.0;
}

double objectStrain = 0.5; // Add a base strain to all objects
objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
return objectStrain;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;

namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
{
/// <summary>
/// Encodes a list of <see cref="MonoStreak"/>s.
/// <see cref="MonoStreak"/>s with the same <see cref="MonoStreak.RunLength"/> are grouped together.
/// </summary>
public class AlternatingMonoPattern
{
/// <summary>
/// <see cref="MonoStreak"/>s that are grouped together within this <see cref="AlternatingMonoPattern"/>.
/// </summary>
public readonly List<MonoStreak> MonoStreaks = new List<MonoStreak>();

/// <summary>
/// The parent <see cref="RepeatingHitPatterns"/> that contains this <see cref="AlternatingMonoPattern"/>
/// </summary>
public RepeatingHitPatterns Parent = null!;

/// <summary>
/// Index of this <see cref="AlternatingMonoPattern"/> within it's parent <see cref="RepeatingHitPatterns"/>
/// </summary>
public int Index;

/// <summary>
/// The first <see cref="TaikoDifficultyHitObject"/> in this <see cref="AlternatingMonoPattern"/>.
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => MonoStreaks[0].FirstHitObject;

/// <summary>
/// Determine if this <see cref="AlternatingMonoPattern"/> is a repetition of another <see cref="AlternatingMonoPattern"/>. This
/// is a strict comparison and is true if and only if the colour sequence is exactly the same.
/// </summary>
public bool IsRepetitionOf(AlternatingMonoPattern other)
{
return HasIdenticalMonoLength(other) &&
other.MonoStreaks.Count == MonoStreaks.Count &&
other.MonoStreaks[0].HitType == MonoStreaks[0].HitType;
}

/// <summary>
/// Determine if this <see cref="AlternatingMonoPattern"/> has the same mono length of another <see cref="AlternatingMonoPattern"/>.
/// </summary>
public bool HasIdenticalMonoLength(AlternatingMonoPattern other)
{
return other.MonoStreaks[0].RunLength == MonoStreaks[0].RunLength;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
using System.Collections.Generic;

namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
{
/// <summary>
/// Encode colour information for a sequence of <see cref="TaikoDifficultyHitObject"/>s. Consecutive <see cref="TaikoDifficultyHitObject"/>s
/// of the same <see cref="Objects.HitType"/> are encoded within the same <see cref="MonoStreak"/>.
/// </summary>
public class MonoStreak
{
/// <summary>
/// List of <see cref="DifficultyHitObject"/>s that are encoded within this <see cref="MonoStreak"/>.
/// </summary>
public List<TaikoDifficultyHitObject> HitObjects { get; private set; } = new List<TaikoDifficultyHitObject>();

/// <summary>
/// The parent <see cref="AlternatingMonoPattern"/> that contains this <see cref="MonoStreak"/>
/// </summary>
public AlternatingMonoPattern Parent = null!;

/// <summary>
/// Index of this <see cref="MonoStreak"/> within it's parent <see cref="AlternatingMonoPattern"/>
/// </summary>
public int Index;

/// <summary>
/// The first <see cref="TaikoDifficultyHitObject"/> in this <see cref="MonoStreak"/>.
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];

/// <summary>
/// The hit type of all objects encoded within this <see cref="MonoStreak"/>
/// </summary>
public HitType? HitType => (HitObjects[0].BaseObject as Hit)?.Type;

/// <summary>
/// How long the mono pattern encoded within is
/// </summary>
public int RunLength => HitObjects.Count;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;

namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data
{
/// <summary>
/// Encodes a list of <see cref="AlternatingMonoPattern"/>s, grouped together by back and forth repetition of the same
/// <see cref="AlternatingMonoPattern"/>. Also stores the repetition interval between this and the previous <see cref="RepeatingHitPatterns"/>.
/// </summary>
public class RepeatingHitPatterns
{
/// <summary>
/// Maximum amount of <see cref="RepeatingHitPatterns"/>s to look back to find a repetition.
/// </summary>
private const int max_repetition_interval = 16;

/// <summary>
/// The <see cref="AlternatingMonoPattern"/>s that are grouped together within this <see cref="RepeatingHitPatterns"/>.
/// </summary>
public readonly List<AlternatingMonoPattern> AlternatingMonoPatterns = new List<AlternatingMonoPattern>();

/// <summary>
/// The parent <see cref="TaikoDifficultyHitObject"/> in this <see cref="RepeatingHitPatterns"/>
/// </summary>
public TaikoDifficultyHitObject FirstHitObject => AlternatingMonoPatterns[0].FirstHitObject;

/// <summary>
/// The previous <see cref="RepeatingHitPatterns"/>. This is used to determine the repetition interval.
/// </summary>
public readonly RepeatingHitPatterns? Previous;

/// <summary>
/// How many <see cref="RepeatingHitPatterns"/> between the current and previous identical <see cref="RepeatingHitPatterns"/>.
/// If no repetition is found this will have a value of <see cref="max_repetition_interval"/> + 1.
/// </summary>
public int RepetitionInterval { get; private set; } = max_repetition_interval + 1;

public RepeatingHitPatterns(RepeatingHitPatterns? previous)
{
Previous = previous;
}

/// <summary>
/// Returns true if other is considered a repetition of this pattern. This is true if other's first two payloads
/// have identical mono lengths.
/// </summary>
private bool isRepetitionOf(RepeatingHitPatterns other)
{
if (AlternatingMonoPatterns.Count != other.AlternatingMonoPatterns.Count) return false;

for (int i = 0; i < Math.Min(AlternatingMonoPatterns.Count, 2); i++)
{
if (!AlternatingMonoPatterns[i].HasIdenticalMonoLength(other.AlternatingMonoPatterns[i])) return false;
}

return true;
}

/// <summary>
/// Finds the closest previous <see cref="RepeatingHitPatterns"/> that has the identical <see cref="AlternatingMonoPatterns"/>.
/// Interval is defined as the amount of <see cref="RepeatingHitPatterns"/> chunks between the current and repeated patterns.
/// </summary>
public void FindRepetitionInterval()
{
if (Previous == null)
{
RepetitionInterval = max_repetition_interval + 1;
return;
}

RepeatingHitPatterns? other = Previous;
int interval = 1;

while (interval < max_repetition_interval)
{
if (isRepetitionOf(other))
{
RepetitionInterval = Math.Min(interval, max_repetition_interval);
return;
}

other = other.Previous;
if (other == null) break;

++interval;
}

RepetitionInterval = max_repetition_interval + 1;
}
}
}
Loading

0 comments on commit 136dcee

Please sign in to comment.