Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworked hit result generator based on accuracy #198

Merged
merged 8 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 62 additions & 13 deletions PerformanceCalculator/Simulate/OsuSimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,68 @@ protected override Dictionary<HitResult, int> 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<HitResult, int>
Expand Down
75 changes: 62 additions & 13 deletions PerformanceCalculatorGUI/RulesetHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,68 @@ private static Dictionary<HitResult, int> 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<HitResult, int>
Expand Down
Loading