Skip to content

Instantly share code, notes, and snippets.

@smoogipoo
Created January 26, 2018 03:05
Show Gist options
  • Save smoogipoo/eca412396557825e5656360df6a813bd to your computer and use it in GitHub Desktop.
Save smoogipoo/eca412396557825e5656360df6a813bd to your computer and use it in GitHub Desktop.
Mania DiffCalc
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using osu.GameModes.Edit.AiMod;
using osu.GameModes.Play;
using osu.GameModes.Play.Rulesets.Mania;
using osu.GameplayElements.HitObjects;
using osu.GameplayElements.HitObjects.Mania;
using osu.GameplayElements.Scoring;
using osu_common;
namespace osu.GameplayElements.Beatmaps
{
public class BeatmapDifficultyCalculatorMania : BeatmapDifficultyCalculator
{
// 0.01 is good for stars
private const double STAR_SCALING_FACTOR = 0.018;
protected override PlayModes PlayMode => PlayModes.OsuMania;
public static Mods RelevantMods => Mods.DoubleTime | Mods.HalfTime | Mods.HardRock | Mods.Easy | Mods.KeyMod;
/// <summary>
/// HitObjects are stored as a member variable.
/// </summary>
internal List<DifficultyHitObjectMania> DifficultyHitObjects;
public override Mods[] ModCombinations
{
get
{
Mods[] mods = new Mods[BASE_MOD_COMBINATIONS.Length];
for (int i = 0; i < BASE_MOD_COMBINATIONS.Length; ++i)
mods[i] = keyMod | BASE_MOD_COMBINATIONS[i];
return mods;
}
}
private Mods keyMod;
public BeatmapDifficultyCalculatorMania(Beatmap beatmap, Mods keyMod)
: base(beatmap)
{
// Let the mania calculator only calculate difficulties for one specific key-mod at a time.
Debug.Assert((keyMod & ~Mods.KeyMod) == Mods.None);
// No key-mods for mania-specific maps. Conversions don't exist.
if (beatmap.PlayMode == PlayModes.OsuMania)
keyMod = Mods.None;
this.keyMod = keyMod;
}
internal override HitObjectManager NewHitObjectManager()
{
return new HitObjectManagerMania(false);
}
protected override bool ModsRequireReload(Mods mods)
{
// If we switch keymods we need to reload
return (HitObjectManager.ActiveMods & Mods.KeyMod) != (mods & Mods.KeyMod);
}
protected override double ComputeDifficulty(Dictionary<String, String> categoryDifficulty)
{
// Fill our custom DifficultyHitObject class, that carries additional information
DifficultyHitObjects = new List<DifficultyHitObjectMania>(HitObjects.Count);
foreach (HitObject hitObject in HitObjects)
{
//DifficultyHitObjects.Add(new DifficultyHitObjectMania(hitObject));
HitCircleManiaRow maniaRow = hitObject as HitCircleManiaRow;
if (maniaRow != null)
{
maniaRow.HitObjects.ForEach(n =>
{
DifficultyHitObjects.Add(new DifficultyHitObjectMania(n)); // This hitcircle should ALWAYS be of type HitCircleMania
});
}
else if (hitObject is HitCircleMania)
{
DifficultyHitObjects.Add(new DifficultyHitObjectMania((HitCircleMania)hitObject));
}
else
Debug.Print("Unknown mania circle type at: " + hitObject.StartTime);
}
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. Not using CompareTo, since it results in a crash (HitObjectBase inherits MarshalByRefObject)
DifficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime - b.BaseHitObject.StartTime);
if (!CalculateStrainValues()) return 0;
double starRating = CalculateDifficulty() * STAR_SCALING_FACTOR;
if (categoryDifficulty != null)
{
categoryDifficulty.Add("Strain", starRating.ToString("0.00", GameBase.nfi));
// Mania already multiplied by the timerate, so essentially the hitwindow stays the same across doubletime and halftime.
// Ceiling is required to lessen the rounding error
categoryDifficulty.Add("Hit window 300", Math.Ceiling(HitObjectManager.HitWindow300 / TimeRate).ToString("0", GameBase.nfi));
categoryDifficulty.Add("Score multiplier", ModManager.ScoreMultiplier(HitObjectManager.ActiveMods & ~(Mods.ScoreIncreaseMods), PlayModes.OsuMania, Beatmap).ToString("0.00", GameBase.nfi));
}
return starRating;
}
protected override bool CalculateStrainValues()
{
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
List<DifficultyHitObjectMania>.Enumerator hitObjectsEnumerator = DifficultyHitObjects.GetEnumerator();
if (!hitObjectsEnumerator.MoveNext()) return false;
DifficultyHitObjectMania currentHitObject = hitObjectsEnumerator.Current;
DifficultyHitObjectMania 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 override double CalculateDifficulty()
{
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
DifficultyHitObjectMania previousHitObject = null;
foreach (DifficultyHitObjectMania 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 individualDecay = Math.Pow(DifficultyHitObjectMania.INDIVIDUAL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
double overallDecay = Math.Pow(DifficultyHitObjectMania.OVERALL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
maximumStrain = previousHitObject.IndividualStrain * individualDecay + previousHitObject.OverallStrain * overallDecay;
}
// Go to the next time interval
intervalEndTime += actualStrainStep;
}
// Obtain maximum strain
double strain = hitObject.IndividualStrain + hitObject.OverallStrain;
maximumStrain = Math.Max(strain, 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;
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
using osu.GameModes.Edit.AiMod;
using osu.GameModes.Play.Rulesets.Mania;
using Microsoft.Xna.Framework;
using osu.GameplayElements.Beatmaps;
namespace osu.GameplayElements.HitObjects.Mania
{
internal class DifficultyHitObjectMania
{
// Factor by how much individual / overall strain decays per second. Those values are results of tweaking a lot and taking into account general feedback.
internal static readonly double INDIVIDUAL_DECAY_BASE = 0.125;
internal static readonly double OVERALL_DECAY_BASE = 0.30;
internal HitCircleMania BaseHitObject;
private double[] heldUntil;
/// <summary>
/// Measures jacks or more generally: repeated presses of the same button
/// </summary>
private double[] individualStrains;
internal double IndividualStrain
{
get
{
return individualStrains[BaseHitObject.Column];
}
set
{
individualStrains[BaseHitObject.Column] = value;
}
}
/// <summary>
/// Measures note density in a way
/// </summary>
internal double OverallStrain = 1;
internal DifficultyHitObjectMania(HitCircleMania baseHitObject)
{
BaseHitObject = baseHitObject;
int columnCount = BaseHitObject.hitObjectManager.ManiaStage.Columns.Count;
individualStrains = new double[columnCount];
heldUntil = new double[columnCount];
for (int i = 0; i < columnCount; ++i)
{
individualStrains[i] = 0;
heldUntil[i] = 0;
}
}
internal void CalculateStrains(DifficultyHitObjectMania previousHitObject, double timeRate)
{
// TODO: Factor in holds
double addition = 1.0;
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
double individualDecay = Math.Pow(INDIVIDUAL_DECAY_BASE, timeElapsed / 1000);
double overallDecay = Math.Pow(OVERALL_DECAY_BASE, timeElapsed / 1000);
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
// Fill up the heldUntil array
for (int i = 0; i < BaseHitObject.hitObjectManager.ManiaStage.Columns.Count; ++i)
{
heldUntil[i] = previousHitObject.heldUntil[i];
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (BaseHitObject.StartTime < heldUntil[i] && BaseHitObject.EndTime > heldUntil[i])
{
holdAddition = 1.0;
}
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (BaseHitObject.EndTime == heldUntil[i])
{
holdAddition = 0;
}
// We give a slight bonus to everything if something is held meanwhile
if (heldUntil[i] > BaseHitObject.EndTime)
{
holdFactor = 1.25;
}
// Decay individual strains
individualStrains[i] = previousHitObject.individualStrains[i] * individualDecay;
}
heldUntil[BaseHitObject.Column] = BaseHitObject.EndTime;
// Increase individual strain in own column
IndividualStrain += (2.0/* + (double)SpeedMania.Column / 8.0*/) * holdFactor;
OverallStrain = previousHitObject.OverallStrain * overallDecay + (addition + holdAddition) * holdFactor;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment