Дода: игра¶
Игра по тесту в реальном времени.
Ведущий (роль teacher или school_admin) управляет игрой с токеном; игроки входят по PIN без учётной записи и действуют с player_token.
Методы ведущего без токена отвечают 401, с ролью не ведущего - 403, чужой тест или игра - 403 или 404.
Порядок работы ведущего:
- «Активная игра»: если игра уже идёт, вернуться в неё; у ведущего одна активная игра.
- «Создание игры» по тесту: в ответе
pinиjoin_urlдля игроков. - Подключение к «Потоку игры» с ролью
teacher; экран строится из снимков. - «Старт», дальше фазы переключает сервер; «Следующая фаза», «Пауза», «Продолжение», «Завершение» - ручное управление.
- «Результаты» после игры.
Порядок работы игрока:
- «Вход в игру» по PIN и нику: в ответе
player_token. - Подключение к «Потоку игры» с ролью
playerиplayer_token. - «Ответ» на каждый вопрос; свой результат игрок видит в снимке на фазе
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, не увеличивая версию.