Когда контекстное окно кончается, а проект — нет
Браузерная игра. Я никогда не писал игры. 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 → деплой
Как видно, сработали ровно те же подходы, что и в человеческих командных процессах разработки. Точно так же сама правка кода занимает мизерную часть по сравнению с подготовкой планов и приёмкой результата.
Самое главное — я уже неделю не переживаю что мелкая правка что-то кардинально сломает. Такое перестало происходить. Это самый важный результат.
А игру ещё дорабатывать и дорабатывать по всяким мелочам, прошу, побудьте бета-тестерами, обещаю исправить все нюансики в кратчайшие сроки.
Отзывы бета-тестеров падают из игры прямо в GitHub Issues (нужен аккаунт на GitHub). Исходники открою самым активным.
PS. В формате after-title show хотелось бы поделиться несколькими интересностями.
-
WebP ужал 600Мб картинок до 29Мб без особой потери качества
-
Картинки тоже генерировала нейронка — сама собрала пайплан на ComfyUI на боксе с RTX4090 и сама написала промпты
-
В проекте есть валидация экономики. Без игрока — это игра с нулевой суммой, только действия игрока могут перевесить чашу весов.
-
Вращение планет и солнц сделано с помощью экви-ректангулярных плоскостей, сгенерированных Stable Diffusion
-
Музыку для игры тоже отчасти будет писать нейросеть — через плагин AbletonOSC она может подкидывать midi-файлы и крутить крутилки у инструментов. То есть интенция, живые инструменты, мастеринг и контроль остаются за мной.
Автор: badattech

