Перейти к содержанию

Дода: игра

Игра по тесту в реальном времени. Ведущий (роль teacher или school_admin) управляет игрой с токеном; игроки входят по PIN без учётной записи и действуют с player_token. Методы ведущего без токена отвечают 401, с ролью не ведущего - 403, чужой тест или игра - 403 или 404.

Порядок работы ведущего:

  1. «Активная игра»: если игра уже идёт, вернуться в неё; у ведущего одна активная игра.
  2. «Создание игры» по тесту: в ответе pin и join_url для игроков.
  3. Подключение к «Потоку игры» с ролью teacher; экран строится из снимков.
  4. «Старт», дальше фазы переключает сервер; «Следующая фаза», «Пауза», «Продолжение», «Завершение» - ручное управление.
  5. «Результаты» после игры.

Порядок работы игрока:

  1. «Вход в игру» по PIN и нику: в ответе player_token.
  2. Подключение к «Потоку игры» с ролью player и player_token.
  3. «Ответ» на каждый вопрос; свой результат игрок видит в снимке на фазе reveal.

Фазы и очки - в разделе «Статусы и коды».

Объект «Игра»

Параметр Тип Описание
id* uuid id игры
test_id* uuid Тест
pin* string PIN для игроков
title* string Название теста
status* string Фаза игры
join_url* string Ссылка для игроков: адрес страницы игры школы с ?pin=
question_seconds* integer Время на вопрос по умолчанию
language* string ru
shuffle_options* boolean Перемешивать варианты
sound_enabled* boolean Звук
final_double* boolean Удвоенные очки за последний вопрос
current_question_index* integer Номер текущего вопроса с 0
question_started_at datetime Начало текущего вопроса
question_deadline_at datetime Конец приёма ответов
countdown_deadline_at datetime Конец отсчёта 3-2-1
reading_deadline_at datetime Конец показа вопроса без вариантов
version* integer Растёт при изменении игры
created_at* datetime Когда создана
{
  "id": "5e8a...", "test_id": "7c3e...", "pin": "234566", "title": "Дроби", "status": "lobby",
  "join_url": "https://app.school.kz/play?pin=234566", "question_seconds": 20, "language": "ru",
  "shuffle_options": true, "sound_enabled": true, "final_double": true, "current_question_index": 0,
  "question_started_at": null, "question_deadline_at": null, "countdown_deadline_at": null,
  "reading_deadline_at": null, "version": 1, "created_at": "2026-11-10T10:00:00Z"
}

Объект «Снимок»

Полное состояние игры; его присылает «Поток игры» и возвращает «Снимок игры».

Параметр Тип Описание
session* object Игра: id, pin, title, status, version, current_question_index, дедлайны фаз
session.question_count* integer Всего вопросов
session.is_final_question* boolean Текущий вопрос последний
players* array Игроки
players[].id, players[].name string id и ник
players[].score, players[].streak integer Очки и серия верных ответов
players[].connected boolean Сокет игрока подключён
current_question object Текущий вопрос; null в lobby и countdown
current_question.id, .order, .kind, .image_url Вопрос
current_question.prompt object Текст вопроса; есть с фазы reading
current_question.options array Варианты {id, label, content}; null в reading; у ordering перемешаны
current_question.numeric object {min, max, step}; на reveal ещё value и tolerance
current_question.why object {prompt, options, correct_option_id}; correct_option_id - на reveal
current_question.answered_count, .player_count integer Сколько ответили из скольких
current_question.correct_option_ids array Верные варианты, с reveal; у ordering - верная последовательность
current_question.accepted_answers, .explanation С reveal
current_question.correct_count, .partial_count, .correct_percent integer Статистика ответов, с reveal
current_question.answer_distribution array С reveal: [{option_id, count}], у type_answer и numeric - [{text, count}], у ordering - []
leaderboard* array Таблица мест
leaderboard[].rank, .id, .name, .score Место, игрок, очки
leaderboard[].rank_delta integer Изменение места, больше нуля - поднялся
leaderboard[].last_points integer Очки за текущий вопрос, на reveal
leaderboard[].last_status string На reveal: correct, partial, wrong; null - не ответил
leaderboard[].last_answer_ms integer Время ответа, мс, на reveal
leaderboard[].last_partial object При partial: {correct_selected, wrong_selected, correct_total}
class_percent integer Процент верных ответов класса, в podium и finished

Игрок и запрос без токена ведущего получают урезанный снимок: у вопроса нет prompt, explanation, accepted_answers, у вариантов только {id, label}, у why нет prompt.

