Рейтинг: витрины статистики на проигрывании истории

SQL-агрегат League Points (SCORED_CTE, сглаженное среднее) заменён
проигрыванием завершённых партий по порядку played_at, finished_at, id:
load_history — один запрос на партии с участниками, scoring.replay считает
рейтинг. Цепочки две: общая (все партии) и групповая (только партии группы,
K — по партиям внутри группы). Правка или удаление прошлой партии
пересчитывает всё после неё без отдельной логики.

Топ, профиль, главная и статистика группы берут игры, победы, винрейт
и среднее место из той же истории; score — рейтинг целым числом, сортировка
по неокруглённому. Главная грузит историю один раз. Лучшая партия — наибольший
ΔR в общей цепочке, лучшая/худшая фракция — средний S − E. League Points
из scoring.py удалён.

Тесты: дуэль новичков ±32, ранжирование после 10 партий, пересчёт после
правки прошлой партии, отдельная групповая цепочка, лучшая партия по ΔR,
одна загрузка истории на главной. #23

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
2026-09-14 22:38:57 +03:00
co-authored by Claude Opus 5
parent 8aa6bb7235
commit 6ea4a67672
7 changed files with 380 additions and 291 deletions
+5 -25
View File
@@ -203,33 +203,13 @@ def replay(matches: Iterable[RatedMatch]) -> Replay:
return out
# ─── League Points (прежняя система; уходит вместе с SQL-витринами) ──────────
PRIOR_GAMES = 10
PRIOR_MEAN = 0.5
SMOOTHED_SCORE_SQL = (
f"({PRIOR_GAMES} * {PRIOR_MEAN} + SUM(s.points)) / ({PRIOR_GAMES} + COUNT(*)) * 100"
)
MATCH_POINTS_SQL = (
"CASE WHEN m.player_count > 1 "
"THEN (m.player_count - mp.place - (t.tie_size - 1) / 2.0) "
"/ (m.player_count - 1) "
"ELSE 1.0 END"
)
def smoothed_score(points_sum: float, games: int) -> float | None:
if games <= 0:
return None
return (PRIOR_GAMES * PRIOR_MEAN + points_sum) / (PRIOR_GAMES + games) * 100
def leaderboard_sort_key(row: dict) -> tuple:
"""Ключ сортировки топа: счёт ↓, winrate ↓, игры ↓, среднее место ↑, ник ↑."""
"""Ключ сортировки топа: рейтинг ↓, winrate ↓, игры ↓, среднее место ↑, ник ↑.
row["rating"] — рейтинг без округления: два игрока с одинаковым целым в топе
всё равно упорядочены по настоящему значению."""
return (
-(row["score"] or 0.0),
-(row["rating"] or 0.0),
-(row["win_rate"] or 0.0),
-(row["games"] or 0),
(row["avg_place"] or 0.0),