Exemple #1
0
        /// <summary>
        /// Correction #2 - The Next Object
        /// Estimate how next object placement affects the difficulty of hitting current object.
        /// </summary>
        private static double calculateNextObjectPlacementCorrection(MovementExtractionParameters p)
        {
            if (p.NextObject == null || p.LastToCurrent.RelativeLength == 0)
            {
                return(0);
            }

            Debug.Assert(p.CurrentToNext != null);

            double movementLengthRatio = p.LastToCurrent.TimeDelta / p.CurrentToNext.Value.TimeDelta;
            double movementAngleCosine = cosineOfAngleBetweenPairs(p.LastToCurrent, p.CurrentToNext.Value);

            if (movementLengthRatio > movement_length_ratio_threshold)
            {
                if (p.CurrentToNext.Value.RelativeLength == 0)
                {
                    return(0);
                }

                double correctionNextMoving = correction_moving_spline.Interpolate(movementAngleCosine);

                double movingness = SpecialFunctions.Logistic(p.CurrentToNext.Value.RelativeLength * 6 - 5) - SpecialFunctions.Logistic(-5);
                return(movingness * correctionNextMoving * 0.5);
            }

            if (movementLengthRatio < 1 / movement_length_ratio_threshold)
            {
                if (p.CurrentToNext.Value.RelativeLength == 0)
                {
                    return(0);
                }

                return((1 - movementAngleCosine) * SpecialFunctions.Logistic((p.CurrentToNext.Value.RelativeLength * movementLengthRatio - 1.5) * 4) * 0.15);
            }

            p.CurrentObjectTemporallyCenteredBetweenNeighbours = true;

            // rescale CurrentToNext so that it's comparable timescale-wise to LastToCurrent
            var timeNormalizedNext = p.CurrentToNext.Value.RelativeVector / p.CurrentToNext.Value.TimeDelta * p.LastToCurrent.TimeDelta;

            // transform nextObject to coordinates anchored in lastObject
            double nextTransformedX = timeNormalizedNext.DotProduct(p.LastToCurrent.RelativeVector) / p.LastToCurrent.RelativeLength;
            double nextTransformedY = (timeNormalizedNext - nextTransformedX * p.LastToCurrent.RelativeVector / p.LastToCurrent.RelativeLength).L2Norm();

            double correctionNextFlow = AngleCorrection.FLOW_NEXT.Evaluate(p.LastToCurrent.RelativeLength, nextTransformedX, nextTransformedY);
            double correctionNextSnap = AngleCorrection.SNAP_NEXT.Evaluate(p.LastToCurrent.RelativeLength, nextTransformedX, nextTransformedY);

            p.LastToNextFlowiness = SpecialFunctions.Logistic((correctionNextSnap - correctionNextFlow - 0.05) * 20);

            return(Math.Max(PowerMean.Of(correctionNextFlow, correctionNextSnap, -10) - 0.1, 0) * 0.5);
        }
