# Toddler Playroom: платформа и гейм-дизайн

Этот документ — единый продуктовый контракт для lobby, навигации, прогресса и всех игр
Toddler Playroom. Он развивает требования из `games-catalogue.json`: самостоятельная игра ребёнка
2–5 лет, сессии по 3–10 минут, iPad как основное устройство, отсутствие рекламы, регистрации,
покупок, трекинга и тёмных паттернов.

## Задачи пользователя

- Когда ребёнок открывает Toddler Playroom, он хочет сразу выбрать привлекательную игру, чтобы начать
  играть без чтения инструкции и помощи взрослого.
- Когда ребёнок проходит небольшой законченный эпизод, он хочет получить понятный добрый знак
  успеха, чтобы видеть свою общую коллекцию достижений во всей платформе.
- Когда ребёнок ошибается или отвлекается, он хочет спокойно продолжить, чтобы экспериментировать
  без страха проиграть или потерять прогресс.
- Когда взрослый даёт ребёнку устройство, он хочет предсказуемую безопасную платформу, чтобы
  ребёнок не попал в рекламу, покупку, регистрацию или на внешний сайт.
- Когда разработчик добавляет игру, он хочет один контракт навигации, завершения и наград, чтобы
  новая игра ощущалась частью Toddler Playroom, а не отдельной страницей.

## Неподвижные принципы

1. На экране всегда одна понятная задача. Если ребёнку нужно выбирать объект, управлять героем
   и читать длинное объяснение одновременно, сцену следует упростить.
2. Главное действие считывается из самой сцены. Короткая фраза или голос помогают, но не являются
   единственным способом понять задачу.
3. Каждое осмысленное действие получает мгновенную добрую реакцию: движение, свет, звук или
   изменение мира. Интерфейс никогда не стыдит ребёнка.
4. По умолчанию игра без таймера, жизней, таблицы лидеров, проигрыша и наказания. Таймер допустим
   только как спокойный индикатор процесса, никогда как угроза потерять прогресс.
5. Ошибка не отнимает звёзды и не сбрасывает пройденное. Объект мягко возвращается, препятствие
   останавливает героя, а после нескольких попыток подсказка становится заметнее.
6. Действие можно повторять сколько угодно. Повтор снова даёт праздник, но не превращается в
   бесконечную ферму общей награды.
7. Один палец — основной способ игры. Точность не должна быть испытанием: цели крупные, зоны
   попадания щедрые, а предмет не скрывается под пальцем.
8. Развлечение и чувство самостоятельности важнее учебной формулировки. Обучение возникает из
   действия, ритма, сравнения и любопытства.
9. Все персонажи, сцены, уровни, тексты, музыка и визуальная айдентика оригинальны. Референсы
   дают механику и возрастную доступность, но не копируемый контент.

## Единая модель платформы

Toddler Playroom состоит из трёх узнаваемых состояний:

1. **Lobby** — дом платформы и каталог игр.
2. **Игра** — полноэкранная сцена с одинаковой платформенной навигацией поверх уникальной
   механики.
3. **Завершение** — короткий праздник с заработанной звездой и ясным следующим выбором.

Переходы всегда обратимы: lobby открывает игру, кнопка «домой» возвращает в lobby, системная
кнопка Back возвращает на предыдущий ожидаемый экран. Прямой URL конкретной игры остаётся
рабочим: ребёнок может начать сразу, а платформа всё равно показывает общие звёзды и путь домой.

Платформенные примитивы — навигация, сохранение звёзд, настройки звука и завершение уровня —
являются обоснованной общей частью продукта. Игровая механика, сцены и runtime-ресурсы остаются
самодостаточными внутри `games/<slug>/`; общие зависимости подключаются относительными путями,
чтобы прямой URL `/games/<slug>/` продолжал работать.

## Lobby

Lobby должен ощущаться как игровая комната, а не административный каталог.

- Вверху находится компактная марка Toddler Playroom и общий счётчик звёзд. Счётчик понятен по форме
  звезды без необходимости читать число.