{
  "session": {"id": "5e8a...", "pin": "234566", "title": "Дроби", "status": "question", "version": 12, "current_question_index": 0, "question_count": 10, "is_final_question": false, "question_started_at": "2026-11-10T10:01:00Z", "question_deadline_at": "2026-11-10T10:01:20Z", "countdown_deadline_at": null, "reading_deadline_at": null},
  "players": [{"id": "a1b2...", "name": "Ару", "score": 0, "streak": 0, "connected": true}],
  "current_question": {"id": "4f1d...", "order": 0, "kind": "single", "image_url": null, "prompt": {"ru": "1/2 = ?"}, "options": [{"id": "9a2c...", "label": "A", "content": {"ru": "2/4"}}], "numeric": null, "why": null, "answered_count": 3, "player_count": 25, "correct_option_ids": null, "accepted_answers": null, "explanation": null, "correct_count": null, "partial_count": null, "correct_percent": null, "answer_distribution": null},
  "leaderboard": [{"rank": 1, "id": "a1b2...", "name": "Ару", "score": 0, "rank_delta": null, "last_points": null, "last_answer_ms": null, "last_status": null, "last_partial": null}],
  "class_percent": null
}

Создание игры

Создаёт игру по тесту ведущего: лобби с PIN.

Запрос: POST {DODA_URL}/v1/sessions, авторизация - токен ведущего.

Параметр Тип Описание
test_id* uuid Тест
question_seconds integer Время на вопрос: 5-120, по умолчанию 20
language string ru по умолчанию
shuffle_options boolean Перемешивать варианты; без поля - как у теста
sound_enabled boolean Звук; без поля - как у теста
final_double boolean Удвоить очки за последний вопрос; по умолчанию true
{"test_id": "7c3e...", "question_seconds": 20}

Ответ 201: объект «Игра».

Код ошибки Когда
403 Тест чужой
409 У ведущего уже есть активная игра
422 В тесте нет пригодных вопросов или question_seconds вне 5-120

Активная игра

Возвращает активную игру ведущего, например после перезагрузки страницы.

Запрос: GET {DODA_URL}/v1/sessions/active, авторизация - токен ведущего.

Ответ 200: объект «Игра» или null.

Старт

Начинает игру: lobby → countdown.

Запрос: POST {DODA_URL}/v1/sessions/{id}/start, авторизация - токен ведущего.

Ответ 200: объект «Игра».

Код ошибки Когда
409 Игра не в лобби

Следующая фаза

Переключает игру на следующую фазу; может досрочно пропустить countdown и reading.

Запрос: POST {DODA_URL}/v1/sessions/{id}/next?from={фаза}, авторизация - токен ведущего.

Параметр Тип Описание
from string В строке запроса: фаза, из которой переходим; защищает от двойного нажатия

Ответ 200: объект «Игра».

Код ошибки Когда
409 Фаза уже сменилась

Пауза

Останавливает таймер вопроса.

Запрос: POST {DODA_URL}/v1/sessions/{id}/pause, авторизация - токен ведущего.

Ответ 200: объект «Игра» со status: "paused".

Код ошибки Когда
409 Сейчас не идёт вопрос

Продолжение

Снимает паузу; начало и дедлайн вопроса сдвигаются на длину паузы.

Запрос: POST {DODA_URL}/v1/sessions/{id}/resume, авторизация - токен ведущего.

Ответ 200: объект «Игра».

Код ошибки Когда
409 Игра не на паузе

Завершение

Завершает игру; PIN и слот активной игры освобождаются.

Запрос: POST {DODA_URL}/v1/sessions/{id}/end, авторизация - токен ведущего.

Ответ 200: объект «Игра» со status: "finished".

Код ошибки Когда
403 Игра чужая

Исключение игрока

Удаляет игрока из игры.

Запрос: POST {DODA_URL}/v1/sessions/{id}/kick, авторизация - токен ведущего.

Параметр Тип Описание
player_id* uuid Игрок

Ответ 200:

{"kicked": "a1b2..."}
Код ошибки Когда
404 Игрока нет в игре

Результаты

Возвращает отчёт по игре.

Запрос: GET {DODA_URL}/v1/sessions/{id}/results, авторизация - токен ведущего.

Ответ 200:

