Входящая интеграция · API результатов игр v1

Результаты клуба → MafiaSpace

MafiaSpace задаёт общий формат; разработчик клуба реализует отправку по этой документации. Одна завершённая игра передаётся одним запросом. Рабочие результаты входят в действующий рейтинг MafiaSpace и рейтинг соответствующего клуба, который используют сайт, iOS и Android. Уже перенесённые архивы не пересчитываются и повторно не загружаются.

1Сначала sandbox

Проверка не создаёт публичных игроков и не влияет на рабочий рейтинг.

2Без дублей

Повтор с тем же ID безопасен, новая revision полностью заменяет результат.

3В действующий рейтинг

Рабочие игры видны на сайте и в приложениях через обычную таблицу клуба.

Адрес и доступ

Базовый адрес: https://mafiaspace.ru

Ключ выдаёт команда MafiaSpace отдельно. Заголовок каждого запроса: Authorization: Bearer <ключ>. Для тела — Content-Type: application/json. Ключ храните только на сервере, без браузера, мобильного клиента и логов.

Есть два отдельных ключа: sandbox и production. Адрес API одинаковый; среду определяет ключ. Тестовые игры не создают публичных игроков и не влияют на настоящий рейтинг. Проверить клуб и среду: GET /v1/integrations/game-results/me. Не отправляйте вымышленные примеры рабочим ключом.

Ключ закреплён за конкретным клубом: поле clubId в игре отсутствует, менять клуб через запрос нельзя. me возвращает acceptGamesFrom — начало разрешённого периода новых игр. Более старые игры отклоняются для защиты от повторной загрузки архива.

Быстрый старт

  1. Получите тестовый ключ, проверьте me: environment должен быть sandbox.
  2. Скачайте example-game.json и game.schema.json.
  3. Отправьте пример командой ниже. Первый ответ — 201, повтор — 200.
  4. Посмотрите тестовую таблицу через GET /v1/integrations/game-results/standings.
  5. Проверьте исправление и отмену. Затем используйте рабочий ключ для настоящих игр.
curl --fail-with-body --request PUT \
  'https://mafiaspace.ru/v1/integrations/game-results/example-game-001' \
  --header "Authorization: Bearer $MAFIASPACE_SANDBOX_TOKEN" \
  --header 'Content-Type: application/json' \
  --data-binary @example-game.json

Методы

Метод Путь Результат
GET /v1/integrations/game-results/me Клуб, среда, дата приёма, лимиты и ссылка на рейтинг
POST /v1/integrations/game-results/validate Проверка состава, баллов и версии без сохранения игры
PUT /v1/integrations/game-results/{externalGameId} Сохранение или полная замена результата
GET /v1/integrations/game-results/{externalGameId} Текущая версия игры и статус
POST /v1/integrations/game-results/{externalGameId}/void Отмена ошибочной игры
GET /v1/integrations/game-results/standings Проверочная таблица только игр данного ключа/источника

validate принимает { "externalGameId": "example-game-001", "game": <JSON игры> }. Успех: { "valid": true, "effect": "write", "warnings": [], "game": {...}, "requestId": "..." }. Для точного повтора effect равен repeat. Проверка не резервирует версию.

Обязательные данные игры

Поле Значение
schemaVersion Число 1
revision Целое 1–2147483647. Первая версия 1, исправление — предыдущая + 1
playedAt Начало игры, RFC 3339 с часовым поясом: 2026-09-15T20:00:00+03:00
finishedAt Конец игры в том же формате; не раньше начала и не более 5 минут в будущем
gameType Строка sport_mafia_10
winner civilians — красные, mafia — чёрные, draw — ничья
players Ровно 10 игроков, уникальные места 1–10 и уникальные ID людей
players[].externalPlayerId Постоянный ID игрока в системе клуба
players[].nickname Ник на момент игры, 1–100 символов
players[].seat Целое 1–10
players[].role civilian × 6, mafia × 2, don × 1, sheriff × 1
players[].isAlive true, если игрок остался в игре на момент окончания; иначе false
players[].points.base Строка "1" победителю, "0" проигравшему; при ничьей всем "0"
players[].points.bonus Сумма всех положительных допов, например "0.3"; без начислений "0"
players[].points.penalty Величина всех вычитаемых штрафов, например "0.1"; без штрафов "0"

Допустимы необязательные externalEventId (ID вечера) и eventTitle (название, 1–200 символов). Они сохраняют контекст результата; календарная запись не создаётся автоматически. Ночные действия, голосования и полный протокол не требуются. Статистика таких действий из итогового результата не выводится.

ID игры в URL и ID игрока/вечера: 1–100 символов [A-Za-z0-9._-]. Для ID игры запрещены me, validate, standings. ID не меняется вместе с ником, датой или названием вечера и не переиспользуется для другой игры/человека. Если своих ID нет, создайте их один раз и сохраните в своей базе.

