diff --git a/app/calculator.py b/app/calculator.py index cc21f0f..950bd45 100644 --- a/app/calculator.py +++ b/app/calculator.py @@ -79,21 +79,152 @@ def calculate_pp( # https://osu.ppy.sh/wiki/Gameplay/Score/Total_score -def calculate_level_to_score(level: int) -> float: - if level <= 100: - # 55 = 4^3 - 3^2 - return 5000 / 3 * (55 - level) + 1.25 * math.pow(1.8, level - 60) +def calculate_level_to_score(n: int) -> float: + if n <= 100: + return 5000 / 3 * (4 * n**3 - 3 * n**2 - n) + 1.25 * 1.8 ** (n - 60) else: - return 26_931_190_827 + 99_999_999_999 * (level - 100) + return 26931190827 + 99999999999 * (n - 100) -def calculate_score_to_level(score: float) -> int: - if score < 5000: - return int(55 - (3 * score / 5000)) # 55 = 4^3 - 3^2 - elif score < 26_931_190_827: - return int(60 + math.log(score / 1.25, 1.8)) - else: - return int((score - 26_931_190_827) / 99_999_999_999 + 100) +# https://github.com/ppy/osu-queue-score-statistics/blob/4bdd479530408de73f3cdd95e097fe126772a65b/osu.Server.Queues.ScoreStatisticsProcessor/Processors/TotalScoreProcessor.cs#L70-L116 +def calculate_score_to_level(total_score: int) -> float: + to_next_level = [ + 30000, + 100000, + 210000, + 360000, + 550000, + 780000, + 1050000, + 1360000, + 1710000, + 2100000, + 2530000, + 3000000, + 3510000, + 4060000, + 4650000, + 5280000, + 5950000, + 6660000, + 7410000, + 8200000, + 9030000, + 9900000, + 10810000, + 11760000, + 12750000, + 13780000, + 14850000, + 15960000, + 17110000, + 18300000, + 19530000, + 20800000, + 22110000, + 23460000, + 24850000, + 26280000, + 27750000, + 29260000, + 30810000, + 32400000, + 34030000, + 35700000, + 37410000, + 39160000, + 40950000, + 42780000, + 44650000, + 46560000, + 48510000, + 50500000, + 52530000, + 54600000, + 56710000, + 58860000, + 61050000, + 63280000, + 65550000, + 67860000, + 70210001, + 72600001, + 75030002, + 77500003, + 80010006, + 82560010, + 85150019, + 87780034, + 90450061, + 93160110, + 95910198, + 98700357, + 101530643, + 104401157, + 107312082, + 110263748, + 113256747, + 116292144, + 119371859, + 122499346, + 125680824, + 128927482, + 132259468, + 135713043, + 139353477, + 143298259, + 147758866, + 153115959, + 160054726, + 169808506, + 184597311, + 208417160, + 248460887, + 317675597, + 439366075, + 655480935, + 1041527682, + 1733419828, + 2975801691, + 5209033044, + 9225761479, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + 99999999999, + ] + + remaining_score = total_score + level = 0.0 + + while remaining_score > 0: + next_level_requirement = to_next_level[ + min(len(to_next_level) - 1, round(level)) + ] + level += min(1, remaining_score / next_level_requirement) + remaining_score -= next_level_requirement + + return level + 1 # https://osu.ppy.sh/wiki/Performance_points/Weighting_system diff --git a/app/database/score.py b/app/database/score.py index 2c811ad..88111ad 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -585,7 +585,7 @@ async def process_user( session.add(previous_score_best) statistics.ranked_score += difference - statistics.level_current = calculate_score_to_level(statistics.ranked_score) + statistics.level_current = calculate_score_to_level(statistics.total_score) statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo) if score.passed and ranked: if previous_score_best_mod is not None: diff --git a/app/database/statistics.py b/app/database/statistics.py index ca32e32..54aa3bb 100644 --- a/app/database/statistics.py +++ b/app/database/statistics.py @@ -1,4 +1,5 @@ from datetime import UTC, datetime +import math from typing import TYPE_CHECKING from app.models.score import GameMode @@ -59,8 +60,7 @@ class UserStatistics(UserStatisticsBase, table=True): grade_sh: int = Field(default=0) grade_a: int = Field(default=0) - level_current: int = Field(default=1) - level_progress: int = Field(default=0) + level_current: float = Field(default=1) user: "User" = Relationship(back_populates="statistics") # type: ignore[valid-type] @@ -97,9 +97,10 @@ class UserStatisticsResp(UserStatisticsBase): "a": obj.grade_a, } s.level = { - "current": obj.level_current, - "progress": obj.level_progress, + "current": int(obj.level_current), + "progress": int(math.fmod(obj.level_current, 1) * 100), } + s.global_rank = await get_rank(session, obj) s.country_rank = await get_rank(session, obj, user_country) return s diff --git a/migrations/versions/8bab62d764a5_statistics_remove_level_progress.py b/migrations/versions/8bab62d764a5_statistics_remove_level_progress.py new file mode 100644 index 0000000..5ae4215 --- /dev/null +++ b/migrations/versions/8bab62d764a5_statistics_remove_level_progress.py @@ -0,0 +1,54 @@ +"""statistics: remove level_progress + +Revision ID: 8bab62d764a5 +Revises: 59c9a0827de0 +Create Date: 2025-08-13 10:34:03.430039 + +""" + +from __future__ import annotations + +from collections.abc import Sequence + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision: str = "8bab62d764a5" +down_revision: str | Sequence[str] | None = "59c9a0827de0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "lazer_user_statistics", + "level_current", + existing_type=mysql.INTEGER(), + type_=sa.Float(), + existing_nullable=False, + ) + op.drop_column("lazer_user_statistics", "level_progress") + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "lazer_user_statistics", + sa.Column( + "level_progress", mysql.INTEGER(), autoincrement=False, nullable=False + ), + ) + op.alter_column( + "lazer_user_statistics", + "level_current", + existing_type=sa.Float(), + type_=mysql.INTEGER(), + existing_nullable=False, + ) + # ### end Alembic commands ###