Overwatch Tournaments нужны функциональные cookie, чтобы сохранять вход и язык интерфейса. Аналитические cookie необязательны и только показывают, как используются страницы, — подробнее в политике конфиденциальности.

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

Турниры, стадии и сетка

Турнир — это корневой объект соревнования внутри воркспейса: у него собственный жизненный цикл, расписание фаз, набор стадий и вычисляемая таблица. Эта статья описывает модель так, как её видит интегратор: какие состояния бывают, что их меняет, как устроена сетка и что именно считается в таблице.

Жизненный цикл

Статус турнира — поле status со значениями announcement, registration, check_in, draft, live, playoffs, completed, archived. У фаз есть канонический порядок 0…7 именно в этом перечислении; автоматика двигается только вперёд по нему.

Переходы проверяются по матрице: всё, чего в ней нет, отклоняется.

ИзРазрешённые цели
announcementregistration, check_in, draft, live
registrationcheck_in, draft, live, announcement
check_indraft, live, registration
draftlive, check_in, registration
liveplayoffs, completed, draft, check_in
playoffscompleted
completedarchived
archivedcompleted

Из матрицы следуют три правила, на которые можно опираться. Скачки вперёд легальны: registration → live — один переход, промежуточные фазы не обязательны. Откат назад существует вплоть до live, чтобы организатор мог переоткрыть фазу без привилегированного обхода. А playoffs ведёт только в completed: назад из плей-офф пути нет.

Поле is_finished хранится в базе, но выводится из статуса: оно истинно ровно для completed и archived, и переписывается на каждом переходе. Фильтровать по нему можно, считать независимым флагом — нет. «Идёт игра» — это live и playoffs; check_in сюда не входит, это ещё лобби.

Расписание фаз

У турнира есть до четырёх строк расписания: registration, check_in, draft, live. announcement, playoffs, completed и archived расписанию не подлежат — они зависят от хода игры, а не от часов. На фазу приходится ровно одна строка, у строки есть starts_at и необязательный ends_at, причём база требует ends_at строго позже starts_at.

Смысл двух полей разный, и это главное, что нужно запомнить:

  • starts_at двигает статус. Наступил момент — фаза становится текущей.
  • ends_at статус не двигает никогда. Он только закрывает окно действий этой фазы — например, чек-ин закрывается за пятнадцать минут до начала игр, но турнир при этом остаётся в check_in, пока его не переведут дальше.

Расписание отдаётся вместе с турниром в поле phase_schedule, записывается целиком: PUT /api/v1/admin/tournaments/{tournament_id}/schedule.

Что меняет статус

Ровно две вещи.

Администратор — PATCH /api/v1/admin/tournaments/{tournament_id}/status. Переход проверяется по матрице, если не передан force (обход для суперпользователя). Вход в registration дополнительно требует сохранённой формы регистрации, иначе 409 с кодом registration_form_missing: статус — это то, что читает кнопка записи, и объявлять запись без формы нельзя. Любой ручной переход в той же транзакции выключает auto_transitions_enabled, чтобы автоматика не спорила с решением организатора.

Тик сервиса турниров, раз в 30 секунд. Он двигает только вперёд, только из announcement, registration, check_in, draft и максимум до live. Если наступило сразу несколько строк расписания, турнир переводится в самую позднюю наступившую фазу, а не прогоняется по промежуточным. Каждый турнир обрабатывается в собственной транзакции под блокировкой строки, поэтому сбой на одном не мешает остальным. Турниры с auto_transitions_enabled = false тик не трогает.

У входа в live есть побочный эффект: автоматически стартует первая волна групповых стадий. Волна — это все незавершённые групповые стадии с минимальным значением order; стадии с одинаковым order считаются одной параллельной фазой (например, два дивизиона) и стартуют вместе. Если у стадии уже есть встречи, она просто активируется; если нет — ставится задача на генерацию сетки.

Видимость

Флаг is_hidden ортогонален статусу. Скрытый турнир и всё вложенное в него отвечают 404 «не найдено» — не 403, чтобы существование события нельзя было подтвердить по коду ответа. Видят его администраторы воркспейса и пользователи из списка предпросмотра (GET и POST /api/v1/admin/tournaments/{tournament_id}/preview-access, удаление — DELETE .../preview-access/{auth_user_id}).

Отдельно проверяется скрытость самого воркспейса: турнир, скрытый только из-за неё, доступен любому участнику воркспейса, а не только администраторам.

