"""Метрика рейтинга. Вынесена отдельно — легко заменить. По умолчанию: League Points — нормированные очки за место с учётом размера стола и ничьих (competition ranking). За партию из N игроков: points = (N - place - (tie_size - 1)/2) / (N - 1) 1-е место = 1.0, последнее = 0.0; равные места делят сумму очков поровну. Рейтинговый счёт игрока — сглаженное среднее (байесовское, формула IMDB): score = (PRIOR_GAMES * PRIOR_MEAN + SUM(points)) / (PRIOR_GAMES + games) * 100 К реальным партиям «дописываются» PRIOR_GAMES виртуальных со средним PRIOR_MEAN: на малой выборке рейтинг держится около 50 и лишь с опытом сходится к чистому среднему — короткая удачная серия новичка не обгоняет стабильного ветерана. """ from __future__ import annotations # Порог числа игр для попадания в ранжированный топ (ниже — «Новички»/provisional). MIN_GAMES = 10 # Порог числа игр на фракцию для расчёта лучшей/худшей фракции. FACTION_MIN_GAMES = 2 # Сглаживание рейтинга: сколько «виртуальных» партий и с каким средним добавляем. PRIOR_GAMES = 10 PRIOR_MEAN = 0.5 # SQL-выражение сглаженного рейтинга поверх агрегата по строкам scored (s.points). # При 0 партий SUM = NULL → score = NULL (рейтинга без игр нет). SMOOTHED_SCORE_SQL = ( f"({PRIOR_GAMES} * {PRIOR_MEAN} + SUM(s.points)) / ({PRIOR_GAMES} + COUNT(*)) * 100" ) # SQL-выражение очков за участие (tie-aware). Использует поля m.player_count, # mp.place и t.tie_size (размер группы игроков с тем же местом в партии). 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: """Тот же сглаженный рейтинг, что и SMOOTHED_SCORE_SQL, но в Python. Нужен там, где строки уже вытащены и агрегировать в SQL нечего (профиль игрока). Держим рядом с SQL-версией и на одних константах: разъехавшиеся реализации одной формулы — источник расхождений, который потом ловится только глазами.""" if games <= 0: return None # рейтинга без игр нет — как SUM(...) = NULL в SQL return (PRIOR_GAMES * PRIOR_MEAN + points_sum) / (PRIOR_GAMES + games) * 100 def leaderboard_sort_key(row: dict) -> tuple: """Ключ сортировки топа: счёт ↓, winrate ↓, игры ↓, среднее место ↑, ник ↑.""" return ( -(row["score"] or 0.0), -(row["win_rate"] or 0.0), -(row["games"] or 0), (row["avg_place"] or 0.0), row["nickname"].lower(), )