- Основное пространство занимают крупные карточки игр с оригинальным изображением, коротким
  названием и одним визуальным намёком на главное действие. Вся карточка — единая цель касания.
- Первая полезная карточка видна без прокрутки на iPad. На телефоне допускается вертикальная
  прокрутка lobby, но сама игра никогда не прокручивает страницу.
- Уже посещённая игра показывает заработанные звёзды, а не процент, оценку или красный статус.
  Новая игра может иметь спокойный значок «новая», который исчезает после первого запуска.
- Недоступных карточек и обещаний «скоро» в детском пространстве нет. Концепты остаются в данных
  проекта, но lobby показывает только действительно игровые маршруты.
- Выбор игры требует одного касания. Между карточкой и сценой нет модального описания, логина,
  рекламы или промежуточной витрины.
- Lobby сохраняет позицию прокрутки на время перехода в игру, чтобы возврат «домой» не заставлял
  искать карточку заново.
- Звук lobby спокойный и необязательный. Автовоспроизведение до пользовательского жеста запрещено;
  состояние mute едино для всей платформы.

## Навигация внутри игры

Каждая игра использует один и тот же компактный платформенный HUD, расположенный с учётом
`safe-area-inset-*` и не перекрывающий цели:

- **Домой** — заметная кнопка с домиком, всегда возвращает в `/games/` без подтверждения и
  наказания. Игра сохраняет уже завершённые результаты до перехода.
- **Звёзды** — общий счётчик, визуально вторичный во время игры; он не закрывает задачу и не
  превращает прохождение в гонку.
- **Звук** — один понятный переключатель для эффектов, музыки и речи; его состояние общее между
  играми. Если игре нужна кнопка повторить фразу, она расположена рядом и имеет другой символ.
- **Полный экран** — видимая кнопка там, где API доступен. Отказ браузера не ломает игру и не
  показывает тревожную ошибку.

Кнопки платформы стоят в одинаковом порядке и имеют одинаковую семантику во всех играх. Их
визуальная тема может слегка подстраиваться под сцену, но символ, положение и поведение остаются
узнаваемыми. Управление механикой не размещается вплотную к системной зоне экрана и кнопке домой.

## Сквозные звёзды

Звезда — знак завершённого маленького приключения, а не валюта и не оценка ребёнка.

### Правила награды

- Первое завершение каждого стабильного уровня даёт ровно одну общую звезду.
- Повтор уровня снова показывает праздник и уже полученную звезду, но не увеличивает общий счёт.
- Звезда никогда не отнимается, не тратится и не сгорает. Нет магазина, серии дней, рейтинга или
  сравнения с другими детьми.
- Общий счёт равен числу уникальных завершённых уровней. Он вычисляется из набора завершений,
  поэтому повторный сигнал, перезагрузка страницы или двойное касание не создают дубликат.
- Игра без традиционных уровней определяет осмысленный законченный эпизод: собранный пазл,
  завершённую доставку, найденный набор, открытую диораму. Простое касание или ожидание не является
  завершением и не должно позволять фармить звёзды.
- Игра может иметь сколько угодно локальных декоративных наград, но только единый платформенный
  сигнал завершения меняет общий счёт.

### Идентичность уровня

Каждое завершение имеет стабильные `gameId` и `levelId`. Идентификаторы не зависят от языка,
названия, порядка карточек или случайной раскладки. После публикации их не переиспользуют для
другого контента. Переименование видимого уровня не должно терять или дублировать звезду.

### Хранение в localStorage

Прогресс хранится локально, без аккаунта и отправки на сервер, в одном версионированном объекте
`localStorage` для всей платформы. В нём есть:

- версия формата;
- набор уникальных пар `gameId` + `levelId`;
- время первого завершения, если оно доступно;
- единые настройки звука и другие явно платформенные предпочтения.