Exemple #2
0
        /// <summary>
        /// Correction #1 - The Previous Object
        /// Estimate how second-last object placement affects the difficulty of hitting current object.
        /// </summary>
        private static double calculatePreviousObjectPlacementCorrection(MovementExtractionParameters p)
        {
            if (p.SecondLastObject == null || p.LastToCurrent.RelativeLength == 0)
            {
                return(0);
            }

            Debug.Assert(p.SecondLastToLast != null);

            double movementLengthRatio = p.LastToCurrent.TimeDelta / p.SecondLastToLast.Value.TimeDelta;
            double movementAngleCosine = cosineOfAngleBetweenPairs(p.SecondLastToLast.Value, p.LastToCurrent);

            if (movementLengthRatio > movement_length_ratio_threshold)
            {
                if (p.SecondLastToLast.Value.RelativeLength == 0)
                {
                    return(0);
                }

                double angleCorrection = correction_moving_spline.Interpolate(movementAngleCosine);

                double movingness = SpecialFunctions.Logistic(p.SecondLastToLast.Value.RelativeLength * 6 - 5) - SpecialFunctions.Logistic(-5);
                return(movingness * angleCorrection * 1.5);
            }

            if (movementLengthRatio < 1 / movement_length_ratio_threshold)
            {
                if (p.SecondLastToLast.Value.RelativeLength == 0)
                {
                    return(0);
                }

                return((1 - movementAngleCosine) * SpecialFunctions.Logistic((p.SecondLastToLast.Value.RelativeLength * movementLengthRatio - 1.5) * 4) * 0.3);
            }

            p.LastObjectTemporallyCenteredBetweenNeighbours = true;

            // rescale SecondLastToLast so that it's comparable timescale-wise to LastToCurrent
            var timeNormalisedSecondLastToLast = -p.SecondLastToLast.Value.RelativeVector / p.SecondLastToLast.Value.TimeDelta * p.LastToCurrent.TimeDelta;
            // transform secondLastObject to coordinates anchored in lastObject
            double secondLastTransformedX = timeNormalisedSecondLastToLast.DotProduct(p.LastToCurrent.RelativeVector) / p.LastToCurrent.RelativeLength;
            double secondLastTransformedY = (timeNormalisedSecondLastToLast - secondLastTransformedX * p.LastToCurrent.RelativeVector / p.LastToCurrent.RelativeLength).L2Norm();

            double correctionSecondLastFlow = AngleCorrection.FLOW_SECONDLAST.Evaluate(p.LastToCurrent.RelativeLength, secondLastTransformedX, secondLastTransformedY);
            double correctionSecondLastSnap = AngleCorrection.SNAP_SECONDLAST.Evaluate(p.LastToCurrent.RelativeLength, secondLastTransformedX, secondLastTransformedY);
            double correctionSecondLastStop = SpecialFunctions.Logistic(10 * Math.Sqrt(secondLastTransformedX * secondLastTransformedX + secondLastTransformedY * secondLastTransformedY + 1) - 12);

            p.SecondLastToCurrentFlowiness = SpecialFunctions.Logistic((correctionSecondLastSnap - correctionSecondLastFlow - 0.05) * 20);

            return(PowerMean.Of(new[] { correctionSecondLastFlow, correctionSecondLastSnap, correctionSecondLastStop }, -10) * 1.3);
        }
Exemple #3
0
        /// <summary>
        /// Correction #3 - 4-object pattern
        /// Estimate how the whole pattern consisting of second-last to next objects affects the difficulty of hitting current object.
        /// This only takes effect when the pattern is not so spaced (i.e. does not contain jumps)
        /// </summary>
        private static double calculateFourObjectPatternCorrection(MovementExtractionParameters p)
        {
            if (!p.LastObjectTemporallyCenteredBetweenNeighbours ||
                !p.CurrentObjectTemporallyCenteredBetweenNeighbours)
            {
                return(0);
            }

            Debug.Assert(p.CurrentToNext != null);
            Debug.Assert(p.SecondLastToLast != null);

            double gap = (p.LastToCurrent.RelativeVector - p.CurrentToNext.Value.RelativeVector / 2 - p.SecondLastToLast.Value.RelativeVector / 2).L2Norm() / (p.LastToCurrent.RelativeLength + 0.1);

            return((SpecialFunctions.Logistic((gap - 1) * 8) - SpecialFunctions.Logistic(-6)) *
                   SpecialFunctions.Logistic((p.SecondLastToLast.Value.RelativeLength - 0.7) * 10) * SpecialFunctions.Logistic((p.CurrentToNext.Value.RelativeLength - 0.7) * 10) *
                   PowerMean.Of(p.SecondLastToCurrentFlowiness, p.LastToNextFlowiness, 2) * 0.6);
        }
Exemple #4
0
        /// <summary>
        /// Calculates attributes related to tapping difficulty.
        /// </summary>
        public static TapAttributes CalculateTapAttributes(List <OsuHitObject> hitObjects, double clockRate)
        {
            var(strainHistory, tapDiff) = calculateTapStrain(hitObjects, 0, clockRate);
            double burstStrain = strainHistory.Max(v => v[0]);

            var    streamnessMask  = CalculateStreamnessMask(hitObjects, burstStrain, clockRate);
            double streamNoteCount = streamnessMask.Sum();

            var(_, mashTapDiff) = calculateTapStrain(hitObjects, 1, clockRate);

            return(new TapAttributes
            {
                TapDifficulty = tapDiff,
                StreamNoteCount = streamNoteCount,
                MashedTapDifficulty = mashTapDiff,
                StrainHistory = strainHistory.Select(x => PowerMean.Of(x, 2.0)).ToArray()
            });
        }
