Игровой API StadMagic (совместимый с DatsMagic)
Эта страница описывает игровой REST API для ботов. У него ровно один метод: клиент отправляет команды и в том же ответе получает актуальный снимок мира. Этот контракт не меняется. У веб-визуализатора есть отдельный realtime-канал, описанный ниже; он не предназначен для ботов. Отдельных игровых `GET`-эндпоинтов нет.
Адрес арены
Текущую арену и её адрес показывает главная страница Hub. Адрес может сохраняться между запусками, но состояние, карта и доступность ковров сбрасываются при смене запуска. Для локального запуска типичный адрес — `http://127.0.0.1:8080`.
Единственный игровой метод
`POST /play/magcarp/player/move`
Заголовки:
X-Auth-Token: <токен-команды>
Content-Type: application/json
Accept: application/json
Accept-Encoding: gzip
Тело — JSON-объект `transports`; каждый элемент относится к одному ковру и содержит его `id` и вектор `acceleration`:
{
"transports": [
{"id": "ec50282c5bbb11d2_0", "acceleration": {"x": 40, "y": 0}},
{"id": "ec50282c5bbb11d2_1", "acceleration": {"x": -12, "y": 16}}
]
}
Один зарегистрированный токен может выполнять не более **5 запросов в скользящую секунду**; шестой запрос в текущем окне получит `429` до сборки полного ответа. Пустые запросы тоже учитываются. Дополнительно остаётся лимит не более одного непустого пакета команд за игровой тик. Сжатие — опциональное: если отправить `Accept-Encoding: gzip`, сервер может вернуть тот же JSON с `Content-Encoding: gzip`; стандартная HTTP-библиотека должна распаковать тело перед JSON parsing. Без этого заголовка приходит обычный JSON.
Realtime-канал сайта
Для `/arena` Hub предоставляет `POST /api/visualizer/ticket`. Игрок передаёт командный токен только заголовком `X-Auth-Token` и пустое JSON-тело `{}`. Наблюдатель вызывает endpoint без токена. Hub возвращает короткоживущий одноразовый ticket и адрес WebSocket активной арены. Сам командный токен никогда не помещается в WebSocket, URL или игровые сообщения.
WebSocket использует подпротокол `stadmagic.v1`; ticket передаётся только на handshake как дополнительный `Sec-WebSocket-Protocol` со значением `stadmagic-ticket.<ticket>`. Сервер отправляет сообщения вида `{"type":"snapshot","tick":42,"state":{...}}`, где `state` — Desert DTO из игрового API. Клиентские команды имеют вид `{"type":"commands","transports":[{"id":"...","acceleration":{"x":12,"y":-8}}]}`. В пределах одного тика веб-команды коалесцируются, используется последняя валидная команда. Это правило относится только к веб-каналу; REST ограничение для ботов остаётся прежним.
Ручной режим сайта продлевает lease отдельным небольшим `POST /api/visualizer/lease` с `X-Auth-Token` раз в 500 мс. Он не проксирует игровой API и не получает снимок. После выключения управления отправляется `{ "releaseLeaseId": "..." }`; если браузер или сеть пропадут, lease истечёт автоматически.
Старый `POST /api/visualizer/move` Hub-proxy оставлен как совместимый fallback. Обычному боту следует обращаться непосредственно к игровому `POST /play/magcarp/player/move`.
Ковры создаются автоматически при первом запросе зарегистрированного токена. В ответе сервер вернёт их реальные непрозрачные ID — используйте именно их, не составляйте ID вручную. ID не является токеном авторизации. Чтобы оставить конкретный ковер без управляющего ускорения, передайте `{ "x": 0, "y": 0 }`. Если элемент отсутствует или в нём нет `acceleration`, новая команда этому ковру не задаётся, а сервер продолжает применять его последнее принятое ускорение.
Успешный ответ `200 OK`
Ответ — полный снимок `Desert` для команды, от имени которой передан токен. Ниже пример формы ответа; конкретные значения, число ковров и сущности зависят от активного мира.
{
"errors": [],
"anomalies": [
{
"effectiveRadius": 850,
"id": "anomaly_12",
"radius": 24,
"strength": -35,
"velocity": {"x": -18, "y": 7},
"x": 4100,
"y": 3200
}
],
"attackCooldownMs": 10000,
"attackDamage": 30,
"attackExplosionRadius": 30,
"attackRange": 200,
"bounties": [
{"points": 75, "radius": 5, "x": 4300, "y": 3000}
],
"enemies": [
{
"health": 100,
"killBounty": 0,
"shieldLeftMs": 0,
"status": "alive",
"velocity": {"x": -20, "y": 3},
"x": 5200,
"y": 2500
}
],
"mapSize": {"x": 10000, "y": 10000},
"maxAccel": 40,
"maxSpeed": 110,
"name": "my-team",
"points": 75,
"reviveTimeoutSec": 2,
"shieldCooldownMs": 40000,
"shieldTimeMs": 5000,
"transportRadius": 5,
"transports": [
{
"anomalyAcceleration": {"x": 0, "y": 0},
"attackCooldownMs": 0,
"deathCount": 0,
"health": 100,
"id": "ec50282c5bbb11d2_0",
"selfAcceleration": {"x": 40, "y": 0},
"shieldCooldownMs": 0,
"shieldLeftMs": 0,
"status": "alive",
"velocity": {"x": 8, "y": 0},
"x": 3200,
"y": 3000
}
],
"wantedList": []
}
Поля ответа
| Поле | Тип | Значение |
|---|---|---|
| `errors` | `string[]` | Ошибки отдельных элементов запроса. Некорректный/чужой ID или нечисловое ускорение могут попасть сюда, тогда как остальные валидные команды пакета будут приняты. |
| `anomalies` | `Anomaly[]` | Текущие аномалии арены. `x`, `y` — центр; `velocity` — вектор движения; `effectiveRadius` — внешняя граница зоны сил; `radius` — смертельное ядро; `strength` — знаковая сила (положительная притягивает, отрицательная отталкивает). |
| `attackCooldownMs`, `attackDamage`, `attackExplosionRadius`, `attackRange` | number | Настройки мира, оставленные в совместимом формате Desert API. Сейчас атака игровым endpoint не реализована. |
| `bounties` | `Bounty[]` | Доступные монеты. `x`, `y` — центр, `points` — начисляемое золото. `radius` в текущем контракте равен `transportRadius`; не считайте его отдельным точным радиусом графического спрайта. |
| `enemies` | `Unit[]` | Ковры всех остальных команд плоским списком. Поле не группирует ковры по командам и включает только существующие сущности. |
| `mapSize` | `{x, y}` | Ширина и высота арены в игровых единицах. Начало координат — левый нижний угол; `x` растёт вправо, `y` вверх. |
| `maxAccel` | number | Максимальная длина управляющего вектора одного ковра. Сервер ограничивает более длинный вектор до этого значения. |
| `maxSpeed` | number | Максимальная длина скорости ковра. Скорость ограничивается этим значением после каждого тика. |
| `name` | string | Имя команды, выбранное при регистрации. |
| `points` | integer | Текущий счёт команды (золото всех её ковров после начислений и штрафов за гибель). |
| `reviveTimeoutSec` | number | Настройка мира о задержке возрождения; фактическое наличие респавна определяется профилем мира. |
| `shieldCooldownMs`, `shieldTimeMs` | number | Совместимые настройки Desert API. Щит игровым endpoint сейчас не реализован. |
| `transportRadius` | number | Радиус ковра, в игровых единицах. Используется, в частности, при сборе монет и проверке столкновений. |
| `transports` | `Transport[]` | Полный список собственных ковров, включая погибшие. Стабильный `id` сохраняется после респавна. |
| `wantedList` | `Unit[]` | Совместимое поле Desert API; сейчас всегда пустое. |
Поля ковра `Transport`
| Поле | Тип | Значение |
|---|---|---|
| `id` | string | Стабильный ID ковра; верните его в элементе команды, чтобы управлять этим ковром. |
| `x`, `y` | number | Координаты центра ковра. |
| `velocity` | `{x, y}` | Фактический вектор скорости сейчас. Его длина — скорость, направление — направление движения. |
| `selfAcceleration` | `{x, y}` | Последний эффективный управляющий вектор после ограничения сервером; не обязательно уже отражает команду, только что отправленную в текущем запросе. |
| `anomalyAcceleration` | `{x, y}` | Суммарный вектор сил аномалий в текущей позиции ковра. Это не команда управления и не часть `selfAcceleration`. |
| `status` | string | `alive` или `dead`. При респавне тот же ID снова становится `alive`. |
| `health` | integer | `100` у живого ковра и `0` у погибшего; сейчас это индикатор статуса, а не уменьшаемый запас здоровья. |
| `deathCount` | integer | Число гибелей этого ID за текущую сессию арены. |
| `attackCooldownMs`, `shieldCooldownMs`, `shieldLeftMs` | number | Совместимые поля; в текущей реализации равны нулю, соответствующие действия не поддерживаются. |
Поля элемента `enemies` имеют форму `Unit`: `x`, `y`, `velocity`, `health`, `status`, `killBounty`, `shieldLeftMs`. У противников `status` также равен `alive`/`dead`; `killBounty` и shield сейчас возвращаются как нули.
Ошибки HTTP
Тело ошибки имеет общий вид `{"error":"..."}`.
| HTTP | Пример `error` | Причина и действие клиента | |---|---|---| | `400` | `invalid vector values` | Тело не соответствует JSON-схеме или содержит некорректные значения. Проверьте `transports` и конечность чисел. | | `400` | `session is not active` | Сессия арены сейчас не принимает команды; повторите запрос после запуска следующей арены. | | `400` | `player_destroyed` | Погиб весь флот, а в мире нет респавна. Этот токен больше не может управлять этой сессией. | | `401` | `unauthorized` | Нет `X-Auth-Token`, токен пустой или не зарегистрирован для Hub-арены. Проверьте токен и регистрацию. | | `429` | `rate limit exceeded: 1 command per tick` | Для команды уже принят пакет ускорений в этом игровом тике. Не ретрайте немедленно; дождитесь следующего тика. | | `429` | `rate limit exceeded: 5 requests per second per token` | Токен уже выполнил 5 запросов за последнюю секунду. Снизьте частоту; не запускайте немедленные повторы. | | `500` | `internal server error` | Ошибка сервера. Повторите позже; не запускайте плотный цикл повторов. |
Ошибки уровня отдельного ковра возвращаются в `errors` успешного ответа. Например, неизвестный ID будет пропущен; валидные команды остальных ковров при этом могут быть применены. Если запрос содержит хотя бы одну валидную команду, команда в целом считается отправившей пакет на этот тик.
Пример запроса
curl --fail-with-body \
-X POST 'http://127.0.0.1:8080/play/magcarp/player/move' \
-H 'X-Auth-Token: my-team-secret' \
-H 'Content-Type: application/json' \
--data '{"transports":[{"id":"ec50282c5bbb11d2_0","acceleration":{"x":40,"y":0}}]}'
Первый ответ содержит созданный флот. Затем отправляйте каждую команду с соответствующим ID. Серверный шаг времени равен 200 мс; ограничение — один пакет с командами на токен за тик. Ответ — снимок состояния при обработке запроса, а не отдельное подтверждение того, что физический тик уже завершился.
Регистрация токена
Сначала откройте `/register` и выберите уникальное имя команды (1–48 печатных символов; регистр букв при проверке уникальности не учитывается). Hub создаст криптографически случайный токен и покажет его один раз после успешной регистрации. Скопируйте и сохраните секрет до закрытия страницы. Один токен соответствует одной команде во всех мирах; первый игровой запрос создаёт её флот. Передавайте токен только в `X-Auth-Token`, не в URL, код публичного репозитория или логи. Имя можно сменить запросом `POST /api/teams` с JSON `{"token":"<ваш токен>","name":"Новое имя"}`.
Арена всегда сверяет токен с приватным реестром Hub. Отсутствующий, неизвестный или отозванный токен получает `401 Unauthorized`; это правило действует и при отдельном запуске сервера. Для отдельного сервера укажите реестр через `DATS_TOKEN_REGISTRY_PATH` либо используйте стандартный `modules/arena-hub/data/registry.json`.
Hub API — не игровое управление
Сайт Hub отдельно предоставляет `/api/worlds`, `/api/runs`, `/api/leaderboard`, `/api/votes` и `POST /api/teams`. Например, `POST /api/teams` с телом `{"name":"My Team"}` создаёт команду; успешный ответ имеет форму `{"team_id":"<идентификатор>","name":"My Team","token":"<секрет, показать один раз>"}`. Эти маршруты обслуживают веб-интерфейс, каталог, регистрацию и результаты; посылать туда игровые команды нельзя. Для игровых команд используется только `POST /play/magcarp/player/move`.