Общий счёт не является отдельным источником истины: он пересчитывается из уникальных завершений.
Запись атомарно заменяет целый валидный объект. Повреждённый JSON, неизвестная старая версия,
запрет `localStorage` или исчерпанная квота не должны блокировать игру: платформа продолжает
текущую сессию, показывает безопасное значение и не обещает сохранение, которого не произошло.
Новые версии читают известные поля и сохраняют совместимый прогресс; миграция никогда не
уменьшает число уже заработанных звёзд.

Если несколько вкладок открыты одновременно, обновление прогресса в одной вкладке отражается в
остальных без перезагрузки. Локальное удаление данных браузера честно начинает новую коллекцию;
платформа не использует fingerprinting или скрытое восстановление.

## Семантика завершения

У каждого уровня есть одно наблюдаемое условие завершения. Оно связано с задачей уровня, а не с
таймером, переходом по ссылке или скрытым техническим событием.

- Пазл завершён, когда все детали стоят на своих местах.
- Лабиринт завершён, когда герой достиг видимой цели.
- Поиск завершён, когда найден оговорённый набор.
- Творческая сцена, у которой нет объективно правильного результата, использует явное доброе
  действие «готово» либо не выдаёт общую звезду.

Сигнал завершения идемпотентен: сколько бы раз игра его ни отправила, пара уровня появляется в
прогрессе один раз. Сначала сохраняется прогресс, затем показывается праздник. Если сохранение
недоступно, ребёнок всё равно получает финальную реакцию, но платформа не увеличивает постоянный
счёт ошибочно.

Экран завершения длится коротко и не захватывает управление надолго. Он показывает большую
звезду, короткую позитивную фразу и три крупные возможности: следующий уровень, сыграть ещё раз,
домой в lobby. Если следующего уровня нет, кнопка «следующий» отсутствует, а не ведёт в тупик.
Праздник понятен без звука и при `prefers-reduced-motion`.

### Проверяемые сценарии

#### Первая звезда

- Given уровень ещё не отмечен завершённым
- When ребёнок выполняет его наблюдаемую задачу
- Then платформа добавляет одну уникальную звезду
- And lobby и экран завершения показывают новый общий счёт

#### Повтор уровня

- Given уровень уже отмечен завершённым
- When ребёнок проходит его повторно
- Then праздник повторяется
- And общий счёт звёзд не меняется

#### Мягкая ошибка

- Given ребёнок выбирает неверный объект или отпускает предмет вне цели
- When игра отвечает на действие
- Then уже заработанный прогресс не уменьшается
- And ребёнок может сразу попробовать снова с доброй или усиленной подсказкой

#### Возврат домой

- Given ребёнок находится в любой активной сцене
- When он нажимает платформенную кнопку домой
- Then открывается lobby в ожидаемой позиции
- And подтверждённые звёзды и настройки остаются видимыми

## Touch, responsive layout и доступность

- Любой интерактивный контрол имеет минимум **44 CSS px** по каждой стороне; игровые цели и
  основные кнопки стремятся к 64 CSS px или больше. Визуальный объект может быть меньше только
  при сохранении щедрой невидимой зоны попадания.
- Проверяются iPad landscape, iPad portrait, телефон шириной 375 px и desktop. HUD не выходит за
  safe area, не перекрывает задачу и не создаёт горизонтальную прокрутку.
- Основное управление построено на Pointer Events. Игра отслеживает один активный указатель;
  дополнительные касания спокойно игнорируются. Потеря pointer capture безопасно завершает жест.
- Системные жесты и прокрутка отключаются только внутри игровой поверхности, а не глобально там,
  где lobby должен прокручиваться.
- Перетаскиваемый предмет смещается так, чтобы оставаться видимым над пальцем. Зона прилипания
  шире видимой цели; отпускание мимо мягко возвращает предмет.
- Mouse и клавиатура остаются рабочими fallback. Интерактивные элементы имеют видимый focus,
  понятные accessible names и логичный порядок. Клавиша Escape выходит из полного экрана, но не
  стирает прогресс.
