# Журнал футбольной партии v1: интеграция клиента Подготовил: Игорь Турченко Журнал предназначен для разбора партии в административной карточке: точки ударов, положение Жоры при проверке, исходы, зоны, очки и паузы. Данные сообщает клиент. Сервер проверяет их согласованность с сохранёнными правилами, но не доказывает подлинность ввода или движения Жоры. Покадровое воспроизведение не входит в этот контракт. ## Подключение и совместимость Все операции используют игровую Bearer-сессию и принадлежащую игроку партию. ```http POST /rounds Content-Type: application/json Authorization: Bearer {"mode_id":"football","request_id":"02816c94-4117-4c60-a75f-de11b892219d","journal_version":1} ``` Ответ содержит `round_id`, `mode_id`, `rules` и `journal_version: 1`. Используйте выданный снимок правил до конца партии. При повторе создания сохраняйте `request_id`, режим и выбор журнала: изменение выбора даёт `409 request_conflict`. Не повторяйте запрос незаметно без журнала при ошибке. Без `journal_version` сохраняется прежний футбольный протокол: завершение со счётчиками без журнала, прерывание с `{}`. Подключать журнал к уже созданной партии нельзя. Подключённый журнал обязателен и для finish, и для abort. Формат v1 выбирается по режиму партии: журнал холодильника здесь не подходит. ## Геометрия и механика Система координат общая для прицела, Жоры и бонусных зон. Внутренний створ: `(0,0) — (10000,10000)`, начало снизу слева, X вправо, Y вверх. Единица X — 1/10000 ширины ворот, единица Y — 1/10000 высоты. Пиксели экрана не передаются. `aim_bounds` — прямоугольник всей доступной области прицеливания, содержащий створ. Передаётся один раз в журнале и неизменен всю партию, в том числе после изменения размера окна. `left/bottom` лежат в −1000000..0, `right/top` — в 10000..1000000. Выбранная точка может лежать на границе этого прямоугольника. Например, `{"left":-3000,"bottom":-1000,"right":13000,"top":14000}` — иллюстративная геометрия, не обязательные размеры клиента. `mechanics_version: "launch_v1"` обозначает текущую семантику: 1. Мяч летит точно в `aim_x/aim_y`; второй пары координат нет. 2. Только `0 < x < 10000` и `0 < y < 10000` могут дать гол или отражение. Граница створа и всё снаружи — `miss`; толщину рамы не моделируем. 3. Внутри створа проверяем отражение **по положению Жоры при запуске**: `abs(aim_x - goalkeeper_x) <= rules.goalkeeper.save_half_width` даёт `save`. Высота на отражение не влияет. `goalkeeper_x` — центр области отражения, в диапазоне 0..10000, а не произвольная точка спрайта. 4. Иначе это `goal`. Выбираем зону из снимка правил: обе границы зоны включены; при общей границе побеждает больший множитель, затем меньший ID в лексикографическом порядке. Вне специальных зон множитель 1, `zone_id: null`. 5. Гол даёт `goal_points × multiplier`. Отражение и промах дают ноль и `zone_id: null`. Смена момента проверки потребует новой поддержанной версии механики. Не передавайте новую семантику под старой меткой. Путь и скорость Жоры между ударами сервер v1 не воспроизводит и не проверяет. ## Поля журнала | Поле | Значение | |---|---| | `version` | Целое `1` | | `client_version` | Неизменяемая версия сборки, 1–64 ASCII-символа, `[A-Za-z0-9][A-Za-z0-9._+-]{0,63}` | | `mechanics_version` | Только `launch_v1` | | `aim_bounds` | Геометрия прицеливания, описанная выше | | `capture_status` | `complete` или `limit_reached` | | `omitted_event_count` | 0 для полной записи; положительный int32 с насыщением для отброшенного суффикса | | `events` | 2–4096 событий, включая начало и терминальное событие | Неизвестные поля отклоняются. Все числовые поля — целые JSON-числа; строки, дробные записи вроде `600.0`, null вместо числа и повторные ключи запрещены. Каждое событие содержит `seq`, `type`, `active_time_ms`, `elapsed_time_ms`. `seq` — плотная нумерация сохранённых событий с нуля, включая терминальное. Оба времени не убывают; одинаковые времена допустимы, порядок задаёт `seq`. `elapsed_time_ms` — монотонные часы от локального старта, включая паузы, без повторных отправок; 0..9007199254740991. Оно не меньше активного времени. Не используйте календарные часы устройства. Активное время округляется вниз до миллисекунд, ограничено `rules.duration_ms` и не движется на паузе. Внутри модели сохраняйте остаток точности, не округляйте каждый шаг отдельно. ## События и порядок | Тип | Обязательные дополнительные поля | |---|---| | `round_started` | Нет; первое событие, `seq = active_time_ms = elapsed_time_ms = 0` | | `shot_started` | `shot_id`, `aim_x`, `aim_y`, `goalkeeper_x` | | `shot_resolved` | `shot_id`, `outcome: goal/save/miss`, `zone_id: string/null`, `points`, `score_after` | | `paused` | `reason: manual/background` | | `resumed` | Нет | | `round_finished` | `state_after` | | `round_aborted` | `reason: player_exit`, `state_after` | `shot_id` — последовательный номер запущенного удара с 1. Отменённое потягивание не создаёт удар. Запишите `shot_started` в точке запуска с положением Жоры, использованным для решения, затем `shot_resolved` после полёта. Одновременно может быть один неразрешённый удар. Следующий запуск не раньше `previous_launch + shot_interval_ms`; при совпадении времени сначала исход, затем новый запуск. У каждого полного удара ровно один исход. Исход имеет активное время `min(launch_time + flight_ms, duration_ms)`. От запуска до исхода по прошедшим часам должно пройти не меньше `flight_ms`. После нуля новый запуск запрещён, но последний мяч долетает и учитывается. Активный таймер остаётся на `duration_ms`, `elapsed_time_ms` продолжает идти. Сначала запишите последний исход, затем `round_finished` с активным временем ровно `duration_ms`. Эти два события могут иметь одинаковые часы. Паузы не ограничены по числу и реальному времени. Они замораживают игру, полёт и незавершённое потягивание отменяют без удара. В фоне записывайте `paused: background`; возврат — через явное продолжение. Игровых событий на паузе нет. Повторный запрос уже действующей паузы/продолжения события не создаёт. `state_after` содержит целые неотрицательные int32 `score`, `shots`, `goals`, `saves`, `misses`. `shots` считает запуски; остальные счётчики — разрешённые исходы. При finish число запусков равно сумме исходов. При abort разрешён один удар без исхода: не записывайте его фиктивным промахом. Abort допустим и после нуля во время долёта, до завершения. Прерванная партия не создаёт результат, рекорд или награду. Если активные часы уже достигли времени исхода удара, сначала запишите исход, затем abort. При долёте после нуля активные часы не позволяют определить остаток полёта; его состояние сообщает клиент. Терминальное событие единственно и стоит последним. ## Завершение и прерывание: примеры Полные готовые тела запросов: [JSON с finish и abort](api/football-journal-v1.examples.json). Они соответствуют упакованным стартовым правилам; используйте фактический снимок своей партии. [JSON Schema](api/football-journal-v1.schema.json) описывает форму журнала; межсобытийные проверки выполняет сервер. Пример одного удара в верхний левый угол, затем исхода: ```json [ {"seq":1,"type":"shot_started","active_time_ms":0,"elapsed_time_ms":0, "shot_id":1,"aim_x":600,"aim_y":9000,"goalkeeper_x":5000}, {"seq":2,"type":"shot_resolved","active_time_ms":320,"elapsed_time_ms":320, "shot_id":1,"outcome":"goal","zone_id":"upper_left","points":50,"score_after":50} ] ``` `POST /rounds/{round_id}/finish` принимает прежние `score`, `active_time_ms`, `football_stats` и добавленный `journal`. Терминальный снимок должен совпадать с итоговыми очками и всеми счётчиками оболочки. Для полной записи сервер пересчитывает очки и счётчики по событиям и правилам. Ответ остаётся прежним. `POST /rounds/{round_id}/abort` принимает `{"journal": ...}`. ## Ограничения, сохранение и повторы Общий лимит несжатого HTTP-тела — 1 MiB, включая оболочку. Сжатые тела запрещены. Резервируйте место для терминального снимка и оболочки заранее. Если предел событий или байтов близок, прекратите запись обычных событий: сохраните непрерывный префикс и терминальное событие, отметьте `limit_reached` и посчитайте все отброшенные события до терминального с насыщением int32. Середину журнала не вырезайте. Нельзя возобновлять запись после первого пропуска. Усечённая запись допускается как диагностика: сервер проверяет сохранённый префикс, форму терминального снимка, совпадение с оболочкой и нижние границы известных счётчиков/очков. Пропущенные удары он не восстанавливает. В админке видны `capture_status` и число пропусков; такой журнал не равнозначен полному. Внешние проверки длительности и теоретических пределов действуют и здесь. Заморозьте весь пакет после терминального события и сохраняйте до подтверждения сервера. Повторы отправляют тот же пакет, не дописывают события и не меняют метки сборки или геометрию. Перестановка ключей JSON и пробелы допустимы. После принятия изменение любого поля даёт `409 result_conflict`; смена finish на abort или обратно — `409 round_state_conflict`. Подтверждение повтора сохраняется независимо от подробного журнала. | Ошибка | Действие клиента | |---|---| | `400 journal_required` | Вернуть обязательный журнал; не пересоздавать партию | | `400 journal_invalid` | Сохранить пакет для диагностики; исправить нарушение протокола | | `400 journal_version_unsupported` | Использовать поддерживаемый формат; не менять уже принятую партию | | `409 journal_not_enabled` | Журнал не подключён при создании; завершить по прежнему протоколу | | `400 round_too_early` | Дождаться минимальной серверной длительности и повторить пакет | | `409 result_conflict / round_state_conflict` | Не начислять результат второй раз; сохранить конфликт для разбора | | `401 invalid_session` | Восстановить сессию того же аккаунта и повторить пакет | | `413 payload_too_large` | Исправить ограничение сборщика; не обрезать уже принятый пакет | | Сеть / `503 service_unavailable` | Повторить неизменённый пакет позже | Ошибка не создаёт рекорд. Для подключённого журнала сохраняются до восьми сводок отказов с кодом и, когда известен, `event_seq`; далее растёт счётчик переполнения. Геометрия или ввод отказавшего пакета туда не копируются. ## Проверка интеграции Создайте партию с журналом, сыграйте гол, отражение, промах по раме и промах снаружи; сделайте паузу и завершите с последним мячом в полёте. Проверьте finish и его повтор, затем отдельный abort в полёте. Администратор читает `GET /admin/rounds/{id}`: `journal`, `football_stats`, сохранённые `rules`, `refusals` и `refusal_overflow`. Список партий полные журналы не загружает. Рисование схемы ударов — отдельная задача интерфейса админки.