1 Commits

Author SHA1 Message Date
b062bbd1b0 refactor: arcsong database importer & arcsong json exporter 2024-10-01 01:06:50 +08:00
8 changed files with 125 additions and 214 deletions

View File

@ -5,6 +5,7 @@ from typing import Iterable, Optional, Type, Union
from sqlalchemy import Engine, func, inspect, select
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, sessionmaker
from arcaea_offline.external.arcsong.arcsong_json import ArcSongJsonBuilder
from arcaea_offline.singleton import Singleton
from .models.v4.config import ConfigBase, Property
@ -402,3 +403,11 @@ class Database(metaclass=Singleton):
return self.__count_table(ScoreBest)
# endregion
# region export
def generate_arcsong(self):
with self.sessionmaker() as session:
arcsong = ArcSongJsonBuilder(session).generate_arcsong_json()
return arcsong
# endregion

View File

@ -147,7 +147,7 @@ class PlayResultBest(ModelsV5ViewBase, ReprHelper):
id: Mapped[int]
song_id: Mapped[str]
rating_class: Mapped[ArcaeaRatingClass]
rating_class: Mapped[int]
score: Mapped[int]
pure: Mapped[Optional[int]]
shiny_pure: Mapped[Optional[int]]

View File

@ -0,0 +1,3 @@
from .api_data import AndrealImageGeneratorApiDataConverter
__all__ = ["AndrealImageGeneratorApiDataConverter"]

View File

@ -0,0 +1,14 @@
class AndrealImageGeneratorAccount:
def __init__(
self,
name: str = "Player",
code: int = 123456789,
rating: int = -1,
character: int = 5,
character_uncapped: bool = False,
):
self.name = name
self.code = code
self.rating = rating
self.character = character
self.character_uncapped = character_uncapped

View File

@ -0,0 +1,98 @@
from typing import Optional, Union
from sqlalchemy import select
from sqlalchemy.orm import Session
from ...models import CalculatedPotential, ScoreBest, ScoreCalculated
from .account import AndrealImageGeneratorAccount
class AndrealImageGeneratorApiDataConverter:
def __init__(
self,
session: Session,
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
):
self.session = session
self.account = account
def account_info(self):
return {
"code": self.account.code,
"name": self.account.name,
"is_char_uncapped": self.account.character_uncapped,
"rating": self.account.rating,
"character": self.account.character,
}
def score(self, score: Union[ScoreCalculated, ScoreBest]):
return {
"score": score.score,
"health": 75,
"rating": score.potential,
"song_id": score.song_id,
"modifier": score.modifier or 0,
"difficulty": score.rating_class,
"clear_type": score.clear_type or 1,
"best_clear_type": score.clear_type or 1,
"time_played": score.date * 1000 if score.date else 0,
"near_count": score.far,
"miss_count": score.lost,
"perfect_count": score.pure,
"shiny_perfect_count": score.shiny_pure,
}
def user_info(self, score: Optional[ScoreCalculated] = None):
if not score:
score = self.session.scalar(
select(ScoreCalculated).order_by(ScoreCalculated.date.desc()).limit(1)
)
if not score:
raise ValueError("No score available.")
return {
"content": {
"account_info": self.account_info(),
"recent_score": [self.score(score)],
}
}
def user_best(self, song_id: str, rating_class: int):
score = self.session.scalar(
select(ScoreBest).where(
(ScoreBest.song_id == song_id)
& (ScoreBest.rating_class == rating_class)
)
)
if not score:
raise ValueError("No score available.")
return {
"content": {
"account_info": self.account_info(),
"record": self.score(score),
}
}
def user_best30(self):
scores = list(
self.session.scalars(
select(ScoreBest).order_by(ScoreBest.potential.desc()).limit(40)
)
)
if not scores:
raise ValueError("No score available.")
best30_avg = self.session.scalar(select(CalculatedPotential.b30))
best30_overflow = (
[self.score(score) for score in scores[30:40]] if len(scores) > 30 else []
)
return {
"content": {
"account_info": self.account_info(),
"best30_avg": best30_avg,
"best30_list": [self.score(score) for score in scores[:30]],
"best30_overflow": best30_overflow,
}
}

View File

@ -1,3 +0,0 @@
from .api_data import AndrealImageGeneratorApiDataExporter
__all__ = ["AndrealImageGeneratorApiDataExporter"]

View File