- Цвет никогда не является единственным признаком. Форма, положение, контур, символ, движение или
  голос дублируют цветовой смысл. Контраст текста и контролов остаётся читаемым на живой сцене.
- Критическое сообщение не передаётся только звуком, вибрацией или анимацией.

## Движение и визуальная безопасность

По умолчанию анимация короткая, плавная и объясняет результат действия. Нельзя использовать
вспышки, резкие shake-эффекты, тревожные красные экраны, внезапный zoom или непрерывное движение,
мешающее выбрать цель.

При `prefers-reduced-motion: reduce`:

- декоративные частицы, параллакс, покачивание и перелёты сокращаются или отключаются;
- состояние до и после действия остаётся различимым;
- завершение показывается статичной звездой и текстом без потери информации;
- камера не совершает автоматических длинных перемещений.

3D-камера стабильна, горизонт предсказуем, цели не скрываются перспективой. Если качество нужно
снизить, сначала упрощаются тени, частицы и постэффекты, а не точность управления. Цель — 60 fps,
приемлемый минимум — 30 fps; тяжёлая работа и звук приостанавливаются в скрытой вкладке.

## Контент и тон

- Текст короткий, позитивный и соответствует возрасту. Одна инструкция — одно действие.
- Голосовые фразы локализованы, повторяемы отдельной кнопкой и не длиннее, чем нужно для задачи.
- Неверный ответ сопровождается нейтральной реакцией: «попробуем ещё», мягким движением или
  усилением подсказки. Нет «ошибка», «проиграл», насмешки или отрицательного счёта.
- Сцена не требует знания брендов, чтения мелкого текста или культурно узкой отсылки.
- В игре нет рекламы, покупок, регистрации, персональных данных, внешних ссылок и социальных
  механик. Взрослая служебная информация не находится внутри игрового потока.

## Единый стиль каталога

Платформенный «хром» одинаков во всех играх, а художественная идентичность сцены — своя у каждой
игры. Граница проходит так: всё, что ребёнок нажимает ради навигации и настроек, — платформенное;
всё, что он нажимает ради игры, — авторское, но подчиняется общим правилам размера и отклика.

- **Токены хрома** заданы в `games/platform/platform-shell.css`: бумага `#fffaf0`, чернила
  `#30254d`, слива `#7452b8`, звезда `#ffd84d`, тень `#39295e`; обводка 3 px, радиус 18–24 px,
  жёсткая смещённая тень, скруглённая гарнитура `ui-rounded, "SF Pro Rounded",
  "Arial Rounded MT Bold", system-ui`. Кнопки «звук» и «полный экран» — квадраты не меньше
  56 × 56 CSS px в правом верхнем углу с одинаковыми нарисованными SVG-иконками (динамик,
  четыре уголка); кнопка «домой» со звёздами — слева вверху, её отрисовывает платформа. Игра
  может перекрасить фон кнопки под сцену, но не меняет форму, иконку, порядок и положение.
- **Типографика.** Детский текст — одна скруглённая гарнитура из списка выше; пиксельная или
  рукописная гарнитура допустима только как часть явной художественной идентичности сцены
  (например, пиксельный город) и только для заголовков и коротких подписей, а не для подсказок.
  Минимальный кегль детского текста — 16 px, подсказок в панели — 18 px.
- **Язык (решение Ивана 2026-11-11, главнее любых прежних правил).** Игры — обучалки для детей-мультилингвов
  из постсоветского пространства в Европе. Каждая игра говорит на трёх языках: русском, английском и
  эстонском, и в какой-то мере учит языкам (названия предметов, цветов, фигур, цифр, букв звучат и
  подписываются на выбранном языке). Язык выбирается один раз на уровне всей платформы (lobby и
  каждая игра), хранится как общая настройка и применяется ко всем играм. Есть режим «смешанный»:
  тогда уровни и задания одной игры даются на разных языках по очереди. Весь детский текст и голос
  хранятся по трём языкам; никакую языковую версию (включая английские mp3) не удалять.
