mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
🎨 拆分函数
This commit is contained in:
@@ -6,7 +6,7 @@ from hashlib import md5, sha512
|
|||||||
from math import ceil, floor
|
from math import ceil, floor
|
||||||
from re import match
|
from re import match
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import Literal, NamedTuple
|
from typing import Literal
|
||||||
from urllib.parse import urlunparse
|
from urllib.parse import urlunparse
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ from .schemas.response import ProcessedData, RawResponse
|
|||||||
from .schemas.user import User
|
from .schemas.user import User
|
||||||
from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, RatedLeague, UserInfo
|
from .schemas.user_info import NeverPlayedLeague, NeverRatedLeague, RatedLeague, UserInfo
|
||||||
from .schemas.user_info import SuccessModel as InfoSuccess
|
from .schemas.user_info import SuccessModel as InfoSuccess
|
||||||
from .schemas.user_records import SoloRecord, UserRecords
|
from .schemas.user_records import MultiRecord, SoloRecord, UserRecords
|
||||||
from .schemas.user_records import SuccessModel as RecordsSuccess
|
from .schemas.user_records import SuccessModel as RecordsSuccess
|
||||||
from .typing import Rank
|
from .typing import Rank
|
||||||
|
|
||||||
@@ -60,6 +60,55 @@ def identify_user_info(info: str) -> User | MessageFormatError:
|
|||||||
return MessageFormatError('用户名/ID不合法')
|
return MessageFormatError('用户名/ID不合法')
|
||||||
|
|
||||||
|
|
||||||
|
def get_value_bounds(values: list[int | float]) -> tuple[int, int]:
|
||||||
|
value_max = 10 * ceil(max(values) / 10)
|
||||||
|
value_min = 10 * floor(min(values) / 10)
|
||||||
|
return value_max, value_min
|
||||||
|
|
||||||
|
|
||||||
|
def get_split(value_max: int, value_min: int) -> tuple[int, int]:
|
||||||
|
offset = 0
|
||||||
|
overflow = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if (new_max_value := value_max + offset + overflow) > TR_MAX:
|
||||||
|
overflow -= 1
|
||||||
|
continue
|
||||||
|
if (new_min_value := value_min - offset + overflow) < TR_MIN:
|
||||||
|
overflow += 1
|
||||||
|
continue
|
||||||
|
if ((new_max_value - new_min_value) / 40).is_integer():
|
||||||
|
split_value = int((value_max + offset - (value_min - offset)) / 4)
|
||||||
|
break
|
||||||
|
offset += 1
|
||||||
|
return split_value, offset + overflow
|
||||||
|
|
||||||
|
|
||||||
|
def get_specified_point(
|
||||||
|
previous_point: TETRIOInfo.TetraLeagueHistory.Data,
|
||||||
|
behind_point: TETRIOInfo.TetraLeagueHistory.Data,
|
||||||
|
point_time: datetime,
|
||||||
|
) -> TETRIOInfo.TetraLeagueHistory.Data:
|
||||||
|
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
previous_point (TETRIOInfo.TetraLeagueHistory.Data): 前面的数据点
|
||||||
|
behind_point (TETRIOInfo.TetraLeagueHistory.Data): 后面的数据点
|
||||||
|
point_time (datetime): 要推算的点的位置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TETRIOInfo.TetraLeagueHistory.Data: 要推算的点的数据
|
||||||
|
"""
|
||||||
|
# 求两个点的斜率
|
||||||
|
slope = (behind_point.tr - previous_point.tr) / (
|
||||||
|
datetime.timestamp(behind_point.record_at) - datetime.timestamp(previous_point.record_at)
|
||||||
|
)
|
||||||
|
return TETRIOInfo.TetraLeagueHistory.Data(
|
||||||
|
record_at=point_time,
|
||||||
|
tr=previous_point.tr + slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.record_at)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Processor(ProcessorMeta):
|
class Processor(ProcessorMeta):
|
||||||
user: User
|
user: User
|
||||||
raw_response: RawResponse
|
raw_response: RawResponse
|
||||||
@@ -125,11 +174,47 @@ class Processor(ProcessorMeta):
|
|||||||
self.command_type = 'query'
|
self.command_type = 'query'
|
||||||
await self.get_user()
|
await self.get_user()
|
||||||
user_info, user_records = await gather(self.get_user_info(), self.get_user_records())
|
user_info, user_records = await gather(self.get_user_info(), self.get_user_records())
|
||||||
user_name = user_info.data.user.username.upper()
|
|
||||||
league = user_info.data.user.league
|
|
||||||
sprint = user_records.data.records.sprint
|
sprint = user_records.data.records.sprint
|
||||||
blitz = user_records.data.records.blitz
|
blitz = user_records.data.records.blitz
|
||||||
if isinstance(league, RatedLeague) and league.vs is not None:
|
if isinstance(sprint.record, MultiRecord) or isinstance(blitz.record, MultiRecord):
|
||||||
|
raise WhatTheFuckError('单人游戏记录是多人游戏记录')
|
||||||
|
try:
|
||||||
|
return UniMessage.image(raw=await self.make_query_image(self.user, user_info, sprint.record, blitz.record))
|
||||||
|
except TypeError:
|
||||||
|
...
|
||||||
|
# fallback
|
||||||
|
league = user_info.data.user.league
|
||||||
|
user_name = user_info.data.user.username.upper()
|
||||||
|
ret_message = ''
|
||||||
|
if isinstance(league, NeverPlayedLeague):
|
||||||
|
ret_message += f'用户 {user_name} 没有排位统计数据'
|
||||||
|
else:
|
||||||
|
if isinstance(league, NeverRatedLeague):
|
||||||
|
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
|
||||||
|
else:
|
||||||
|
if league.rank == 'z':
|
||||||
|
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
|
||||||
|
else:
|
||||||
|
ret_message += (
|
||||||
|
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
|
||||||
|
)
|
||||||
|
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
|
||||||
|
lpm = league.pps * 24
|
||||||
|
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
|
||||||
|
ret_message += f'\nAPM: {league.apm} ( x{round(league.apm/lpm,2)} )'
|
||||||
|
if league.vs is not None:
|
||||||
|
adpm = league.vs * 0.6
|
||||||
|
ret_message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
|
||||||
|
if sprint.record is not None:
|
||||||
|
ret_message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
|
||||||
|
ret_message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
|
||||||
|
if blitz.record is not None:
|
||||||
|
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
|
||||||
|
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
|
||||||
|
return UniMessage(ret_message)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def query_historical_data(user: User, user_info: InfoSuccess) -> list[TETRIOInfo.TetraLeagueHistory.Data]:
|
||||||
today = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
|
today = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
forward = timedelta(days=9)
|
forward = timedelta(days=9)
|
||||||
start_time = (today - forward).astimezone(UTC)
|
start_time = (today - forward).astimezone(UTC)
|
||||||
@@ -139,7 +224,7 @@ class Processor(ProcessorMeta):
|
|||||||
select(HistoricalData)
|
select(HistoricalData)
|
||||||
.where(HistoricalData.trigger_time >= start_time)
|
.where(HistoricalData.trigger_time >= start_time)
|
||||||
.where(HistoricalData.game_platform == GAME_TYPE)
|
.where(HistoricalData.game_platform == GAME_TYPE)
|
||||||
.where(HistoricalData.user_unique_identifier == self.user.unique_identifier)
|
.where(HistoricalData.user_unique_identifier == user.unique_identifier)
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
if historical_data:
|
if historical_data:
|
||||||
@@ -147,7 +232,7 @@ class Processor(ProcessorMeta):
|
|||||||
await session.scalars(
|
await session.scalars(
|
||||||
select(HistoricalData)
|
select(HistoricalData)
|
||||||
.where(HistoricalData.game_platform == GAME_TYPE)
|
.where(HistoricalData.game_platform == GAME_TYPE)
|
||||||
.where(HistoricalData.user_unique_identifier == self.user.unique_identifier)
|
.where(HistoricalData.user_unique_identifier == user.unique_identifier)
|
||||||
.order_by(HistoricalData.id.desc())
|
.order_by(HistoricalData.id.desc())
|
||||||
.where(HistoricalData.id < min([i.id for i in historical_data]))
|
.where(HistoricalData.id < min([i.id for i in historical_data]))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -157,13 +242,9 @@ class Processor(ProcessorMeta):
|
|||||||
historical_data = list(historical_data)
|
historical_data = list(historical_data)
|
||||||
historical_data.append(extra)
|
historical_data.append(extra)
|
||||||
|
|
||||||
class HistoricalTr(NamedTuple):
|
|
||||||
time: datetime
|
|
||||||
tr: float
|
|
||||||
|
|
||||||
histories = [
|
histories = [
|
||||||
HistoricalTr(
|
TETRIOInfo.TetraLeagueHistory.Data(
|
||||||
time=i.processed_data.user_info.cache.cached_at.astimezone(ZoneInfo('Asia/Shanghai')),
|
record_at=i.processed_data.user_info.cache.cached_at.astimezone(ZoneInfo('Asia/Shanghai')),
|
||||||
tr=i.processed_data.user_info.data.user.league.rating,
|
tr=i.processed_data.user_info.data.user.league.rating,
|
||||||
)
|
)
|
||||||
for i in historical_data
|
for i in historical_data
|
||||||
@@ -172,34 +253,11 @@ class Processor(ProcessorMeta):
|
|||||||
and isinstance(i.processed_data.user_info.data.user.league, RatedLeague)
|
and isinstance(i.processed_data.user_info.data.user.league, RatedLeague)
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_specified_point(
|
|
||||||
previous_point: HistoricalTr, behind_point: HistoricalTr, point_time: datetime
|
|
||||||
) -> HistoricalTr:
|
|
||||||
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
previous_point (HistoricalTr): 前面的数据点
|
|
||||||
behind_point (HistoricalTr): 后面的数据点
|
|
||||||
point_time (datetime): 要推算的点的位置
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HistoricalTr: 要推算的点的数据
|
|
||||||
"""
|
|
||||||
# 求两个点的斜率
|
|
||||||
slope = (behind_point.tr - previous_point.tr) / (
|
|
||||||
datetime.timestamp(behind_point.time) - datetime.timestamp(previous_point.time)
|
|
||||||
)
|
|
||||||
return HistoricalTr(
|
|
||||||
time=point_time,
|
|
||||||
tr=previous_point.tr
|
|
||||||
+ slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.time)),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 按照时间排序
|
# 按照时间排序
|
||||||
histories = sorted(histories, key=lambda x: x.time)
|
histories = sorted(histories, key=lambda x: x.record_at)
|
||||||
for index, value in enumerate(histories):
|
for index, value in enumerate(histories):
|
||||||
# 在历史记录里找有没有今天0点后的数据
|
# 在历史记录里找有没有今天0点后的数据
|
||||||
if value.time > today:
|
if value.record_at > today:
|
||||||
histories = histories[:index] + [
|
histories = histories[:index] + [
|
||||||
get_specified_point(histories[index - 1], histories[index], today.replace(microsecond=1000))
|
get_specified_point(histories[index - 1], histories[index], today.replace(microsecond=1000))
|
||||||
]
|
]
|
||||||
@@ -208,57 +266,39 @@ class Processor(ProcessorMeta):
|
|||||||
histories.append(
|
histories.append(
|
||||||
get_specified_point(
|
get_specified_point(
|
||||||
histories[-1],
|
histories[-1],
|
||||||
HistoricalTr(user_info.cache.cached_at, league.rating),
|
TETRIOInfo.TetraLeagueHistory.Data(
|
||||||
|
record_at=user_info.cache.cached_at, tr=user_info.data.user.league.rating
|
||||||
|
),
|
||||||
today.replace(microsecond=1000),
|
today.replace(microsecond=1000),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if histories[0].time < (today - forward):
|
if histories[0].record_at < (today - forward):
|
||||||
histories[0] = get_specified_point(
|
histories[0] = get_specified_point(
|
||||||
histories[0],
|
histories[0],
|
||||||
histories[1],
|
histories[1],
|
||||||
today - forward,
|
today - forward,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
histories.insert(0, HistoricalTr((today - forward), histories[0].tr))
|
histories.insert(0, TETRIOInfo.TetraLeagueHistory.Data(record_at=today - forward, tr=histories[0].tr))
|
||||||
|
return histories
|
||||||
def get_value_bounds(values: list[int | float]) -> tuple[int, int]:
|
|
||||||
value_max = 10 * ceil(max(values) / 10)
|
|
||||||
value_min = 10 * floor(min(values) / 10)
|
|
||||||
return value_max, value_min
|
|
||||||
|
|
||||||
def get_split(value_max: int, value_min: int) -> tuple[int, int]:
|
|
||||||
offset = 0
|
|
||||||
overflow = 0
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if (new_max_value := value_max + offset + overflow) > TR_MAX:
|
|
||||||
overflow -= 1
|
|
||||||
continue
|
|
||||||
if (new_min_value := value_min - offset + overflow) < TR_MIN:
|
|
||||||
overflow += 1
|
|
||||||
continue
|
|
||||||
if ((new_max_value - new_min_value) / 40).is_integer():
|
|
||||||
split_value = int((value_max + offset - (value_min - offset)) / 4)
|
|
||||||
break
|
|
||||||
offset += 1
|
|
||||||
return split_value, offset + overflow
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def make_query_image(
|
||||||
|
user: User, user_info: InfoSuccess, sprint: SoloRecord | None, blitz: SoloRecord | None
|
||||||
|
) -> bytes:
|
||||||
|
league = user_info.data.user.league
|
||||||
|
if not isinstance(league, RatedLeague) or league.vs is None:
|
||||||
|
raise TypeError
|
||||||
|
user_name = user_info.data.user.username.upper()
|
||||||
|
histories = await Processor.query_historical_data(user, user_info)
|
||||||
value_max, value_min = get_value_bounds([i.tr for i in histories])
|
value_max, value_min = get_value_bounds([i.tr for i in histories])
|
||||||
split_value, offset = get_split(value_max, value_min)
|
split_value, offset = get_split(value_max, value_min)
|
||||||
|
if sprint is not None:
|
||||||
if sprint.record is None:
|
duration = timedelta(milliseconds=sprint.endcontext.final_time).total_seconds()
|
||||||
sprint_value = 'N/A'
|
|
||||||
else:
|
|
||||||
if not isinstance(sprint.record, SoloRecord):
|
|
||||||
raise WhatTheFuckError('40L记录不是单人记录')
|
|
||||||
duration = timedelta(milliseconds=sprint.record.endcontext.final_time).total_seconds()
|
|
||||||
sprint_value = f'{duration:.1f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.1f}s' # noqa: PLR2004
|
sprint_value = f'{duration:.1f}s' if duration < 60 else f'{duration // 60:.0f}m {duration % 60:.1f}s' # noqa: PLR2004
|
||||||
if blitz.record is None:
|
|
||||||
blitz_value = 'N/A'
|
|
||||||
else:
|
else:
|
||||||
if not isinstance(blitz.record, SoloRecord):
|
sprint_value = 'N/A'
|
||||||
raise WhatTheFuckError('Blitz记录不是单人记录')
|
blitz_value = f'{blitz.endcontext.score:,}' if blitz is not None else 'N/A'
|
||||||
blitz_value = f'{blitz.record.endcontext.score:,}'
|
|
||||||
async with HostPage(
|
async with HostPage(
|
||||||
await render(
|
await render(
|
||||||
'tetrio/info',
|
'tetrio/info',
|
||||||
@@ -287,7 +327,7 @@ class Processor(ProcessorMeta):
|
|||||||
adpl=round(adpm / lpm, 2),
|
adpl=round(adpm / lpm, 2),
|
||||||
),
|
),
|
||||||
tetra_league_history=TETRIOInfo.TetraLeagueHistory(
|
tetra_league_history=TETRIOInfo.TetraLeagueHistory(
|
||||||
data=[TETRIOInfo.TetraLeagueHistory.Data(record_at=time, tr=tr) for time, tr in histories],
|
data=histories,
|
||||||
split_interval=split_value,
|
split_interval=split_value,
|
||||||
min_tr=value_min,
|
min_tr=value_min,
|
||||||
max_tr=value_max,
|
max_tr=value_max,
|
||||||
@@ -305,44 +345,7 @@ class Processor(ProcessorMeta):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
) as page_hash:
|
) as page_hash:
|
||||||
return UniMessage.image(
|
return await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
|
||||||
raw=await screenshot(urlunparse(('http', get_self_netloc(), f'/host/{page_hash}.html', '', '', '')))
|
|
||||||
)
|
|
||||||
# fallback
|
|
||||||
ret_message = ''
|
|
||||||
if isinstance(league, NeverPlayedLeague):
|
|
||||||
ret_message += f'用户 {user_name} 没有排位统计数据'
|
|
||||||
else:
|
|
||||||
if isinstance(league, NeverRatedLeague):
|
|
||||||
ret_message += f'用户 {user_name} 暂未完成定级赛, 最近十场的数据:'
|
|
||||||
else:
|
|
||||||
if league.rank == 'z':
|
|
||||||
ret_message += f'用户 {user_name} 暂无段位, {round(league.rating,2)} TR'
|
|
||||||
else:
|
|
||||||
ret_message += (
|
|
||||||
f'{league.rank.upper()} 段用户 {user_name} {round(league.rating,2)} TR (#{league.standing})'
|
|
||||||
)
|
|
||||||
ret_message += f', 段位分 {round(league.glicko,2)}±{round(league.rd,2)}, 最近十场的数据:'
|
|
||||||
lpm = league.pps * 24
|
|
||||||
ret_message += f"\nL'PM: {round(lpm, 2)} ( {league.pps} pps )"
|
|
||||||
ret_message += f'\nAPM: {league.apm} ( x{round(league.apm/lpm,2)} )'
|
|
||||||
if league.vs is not None:
|
|
||||||
adpm = league.vs * 0.6
|
|
||||||
ret_message += f'\nADPM: {round(adpm,2)} ( x{round(adpm/lpm,2)} ) ( {league.vs}vs )'
|
|
||||||
user_records = await self.get_user_records()
|
|
||||||
sprint = user_records.data.records.sprint
|
|
||||||
if sprint.record is not None:
|
|
||||||
if not isinstance(sprint.record, SoloRecord):
|
|
||||||
raise WhatTheFuckError('40L记录不是单人记录')
|
|
||||||
ret_message += f'\n40L: {round(sprint.record.endcontext.final_time/1000,2)}s'
|
|
||||||
ret_message += f' ( #{sprint.rank} )' if sprint.rank is not None else ''
|
|
||||||
blitz = user_records.data.records.blitz
|
|
||||||
if blitz.record is not None:
|
|
||||||
if not isinstance(blitz.record, SoloRecord):
|
|
||||||
raise WhatTheFuckError('Blitz记录不是单人记录')
|
|
||||||
ret_message += f'\nBlitz: {blitz.record.endcontext.score}'
|
|
||||||
ret_message += f' ( #{blitz.rank} )' if blitz.rank is not None else ''
|
|
||||||
return UniMessage(ret_message)
|
|
||||||
|
|
||||||
async def get_user(self) -> None:
|
async def get_user(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user