Exemple #5
0
        /// <summary>
        /// Calculates the strain values at each note and the maximum strain values
        /// </summary>
        private static (List <Vector <double> >, double) calculateTapStrain(List <OsuHitObject> hitObjects,
                                                                            double mashLevel,
                                                                            double clockRate)
        {
            var strainHistory = new List <Vector <double> >
            {
                Vector <double> .Build.Dense(timescale_count),
                Vector <double> .Build.Dense(timescale_count)
            };
            var currStrain = Vector <double> .Build.Dense(timescale_count);

            // compute strain at each object and store the results into strainHistory
            if (hitObjects.Count >= 2)
            {
                double prevPrevTime = hitObjects[0].StartTime / 1000.0;
                double prevTime     = hitObjects[1].StartTime / 1000.0;

                for (int i = 2; i < hitObjects.Count; i++)
                {
                    double currTime = hitObjects[i].StartTime / 1000.0;

                    // compute current strain after decay
                    currStrain = currStrain.PointwiseMultiply((-decay_coeffs * (currTime - prevTime) / clockRate).PointwiseExp());

                    strainHistory.Add(currStrain.PointwisePower(1.1 / 3) * 1.5);

                    double distance   = (hitObjects[i].StackedPosition - hitObjects[i - 1].StackedPosition).Length / (2 * hitObjects[i].Radius);
                    double spacedBuff = calculateSpacedness(distance) * spaced_buff_factor;

                    double deltaTime = Math.Max((currTime - prevPrevTime) / clockRate, 0.01);

                    // for 1/4 notes above 200 bpm the exponent is -2.7, otherwise it's -2
                    double strainAddition = Math.Max(Math.Pow(deltaTime, -2.7) * 0.265, Math.Pow(deltaTime, -2));

                    currStrain += decay_coeffs * strainAddition *
                                  Math.Pow(calculateMashNerfFactor(distance, mashLevel), 3) *
                                  Math.Pow(1 + spacedBuff, 3);

                    prevPrevTime = prevTime;
                    prevTime     = currTime;
                }
            }

            // compute difficulty by aggregating strainHistory
            var strainResult = Vector <double> .Build.Dense(timescale_count);

            for (int j = 0; j < decay_coeffs.Count; j++)
            {
                double[] singleStrainHistory = new double[hitObjects.Count];

                for (int i = 0; i < hitObjects.Count; i++)
                {
                    singleStrainHistory[i] = strainHistory[i][j];
                }

                Array.Sort(singleStrainHistory);
                Array.Reverse(singleStrainHistory);

                double singleStrainResult = 0;
                double k = 1 - 0.04 * Math.Sqrt(decay_coeffs[j]);

                for (int i = 0; i < hitObjects.Count; i++)
                {
                    singleStrainResult += singleStrainHistory[i] * Math.Pow(k, i);
                }

                strainResult[j] = singleStrainResult * (1 - k) * timescale_factors[j];
            }

            double diff = PowerMean.Of(strainResult, 2);

            return(strainHistory, diff);
        }
Exemple #6
0
        public override double Calculate(Dictionary <string, double> categoryRatings = null)
        {
            mods          = Score.Mods;
            accuracy      = Score.Accuracy;
            scoreMaxCombo = Score.MaxCombo;
            countGreat    = Score.Statistics.GetOrDefault(HitResult.Great);
            countOk       = Score.Statistics.GetOrDefault(HitResult.Ok);
            countMeh      = Score.Statistics.GetOrDefault(HitResult.Meh);
            countMiss     = Score.Statistics.GetOrDefault(HitResult.Miss);

            greatWindow = 79.5 - 6 * Attributes.OverallDifficulty;

            double multiplier = 2.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things

            // guess the number of misses + slider breaks from combo
            double comboBasedMissCount;

            if (Attributes.SliderCount == 0)
            {
                if (scoreMaxCombo < Attributes.MaxCombo)
                {
                    comboBasedMissCount = (double)Attributes.MaxCombo / scoreMaxCombo;
                }
                else
                {
                    comboBasedMissCount = 0;
                }
            }
            else
            {
                double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
                if (scoreMaxCombo < fullComboThreshold)
                {
                    comboBasedMissCount = fullComboThreshold / scoreMaxCombo;
                }
                else
                {
                    comboBasedMissCount = Math.Pow((Attributes.MaxCombo - scoreMaxCombo) / (0.1 * Attributes.SliderCount), 3);
                }
            }

            effectiveMissCount = Math.Max(countMiss, comboBasedMissCount);

            // Custom multipliers for NoFail and SpunOut.
            if (mods.Any(m => m is OsuModNoFail))
            {
                multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
            }

            if (mods.Any(m => m is OsuModSpunOut))
            {
                multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
            }

            double aimValue      = computeAimValue();
            double tapValue      = computeTapValue();
            double accuracyValue = computeAccuracyValue();

            double totalValue = PowerMean.Of(new[] { aimValue, tapValue, accuracyValue }, total_value_exponent) * multiplier;

            if (categoryRatings != null)
            {
                categoryRatings.Add("Aim", aimValue);
                categoryRatings.Add("Tap", tapValue);
                categoryRatings.Add("Accuracy", accuracyValue);
                categoryRatings.Add("OD", Attributes.OverallDifficulty);
                categoryRatings.Add("AR", Attributes.ApproachRate);
                categoryRatings.Add("Max Combo", Attributes.MaxCombo);
            }

            return(totalValue);
        }
