add per-game accuracy and outcome table to stats/notifications
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 31s

Games-of-period responses can now include a per-game breakdown
(include_games) with Lichess post-analysis accuracy, plus which side
the tracked player was on and their rating change for that game.

- lichess_client.py requests accuracy=true from Lichess; stats_service
  computes per-mode average accuracy and builds GameRow entries
  (tracked_is_white, tracked_rating_diff) for blitz/rapid/classical
- /games/{username}/period gained an include_games query param
- formatters.py renders a column-aligned monospace table per game:
  outcome circle (win/loss/draw relative to the tracked player),
  rating change, accuracy, names/ratings, result — for today/yesterday
  and periodic notifications; week keeps an aggregate accuracy line
- usernames are Markdown-escaped before formatting since messages are
  now sent with parse_mode='Markdown'
This commit is contained in:
vrubelroman 2026-07-03 09:38:51 +00:00
parent 4a783225af
commit d479e14bf9
8 changed files with 364 additions and 75 deletions

View file

@ -13,7 +13,7 @@ Lichess Statistics API - Модели данных
"""
from pydantic import BaseModel, Field
from typing import Dict, Optional, Any
from typing import Dict, List, Optional, Any
# =============================================================================
# МОДЕЛИ СТАТИСТИКИ ЗАДАЧ (ПАЗЛОВ)
@ -125,6 +125,7 @@ class GamePlayer(BaseModel):
user: Optional[Dict[str, Any]] = Field(None, description="Информация о пользователе")
rating: Optional[int] = Field(None, description="Рейтинг игрока")
ratingDiff: Optional[int] = Field(None, description="Изменение рейтинга")
analysis: Optional[Dict[str, Any]] = Field(None, description="Пост-анализ партии (только если игрок запускал анализ на Lichess), содержит поле 'accuracy'")
class Game(BaseModel):
"""
@ -147,6 +148,23 @@ class Game(BaseModel):
winner: Optional[str] = Field(None, description="Победитель (white, black или null)")
moves: str = Field(..., description="Ходы игры в PGN формате")
class GameRow(BaseModel):
"""
Одна отдельная партия для построчного разбора (today/yesterday/периодические уведомления).
Порядок игроков как в партии (белые/чёрные), а не "отслеживаемый пользователь/соперник".
"""
white_name: str = Field(..., description="Имя игрока белыми")
black_name: str = Field(..., description="Имя игрока чёрными")
white_rating: Optional[int] = Field(None, description="Рейтинг белых")
black_rating: Optional[int] = Field(None, description="Рейтинг чёрных")
white_accuracy: Optional[float] = Field(None, description="Точность белых, если партия анализировалась")
black_accuracy: Optional[float] = Field(None, description="Точность чёрных, если партия анализировалась")
result: str = Field(..., description="Результат партии: '1-0', '0-1' или '1/2-1/2'")
created_at: int = Field(..., description="Время создания партии (Unix timestamp в миллисекундах)")
tracked_is_white: Optional[bool] = Field(None, description="True, если отслеживаемый пользователь играл белыми; False — чёрными; None — не удалось определить сторону")
tracked_rating_diff: Optional[int] = Field(None, description="Изменение рейтинга отслеживаемого пользователя по итогам этой партии")
class GameStats(BaseModel):
"""
Статистика игр по конкретному типу (Bullet, Blitz, Rapid и т.д.).
@ -163,6 +181,8 @@ class GameStats(BaseModel):
draws: int = Field(..., description="Количество ничьих", example=1)
rating_change: int = Field(..., description="Общее изменение рейтинга", example=15)
rating: Optional[int] = Field(None, description="Итоговый рейтинг после последней игры (только если games_played > 0)", example=2850)
accuracy: Optional[float] = Field(None, description="Средняя точность отслеживаемого пользователя по проанализированным партиям этого режима; None если нет проанализированных партий", example=82.5)
games: Optional[List[GameRow]] = Field(None, description="Построчный список отдельных партий (только blitz/rapid/classical), заполняется только если запрошено include_games=True")
class GamesOfPeriodStats(BaseModel):
"""