@ -1,172 +0,0 @@
import statistics
from dataclasses import dataclass
from typing import List, Optional, Union
from sqlalchemy import select
from sqlalchemy.orm import Session
from arcaea_offline.constants.enums.arcaea import ArcaeaRatingClass
from arcaea_offline.database.models.v5 import (
PlayResultBest,
PlayResultCalculated,
)
from .definitions import (
AndrealImageGeneratorApiDataAccountInfo,
AndrealImageGeneratorApiDataRoot,
AndrealImageGeneratorApiDataScoreItem,
)
@dataclass
class AndrealImageGeneratorAccount:
name: str = "Player"
code: int = 123456789
rating: int = -1
character: int = 5
character_uncapped: bool = False
class AndrealImageGeneratorApiDataExporter:
@staticmethod
def craft_account_info(
account: AndrealImageGeneratorAccount,
) -> AndrealImageGeneratorApiDataAccountInfo:
return {
"code": account.code,
"name": account.name,
"is_char_uncapped": account.character_uncapped,
"rating": account.rating,
"character": account.character,
}
@staticmethod
def craft_score_item(
play_result: Union[PlayResultCalculated, PlayResultBest],
) -> AndrealImageGeneratorApiDataScoreItem:
modifier = play_result.modifier.value if play_result.modifier else 0
clear_type = play_result.clear_type.value if play_result.clear_type else 0
return {
"score": play_result.score,
"health": 75,
"rating": play_result.potential,
"song_id": play_result.song_id,
"modifier": modifier,
"difficulty": play_result.rating_class.value,
"clear_type": clear_type,
"best_clear_type": clear_type,
"time_played": int(play_result.date.timestamp() * 1000)
if play_result.date
else 0,
"near_count": play_result.far,
"miss_count": play_result.lost,
"perfect_count": play_result.pure,
"shiny_perfect_count": play_result.shiny_pure,
}
@classmethod
def user_info(
cls,
play_result_calculated: PlayResultCalculated,
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
) -> AndrealImageGeneratorApiDataRoot:
return {
"content": {
"account_info": cls.craft_account_info(account),
"recent_score": [cls.craft_score_item(play_result_calculated)],
}
}
@classmethod
def user_best(
cls,
play_result_best: PlayResultBest,
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
) -> AndrealImageGeneratorApiDataRoot:
return {
"content": {
"account_info": cls.craft_account_info(account),
"record": cls.craft_score_item(play_result_best),
}
}
@classmethod
def user_best30(
cls,
play_results_best: List[PlayResultBest],
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
) -> AndrealImageGeneratorApiDataRoot:
play_results_best_sorted = sorted(
play_results_best, key=lambda it: it.potential, reverse=True
)
best30_list = play_results_best_sorted[:30]
best30_overflow = play_results_best_sorted[30:]
best30_avg = statistics.fmean([it.potential for it in best30_list])
return {
"content": {
"account_info": cls.craft_account_info(account),
"best30_avg": best30_avg,
"best30_list": [cls.craft_score_item(it) for it in best30_list],
"best30_overflow": [cls.craft_score_item(it) for it in best30_overflow],
}
}
@classmethod
def craft_user_info(
cls,
session: Session,
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
) -> Optional[AndrealImageGeneratorApiDataRoot]:
play_result_calculated = session.scalar(
select(PlayResultCalculated)
.order_by(PlayResultCalculated.date.desc())
.limit(1)
)
if play_result_calculated is None:
return None
return cls.user_info(play_result_calculated, account)
@classmethod
def craft_user_best(
cls,
session: Session,
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
*,
song_id: str,
rating_class: ArcaeaRatingClass,
):
play_result_best = session.scalar(
select(PlayResultBest).where(
(PlayResultBest.song_id == song_id)
& (PlayResultBest.rating_class == rating_class)
)
)
if play_result_best is None:
return None
return cls.user_best(play_result_best, account)
@classmethod
def craft(
cls,
session: Session,
account: AndrealImageGeneratorAccount = AndrealImageGeneratorAccount(),
*,
limit: int = 40,
) -> Optional[AndrealImageGeneratorApiDataRoot]:
play_results_best = list(
session.scalars(
select(PlayResultBest)
.order_by(PlayResultBest.potential.desc())
.limit(limit)
).all()
)
return cls.user_best30(play_results_best, account)

View File

@ -1,38 +0,0 @@
from typing import List, Optional, TypedDict
class AndrealImageGeneratorApiDataAccountInfo(TypedDict):
name: str
code: int
rating: int
character: int
is_char_uncapped: bool
class AndrealImageGeneratorApiDataScoreItem(TypedDict):
score: int
health: int
rating: float
song_id: str
modifier: int
difficulty: int
clear_type: int
best_clear_type: int
time_played: int
near_count: Optional[int]
miss_count: Optional[int]
perfect_count: Optional[int]
shiny_perfect_count: Optional[int]
class AndrealImageGeneratorApiDataContent(TypedDict, total=False):
account_info: AndrealImageGeneratorApiDataAccountInfo
recent_score: List[AndrealImageGeneratorApiDataScoreItem]
record: AndrealImageGeneratorApiDataScoreItem
best30_avg: float
best30_list: List[AndrealImageGeneratorApiDataScoreItem]
best30_overflow: List[AndrealImageGeneratorApiDataScoreItem]
class AndrealImageGeneratorApiDataRoot(TypedDict):
content: AndrealImageGeneratorApiDataContent