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

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

Вышла версия 2.10v2.10.0

Схема турнира, лиги одним турниром и таблица сезона

Что нового

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

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

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

Статус турнира — поле 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 тик не трогает.

Видимость

Флаг 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, individual_swiss.

Состояние стадии — статус её узла потока, он отдаётся как status (и node_id, чтобы к нему обратиться): draft → ready → live → completed. Двигает его только POST /api/v1/admin/flow-nodes/{node_id}/transition с телом {"status": "…"}: draft → ready рассаживает входы стадии из её входа в потоке (ничья на границе квалификации, которую не разрешил ни один пин, — 409 boundary_tie), ready → live публикует стадию и генерирует её встречи, если их нет, ready → draft отклоняется с 409, как только по какому-то матчу сообщили результат. Одновременно может идти несколько стадий live. Воркер таблиц переводит live → completed, когда все матчи сыграны, и обратно в live, если правка результата вновь открыла матч.

is_published — то, по чему проверяются действия капитанов: он выставляется, когда стадия становится live, и сбрасывается переходом ready → draft. Все действия капитанов и pick/ban требуют его, поэтому записи в непубликованную предпросмотровую сетку отклоняются с 409.

Стадия раскладывается на элементы (stage_item) с типом group, bracket_upper, bracket_lower или single_bracket, а у элемента есть входы (stage_item_input) — по одному на слот. Вход бывает final (команда известна; source_result_id — строка результата потока, из которой её посадили, у посеянной вручную — null) или empty. Кто проходит дальше, задаёт не стадия, а узел filter (per_item_top, у двойного выбывания ещё upper для стартующих в нижней сетке) между стадиями в потоке.

ffa_league

FFA-стадия не сводит команды парами: её элементы — обычные группы, но встреча в такой группе одна на всю группу и имеет slot_count больше 2 вместо двух мест. Это лобби: от 3 до 100 участников, best_of игр, в каждой игре у каждого участника одно место и по одному значению на каждый столбец, который ведёт стадия. teams[] лобби перечисляет все места, а списки встреч по умолчанию отдают только дуэли (slot_count по умолчанию 2) — читайте лобби через его собственные маршруты:

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 — одно; форма ответа в обоих случаях одна и та же. В ней приходят 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 на воркспейс турнира.

Правила считаются из поля scoring стадии (preset points, рядом с win/draw/loss, которые платит таблица): 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

individual_swiss

Личная швейцарка ранжирует игроков, а не команды, и каждый раунд собирает их в новые команды. Пул замораживается при старте стадии: у каждого игрока одна роль, а число игроков каждой роли должно быть ровно T × число слотов этой роли при чётном T ≥ 2 команд в раунде; иначе старт отклоняется кодовым 409, и ничего не отсекается автоматически. У стадии есть max_rounds (по умолчанию 5), best_of, pairing_mode (score: каждая роль упорядочивается по личной таблице, соседи собираются в лобби, которое делится на две команды; balance: весь пул балансируется в T команд, пары случайные) и playoff_team_count K (по умолчанию 4; оба новых поля у остальных типов равны null). Следующий раунд создаёт задача пересчёта таблицы, когда текущий закрыт; уже созданный раунд при смене результата не пересобирается.

У места в раунде нет команды турнира: в teams[] поле team_id равно null, а место называется Команда <капитан>; состав, роли и капитан раунда (старший по рангу в команде) читаются из GET /api/v1/tournaments/{id}/stages/{stage_id}/individual-rounds. Личная таблица — GET /api/v1/tournaments/{id}/player-standings: у игрока position внутри роли, points, wins/draws/losses, maps_won/maps_lost/map_diff, buchholz, is_qualified (в топ-K × слотов своей роли), is_captain (один из K старших по рангу прошедших) и is_pinned. Порядок: очки, разница карт, Бухгольц (сумма по матчам игрока средних итоговых очков соперников) и ранг; место, выставленное организатором, задаёт PUT /api/v1/admin/tournaments/{tournament_id}/stages/{stage_id}/player-standing-pins.

rank игрока может следовать за результатами: rank_win_delta и rank_loss_delta (целые 0–1000, по умолчанию 0 — выключено, у остальных типов null) прибавляют первое за каждую выигранную и вычитают второе за каждую проигранную встречу стадии; ничья ничего не меняет, ранг не опускается ниже 1. В таблице рядом с rank есть start_rank (замороженный при старте стадии); rank выводится из него задачей пересчёта таблицы при каждом пересчёте — исправленный результат или изменённые значения применяются ко всем подтверждённым встречам. Следующие раунды собираются, получают капитанов и разбивают равенства по изменённому rank; ранги воркспейса не меняются.

