Создание игр с ИИ-агентом: сид, команды и скриншот-диффы для Claude Code
Чтобы ИИ-агент собирал игру, а не ломал её, игру делают обвязкой: весь рандом через один генератор с сидом, действия - текстовыми командами, правила отдельно от движка, скриншоты сравнивает odiff и отдаёт агенту число. Тогда Claude Code сам повторяет баг и проверяет правку.
Создание игр с ИИ-агентом: как сид-рандом, паттерн команд, симуляция без движка и odiff дают Claude Code проверять свои правки. Цифры из ДнД-бота.

В статье · 8 разделовразвернутьсвернуть
Создание игр с ИИ-агентом ломается не на коде, а на том, что агент не видит игру. Он не знает, что выпало на кубике, не может посмотреть на экран и не умеет повторить баг. Лечится это не промптом, а архитектурой: один сид на весь рандом, изменения состояния командами, симуляция отдельно от движка, скриншоты сравнивает утилита, а агенту отдаётся только число. Тогда Claude Code сам проверяет свою правку и перестаёт ломать то, что уже работало.
Идея не моя. 10 октября 2026 года геймпрограммист Кайл Кукштел (15 лет в профессии) выложил текст "Your Game Is A Harness", и его главная мысль мне понравилась: игры плохо дружат с LLM, потому что половина игры лежит вне кода, в ассетах и сценах. Это проблема контекста, а не врождённое свойство игр. Я гоняю агентов на ДнД-боте, где ИИ-мастер ведёт партию и каждый ход катит кубики, и часть этих приёмов пришлось сделать руками ещё до того, как я прочитал Кукштела. Ниже то, что из этого реально работает, с цифрами из моего репозитория.
Что такое игра-обвязка для ИИ-агента
Игра-обвязка (harness) - это игра, устроенная так, чтобы агент мог её запустить, повторить и проверить без человека: случайность управляется сидом, любое действие записывается текстовой командой, логика живёт отдельно от рендера, а визуал и производительность сводятся к числам. Агент правит код, сам прогоняет цикл и видит результат за секунды, а не просит вас "глянуть, всё ли ок".
Кукштел формулирует цель одной фразой: "The tighter you can make the agentic loops, the better!" Чем короче петля "правка - проверка - вывод", тем меньше агент фантазирует. Разработка игр с ИИ тут ничем не отличается от любой другой агентной работы, просто в играх петлю собрать сложнее. Вот что входит в обвязку и что каждая часть даёт агенту:
| Приём | Что даёт агенту | Инструмент | Есть у меня в ДнД-боте |
|---|---|---|---|
| Единый сид-рандом | баг повторяется один в один | свой RNG-класс, сид в лог | да, TurnRng / ReplayRng |
| Паттерн команд | ход можно переиграть без человека | JSON-команды, лог команд | да, слепки ходов |
| Симуляция отдельно от движка | тесты без запуска движка | чистые функции, отдельная библиотека | частично, 113 функций вынесено |
| Данные вне кода | баланс без перекомпиляции | JSON, таблицы, редактор данных | частично |
| Скриншот-дифф | визуальная правка проверяется числом | odiff, --write-movie в Godot | нет |
| Текстовый профайлер | агент читает, где тормозит | samply | нет |
Последние две строчки у меня пустые честно: ДнД-бот текстовый, рендерить там нечего. Про них расскажу по источникам, а не по опыту.
Зачем в игре с ИИ-агентом детерминированный рандом
Детерминированный рандом нужен, чтобы агент мог повторить конкретный баг. Если весь случайный выбор в игре идёт через один генератор с сидом, то сид плюс те же действия дают тот же результат. Без этого агент чинит баг, запускает игру, баг не воспроизводится, и агент радостно рапортует "исправлено". Хотя ему просто повезло с кубиком.
У меня это было больной темой. В ДнД-боте кубики катятся везде: проверки навыков, бой, лут, награды за квесты. Баг "игрок ударил, а урон 0" нельзя чинить, если следующий ход выкинет другое число. Поэтому в сентябре 2026 я завёл один источник случайности на ход: три класса TurnRng, RecordingRng и ReplayRng. Первый катит на проде, второй катит и записывает, третий подставляет записанные значения при реплее. Файл rng.py вышел на 171 строку, а инъекция генератора в бросок кубиков, проверки и бой затронула 6 файлов (154 строки добавлено).
Минимальная версия для любой игры выглядит так:
import random
class GameRng:
def __init__(self, seed: int):
self.seed = seed
self._r = random.Random(seed)
self.log: list[int] = []
def roll(self, sides: int) -> int:
value = self._r.randint(1, sides)
self.log.append(value)
return value
# сид пишется в лог в начале каждой сессии
rng = GameRng(seed=1801)Главная грабля не в классе, а в том, что старый код продолжает звать random напрямую. У меня ревью первого PR нашло ещё три модуля мимо общего генератора: баланс встреч, награды за квесты и базовый лут. Их пришлось отдельным коммитом переводить на current_rng(). Если бы не нашли, реплей молча расходился бы на каждом бою с лутом, и я бы неделю думал, что сломан реплей, а не код. Отсюда правило: после внедрения сида грепни проект на random. и Math.random, и пусть агент сделает это первым же шагом.
И ещё одна мелочь, которую я бы заложил в графической игре с первого дня: косметический рандом (частицы, тряска камеры) лучше катать отдельным генератором. Иначе новая искра от удара сдвинет все следующие боевые броски, и старые записи перестанут сходиться. У меня в текстовом боте косметики нет, так что это не опыт, а прямое следствие схемы: реплей подставляет записанные броски по очереди, и лишний бросок в середине сдвигает всю очередь.
Как паттерн команд помогает агенту тестировать игру
Паттерн команд - это когда каждое изменение состояния игры оформлено объектом-командой ("атаковать", "купить", "сдвинуться на клетку"), который можно сохранить текстом и выполнить заново. Агенту это даёт главное: он может сам собрать сценарий из команд, проиграть его и сравнить итоговое состояние с ожидаемым, без кнопок, геймпада и вас.
Это то же тестирование игр по сценарию, что делает живой QA, только сценарий лежит в файле. У Кукштела команды пишутся в текстовом редакторе или самим агентом и прогоняются без перекомпиляции. Выглядит примерно так:
[
{ "cmd": "spawn", "thing": "goblin", "x": 4, "y": 2 },
{ "cmd": "attack", "from": "player", "to": "goblin", "seed": 1801 },
{ "cmd": "assert", "path": "goblin.hp", "op": "<", "value": 7 }
]В ДнД-боте командой работает сам ход. Сообщение игрока, ответы всех LLM-вызовов и все броски кубиков пишутся в слепок, а реплей подставляет ответы моделей через ReplayLLM и броски через ReplayRng. Сначала на проде записалось 66 слепков, и прогон дал 69 passed (66 слепков плюс три проверки формата). После второго среза рефакторинга я пересобрал набор: 21 цепочка ходов, 76 слепков со строгими отпечатками запросов к LLM. Нарочно внесённую поломку новый набор поймал: прогон показал 74 из 76, два слепка покраснели и подняли тревогу. Как собирались эти слепки, я подробно разбирал в статье про трос из 66 слепков перед распилом pipeline.py.
Читайте такжеНейросети для DnD: как 66 слепков живых ходов поймали баг раньше, чем я тронул 19 000 строк pipeline.pyGolden master и characterization-тесты для ИИ-мастера DnD: как 66 слепков живых ходов с прода стали тросом безопасности перед распилом монолита pipeline.py на 19 000 строк.Важная оговорка про тестовые данные. Однажды агент-QA прибежал с багом "урон игрока не снимает HP врага, бой бесконечен". Копали, а дефекта в пути урона нет. Тестовый персонаж был засижен с пустым equipped, меч лежал в рюкзаке, игра честно била кулаком: 1d1 при Силе 9, модификатор -1, итого 0 урона каждый ход. На проде такой же пустой слот нашёлся у 6 из 599 активных персонажей. Вывод простой: команды и фикстуры тоже код, и агент верит им слепо. Проверяйте сид-персонажа так же, как проверяете игру.
Как агенту проверять визуальные изменения без скриншотов в контексте
Агенту не нужно смотреть на скриншот глазами. Игра сама снимает кадр до правки и после, утилита сравнения картинок считает разницу, а в контекст агента уходит только число и путь к картинке-диффу. Такой подход экономит токены и убирает главную беду: модель, которая "видит" на картинке то, что хочет увидеть.
Кукштел предлагает три вещи. Первая - встроенный API скриншотов движка (он есть почти везде). Вторая - возможность выключать слои: UI отдельно, геймплей отдельно, чтобы дифф не тонул в мигающем счётчике FPS. Третья - бесплатная утилита odiff, которая сравнивает две картинки попиксельно и сохраняет третью с подсвеченной разницей:
odiff before.png after.png diff.pngПо словам авторов, odiff примерно в 6 раз быстрее ImageMagick за счёт SIMD и читает png, jpeg, webp и tiff. Порог чувствительности и сглаживание настраиваются флагами, а точный список стоит смотреть в odiff --help вашей версии: README прямо отсылает туда.
Чтобы кадры были сравнимыми, их надо снимать детерминированно. В Godot 4 для этого есть режим записи: --write-movie пишет кадры в png, --fixed-fps фиксирует шаг времени, --quit-after останавливает игру через нужное число кадров (документация Godot 4.4). Вместе с сидом это даёт одинаковый кадр 120 при одинаковых командах. А вот флаг --headless тут не помощник: он включает заглушку дисплея, и рисовать будет нечем.
Своего опыта со скриншот-диффом у меня нет, скажу прямо. Визуал мини-приложения ДнД-бота у меня проверяют два QA-агента с реальными аккаунтами, то есть глазами модели. Это дороже и шумнее, чем дифф в процентах, и для игры с камерой и анимациями я бы начинал сразу с odiff.
Как отделить симуляцию от движка
Отделить симуляцию - значит вынести правила игры (урон, ходы, экономику) в код, который не знает про движок, рендер и ввод. Движок только рисует то, что ему прислала симуляция, а симуляция принимает команды. Тогда агент тестирует логику обычными юнит-тестами за секунды и не ждёт компиляции сцены.
Кукштел называет это blackbox-архитектурой и ссылается на Monster Train и Solium Infernum: ядро живёт отдельной библиотекой, движок "тупой" и просто исполняет то, что пришло из симуляции. Его формулировка: агентский ход проходит весь такой цикл "за секунды".
У меня та же история без движка, но с монолитом. Пайплайн хода ИИ-мастера был файлом на 19 тысяч строк, и первым срезом рефакторинга я вынес из него 113 чистых функций в отдельный пакет, а 18 нечистых сложил в helpers. Приёмка среза: реплей 72 из 72, смок 10 из 10. Чистую функцию агент проверяет без базы, без LLM и без бота, и это самый дешёвый тест из всех, что у меня есть. Про то же разделение "что считает модель, а что код" есть отдельный разбор баланса игры с нейросетью.
Игровые движки тут не враги: Unity, Unreal или Godot остаются рисовать и читать ввод, просто правила не живут внутри сцен и префабов. Тот же принцип касается данных. Статы, скорости и урон лучше держать вне кода, в JSON или таблице, и читать при старте. У Кукштела так работала команда Cantata, где с данными возились примерно 7 человек, не пишущих код. Агенту это даёт возможность крутить баланс правкой файла, без пересборки.
Как подготовить архитектуру игры для ИИ-агента: пошагово
Начинать стоит с того, что дешевле всего и даёт больше всего: сид и лог команд. Скриншоты и профайлер добавляются потом, когда появится визуал, который реально ломается. Порядок, в котором я бы делал это в новой игре:
- Заведите один класс случайности с сидом и пишите сид в лог в начале каждой сессии.
- Попросите агента грепнуть проект на прямые вызовы
randomи перевести их на общий генератор, потом проверьте сами. - Оформите действия игрока командами и сделайте режим "проиграть файл команд и вывести состояние в JSON".
- Вынесите правила в модуль без зависимостей от движка и покройте его тестами, которые агент запускает одной командой.
- Унесите числа баланса в файл данных, который читается при старте.
- Добавьте снимок кадра с фиксированным шагом времени и сравнение через odiff, порог разницы пропишите в CLAUDE.md.
- Если упираетесь в производительность, дайте агенту текстовый вывод профайлера (у Кукштела это samply), а не скриншот графиков.
Сид, рекордер и реплей у меня уместились в один, нулевой, срез рефакторинга, и вкручивал я их в живой монолит. В новой игре, которая с первого дня строится так, это заметно проще.
Когда игре обвязка не нужна
Обвязка окупается, когда вы правите игру агентом неделями и в ней есть случайность или состояние, которое копится. Для прототипа на выходные, игры одним промптом или геймджема всё это лишнее: проще перегенерировать, чем строить реплей. Тем более если игру вы через неделю выкинете. И если вы делаете первые инди игры и учите геймдев с нуля, сначала доведите до конца маленькую игру, а архитектуру под агента ставьте на вторую.
Моя граница такая. Если проект живёт дольше месяца и баг хотя бы раз "сам пропал" после перезапуска, сид нужен сегодня, а не потом. Если визуальных правок меньше одной в неделю, скриншот-дифф подождёт, глаз автора дешевле. А если в игре вообще нет рандома и состояния (пазл с фиксированными уровнями), хватит обычных тестов на логику. Что агент вытягивает за выходные без всякой обвязки, я разбирал в статье про создание игр с Claude Code за выходные.
Читайте такжеСоздание игр с Claude Code: что ИИ-агент успевает за выходныеСоздание игр с Claude Code: сколько прототипа ИИ-агент собирает за выходные, какой движок ему дать, где он застревает и можно ли доверить ему игровую логику.Короче, агент не ломает игру из вредности. Он ломает её, потому что не может проверить свою правку. Дайте ему сид, команды и число вместо картинки, и он начнёт проверять сам. А я пойду дописывать реплей для следующего среза)
Частые вопросы
Может ли Claude Code сам тестировать игру?
Может, если игра даёт ему текстовые входы и выходы: команду запуска сценария, лог состояния в JSON, тесты логики без движка и число от сравнения скриншотов. Без этого агент видит только код и рапортует об исправлении наугад.
Какой сид выбрать для рандома в игре?
Любое целое число: важно не значение, а то, что весь геймплейный рандом идёт через один генератор с этим сидом и сид пишется в лог в начале сессии. Тогда по логу баг повторяется один в один.
Чем odiff лучше, чем показать скриншот нейросети?
odiff считает попиксельную разницу и сохраняет картинку-дифф, агенту достаётся число вместо изображения. Это дешевле по токенам и не зависит от того, что модель 'увидела' на картинке.
Как готовился материал: черновик собран моим AI-конвейером по темам и заметкам из практики, факты сверены с первоисточниками, финальный текст я прочитал, поправил и утвердил перед публикацией. Обложку к статье тоже рисует нейросеть. Про сам конвейер — в разделе обо мне.
