private double CalculateDifficulty(DifficultyType Type) { // Find the highest strain value within each strain step List <double> HighestStrains = new List <double>(); double IntervalEndTime = STRAIN_STEP; double MaximumStrain = 0; // We need to keep track of the maximum strain in the current interval tpHitObject PreviousHitObject = null; foreach (tpHitObject hitObject in tpHitObjects) { // 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(tpHitObject.DECAY_BASE[(int)Type], (double)(IntervalEndTime - PreviousHitObject.BaseHitObject.StartTime) / 1000); MaximumStrain = PreviousHitObject.Strains[(int)Type] * Decay; } // Go to the next time interval IntervalEndTime += STRAIN_STEP; } // Obtain maximum strain if (hitObject.Strains[(int)Type] > MaximumStrain) { MaximumStrain = hitObject.Strains[(int)Type]; } 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); }
private void CalculateSpecificStrain(tpHitObject PreviousHitObject, AiModtpDifficulty.DifficultyType Type) { double Addition = 0; double TimeElapsed = BaseHitObject.StartTime - PreviousHitObject.BaseHitObject.StartTime; double Decay = Math.Pow(DECAY_BASE[(int)Type], TimeElapsed / 1000); if ((BaseHitObject.Type & HitObjectType.Spinner) > 0) { // Do nothing for spinners } else if ((BaseHitObject.Type & HitObjectType.Slider) > 0) { switch (Type) { case AiModtpDifficulty.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.LazySliderLengthFirst + PreviousHitObject.LazySliderLengthSubsequent * (PreviousHitObject.BaseHitObject.SegmentCount - 1) + DistanceTo(PreviousHitObject), Type) * SPACING_WEIGHT_SCALING[(int)Type]; break; case AiModtpDifficulty.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.LazySliderLengthFirst, Type) + SpacingWeight(PreviousHitObject.LazySliderLengthSubsequent, Type) * (PreviousHitObject.BaseHitObject.SegmentCount - 1) + SpacingWeight(DistanceTo(PreviousHitObject), Type) ) * SPACING_WEIGHT_SCALING[(int)Type]; break; } } else if ((BaseHitObject.Type & HitObjectType.Normal) > 0) { 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; }
// Exceptions would be nicer to handle errors, but for this small project it shall be ignored. private bool CalculateStrainValues() { // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. List <tpHitObject> .Enumerator HitObjectsEnumerator = tpHitObjects.GetEnumerator(); if (HitObjectsEnumerator.MoveNext() == false) { Reports.Add(new AiReport(Severity.Info, "Can not compute difficulty of empty beatmap.")); return(false); } tpHitObject CurrentHitObject = HitObjectsEnumerator.Current; tpHitObject NextHitObject; // First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See tpHitObject. while (HitObjectsEnumerator.MoveNext()) { NextHitObject = HitObjectsEnumerator.Current; NextHitObject.CalculateStrains(CurrentHitObject); CurrentHitObject = NextHitObject; } return(true); }
private void CalculateSpecificStrain(tpHitObject PreviousHitObject, AiModtpDifficulty.DifficultyType Type) { double Addition = 0; double TimeElapsed = BaseHitObject.StartTime - PreviousHitObject.BaseHitObject.StartTime; double Decay = Math.Pow(DECAY_BASE[(int)Type], TimeElapsed / 1000); if ((BaseHitObject.Type & HitObjectType.Spinner) > 0) { // Do nothing for spinners } else if ((BaseHitObject.Type & HitObjectType.Slider) > 0) { switch(Type) { case AiModtpDifficulty.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.LazySliderLengthFirst + PreviousHitObject.LazySliderLengthSubsequent * (PreviousHitObject.BaseHitObject.SegmentCount - 1) + DistanceTo(PreviousHitObject), Type) * SPACING_WEIGHT_SCALING[(int)Type]; break; case AiModtpDifficulty.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.LazySliderLengthFirst, Type) + SpacingWeight(PreviousHitObject.LazySliderLengthSubsequent, Type) * (PreviousHitObject.BaseHitObject.SegmentCount - 1) + SpacingWeight(DistanceTo(PreviousHitObject), Type) ) * SPACING_WEIGHT_SCALING[(int)Type]; break; } } else if ((BaseHitObject.Type & HitObjectType.Normal) > 0) { 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; }
public double DistanceTo(tpHitObject other) { // Scale the distance by circle size. return (NormalizedStartPosition - other.NormalizedEndPosition).Length(); }
public void CalculateStrains(tpHitObject PreviousHitObject) { CalculateSpecificStrain(PreviousHitObject, AiModtpDifficulty.DifficultyType.Speed); CalculateSpecificStrain(PreviousHitObject, AiModtpDifficulty.DifficultyType.Aim); }
public double DistanceTo(tpHitObject other) { // Scale the distance by circle size. return((NormalizedStartPosition - other.NormalizedEndPosition).Length()); }