mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:07:52 +08:00
Add difficulty calculation
Adds base classes for difficulty calculations, hooks them up with carousel container, and adds a port of the osu difficulty calculator.
This commit is contained in:
parent
5721e7f2d2
commit
417f146386
@ -1 +1 @@
|
||||
Subproject commit 697d8b7e9530ba7914d9c78012329ce1559e3681
|
||||
Subproject commit f370d4d6ef9f975932324d30272dfffc929a6622
|
27
osu.Game.Modes.Catch/CatchDifficultyCalculator.cs
Normal file
27
osu.Game.Modes.Catch/CatchDifficultyCalculator.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Modes.Catch.Objects;
|
||||
using osu.Game.Modes.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Modes.Catch
|
||||
{
|
||||
public class CatchDifficultyCalculator : DifficultyCalculator<CatchBaseHit>
|
||||
{
|
||||
protected override PlayMode PlayMode => PlayMode.Catch;
|
||||
|
||||
public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
protected override HitObjectConverter<CatchBaseHit> Converter => new CatchConverter();
|
||||
|
||||
protected override double ComputeDifficulty(Dictionary<String, String> categoryDifficulty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Modes.Catch.UI;
|
||||
using osu.Game.Modes.Objects;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using osu.Game.Modes.Osu.UI;
|
||||
using osu.Game.Modes.UI;
|
||||
using osu.Game.Beatmaps;
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Modes.Catch
|
||||
{
|
||||
@ -25,5 +24,7 @@ namespace osu.Game.Modes.Catch
|
||||
public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => null;
|
||||
|
||||
public override HitObjectParser CreateHitObjectParser() => new NullHitObjectParser();
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new CatchDifficultyCalculator(beatmap);
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="CatchDifficultyCalculator.cs" />
|
||||
<Compile Include="Objects\CatchBaseHit.cs" />
|
||||
<Compile Include="Objects\CatchConverter.cs" />
|
||||
<Compile Include="Objects\Drawable\DrawableFruit.cs" />
|
||||
|
30
osu.Game.Modes.Mania/ManiaDifficultyCalculator.cs
Normal file
30
osu.Game.Modes.Mania/ManiaDifficultyCalculator.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Modes.Mania.Objects;
|
||||
using osu.Game.Modes.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Modes.Mania
|
||||
{
|
||||
public class ManiaDifficultyCalculator : DifficultyCalculator<ManiaBaseHit>
|
||||
{
|
||||
protected override PlayMode PlayMode => PlayMode.Mania;
|
||||
|
||||
private int columns;
|
||||
|
||||
public ManiaDifficultyCalculator(Beatmap beatmap, int columns = 5) : base(beatmap)
|
||||
{
|
||||
this.columns = columns;
|
||||
}
|
||||
|
||||
protected override HitObjectConverter<ManiaBaseHit> Converter => new ManiaConverter(columns);
|
||||
|
||||
protected override double ComputeDifficulty(Dictionary<String, String> categoryDifficulty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +1,13 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Modes.Mania.UI;
|
||||
using osu.Game.Modes.Objects;
|
||||
using osu.Game.Modes.Osu;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using osu.Game.Modes.Osu.UI;
|
||||
using osu.Game.Modes.UI;
|
||||
using osu.Game.Beatmaps;
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Modes.Mania
|
||||
{
|
||||
@ -26,5 +24,7 @@ namespace osu.Game.Modes.Mania
|
||||
public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => null;
|
||||
|
||||
public override HitObjectParser CreateHitObjectParser() => new NullHitObjectParser();
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new ManiaDifficultyCalculator(beatmap);
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ManiaDifficultyCalculator.cs" />
|
||||
<Compile Include="Objects\Drawable\DrawableNote.cs" />
|
||||
<Compile Include="Objects\HoldNote.cs" />
|
||||
<Compile Include="Objects\ManiaBaseHit.cs" />
|
||||
|
@ -108,7 +108,7 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 scaleToCircle => new Vector2(circle.Scale * circle.DrawWidth / DrawWidth) * 0.95f;
|
||||
private Vector2 scaleToCircle => (circle.Scale * circle.DrawWidth / DrawWidth) * 0.95f;
|
||||
|
||||
private float spinsPerMinuteNeeded = 100 + (5 * 15); //TODO: read per-map OD and place it on the 5
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Modes.Osu.Objects
|
||||
{
|
||||
public class HitCircle : OsuHitObject
|
||||
{
|
||||
public override HitObjectType Type => HitObjectType.Circle;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
|
||||
public float Scale { get; set; } = 1;
|
||||
|
||||
public abstract HitObjectType Type { get; }
|
||||
|
||||
public override void SetDefaultsFromBeatmap(Beatmap beatmap)
|
||||
{
|
||||
base.SetDefaultsFromBeatmap(beatmap);
|
||||
@ -36,14 +38,12 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
[Flags]
|
||||
public enum HitObjectType
|
||||
{
|
||||
Circle = 1,
|
||||
Slider = 2,
|
||||
NewCombo = 4,
|
||||
CircleNewCombo = 5,
|
||||
SliderNewCombo = 6,
|
||||
Spinner = 8,
|
||||
Circle = 1 << 0,
|
||||
Slider = 1 << 1,
|
||||
NewCombo = 1 << 2,
|
||||
Spinner = 1 << 3,
|
||||
ColourHax = 122,
|
||||
Hold = 128,
|
||||
ManiaLong = 128,
|
||||
Hold = 1 << 7,
|
||||
SliderTick = 1 << 8,
|
||||
}
|
||||
}
|
||||
|
200
osu.Game.Modes.Osu/Objects/OsuHitObjectDifficulty.cs
Normal file
200
osu.Game.Modes.Osu/Objects/OsuHitObjectDifficulty.cs
Normal file
@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using OpenTK;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Modes.Osu.Objects
|
||||
{
|
||||
class OsuHitObjectDifficulty
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor by how much speed / aim strain decays per second.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These values are results of tweaking a lot and taking into account general feedback.
|
||||
/// Opinionated observation: Speed is easier to maintain than accurate jumps.
|
||||
/// </remarks>
|
||||
internal static readonly double[] DECAY_BASE = { 0.3, 0.15 };
|
||||
|
||||
/// <summary>
|
||||
/// Pseudo threshold values to distinguish between "singles" and "streams"
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Of course the border can not be defined clearly, therefore the algorithm has a smooth transition between those values.
|
||||
/// They also are based on tweaking and general feedback.
|
||||
/// </remarks>
|
||||
private const double STREAM_SPACING_TRESHOLD = 110,
|
||||
SINGLE_SPACING_TRESHOLD = 125;
|
||||
|
||||
/// <summary>
|
||||
/// Scaling values for weightings to keep aim and speed difficulty in balance.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Found from testing a very large map pool (containing all ranked maps) and keeping the average values the same.
|
||||
/// </remarks>
|
||||
private static readonly double[] SPACING_WEIGHT_SCALING = { 1400, 26.25 };
|
||||
|
||||
/// <summary>
|
||||
/// Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming.
|
||||
/// </summary>
|
||||
private const double ALMOST_DIAMETER = 90;
|
||||
|
||||
internal OsuHitObject BaseHitObject;
|
||||
internal double[] Strains = { 1, 1 };
|
||||
|
||||
internal int MaxCombo = 1;
|
||||
|
||||
private Vector2 normalizedStartPosition;
|
||||
private Vector2 normalizedEndPosition;
|
||||
private float lazySliderLength;
|
||||
|
||||
internal OsuHitObjectDifficulty(OsuHitObject baseHitObject)
|
||||
{
|
||||
BaseHitObject = baseHitObject;
|
||||
float circleRadius = baseHitObject.Scale * 64;
|
||||
|
||||
Slider slider = BaseHitObject as Slider;
|
||||
if (slider != null)
|
||||
MaxCombo += slider.Ticks.Count();
|
||||
|
||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||
float scalingFactor = (52.0f / circleRadius);
|
||||
if (circleRadius < 30)
|
||||
{
|
||||
float smallCircleBonus = Math.Min(30.0f - circleRadius, 5.0f) / 50.0f;
|
||||
scalingFactor *= 1.0f + smallCircleBonus;
|
||||
}
|
||||
|
||||
normalizedStartPosition = BaseHitObject.StackedPosition * scalingFactor;
|
||||
|
||||
lazySliderLength = 0;
|
||||
|
||||
// Calculate approximation of lazy movement on the slider
|
||||
if (slider != null)
|
||||
{
|
||||
float sliderFollowCircleRadius = circleRadius * 3; // Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests.
|
||||
|
||||
// For simplifying this step we use actual osu! coordinates and simply scale the length, that we obtain by the ScalingFactor later
|
||||
Vector2 cursorPos = baseHitObject.StackedPosition;
|
||||
|
||||
Action<Vector2> addSliderVertex = delegate (Vector2 pos)
|
||||
{
|
||||
Vector2 difference = pos - cursorPos;
|
||||
float distance = difference.Length;
|
||||
|
||||
// Did we move away too far?
|
||||
if (distance > sliderFollowCircleRadius)
|
||||
{
|
||||
// Yep, we need to move the cursor
|
||||
difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference
|
||||
distance -= sliderFollowCircleRadius;
|
||||
cursorPos += difference * distance; // We move the cursor just as far as needed to stay in the follow circle
|
||||
lazySliderLength += distance;
|
||||
}
|
||||
};
|
||||
|
||||
// Actual computation of the first lazy curve
|
||||
foreach (var tick in slider.Ticks)
|
||||
addSliderVertex(tick.StackedPosition);
|
||||
|
||||
addSliderVertex(baseHitObject.StackedEndPosition);
|
||||
|
||||
lazySliderLength *= scalingFactor;
|
||||
normalizedEndPosition = cursorPos * scalingFactor;
|
||||
}
|
||||
// We have a normal HitCircle or a spinner
|
||||
else
|
||||
normalizedEndPosition = normalizedStartPosition;
|
||||
}
|
||||
|
||||
internal void CalculateStrains(OsuHitObjectDifficulty previousHitObject, double timeRate)
|
||||
{
|
||||
calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Speed, timeRate);
|
||||
calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Aim, timeRate);
|
||||
}
|
||||
|
||||
// Caution: The subjective values are strong with this one
|
||||
private static double spacingWeight(double distance, OsuDifficultyCalculator.DifficultyType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case OsuDifficultyCalculator.DifficultyType.Speed:
|
||||
if (distance > SINGLE_SPACING_TRESHOLD)
|
||||
return 2.5;
|
||||
else if (distance > STREAM_SPACING_TRESHOLD)
|
||||
return 1.6 + 0.9 * (distance - STREAM_SPACING_TRESHOLD) / (SINGLE_SPACING_TRESHOLD - STREAM_SPACING_TRESHOLD);
|
||||
else if (distance > ALMOST_DIAMETER)
|
||||
return 1.2 + 0.4 * (distance - ALMOST_DIAMETER) / (STREAM_SPACING_TRESHOLD - ALMOST_DIAMETER);
|
||||
else if (distance > ALMOST_DIAMETER / 2)
|
||||
return 0.95 + 0.25 * (distance - (ALMOST_DIAMETER / 2)) / (ALMOST_DIAMETER / 2);
|
||||
else
|
||||
return 0.95;
|
||||
|
||||
case OsuDifficultyCalculator.DifficultyType.Aim:
|
||||
return Math.Pow(distance, 0.99);
|
||||
}
|
||||
|
||||
Debug.Assert(false, "Invalid osu difficulty hit object type.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void calculateSpecificStrain(OsuHitObjectDifficulty previousHitObject, OsuDifficultyCalculator.DifficultyType type, double timeRate)
|
||||
{
|
||||
double addition = 0;
|
||||
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
|
||||
double decay = Math.Pow(DECAY_BASE[(int)type], timeElapsed / 1000);
|
||||
|
||||
if (BaseHitObject.Type == HitObjectType.Spinner)
|
||||
{
|
||||
// Do nothing for spinners
|
||||
}
|
||||
else if (BaseHitObject.Type == HitObjectType.Slider)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case OsuDifficultyCalculator.DifficultyType.Speed:
|
||||
|
||||
// For speed strain we treat the whole slider as a single spacing entity, since "Speed" is about how hard it is to click buttons fast.
|
||||
// The spacing weight exists to differentiate between being able to easily alternate or having to single.
|
||||
addition =
|
||||
spacingWeight(previousHitObject.lazySliderLength +
|
||||
DistanceTo(previousHitObject), type) *
|
||||
SPACING_WEIGHT_SCALING[(int)type];
|
||||
|
||||
break;
|
||||
case OsuDifficultyCalculator.DifficultyType.Aim:
|
||||
|
||||
// For Aim strain we treat each slider segment and the jump after the end of the slider as separate jumps, since movement-wise there is no difference
|
||||
// to multiple jumps.
|
||||
addition =
|
||||
(
|
||||
spacingWeight(previousHitObject.lazySliderLength, type) +
|
||||
spacingWeight(DistanceTo(previousHitObject), type)
|
||||
) *
|
||||
SPACING_WEIGHT_SCALING[(int)type];
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (BaseHitObject.Type == HitObjectType.Circle)
|
||||
{
|
||||
addition = spacingWeight(DistanceTo(previousHitObject), type) * SPACING_WEIGHT_SCALING[(int)type];
|
||||
}
|
||||
|
||||
// Scale addition by the time, that elapsed. Filter out HitObjects that are too close to be played anyway to avoid crazy values by division through close to zero.
|
||||
// You will never find maps that require this amongst ranked maps.
|
||||
addition /= Math.Max(timeElapsed, 50);
|
||||
|
||||
Strains[(int)type] = previousHitObject.Strains[(int)type] * decay + addition;
|
||||
}
|
||||
|
||||
internal double DistanceTo(OsuHitObjectDifficulty other)
|
||||
{
|
||||
// Scale the distance by circle size.
|
||||
return (normalizedStartPosition - other.normalizedEndPosition).Length;
|
||||
}
|
||||
}
|
||||
}
|
@ -110,6 +110,8 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override HitObjectType Type => HitObjectType.Slider;
|
||||
}
|
||||
|
||||
public enum CurveTypes
|
||||
|
@ -5,5 +5,7 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
public class SliderTick : OsuHitObject
|
||||
{
|
||||
public int RepeatIndex { get; set; }
|
||||
|
||||
public override HitObjectType Type => HitObjectType.SliderTick;
|
||||
}
|
||||
}
|
||||
|
@ -10,5 +10,7 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
public double Length;
|
||||
|
||||
public override double EndTime => StartTime + Length;
|
||||
|
||||
public override HitObjectType Type => HitObjectType.Spinner;
|
||||
}
|
||||
}
|
||||
|
193
osu.Game.Modes.Osu/OsuDifficultyCalculator.cs
Normal file
193
osu.Game.Modes.Osu/OsuDifficultyCalculator.cs
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Modes.Objects;
|
||||
|
||||
namespace osu.Game.Modes.Osu
|
||||
{
|
||||
public class OsuDifficultyCalculator : DifficultyCalculator<OsuHitObject>
|
||||
{
|
||||
private const double STAR_SCALING_FACTOR = 0.0675;
|
||||
private const double EXTREME_SCALING_FACTOR = 0.5;
|
||||
|
||||
protected override PlayMode PlayMode => PlayMode.Osu;
|
||||
|
||||
/// <summary>
|
||||
/// HitObjects are stored as a member variable.
|
||||
/// </summary>
|
||||
internal List<OsuHitObjectDifficulty> DifficultyHitObjects = new List<OsuHitObjectDifficulty>();
|
||||
|
||||
public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
protected override HitObjectConverter<OsuHitObject> Converter => new OsuHitObjectConverter();
|
||||
|
||||
protected override void PreprocessHitObjects()
|
||||
{
|
||||
foreach (var h in Objects)
|
||||
if (h.Type == HitObjectType.Slider)
|
||||
((Slider)h).Curve.Calculate();
|
||||
}
|
||||
|
||||
protected override double ComputeDifficulty(Dictionary<String, String> categoryDifficulty)
|
||||
{
|
||||
// Fill our custom DifficultyHitObject class, that carries additional information
|
||||
DifficultyHitObjects.Clear();
|
||||
|
||||
foreach (var hitObject in Objects)
|
||||
DifficultyHitObjects.Add(new OsuHitObjectDifficulty(hitObject));
|
||||
|
||||
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
|
||||
DifficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
|
||||
|
||||
if (!CalculateStrainValues()) return 0;
|
||||
|
||||
double speedDifficulty = CalculateDifficulty(DifficultyType.Speed);
|
||||
double aimDifficulty = CalculateDifficulty(DifficultyType.Aim);
|
||||
|
||||
// OverallDifficulty is not considered in this algorithm and neither is HpDrainRate. That means, that in this form the algorithm determines how hard it physically is
|
||||
// to play the map, assuming, that too much of an error will not lead to a death.
|
||||
// It might be desirable to include OverallDifficulty into map difficulty, but in my personal opinion it belongs more to the weighting of the actual peformance
|
||||
// and is superfluous in the beatmap difficulty rating.
|
||||
// If it were to be considered, then I would look at the hit window of normal HitCircles only, since Sliders and Spinners are (almost) "free" 300s and take map length
|
||||
// into account as well.
|
||||
|
||||
// The difficulty can be scaled by any desired metric.
|
||||
// In osu!tp it gets squared to account for the rapid increase in difficulty as the limit of a human is approached. (Of course it also gets scaled afterwards.)
|
||||
// It would not be suitable for a star rating, therefore:
|
||||
|
||||
// The following is a proposal to forge a star rating from 0 to 5. It consists of taking the square root of the difficulty, since by simply scaling the easier
|
||||
// 5-star maps would end up with one star.
|
||||
double speedStars = Math.Sqrt(speedDifficulty) * STAR_SCALING_FACTOR;
|
||||
double aimStars = Math.Sqrt(aimDifficulty) * STAR_SCALING_FACTOR;
|
||||
|
||||
if (categoryDifficulty != null)
|
||||
{
|
||||
categoryDifficulty.Add("Aim", aimStars.ToString("0.00"));
|
||||
categoryDifficulty.Add("Speed", speedStars.ToString("0.00"));
|
||||
|
||||
double hitWindow300 = 30/*HitObjectManager.HitWindow300*/ / TimeRate;
|
||||
double preEmpt = 450/*HitObjectManager.PreEmpt*/ / TimeRate;
|
||||
|
||||
categoryDifficulty.Add("OD", (-(hitWindow300 - 80.0) / 6.0).ToString("0.00"));
|
||||
categoryDifficulty.Add("AR", (preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0).ToString("0.00"));
|
||||
|
||||
int maxCombo = 0;
|
||||
foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects)
|
||||
maxCombo += hitObject.MaxCombo;
|
||||
|
||||
categoryDifficulty.Add("Max combo", maxCombo.ToString());
|
||||
}
|
||||
|
||||
// Again, from own observations and from the general opinion of the community a map with high speed and low aim (or vice versa) difficulty is harder,
|
||||
// than a map with mediocre difficulty in both. Therefore we can not just add both difficulties together, but will introduce a scaling that favors extremes.
|
||||
double starRating = speedStars + aimStars + Math.Abs(speedStars - aimStars) * EXTREME_SCALING_FACTOR;
|
||||
// Another approach to this would be taking Speed and Aim separately to a chosen power, which again would be equivalent. This would be more convenient if
|
||||
// the hit window size is to be considered as well.
|
||||
|
||||
// Note: The star rating is tuned extremely tight! Airman (/b/104229) and Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars.
|
||||
// Expect the easier kind of maps that officially get 5 stars to obtain around 2 by this metric. The tutorial still scores about half a star.
|
||||
// Tune by yourself as you please. ;)
|
||||
|
||||
return starRating;
|
||||
}
|
||||
|
||||
protected bool CalculateStrainValues()
|
||||
{
|
||||
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
|
||||
List<OsuHitObjectDifficulty>.Enumerator hitObjectsEnumerator = DifficultyHitObjects.GetEnumerator();
|
||||
|
||||
if (!hitObjectsEnumerator.MoveNext()) return false;
|
||||
|
||||
OsuHitObjectDifficulty currentHitObject = hitObjectsEnumerator.Current;
|
||||
OsuHitObjectDifficulty nextHitObject;
|
||||
|
||||
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
|
||||
while (hitObjectsEnumerator.MoveNext())
|
||||
{
|
||||
nextHitObject = hitObjectsEnumerator.Current;
|
||||
nextHitObject.CalculateStrains(currentHitObject, TimeRate);
|
||||
currentHitObject = nextHitObject;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
|
||||
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
|
||||
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
|
||||
/// </summary>
|
||||
protected const double STRAIN_STEP = 400;
|
||||
|
||||
/// <summary>
|
||||
/// The weighting of each strain value decays to this number * it's previous value
|
||||
/// </summary>
|
||||
protected const double DECAY_WEIGHT = 0.9;
|
||||
|
||||
protected double CalculateDifficulty(DifficultyType type)
|
||||
{
|
||||
double actualStrainStep = STRAIN_STEP * TimeRate;
|
||||
|
||||
// Find the highest strain value within each strain step
|
||||
List<double> highestStrains = new List<double>();
|
||||
double intervalEndTime = actualStrainStep;
|
||||
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
|
||||
|
||||
OsuHitObjectDifficulty previousHitObject = null;
|
||||
foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects)
|
||||
{
|
||||
// While we are beyond the current interval push the currently available maximum to our strain list
|
||||
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
|
||||
{
|
||||
highestStrains.Add(maximumStrain);
|
||||
|
||||
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
|
||||
// until the beginning of the next interval.
|
||||
if (previousHitObject == null)
|
||||
{
|
||||
maximumStrain = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
double decay = Math.Pow(OsuHitObjectDifficulty.DECAY_BASE[(int)type], (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
|
||||
maximumStrain = previousHitObject.Strains[(int)type] * decay;
|
||||
}
|
||||
|
||||
// Go to the next time interval
|
||||
intervalEndTime += actualStrainStep;
|
||||
}
|
||||
|
||||
// Obtain maximum strain
|
||||
maximumStrain = Math.Max(hitObject.Strains[(int)type], maximumStrain);
|
||||
|
||||
previousHitObject = hitObject;
|
||||
}
|
||||
|
||||
// Build the weighted sum over the highest strains for each interval
|
||||
double difficulty = 0;
|
||||
double weight = 1;
|
||||
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
|
||||
|
||||
foreach (double strain in highestStrains)
|
||||
{
|
||||
difficulty += weight * strain;
|
||||
weight *= DECAY_WEIGHT;
|
||||
}
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
// Those values are used as array indices. Be careful when changing them!
|
||||
public enum DifficultyType : int
|
||||
{
|
||||
Speed = 0,
|
||||
Aim,
|
||||
};
|
||||
}
|
||||
}
|
@ -40,6 +40,8 @@ namespace osu.Game.Modes.Osu
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => new OsuScoreProcessor(hitObjectCount);
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new OsuDifficultyCalculator(beatmap);
|
||||
|
||||
protected override PlayMode PlayMode => PlayMode.Osu;
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,11 @@
|
||||
<Compile Include="Objects\Drawables\Pieces\TrianglesPiece.cs" />
|
||||
<Compile Include="Objects\Drawables\Pieces\SliderBall.cs" />
|
||||
<Compile Include="Objects\Drawables\Pieces\SliderBody.cs" />
|
||||
<Compile Include="Objects\OsuHitObjectDifficulty.cs" />
|
||||
<Compile Include="Objects\OsuHitObjectParser.cs" />
|
||||
<Compile Include="Objects\SliderCurve.cs" />
|
||||
<Compile Include="Objects\SliderTick.cs" />
|
||||
<Compile Include="OsuDifficultyCalculator.cs" />
|
||||
<Compile Include="OsuScore.cs" />
|
||||
<Compile Include="OsuScoreProcessor.cs" />
|
||||
<Compile Include="UI\OsuComboCounter.cs" />
|
||||
|
27
osu.Game.Modes.Taiko/TaikoDifficultyCalculator.cs
Normal file
27
osu.Game.Modes.Taiko/TaikoDifficultyCalculator.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Modes.Objects;
|
||||
using osu.Game.Modes.Taiko.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Modes.Taiko
|
||||
{
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator<TaikoBaseHit>
|
||||
{
|
||||
protected override PlayMode PlayMode => PlayMode.Taiko;
|
||||
|
||||
public TaikoDifficultyCalculator(Beatmap beatmap) : base(beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
protected override HitObjectConverter<TaikoBaseHit> Converter => new TaikoConverter();
|
||||
|
||||
protected override double ComputeDifficulty(Dictionary<String, String> categoryDifficulty)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,8 @@
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Modes.Objects;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using osu.Game.Modes.Osu.UI;
|
||||
using osu.Game.Modes.Taiko.UI;
|
||||
using osu.Game.Modes.UI;
|
||||
@ -26,5 +24,7 @@ namespace osu.Game.Modes.Taiko
|
||||
public override ScoreProcessor CreateScoreProcessor(int hitObjectCount) => null;
|
||||
|
||||
public override HitObjectParser CreateHitObjectParser() => new NullHitObjectParser();
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new TaikoDifficultyCalculator(beatmap);
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="TaikoDifficultyCalculator.cs" />
|
||||
<Compile Include="Objects\Drawable\DrawableTaikoHit.cs" />
|
||||
<Compile Include="Objects\TaikoBaseHit.cs" />
|
||||
<Compile Include="Objects\TaikoConverter.cs" />
|
||||
|
@ -1,18 +1,15 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using NUnit.Framework;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Beatmaps.Samples;
|
||||
using osu.Game.Modes;
|
||||
using osu.Game.Modes.Osu;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
|
50
osu.Game/Beatmaps/DifficultyCalculator.cs
Normal file
50
osu.Game/Beatmaps/DifficultyCalculator.cs
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Modes;
|
||||
using osu.Game.Modes.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
public abstract class DifficultyCalculator
|
||||
{
|
||||
protected abstract PlayMode PlayMode { get; }
|
||||
|
||||
protected double TimeRate = 1;
|
||||
|
||||
protected abstract double ComputeDifficulty(Dictionary<String, String> categoryDifficulty);
|
||||
|
||||
private void loadTiming()
|
||||
{
|
||||
// TODO: Handle mods
|
||||
int audioRate = 100;
|
||||
TimeRate = audioRate / 100.0;
|
||||
}
|
||||
|
||||
public double GetDifficulty(Dictionary<string, string> categoryDifficulty = null)
|
||||
{
|
||||
loadTiming();
|
||||
double difficulty = ComputeDifficulty(categoryDifficulty);
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DifficultyCalculator<T> : DifficultyCalculator where T : HitObject
|
||||
{
|
||||
protected List<T> Objects;
|
||||
|
||||
protected abstract HitObjectConverter<T> Converter { get; }
|
||||
|
||||
public DifficultyCalculator(Beatmap beatmap)
|
||||
{
|
||||
Objects = Converter.Convert(beatmap);
|
||||
PreprocessHitObjects();
|
||||
}
|
||||
|
||||
protected virtual void PreprocessHitObjects()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
public BeatmapGroup(WorkingBeatmap beatmap, BeatmapSetInfo set = null)
|
||||
public BeatmapGroup(WorkingBeatmap beatmap)
|
||||
{
|
||||
Header = new BeatmapSetHeader(beatmap)
|
||||
{
|
||||
@ -68,6 +68,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
RelativeSizeAxes = Axes.X,
|
||||
};
|
||||
|
||||
BeatmapSet = beatmap.BeatmapSetInfo;
|
||||
BeatmapPanels = beatmap.BeatmapSetInfo.Beatmaps.Select(b => new BeatmapPanel(b)
|
||||
{
|
||||
Alpha = 0,
|
||||
@ -76,7 +77,6 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}).ToList();
|
||||
|
||||
BeatmapSet = set;
|
||||
Header.AddDifficultyIcons(BeatmapPanels);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,6 @@
|
||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace osu.Game.Beatmaps.Timing
|
||||
{
|
||||
class TimingChange : ControlPoint
|
||||
|
@ -68,7 +68,6 @@ namespace osu.Game.Beatmaps
|
||||
beatmap = decoder?.Decode(stream);
|
||||
}
|
||||
|
||||
|
||||
if (WithStoryboard && beatmap != null && BeatmapSetInfo.StoryboardFile != null)
|
||||
using (var stream = new StreamReader(reader.GetStream(BeatmapSetInfo.StoryboardFile)))
|
||||
decoder?.Decode(stream, beatmap);
|
||||
|
@ -8,6 +8,7 @@ using osu.Game.Modes;
|
||||
using osu.Game.Screens.Play;
|
||||
using SQLite.Net.Attributes;
|
||||
using SQLiteNetExtensions.Attributes;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
@ -73,7 +74,23 @@ namespace osu.Game.Database
|
||||
// Metadata
|
||||
public string Version { get; set; }
|
||||
|
||||
public float StarDifficulty => BaseDifficulty?.OverallDifficulty ?? 5; //todo: implement properly
|
||||
//todo: background threaded computation of this
|
||||
private float starDifficulty = -1;
|
||||
public float StarDifficulty
|
||||
{
|
||||
get
|
||||
{
|
||||
return (starDifficulty < 0) ? (BaseDifficulty?.OverallDifficulty ?? 5) : starDifficulty;
|
||||
}
|
||||
|
||||
set { starDifficulty = value; }
|
||||
}
|
||||
|
||||
internal void ComputeDifficulty(BeatmapDatabase database)
|
||||
{
|
||||
WorkingBeatmap wb = new WorkingBeatmap(this, BeatmapSet, database);
|
||||
StarDifficulty = (float)Ruleset.GetRuleset(Mode).CreateDifficultyCalculator(wb.Beatmap).GetDifficulty();
|
||||
}
|
||||
|
||||
public bool Equals(BeatmapInfo other)
|
||||
{
|
||||
|
@ -32,6 +32,8 @@ namespace osu.Game.Modes
|
||||
|
||||
public abstract HitObjectParser CreateHitObjectParser();
|
||||
|
||||
public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap);
|
||||
|
||||
public static void Register(Ruleset ruleset) => availableRulesets.TryAdd(ruleset.PlayMode, ruleset.GetType());
|
||||
|
||||
protected abstract PlayMode PlayMode { get; }
|
||||
|
@ -337,11 +337,13 @@ namespace osu.Game.Screens.Select
|
||||
if (b.Metadata == null) b.Metadata = beatmapSet.Metadata;
|
||||
});
|
||||
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
b.ComputeDifficulty(database);
|
||||
beatmapSet.Beatmaps = beatmapSet.Beatmaps.OrderBy(b => b.StarDifficulty).ToList();
|
||||
|
||||
var beatmap = new WorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault(), beatmapSet, database);
|
||||
|
||||
var group = new BeatmapGroup(beatmap, beatmapSet)
|
||||
var group = new BeatmapGroup(beatmap)
|
||||
{
|
||||
SelectionChanged = selectionChanged,
|
||||
StartRequested = b => start()
|
||||
|
@ -66,6 +66,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Beatmaps\Drawables\BeatmapBackgroundSprite.cs" />
|
||||
<Compile Include="Beatmaps\DifficultyCalculator.cs" />
|
||||
<Compile Include="Graphics\Backgrounds\Triangles.cs" />
|
||||
<Compile Include="Graphics\Cursor\CursorTrail.cs" />
|
||||
<Compile Include="Graphics\Sprites\OsuSpriteText.cs" />
|
||||
|
Loading…
Reference in New Issue
Block a user