Детектив на статике: как я собрал ARG с пятью шифрами без бэкенда, регистрации и бюджета


Привет, Хабр. Я соло разработчик: студия из одного человека и ИИ ассистента. У нас есть сайт tempapp.ru, и на нём живёт «дело № 031»: сезонный детектив в формате ARG (alternate reality game). Игрок находит спрятанную папку с делом, ловит голубя свидетеля, читает чужой дневник 1999 года, разгадывает цепочку шифров и в финале выясняет, что расследование было не совсем расследованием.
Вся игра работает на статическом хостинге. Ни одной серверной строчки, ни регистрации, ни базы. Расходы на инфраструктуру: ноль рублей в месяц. Под катом разбираю, как это устроено и какие компромиссы пришлось принять.
Задача и ограничения
Хотелось: многошаговую игру на 6 эпизодов, прогресс игрока между эпизодами, сезонный ритм (особые события по пятницам), воронку аналитики, и чтобы всё это переживало паузы: сезон должен быть проходим в любое время, без живого админа.
Ограничения: статика на Object Storage + CDN, один разработчик, телефоны у большинства игроков.
Проблема первая: синк прогресса без аккаунтов
Серверного состояния нет. Прогресс игрока хранится в localStorage хаба, но «спутники» расследования (отдельные страницы спутники с уликами) должны как то сообщить хабу: «игрок дошёл, засчитай этап». Можно было бы положиться на общий localStorage внутри одного домена, но это ломается сценарии вроде «спутник прошёл на телефоне, хаб открыл на десктопе» и вообще выглядит хрупко.
Решение: кодовые слова как явный протокол синхронизации. Спутник в финале своего эпизода выдаёт игроку слово. Игрок вносит его на хабе, хаб засчитывает улику. Слово это одновременно и механика, и часть сюжета: оно существует внутри мира игры, а не в интерфейсе.

Проверка на хабе выглядит примерно так:
function checkCode(word){ const w = (word || '').toLowerCase().trim(); if (w === 'клевета'){ /* улики 3-5, цель case031_kleveta_hub */ } else if (w === 'протокол'){ /* улики 6-7, цель case031_protokol_hub */ } else if (w === 'голубь'){ /* уже сработало раньше */ } else { /* ложный след, цель case031_wrong_code */ }}Побочный эффект оказался важнее самой механики: игрок физически переносит слово из одного места в другое, как бумажную улику. Это ощущается как работа следователя, а не как форма на сайте.
Проблема вторая: шифры, которые решает человек
Цепочка сезона собрана так, чтобы каждый шаг требовал сменить способ мышления, но каждый шаг решался карандашом:
Шаг | Шифр | Разгадка |
|---|---|---|
1 | Цезарь +3: «ЁСОЦДЯ» | ГОЛУБЬ (алфавит 33 буквы, с Ё) |
2 | Морзянка | КЛЕВЕТА |
3 | Акростих из 8 записей дневника | ПРОТОКОЛ |
4 | Перестановка «УПШСОИ», ключ 1-7-0-9-1-9 | ТИШИНА (ключ: дата газеты 17.09.1999) |
5 | Дверь из пяти слов | ОТДЕЛ |
Морзянку голубь «стучит» сам: пульсы opacity на картинке, единица 170 мс. Ключ перестановки спрятан внутри лора: газета датирована 17.09.1999, и это реальная пятница (проверял по календарю, да). Каноническая дата исчезновения: ночь с четверга на пятницу. Игроки такое находят и ценят.
Проблема третья: сезонный ритм без сервера
События по расписанию обычно означают cron где то в облаке. Здесь расписание вычисляется на клиенте:
const d = new Date();const isSignal = d.getDay() === 5 && d.getHours() >= 19;Пятница, 19:00: на хабе загорается «сигнал» у папки дела, на спутниках открываются особые фразы и скрытая запись дневника. Сезонный ритм без единого таймера на бэке. Да, время клиента можно подкрутить. Для сюжетной игры это приемлемая цена: мы не турнир с призовым фондом, а честность игрока с самим собой часть жанра.
Проблема четвёртая: мобильные игроки
На десктопе есть приятный секрет: тайп‑трап. Игрок просто печатает слово на клавиатуре в любом месте страницы, буфер последних 8 клавиш ловит его:
buf = (buf + e.key.toLowerCase()).slice(-8);На телефоне клавиатуры нет, тайп‑трап мёртв. Решение: на спутниках живёт «полевая рация» (обычный input, стилизованный под рацию), а на хабе «полевой ввод кода». Тайп‑трап остался десктопным бонусом для внимательных, критический путь не зависит от него.
Аналитика без регистрации
Вся воронка собирается целями Яндекс.Метрики: case031_open, case031_ep1, case031_morse, case031_kleveta_hub, case031_friday9 и так далее. Каждый вызов обёрнут в try/catch, чтобы блокировщики не ломали игру:
function caseGoal(name){ try { ym(111******, 'reachGoal', name); } catch(e) {}}Так я вижу, где игроки застревают, не зная о них ничего лишнего. Идеальная сделка: у игры нет персональных данных, у меня есть воронка.
Что в итоге
Три эпизода из шести уже живут, суммарно это примерно 80 КБ HTML и JS. Сезон дешевле месяца аренды самого слабого VPS. Главные выводы для себя:
Кодовое слово как протокол синка: просто, надёжно, работает между устройствами и легко объясняется сюжетом.
Расписание на клиенте: годится для всего, где подкрутка часов не рушит экономику.
Шифр должен решаться карандашом. Если нужен CyberChef, порог входа уже слишком высокий.
Метрика целями вместо аналитического бэкенда: ноль персональных данных, вся воронка на месте.
Сайт: https://tempapp.ru (папку дела найдёте, если осмотритесь). Сезон проходится в любое время, а сигнал, как положено, по пятницам после 19:00.
Спрашивайте в комментариях, отвечу на всё техническое. На сюжетное: без спойлеров, дело ещё открыто.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.