Единственное исключение из правила предпросмотра — контейнер скримов: скрытый служебный турнир, по одному на воркспейс, внутри которого живут комнаты скримов. Его читает любой участник воркспейса, потому что соперник должен открыть комнату по ссылке до того, как заявит за себя сторону. В списках турниров контейнер при этом не появляется никогда. Подробнее о комнатах — в статье Встречи, результаты и логи.

Стадии

Стадия — это один соревновательный отрезок турнира. Тип задаётся полем stage_type: round_robin, single_elimination, double_elimination, swiss, ffa_league.

Отдельного поля состояния у стадии нет — оно выводится из трёх булевых флагов, которые отдаёт API:

СостояниеУсловие
Черновиквстреч ещё нет
Предпросмотрвстречи сгенерированы, но is_published ещё false
В игреis_published или is_active
Завершенаis_completed

Разделение is_active и is_published важно для интегратора. is_active — это «текущая выбранная стадия», он гаснет, когда организатор активирует следующую. is_published — липкий: выставляется один раз при активации и больше не сбрасывается. Все действия капитанов и pick/ban проверяют именно is_published, поэтому стадия, которая уже выходила в игру, остаётся рабочей навсегда, а по непубликованному предпросмотру сетки записи отклоняются с 409.

Стадия раскладывается на элементы (stage_item) с типом group, bracket_upper, bracket_lower или single_bracket, а у элемента есть входы (stage_item_input) — по одному на слот. Вход бывает final (команда известна), tentative (место занято результатом другой стадии — есть source_stage_item_id и source_position) или empty. Сколько команд проходит дальше, задаёт advance_count на стадии, с возможностью переопределить его на отдельной группе.

ffa_league

FFA-стадия не сводит команды парами: её элементы — обычные группы, но встреча в такой группе одна на всю группу и несёт format: "ffa" вместо двух сторон. Это лобби: от 2 до 100 участников, best_of игр, в каждой игре у каждого участника одно место и по одному значению на каждый столбец, который ведёт стадия. Поля home_team_id и away_team_id у лобби пустые, и списки встреч по умолчанию его не отдают — читайте лобби через его собственные маршруты:

GET /api/v1/tournaments/42/stages/7/ffa HTTP/1.1
Host: owt.craazzzyyfoxx.me

GET /api/v1/tournaments/{id}/stages/{stage_id}/ffa отдаёт все лобби стадии, GET /api/v1/encounters/{encounter_id}/ffa — одно; форма ответа в обоих случаях одна и та же. В ней приходят advance_count (переопределение группы, иначе число стадии; null — линии выхода нет), rules и rows. Каждая строка — участник: team_id, slot, position, tie_group, points, games_played, wins, stats (сумма по каждому столбцу) и массив games по одной ячейке на позицию игры; в ячейке — placement, points и собственный stats. У неоткрытой игры state равен null — это не ноль очков, а игра, которую ещё никто не вводил. Оба маршрута публичные и отдают только ПУБЛИЧНЫЕ столбцы: столбца с public: false нет ни в rules.columns, ни в stats строки, ни в stats ячейки. Организатор читает лобби целиком через GET /api/v1/admin/tournaments/{id}/stages/{stage_id}/ffa — нужно право match.update на воркспейс турнира.

Правила считаются из поля ffa_scoring стадии: columns (что игра записывает по каждой команде — key, label, public, better; не больше 10), placement_points (очки за 1-е, 2-е, … место; за концом списка — ноль) и formula — выражение, по которому считаются очки игры. Формула читает ключи столбцов плюс place (место в этой игре), place_pts (placement_points[место − 1], ноль за концом списка) и teams (размер лобби); доступны арифметика, сравнения, and/or/not, min, max, abs, round и if(условие, то, иначе) — и больше ничего. Разбор идёт стандартным модулем ast по белому списку узлов, без eval (backend/shared/domain/ffa_formula.py); отвергнутая формула отвечает 422 с кодом (ffa_formula_syntax, ffa_formula_unknown_name, ffa_formula_unsupported, ffa_formula_too_complex) и позицией в строке. Очки игры — значение этого выражения, округлённое до 4 знаков; деление на ноль даёт ноль, отрицательные очки допустимы (столбец-штраф). Обязательно ли место при вводе игры — не настройка, а факт: читает ли формула place или place_pts. Если не читает, места выводятся из очков игры, равные очки делят место — и выводятся при каждом чтении, а не хранятся: правка формулы сдвигает места вместе с очками. Введённое организатором место хранится и переживает любую последующую правку. Очки нигде не хранятся — запись, таблица и чтение лобби считают их из сырых значений, поэтому правка формулы пересчитывает все игры стадии. Такая правка отвергается с 409, как только любой элемент стадии посеял уже начатую следующую стадию, а удаление или переименование столбца, под ключом которого уже есть значения, — с 422 ffa_column_in_use. Формула, читающая place или place_pts, отвергается с 422 ffa_formula_places_missing, пока в живой игре стадии не введены места: места такой игры выводятся из очков самой формулы, а такая формула их дать не может — сначала введите эти места.

