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

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

Встречи, результаты и логи

Результат в OWT живёт на трёх уровнях, и путать их дорого: встреча — это серия, игра серии — это позиция в ней и единственный носитель решения о счёте, а матч — это разобранный лог одной карты и носитель статистики. Статья объясняет все три, а также отчёты капитанов, pick/ban, скримы и приём логов.

Три уровня

ОбъектЧто этоЧто на нём авторитетно
Встреча (encounter)серия best-of между двумя командамиитоговый счёт серии и продвижение по сетке
Игра серии (encounter_game)одна позиция серии: её карта и её результатпринятый счёт карты, result_source
Матч (matches.match)разобранный лог одной сыгранной картыстатистика, килл-фид, события

Ключевое: матч не решает результат. Он фиксирует, что лог наблюдал; игра серии фиксирует, что турнир решил. Отчёты капитанов строк matches.match больше не создают — они приходят только из разбора логов. Поле source у матча может быть log_parser или captain_report, но записывается сегодня только log_parser; значение captain_report осталось у исторических строк. Флаг has_logs у встречи вычисляется как «существует матч с source = log_parser», поэтому он не загорается от одних капитанских подтверждений.

Игры серии

Игра серии — это position (с единицы), map_id, state, принятый счёт и его происхождение:

  • state: planned (позиция есть, карта ещё не выбрана), awaiting_result (карта известна, ждём заявки сторон), disputed (заявки разошлись), confirmed (счёт принят), cancelled (история после отмены или сброса — такая позиция не оживает).
  • accepted_home_score / accepted_away_score, result_source (captain_agreement, admin, admin_log), confirmed_at.
  • result_version растёт на каждой записи принятого результата (подтверждение, коррекция) и никогда не уменьшается — по нему клиенты и события отличают свежий результат от устаревшего.

База гарантирует две вещи: среди неотменённых игр на встречу приходится не больше одной игры на позицию, а состояние confirmed требует полного набора — оба принятых счёта, result_source и confirmed_at.

Отчёты по карте

Каждая сторона подаёт свою заявку на конкретную игру, не на карту: серия может сыграть одну и ту же карту дважды, и различает их только позиция.

POST /api/v1/encounters/512/games/1841/report HTTP/1.1
Host: owt.craazzzyyfoxx.me
Authorization: Bearer <токен>
Content-Type: application/json

{ "home_score": 2, "away_score": 1 }

На игру приходится максимум одна заявка со стороны (home или away); счёт всегда в ориентации встречи, а не подающего. Дальше:

  • Заявки совпали — игра переходит в confirmed с result_source = captain_agreement, счёт серии пересчитывается, и открывается следующая карта (для прогрессивных раундов pick/ban это и есть сигнал «карта доиграна»).
  • Заявки разошлись — игра переходит в disputed, обе заявки остаются на месте, организатор получает уведомление.
  • Игра уже confirmed — заявка отклоняется с кодом result_locked.
  • Сетка стадии — предпросмотр — запись отклоняется с 409: пока стадия не опубликована, капитаны по ней не отчитываются.

Изменить подтверждённый результат может только администратор: POST /api/v1/admin/encounters/{encounter_id}/games/{game_id}/result, и коррекция обязана нести причину, которая уходит в журнал. Коррекция помнит о зависимостях: результат позиции N — это то, что открыло баны карты N+1, поэтому смена победителя N делает следующий раунд неправильным (его открыла проигравшая сторона). Такой раунд пересобирается на новом исходе, но только пока он не тронут: если в нём уже банили или по более поздней позиции есть заявка, коррекция отклоняется с downstream_started, а не стирает настоящую историю. Коррекция, не меняющая исход (2:1 → 3:1), не трогает ни один раунд.

Журнал результатов читается на GET /api/v1/admin/encounters/{encounter_id}/result-audit. Действия: confirm, reopen, auto_confirm, auto_dispute, import, cascade_reset и три «игровых» — game_confirm, game_correct, game_cancel.