- **Голос (решение Ивана 2026-11-12).** Весь голос во всех играх и на всех трёх языках озвучивает один
  рассказчик: голос Lila (ElevenLabs, voice_id `NoOVOzCQFLOvtsMoNcdT`) на модели `eleven_v3` с
  `language_code` языка реплики. Генерация только через `tools/build_audio_assets.py`; файлы
  называются `voice-<line>-<lang>.mp3`. Другие голоса и синтез браузера допустимы только как
  временная замена, пока клип не записан. Эстонские реплики пишутся короткими предложениями
  («See on õun!», «Üks kristall!»), а не голыми словами: одно-двухсловные эстонские клипы Lila на
  `eleven_v3` читает с английским акцентом. Машинная проверка коротких эстонских клипов —
  ElevenLabs Scribe с принудительным `language_code=et` (`bb voice transcribe` эстонского почти не
  слышит); окончательно эстонский принимает носитель языка по списку `/estonian-review`.
- **Тихие тесты.** Автоматические браузеры (Playwright, `navigator.webdriver`) никогда не говорят
  синтезом речи ОС: `platform.speak` в них молчит, если нет готового mp3. Тестовые браузеры
  запускаются только с `--mute-audio` (Playwright добавляет его сам; не отключать через
  `ignore_default_args`), ни `afplay`, ни headed-браузер со звуком для проверок не используются.
- **Иконография.** Любой символ в интерфейсе — нарисованная SVG- или canvas-иконка с единой
  толщиной штриха 2–2.5 px на сетке 24 px (или пиксельный спрайт в пиксельной игре). Один и тот
  же смысл — одна и та же иконка во всех играх: домой, звук, полный экран, повтор фразы, подсказка
  «покажи путь», «ещё раз», «дальше», звезда.
- **Грамматика отклика.** Касание цели — мгновенный визуальный ответ (сжатие/подсветка) и
  короткий звук. Верно — объект «садится» на место с мягким отскоком и одним радостным звуком.
  Неверно — объект мягко возвращается без красного и без тряски; с третьей попытки подсказка
  усиливается сама. Завершение уровня — единый платформенный праздник: большая нарисованная
  звезда, короткая фраза, три крупные кнопки «ещё раз», «дальше», «домой».
- **Движение.** Переходы 150–400 мс, кривые с лёгким перелётом, никакого непрерывного дрожания
  целей; при `prefers-reduced-motion` остаются только статичные состояния.

## Языковой слой платформы

**Binding API, implemented in `games/platform/platform-shell.mjs`.** Игры не читают и не пишут
`localStorage` языка напрямую — только через API ниже.

- **Хранение.** Отдельный ключ `tokenville.games.language.v1` со значением
  `{"version":1,"language":"ru"|"en"|"et"|"mix"}` (не поле внутри `tokenville.games.settings.v1`;
  смешанный режим называется `"mix"`). Без сохранённого значения язык берётся из
  `navigator.languages` (первый из ru/en/et), иначе `"ru"`.
- **Событие.** `LANGUAGE_EVENT = "tokenville:language-changed"` — `CustomEvent` на `window`,
  `detail: { language }`. Изменение в другой вкладке приходит через `storage` и тоже
  превращается в это событие.
- **Экспорт модуля:** `LANGUAGE_STORAGE_KEY`, `LANGUAGE_EVENT`, `LANGUAGES` (`["ru","en","et"]`),
  `LANGUAGE_SETTINGS` (`["ru","en","et","mix"]`), `getLanguage()`, `setLanguage(setting)`,
  `languageFor(key, setting?)`, `pickText(textByLang, lang)`, `speak(lines, options)`.