Баллы — строки с точкой, без знака минус, от 0 до 1000, максимум два знака после точки. Все три компонента обязательны. null, неизвестный итог, неполный состав, незнакомые поля и произвольный base отклоняются. При ничьей все три компонента равны нулю — это правило действующего рейтинга MafiaSpace. bonus должен уже включать все положительные начисления; не отправляйте туда общий итог. Штраф не вычитайте из bonus: передавайте отдельно в penalty.

Как игра попадает в рейтинг

Сервер проверяет победителя и базовые баллы, затем считает:

Рабочий запрос создаёт обычную завершённую игру и проекцию статистики. В рейтинге учитываются принятые по API игры вместе с остальными подходящими играми клуба. Полный протокол при этом не выдумывается: источник помечен club_result_api. Частные зачёты с отдельными регламентами и номинациями по ночным событиям не назначаются автоматически; этот API пополняет текущий публичный рейтинг.

Одинаковый ник не объединяет людей. Постоянный externalPlayerId получает постоянную карточку игрока MafiaSpace. Уже существующие игровые профили можно связать через обычный механизм объединения профилей; аккаунт и права доступа от отправки результата не появляются. Не создавайте новый ID при смене ника.

Ссылка на полный рейтинг конкретного клуба приходит в me и в ответе рабочего сохранения как ratingUrl. Проверочная таблица standings относится только к вашему источнику, поэтому может отличаться от полной клубной таблицы, где есть другие игры и связанные профили.

Ответ на сохранение

Первая запись — 201. Исправление и точный повтор — 200. Пример ответа:

{
  "externalGameId": "example-game-001",
  "resultId": "<UUID приёма>",
  "gameId": "<UUID игры, только для production>",
  "revision": 1,
  "status": "saved",
  "environment": "production",
  "statistics": "updated",
  "visibility": "public",
  "ratingUrl": "https://mafiaspace.ru/clubs/56a5df76-0f19-482b-98a7-563953b0b0e1/ratings",
  "game": { "...": "полная нормализованная игра" },
  "voidReason": null,
  "requestId": "<ID запроса>"
}

В sandbox: gameId и ratingUrl равны null, statistics = sandbox_only, visibility = private. Проверяйте environment в ответах. updated подтверждает сохранение и обновление серверной проекции рейтинга в одной транзакции. Сайт и приложение получают результат при следующем обновлении данных; уже открытый или офлайн-экран сам по себе не подтверждает свежесть. Кэш рейтинга на обслуживающем сервере сбрасывается после записи; соседний сервер может держать данные до 45 секунд. Для разбора ошибки сохраните requestId. Годовой фильтр действующего рейтинга использует UTC; передавайте реальное время с правильным смещением часового пояса.

Повтор, исправление, отмена

Сохраняйте исходящие запросы в очереди на своём сервере. При тайм-ауте, сетевой ошибке или 5xx повторяйте тот же ID, revision и JSON: через 2, 5, 15, 60 секунд, затем раз в 5 минут с небольшим случайным разбросом. Не повышайте версию ради повтора. Удаляйте запрос из очереди после 200/201 с ожидаемой версией либо подтверждающего GET.

Лимиты: тело до 64 KiB, 60 запросов в минуту на источник, до 2 одновременно на обслуживающем сервере. При 429 соблюдайте заголовок Retry-After.

Ошибки

Формат: {"error":{"code":"invalid_game","message":"invalid_game","fields":[]},"requestId":"..."}. fields при ошибке поля содержит path и code. Ответ прокси при 5xx может быть не JSON.

HTTP Код Действие
400 invalid_json Исправить JSON
401 invalid_token Проверить ключ; остановить очередь до исправления доступа
403 integration_disabled Доступ отключён, обратиться в MafiaSpace
404 result_not_found Проверить ID и среду
405 method_not_allowed Проверить HTTP-метод
409 revision_conflict, revision_gap, result_voided, historical_import_requires_mapping Прочитать GET и исправить причину; не создавать новый ID
413 payload_too_large Уменьшить тело
415 unsupported_media_type Передавать application/json
422 invalid_game, duplicate_player_identity Исправить поля, состав или баллы
429 rate_limited Повторить после Retry-After
500/502/503/504 internal_error / ответ прокси Повторить ту же версию

Проверка перед запуском

В sandbox: проверка без записи → первая игра → повтор → исправление баллов → смена ника с прежним ID → отмена. Количество игр в проверочной таблице: 0 → 1 → 1 → 1 → 1 → 0. Убедитесь, что очередь переживает перезапуск вашего сервера.

Перед первой рабочей отправкой проверьте me, дату приёма и отсутствие этих же игр в уже загруженном архиве/других источниках. Один и тот же externalGameId предотвращает повтор внутри источника; другой ID или другой источник — другая запись, поэтому не отправляйте одну игру по двум каналам без сопоставления.

Ключи и поддержку предоставляет MafiaSpace. Передавать внутренний формат клубной системы не требуется: реализация ведётся по этой спецификации.