Продукт студии
Quizar
Платформа живых викторин: ведущий открывает комнату, вопросы идут на общий экран, участники отвечают с телефона по коду.
- участников в комнате
- 500
- типов вопросов
- 7
- наборов тестов
- 35

Ведущий создаёт викторину и открывает комнату, вопросы идут на общий экран, участники входят с телефона по шестизначному коду или QR. Любую опубликованную викторину можно пройти и в одиночку — без ведущего и без регистрации.
Что здесь на самом деле трудно
Собрать викторину несложно. Трудностей три, и все они начинаются после того, как в комнате появляется тридцать человек с разным интернетом.
Очки не должны зависеть от скорости связи. Главная претензия к подобным платформам ровно эта: выигрывает не тот, кто знает, а тот, у кого лучше вайфай.
Обрыв связи не должен стоить игры. Телефон блокируется, вкладка перезагружается, сеть в школе моргает — и участник не должен терять имя, очки и уже отправленный ответ.
Правильный ответ не должен физически находиться на устройстве участника, пока вопрос открыт. Иначе игра ломается одним открытым инспектором.
Сервер — единственный источник истины
Клиент никогда не решает, когда закончился вопрос, какой ответ правильный и сколько начислить. Он показывает то, что прислал сервер, и отправляет то, что сделал участник.
Игровые правила живут в чистых модулях без ввода-вывода: конечный автомат комнаты — редьюсер, возвращающий декларативные эффекты; формула очков; проверка правильности по типам вопросов; компенсация задержки. Всё, что зависит от времени, сети или базы, спрятано за интерфейсами — хранилище комнат, рассылка, часы, сохранение. Поэтому движок целиком тестируется без сокета и без базы данных, и именно поэтому на него есть 35 наборов тестов, а не два смоук-прогона.
Очки, которые не зависят от интернета
очки = баллы × правильность × (0,70 + 0,30 × скорость) × множитель серии
Мгновенный верный ответ даёт 100%, верный у самого гудка — 70%. Максимальное преимущество за скорость — около 43%, а не двукратное: медленный правильный ответ всегда бьёт быстрый неправильный.
Компенсация задержки. Сервер видит только «отправил вопрос» и «получил ответ» — в этот промежуток входят два сетевых прыжка. Поэтому время размышления измеряет клиент, от момента отрисовки вопроса. Сервер этому числу не верит: он зажимает его в окно, ширина которого — измеренный самим сервером RTT этого сокета. Смошенничать можно ровно на величину собственной задержки, то есть на ту, которую и так списали несправедливо.
Первые 15% времени, но не больше полутора секунд, дают полный бонус — за это время вопрос только читают. Время на паузе вычитается из времени размышления: иначе разбор вопроса посреди игры обнулял бы бонус всем сразу.
Участник видит разбивку: сколько за правильность, сколько за скорость, сколько за серию.
Переподключение
При входе участник получает два токена. Короткоживущий подписанный JWT авторизует подключение сокета; долгоживущий токен возврата браузер хранит локально. На сервере лежит только SHA-256 этого токена, поэтому утечка базы не даёт угнать чужую сессию.
Возвращение восстанавливает имя, очки, серию, текущее состояние и уже отправленный ответ. Экраны ведущего возвращаются так же: пульт, перезагруженный посреди вопроса, сразу получает текущий вопрос, число ответов и разбор, а не пустую сцену до следующего. Ведущий считается ушедшим, только когда закрыт последний его экран, — закрытая вкладка проектора при открытом пульте участникам ничего не сообщает.
Правильные ответы не уходят на телефоны
Полезная нагрузка для клиента собирается по белому списку полей, а не вычёркиванием лишнего. Разница принципиальная: при белом списке новое поле в модели вопроса не может утечь по умолчанию — чтобы оно попало наружу, его нужно добавить осознанно.
На это есть тесты: они сериализуют открытый вопрос каждого из семи типов и проверяют, что внутри нет ни признака правильности, ни пояснения, ни принимаемых ответов.
Снимок викторины
При запуске игры викторина замораживается. Ответы ссылаются на идентификаторы вопросов из снимка обычными строками, а не внешними ключами, поэтому позднее редактирование или архивирование викторины не может задним числом изменить результаты прошедших игр.
Семь типов вопросов с частичными баллами
Один ответ, несколько ответов, правда/ложь, короткий ответ, расстановка по порядку, сопоставление пар и опрос без баллов. Частичные баллы там, где они осмысленны: у множественного выбора — попадания минус ложные срабатывания, у расстановки — нормированный коэффициент Кендалла, у сопоставления — доля верных пар.
Короткий ответ нормализует ввод: регистр, лишние пробелы, пунктуация и ё/е на результат не влияют, опечатки в пределах допуска считаются верным ответом.
Картинки перекодируются, а не проверяются
Загруженный файл декодируется и кодируется заново в WebP. Это не перестраховка, а сама суть проверки: файл, который одновременно корректный PNG и корректный HTML, проходит любую проверку по сигнатуре, но не переживает перекодирование. Заодно снимаются метаданные EXIF — включая координаты, где снят кадр.
Формат определяется декодером, а не тем, что заявил браузер. SVG отклоняется, хотя декодируется прекрасно: это документ, который умеет исполнять скрипты, и отдавать его с собственного домена нельзя.
Один порт, один процесс
App Router не умеет держать WebSocket-сервер в обработчике маршрута, поэтому собственная точка входа поднимает Next программно и вешает Socket.IO на тот же HTTP-сервер. Один порт, один процесс, одна команда запуска.
По умолчанию состояние комнат живёт в памяти. Если задать адрес Redis, на него переключаются сразу три вещи — хранилище комнат, ограничитель частоты и рассылка сокета, — и больше в коде не меняется ничего.
Доступность
Варианты ответа различаются цветом, формой, номером и клавишей: играть можно в оттенках серого
и на выцветшем проекторе. Есть режим для нарушения цветовосприятия с палитрой Окабэ — Ито,
уменьшенная анимация по системной настройке и по настройке в профиле, полное управление
с клавиатуры и aria-live на всём, что обновляется само. Адаптив — от 360 px до проектора.
TypeScript · Next.js 16 · React 19 · Socket.IO · PostgreSQL 18 · Prisma 7 · Auth.js · Tailwind CSS · Vitest · Playwright