- **Контроллер `mountPlatform({ gameId })`** дополнительно отдаёт:
  - `getLanguage()` -> `"ru" | "en" | "et" | "mix"` — сохранённая настройка;
  - `setLanguage(setting)` — меняет язык для всей платформы;
  - `languageFor(key)` -> `"ru" | "en" | "et"` — конкретный язык уровня или задания. В режиме `mix`
    это детерминированная ротация по `gameId` и ключу (индекс уровня, `levelId`, id задания):
    соседние уровни и задания звучат на разных языках, а повтор того же ключа — на том же;
  - `text({ ru, en, et }, key?)` -> строка на языке `languageFor(key)` (с откатом на ru/en/et);
  - `speak(lines, { key?, audio?, rate? })` -> `Promise<boolean>`: `lines` — `{ ru, en, et }` или
    `{ lang, text }`; `audio` — `{ ru: url, en: url, et: url }` с готовыми файлами, которые всегда
    важнее Speech Synthesis (эстонского голоса на iPad часто нет). Учитывает общий mute, ищет голос
    нужного языка, при отсутствии голоса и файла молча возвращает `false`, никогда не бросает;
  - `onLanguageChange(callback)` -> функция отписки; при смене языка игра перерисовывает текст.
- **Контрол.** Платформа сама вставляет в каждую игру компактную кнопку языка
  (`[data-tokenville-language]`, надпись RU / EN / ET / MIX) в левом верхнем углу сразу под кнопкой
  «домой»; касание открывает маленькое меню из четырёх вариантов. Игре не нужна своя разметка.
  Левый верхний «платформенный угол» — от края до `var(--tokenville-corner-right)` по горизонтали и
  до `var(--tokenville-corner-bottom)` по вертикали (около 86 × 114 px на телефоне и 100 × 118 px на
  desktop) — игра держит свободным от своих панелей и целей. Пока игра этого не сделала, платформа
  временно переносит кнопку в первое свободное место (справа от «домой», внизу слева, внизу справа),
  но это запасной путь, а не контракт: содержимое canvas он не видит. Если угол занять нечем
  освободить (карта упирается в высоту телефона), игра объявляет своё место —
  `<span data-tokenville-language-slot></span>` в своём HUD; платформа вставляет кнопку туда
  (не меньше 64 × 44 px на телефоне, 86 × 44 px на desktop), меню остаётся в пределах экрана.
  В lobby тот же выбор — четыре кнопки в шапке.
- **Подключение в игре.** Детский текст и голос хранятся как `{ ru, en, et }`; язык уровня или
  задания берётся из `platform.languageFor(levelId)` (или `` `${levelId}:${taskIndex}` ``), никогда
  напрямую из `getLanguage()`, иначе режим `mix` сломается; озвучка — через `platform.speak` с
  готовыми файлами по языкам, где они есть; при `onLanguageChange` игра сразу перерисовывает свои
  подписи вне уровня (карта, старт, кнопки) без сброса уровня и звёзд, а идущий уровень доигрывается
  на своём языке — новый язык действует со следующего уровня (так сделан «Буквенный патруль»).
  Язык выбирается только платформой: собственный переключатель языка или своё сохранённое значение
  языка в игре запрещены (решение Ивана 2026-11-12: один выбор для всего, доступный с любого экрана).
  Кнопка «домой» и тост звезды уже переводятся платформой. Тост «Звёздочка твоя!» говорит на
  языке уровня: по умолчанию это `languageFor(levelId)` — того `levelId`, который игра передаёт в
  `completeLevel`. Ротация в `mix` идёт только по ключам с номером на конце (`level-1`, `level-2`);
  у словесного id вроде `spring-clouds` язык стабильный, но не чередуется. Поэтому игра, которая
  берёт язык уровня по индексу (`languageFor("level-1")`), а звезду отдаёт по словесному id, передаёт
  тот же ключ в завершение: `completeLevel("spring-clouds", { languageKey: "level-1" })` или готовый
  язык `completeLevel(levelId, { language: "ru" })`. Иначе в `mix` уровень и тост разойдутся.
  Хром платформы (слово «Игры» в кнопке «домой», меню языка, aria-подписи) в `mix` говорит на
  стабильном языке игры — `languageFor(gameId)`, — а не на языке текущего уровня: он один на всех
  экранах игры и не прыгает между уровнями; это принятое решение, а не дефект. Игра показывает язык
  текущего уровня своим нарисованным значком RU / EN / ET на панели задания.
  Допустимые приёмы, проверенные в каталоге: ранний inline-модуль сразу после `mountPlatform`, который
  переписывает статичную русскую разметку на выбранном языке до загрузки движка с CDN (или отдельный
  `boot.mjs`, который при сбое CDN показывает трёхъязычное сообщение); ключ уровня с номером
  (`level-N`), чтобы соседние уровни в `mix` всегда различались; очередь голоса, в которой после
  текущей реплики играет только последняя ожидающая — реплики не обрываются, а одновременно звучит
  всё равно одна.