Счёт серии

Счёт серии — это число подтверждённых неотменённых игр, выигранных каждой стороной. Пока встреча идёт, он материализуется в home_score / away_score встречи на каждом принятом результате. Как только встреча стала completed, писать её счёт имеет право только финализация — единственный путь, через который проходят автоподтверждение капитанов, решение администратора и импорт из Challonge. Она же запускает продвижение по сетке (см. Турниры, стадии и сетка).

Отчёт по серии

Помимо покарточных заявок есть отчёт о серии целиком — по одному на пару «встреча, команда». В нём home_score/away_score (снова в ориентации встречи), необязательная оценка напряжённости closeness от 1 до 10, комментарий, произвольные текстовые поля организатора и коды матчей по картам.

Два отчёта с одинаковым счётом автоматически подтверждают встречу: result_status становится confirmed, closeness встречи — среднее двух оценок, делённое на 10 (то есть 0…1), и включается продвижение по сетке. Разный счёт даёт disputed. Пока отчёт один, result_status — pending_confirmation. После confirmed менять результат может только администратор (POST /api/v1/admin/encounters/{encounter_id}/result, сброс — .../result/reopen).

Состав формы настраивается на турнире: встроенные поля closeness, map_codes, comment включаются и делаются обязательными по отдельности, плюс произвольные текстовые поля. Выключенные поля из присланного тела отбрасываются, а не вызывают 422.

  • GET /api/v1/encounters/{encounter_id}/my-role — какая сторона у текущего пользователя.
  • POST /api/v1/encounters/{encounter_id}/report — подать отчёт о серии.
  • GET /api/v1/encounters/{encounter_id}/reports — прочитать отчёты.

Pick/ban

Один движок обслуживает два вида: map и hero (kind едет сегментом пути). Конфигурация ищется по каскаду турнир → стадия → раунд, отдельно для каждого вида: на одном уровне могут сосуществовать карта и герои. Уровень с пустым пулом — это шаблон правил, а не комната: сессия по нему не открывается.

Режимы: pool (один общий пул) и slots (по слоту на карту серии, в каждом не меньше двух кандидатов, у слота может быть резервная карта на случай ничьей). Разыгрывается набор правил (ruleset_json): фазы, которые выбираются условием when по раунду, и шаги внутри них. Шаг задаёт action (бан/пик/защита/решающий), actors (first, second, both, home, away, winner_prev, loser_prev, system), сколько вариантов берёт каждая ходящая сторона (count/min), идёт ли он вслепую (blind), указывает ли каждый вариант игрока соперника (target), на сколько карт живёт бан (lifetime), таймер и политику таймаута, лимит переигровок, а также деревья условий для допустимых вариантов (eligible) и constraints. Вместо шагов фаза может нести generator (bracket, slot_veto), который строит шаги вето карт по best_of и размеру пула.

Сессия открывается, только когда сошлось всё сразу: обе команды известны, стадия опубликована, конфигурация найдена и её пул не пуст, в слотовом режиме слотов хватает на best_of и каждый заполнен, обе стороны нажали готовность (POST /api/v1/encounters/{encounter_id}/ready — одно подтверждение покрывает оба вида), а для героев раунда N уже определена карта N. Если сессии нет, состояние возвращает причину: teams_unknown, bracket_preview, not_configured, slot_count_mismatch, slot_underfilled, not_ready, waiting_map. Создание сессии — это ЗАПИСЬ: её делает починщик комнаты сразу после записи, снявшей последнее ограничение (и фоновая проходка раз в минуту), но никогда — чтение состояния: оно чистое, без блокировок, записей и коммитов. Тот же починщик закрывает шаг по таймеру (тик раз в секунду), добавляет геройский раунд, чинит игры серии и открывает следующую позицию фриплея, и сигналит комнате на encounter:{id}:map-veto / encounter:{id}:pick-ban:hero, когда закончит.

