javascript

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

  • вторник, 6 октября 2026 г. в 00:00:07
https://habr.com/ru/articles/1090476/

Привет, Хабр. Я соло разработчик: студия из одного человека и ИИ ассистента. У нас есть сайт 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. Главные выводы для себя:

  1. Кодовое слово как протокол синка: просто, надёжно, работает между устройствами и легко объясняется сюжетом.

  2. Расписание на клиенте: годится для всего, где подкрутка часов не рушит экономику.

  3. Шифр должен решаться карандашом. Если нужен CyberChef, порог входа уже слишком высокий.

  4. Метрика целями вместо аналитического бэкенда: ноль персональных данных, вся воронка на месте.

Сайт: https://tempapp.ru (папку дела найдёте, если осмотритесь). Сезон проходится в любое время, а сигнал, как положено, по пятницам после 19:00.

Спрашивайте в комментариях, отвечу на всё техническое. На сюжетное: без спойлеров, дело ещё открыто.