Своя игра для домашней компании: ведущий за ноутбуком, игроки — со своих телефонов. Вся логика на сервере, телефон нужен только чтобы жать кнопку и вводить ответы в финале.
npm install
npm run build:packs # разложить демо-паки в data/packs
npm run dev # сервер :3001 и клиент :5173При старте в консоль печатается ссылка для игроков и QR-код — наведите на него камеру телефона, подключённого к той же Wi-Fi сети.
| Экран | Адрес |
|---|---|
| Ведущий | http://localhost:5173/host |
| Игрок | http://<ваш-IP>:5173/join |
| Общий экран | http://localhost:5173/board |
| Редактор | http://localhost:5173/editor |
| Игры | http://localhost:5173/games |
npm run dev нужен, чтобы править код. Для настоящей игры удобнее собранный
вариант: всё работает одним процессом на порту 3001, а телефоны получают
готовые чанки вместо сотен модулей дев-сервера — первая загрузка заметно
быстрее, особенно на старых Android.
npm start # собрать клиент и запустить всё на :3001Адреса те же, что в таблице, только с портом 3001. Ссылка и QR в консоли сами ведут на нужный порт.
Общий экран стоит вывести на телевизор: на нём табло, вопрос и счёт без управления.
- Ведущий открывает
/host, выбирает пак — появляются код комнаты и QR. - Гости наводят камеру на QR и вводят имя. Код подставляется сам.
- Ведущий жмёт «Начать игру». Право хода получает первый вошедший.
- Чей ход — тот называет тему и цену вслух, а открывает вопрос ведущий. На телефоне этого игрока показано табло, но только для чтения.
- Ведущий читает вопрос и сам жмёт «Открыть кнопку»: темп задаёт он, а не таймер. Нажатие до открытия — фальстарт, блокировка на пару секунд.
- Кто нажал первым, отвечает вслух. Ведущий решает, когда времени было достаточно, и жмёт «Верно» или «Неверно».
- Неверный ответ снимает стоимость вопроса, и кнопка открывается остальным. Время, потраченное на чужой устный ответ, из их окна не вычитается. Вопрос закрывается, только когда ошиблись все или когда при открытой кнопке никто не нажал.
Ведущий в любой момент может поправить счёт, передать право хода, поставить паузу, снять вопрос, добавить времени на кнопку и отменить последнее действие.
Основной ход игры доступен с клавиатуры — ведущий читает вслух и смотрит на гостей, а не на мышь. Клавиши физические, поэтому работают и в русской раскладке; в поле ввода (правка счёта) они не срабатывают.
| Клавиша | Что делает |
|---|---|
| Пробел, Enter | дальше: открыть кнопку, к вопросу, после ответа, новый раунд |
+ |
верно (и в финале) |
− |
неверно (и в финале) |
| P | пауза и продолжить |
| ⌘Z, Ctrl+Z | отменить последнее действие |
Пробел нарочно не судит: вердикт выносится только плюсом или минусом.
Клиент раз в две секунды измеряет задержку до сервера и шлёт нажатие со своей меткой времени. Сервер переводит метку в своё время, отбрасывает неправдоподобные значения и, получив первое нажатие, ждёт ещё 150 мс, чтобы собрать остальные. Побеждает самая ранняя метка, а не самый быстрый интернет.
Кот в мешке передаётся другому игроку. Открывший видит только тему кота и его стоимость — текст вопроса скрыт от всех игроков, чтобы кота нельзя было отдать «с умыслом». Отвечает только получивший, кнопка не работает. Верно — деньги и право хода ему; неверно — минус ему, а право хода остаётся у передавшего.
Аукцион разыгрывается вслепую: игроки видят тему и цену, но не вопрос. Номинал ставит открывший, дальше по кругу можно поднять минимум на шаг, уйти ва-банк или спасовать. Пас необратим, а тому, кому нечем перебить, ход не предлагается. Победитель играет один, и счёт меняется на его ставку, а не на номинал.
Под клеткой-модификатором нет вопроса. Игрок называет клетку, и на него срабатывает эффект — иногда доброжелательный, иногда нет.
Сколько таких клеток попадёт на доску, задаётся в игре — от 0 до 5 на
раунд, а какие виды вообще могут выпасть, отмечается галочками там же, в
конструкторе на /games. Но какая именно клетка окажется модификатором,
решается один раз при создании комнаты и дальше не меняется до конца
вечера — переигровка комнаты не перетасует их заново.
Клетка неотличима от обычной цены, пока её не открыли, — и не только для игроков: ведущий тоже не знает заранее, под какими клетками спрятаны модификаторы. Право хода при этом остаётся у того, кто клетку открыл: вопроса не было, отвечать было нечего, и забирать у него ход было бы чистым наказанием.
Модификаторы не встречаются в финальном раунде и никогда не занимают клетку кота в мешке или аукциона — те места автор пака расставил осознанно, модификатор их не подвинет.
Сцену модификатора держит на экране modifierMs (по умолчанию 6 секунд)
или закрывает сам ведущий.
| Модификатор | Эффект |
|---|---|
| Перевёртыш | счёт открывшего меняет знак |
| Джекпот | +2000 открывшему |
| Пустышка | ничего не происходит |
| Ограбление | −500 лидеру среди остальных, +500 открывшему |
| Удвоение | счёт открывшего удваивается |
| Щедрость | −500 открывшему, +500 каждому другому |
| Подсказка | жетон подсказки открывшему |
| Обмен | открывший выбирает, с кем поменяться счётом |
Подсказка — это жетон, а не спрятанный текст: сам ведущий и так видит правильный ответ, поэтому он просто говорит подсказку вслух и списывает жетон кнопкой рядом с именем игрока, когда сочтёт нужным. Жетон сохраняется за игроком, пока его не потратят, — можно и на следующем вопросе.
Обмен — единственный модификатор с двумя действиями: сначала открывший выбирает цель на своём телефоне, и только после этого счета меняются местами.
В финал проходят игроки с положительным счётом — это отключается настройкой. Дальше:
- Игроки по очереди, начиная с того, у кого меньше очков, убирают темы, пока не останется одна.
- Каждый втайне ставит от 1 до своего счёта. Вопрос в этот момент ещё не показывают: ставят, зная только тему.
- Появляется вопрос, каждый пишет ответ текстом.
- Ведущий вскрывает ответы по одному, от меньшего счёта к большему, и судит. Чужую ставку и чужой ответ до вскрытия не видит никто.
Если кто-то отвалился и держит всех, ведущий жмёт «Не ждать отставших» — недостающие ставки считаются минимальными.
При вскрытии ведущий видит подсказку: письменный ответ сравнивается с правильным и принимаемыми без учёта регистра, ё, кавычек и пунктуации. «Совпадает» подсвечивает «Верно», «не похоже» — «Неверно», а опечатка, одна фамилия вместо полного имени или ответ внутри фразы помечаются как «похоже» и не подсвечивают ничего. Судит всё равно ведущий.
Под табло на /host свёрнута шпаргалка: все ещё не открытые вопросы
раунда с ответами, комментариями и пометкой кота и аукциона — чтобы
прочитать заранее и не споткнуться на имени. Экран ведущего бывает виден
гостям, поэтому по умолчанию она закрыта. Модификаторы в ней не выдаются:
под такой клеткой шпаргалка показывает вопрос, как у обычной.
Пока телефону нечего делать, внизу ряд реакций — 😂 🔥 👏 😱 🤦 🤔. Они всплывают на общем экране с именем игрока. Набор закрытый, а чаще раза в 600 мс с одного телефона сервер их не пропускает.
Телефон не вибрирует: большинство гостей играет с iPhone, а Safari
вибрацию из браузера не поддерживает, так что о событиях сообщает звук.
Браузер разрешает звук только после настоящего нажатия на страницу,
поэтому телефон, открывший игру не через /join (по ссылке или после
перезагрузки), сначала просит нажать на экран один раз.
Пока открыта игра, экран телефона и общего экрана не гаснет: иначе браузер рвал бы связь, пока телефон лежит без дела, и переподключался бы как раз к открытию кнопки.
Редактор живёт на /editor и работает без запущенной игры.
-
Быстрая вставка темы. Кнопка «Вставить тему текстом» принимает строки вида
цена | вопрос | ответ; четвёртая колонка — другие принимаемые ответы через точку с запятой. Перед добавлением показывается разбор и строки с ошибками.Тема: Кино 100 | Кто снял «Крёстного отца»? | Фрэнсис Форд Коппола | Коппола 200 | Какого цвета таблетку глотает Нео? | Красную -
Медиа. К вопросу и к ответу можно приложить картинку, аудио или видео до 50 МБ — файлы лежат в
uploads/<пак>/. -
Спецвопросы. Тип выбирается в форме: обычный, кот в мешке (с темой и своей стоимостью) или аукцион.
-
Порядок. Темы и вопросы перетаскиваются за значок слева.
-
Экспорт и импорт. «Скачать JSON» сохраняет пак файлом, «Импорт JSON» загружает его обратно как новый пак.
-
Паки из SIGame. «Импорт .siq» принимает архивы из сети: разбираются обе распространённые версии формата, переносятся типы вопросов, медиа из архива и комментарии к ответу. Формат менялся годами, поэтому после импорта показывается отчёт: что перенеслось, какие файлы не нашлись и какие вопросы нужно поправить руками. Молча ничего не теряется. Вопрос из одной картинки или видео получает текст-подсказку («Что на картинке?» или комментарий темы), варианты ответа дописываются в текст вопроса, а архив можно грузить до 512 МБ.
Пак без финала играть можно: после последнего раунда игра сразу покажет итоги. Так устроены многие паки SIGame.
Недоделанный пак сохраняется свободно — правка идёт постепенно. Но пока в проверке есть ошибки (пустой вопрос или ответ), создать комнату на этом паке нельзя.
Пак — это содержимое: темы и вопросы. Игра — это состав вечера: сколько
раундов, сколько тем в каждом и что взять для финала, собранные из одного
пака или сразу нескольких. Конструктор живёт на /games, рядом с
редактором.
В конструкторе ведущий отмечает, из каких паков брать темы, сколько будет раундов, сколько тем на раунд и сколько тем в финале, а затем сохраняет состав под именем. Там же включаются модификаторы (см. ниже) — сколько их на раунд и какие виды разрешены.
Сохранённая игра появляется на /host списком над обычным мастером
создания комнаты: если состав уже собран и проверен, его не нужно
пересобирать каждый вечер — выбрали по имени и начали.
Игра хранит не вопросы, а указатели на темы внутри паков. Поэтому правка вопроса или ответа в паке сама подтягивается во все игры, где эта тема участвует, — второй копии, которую можно забыть поправить, не существует. У указателей есть и обратная сторона: если пак с темой удалён, игра честно сообщает «потерялось тем: N» и отказывается начинаться, пока состав не пересобрали. Это осознанно — игра, которая молча уменьшилась, обнаружилась бы уже перед гостями.
Когда игра доходит до итогов, сервер записывает их в data/history/. На
/history (ссылка на главной) видно, кто сколько раз побеждал за все
вечера, сколько игр сыграл и какой у него рекорд, а ниже — итоги каждой
игры. Одно имя — один человек, без учёта регистра, как и при входе в
комнату. Победитель — тот, у кого лучший счёт, если он больше нуля; при
ничьей победа засчитывается всем. Если ведущий отменит действие и снова
доведёт игру до итогов, запись обновится, а не задвоится.
Если друзья не в вашей квартире, поднимите туннель к клиенту — порт 5173
при npm run dev или 3001 при npm start. Запросы к серверу пойдут через
тот же адрес. Ниже примеры для npm run dev.
Cloudflare Tunnel (не требует регистрации):
brew install cloudflared # или скачайте с сайта Cloudflare
cloudflared tunnel --url http://localhost:5173Команда напечатает адрес вида https://random-words.trycloudflare.com.
ngrok:
ngrok http 5173Дальше перезапустите игру, передав внешний адрес, — тогда ссылка и QR на экране ведущего будут вести туда, а не в вашу локальную сеть:
PUBLIC_URL=https://random-words.trycloudflare.com npm run devДомены trycloudflare.com, ngrok-free.app, ngrok.io и loca.lt
разрешены заранее. Для другого туннеля добавьте его домен:
ALLOWED_HOSTS=.example.com PUBLIC_URL=https://tunnel.example.com npm run devУчтите: туннель открывает игру всем, у кого есть ссылка. Комната защищена только кодом, так что не публикуйте адрес там, где его увидят посторонние.
Редактор, конструктор игр и паки через туннель не работают: менять их можно
только из домашней сети (localhost, адрес вида 192.168.…, имя ….local).
Так по ссылке нельзя ни удалить пак, ни подсмотреть ответы перед вопросом.
Сервер отличает туннель по заголовку Host, поэтому если запускаете свой
прокси перед сервером, не подменяйте его.
Задаются при создании комнаты, значения по умолчанию — в
shared/src/settings.ts:
| Настройка | По умолчанию | Что делает |
|---|---|---|
buzzOpenMs |
8000 | сколько всего кнопка открыта на вопрос |
buzzGraceMs |
150 | окно сбора нажатий |
buzzReopenMinMs |
3000 | минимум времени остальным после чужой ошибки |
falseStartLockMs |
2500 | блокировка за фальстарт |
penaltyOnWrong |
да | снимать ли стоимость за неверный ответ |
allowNegative |
да | разрешать ли уходить в минус |
finalRequiresPositive |
да | пускать в финал только с положительным счётом |
auctionStep |
100 | минимальный шаг повышения ставки |
answerRevealMs |
10000 | сколько держать правильный ответ на экране; 0 — закрывает ведущий |
modifierMs |
6000 | сколько держать сцену модификатора; 0 — закрывает ведущий |
Таймеров на чтение вопроса и на устный ответ нет: темп игры задаёт ведущий.
Игра нарисована «маркером»: толстые чёрные обводки, жёсткие смещённые
тени, кривоватые рамки на Rough.js и плывущие каракули на индиговом фоне.
Страница /style — живой стиль-гид: там собраны палитра, шрифты,
кнопки, аватары, таймер и спецсцены, и всё это можно потрогать.
Персонаж игрока собирается из его имени — форма головы, глаза, рот, причёска и цвет. У человека он один и тот же на всех экранах, и его же цвет ходит за ним в счёте, рамках и конфетти.
Звуки не скачиваются, а синтезируются скриптом в WAV — ни внешних сервисов, ни вопросов с лицензиями, работает без интернета:
node client/scripts/makeSounds.mjs # пересобрать сэмплы в client/public/soundsШрифты (Unbounded, Nunito и Caveat) тоже подключены локально через npm, а не с Google Fonts — по той же причине.
Спокойный режим. Игра уважает системную настройку «уменьшить движение», а на странице стиля есть переключатель, оставляющий только затухания, и тумблер плотности фоновых каракулей — на случай слабого ноутбука или стрима.
npm test # тесты: движок, сервер, клиент
npm run typecheck
npm run lint # oxlint
npm run lint:packs # паки в data/packs и их медиа
npm run build:packs
node client/scripts/makeSounds.mjsСлужебный скрипт для проверки поздних стадий игры без разыгрывания
партии: поднимает комнату с двумя игроками (токены seed-a, seed-b,
ведущий seed-host-token).
npx tsx --tsconfig server/tsconfig.json server/src/packs/seedRoom.ts 7777 round2
npx tsx --tsconfig server/tsconfig.json server/src/packs/seedRoom.ts 5555 finalshared/ типы пака, состояния комнаты и сокет-событий, валидация
server/ игровой движок, комнаты, хранилище, импорт паков
client/ экраны ведущего, игрока, табло и редактора
data/ паки, автосохранения комнат, история игр
uploads/ медиа вопросов
docs/ дизайн-документ и планы реализации
Игровая логика — чистый редьюсер: reduce(state, action) → {state, effects}, без сети и таймеров внутри. Таймеры и рассылка живут снаружи,
поэтому правила тестируются без сокетов, отмена действия — это снимок
состояния, а автосохранение — сериализация после каждого действия.
Клиенту уходит не состояние комнаты, а проекция его роли. Правильный ответ, комментарии ведущего и чужие ставки физически не попадают в данные игрока — на это стоят тесты, которые ищут ответ в сериализованной проекции.
Комната переживает перезагрузку страницы у любого участника и перезапуск
сервера: игрок возвращается по токену из localStorage, а если тот
потерян — по имени, вместе со своим счётом.
Телефон не открывает страницу. Проверьте, что он в той же сети, а не в мобильном интернете. Некоторые роутеры включают изоляцию клиентов — тогда поможет только туннель. На macOS может мешать файрвол: разрешите входящие подключения для Node.
Игрок вышел и не может вернуться. Пусть зайдёт заново по коду с тем же именем — счёт вернётся.
Звука нет. Браузеры не дают играть звук до первого касания экрана: нажмите что-нибудь. Выключатель звука есть на экранах ведущего и игрока, и выбор запоминается.
Проверяете с одного компьютера. Сессии ведущего, игрока и табло
хранятся раздельно, так что их можно открыть в соседних вкладках. Но два
игрока в одном браузере делят одну запись localStorage и перетирают друг
друга. Второго игрока откройте на другом адресе — localhost, 127.0.0.1
или адрес в локальной сети: для браузера это разные хранилища.