Когда стадия завершена, POST /api/v1/admin/tournaments/{tournament_id}/stages/{stage_id}/lock-qualification закрепляет прошедших и завершает узел стадии; пул балансировщика не меняется, а кто играет дальше, решает фильтр ниже по потоку. Затем капитаны драфтуют прошедших, а плей-офф — обычная стадия с посевом по рангу капитана.

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

Встреча (encounter) — это серия между посаженными командами: slot_count и teams[] — по записи на каждого живого участника (id, slot, team_id, name, seed, score, placement), а также best_of, result_source, result_version, round, status (open, pending, completed) и result_status (none, pending_confirmation, confirmed, disputed).

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

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

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

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

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

Пороги швейцарки досрочно завершают её для команды; оба необязательны. Набрав swiss_qualify_wins побед (bye считается победой), команда проходит дальше, набрав swiss_eliminate_losses поражений — выбывает; ничья не считается ни тем, ни другим. Решённую команду больше не ставят в пары. Когда нерешённых команд остаётся меньше двух, следующий раунд не генерируется и группа считается завершённой даже до max_rounds, который остаётся жёстким пределом. Стадия с порогом никогда не разворачивается в круговую систему целиком. Матч, где одной из команд не хватает одной победы до прохода, играется как best_of.swiss_qualify, одного поражения до вылета — как best_of.swiss_eliminate (если и то и другое — берётся больший); оба значения важнее best-of раунда. Выход в следующую стадию по-прежнему берёт первые места таблицы через узел filter после стадии.

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

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

Таблица

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

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

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

В строке таблицы приходят position, overall_position, matches, win, draw, lose, points, buchholz (усечённый, он же медианный), full_buchholz (полная сумма), score_differential, tie_group, is_pinned, swiss_status (qualified / eliminated, когда команда достигла порога швейцарки, иначе null), а также 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.

Список встреч фильтруется по tournament_id, stage_id, stage_item_id, best_of, discipline, status, has_logs, is_final, is_upset, closeness_min/closeness_max и scope (all или my_team), а ищет по query:

  • status — сохранённый статус (open, pending, completed) или производный: live (идёт сейчас) и upcoming (открыта или ожидает и ещё не идёт). Любое другое значение — 400.
  • is_final=true оставляет последний положительный раунд стадии с одиночным или двойным выбыванием (в двойном — гранд-финал); false его исключает. Каждая встреча несёт и поле is_final.
  • is_upset=true оставляет завершённые дуэли, которые выиграла команда, стоящая ниже (с большим position) в том же элементе стадии; false их исключает.
  • Напряжённость — полуинтервал [closeness_min, closeness_max); при closeness_max, равном 1, он замкнут.
  • query с fields=name ищет по названию встречи (в любом порядке команд), по названиям участвующих команд и по именам игроков в их составах.
  • sort — одно из id, name, best_of, status, scheduled_at, closeness, has_logs, discipline и played_at (иначе 400). NULL в любом направлении идут последними, равенство разрешает id; played_at упорядочивает по дате игры, затем по расписанию, затем по датам турнира.

/api/v1/encounters/overview принимает те же фильтры и отдаёт счётчики: сегменты статуса (all, live, upcoming, completed) не учитывают status, а каждый переключатель (finals, upsets, with_logs, my_team) считает текущие фильтры с ним самим включённым. Ещё в ответе completed_with_logs, recent_completed (сыгранные за последние 7 дней) и до 4 идущих встреч. /api/v1/encounters/insights принимает их же, но status всегда completed, и отдаёт аналитику: гистограмму напряжённости по десятым, счета, виды стадий, самые частые карты, самые напряжённые встречи, среднюю длительность встречи и сухие победы. Доли — дроби от 0 до 1.

Полный перечень маршрутов и схемы ответов — в справочнике эндпоинтов.

Административная часть живёт под /api/v1/admin/: статус и расписание турнира, CRUD стадий, элементов и входов, flow-nodes/{node_id}/transition для жизненного цикла стадии, stages/{id}/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, и в проверке доступа к топикам: подписка на его топики анонимному клиенту не выдаётся.

См. также