Параметр Тип Описание
session* object {id, pin, title, status}
class_percent integer Процент верных ответов класса
players[]* array {rank, id, name, score, correct}; correct - число полностью верных ответов
questions[]* array {id, order, kind, prompt, correct_option_ids, accepted_answers, total_answers, correct_answers, answer_distribution}; answer_distribution - как в снимке
{
  "session": {"id": "5e8a...", "pin": "234566", "title": "Дроби", "status": "finished"},
  "class_percent": 76,
  "players": [{"rank": 1, "id": "a1b2...", "name": "Ару", "score": 4200, "correct": 8}],
  "questions": [{"id": "4f1d...", "order": 0, "kind": "single", "prompt": {"ru": "1/2 = ?"}, "correct_option_ids": ["9a2c..."], "accepted_answers": null, "total_answers": 25, "correct_answers": 19, "answer_distribution": [{"option_id": "9a2c...", "count": 19}]}]
}
Код ошибки Когда
403 Игра чужая

Вход в игру

Добавляет игрока в лобби по PIN.

Запрос: POST {DODA_URL}/v1/sessions/join, авторизация не нужна.

Параметр Тип Описание
pin* string PIN игры
name* string Ник: 1-64 символа без управляющих символов, например переноса строки и табуляции; уникален в игре с учётом регистра, пробелы по краям обрезаются; занятый ник Дода дополняет номером: «Ару 2», «Ару 3»
{"pin": "234566", "name": "Ару"}

Ответ 200:

Параметр Тип Описание
session_id* uuid Игра
player_id* uuid Игрок
player_token* string Токен игрока; сохраните его, с ним игрок переподключается
name* string Ник, под которым игрок вошёл; если запрошенный был занят - с номером, показывайте игроку этот
Код ошибки Когда
404 Нет лобби с таким PIN
409 GAME_ALREADY_STARTED или в игре нет мест
422 NICKNAME_INVALID

Ответ

Записывает ответ игрока на текущий вопрос; верно или нет, игрок узнаёт на фазе reveal.

Запрос: POST {DODA_URL}/v1/sessions/{id}/answer, авторизация - player_token в теле.

Параметр Тип Описание
player_token* string Токен игрока
selected_option_id uuid single, true_false
selected_option_ids array multi
ordered_option_ids array ordering: вся последовательность
text_answer string type_answer
number_answer number numeric
why_selected_option_id uuid Ответ на «Почему?», если он есть у вопроса
{"player_token": "...", "selected_option_id": "9a2c..."}

Ответ 200:

{"recorded": true, "already_answered": false}
Код ошибки Когда
401 Неверный player_token
409 Приём ответов закрыт

Смена ника

Меняет ник игрока в лобби.

Запрос: POST {DODA_URL}/v1/sessions/{id}/nickname, авторизация - player_token в теле.

Параметр Тип Описание
player_token* string Токен игрока
name* string Новый ник, по правилам метода «Вход в игру»

Ответ 200:

{"player_id": "a1b2...", "name": "Новый ник"}
Код ошибки Когда
401 PLAYER_NOT_FOUND
404 Игры нет
409 NICKNAME_TAKEN или GAME_ALREADY_STARTED
422 NICKNAME_INVALID

Выход из лобби

Убирает игрока из лобби; повторный вызов тоже отвечает 200.

Запрос: POST {DODA_URL}/v1/sessions/{id}/leave, авторизация - player_token в теле.

Параметр Тип Описание
player_token* string Токен игрока

Ответ 200:

{"left": true}
Код ошибки Когда
404 Игры нет
409 GAME_ALREADY_STARTED

Снимок игры

Возвращает объект «Снимок» по запросу, например при переподключении.

Запрос: GET {DODA_URL}/v1/sessions/{id}/snapshot, авторизация не нужна; с токеном ведущего снимок полный.

Ответ 200: объект «Снимок».

Код ошибки Когда
404 Игры нет

Поток игры

WebSocket, по которому сервер присылает объект «Снимок» при подключении и при каждом изменении игры.

Подключение: WS {DODA_URL}/v1/sessions/{id}/stream?role={роль}&token={токен}.

Параметр Тип Описание
role string teacher - полный снимок; player (по умолчанию) - урезанный
token string Для teacher - токен ведущего; для player - player_token, с ним игрок отмечается онлайн
Код ошибки Когда
401 role=teacher без верного токена ведущего; сокет не открывается

Сообщение сервера:

{"type": "snapshot", "session": {}, "players": [], "current_question": null, "leaderboard": [], "class_percent": null}
  • Других сообщений сервер не шлёт, сообщения клиента игнорируются.
  • Частые события склеиваются в одно сообщение раз в 250 мс; медленный клиент получает только последнее.
  • Сообщение со session.version меньше уже показанного - устаревшее, его пропускайте. Сообщение с той же версией показывайте: подключение и отключение игрока меняет players[].connected, не увеличивая версию.