Раунды бывают прогрессивными: слотовый режим и любой героический конфиг растут по одному раунду за карту, поэтому раунд 2 не может начаться раньше, чем сыграна карта 1. Плоский конфиг карт — это классическое вето, которое разыгрывает порядок карт всей серии сразу.

Дополнительные правила:

  • Память между раундами — это условие, а не колонка: pool_filter фазы и eligible шага читают листья вроде banned_by/picked_by (self/opponent/any в области series/round/previous_round), item_group, item_in и target_role_match. Ограничения (one_per_target, max_per_group, min_per_group) проверяются на каждой отправке.
  • Выбор шага — это отправка (submission), а не строка на действие: открытый шаг добавляет по одному варианту и сразу публикует его, шаг вслепую держит драфт каждой стороны закрытым, пока обе не зафиксируют, а одинаковые баны обеих сторон схлопываются в одну забаненную запись, которая остаётся в обеих отправках. Бан, чей lifetime длиннее этой карты, переносится в следующий раунд отдельной записью.
  • Первый ход определяется правилом higher_seed. Ротация первого бана — fixed, alternate, result_winner_first, result_loser_first или result_loser_choice. Последняя останавливает сессию на awaiting_choice: право выбрать открывающего есть только у стороны из pending_loser_side, и реализуется вызовом elect-opener.
  • Откат последнего шага — двустороннее согласие: одна сторона просит (запоминаются undo_requested_by и undo_target_index), встречный вызов соперника применяет откат. Любое новое действие запрос сбрасывает, а откат отказывается работать, если раунд уже сыгран или открылся следующий. Раскрытый шаг вслепую можно вдобавок переиграть в одностороннем порядке через dispute — в пределах dispute.max шага. У администратора есть собственные pick-ban-act, pick-ban-submit, pick-ban-reopen, pick-ban-elect-opener и сброс сессии.
  • Готовность тоже можно переопределить: POST /api/v1/admin/encounters/{encounter_id}/readiness с телом {side, ready} нажимает (или снимает) готовность стороны за недоступного капитана и отвечает {readiness}. Снятие даёт 409, если сессия любого вида уже создана — готовность управляет только созданием сессии, так что назад ведёт сброс сессии. Собственное «готов» капитана теперь тоже сигналит в encounter:{id}:map-veto: пока сессии нет, комната ничего не опрашивает, и без сигнала соперник ждал бы перезагрузки страницы.
  • У сломавшейся комнаты есть четыре организаторских рычага — все POST /api/v1/admin/encounters/{encounter_id}/… под правом match.result, каждый пишется в журнал комнаты вместе с указанной причиной. pick-ban-pause {kind, paused} замораживает вид: дедлайн исчезает и ничего не истекает по таймеру (системные шаги и раскрытия по-прежнему отрабатывают, так что действия организатора во время паузы сразу видны на доске), а любая запись капитана отвечает 409 Pick-ban session is paused — при этом админские переопределения продолжают работать. Снятие паузы возвращает шагу ровно то время, что у него оставалось, а шаг, открывшийся во время паузы, получает полный таймер. pick-ban-extend {kind, seconds} (10..3600) сдвигает старт открытого шага вперёд и работает в том числе на паузе; без шага с таймером — 409. pick-ban-cancel {kind, reason} закрывает сессию: отменённая героическая больше не открывает раунды, отменённая карточная возвращает серию в режим freeplay — уже разыгранные карты сохраняют свои позиции, остальные называют капитаны, — а назад из обеих ведёт сброс сессии.
  • technical-loss {loser_side, home_score?, away_score?, reason} завершает матч, который никто не сыграет: все живые сессии отменяются, все неподтверждённые позиции отменяются, и результат подтверждается в той же транзакции (те же проверки, что у /result: 409 на предпросмотровой сетке, на результате, из которого уже посеяна следующая стадия, и на уже подтверждённом). Если счёт не указан, он выводится: победителю достаётся best_of // 2 + 1 побед или больше, если у него уже было больше, а сдавшаяся сторона сохраняет реально выигранные карты, но не больше чем на одну меньше победной — Bo5 при счёте 2:0 в пользу сдающейся стороны записывается как 2:3.
  • GET /api/v1/admin/tournaments/{tournament_id}/pregame-rooms — сводка всех комнат турнира для организатора: по строке на встречу с готовностью, открытым шагом и дедлайном каждого вида, счётчиками игр серии, единым phase (teams_unknown, readiness, map, hero, report, done, idle) и флагами attention (game_disputed, result_disputed, awaiting_choice, overdue, late_not_ready). Строго только чтение — как и чтение состояния самой комнаты: ни то, ни другое не создаёт сессию и не открывает позицию серии.
  • GET /api/v1/admin/encounters/{encounter_id}/room-history — журнал конкретной комнаты: всё, что комната записала (готовность, создание/сброс/завершение сессии, каждый раунд, все действия капитанов и организаторов, раскрытия, срабатывания таймера, диспуты, откаты, отчёты по картам и по серии, четыре аварийных рычага), слитое с аудитом результата встречи и отданное от нового к старому (limit 1..500, по умолчанию 200). У каждой записи есть origin (room/result), source (captain/admin/system), side, автор и reason, плюс объект data под конкретное действие. Журнал привязан к встрече, а не к сессии: сброс сессии стирает драфт, но не историю.

