Как ИИ спас от забвения еще одну безвестную игру. Даже текстовую
Устраивайтесь поудобнее. Сегодня обойдется без громких заявлений — только сухие цифры и мои собственные ошибки.
188 тысяч строк кода и всего 41 активный игрок за неделю. Вот в этой системе координат я и существую третий год. Первый показатель я сгенерировал сам, второй почти не двигается с места. Отсюда и такое название статьи.
В прошлом материале я рассказывал про аварийную кнопку вайпа, нажать которую до сих пор не решаюсь. На повестке дня — фундамент, без которого этот проект просто не существовал бы.
Если вы заглянули сюда впервые, вкратце обрисую суть. Wild World — это текстовая MMORPG прямо в Telegram. Мрачный остров после глобального катаклизма, где выжившие ищут воду и хлам, занимаются крафтом, возводят убежища, сражаются с монстрами и периодически конфликтуют друг с другом. Я пилю её в одиночку по вечерам уже третий год. Никаких венчурных инвестиций и стартапов — исключительно для души. Рекламного бюджета ноль, есть только я и моя неиссякаемая тяга к ковырянию в коде.
Масштаб бедствия впечатляет: 188 887 строк в директории app/, 1367 PHP-скриптов, 464 миграции, 2401 тест и строжайший PHPStan девятого уровня. И всю эту инфраструктуру качают сорок с небольшим человек. Понимаю, со стороны это выглядит комично.
Проект действительно балансировал на краю
Первую версию я выкатил в 2024 году. Затем завертелась рутинная работа, навалилась усталость, и я на полгода забросил репозиторий. Хрестоматийная кончина пет-проекта. Те игроки, что оставались преданными, в конце концов разошлись.
Когда я вернулся, меня встретил собственный код годовой давности. Знакомое давящее чувство: ты вроде бы создатель, но читаешь всё как чужое рукописное творение. Полсотни моделей, россыпь контроллеров, «магические» константы в самых неожиданных местах и полное отсутствие тестов. Чтобы элементарно скорректировать баланс добычи ресурсов, приходилось вспоминать, в каком из трех файлов запрятаны эти формулы.
По-хорошему, такую кодовую базу нужно переписывать с нуля. Но по-честному, у одиночки после работы банально не хватит на это моральных сил. Именно в этот момент пет-проекты отправляются на кладбище: не от скуки, а потому что стоимость правок начинает превышать твой остаток энергии.
Именно тогда я подключил искусственный интеллект. Не в утопическом ключе «напиши мне игру за меня», а как инструмент для переваривания этого колоссального массива.
Сразу отвечу на самый очевидный вопрос. Я использую Claude Code — терминальную CLI-утилиту, тариф Max за 200 долларов в месяц. Никаких плагинов в среду разработки, только консоль радом с репозиторием.
Две сотки зелени в месяц за хобби звучит безумно, согласен. Но называть его «неким ассистентом» было бы глупо — по контексту всё равно легко догадаться. Так что закрываем этот вопрос раз и навсегда. Приведенные ниже методы применимы с любой передовой нейросетью, а вторая половина статьи и вовсе посвящена моментам, где эта технология меня откровенно подводила.
С чем ИИ справился блестяще
Не буду бросаться популистскими фразами про «десятикратное ускорение». Опишу конкретные инженерные задачи, которые в одиночку я бы просто не осилил.
Системные баги вместо единичных казусов. У меня была забавная, но неприятная уязвимость: персонаж выпивал успокоительное, затем стимулятор, и второй препарат полностью аннулировал эффект первого.
Классическая проблема потерянного обновления (lost update). Код выглядел примерно так:
$character = $this->characterModel->find($id);
$health = min(100, $character['health'] + $healAmount);
$this->characterModel->update($id, ['health' => $health]);
Мы считываем текущее состояние, вычисляем новое на стороне PHP и перезаписываем абсолютное значение. Откуда здесь гонка данных у одиночного игрока? Всё просто: пользователь жмет интерфейсные кнопки быстрее, чем сервер успевает ответить, а поверх этого в фоновом режиме срабатывает игровой цикл (тик), который тоже корректирует здоровье и выносливость. Два независимых запроса, каждый со своим слепком данных, и тот, что записал информацию последним, безжалостно затирал предыдущий.
Решение свелось к следующему виду:
$this->stats->adjust($id, ['health' => $healAmount]);
Теперь метод принимает не готовое значение, а дельту (прирост или убыль). Внутри отрабатывает единая транзакция с блокировкой строки, обновляющая исключительно изменившиеся поля.
Исправить аптечку точечно — дело получаса. Но подобная архитектурная беда была размазана по всему проекту, и я даже не подозревал о масштабах бедствия. Тут-то ассистент и проявил себя: он просканировал все участки, где затрагиваются семь базовых характеристик персонажа, и структурировал их. Где-то прямая запись оправдана (например, респавн), где-то нужна дельта (добыча лута), а где-то множитель (гибель).
В итоге переработке подверглись около тридцати мест. Все операции с дельтами и множителями переехали в единый сервис с блокировкой строк. Бонусом выяснилось, что фоновая регенерация здоровья генерировала отдельный запрос UPDATE на каждого персонажа во время тика. Я переписал это на один-единственный запрос для всех страждущих.
Вручную я бы за это не взялся. Не из-за запредельной сложности, а из-за рутинности — на такую тягомотину у меня банально нет свободного вечера.
Визуал, который существовал, но был невидим. В игре 24 модели огнестрельного оружия, и у каждой по-хорошему должна быть уникальная иконка. Рисую я отвратительно, поэтому генерацию картинок я делегировал нейросети по строгому стилевому промпту, чтобы выдержать единую эстетику постапокалипсиса.
История началась вовсе не с погони за красотой. Логи экрана инвентаря начали засоряться ошибками. Один игрок за четыре минуты умудрился сгенерировать 31 сбой. Стали разбираться и вскрыли любопытный факт.
Персональные иконки отображались ровно у 4 стволов из 24. Еще для 10 единиц файл физически лежал на диске и запись в БД присутствовала, но в связующей таблице не хватало строки-ассоциации, поэтому графический резолвер молча подсовывал стандартную заглушку. Молча, на протяжении нескольких лет! В логах об этом не было ни слова, а к багу привел совершенно другой экран.
Оставшиеся 10 пушек вообще не имели артов — их пришлось догенерировать. Сухой остаток: 4 базовых + 10 потерянных по пути файлов + 10 вновь созданных, итого полный комплект 24 из 24. Больше всего меня поразила та самая «средняя десятка»: работа была проделана заблаговременно и пылилась в репозитории, но игроки её в упор не видели.
Централизованные настройки вместо «магических чисел». На текущий момент в административной панели зашито 707 параметров баланса. Саму логику и цифры придумываю я, а вот их классификацией и описанием занимается нейросеть: почему выбрана именно эта величина, к чему приведет её увеличение или уменьшение. Обоснования прописаны для 704 параметров, до оставшихся трех руки пока не дошли.
Самое забавное, что эти подсказки я перечитываю сам — спустя полгода, когда напрочь вылетает из головы логика собственных решений.
Три эпизода слепой веры в авторитет ИИ
Этот блок, на мой взгляд, гораздо ценнее. Восторженные мантры в стиле «ИИ полностью переписал мой код» звучат как рекламный буклет, тогда как суровая практика выглядит иначе.
Сразу оговорюсь, чтобы не перекладывать ответственность на алгоритмы. Только в первом случае имела место откровенная дезинформация со стороны нейросети. Второй и третий инциденты — это дыры в моем собственном пайплайне, которые ИИ просто подсветил своей безапелляционной уверенностью: когда терминал выдает красивый и аргументированный вердикт, вместо здорового скепсиса ты машинально киваешь.
Эпизод первый: «функция не работает, она отключена». Почти весь функционал у меня спрятан за фича-флагами. Новая механика сперва обкатывается на тестовом окружении, а затем активируется через админку.
И вот ИИ берется проанализировать работоспособность одной из фич. Изучает кодовую базу, натыкается на дефолтное значение false и победно рапортует: функция неактивна, её нужно включить. Звучит безупречно логично. Вот только значение по умолчанию отражает лишь состояние на момент написания кода. Истинное же положение дел хранится в боевой базе данных:
SELECT value_bool FROM game_settings
WHERE setting_key = 'onboarding.cold_open_v2.single_screen';
А там красуется единица, установленная еще в июне.
За один рабочий день я поймал три таких ложных вердикта подряд. Трижды искусственный интеллект уверял, что «эта штука мертва», и трижды простой SQL-запрос доказывал обратное.
Проблема решается элементарно. Функционирует опция или нет, нужно проверять непосредственно в продакшн-базе.
Эпизод второй: ложноположительные тревоги в тестах. Я пилил подсказку, которая должна указывать игроку направление на вулканы. Запустил локальные тесты — подсказка пустует. Напрашивающийся вывод: я что-то сломал.
Реальность оказалась прозаичнее. Модульные тесты обращаются к изолированной тестовой БД, в которой попросту отсутствовала таблица с картой игрового мира. Данные не были загружены, поэтому метод честно вернул пустоту. Сама же механика прекрасно функционировала, что подтвердил тестовый сервер с полноценным датасетом.
Мораль банальна, но критически важна. Отсутствие результата — еще не свидетельство поломки. Это либо баг, либо у вас банально нет данных для проверки, и научиться отличать одно от другого нужно вручную. Справедливости ради: будь у меня настроены нормальные фикстуры, коллизии бы не возникло.
Эпизод третий: «зеленый» тест, который ничего не верифицирует. Самый коварный сценарий. ИИ бодро генерирует тестовое покрытие, отчет горит зеленым цветом, разработчик счастлив. Вот только тест не падает даже тогда, когда ты намеренно ломаешь логику, которую он якобы проверяет.
В моем случае тест на новую подсказку верифицировал лишь то, что функция возвращает строку. А она и возвращала строку всегда — иногда пустую, иногда со старым текстом из устаревшей механики. Формальная проверка формально существовала, но на практике она пропустила бы любой мой архитектурный промах.
Теперь у меня действует железное правило: каждый новый тест проходит проверку мутационным анализом. Намеренно коверкаешь код и смотришь, рухнул ли тест и выдал ли он ожидаемое сообщение об ошибке. Именно так я выявил пару собственных «бумажных» тестов.
Бонус: баг, неподвластный ни одному тесту
Этот случай вовсе не связан с погрешностями нейросетей. Он иллюстрирует класс проблем, которые не способна перехватить ни одна автоматизированная проверка.
Девятого июля я полностью перекроил нижнюю навигационную панель бота, переименовав кнопки. При этом тексты старых обучающих подсказок продолжали ссылаться на прежние наименования. Четыре активных совета в базе данных отправляли новичков жать несуществующие элементы интерфейса: три ссылались на кнопку «Перс», один — на экран «Карта». Обе кнопки уже две недели назывались совершенно иначе.
Баг копеечный. Но показательный: формально всё функционирует исправно. Текст рендерится, кнопка на месте, просто их терминологический аппарат разошелся. Ни один классический тест такое не отловит, потому что технически ломаться нечему.
Проблему я решил не точечной заменой слов, а на уровне архитектуры источника. Теперь текстовые упоминания кнопок подтягиваются динамически из того же репозитория, откуда формируется сама клавиатура. Сверху нахлобучен статический тест, сканирующий исходный код и поднимающий тревогу, если кто-то вновь попытается хардкодить названия:
foreach (glob(APPPATH . 'Services/Onboarding/*.php') as $file) { $source = (string) file_get_contents($file); // Вырезаем комментарии: в них я как раз документирую данный инцидент $code = (string) preg_replace('~^\s*(//|\*|/\*).*$~mu', '', $source);foreach (['«Перс»', '«Карта»', '«Крафт»', '«База»'] as $stale) { $this->assertStringNotContainsString($stale, $code, basename($file)); }}
Что касается обучающих советов, хранящихся строками в базе данных, код здесь бессилен — там задействована собственная логика подстановок. В самих текстах я вовсе избавился от точных названий кнопок: формулировка «откройте хаб крафта» не потеряет актуальность никогда.
И да, данный тест я проверил мутацией. Вернул старую терминологию — тест мгновенно покраснел. Значит, работоспособен.
Три рубежа обороны между ИИ и игроками
Если суммировать мой рабочий процесс, он сводится к трем незыблемым принципам.
Первое. Статический анализ и бескомпромиссные тесты на входе. PHPStan на девятом уровне и 2401 тест. Но тестом я признаю лишь тот, что прошел проверку мутацией. Зеленый индикатор успешного теста, не реагирующий на вредительство в коде, хуже полного их отсутствия — он создает фальшивое чувство безопасности.
Второе. Верификация с боевой базой данных. Любое утверждение искусственного интеллекта в духе «эта функция активирована» или «этот метод мертв» перепроверяется прямым запросом к продакшну. Исходный код отражает намерения разработчика, но абсолютную правду знает только СУБД.
Третье. Натурные испытания на «живом» боте. Обязательный клик по кнопке своими руками в реальном окружении Telegram на тестовом сервере. Со временем я научился отлавливать самую коварную пакость: если структура исходящего сообщения повреждена, мессенджер тихо отклоняет его, и до пользователя контент просто не доходит. В системных логах абсолютная чистота, а игрок сидит с пустым экраном. Поэтому перед деплоем текстов я отправляю их реальным запросом и инспектирую входящий ответ.
Никакой высшей математики, сплошная рутина. Но именно эта скучная дисциплина превращает сырой «нейросетевой генереж» в стабильный продукт, который не стыдно показать людям.
Спаситель или нет?
Отвечу предельно откровенно, без красивых художественных финалов.
Игру спас вовсе не искусственный интеллект. Игру спасло то, что у меня вновь появился стимул открывать проект. А возвращаться к нему стало возможно лишь потому, что снизилась косточкинская цена изменений. Раньше задача «отрегулировать баланс добычи» превращалась в вечер археологических раскопок в собственных дебрях. Сегодня это пара часов размеренной работы.
Здесь кроется неприятная правда, которую в подобных победных реляциях обычно замалчивают. Никакой ИИ не сделал мою игру популярной. Он не привлек ни единого нового пользователя. Он вообще не про маркетинг, и показатель в 41 игрока за неделю яркое тому подтверждение. Разумеется, мне хочется большей аудитории, но пока все свободные вечера съедает кодинг.
Его реальная заслуга в другом — он не дал проекту умереть во второй раз. Грань между статусами «заброшено» и «живет» для пет-проекта измеряется не количеством навороченных фич, а тем, тянется ли твоя рука открыть редактор кода в среду вечером.
За последний месяц у меня набралось 120 коммитов. Год назад за аналогичный период их было ровно ноль.
Если вы в одиночку тащите собственный пет-проект и уже всерьез подумывали поставить на нем крест, попробуйте сначала снизить порог входа для изменений. А новые фичи подождут. Мне этот подход помог.
Как выглядит игровой процесс
Раз уж я зазываю в проект, вкратце обрисую здешний геймплей, чтобы вы сами решили, стоит ли оно того.
Игра пошаговая и неторопливая, полностью завязана на текстовый интерфейс в диалоге с ботом. Жмете кнопку «идти на север», перемещаетесь на одну клетку и изучаете окружение. Добыча ресурсов и крафт работают на таймерах: запустили заготовку древесины, закрыли Telegram, заглянули через десять минут за результатами. Дальние вылазки через команду «Поход» также выполняются в фоне, а отчет прилетит в чат. Длительность сессии варьируется от двух минут (проверить статус вылазки) до часа, если вас затянули строительные заботы.
Далее включается классическая MMO-составляющая. Игровой мир един для всех, сетка карты насчитывает миллион клеток, а все ресурсы и объекты на ней общие. Ваша база привязывается к конкретным координатам и физически существует на местности. В южных регионах новички находятся в безопасности, на арену вы попадаете исключительно по собственному желанию, а вот в глубинах континента можно легко нарваться на неприятности.
Оглашу честную статистику смертей, поскольку это главный камень преткновения для новичков (значения актуальны на момент написания, баланс я периодически правлю). Пока вы развиваетесь до десятого уровня, смерть практически бесплатна. При наличии базы вы не теряете ничего, без базы — десятую долю нажитого. После десятого уровня ставки растут: с базой налог составляет скромные 3%, без базы — ровно половину накопленного. При этом персональный склад не грабится никогда. Проект открыто намекает: обустроить дом выгодно, потери будут в разы ниже.
Сорок с небольшим человек на карту в миллион клеток означает, что живого конкурента в первый же вечер вы вряд ли встретите. Зато следы чужой жизнедеятельности видны повсеместно: на карте возвышаются чужие базы, функционирует общий чат и рейтинговая таблица, а специальный экран транслирует пульс острова за прошедшие сутки. Если вы ищете массовые замесы — вам точно мимо. Если же по душе медитативное одиночное выживание, где изредка ощущается чужое присутствие на горизонте, концепция вам зайдет.
Оценить воочию то, во что это вылилось, можно по ссылке: Wild World. Игра абсолютно бесплатна, микротранзакций нет в принципе, продавать вам здесь ничего не будут.
Как ИИ-тренер по CS2 на DeepSeek ошибается и почему недельная подготовка обошлась в $1,45
Как превратить домашний сервер в игровую консоль для любого устройства в доме
Летний гейминг при нестабильном интернете
«Жизнь и страдания господина Бранте» вышла на Nintendo Switch
Запуск DOOM на процессоре собственной разработки
Эволюция футбольных симуляторов: от истоков до наших дней
Возвращение морских сражений в Battlefield 6: что нового в четвертом сезоне?
«Жизнь и страдания принца Джериана» вышла в Steam