Перейти к содержимому

Рейтинг — пакет формул и начисление

Все рейтинговые формулы живут только в пакете repos/poker-api/packages/rating — чистые функции без обращений к базе, сети и времени. Это не стилистика: по этим числам игроки сравнивают себя друг с другом, поэтому формулы обязаны быть проверяемы отдельно от всего остального. Пакет покрыт тестами на 100%, и это условие, а не достижение.

Правило простое: если формула появилась вне этого пакета — это ошибка.

Одна структура RatingConfig со значениями по умолчанию:

Поле Значение Что задаёт
pointsBase 10 множитель формулы очков
minPoints 1 минимум очков за участие
xpLevelBase 50 база порога уровня
xpLevelExponent 1.5 показатель порога уровня
skillAlpha 0.1 сглаживание силы
skillInitial 0.4444 стартовая сила, даёт отображаемые 5.0
strikesToBan 2 страйков до блокировки записи
strikeWindowDays 30 окно подсчёта страйков
banDays 14 длительность блокировки

Значения меняет админ платформы через GET/PUT /admin/rating-config. Обратите внимание: пороги страйков и бана лежат здесь же, а не в модуле записи — это одна настроечная поверхность.

Очки сезона. round(pointsBase * sqrt(N) * (N - place + 1) / N), не меньше minPoints. Здесь N — число реально игравших, place — от 1 до N. При N <= 0 или месте вне диапазона бросается RangeError.

Перформанс. (N - place) / (N - 1); при N === 1 возвращает 1.

Новая сила. prev + skillAlpha * (perf - prev), зажатая в [0, 1]. Это экспоненциальное сглаживание: один турнир смещает силу на десятую часть разрыва между текущим значением и результатом.

Отображаемая сила. 1 + 9 * skill, округление до 0.1. Отсюда стартовые 0.4444 → 5.0.

Порог уровня. reachXp(L) = round(xpLevelBase * (L - 1) ^ xpLevelExponent), причём reachXp(1) === 0. Уровень по XP — максимальный L, для которого reachXp(L) <= xp; цикл ограничен MAX_LEVEL = 1000 как защита от зацикливания на аномальном значении.

XP начисляется один в один с очками сезона.

Метод и путь Что отдаёт
POST /tournaments/{id}/confirm-results подтверждение результатов — момент начисления
GET /tournaments/{id}/results итоги турнира
GET /players/{userId}/results результаты игрока
GET /leaderboards/season сезонный лидерборд
GET /leaderboards/season/summary топ-3 и строка игрока
GET /leaderboards/level уровневый, топ-100 по XP
GET /seasons/current, GET /seasons сезоны
GET/PUT /admin/rating-config настройки платформы

Пути даны без префикса /api/v1.

Сезон — календарный квартал. ensureCurrentSeason() идемпотентен: гонка ловится по уникальному startsOn. Вызывается и джобой season-rollover (5 0 * * * UTC), и лидербордами — то есть первый же запрос после полуночи заведёт квартал, даже если джоба не отработала.

  • Начисление происходит только при подтверждении результатов и необратимо для клуба. Пересчёта задним числом нет — исправление делает платформа вручную.
  • Нокауты не учитываются. В рейтинге есть только места и победы.
  • Уровневый лидерборд ограничен топ-100, сезонный отдаётся целиком.