Комната читается и управляется через GET /api/v1/encounters/{encounter_id}/pick-ban/{kind}/state и POST .../act, .../submit, .../dispute, .../elect-opener, .../undo; каталог листьев и ограничений — через GET /api/v1/pick-ban-rules/catalog, конфигурации турнира — через GET /api/v1/tournaments/{tournament_id}/pick-ban-configs. Живые обновления идут на топики encounter:{id}:map-veto (карты) и encounter:{id}:pick-ban:hero (герои) — два топика, а не один, потому что аудитория и права у них разные; чат комнаты — encounter:{id}:chat.

Скримы

Скрим — это тот же конвейер, урезанный до одной встречи. Комната — это одна стадия, две команды без ростеров и одна встреча, опубликованная сразу: стадия создаётся с is_published = true, так что предпросмотрового состояния у комнаты нет.

Создатель занимает сторону home, сторона away остаётся свободной и занимается по токену из ссылки — это адрес, а не секрет прав доступа: прочитать комнату может любой участник воркспейса. Сторона зрителя определяется на сервере по капитану команды, а не по параметру клиента. Один человек не может быть капитаном обеих сторон. Число открытых комнат на пользователя и максимальный best_of ограничены платформенной настройкой; закрытие освобождает слот, но сама комната остаётся читаемой.

POST /api/v1/scrims/{token}/claim HTTP/1.1
Host: owt.craazzzyyfoxx.me
Authorization: Bearer <токен>

Маршруты: POST /api/v1/scrims, GET /api/v1/scrims, GET /api/v1/scrims/{token}, POST /api/v1/scrims/{token}/claim, POST /api/v1/scrims/{token}/close. Все комнаты воркспейса живут в одном скрытом контейнерном турнире, и пересчёт таблиц на нём не запускается: у контейнера по построению нет ни сетки, ни таблицы, ни ростеров.

У GET /api/v1/scrims есть необязательный scope: mine (по умолчанию) отдаёт комнаты самого вызывающего, workspace — все комнаты воркспейса и требует на нём права match.result. Это же право позволяет персоналу закрыть комнату, которую он не создавал и в которой не играет; у каждой сериализованной комнаты есть поле can_close — единственное правило, по которому интерфейс рисует это действие.

Логи матчей

Приём. Три пути: многочастная загрузка организатором (POST /api/v1/admin/logs/upload с tournament_id, необязательным encounter_id и одним или несколькими files[]), приём из Discord-бота через очередь и ручная постановка на обработку (POST /api/v1/logs/{id}). Источник фиксируется в записи как upload, discord или manual. Имя файла проверяется на обход каталогов, содержимое обязано быть валидным UTF-8, файл кладётся в объектное хранилище до создания строки в базе. Загрузка лога без строки match_end отклоняется для этого файла ещё до сохранения — матч не доигран или игра ещё пишет файл, — и поэтому автозагрузка в браузере может повторить её позже.

