"""Метрика рейтинга. Вынесена отдельно — легко заменить. По умолчанию: League Points — нормированные очки за место с учётом размера стола и ничьих (competition ranking). За партию из N игроков: points = (N - place - (tie_size - 1)/2) / (N - 1) 1-е место = 1.0, последнее = 0.0; равные места делят сумму очков поровну. Рейтинговый счёт игрока = AVG(points) * 100. """ from __future__ import annotations # Порог числа игр для попадания в ранжированный топ (ниже — «Новички»/provisional). MIN_GAMES = 3 # Порог числа игр на фракцию для расчёта лучшей/худшей фракции. FACTION_MIN_GAMES = 2 # 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 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(), )