Организатор вводит игру позиции p (POST /api/v1/admin/encounters/{id}/ffa/games/{position}/results), аннулирует её (…/cancel) или меняет число игр лобби (…/ffa/games-count). Лобби завершается само, когда подтверждённых живых игр стало не меньше best_of.

GET /api/v1/tournaments/42/stages HTTP/1.1
Host: owt.craazzzyyfoxx.me

Сетка: встречи и связи

Встреча (encounter) — это серия best-of между двумя командами: home_team_id, away_team_id, home_score, away_score, best_of, round, status (open, pending, completed) и result_status (none, pending_confirmation, confirmed, disputed).

Сетка — это не подразумеваемое дерево, а явные рёбра. Каждое ребро говорит: победитель или проигравший (role: winner / loser) встречи-источника занимает слот home или away встречи-цели. Входящие рёбра отдаются на встрече в поле sources, поэтому пустой слот можно подписать («проигравший матча 3»), не угадывая форму сетки по номерам раундов.

Генерация зависит от типа стадии:

  • Круговая — метод кругов, bye пропускается; рёбер продвижения нет вовсе.
  • Одиночное и двойное выбывание — скелет сетки плюс рёбра. В двойном выбывании нижнюю сетку можно дополнительно засеять командами из групп: advance_upper_count у групповой стадии (или у отдельной группы) задаёт, сколько из проходящих стартуют в верхней сетке; остальные стартуют в элементе bracket_lower. Вместо сгенерированной схемы админ может нарисовать свою (Stage.bracket_template): слоты с местами под посевы U#/L#; превью, план раундов и генерация строятся по ней, а редактировать её можно, только пока у стадии нет матчей.
  • Швейцарка — раунд за раундом.

Гранд-финальный сброс в двойном выбывании создаётся лениво. Стадия должна быть настроена как de_grand_final_type = "with_reset", и матч появляется только в момент, когда гранд-финал выигрывает команда, пришедшая из нижней сетки. Если победил представитель верхней сетки, турнир заканчивается на гранд-финале, а заготовка сброса удаляется.

Швейцарская жеребьёвка работает так. Команды сортируются по очкам и Бухгольцу; группы с большим числом очков разбираются первыми; внутри группы предпочитаются пары «верхняя половина против нижней», а соперник из своей группы — предпочтительнее спуска в соседнюю. Повторы пар избегаются, причём генератор заглядывает на раунд вперёд: локально красивая жеребьёвка может загнать поле в состояние, где следующий раунд без повторов уже не собрать. Если поле действительно доигралось до угла, раунд всё равно строится — с минимальным числом повторов, потому что полное поле важнее первой встречи, — и только при полной невозможности собрать раунд возвращается ошибка. При нечётном числе команд bye достаётся самой низкой в таблице команде, у которой его ещё не было.

Продвижение срабатывает, когда встреча стала completed и счёт не ничейный. Оно идемпотентно: если в целевом слоте уже стоит та же команда, ничего не происходит. Ничья не может завершить встречу в сетке на выбывание — такой счёт отклоняется с 400, иначе рёбра не сработают и сетка встанет.

Если результат выше по сетке исправили и победитель сменился, всё, что старый результат успел продвинуть, каскадно сбрасывается: целевая встреча «разыгрывается обратно», и то же самое происходит с её собственными потомками. В журнале результатов такие переходы помечены действием cascade_reset, а инициировавшее действие администратора — reopen.

Таблица

Таблица считается по стадии и отдаётся на GET /api/v1/tournaments/{id}/standings.

Главное правило: учитываются только полностью закрытые раунды. Раунд считается играбельным, если обе команды в нём известны; если хотя бы одна играбельная встреча раунда ещё не завершена, весь раунд в расчёт не идёт. Иначе форма команды убегала бы вперёд собственных побед и поражений.