Дедупликация. Считается SHA-256 от байтов файла. Если для той же пары «турнир, имя файла» уже есть запись в состоянии done с тем же хешем, файл не переобрабатывается. Существующая запись в pending или failed для того же имени переиспользуется, при этом счётчик попыток сбрасывается в ноль: содержимое могло измениться, и бюджет повторов начинается заново.

Состояния записи: pending, processing, done, failed; у записи есть error_message, started_at, finished_at, attempts, content_hash и необязательная привязка к встрече. Очередь и история читаются администратором на /api/v1/admin/logs/queue-status, /api/v1/admin/logs/history, /api/v1/admin/logs/stats, повтор — POST /api/v1/admin/logs/{id}/retry; необязательное поле тела encounter_id сначала привязывает незавершённый лог к этой встрече. Без привязки пару команд, которая встречается не один раз, различает карта из лога — маппул раунда, который её допускает, и серия, где этой карты из другого лога ещё нет; если неоднозначность остаётся, лог падает с encounter_ambiguous. Прогресс обработки приходит подписчикам как протухание ресурса workspace.logs.

Что даёт разбор. Одна строка matches.match на карту с source = log_parser, длительностью, именем файла и ссылкой на запись обработки; статистика по игрокам — строки «имя показателя и значение», отдельно по раундам и отдельной сводкой за весь матч (раунд 0), с разрезом по героям там, где показатель героезависим; килл-фид с временем, раундом, номером боя, убийцей, жертвой, героями, уроном и признаками крита и environmental-килла; события — ассисты, ульты, смены героев, воскрешения. Читается это на GET /api/v1/matches, GET /api/v1/matches/{id} и GET /api/v1/matches/{id}/kill-feed.

Impact и MVP. Поверх разобранного матча считается вклад игрока. Килл-фид режется на бои: новый бой начинается со сменой раунда или после паузы длиннее пятнадцати секунд между убийствами, и границы боёв дают «первый убитый» и «первая смерть». Игроку приписывается доминирующая роль по времени на героях; статистика сравнивается с базовыми показателями — отдельно по роли и по паре «роль и ранг» — и взвешивается по доле сыгранного времени, с обрезкой выбросов. Флекс-строки и слишком короткие выходы не оцениваются. Результат кладётся двумя показателями: impact_points (база по роли) и overperformance_score (база по роли и рангу); второй и есть основа MVP-достижений.

Кто это потребляет

Достижения. Правило достижения — декларативное дерево условий в JSON, которое считается по данным матчей, таблиц и пути по сетке; итоговый набор у игрока — это результат вычисления плюс ручные выдачи минус ручные отзывы. Пересчёт запускается сам после успешного разбора лога, либо вручную (POST /api/v1/achievement/calculate, POST /api/v1/achievement/calculate/{tournament_id}); публичное чтение — GET /api/v1/achievements/user/{user_id} и GET /api/v1/achievements/{id}/users.

Аналитика. Рейтинги и прогнозы строятся по тем же встречам и матчам: аналитический прогон переигрывает встречи в хронологическом порядке, отдельно считается качество встречи (конкурентность, предсказуемость, баланс сил) по счёту серии и предматчевым рейтингам команд. Чтения — /api/v1/analytics, /api/v1/analytics/match-quality, /api/v1/analytics/performance и соседние маршруты; на воркспейс допускается одна активная задача расчёта за раз. Какие таблицы за всем этим стоят, показано в статье Модель данных и на странице Схема БД.

Коррекция результата игры необратима для того, что уже было построено на старом исходе: непрошедшие раунды pick/ban пересобираются, а если их успели тронуть — коррекция будет отклонена. Планируйте интеграцию так, чтобы не полагаться на устойчивость уже открытых раундов.

См. также