- **Ранний монтаж.** В `index.html` каждой игры перед её модулем стоит
  `<script type="module">import { mountPlatform } from "../platform/platform-shell.mjs"; mountPlatform({ gameId: "<slug>" });</script>`:
  угол платформы (домой, звёзды, язык) появляется до загрузки 3D-движка с CDN и остаётся рабочим на
  экране загрузки и при сбое CDN. Повторный `mountPlatform` из модуля игры возвращает тот же контроллер.
  `tests/language_browser_verify.py` проверяет кнопку языка во всех играх на старте, в игре, на
  празднике, на экранах ошибки и загрузки и при сбое CDN в четырёх размерах экрана.

## Планка качества игры

Игра считается готовой только когда одновременно выполнено всё ниже; эта планка намеренно выше,
чем у обычной бесплатной детской игры.

1. **Уровни и разнообразие.** Не меньше трёх различимых уровней или миров с видимой прогрессией
   (новая механическая деталь, новая сцена или новый тип цели — не просто «больше того же»).
   Раскладка не повторяется дословно при повторном запуске.
2. **Концепция.** У игры есть одна собственная идея, которой нет у соседей по каталогу, и она
   читается с первого экрана без текста.
3. **Сцена и UI.** Нет текстовых кнопок там, где у других игр иконки; нет перекрытий HUD и целей
   на четырёх контрольных viewport; нет интерфейсных ошибок в консоли.
4. **Без проигрыша.** Нет таймера-угрозы, жизней, штрафа и тупика; любой уровень проходим одним
   пальцем без подсказки взрослого.
5. **Сочность.** Каждое действие даёт звук и движение; завершение — платформенный праздник;
   режим без звука и `prefers-reduced-motion` остаются понятными.
6. **Проверки.** Логика уровней покрыта `node:test`; четыре viewport, pointer cancel, Back,
   звезда и повтор уровня проверены в браузере; скриншоты приложены к записи в Beads.

## Запрет эмодзи

Эмодзи запрещены везде: в интерфейсе, тексте, `aria-label`, каталоге `games-catalogue.json`,
голосовых репликах, обложках, исходном коде, комментариях, тестах и документации репозитория.
Под запрет попадают все символы со свойством Unicode `Emoji`/`Extended_Pictographic`, флаги,
модификаторы и последовательности ZWJ, а также любые пиктографические символы Unicode,
используемые как иконки: звёзды (`U+2605`, `U+2606`, `U+2B50`), галочки (`U+2713`, `U+2714`),
стрелки (`U+2190`–`U+21FF`, `U+27A1` и подобные), геометрические фигуры (`U+25A0`–`U+25FF`),
ноты, домики, лампочки, солнца и прочие знаки блоков `U+2600`–`U+27BF`, `U+2B00`–`U+2BFF`.
Каждый такой знак заменяется нарисованной SVG- или canvas-иконкой с `aria-hidden="true"` и
текстовым accessible name на кнопке. Разрешены только буквы, цифры и типографская пунктуация
(`–`, `—`, `…`, `«»`, `·`, `№`). Репозиторный тест сканирует `games/**` (кроме `vendor/`),
`games-catalogue.json` и `docs/` и падает при первом найденном запрещённом символе.

## Аудио

