Балансировщик, драфт и миксы
Собрать команды можно двумя способами: отдать пул солверу или провести живой драфт капитанов. Турнир выбирает один из них, и рядом живут миксы — кастомки воркспейса со своим, более простым движком. Здесь описано, что каждый из них оптимизирует, какие состояния бывают и как это выглядит снаружи; полный список маршрутов — в справочнике эндпоинтов.
Способ формирования команд
tournament.team_formation — строка со значением по умолчанию balancer. В коде используются два значения:
| Значение | Что происходит |
|---|---|
balancer | Пул уходит солверу, организатор принимает один из вариантов и экспортирует его в турнирные команды |
draft | Капитаны разбирают пул вживую, экспорт идёт из завершённой сессии драфта |
Команды, которые капитаны собрали ещё на регистрации, — третий, отдельный путь экспорта в турнирные команды; он описан в статье Регистрация и составы. Менять team_formation или форму ростера, пока активна сессия драфта, нельзя — запрос отвечает 400 и называет статус мешающей сессии.
Балансировщик
Общая подготовка
Прежде чем запустится любой из движков, пул проходит одну и ту же подготовку.
- Игроки, которые не могут сыграть ни одну нужную роль, отбрасываются. Если на какую-то роль формы ростера нет ни одного способного игрока — ошибка.
- Число команд считается как целая часть от деления размера пула на размер команды, а затем ограничивается потолком конкретного движка.
- Остаток не блокирует запуск — лишние просто сидят. Игроки, помеченные
must_play, в остаток не попадают: их переносят в начало перед отсечением, и только если их больше, чем есть слотов, запрос отказывает. Внутри остальных порядок отсечения задаётrotation_priorityпо возрастанию. - Если капитаны включены, верхние по рейтингу игроки помечаются капитанами и закрепляются за ролью: первой подходящей из их собственных предпочтений, иначе первой доступной. Ранжирование капитанов детерминированное — по убыванию максимального рейтинга, при равенстве по идентификатору игрока.
- Дальше идёт поиск допустимого распределения ролей с откатом, который никогда не выселяет капитана. Не нашлось — ошибка с объяснением, каких ролей не хватает.
Турнирный движок
Турнирный солвер — многокритериальный эволюционный поиск на Rust, который строит произвольное число команд и оптимизирует две цели одновременно:
- баланс — разброс суммарной силы команд, худший разрыв между командами, разброс средних, перекос по ролевым линиям, разброс внутри команды и отдельно посчитанный танковый баланс (разрыв между соседними по силе танками и разброс танковой линии), плюс штраф за пары низкоранговых игроков в одном составе;
- комфорт — ролевой дискомфорт: ноль за флекс-игрока на любой играбельной роли, растущая цена за роль ниже приоритета, отдельная большая цена за играбельную, но нежеланную роль и ещё большая — за неиграбельную. Сюда же входят худшая боль по турниру, средний по командам максимум боли и столкновения саб-ролей в одном составе.
Результат — набор недоминируемых вариантов, из которых организатор выбирает. У каждого запуска есть жёсткий предел по времени (time_limit_ms, по умолчанию и максимально 600000 мс): по его исчерпании эволюция и полировка останавливаются и отдаётся лучшее найденное. Это осознанный размен воспроизводимости на задержку — обычные запуски сходятся задолго до предела.
Движок миксов
Движок миксов — вендоренное C++-ядро, которое перебирает все допустимые разбиения перебором по битовым маскам с ранним отсечением, и работает ровно на двух равных командах. Он собирается только под Linux; там, где расширение недоступно, расчёт микса молча уходит на турнирный движок.
Ручка mix_comfort_tilt наклоняет качество между «поровну по силе» и «побольше желаемых ролей»; середина воспроизводит собственные веса движка. Порог отсечения кандидатов — не требование: если лучшее разбиение оказалось выше него, движок отвечает ошибкой, и запрос повторяется без порога. Поскольку перебор уже закончен к моменту усечения, миксу отдаётся глубокий список вариантов, а не десяток.
Как это вызвать
Турнирный пул считается на сервере: тело запроса не несёт игроков вовсе, поэтому браузер не может подсунуть алгоритму другие ранги, чем видит драфт.
POST /api/v1/balancer/tournaments/42/balance HTTP/1.1
Host: owt.craazzzyyfoxx.me
Authorization: Bearer <токен сессии или API-ключ>
Content-Type: application/json
{}
Ответ — 202 и job_id со ссылками status_url / result_url; статус читается как queued, running, succeeded или failed. Есть и синхронный близнец для интеграций — POST /api/v1/balancer/balance?workspace_id=1, где весь пул едет в player_data, форма ростера в role_mask, а команды возвращаются в том же ответе. Из воркспейса он не читает ничего, бюджет солвера у него срезан до 60 секунд, и пул, который в них не укладывается, отвечает 504 — такой считается асинхронно.
Живой драфт
Сессия
Состояния сессии: setup → ready → live ⇄ paused → completed | cancelled.
Форматы — snake, linear, custom. В custom у каждого раунда своё правило: linear, reverse, weakest_first, strongest_first, team_avg_asc, team_avg_desc. Порядок капитанов задаётся отдельно: manual (порядок выбора), weakest_first, strongest_first или random — детерминированное перемешивание Mulberry32 с сидом из настроек, чтобы предпросмотр и подтверждение всегда сходились. Стратегия автопика — best_fit (по умолчанию), best_available или role_need.
Число раундов не передаётся: оно выводится из формы ростера как team_size - 1. Форму, для которой раундов не остаётся, драфт не принимает.
Часы и конкуренция
Часы серверные. Их ведёт отдельный супервизор, владение сессионными часами удерживается распределённой блокировкой с TTL 10 секунд и продлением раз в 3 секунды, а клиенту остаётся только отрисовывать присланный дедлайн.
Ручной пик и автопик — один и тот же условный UPDATE, ограниченный одновременно состоянием «на часах» и оптимистичным токеном version. Ровно один писатель обновляет строку; проигравший получает 409 pick_already_resolved.
POST /api/v1/balancer/draft/picks/981/select HTTP/1.1
Host: owt.craazzzyyfoxx.me
Authorization: Bearer <токен сессии>
Content-Type: application/json
{ "player_id": 314, "expected_version": 17, "target_role": "damage" }
Тот же expected_version требует и автопик. Отдельно от него draft_session_stale (409) сторожит настройку сессии: если сид пересобрали, предпросмотр подтвердить нельзя.
Автопик и паузы
Автопик выбирает только из пар «игрок — роль», признанных безопасными. Безопасность считается двудольным паросочетанием между оставшимися слотами и доступными игроками, и гипотетический пик проверяется до применения — чтобы он не обнулил чужой слот. Если безопасного варианта нет, драфт не делает принудительный пик: он встаёт на паузу с причиной role_shortage.
Вторая причина паузы — order_recalculated. Правила team_avg_asc и team_avg_desc пересаживают раунд по живым средним в момент его начала, поэтому очередь на первом пике такого раунда может измениться, и драфт останавливается, чтобы это увидели.
Кандидаты берутся из ростеров того же движка, что и баланс: игрок оценивается по рангу той роли, которую реально займёт, а игрок без единого ранга в кандидаты не попадает вовсе — вместо того чтобы стать «последним с нулём». Сама формула фита проще балансировщика намеренно: автопик и подсказки должны отвечать синхронно и детерминированно.
Миксы и кастомки
Статусы кастомки — draft, balanced, completed, cancelled. Участие игрока в пуле — must_play, pool или benched; роли берутся либо все ранжированные (all_ranked), либо явно перечисленные (explicit).
Пределы: не больше 8 команд и не больше 16 со-хостов на игру. Права — отдельная системная роль воркспейса host: это всё, что есть у обычного участника, плюс полное владение ресурсом custom_game; одного членства в воркспейсе для создания микса недостаточно. Писать в пул может только хост, часть действий доступна и со-хостам.
Рекомендация ротации на следующую карту ранжирует пул по одному ключу, по порядку: самая длинная текущая серия пропусков, затем самая короткая серия сыгранных подряд, затем меньше всего сыгранных карт, затем исходный порядок для детерминизма. Закреплённые must_play всегда получают место и стоят впереди всех. Если мест хватает на весь пул или ни у кого из незакреплённых ещё нет истории, все возвращаются как neutral: выдавать стабильность сортировки за вывод о справедливости нельзя.
Realtime
Состояние драфта и балансировщика едет по разным топикам.
| Топик | Что несёт |
|---|---|
tournament:{id}:draft | События драфта: draft.pick_started, draft.paused, draft.resumed, draft.blocked, draft.completed, draft.cancelled, draft.session_updated, draft.player_updated |
tournament:{id}:balancer | Правки данных (balancer.registrations_changed, balancer.balance_saved, balancer.teams_changed, balancer.config_changed) и жизненный цикл задач (balancer_job.queued / running / progress / succeeded / failed) |
draft:{session_id}:chat | Чат комнаты драфта — отдельный топик с собственными правами; пересев сессии создаёт новую комнату |
workspace:{id}:pickup_mix | События миксов воркспейса |
Тики прогресса задачи не сохраняются — поздно подключившийся клиент берёт снимок из статуса задачи. Эфемерны также balancer.presence, который шлюз синтезирует из живых соединений, и balancer.drag — единственный тип события, который шлюз принимает от клиента.
Экспорт команд объявляет устаревание отдельно, как инвалидацию турнирных ресурсов: сами события драфта ничего не инвалидируют, потому что пик пишет только в таблицы драфта.
См. также
- Регистрация и составы — откуда берётся пул, ранги и форма ростера.
- Realtime (WebSocket) — рукопожатие, подписки и доигрывание событий.
- Турниры, стадии и сетка — куда попадают собранные команды.
- Справочник эндпоинтов — полный список маршрутов.