Очки берутся из турнира — win_points (по умолчанию 1.0), draw_points (0.5), loss_points (0.0) — и могут быть переопределены на стадии полем scoring (win, draw, loss; член со значением null наследует значение турнира). Швейцарский bye по умолчанию оценивается как победа; отдельное значение задаётся полем стадии swiss_bye_points.

В строке таблицы приходят position, overall_position, matches, win, draw, lose, points, buchholz (усечённый, он же медианный), full_buchholz (полная сумма), score_differential, tie_group, is_pinned, а также tiebreak_order и source_rule_profile — применённый порядок тай-брейков и профиль, из которого он взят.

Сам порядок тай-брейков задаётся полем стадии tiebreak_order, а пока оно равно null, берётся пресет — ranking_preset стадии или пресет по её типу. Известные метрики: points, match_wins, head_to_head, buchholz, median_buchholz, score_differential, map_differential, wins_as_higher_stage_specific_metric. Неизвестные строки из настроенного порядка отбрасываются, дубликаты схлопываются до первого вхождения.

На FFA-стадии таблица группы считается по её лобби, и метрики другие — у лобби нет соперника, поэтому личные встречи и обе разновидности Бухгольца там не вычисляются вовсе. Доступны ffa_game_wins (число игр, выигранных с 1-го места; общее первое место считается), ffa_best_placement (лучшее место), ffa_last_placement (место в последней сыгранной игре) и по одной метрике на каждый столбец стадии — ffa_stat:<ключ>, сумма этого столбца. Столбец с better: "lower" (смерти, штрафы) сортируется по возрастанию. Метрики мест и суммы столбцов с better: "lower" возвращаются со знаком минус, чтобы сортировка «больше — лучше» оставалась одна на все метрики. Запись ffa_stat: с ключом столбца, которого на стадии больше нет, отбрасывается, как любая неизвестная метрика. Пресет по умолчанию — ffa_default: points, ffa_game_wins, ffa_stat:<первый столбец> (если он есть), ffa_last_placement.

Места, закреплённые организатором, хранятся в tournament.standing_pin: одна строка на закреплённую команду в каждой таблице, где таблица — пара (stage_id, stage_item_id). Закрепления применяются после ранжирования: закреплённая команда занимает своё место, незакреплённые заполняют свободные места в вычисленном порядке. Соседние незакреплённые команды, делившие место в плей-офф, продолжают его делить. У закреплённой строки выставлен is_pinned, и она никогда не входит в tie_group — её место решено, а не осталось неразведённым. PUT /api/v1/admin/stages/{stage_id}/standing-pins с телом { "stage_item_id": …, "pins": [{ "team_id": …, "position": … }] } заменяет все закрепления одной таблицы (пустой список снимает их) и отвечает 202 с задачей пересчёта. Команда не из этой таблицы, место за её концом или дубликат дают 422. Когда плей-офф, посеянный из группы, уже начался, изменение, затрагивающее её места выхода или снимающее закрепление, отклоняется с 409.

Пересчёт таблицы — фоновая задача: POST /api/v1/admin/standings/recalculate/{tournament_id} отвечает 202, статус задачи читается через /api/v1/admin/tournament-jobs. Для швейцарки пересчёт заодно генерирует следующий раунд, когда текущий закрылся.

Что читать и на что подписываться

Публичные чтения — /api/v1/tournaments, /api/v1/tournaments/{id}, /api/v1/tournaments/{id}/stages, /api/v1/tournaments/{id}/standings, /api/v1/encounters, /api/v1/encounters/{id}. Список турниров фильтруется по workspace_id, status и is_league; список встреч — по tournament_id, stage_id, stage_item_id, best_of, status, has_logs, closeness_min/closeness_max и scope (all или my_team). Полный перечень маршрутов и схемы ответов — в справочнике эндпоинтов.

Административная часть живёт под /api/v1/admin/: статус и расписание турнира, CRUD стадий, элементов и входов, activate-and-generate для генерации сетки (202, задача), пересчёт таблицы.

Изменения приходят не отдельными событиями на каждый объект, а именами протухших ресурсов на топике tournament:{id}:invalidation. Для этой статьи существенны tournament.detail, tournament.stages, tournament.encounters, tournament.standings, tournament.teams и tournament.structure — последний означает, что изменился сам набор разделов страницы турнира. Механика подписки и доигрывания описана в статье Realtime (WebSocket).

{ "resources": ["tournament.encounters", "tournament.standings"] }

Скрытый турнир отдаёт 404 и на HTTP, и в проверке доступа к топикам: подписка на его топики анонимному клиенту не выдаётся.

См. также