3 Commits

8 changed files with 214 additions and 125 deletions

View File

@ -5,7 +5,6 @@ from typing import Iterable, Optional, Type, Union
from sqlalchemy import Engine, func, inspect, select from sqlalchemy import Engine, func, inspect, select
from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, sessionmaker from sqlalchemy.orm import DeclarativeBase, InstrumentedAttribute, sessionmaker
from arcaea_offline.external.arcsong.arcsong_json import ArcSongJsonBuilder
from arcaea_offline.singleton import Singleton from arcaea_offline.singleton import Singleton
from .models.v4.config import ConfigBase, Property from .models.v4.config import ConfigBase, Property
@ -403,11 +402,3 @@ class Database(metaclass=Singleton):
return self.__count_table(ScoreBest) return self.__count_table(ScoreBest)
# endregion # 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] id: Mapped[int]
song_id: Mapped[str] song_id: Mapped[str]
rating_class: Mapped[int] rating_class: Mapped[ArcaeaRatingClass]
score: Mapped[int] score: Mapped[int]
pure: Mapped[Optional[int]] pure: Mapped[Optional[int]]
shiny_pure: Mapped[Optional[int]] shiny_pure: Mapped[Optional[int]]

View File

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

View File

@ -1,14 +0,0 @@
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

@ -1,98 +0,0 @@
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

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

View File

@ -0,0 +1,172 @@
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

@ -0,0 +1,38 @@
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