mirror of
https://github.com/283375/arcaea-offline.git
synced 2025-07-04 05:36:26 +00:00
Compare commits
1 Commits
e93904bb0d
...
b062bbd1b0
Author | SHA1 | Date | |
---|---|---|---|
b062bbd1b0
|
@ -5,6 +5,7 @@ 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
|
||||||
@ -402,3 +403,11 @@ 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
|
||||||
|
@ -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[ArcaeaRatingClass]
|
rating_class: Mapped[int]
|
||||||
score: Mapped[int]
|
score: Mapped[int]
|
||||||
pure: Mapped[Optional[int]]
|
pure: Mapped[Optional[int]]
|
||||||
shiny_pure: Mapped[Optional[int]]
|
shiny_pure: Mapped[Optional[int]]
|
||||||
|
3
src/arcaea_offline/external/andreal/__init__.py
vendored
Normal file
3
src/arcaea_offline/external/andreal/__init__.py
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .api_data import AndrealImageGeneratorApiDataConverter
|
||||||
|
|
||||||
|
__all__ = ["AndrealImageGeneratorApiDataConverter"]
|
14
src/arcaea_offline/external/andreal/account.py
vendored
Normal file
14
src/arcaea_offline/external/andreal/account.py
vendored
Normal 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
|
98
src/arcaea_offline/external/andreal/api_data.py
vendored
Normal file
98
src/arcaea_offline/external/andreal/api_data.py
vendored
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
from .api_data import AndrealImageGeneratorApiDataExporter
|
|
||||||
|
|
||||||
__all__ = ["AndrealImageGeneratorApiDataExporter"]
|
|
@ -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)
|
|
@ -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
|
|
Reference in New Issue
Block a user