RUEN
Обсудить проект

Продукт студии

Quizar

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

участников в комнате
500
типов вопросов
7
наборов тестов
35

Открыть сайт — quizar.cc ↗

Первый экран Quizar

Ведущий создаёт викторину и открывает комнату, вопросы идут на общий экран, участники входят с телефона по шестизначному коду или 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