private static string generateGraphText(List <OsuMovement> movements, double tp) { var sw = new StringWriter(); foreach (var movement in movements) { double time = movement.Time; double ipRaw = movement.IP12; double ipCorrected = FittsLaw.CalculateIP(movement.D, movement.MT * (1 + defaultCheeseLevel * movement.CheesableRatio)); double missProb = 1 - calculateCheeseHitProb(movement, tp, defaultCheeseLevel); sw.WriteLine($"{time} {ipRaw} {ipCorrected} {missProb}"); } string graphText = sw.ToString(); sw.Dispose(); return(graphText); }
public static List <OsuMovement> ExtractMovement(OsuHitObject obj0, OsuHitObject obj1, OsuHitObject obj2, OsuHitObject obj3, Vector <double> tapStrain, double clockRate) { var movement = new OsuMovement(); double t12 = (obj2.StartTime - obj1.StartTime) / clockRate / 1000.0; movement.RawMT = t12; movement.Time = obj2.StartTime / 1000.0; if (obj2 is Spinner || obj1 is Spinner) { movement.IP12 = 0; movement.D = 0; movement.MT = 1; movement.Cheesablility = 0; movement.CheesableRatio = 0; return(new List <OsuMovement>() { movement }); } if (obj0 is Spinner) { obj0 = null; } if (obj3 is Spinner) { obj3 = null; } var pos1 = Vector <double> .Build.Dense(new[] { (double)obj1.Position.X, (double)obj1.Position.Y }); var pos2 = Vector <double> .Build.Dense(new[] { (double)obj2.Position.X, (double)obj2.Position.Y }); var s12 = (pos2 - pos1) / (2 * obj2.Radius); double d12 = s12.L2Norm(); double IP12 = FittsLaw.CalculateIP(d12, t12); movement.IP12 = IP12; var s01 = Vector <double> .Build.Dense(2); var s23 = Vector <double> .Build.Dense(2); double d01 = 0; double d23 = 0; double t01 = 0; double t23 = 0; double flowiness012 = 0; double flowiness123 = 0; bool obj1InTheMiddle = false; bool obj2InTheMiddle = false; // Correction #1 - The Previous Object // Estimate how obj0 affects the difficulty of hitting obj2 double correction0 = 0; if (obj0 != null) { var pos0 = Vector <double> .Build.Dense(new[] { (double)obj0.Position.X, (double)obj0.Position.Y }); s01 = (pos1 - pos0) / (2 * obj2.Radius); d01 = s01.L2Norm(); t01 = (obj1.StartTime - obj0.StartTime) / clockRate / 1000.0; if (d12 != 0) { double tRatio0 = t12 / t01; if (tRatio0 > tRatioThreshold) { if (d01 == 0) { correction0 = correction0Still; } else { double cos012 = Math.Min(Math.Max(-s01.DotProduct(s12) / d01 / d12, -1), 1); double correction0_moving = correction0MovingSpline.Interpolate(cos012); double movingness = SpecialFunctions.Logistic(d01 * 2) * 2 - 1; correction0 = (movingness * correction0_moving + (1 - movingness) * correction0Still) * 1.5; } } else if (tRatio0 < 1 / tRatioThreshold) { if (d01 == 0) { correction0 = 0; } else { double cos012 = Math.Min(Math.Max(-s01.DotProduct(s12) / d01 / d12, -1), 1); correction0 = (1 - cos012) * SpecialFunctions.Logistic((d01 * tRatio0 - 1.5) * 4) * 0.3; } } else { obj1InTheMiddle = true; var normalized_pos0 = -s01 / t01 * t12; double x0 = normalized_pos0.DotProduct(s12) / d12; double y0 = (normalized_pos0 - x0 * s12 / d12).L2Norm(); double correction0Flow = calcCorrection0Or3(d12, x0, y0, k0fInterp, scale0fInterp, coeffs0fInterps); double correction0Snap = calcCorrection0Or3(d12, x0, y0, k0sInterp, scale0sInterp, coeffs0sInterps); double correction0Stop = calcCorrection0Stop(d12, x0, y0); flowiness012 = SpecialFunctions.Logistic((correction0Snap - correction0Flow - 0.05) * 20); correction0 = Mean.PowerMean(new double[] { correction0Flow, correction0Snap, correction0Stop }, -10) * 1.3; //Console.Write(obj2.StartTime + " "); //Console.Write(correction0Flow.ToString("N3") + " "); //Console.Write(correction0Snap.ToString("N3") + " "); //Console.Write(correction0Stop.ToString("N3") + " "); //Console.Write(correction0.ToString("N3") + " "); //Console.WriteLine(); } } } // Correction #2 - The Next Object // Estimate how obj3 affects the difficulty of hitting obj2 double correction3 = 0; if (obj3 != null) { var pos3 = Vector <double> .Build.Dense(new[] { (double)obj3.Position.X, (double)obj3.Position.Y }); s23 = (pos3 - pos2) / (2 * obj2.Radius); d23 = s23.L2Norm(); t23 = (obj3.StartTime - obj2.StartTime) / clockRate / 1000.0; if (d12 != 0) { double tRatio3 = t12 / t23; if (tRatio3 > tRatioThreshold) { if (d23 == 0) { correction3 = 0; } else { double cos123 = Math.Min(Math.Max(-s12.DotProduct(s23) / d12 / d23, -1), 1); double correction3_moving = correction0MovingSpline.Interpolate(cos123); double movingness = SpecialFunctions.Logistic(d23 * 6 - 5) - SpecialFunctions.Logistic(-5); correction3 = (movingness * correction3_moving) * 0.5; } } else if (tRatio3 < 1 / tRatioThreshold) { if (d23 == 0) { correction3 = 0; } else { double cos123 = Math.Min(Math.Max(-s12.DotProduct(s23) / d12 / d23, -1), 1); correction3 = (1 - cos123) * SpecialFunctions.Logistic((d23 * tRatio3 - 1.5) * 4) * 0.15; } } else { obj2InTheMiddle = true; var normalizedPos3 = s23 / t23 * t12; double x3 = normalizedPos3.DotProduct(s12) / d12; double y3 = (normalizedPos3 - x3 * s12 / d12).L2Norm(); double correction3Flow = calcCorrection0Or3(d12, x3, y3, k3fInterp, scale3fInterp, coeffs3fInterps); double correction3Snap = calcCorrection0Or3(d12, x3, y3, k3sInterp, scale3sInterp, coeffs3sInterps); flowiness123 = SpecialFunctions.Logistic((correction3Snap - correction3Flow - 0.05) * 20); correction3 = Math.Max(Mean.PowerMean(correction3Flow, correction3Snap, -10) - 0.1, 0) * 0.5; } } } // Correction #3 - 4-object pattern // Estimate how the whole pattern consisting of obj0 to obj3 affects // the difficulty of hitting obj2. This only takes effect when the pattern // is not so spaced (i.e. does not contain jumps) double patternCorrection = 0; if (obj1InTheMiddle && obj2InTheMiddle) { double gap = (s12 - s23 / 2 - s01 / 2).L2Norm() / (d12 + 0.1); patternCorrection = (SpecialFunctions.Logistic((gap - 0.75) * 8) - SpecialFunctions.Logistic(-6)) * SpecialFunctions.Logistic((d01 - 0.7) * 10) * SpecialFunctions.Logistic((d23 - 0.7) * 10) * Mean.PowerMean(flowiness012, flowiness123, 2) * 0.6; //patternCorrection = 0; } // Correction #4 - Tap Strain // Estimate how tap strain affects difficulty double tapCorrection = 0; if (d12 > 0 && tapStrain != null) { tapCorrection = SpecialFunctions.Logistic((Mean.PowerMean(tapStrain, 2) / IP12 - 1.34) / 0.1) * 0.25; } // Correction #5 - Cheesing // The player might make the movement of obj1 -> obj2 easier by // hitting obj1 early and obj2 late. Here we estimate the amount of // cheesing and update MT accordingly. double timeEarly = 0; double timeLate = 0; double cheesabilityEarly = 0; double cheesabilityLate = 0; if (d12 > 0) { double t01Reciprocal; double ip01; if (obj0 != null) { t01Reciprocal = 1 / (t01 + 1e-10); ip01 = FittsLaw.CalculateIP(d01, t01); } else { t01Reciprocal = 0; ip01 = 0; } cheesabilityEarly = SpecialFunctions.Logistic((ip01 / IP12 - 0.6) * (-15)) * 0.5; timeEarly = cheesabilityEarly * (1 / (1 / (t12 + 0.07) + t01Reciprocal)); double t23Reciprocal; double ip23; if (obj3 != null) { t23Reciprocal = 1 / (t23 + 1e-10); ip23 = FittsLaw.CalculateIP(d23, t23); } else { t23Reciprocal = 0; ip23 = 0; } cheesabilityLate = SpecialFunctions.Logistic((ip23 / IP12 - 0.6) * (-15)) * 0.5; timeLate = cheesabilityLate * (1 / (1 / (t12 + 0.07) + t23Reciprocal)); } // Correction #6 - Small circle bonus double smallCircleBonus = SpecialFunctions.Logistic((55 - 2 * obj2.Radius) / 3.0) * 0.3; // Correction #7 - Stacked notes nerf double stackedThreshold = 0.8; double d12StackedNerf; if (d12 < stackedThreshold) { d12StackedNerf = Math.Max(1.4 * (d12 - stackedThreshold) + stackedThreshold, 0); } else { d12StackedNerf = d12; } // Apply the corrections double d12WithCorrection = d12StackedNerf * (1 + smallCircleBonus) * (1 + correction0 + correction3 + patternCorrection) * (1 + tapCorrection); movement.D = d12WithCorrection; movement.MT = t12; movement.Cheesablility = cheesabilityEarly + cheesabilityLate; movement.CheesableRatio = (timeEarly + timeLate) / (t12 + 1e-10); var movementWithNested = new List <OsuMovement>() { movement }; // add zero difficulty movements corresponding to slider ticks/slider ends so combo is reflected properly int extraNestedCount = obj2.NestedHitObjects.Count - 1; for (int i = 0; i < extraNestedCount; i++) { movementWithNested.Add(GetEmptyMovement(movement.Time)); } return(movementWithNested); }