diff --git a/PerformanceCalculator/Simulate/OsuSimulateCommand.cs b/PerformanceCalculator/Simulate/OsuSimulateCommand.cs index 3fdf0f307..2f8954511 100644 --- a/PerformanceCalculator/Simulate/OsuSimulateCommand.cs +++ b/PerformanceCalculator/Simulate/OsuSimulateCommand.cs @@ -62,19 +62,68 @@ protected override Dictionary GenerateHitResults(double accuracy } else { - // Let Great=6, Good=2, Meh=1, Miss=0. The total should be this. - var targetTotal = (int)Math.Round(accuracy * totalResultCount * 6); - - // Start by assuming every non miss is a meh - // This is how much increase is needed by greats and goods - var delta = targetTotal - (totalResultCount - countMiss); - - // Each great increases total by 5 (great-meh=5) - countGreat = delta / 5; - // Each good increases total by 1 (good-meh=1). Covers remaining difference. - countGood = delta % 5; - // Mehs are left over. Could be negative if impossible value of amountMiss chosen - countMeh = totalResultCount - countGreat - countGood - countMiss; + // Total result count excluding countMiss + int relevantResultCount = totalResultCount - countMiss; + + // Accuracy excluding countMiss. We need that because we're trying to achieve target accuracy without touching countMiss + // So it's better to pretened that there were 0 misses in the 1st place + double relevantAccuracy = accuracy * totalResultCount / relevantResultCount; + + // Clamp accuracy to account for user trying to break the algorithm by inputting impossible values + relevantAccuracy = Math.Clamp(relevantAccuracy, 0, 1); + + // Main curve for accuracy > 25%, the closer accuracy is to 25% - the more 50s it adds + if (relevantAccuracy >= 0.25) + { + // Main curve. Zero 50s if accuracy is 100%, one 50 per 9 100s if accuracy is 75% (excluding misses), 4 50s per 9 100s if accuracy is 50% + double ratio50To100 = Math.Pow(1 - (relevantAccuracy - 0.25) / 0.75, 2); + + // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c50 = c100 * ratio50to100 + double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4); + + // Get count50 according to c50 = c100 * ratio50to100 + double count50Estimate = count100Estimate * ratio50To100; + + // Round it to get int number of 100s + countGood = (int?)Math.Round(count100Estimate); + + // Get number of 50s as difference between total mistimed hits and count100 + countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood); + } + // If accuracy is between 16.67% and 25% - we assume that we have no 300s + else if (relevantAccuracy >= 1.0 / 6) + { + // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = 0 + double count100Estimate = 6 * relevantResultCount * relevantAccuracy - relevantResultCount; + + // We only had 100s and 50s in that scenario so rest of the hits are 50s + double count50Estimate = relevantResultCount - count100Estimate; + + // Round it to get int number of 100s + countGood = (int?)Math.Round(count100Estimate); + + // Get number of 50s as difference between total mistimed hits and count100 + countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood); + } + // If accuracy is less than 16.67% - it means that we have only 50s or misses + // Assuming that we removed misses in the 1st place - that means that we need to add additional misses to achieve target accuracy + else + { + // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = c100 = 0 + double count50Estimate = 6 * relevantResultCount * relevantAccuracy; + + // We have 0 100s, because we can't start adding 100s again after reaching "only 50s" point + countGood = 0; + + // Round it to get int number of 50s + countMeh = (int?)Math.Round(count50Estimate); + + // Fill the rest results with misses overwriting initial countMiss + countMiss = (int)(totalResultCount - countMeh); + } + + // Rest of the hits are 300s + countGreat = (int)(totalResultCount - countGood - countMeh - countMiss); } return new Dictionary diff --git a/PerformanceCalculatorGUI/RulesetHelper.cs b/PerformanceCalculatorGUI/RulesetHelper.cs index c99ad0b9d..66c79229e 100644 --- a/PerformanceCalculatorGUI/RulesetHelper.cs +++ b/PerformanceCalculatorGUI/RulesetHelper.cs @@ -127,19 +127,68 @@ private static Dictionary generateOsuHitResults(double accuracy, } else { - // Let Great=6, Good=2, Meh=1, Miss=0. The total should be this. - var targetTotal = (int)Math.Round(accuracy * totalResultCount * 6); - - // Start by assuming every non miss is a meh - // This is how much increase is needed by greats and goods - var delta = targetTotal - (totalResultCount - countMiss); - - // Each great increases total by 5 (great-meh=5) - countGreat = delta / 5; - // Each good increases total by 1 (good-meh=1). Covers remaining difference. - countGood = delta % 5; - // Mehs are left over. Could be negative if impossible value of amountMiss chosen - countMeh = totalResultCount - countGreat - countGood - countMiss; + // Total result count excluding countMiss + int relevantResultCount = totalResultCount - countMiss; + + // Accuracy excluding countMiss. We need that because we're trying to achieve target accuracy without touching countMiss + // So it's better to pretened that there were 0 misses in the 1st place + double relevantAccuracy = accuracy * totalResultCount / relevantResultCount; + + // Clamp accuracy to account for user trying to break the algorithm by inputting impossible values + relevantAccuracy = Math.Clamp(relevantAccuracy, 0, 1); + + // Main curve for accuracy > 25%, the closer accuracy is to 25% - the more 50s it adds + if (relevantAccuracy >= 0.25) + { + // Main curve. Zero 50s if accuracy is 100%, one 50 per 9 100s if accuracy is 75% (excluding misses), 4 50s per 9 100s if accuracy is 50% + double ratio50To100 = Math.Pow(1 - (relevantAccuracy - 0.25) / 0.75, 2); + + // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c50 = c100 * ratio50to100 + double count100Estimate = 6 * relevantResultCount * (1 - relevantAccuracy) / (5 * ratio50To100 + 4); + + // Get count50 according to c50 = c100 * ratio50to100 + double count50Estimate = count100Estimate * ratio50To100; + + // Round it to get int number of 100s + countGood = (int?)Math.Round(count100Estimate); + + // Get number of 50s as difference between total mistimed hits and count100 + countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood); + } + // If accuracy is between 16.67% and 25% - we assume that we have no 300s + else if (relevantAccuracy >= 1.0 / 6) + { + // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = 0 + double count100Estimate = 6 * relevantResultCount * relevantAccuracy - relevantResultCount; + + // We only had 100s and 50s in that scenario so rest of the hits are 50s + double count50Estimate = relevantResultCount - count100Estimate; + + // Round it to get int number of 100s + countGood = (int?)Math.Round(count100Estimate); + + // Get number of 50s as difference between total mistimed hits and count100 + countMeh = (int?)(Math.Round(count100Estimate + count50Estimate) - countGood); + } + // If accuracy is less than 16.67% - it means that we have only 50s or misses + // Assuming that we removed misses in the 1st place - that means that we need to add additional misses to achieve target accuracy + else + { + // Derived from the formula: Accuracy = (6 * c300 + 2 * c100 + c50) / (6 * totalHits), assuming that c300 = c100 = 0 + double count50Estimate = 6 * relevantResultCount * relevantAccuracy; + + // We have 0 100s, because we can't start adding 100s again after reaching "only 50s" point + countGood = 0; + + // Round it to get int number of 50s + countMeh = (int?)Math.Round(count50Estimate); + + // Fill the rest results with misses overwriting initial countMiss + countMiss = (int)(totalResultCount - countMeh); + } + + // Rest of the hits are 300s + countGreat = (int)(totalResultCount - countGood - countMeh - countMiss); } return new Dictionary