Браузерная игра. Я никогда не писал игры. 114 тысяч строк TypeScript. Навайбкожена за три недели. То, что ИИ умеет писать рабочий код, уже не новость. Я расскажу вам о том, что удерживает проект такого размера управляемым, когда код уже не влезает в контекстное окно.

Игра - Vacuum Rogues. Тот же принцип, что и у безоблачных инструментов. Всё в браузере, без сервера.

За 3 недели (вечера и выходные) был разработан игровой движок. Целый мир - планеты, космос, NPC, экономика, квесты, бои, даже игрушечная банковская система для реализации простейшего кредитования.

Осталось множество мелких правок UI, квестов и экономического/военного баланса. Всё, что требует кропотливого описания, как это должно выглядеть, - требует ещё недели три доработок. А мне хотелось бы делегировать эту работу и переключиться к написанию саундтреков для игры. Поэтому решено зарелизить Beta-версию c формой обратной связи. Issue можно оставить из интерфейса игры.

Сначала несколько обзорных слов о проекте, а потом перейдём к методике.

Масштаб

  • 100 часов вербализации намерения в течение 3-х недель

  • 611 коммитов

  • 114 544 строки кода

  • 63 юзер стори

  • 538 TS-файлов

  • 206 e2e-тестов в 36 playwright spec-файлах

  • 2628 юнит-тестов в 302 файлах

  • всего 3263 файла

  • 59 МБ - prod bundle (вместе с 26Мб картинок)

При таком объёме главная проблема - не сломать то, чего сейчас нет перед глазами.

Поиски подхода

От промпт-driven подхода я отказался сразу. One-shot не работает нигде.

В компании, где я работаю, есть доступ к open-source нейросетям, развернутым в контуре. Я успел наиграться с ними и уже обкатал подход, который показывал лучший и предсказуемый результат там - SPDD (Structured Prompt Driven Development). Этот подход предполагает создание сложных всеобъемлющих промптов, в которых очень высокий уровень детализации - какой файл и как править, вплоть до типов и переменных. Промпт как часть проекта. На что платный ИИ мне сразу сказал - не надо так делать, давай лучше BDD.

С BDD дело пошло лучше и быстрее. Но мне стало не хватать автоматизации. Я шел одним и тем же паттерном по кругу - архитектура, BDD, план, тесты, реализация, ревью, фиксы, коммит.

Я решил поднять уровень абстракции и установил ralphex, чем автоматизировал весь цикл работы.

План как единица работы

Физически это markdown-файл в docs/plans/ с разбивкой на задачи, у каждой задачи обязательные тесты, и следующая задача не начинается, пока тесты предыдущей не зелёные. План переживает и переполнение контекста, и обрыв сессии - после рестарта видно, что сделано, и работа продолжается с того же места. За три недели 62 плана. По сути, это 62 User Story.

Выполняет план не интерактивная сессия, а оркестратор. Он гоняет задачи по циклу и, главное, переживает ошибки API.

План и вся память о проекте - строго на английском для экономии токенов.

На случай сомнений модели, в проекте лежит файл SOURCE-OF-TRUTH.md, содержащий high-level game design документ для избегания лишних вопросов.

Работа заспорилась, но через пару дней я обнаружил, что появились файлы больше 5000 строк, а сделав один план, мы ломаем то, что работало до этого.

Настало время принимать меры.

Летсплей нейронкой

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

Знать радиус поражения до правки

Правка ядра - главный источник каскадных поломок. Особенно когда ядро не влезает в контекстное окно.

Радиус поражения считается с помощью gitNexus, а для экономии контекста я попросил саму нейросеть написать “vacuum MCP” - чтобы можно было брать тело функции из файла в 5000 строк без чтения всего файла. В большинстве случаев (95%) используется поиск и чтение через grep/Read. Но в 5% случаев без графа зависимостей и vacuum MCP никак.

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

SOLID и GRASP ИИ понимает на уровне типичного Senior. То есть может рассказать, что значит аббрревиатура и даже интуитивно применить. Но как именно это применить - индивидуально для каждого проекта. Применение шаблонов сильно зависит от степени мутности представления о будущем развитии функциональности. Поэтому архитектура - решает.

Мне повезло - моё смутное видение не множилось на количество участников, как бывает при обычном согласовании и всё порезалось на модули довольно легко, так как я имел ясное представление о том, чего я хочу получить в итоге.

Однонаправленная архитектура

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

Было - связи в обе стороны        Стало - однонаправленный поток

  presentation ⇄ systems                presentation → systems → domain → ECS
  systems ⇄ domain                         правка нижнего слоя не тянет верхние
  presentation ⇄ domain

В AGENTS.md это одна строка - strict dependency direction, top to bottom only. Записана там, где модель прочитает её прежде, чем начнёт писать. Domain - чистые сущности без PixiJS и DOM, поэтому игровая логика тестируется без экрана.