- Web Audio и речь начинают звучать только после пользовательского жеста.
- Mute доступен всегда, действует во всех играх и не препятствует прохождению.
- Звуки короткие, добрые, различимые и не складываются в перегруженный шум. Одновременно звучит
  не более одного главного голосового сообщения.
- Похвала разнообразна, но не оценивает личность ребёнка. Подтверждается действие: «нашёл»,
  «готово», «звезда», а не превосходство над другими.
- Повтор голосового задания не сбрасывает сцену. Смена языка не меняет идентичность уровня и
  заработанные звёзды.

## Тестовый контракт

### Логика и данные

- первое завершение добавляет одну звезду;
- повторное и конкурентное завершение идемпотентны;
- общий счёт выводится из уникальных пар уровня;
- повреждённый, пустой, старый и недоступный `localStorage` обрабатываются без падения;
- миграция сохраняет или увеличивает число звёзд, но не уменьшает его;
- стабильные идентификаторы уровней уникальны во всём каталоге.

### DOM-контракт

- во всех играх присутствуют доступные кнопки домой, mute и полного экрана, когда он поддержан;
- контролы имеют минимум 44 CSS px и accessible names;
- прямые и относительные пути работают из `/games/<slug>/`;
- экран завершения предоставляет replay, следующий доступный уровень и lobby без тупиков.

### Browser verification

- desktop, iPad portrait, iPad landscape и телефон 375 px не имеют переполнения HUD;
- touch, drag, pointer cancel, keyboard fallback, browser Back и возврат в lobby проверены;
- звезда, заработанная в одной игре, видна в другой игре и в lobby после перезагрузки;
- повтор уровня не увеличивает счёт;
- mute и reduced motion сохраняют понятную обратную связь;
- WebGL canvas имеет разнообразные пиксели, камера показывает сцену, console errors отсутствуют;
- отдельный прогон проверяет production URL после публикации.

## SLC-порядок развития платформы

1. **Единое основание:** узнаваемый lobby, кнопка домой, общий счёт и одна идемпотентная звезда за
   завершённый уровень. Срез закончен, когда ребёнок может пройти путь lobby, игра, звезда и снова lobby
   хотя бы в одной реальной игре.
2. **Покрытие каталога:** каждая опубликованная игра получает стабильные уровни, единый HUD,
   completion semantics и responsive browser-проверки. Срез закончен, когда ни одна карточка не
   выбивается из платформенного маршрута.
3. **Полировка:** визуальные темы карточек, переходы, усиленные подсказки, миграции формата и
   производительность улучшаются только после полного рабочего контура.

Каждый срез должен быть простым, привлекательным для ребёнка и полностью проходиться от выбора
игры до безопасного возврата. Половина инфраструктуры без законченного детского маршрута не
считается релизом.

## Чеклист новой игры

1. Сформулировать одну понятную задачу и наблюдаемое завершение каждого уровня.
2. Назначить стабильные `gameId` и `levelId`; проверить уникальность и невозможность фарма звёзд.
3. Создать `games/<slug>/index.html` и локальные ресурсы с относительными URL.
4. Подключить единый HUD, общий mute, полный экран, кнопку домой и платформенное завершение.
5. Сделать неверное действие мягким; убрать ненужный таймер, жизни, проигрыш и наказание.
6. Проверить управление одним пальцем, pointer cancel, mouse и keyboard fallback.
7. Проверить цели минимум 44 CSS px, предпочтительно 64 CSS px, safe areas и отсутствие перекрытий.
8. Реализовать понятный режим без звука и `prefers-reduced-motion`.
9. Добавить logic-тесты механики и completion, DOM-контракт и responsive browser-проверки четырёх
   viewport; проверить canvas pixels и отсутствие console errors.
10. Добавить только готовую игру в lobby и `games-catalogue.json`; концепт не показывать ребёнку.
11. Запустить полный локальный набор тестов, затем production browser suite.
12. Опубликовать `/games`, проверить прямой URL, возврат в lobby и сквозную звезду.
