Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
vunyunt committed Nov 24, 2024
1 parent 2c0140f commit eabd810
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 1 deletion.
29 changes: 29 additions & 0 deletions osu.Game.Rulesets.Taiko/Difficulty/Evaluators/PatternEvaluator.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,29 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

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

namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public static class PatternEvaluator
{
public static double EvaluateDifficultyOf(
TaikoDifficultyHitObject hitObject,
TaikoPatternFields fields,
double errorStandardDeviation,
double hitWindowStandardDeviation)
{
if (hitObject.BaseObject is not Hit) return 0;

double errorAmplitude = fields.RhythmField.GetAmplitude(hitObject.StartTime, errorStandardDeviation);
double hitAmplitude = fields.RhythmField.GetAmplitude(hitObject.StartTime, hitWindowStandardDeviation);

// System.Console.WriteLine("errorAmplitude: " errorAmplitude ", hitAmplitude: " hitAmplitude);

hitAmplitude = Math.Max(hitAmplitude, 0.1);
return errorAmplitude / hitAmplitude;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 1,25 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

public class TaikoPatternFields

Check failure on line 4 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/Data/TaikoPatternFields.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Namespace does not correspond to file location, must be: 'osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Pattern.Data' in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\Data\TaikoPatternFields.cs on line 4
{
/// <summary>
/// Stores rhythm alignment amplitudes between all notes
/// </summary>
public TaikoTimeField RhythmField = new TaikoTimeField();

/// <summary>
/// Stores rhythm alignment amplitudes between centre (don) notes
/// </summary>
public TaikoTimeField CentreField = new TaikoTimeField();

/// <summary>
/// Stores rhythm alignment amplitudes between rim (katsu) notes
/// </summary>
public TaikoTimeField RimField = new TaikoTimeField();

/// <summary>
/// Stores rhythm alignment amplitudes between colour changes
/// </summary>
public TaikoTimeField ColourChangeField = new TaikoTimeField();
}
Original file line number Diff line number Diff line change
@@ -0,0 1,93 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

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

/// <summary>
/// Stores amplitude points data by time. Getting amplitude at a specific time is defined as the sum of amplitudes
/// within a range scaled by a normal distribution.
/// </summary>
public class TaikoTimeField

Check failure on line 12 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/Data/TaikoTimeField.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Namespace does not correspond to file location, must be: 'osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Pattern.Data' in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\Data\TaikoTimeField.cs on line 12
{
private readonly SortedList<double, double> amplitudesByTime = new SortedList<double, double>();

public void AddNode(double time, double amplitude)
{
double resultAmplitude = amplitudesByTime.GetValueOrDefault(time, 0) amplitude;
amplitudesByTime[time] = resultAmplitude;
}

private double bellCurve(double x, double standardDeviation)
{
double scaledX = x / standardDeviation;
double numerator = Math.Pow(Math.E, (-(scaledX * scaledX)) / 2);
return numerator;
}

private int binarySearchGte(double startTime)
{
int left = 0;
int right = amplitudesByTime.Count - 1;
int startIndex = -1;

while (left <= right)
{
int mid = left (right - left) / 2;
if (amplitudesByTime.Keys[mid] == startTime)

Check failure on line 38 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/Data/TaikoTimeField.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Blank lines are missing, expected minimum 1 instead of 0 in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\Data\TaikoTimeField.cs on line 38
{
startIndex = mid;
break;
}
else if (amplitudesByTime.Keys[mid] < startTime)
{
left = mid 1;
}
else
{
startIndex = mid;
right = mid - 1;
}
}

return startIndex;
}

private List<(double time, double amplitude)> getNodesIn(double startTime, double endTime)
{
List<(double, double)> withinRangeAmplitudes = new List<(double, double)>();
int startIndex = binarySearchGte(startTime);

if (startIndex != -1)
{
for (int i = startIndex; i < amplitudesByTime.Count && amplitudesByTime.Keys[i] <= endTime; i )
{
withinRangeAmplitudes.Add((amplitudesByTime.Keys[i], amplitudesByTime.Values[i]));
}
}

return withinRangeAmplitudes;
}

/// <summary>
/// Returns the sum of amplitudes within a range scaled by a normal distribution.
/// </summary>
///
/// <param name="time">The center time point</param>
/// <param name="standardDeviation">
/// Range of the normal distribution. Note that only amplitudes within 3 standard
/// deviations are considered
/// </param>
/// <returns></returns>
public double GetAmplitude(double time, double standardDeviation)
{
List<(double time, double amplitude)> nodes = getNodesIn(
time - standardDeviation * 3, time standardDeviation * 3);
return nodes
.Sum(x =>
{
return bellCurve(time - x.time, standardDeviation) * x.amplitude;

Check failure on line 90 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/Data/TaikoTimeField.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Use lambda expression in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\Data\TaikoTimeField.cs on line 90
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 1,93 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Taiko.Objects;

namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Pattern
{
public static class TaikoPatternDifficultyPreprocessor
{
private const int harmonics = 8;

private const double cycles_count = 4;

private const double decay_base = Math.E;

private static void createAlignmentPoints(double time, double interval, TaikoTimeField timeField)
{
for (int i = 0; i < harmonics; i )
{
double harmonicInterval = interval * Math.Pow(2, i);
double harmonicAmplitude = Math.Pow(0.5, i);

for (int j = 0; j < cycles_count * (i 1); j )
{
double t = time harmonicInterval * j;
double amplitude = harmonicAmplitude * Math.Pow(decay_base, j / (i 1));

Check failure on line 30 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/TaikoPatternDifficultyPreprocessor.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Possible loss of fraction in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\TaikoPatternDifficultyPreprocessor.cs on line 30
timeField.AddNode(t, amplitude);
}
}
}

private static void createRhythmAlignmentPoints(
IEnumerable<TaikoDifficultyHitObject> hitObjects, TaikoTimeField timeField)
{
hitObjects
.Where(hitObject => hitObject.DeltaTime > 0)
.ForEach(hitObject => createAlignmentPoints(hitObject.StartTime, hitObject.DeltaTime, timeField));
}

private static void createColourAlignmentPoints(
IEnumerable<TaikoDifficultyHitObject> hitObject, HitType type, TaikoTimeField timeField)
{
hitObject
.Where(hitObject => (hitObject.BaseObject as Hit)?.Type == type)

Check failure on line 48 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/TaikoPatternDifficultyPreprocessor.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Parameter 'hitObject' hides outer parameter with the same name in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\TaikoPatternDifficultyPreprocessor.cs on line 48
.SelectPair((previous, current) =>
(interval: current.StartTime - previous.StartTime, hitObject: current))
.ForEach(x => createAlignmentPoints(x.hitObject.StartTime, x.interval, timeField));
}


private static void createCentreAlignmentPoints(

Check failure on line 55 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/TaikoPatternDifficultyPreprocessor.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Blank lines are redundant, expected maximum 1 instead of 2 in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\TaikoPatternDifficultyPreprocessor.cs on line 55
IEnumerable<TaikoDifficultyHitObject> hitObject, TaikoPatternFields fields
) => createColourAlignmentPoints(hitObject, HitType.Centre, fields.CentreField);

private static void createRimAlignmentPoints(
IEnumerable<TaikoDifficultyHitObject> hitObject, TaikoPatternFields fields
) => createColourAlignmentPoints(hitObject, HitType.Rim, fields.RimField);

private static void createColourChangeAlignmentPoints(
IEnumerable<TaikoDifficultyHitObject> hitObject, TaikoPatternFields fields
)
{
hitObject
.Where(hitObject => hitObject.BaseObject is Hit)

Check failure on line 68 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/TaikoPatternDifficultyPreprocessor.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Line is not indented relative to the previous line, expected indent 4 spaces in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\TaikoPatternDifficultyPreprocessor.cs on line 68

Check failure on line 68 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/TaikoPatternDifficultyPreprocessor.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Parameter 'hitObject' hides outer parameter with the same name in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\TaikoPatternDifficultyPreprocessor.cs on line 68
.SelectPair((previous, current) =>
{
var previousHit = (Hit)previous.BaseObject;
var currentHit = (Hit)current.BaseObject;
return previousHit.Type == currentHit.Type ? current : null;
})
.OfType<TaikoDifficultyHitObject>()
.SelectPair(
(previous, current) => (interval: current.StartTime - previous.StartTime, hitObject: current))
.ForEach(x => createAlignmentPoints(x.hitObject.StartTime, x.interval, fields.ColourChangeField));
}

public static TaikoPatternFields ComputeFields(IEnumerable<TaikoDifficultyHitObject> hitObjects)
{
TaikoPatternFields fields = new TaikoPatternFields();

createRhythmAlignmentPoints(hitObjects, fields.RhythmField);
createCentreAlignmentPoints(hitObjects, fields);
createRimAlignmentPoints(hitObjects, fields);
createColourChangeAlignmentPoints(hitObjects, fields);

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

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

public static class CollectionUtils

Check failure on line 8 in osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/Pattern/Utils/CollectionUtils.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Namespace does not correspond to file location, must be: 'osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Pattern.Utils' in osu.Game.Rulesets.Taiko\Difficulty\Preprocessing\Pattern\Utils\CollectionUtils.cs on line 8
{
public static IEnumerable<TargetType> SelectPair<SourceType, TargetType>(
this IEnumerable<SourceType> source, Func<SourceType, SourceType, TargetType> operation)
{
return source
.Zip(source.Skip(1), source.SkipLast(1))
.Select(x => operation(x.First, x.Second));
}
}
54 changes: 54 additions & 0 deletions osu.Game.Rulesets.Taiko/Difficulty/Skills/Pattern.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,54 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.


using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Scoring;

namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
public class Pattern : StrainDecaySkill
{
protected override double SkillMultiplier => 1;

protected override double StrainDecayBase => 0;

private TaikoPatternFields? fields;

private const double rhythm_error_standard_deviation = 1000.0;

private double? hitWindowStandardDeviation;

public Pattern(Mod[] mods) : base(mods) { }

public void Initialize(IBeatmap beatmap, TaikoPatternFields fields)
{
this.fields = fields;

HitWindows hitWindows = new TaikoHitWindows();
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
hitWindowStandardDeviation = hitWindows.WindowFor(HitResult.Great) / 4;
}

protected override double StrainValueOf(DifficultyHitObject current)
{
if (fields == null || hitWindowStandardDeviation == null)
{
throw new InvalidOperationException("Pattern skill class has not been initialized. Please call Initialize first.");
}

return PatternEvaluator.EvaluateDifficultyOf(
(TaikoDifficultyHitObject)current,
fields,
rhythm_error_standard_deviation,
(double)hitWindowStandardDeviation);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 28,12 @@ public class TaikoDifficultyAttributes : DifficultyAttributes
[JsonProperty("rhythm_difficulty")]
public double RhythmDifficulty { get; set; }

/// <summary>
/// The difficulty corresponding to the rhythm skill.
/// </summary>
[JsonProperty("pattern_difficulty")]
public double PatternDifficulty { get; set; }

/// <summary>
/// The difficulty corresponding to the colour skill.
/// </summary>
Expand Down
Loading

0 comments on commit eabd810

Please sign in to comment.