1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
//! https://discord.com/channels/339597420239519755/389194939881488385/735175202006237344 //! The following implementations are bit-accurate to the Etterna game code as of 2020-07-21 fn is_rating_okay(rating: f32, ssrs: &[f32], delta_multiplier: f32) -> bool { // Notice the somewhat peculiar usage of f32 and f64 in here. That's to mirror the C++ // implementation as closely as possible - we thrive for bit-accuracy after all let max_power_sum: f64 = 2f64.powf(rating as f64 * 0.1); let power_sum: f64 = ssrs .iter() .map(|&ssr| (2.0 / libm::erfcf(delta_multiplier * (ssr - rating)) - 2.0) as f64) .filter(|&x| x > 0.0) .sum(); power_sum < max_power_sum } /* The idea is the following: we try out potential skillset rating values until we've found the lowest rating that still fits (I've called that property 'okay'-ness in the code). How do we know whether a potential skillset rating fits? We give each score a "power level", which is larger when the skillset rating of the specific score is high. Therefore, the user's best scores get the highest power levels. Now, we sum the power levels of each score and check whether that sum is below a certain limit. If it is still under the limit, the rating fits (is 'okay'), and we can try a higher rating. If the sum is above the limit, the rating doesn't fit, and we need to try out a lower rating. */ fn calc_rating(ssrs: &[f32], final_multiplier: f32, delta_multiplier: f32) -> f32 { let num_iters: u32 = 11; // if needed, make this a parameter in the future let mut rating: f32 = 0.0; let mut resolution: f32 = 10.24; // Repeatedly approximate the final rating, with better resolution // each time for _ in 0..num_iters { // Find lowest 'okay' rating with certain resolution while !is_rating_okay(rating + resolution, ssrs, delta_multiplier) { rating += resolution; } // Now, repeat with smaller resolution for better approximation resolution /= 2.0; } // Always be ever so slightly above the target value instead of below rating += resolution * 2.0; rating * final_multiplier } /// Calculate a score's overall difficulty from the score's seven individual skillsets. /// /// `AggregateRatings` in Etterna game code: /// https://github.com/etternagame/etterna/blob/0b7a28d2371798a8138e78e5789d0014b16b4534/src/Etterna/MinaCalc/MinaCalc.cpp#L194-L199, /// https://github.com/etternagame/etterna/blob/0b7a28d2371798a8138e78e5789d0014b16b4534/src/Etterna/MinaCalc/MinaCalcHelpers.h#L40-L58 pub fn calculate_score_overall(skillsets: &[f32; 7]) -> f32 { calc_rating(skillsets, 1.11, 0.25) } /// Calculate a player's skillset rating from the individual scores' skillset ratings /// /// `AggregateSSRs` in Etterna game code: /// https://github.com/etternagame/etterna/blob/0b7a28d2371798a8138e78e5789d0014b16b4534/src/Etterna/Singletons/ScoreManager.cpp#L808-L837 pub fn calculate_player_skillset_rating(ssrs: &[f32]) -> f32 { calc_rating(ssrs, 1.05, 0.1) } /// This is the pre-0.70 variant of [`calculate_player_skillset_rating`]. pub fn calculate_player_skillset_rating_pre_070(ssrs: &[f32]) -> f32 { calc_rating(ssrs, 1.04, 0.1) } /// Calculate a player's overall rating from the player's seven individual skillset ratings. /// /// `AggregateSkillsets` in Etterna game code: /// https://github.com/etternagame/etterna/blob/0b7a28d2371798a8138e78e5789d0014b16b4534/src/Etterna/Singletons/ScoreManager.cpp#L763-L806 pub fn calculate_player_overall(skillsets: &[f32; 7]) -> f32 { calc_rating(skillsets, 1.125, 0.1) } #[cfg(test)] mod tests { use super::*; #[test] fn test_everything() { // These test values come from a C++ program containing the actual algorithms from Etterna // itself, only slightly modified to be standalone #[allow(clippy::excessive_precision)] // yes, we're being excessively precise around here let test_values: &[([f32; 7], f32, f32, f32, f32)] = &[ ( [21.0, 24.0, 23.0, 14.0, 17.0, 25.0, 24.0], 25.27470016, 21.94499779, 21.73599815, 23.51249886, ), ( [25.0, 23.0, 30.0, 30.0, 17.0, 25.0, 24.0], 30.51390076, 25.70400047, 25.45919991, 27.54000092, ), ( [26.0, 23.0, 29.0, 15.0, 19.0, 22.0, 25.0], 28.62689972, 24.01350021, 23.78479958, 25.72875023, ), ( [23.0, 24.0, 24.0, 23.0, 25.0, 24.0, 23.0], 25.46340179, 22.68000031, 22.46399879, 24.30000114, ), ( [10.0, 100.0, 42.0, 69.0, 3.0, 88.0, 50.0], 101.82029724, 85.09198761, 84.28159332, 91.16999054, ), ]; #[allow(clippy::float_cmp)] // yeah, I am doing == with floats. That's because I thrive for bit-perfect accuracy // on these functions. for &(numbers, s_overall, p_ss, p_ss_old, p_overall) in test_values { assert_eq!(calculate_score_overall(&numbers), s_overall); assert_eq!(calculate_player_skillset_rating(&numbers), p_ss); assert_eq!(calculate_player_skillset_rating_pre_070(&numbers), p_ss_old); assert_eq!(calculate_player_overall(&numbers), p_overall); } } }