Exemple #7
0
        private double computeAimValue()
        {
            if (Attributes.TotalObjectCount <= 1)
            {
                return(0);
            }

            // Get player's throughput according to combo
            int comboTpCount     = Attributes.ComboThroughputs.Length;
            var comboPercentages = Generate.LinearSpaced(comboTpCount, 1.0 / comboTpCount, 1);

            double scoreComboPercentage = ((double)scoreMaxCombo) / Attributes.MaxCombo;
            double comboTp = LinearSpline.InterpolateSorted(comboPercentages, Attributes.ComboThroughputs)
                             .Interpolate(scoreComboPercentage);

            // Get player's throughput according to miss count
            double missTp = LinearSpline.InterpolateSorted(Attributes.MissCounts, Attributes.MissThroughputs)
                            .Interpolate(effectiveMissCount);

            missTp = Math.Max(missTp, 0);

            // Combine combo based throughput and miss count based throughput
            double tp = PowerMean.Of(comboTp, missTp, 20);

            // Hidden mod
            if (mods.Any(h => h is OsuModHidden))
            {
                double hiddenFactor = Attributes.AimHiddenFactor;

                // the buff starts decreasing at AR9.75 and reaches 0 at AR10.75
                if (Attributes.ApproachRate > 10.75)
                {
                    hiddenFactor = 1;
                }
                else if (Attributes.ApproachRate > 9.75)
                {
                    hiddenFactor = 1 + (1 - Math.Pow(Math.Sin((Attributes.ApproachRate - 9.75) * Math.PI / 2), 2)) * (hiddenFactor - 1);
                }

                tp *= hiddenFactor;
            }

            // Account for cheesing
            double modifiedAcc      = getModifiedAcc();
            double accOnCheeseNotes = 1 - (1 - modifiedAcc) * Math.Sqrt(totalHits / Attributes.CheeseNoteCount);

            // accOnCheeseNotes can be negative. The formula below ensures a positive acc while
            // preserving the value when accOnCheeseNotes is close to 1
            double accOnCheeseNotesPositive = Math.Exp(accOnCheeseNotes - 1);
            double urOnCheeseNotes          = 10 * greatWindow / (Math.Sqrt(2) * SpecialFunctions.ErfInv(accOnCheeseNotesPositive));
            double cheeseLevel  = SpecialFunctions.Logistic(((urOnCheeseNotes * Attributes.AimDifficulty) - 3200) / 2000);
            double cheeseFactor = LinearSpline.InterpolateSorted(Attributes.CheeseLevels, Attributes.CheeseFactors)
                                  .Interpolate(cheeseLevel);

            if (mods.Any(m => m is OsuModTouchDevice))
            {
                tp = Math.Min(tp, 1.47 * Math.Pow(tp, 0.8));
            }

            double aimValue = tpToPP(tp * cheeseFactor);

            // penalize misses
            aimValue *= Math.Pow(0.96, Math.Max(effectiveMissCount - miss_count_leniency, 0));

            // Buff long maps
            aimValue *= 1 + (SpecialFunctions.Logistic((totalHits - 2800) / 500.0) - SpecialFunctions.Logistic(-2800 / 500.0)) * 0.22;

            // Buff very high AR and low AR
            double approachRateFactor = 1.0;

            if (Attributes.ApproachRate > 10)
            {
                approachRateFactor += (0.05 + 0.35 * Math.Pow(Math.Sin(Math.PI * Math.Min(totalHits, 1250) / 2500), 1.7)) *
                                      Math.Pow(Attributes.ApproachRate - 10, 2);
            }
            else if (Attributes.ApproachRate < 8.0)
            {
                approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
            }

            aimValue *= approachRateFactor;

            if (mods.Any(h => h is OsuModFlashlight))
            {
                // Apply object-based bonus for flashlight.
                aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
                            (totalHits > 200
                                ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
                             (totalHits > 500 ? (totalHits - 500) / 2000.0 : 0.0)
                                : 0.0);
            }

            // Scale the aim value down with accuracy
            double accLeniency = greatWindow * Attributes.AimDifficulty / 300;
            double accPenalty  = (0.09 / (accuracy - 1.3) + 0.3) * (accLeniency + 1.5);

            aimValue *= 0.2 + SpecialFunctions.Logistic(-((accPenalty - 0.24953) / 0.18));

            return(aimValue);
        }
