From e4cfb6ff89d1bda459a676d32dce5eb51523418f Mon Sep 17 00:00:00 2001 From: minisbett <39670899+minisbett@users.noreply.github.com> Date: Wed, 4 Dec 2024 20:42:46 +0100 Subject: [PATCH 1/4] Update PP for std, catch and mania --- aiosu/models/beatmap.py | 2 + aiosu/models/score.py | 2 + aiosu/utils/performance.py | 158 +++++++++++++++++++++---------------- 3 files changed, 94 insertions(+), 68 deletions(-) diff --git a/aiosu/models/beatmap.py b/aiosu/models/beatmap.py index d311df8..9be3675 100644 --- a/aiosu/models/beatmap.py +++ b/aiosu/models/beatmap.py @@ -368,6 +368,8 @@ class BeatmapDifficultyAttributes(BaseModel): slider_factor: Optional[float] = None speed_difficulty: Optional[float] = None speed_note_count: Optional[float] = None + aim_difficult_strain_count: Optional[float] = None + speed_difficult_strain_count: Optional[float] = None # osu taiko stamina_difficulty: Optional[float] = None rhythm_difficulty: Optional[float] = None diff --git a/aiosu/models/score.py b/aiosu/models/score.py index efa4a4c..9586231 100644 --- a/aiosu/models/score.py +++ b/aiosu/models/score.py @@ -114,6 +114,8 @@ class ScoreStatistics(BaseModel): count_300: int count_geki: int count_katu: int + count_large_tick_miss: Optional[int] = None + count_slider_tail_hit: Optional[int] = None @model_validator(mode="before") @classmethod diff --git a/aiosu/utils/performance.py b/aiosu/utils/performance.py index ecf37a5..8db4b7e 100644 --- a/aiosu/utils/performance.py +++ b/aiosu/utils/performance.py @@ -33,10 +33,8 @@ "TaikoPerformanceCalculator", ] -OSU_BASE_MULTIPLIER = 1.14 +OSU_BASE_MULTIPLIER = 1.15 TAIKO_BASE_MULTIPLIER = 1.13 -MANIA_BASE_MULTIPLIER = 8.0 -CATCH_BASE_MULTIPLIER = 1.0 clamp: Callable[[float, float, float], float] = lambda x, l, u: ( l if x < l else u if x > u else x @@ -81,6 +79,9 @@ class OsuPerformanceCalculator(AbstractPerformanceCalculator): :type difficulty_attributes: BeatmapDifficultyAttributes """ + def _is_slider_head_accuracy(self, Score) -> bool: + return True + def calculate(self, score: Score) -> OsuPerformanceAttributes: r"""Calculates performance points for a score. @@ -93,13 +94,32 @@ def calculate(self, score: Score) -> OsuPerformanceAttributes: if score.beatmap is None: raise ValueError("Given score does not have a beatmap.") - effective_miss_count = self._calculate_effective_miss_count(score) total_hits = ( score.statistics.count_300 + score.statistics.count_100 + score.statistics.count_50 + score.statistics.count_miss ) + effective_miss_count = score.statistics.count_miss + + if score.beatmap.count_sliders > 0: # type: ignore + if self._is_slider_head_accuracy(score): + full_combo_threshold = self.difficulty_attributes.max_combo - 0.1 * score.beatmap.count_sliders # type: ignore + + if score.max_combo < full_combo_threshold: + effective_miss_count = full_combo_threshold / max(1, score.max_combo) + + effective_miss_count = min(effective_miss_count, score.statistics.count_100 + score.statistics.count_100 + score.statistics.count_miss) + else: + full_combo_threshold = self.difficulty_attributes.max_combo - (score.beatmap.count_sliders - score.statistics.count_slider_tail_hit) # type: ignore + + if score.max_combo < full_combo_threshold: + effective_miss_count = full_combo_threshold / max(1, score.max_combo) + + effective_miss_count = min(effective_miss_count, score.statistics.count_large_tick_miss + score.statistics.count_miss) # type: ignore + + effective_miss_count = clamp(effective_miss_count, score.statistics.count_miss, total_hits) + multiplier = OSU_BASE_MULTIPLIER @@ -112,6 +132,19 @@ def calculate(self, score: Score) -> OsuPerformanceAttributes: 0.85, ) + if Mod.Relax in score.mods: + adjusted_od = self.difficulty_attributes.overall_difficulty / 13.33 # type: ignore + ok_multiplier = max(0, (1 - pow(adjusted_od, 1.8)) if self.difficulty_attributes.overall_difficulty > 0 else 1) # type: ignore + meh_multiplier = max(0, (1 - pow(adjusted_od, 5)) if self.difficulty_attributes.overall_difficulty > 0 else 1) # type: ignore + + effective_miss_count = min( + effective_miss_count + + score.statistics.count_100 * ok_multiplier + + score.statistics.count_50 * meh_multiplier, + total_hits + ) + + aim_value = self._compute_aim_value(score, effective_miss_count, total_hits) speed_value = self._compute_speed_value(score, effective_miss_count, total_hits) accuracy_value = self._compute_accuracy_value(score, total_hits) @@ -164,12 +197,7 @@ def _compute_aim_value( aim_value *= length_bonus if effective_miss_count > 0: - aim_value *= 0.97 * math.pow( - 1 - math.pow(effective_miss_count / total_hits, 0.775), - effective_miss_count, - ) - - aim_value *= self._get_combo_scaling_factor(score) + aim_value *= self._calculate_miss_penalty(effective_miss_count, self.difficulty_attributes.aim_difficult_strain_count) # type: ignore approach_rate_factor = 0.0 if self.difficulty_attributes.approach_rate > 10.33: # type: ignore @@ -186,30 +214,33 @@ def _compute_aim_value( if Mod.Hidden in score.mods: aim_value *= 1.0 + 0.04 * (12.0 - self.difficulty_attributes.approach_rate) # type: ignore + estimate_difficult_sliders = score.beatmap.count_sliders * 0.15 # type: ignore + if score.beatmap.count_sliders > 0: # type: ignore - estimate_difficult_sliders = score.beatmap.count_sliders * 0.15 # type: ignore + estimate_improperly_followed_difficult_sliders = 0 + + if self._is_slider_head_accuracy(score): + maximum_possible_dropped_sliders = score.statistics.count_100 + score.statistics.count_50 + score.statistics.count_miss + estimate_improperly_followed_difficult_sliders = clamp( + min(maximum_possible_dropped_sliders, self.difficulty_attributes.max_combo - score.max_combo), + 0, + estimate_difficult_sliders + ) + else: + estimate_improperly_followed_difficult_sliders = clamp( + score.beatmap.count_sliders - score.statistics.count_slider_tail_hit + score.statistics.count_large_tick_miss, # type: ignore + 0, + estimate_difficult_sliders + ) - estimate_slider_ends_dropped = clamp( - min( - score.statistics.count_100 - + score.statistics.count_50 - + score.statistics.count_miss, - self.difficulty_attributes.max_combo - score.max_combo, - ), - 0, - estimate_difficult_sliders, + slider_nerf_factor = ( + (1 - self.difficulty_attributes.slider_factor) # type: ignore + * pow(1 - estimate_improperly_followed_difficult_sliders / estimate_difficult_sliders, 3) + + self.difficulty_attributes.slider_factor ) - - slider_nerf_factor = ( # type: ignore - 1 - self.difficulty_attributes.slider_factor # type: ignore - ) * math.pow( - 1 - estimate_slider_ends_dropped / estimate_difficult_sliders, - 3, - ) + self.difficulty_attributes.slider_factor - aim_value *= slider_nerf_factor - accuracy = score.accuracy if score.accuracy <= 1.0 else score.accuracy / 100 + accuracy = score.accuracy aim_value *= accuracy aim_value *= ( 0.98 + math.pow(self.difficulty_attributes.overall_difficulty, 2) / 2500 # type: ignore @@ -223,6 +254,9 @@ def _compute_speed_value( effective_miss_count: float, total_hits: int, ) -> float: + if Mod.Relax in score.mods: + return 0 + speed_value = ( math.pow( 5.0 * max(1.0, self.difficulty_attributes.speed_difficulty / 0.0675) # type: ignore @@ -235,17 +269,12 @@ def _compute_speed_value( length_bonus = ( 0.95 + 0.4 * min(1.0, total_hits / 2000.0) - + ((math.log10(total_hits / 2000.0) * 0.5) * int(total_hits > 2000)) + + (((math.log10(total_hits / 2000.0) * 0.5) * int(total_hits > 2000)) if total_hits > 0 else 0) ) speed_value *= length_bonus if effective_miss_count > 0: - speed_value *= 0.97 * math.pow( - 1 - math.pow(effective_miss_count / total_hits, 0.775), - math.pow(effective_miss_count, 0.875), - ) - - speed_value *= self._get_combo_scaling_factor(score) + speed_value *= self._calculate_miss_penalty(effective_miss_count, self.difficulty_attributes.speed_difficult_strain_count) # type: ignore approach_rate_factor = 0.0 if self.difficulty_attributes.approach_rate > 10.33: # type: ignore @@ -255,6 +284,9 @@ def _compute_speed_value( speed_value *= 1.0 + approach_rate_factor * length_bonus + # if Mod.Blinds in score.mods: + # speed_value *= 1.12 + if Mod.Hidden in score.mods: speed_value *= 1.0 + 0.04 * ( 12.0 - self.difficulty_attributes.approach_rate # type: ignore @@ -299,8 +331,7 @@ def _compute_speed_value( speed_value *= math.pow( 0.99, - (score.statistics.count_50 - total_hits / 500.0) - * int(score.statistics.count_50 > total_hits / 500.0), + 0 if score.statistics.count_50 < total_hits / 500 else score.statistics.count_50 - total_hits / 500 ) return speed_value @@ -310,6 +341,9 @@ def _compute_accuracy_value( score: Score, total_hits: int, ) -> float: + if Mod.Relax in score.mods: + return 0 + accuracy_calculator = OsuAccuracyCalculator() better_accuracy_percentage = accuracy_calculator.calculate_weighted(score) @@ -324,6 +358,12 @@ def _compute_accuracy_value( math.pow(score.beatmap.count_circles / 1000.0, 0.3), # type: ignore ) + # if Mod.Blinds in score.mods: + # accuracy_value *= 1.14 + + # if Mod.Traceable in score.mods: + # accuracy_value *= 1.08 + if Mod.Hidden in score.mods: accuracy_value *= 1.08 @@ -366,29 +406,7 @@ def _compute_flashlight_value( ) return flashlight_value - - def _calculate_effective_miss_count(self, score: Score) -> float: - combo_based_miss_count = 0.0 - - if score.beatmap.count_sliders > 0: # type: ignore - full_combo_threshold = ( - self.difficulty_attributes.max_combo - 0.1 * score.beatmap.count_sliders # type: ignore - ) - - if score.max_combo < full_combo_threshold: - combo_based_miss_count = full_combo_threshold / max( - 1.0, - score.max_combo, - ) - - combo_based_miss_count = min( - combo_based_miss_count, - score.statistics.count_100 - + score.statistics.count_50 - + score.statistics.count_miss, - ) - - return max(score.statistics.count_miss, combo_based_miss_count) + def _get_combo_scaling_factor(self, score: Score) -> float: if self.difficulty_attributes.max_combo <= 0: @@ -399,6 +417,14 @@ def _get_combo_scaling_factor(self, score: Score) -> float: / math.pow(self.difficulty_attributes.max_combo, 0.8), 1.0, ) + + + def _calculate_miss_penalty( + self, + effective_miss_count: float, + difficult_strain_count: float + ) -> float: + return 0.96 / ((effective_miss_count / (4 * pow(math.log(difficult_strain_count), 0.94))) + 1) class TaikoPerformanceCalculator(AbstractPerformanceCalculator): @@ -562,7 +588,7 @@ def calculate(self, score: Score) -> ManiaPerformanceAttributes: + score.statistics.count_miss ) - multiplier = MANIA_BASE_MULTIPLIER + multiplier = 1.0 if Mod.NoFail in score.mods: multiplier *= 0.75 @@ -580,7 +606,7 @@ def calculate(self, score: Score) -> ManiaPerformanceAttributes: def _compute_difficulty_value(self, accuracy: float, total_hits: int) -> float: difficulty_value = ( - math.pow(max(self.difficulty_attributes.star_rating - 0.15, 0.05), 2.2) + 8 * math.pow(max(self.difficulty_attributes.star_rating - 0.15, 0.05), 2.2) * max(0.0, 5.0 * accuracy - 4.0) * (1.0 + 0.1 * min(1.0, total_hits / 1500)) ) @@ -612,8 +638,6 @@ def calculate(self, score: Score) -> CatchPerformanceAttributes: + score.statistics.count_300 ) - multiplier = CATCH_BASE_MULTIPLIER - total_value = ( math.pow( 5.0 * max(1.0, self.difficulty_attributes.star_rating / 0.0049) - 4.0, @@ -666,8 +690,6 @@ def calculate(self, score: Score) -> CatchPerformanceAttributes: total_value *= math.pow(accuracy, 5.5) if Mod.NoFail in score.mods: - total_value *= 0.90 - - total_value *= multiplier + total_value *= max(0.90, 1 - 0.02 * score.statistics.count_miss) return CatchPerformanceAttributes(total=total_value) From 971b56c130515311f78b2f6df031adb7e0a1c207 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 19:44:06 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- aiosu/utils/performance.py | 85 +++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/aiosu/utils/performance.py b/aiosu/utils/performance.py index 8db4b7e..97bdd4a 100644 --- a/aiosu/utils/performance.py +++ b/aiosu/utils/performance.py @@ -107,19 +107,29 @@ def calculate(self, score: Score) -> OsuPerformanceAttributes: full_combo_threshold = self.difficulty_attributes.max_combo - 0.1 * score.beatmap.count_sliders # type: ignore if score.max_combo < full_combo_threshold: - effective_miss_count = full_combo_threshold / max(1, score.max_combo) - - effective_miss_count = min(effective_miss_count, score.statistics.count_100 + score.statistics.count_100 + score.statistics.count_miss) + effective_miss_count = full_combo_threshold / max( + 1, score.max_combo, + ) + + effective_miss_count = min( + effective_miss_count, + score.statistics.count_100 + + score.statistics.count_100 + + score.statistics.count_miss, + ) else: full_combo_threshold = self.difficulty_attributes.max_combo - (score.beatmap.count_sliders - score.statistics.count_slider_tail_hit) # type: ignore if score.max_combo < full_combo_threshold: - effective_miss_count = full_combo_threshold / max(1, score.max_combo) + effective_miss_count = full_combo_threshold / max( + 1, score.max_combo, + ) effective_miss_count = min(effective_miss_count, score.statistics.count_large_tick_miss + score.statistics.count_miss) # type: ignore - effective_miss_count = clamp(effective_miss_count, score.statistics.count_miss, total_hits) - + effective_miss_count = clamp( + effective_miss_count, score.statistics.count_miss, total_hits, + ) multiplier = OSU_BASE_MULTIPLIER @@ -138,13 +148,12 @@ def calculate(self, score: Score) -> OsuPerformanceAttributes: meh_multiplier = max(0, (1 - pow(adjusted_od, 5)) if self.difficulty_attributes.overall_difficulty > 0 else 1) # type: ignore effective_miss_count = min( - effective_miss_count + - score.statistics.count_100 * ok_multiplier + - score.statistics.count_50 * meh_multiplier, - total_hits + effective_miss_count + + score.statistics.count_100 * ok_multiplier + + score.statistics.count_50 * meh_multiplier, + total_hits, ) - aim_value = self._compute_aim_value(score, effective_miss_count, total_hits) speed_value = self._compute_speed_value(score, effective_miss_count, total_hits) accuracy_value = self._compute_accuracy_value(score, total_hits) @@ -220,24 +229,34 @@ def _compute_aim_value( estimate_improperly_followed_difficult_sliders = 0 if self._is_slider_head_accuracy(score): - maximum_possible_dropped_sliders = score.statistics.count_100 + score.statistics.count_50 + score.statistics.count_miss + maximum_possible_dropped_sliders = ( + score.statistics.count_100 + + score.statistics.count_50 + + score.statistics.count_miss + ) estimate_improperly_followed_difficult_sliders = clamp( - min(maximum_possible_dropped_sliders, self.difficulty_attributes.max_combo - score.max_combo), + min( + maximum_possible_dropped_sliders, + self.difficulty_attributes.max_combo - score.max_combo, + ), 0, - estimate_difficult_sliders + estimate_difficult_sliders, ) else: estimate_improperly_followed_difficult_sliders = clamp( score.beatmap.count_sliders - score.statistics.count_slider_tail_hit + score.statistics.count_large_tick_miss, # type: ignore 0, - estimate_difficult_sliders + estimate_difficult_sliders, ) slider_nerf_factor = ( - (1 - self.difficulty_attributes.slider_factor) # type: ignore - * pow(1 - estimate_improperly_followed_difficult_sliders / estimate_difficult_sliders, 3) - + self.difficulty_attributes.slider_factor - ) + 1 - self.difficulty_attributes.slider_factor + ) * pow( # type: ignore + 1 + - estimate_improperly_followed_difficult_sliders + / estimate_difficult_sliders, + 3, + ) + self.difficulty_attributes.slider_factor aim_value *= slider_nerf_factor accuracy = score.accuracy @@ -256,7 +275,7 @@ def _compute_speed_value( ) -> float: if Mod.Relax in score.mods: return 0 - + speed_value = ( math.pow( 5.0 * max(1.0, self.difficulty_attributes.speed_difficulty / 0.0675) # type: ignore @@ -269,7 +288,11 @@ def _compute_speed_value( length_bonus = ( 0.95 + 0.4 * min(1.0, total_hits / 2000.0) - + (((math.log10(total_hits / 2000.0) * 0.5) * int(total_hits > 2000)) if total_hits > 0 else 0) + + ( + ((math.log10(total_hits / 2000.0) * 0.5) * int(total_hits > 2000)) + if total_hits > 0 + else 0 + ) ) speed_value *= length_bonus @@ -331,7 +354,11 @@ def _compute_speed_value( speed_value *= math.pow( 0.99, - 0 if score.statistics.count_50 < total_hits / 500 else score.statistics.count_50 - total_hits / 500 + ( + 0 + if score.statistics.count_50 < total_hits / 500 + else score.statistics.count_50 - total_hits / 500 + ), ) return speed_value @@ -406,7 +433,6 @@ def _compute_flashlight_value( ) return flashlight_value - def _get_combo_scaling_factor(self, score: Score) -> float: if self.difficulty_attributes.max_combo <= 0: @@ -417,14 +443,14 @@ def _get_combo_scaling_factor(self, score: Score) -> float: / math.pow(self.difficulty_attributes.max_combo, 0.8), 1.0, ) - def _calculate_miss_penalty( - self, - effective_miss_count: float, - difficult_strain_count: float + self, effective_miss_count: float, difficult_strain_count: float, ) -> float: - return 0.96 / ((effective_miss_count / (4 * pow(math.log(difficult_strain_count), 0.94))) + 1) + return 0.96 / ( + (effective_miss_count / (4 * pow(math.log(difficult_strain_count), 0.94))) + + 1 + ) class TaikoPerformanceCalculator(AbstractPerformanceCalculator): @@ -606,7 +632,8 @@ def calculate(self, score: Score) -> ManiaPerformanceAttributes: def _compute_difficulty_value(self, accuracy: float, total_hits: int) -> float: difficulty_value = ( - 8 * math.pow(max(self.difficulty_attributes.star_rating - 0.15, 0.05), 2.2) + 8 + * math.pow(max(self.difficulty_attributes.star_rating - 0.15, 0.05), 2.2) * max(0.0, 5.0 * accuracy - 4.0) * (1.0 + 0.1 * min(1.0, total_hits / 1500)) ) From edf70d2a8b94e9a3977c075abbe4598b6f999900 Mon Sep 17 00:00:00 2001 From: Andrei Baciu <8437201+NiceAesth@users.noreply.github.com> Date: Thu, 5 Dec 2024 18:39:20 +0200 Subject: [PATCH 3/4] Update aiosu/utils/performance.py --- aiosu/utils/performance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiosu/utils/performance.py b/aiosu/utils/performance.py index 97bdd4a..0045741 100644 --- a/aiosu/utils/performance.py +++ b/aiosu/utils/performance.py @@ -79,7 +79,7 @@ class OsuPerformanceCalculator(AbstractPerformanceCalculator): :type difficulty_attributes: BeatmapDifficultyAttributes """ - def _is_slider_head_accuracy(self, Score) -> bool: + def _is_slider_head_accuracy(self, score: Score) -> bool: return True def calculate(self, score: Score) -> OsuPerformanceAttributes: From 001c3ccb8e9161e367a5ce87dc91fdddc074a6e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 16:40:24 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- aiosu/utils/performance.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/aiosu/utils/performance.py b/aiosu/utils/performance.py index 0045741..c4ac515 100644 --- a/aiosu/utils/performance.py +++ b/aiosu/utils/performance.py @@ -108,7 +108,8 @@ def calculate(self, score: Score) -> OsuPerformanceAttributes: if score.max_combo < full_combo_threshold: effective_miss_count = full_combo_threshold / max( - 1, score.max_combo, + 1, + score.max_combo, ) effective_miss_count = min( @@ -122,13 +123,16 @@ def calculate(self, score: Score) -> OsuPerformanceAttributes: if score.max_combo < full_combo_threshold: effective_miss_count = full_combo_threshold / max( - 1, score.max_combo, + 1, + score.max_combo, ) effective_miss_count = min(effective_miss_count, score.statistics.count_large_tick_miss + score.statistics.count_miss) # type: ignore effective_miss_count = clamp( - effective_miss_count, score.statistics.count_miss, total_hits, + effective_miss_count, + score.statistics.count_miss, + total_hits, ) multiplier = OSU_BASE_MULTIPLIER @@ -445,7 +449,9 @@ def _get_combo_scaling_factor(self, score: Score) -> float: ) def _calculate_miss_penalty( - self, effective_miss_count: float, difficult_strain_count: float, + self, + effective_miss_count: float, + difficult_strain_count: float, ) -> float: return 0.96 / ( (effective_miss_count / (4 * pow(math.log(difficult_strain_count), 0.94)))