from datetime import datetime import math from app.database.user import User from app.models.score import MODE_TO_INT, APIMod, GameMode, Rank from .beatmap import Beatmap, BeatmapResp from .beatmapset import BeatmapsetResp from pydantic import BaseModel from sqlalchemy import Column, DateTime from sqlmodel import JSON, BigInteger, Field, Relationship, SQLModel class ScoreBase(SQLModel): # 基本字段 accuracy: float map_md5: str = Field(max_length=32, index=True) best_id: int | None = Field(default=None) build_id: int | None = Field(default=None) classic_total_score: int | None = Field( default=0, sa_column=Column(BigInteger) ) # solo_score ended_at: datetime = Field(sa_column=Column(DateTime)) has_replay: bool max_combo: int mods: list[APIMod] = Field(sa_column=Column(JSON)) passed: bool playlist_item_id: int | None = Field(default=None) # multiplayer pp: float preserve: bool = Field(default=True) rank: Rank room_id: int | None = Field(default=None) # multiplayer started_at: datetime = Field(sa_column=Column(DateTime)) total_score: int = Field(default=0, sa_column=Column(BigInteger)) type: str # optional # TODO: current_user_attributes position: int | None = Field(default=None) # multiplayer class ScoreStatistics(BaseModel): count_miss: int count_50: int count_100: int count_300: int count_geki: int count_katu: int count_large_tick_miss: int | None = None count_slider_tail_hit: int | None = None class Score(ScoreBase, table=True): __tablename__ = "scores" # pyright: ignore[reportAssignmentType] id: int = Field(primary_key=True) beatmap_id: int = Field(index=True, foreign_key="beatmaps.id") user_id: int = Field(foreign_key="users.id", index=True) # ScoreStatistics n300: int = Field(exclude=True) n100: int = Field(exclude=True) n50: int = Field(exclude=True) nmiss: int = Field(exclude=True) ngeki: int = Field(exclude=True) nkatu: int = Field(exclude=True) nlarge_tick_miss: int | None = Field(default=None, exclude=True) nslider_tail_hit: int | None = Field(default=None, exclude=True) gamemode: GameMode = Field(index=True, alias="ruleset_id") # optional beatmap: "Beatmap" = Relationship() user: "User" = Relationship() class ScoreResp(ScoreBase): id: int is_perfect_combo: bool = False legacy_perfect: bool = False legacy_total_score: int = 0 # FIXME processed: bool = False # solo_score weight: float = 0.0 ruleset_id: int | None = None beatmap: BeatmapResp | None = None beatmapset: BeatmapsetResp | None = None # FIXME: user: APIUser | None = None statistics: ScoreStatistics | None = None @classmethod def from_db(cls, score: Score) -> "ScoreResp": s = cls.model_validate(score.model_dump()) s.beatmap = BeatmapResp.from_db(score.beatmap) s.beatmapset = BeatmapsetResp.from_db(score.beatmap.beatmapset) s.is_perfect_combo = s.max_combo == s.beatmap.max_combo s.legacy_perfect = s.max_combo == s.beatmap.max_combo s.ruleset_id = MODE_TO_INT[score.gamemode] if score.best_id: # https://osu.ppy.sh/wiki/Performance_points/Weighting_system s.weight = math.pow(0.95, score.best_id) s.statistics = ScoreStatistics( count_miss=score.nmiss, count_50=score.n50, count_100=score.n100, count_300=score.n300, count_geki=score.ngeki, count_katu=score.nkatu, count_large_tick_miss=score.nlarge_tick_miss, count_slider_tail_hit=score.nslider_tail_hit, ) return s