Exemple #8
0
        protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
        {
            var hitObjects = beatmap.HitObjects as List <OsuHitObject>;

            double mapLength = 0;

            if (beatmap.HitObjects.Count > 0)
            {
                mapLength = (beatmap.HitObjects.Last().StartTime - beatmap.HitObjects.First().StartTime) / 1000 / clockRate;
            }

            double preemptNoClockRate = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
            var    noteDensities      = NoteDensity.Calculate(hitObjects, preemptNoClockRate);

            // Tap
            var tapAttributes = Tap.CalculateTapAttributes(hitObjects, clockRate);

            // Finger Control
            double fingerControlDiff = FingerControl.CalculateFingerControlDiff(hitObjects, clockRate);

            // Aim
            var aimAttributes = Aim.CalculateAimAttributes(hitObjects, clockRate, tapAttributes.StrainHistory, noteDensities);

            double tapStarRating           = tap_multiplier * Math.Pow(tapAttributes.TapDifficulty, star_rating_exponent);
            double aimStarRating           = aim_multiplier * Math.Pow(aimAttributes.FcProbabilityThroughput, star_rating_exponent);
            double fingerControlStarRating = finger_control_multiplier * Math.Pow(fingerControlDiff, star_rating_exponent);
            double combinedStarRating      = PowerMean.Of(new[] { tapStarRating, aimStarRating, fingerControlStarRating }, 7) * 1.131;

            HitWindows hitWindows = new OsuHitWindows();

            hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);

            // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future
            double hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate;
            double preempt        = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;

            int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
            int sliderCount     = beatmap.HitObjects.Count(h => h is Slider);
            int spinnerCount    = beatmap.HitObjects.Count(h => h is Spinner);

            int beatmapMaxCombo = beatmap.HitObjects.Count;

            // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
            beatmapMaxCombo += beatmap.HitObjects.OfType <Slider>().Sum(s => s.NestedHitObjects.Count - 1);

            return(new OsuDifficultyAttributes
            {
                StarRating = combinedStarRating,
                Mods = mods,
                Length = mapLength,

                TapStarRating = tapStarRating,
                TapDifficulty = tapAttributes.TapDifficulty,
                StreamNoteCount = tapAttributes.StreamNoteCount,
                MashTapDifficulty = tapAttributes.MashedTapDifficulty,

                FingerControlStarRating = fingerControlStarRating,
                FingerControlDifficulty = fingerControlDiff,

                AimStarRating = aimStarRating,
                AimDifficulty = aimAttributes.FcProbabilityThroughput,
                AimHiddenFactor = aimAttributes.HiddenFactor,
                ComboThroughputs = aimAttributes.ComboThroughputs,
                MissThroughputs = aimAttributes.MissThroughputs,
                MissCounts = aimAttributes.MissCounts,
                CheeseNoteCount = aimAttributes.CheeseNoteCount,
                CheeseLevels = aimAttributes.CheeseLevels,
                CheeseFactors = aimAttributes.CheeseFactors,

                ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
                OverallDifficulty = (80 - hitWindowGreat) / 6,
                MaxCombo = beatmapMaxCombo,
                TotalObjectCount = beatmap.HitObjects.Count,
                HitCircleCount = hitCirclesCount,
                SliderCount = sliderCount,
                SpinnerCount = spinnerCount
            });
        }