Получилось буквально - ядро - центральный модуль игры, который описыват, ЧТО система умеет, но не КАК, - и слои вокруг него. Например, порт ISaveService умеет сохранять и загружать, без подробностей. Адаптер - конкретная реализация SaveManagerAdapter, которая дергает “внешний мир”.

То есть схема такая:

ядро ──(знает только порт-контракт)──▶ ПОРТ ◀──(реализует)── АДАПТЕР ──(дёргает)──▶ IndexedDB / ONNX / PixiJS

А в сборке архитектура движка выглядит так:

            GameEngine
   цикл кадров · смена сцен · суточная симуляция мира
                  │
                  ▼   всё через порты ядра (core)

   presentation  →  systems  →  domain + ECS
   рендер PixiJS     механика     чистое состояние без экрана

   сбоку - data (сейвы, миграции) · ai (ONNX + fallback)

   что внутри systems →
     бой · NPC · галактика · торговля · квесты · банк ·
     снаряжение · физика · диалоги · звук · справочник

Рефлексия модели

После нескольких фиксов в стиле “ой, да, ты прав, недосмотрел” я завёл скилл /learn, чтобы выученные уроки сохранялись для дальнейшего избегания проблем. При этом возникла другая проблема - AGENTS.md тоже не резиновый.

А раз модули к этому времени стали максимально изолированными, решение - иерархия контекстов.

Память и слоистый контекст

Чтобы сессия не начиналась с нуля, работают два механизма. Кросс-сессионная память держит решения, которые переживают конец сессии, вроде “этот модуль не трогаем” или “здесь была ловушка”. Слоистый AGENTS.md разложен по подпапкам, поэтому в контекст подтягивается ровно то, что относится к текущему модулю, а не вся документация разом.

Я люблю писать промпты в стиле “планеты должны вращаться в разных направлениях, вид корпуса должен меняться при покупке нового и запили АБС”. ralphex очень выручает в таких случаях, раскидывая это по таскам в разных планах в зависимости от места изменений, чтобы контекст не перегружался.

Зелёные тесты - это ещё не рабочая игра

Текстовый тест проверяет логику, но не видит картинку. Поэтому поверх него два контура. Regression-first - на найденный баг сначала пишется красный тест, и только потом фикс, так регрессия не возвращается. В истории это видно дословно, коммит reproduce wait-turn pause runaway as red regression test, и следом починка. Второй контур - верификация скриншотом, то, что должно отрисоваться, проверяется по реальному кадру, а не по зелёному ассерту.

В момент, когда я понял, что большая часть работает как надо, я запретил ИИ менять тесты - только добавлять. Это важно, потому что иначе он бессовестно переписывает тесты под реализацию. Стало жить гораздо проще.

Гейты, чтобы регрессия не уехала в master

Последний рубеж - автоматические хуки (я использую lefthook). Они отбраковывают плохой коммит до того, как он попадёт в историю, и работают одинаково, кто бы ни писал код - человек или модель.

правка
  │
  ▼
pre-commit ── eslint --fix · prettier · tsc --noEmit · тесты по файлам ──┐
  │ прошло                                              упало ──┘→ к правке
  ▼
pre-push ──── полные тесты · полный линт · сборка ───────────────────────┐
  │ прошло                                              упало ──┘→ к правке
  ▼
master → авто-деплой

До push доходит только то, что прошло все три. Из 594 коммитов 138 - это правки по итогам мульти-агентного ревью (ralphex запускает для этого аж 5 субагентов). Откатывать не приходилось.

Итог

Игра получилась благодаря строгим рамкам, авто-планам, иерархии модулей и strictly-english контексту.

память → план → оркестратор → impact → правка → регресс → гейты → master → деплой

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

Самое главное - я уже неделю не переживаю что мелкая правка что-то кардинально сломает. Такое перестало происходить. Это самый важный результат.

А игру ещё дорабатывать и дорабатывать по всяким мелочам, прошу, побудьте бета-тестерами, обещаю исправить все нюансики в кратчайшие сроки.

→ Vacuum Rogues

Отзывы бета-тестеров падают из игры прямо в GitHub Issues (нужен аккаунт на GitHub). Исходники открою самым активным.

PS. В формате after-title show хотелось бы поделиться несколькими интересностями.

  • WebP ужал 600Мб картинок до 29Мб без особой потери качества

  • Картинки тоже генерировала нейронка - сама собрала пайплан на ComfyUI на боксе с RTX4090 и сама написала промпты

  • В проекте есть валидация экономики. Без игрока - это игра с нулевой суммой, только действия игрока могут перевесить чашу весов.

  • Вращение планет и солнц сделано с помощью экви-ректангулярных плоскостей, сгенерированных Stable Diffusion

  • Музыку для игры тоже отчасти будет писать нейросеть - через плагин AbletonOSC она может подкидывать midi-файлы и крутить крутилки у инструментов. То есть интенция, живые инструменты, мастеринг и контроль остаются за мной.