Вайбкодинг игры: что соло-разработчику взять из RTS Саймона Уиллисона
Да, в одиночку с ИИ-агентом RTS сделать можно: Саймон Уиллисон собрал браузерную Battle Drones за 321 коммит Claude Code (июль-октябрь 2026), почти не читая код. Держится всё на детерминированной симуляции: она даёт мультиплеер, реплеи и 1200 автоматических матчей, на которых агент и делал игру "веселее".
Вайбкодинг игры на примере RTS Battle Drones: 321 коммит Claude Code, детерминированная симуляция, Math.sin в Safari и промпт "сделай веселее" на 1200 матчах.

В статье · 9 разделовразвернутьсвернуть
Вайбкодинг игры - это когда игру пишет ИИ-агент, а ты ставишь задачи, играешь и почти не читаешь код. Саймон Уиллисон так собрал браузерную RTS Battle Drones: 321 коммит, в основном Claude Code на Opus 5.5, с июля по октябрь 2026. Соло-геймдеву из этого кейса стоит забрать две вещи: детерминированную симуляцию с первого дня и привычку мерить "веселье" массовыми прогонами, а не на глаз.
Пост Уиллисона вышел 20 октября 2026 года (simonwillison.net), и я залип в него на вечер. Не из-за 3D-дронов, а из-за двух мелочей, которые легко проскочить. Первая: Claude пришлось переписать Math.sin и Math.cos, потому что Chrome и Safari считают их по-разному в последнем бите. Вторая: промпт "сделай игру веселее" сработал, но совсем не потому, что модель понимает, что такое весело. Обе мелочи я узнал на своей шкуре, пока гонял агентов на ДнД-боте с ИИ-мастером. Ниже разбор с кодом из репозитория Battle Drones и цифрами из моего.
Можно ли в одиночку с ИИ-агентом сделать RTS?
Да, можно, и Battle Drones это показывает: одна человеко-голова, агент Claude Code и примерно три месяца. На выходе браузерная стратегия в духе Command & Conquer с 3D-юнитами, туманом войны, шестью картами, генератором карт и мультиплеером. Но игра выросла за 12 больших итераций, а не одним промптом, и почти весь код автор не читал.
Уиллисон начал проект в июле 2026 года как эксперимент с вайбкодингом. Сам термин придумал Андрей Карпаты в феврале 2025-го: "полностью отдаться вайбу и забыть, что код вообще существует" (questera.ai). Уиллисон честен ровно в этом смысле: "Я не просмотрел почти ни строчки кода, и он написан на языке (TypeScript), которым я сам почти не пользовался". И ещё признаётся, что до сих пор не отпустило чувство вины за такой способ делать софт. Вот что вышло на момент поста:
| Что | Сколько / чем | Комментарий |
|---|---|---|
| Коммиты | 321 на 20.10.2026 | в README репозитория сейчас уже 346, игру допиливают дальше |
| Модель | Claude Opus 5.5 в Claude Code | для дуэлей агентов подключены ещё Claude Haiku 5.5 и GPT-6 Luna |
| Стек | TypeScript, Phaser в v1-v4, three.js с v5, Vite | симуляция в src/sim/ без единого импорта движка |
| Контент | 9 зданий, 7 наземных машин, 6 воздушных, 3 вида пехоты | все модели 3D, спрайты и ассеты генерил OpenAI API по запросу Claude |
| Карты | 6 карт, 5 биомов, генератор случайных карт | умеренный, пустыня, арктика, вулканы, тропики |
| Режимы | Skirmish, Last Stand, ИИ против ИИ, агент против агента | плюс кампания Ops на 8 миссий после "фан-апдейта" |
| Мультиплеер | по звуку между двумя устройствами рядом + онлайн | онлайн-релей на одном инстансе Fly.io shared-cpu-1x, по словам автора "не дороже $2.19 в месяц" |
| Тесты | Vitest на симуляцию, смоук на Playwright | баланс проверяется круговым турниром ИИ против ИИ |
Источники: пост Уиллисона и README репозитория, лицензия Apache-2.0, проверено 28.10.2026.
Главное в таблице не дроны, а последняя строчка. Мультиплеер, реплеи, бои агентов и автотесты баланса держатся на одном архитектурном решении. О нём дальше.
Что такое детерминированная симуляция в игре?
Детерминированная симуляция - это игровая логика, которая из одного и того же начального состояния и одних и тех же команд игрока всегда даёт один и тот же результат, бит в бит, на любом компьютере. Тогда по сети и в файл реплея можно передавать только команды ("юнит 14 - атаковать"), а не состояние всего мира.
Идее 25 лет. В 2001 году Пол Беттнер и Марк Террано описали сетевой код Age of Empires в статье "1500 Archers on a 28.8" (Game Developer). Целевое железо - Pentium 90 с 16 МБ памяти и модем 28,8 кбит/с. Если слать состояние юнитов, влезало бы около 250 юнитов. Поэтому каждая машина гоняла "в точности ту же симуляцию", обмениваясь только командами, ходами примерно по 200 мс. Цена решения там же: мельчайшие расхождения "множатся со временем", и один сдвинутый олень при генерации карты каскадом превращается в другую партию.
У Battle Drones та же схема, только в браузере. Симуляция тикает 10 раз в секунду, ввод идёт очередью команд, а в онлайн-дуэли через интернет летают только команды. Релей на Fly.io лишь сводит две вкладки по коду комнаты и ничего не хранит. Именно поэтому сервер такой дешёвый: он не считает игру.
Читайте такжеСоздание игр с ИИ-агентом: сид, команды и скриншот-диффы для Claude CodeСоздание игр с ИИ-агентом: как сид-рандом, паттерн команд, симуляция без движка и odiff дают Claude Code проверять свои правки. Цифры из ДнД-бота.Зачем браузерной игре детерминированная симуляция?
Браузерной игре с ИИ-агентом детерминизм даёт сразу четыре вещи: дешёвый онлайн, где по сети идут только команды, реплеи, которые весят килобайты, бои агентов, которые можно переиграть и проверить, и автотесты баланса на тысячах партий. Без детерминизма каждая из этих фич стоит отдельной архитектуры.
Для вайбкодинга важнее всего последний пункт. Агент не умеет "поиграть и почувствовать", но умеет запустить 1200 матчей ИИ против ИИ без экрана и посчитать цифры. Это работает, только если матч с тем же сидом и теми же командами воспроизводится один в один. Иначе любой баг "у меня один раз юнит застрял" агент не повторит, а починку не проверит.
Сид в Battle Drones устроен жёстче, чем я привык. В src/sim/rand.ts вообще нет генератора с состоянием. Любое случайное решение - это хеш от сида матча плюс того, что это решение идентифицирует: тик, id юнита, строковая "соль". Комментарий в начале файла: симуляция "никогда не должна звать Math.random()", чтобы матч "повторялся идентично из того же сида и команд". Плюс такого подхода: новый случайный бросок в одном месте не сдвигает очередь бросков во всех остальных. Внутри обычный murmur-финализатор:
h = Math.imul(h ^ (x | 0), 0x85ebca6b); h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35); h ^= h >>> 16;А в src/sim/checksum.ts лежит хеш мира. Записи боёв агентов фиксируют его каждые 100 тиков, а звуковые дуэли сворачивают в цепочку на каждом тике, "чтобы поймать два телефона, разъехавшиеся в стороны". Это и есть страховка: рассинхрон видно сразу, а не через полчаса странной партии.
Почему Math.sin ломает реплей в Safari?
Потому что стандарт JavaScript не фиксирует результат Math.sin, Math.cos, atan2, hypot и pow. ECMA-262 называет их "implementation-approximated": каждый движок считает по-своему, и V8 в Chrome с JavaScriptCore в Safari расходятся в последнем бите. В обычном коде это незаметно, в детерминированной симуляции один бит за минуту превращается в другую битву.
Уиллисон пишет про это одной фразой: реплеи рассинхронизировались, потому что Chrome и Safari возвращали "очень немного разные числа". А вот Claude, когда чинил, замерил расхождение и оставил цифры в шапке src/sim/fmath.ts. На 20 000 входных значений V8 (Node, Chrome, Edge) и JavaScriptCore (Safari, Bun) разошлись в последнем бите:
| Функция | Доля расхождений V8 и JavaScriptCore |
|---|---|
hypot | 35% |
atan2 | 17% |
pow | 9% |
sin | 3% |
cos | 3% |
Источник: комментарий в fmath.ts, замер агента, проверено 28.10.2026.
Итог в том же комментарии: реплей, записанный в Node, "уплывал за секунды" в Safari. Лечение - свои версии функций по мотивам fdlibm (той же библиотеки, из которой растут версии V8). Они используют только сложение, вычитание, умножение, деление, сравнения и Math.sqrt, floor, abs. Эти операции IEEE 754 и ECMAScript определяют точно, поэтому у всех движков одни и те же биты. Синус выглядит так:
export function sin(x: number): number {
if (!Number.isFinite(x)) return NaN;
const a = Math.abs(x);
if (a < 1e-9) return x;
const [q, y] = reduce(a);
const s = q === 0 ? kSin(y) : q === 1 ? kCos(y) :
q === 2 ? -kSin(y) : -kCos(y);
return x < 0 ? -s : s;
}И это не единичный случай. В августе 2026 у физического движка Bounce завели issue #8: сцена с вращающимися телами за 900 шагов давала разные хеши в Node 20 и Chrome 149, хотя внутри у обоих V8, просто разных версий. Фикс тот же, fdlibm-ядра, цена по замеру автора issue - в 1,1 раза медленнее.
Самая полезная строчка в fmath.ts для меня даже не код, а правило: все модули в src/sim обязаны использовать эти версии, и tests/fmath.test.ts запрещает там оригинальные Math. То есть защита не на совести агента, а в тесте. Агент забудет, тест не забудет.
Как я сделал детерминизм в ДнД-боте и где споткнулся
У меня не RTS, а текстовая RPG в Telegram: ИИ-мастер ведёт партию, на каждом ходу катятся кубики. Но задача та же. Чтобы агент мог повторить баг и проверить свою правку, ход должен переигрываться один в один. Сделал в сентябре 2026, и самая дорогая ошибка была ровно та, от которой Уиллисона спас тест.
Схема: три класса, TurnRng катит на проде, RecordingRng катит и записывает, ReplayRng подставляет записанное при реплее. Каждый ход сохраняется "слепком": сообщение игрока, ответы LLM, все броски. Вот как это выглядит в git-логе бота, дословно:
33737614 feat(bot): TurnRng/RecordingRng/ReplayRng - источник случайности хода (dnd-2g0e.1, D11)
bot/src/services/dice/rng.py | 171 ++++++++++++++++++++++++++
286331d0 feat(bot): инъекция rng в roller/DiceService/checks/combat/combat_manager (dnd-2g0e.1, D11)
40275bd3 fix(bot): current_rng() в encounter_balance/quest_rewards/basic_loot; слепок result_kind=error при исключении хода (ревью PR A, dnd-2g0e.1)Третья строчка - мой "Math.sin". Генератор я внедрил, а баланс встреч, награды за квесты и базовый лут продолжали катить кубики мимо него. Поймало это ревью, а не тест. Если бы не поймало, реплей расходился бы на каждом бою с лутом, и я бы неделю чинил реплей, а не код. Теста-запрета, как fmath.test.ts, у меня тогда не было, был греп по random. руками. Да, повторяю ошибку, которую сейчас советую не повторять)
Что это дало в цифрах. Срез принят 8 сентября 2026: рекордер на проде, 66 слепков реальных ходов зелёные, и за время приёмки они поймали три поломки. После следующего большого распила набор пересняли: 21 цепочка, 76 слепков со строгими отпечатками запросов к LLM, и новый набор сразу поймал поломку, 74 из 76 зелёных. Подробно про "трос" из слепков я писал отдельно, а с точки зрения этой статьи вывод простой: детерминизм окупается в первый же месяц, даже в пошаговой текстовой игре без всякого мультиплеера.
Работает ли промпт "сделай игру веселее"?
Работает, но не как магия. Промпт Уиллисона "This game isn't fun enough yet. Make it a LOT more fun." запустил многочасовую работу Claude Code в режиме Ultracode с субагентами, с вечера до позднего утра. Модель не "почувствовала" веселье, а перевела его в проверяемые метрики и прогнала 1200 матчей ИИ против ИИ.
This game isn't fun enough yet. Make it a LOT more fun.Что Claude добавил в v4: дебютные стратегии (Blitz, Industry, Air Wing), вызываемые способности вроде Barrage, EMP и Drop Pod, цели на карте (ретрансляторы, сбросы припасов), адаптивную музыку и голоса юнитов, режим выживания Last Stand, кампанию на 8 миссий, героев и 7 мутаторов. А дальше главное, цитата из поста: "В 1200 тестовых матчах ИИ против ИИ на Normal доля партий, не решённых за 25 минут, упала с 14% до 0,9%. Доля побед стороны, отстававшей на 800+ scrap, выросла с 24% до 46%".
Посмотри, что тут измерено. Не "веселье", а два его заместителя: партии не затягиваются и отстающий может отыграться. Это классические метрики геймдизайнера, только посчитанные на тысяче партий за ночь. Я в мобильном геймдеве с 2020 года, и баланс мы всегда сводили так же: придумать измеримый заместитель ощущения, прогнать симуляцию, посмотреть на распределение. Нейросеть тут сделала работу аналитика баланса, а не игрока. И сделать её она смогла только потому, что матч переигрывается без экрана и без человека. Убери детерминированную симуляцию - и промпт "сделай веселее" превращается в "накидай фич и надейся".
Отсюда моя граница: "сделай веселее" стоит писать агенту, только когда у игры уже есть безголовый прогон партии и хотя бы одна метрика, которую агент может посчитать сам. Без этого он добавит восемь фич, и ты не узнаешь, какая из них что-то дала. Про то, как развести работу нейросети и кода в балансе, подробнее в разборе баланса игры с нейросетью и симуляцией экономики.
Читайте такжеБаланс игры с нейросетью: что LLM считает сама, а что проверяет кодБаланс игры с нейросетью: что LLM делает в игровой экономике, где ошибается и как проверить конфиг симуляцией на Python. Код, цифры прогона и пример из DnD-бота.Что взять соло-геймдеву из Battle Drones: пошагово
Если делаешь игру с агентом и хочешь, чтобы через три месяца она не развалилась, вот порядок, который я бы повторил. Это не чек-лист Уиллисона, а моя выжимка из его репозитория и своих граблей.
- В первом же промпте вынеси симуляцию в отдельную папку без импортов движка и рендера. В Battle Drones это
src/sim/, у меня - отдельные модули хода. - Запрети
Math.random()в симуляции и сделай случайность функцией от сида и контекста решения (тик, id сущности, соль). - Сразу поставь тест-запрет: грепом или линтером валить сборку, если в папке симуляции встречаются
Math.random,Math.sin,Math.cos,atan2,hypot,pow. Свои функции - по fdlibm, агент их напишет. - Добавь хеш мира и пиши его в реплей раз в N тиков. Разъехались хеши - агент сам найдёт тик, где начался рассинхрон.
- Сделай безголовый прогон "ИИ против ИИ" и пару метрик партии: длительность, доля камбэков, доля побед каждой стороны.
- Только после этого проси "сделай веселее" и требуй от агента отчёт по метрикам до и после, как в v4.
Шаги 1-4 - это день работы агента, если делать их сразу. Если прикручивать к готовой игре, это недели раскопок, где ещё кто-то катит кубик мимо общего генератора. Проверено на себе.
Когда детерминированная симуляция не нужна?
Детерминированная симуляция не нужна, если в игре нет реплеев, мультиплеера и массовых автопрогонов, а сама игра маленькая: аркада на вечер, головоломка, кликер. Ещё её не стоит тащить в игру, где всё решает физический движок, которому ты не хозяин: переписать его математику дороже, чем жить без реплеев.
Граница у меня такая. Делаешь прототип на выходные, чтобы проверить одну механику, - забей, ИИ-агент соберёт его и так, я разбирал это в статье про создание игр с Claude Code за выходные. Планируешь жить с проектом дольше месяца, отдаёшь код агенту целиком и хочешь мультиплеер или реплеи - закладывай детерминизм в первую неделю. У Уиллисона он появился не сразу, и за это пришлось платить переписыванием тригонометрии посреди проекта.
И второй минус, о котором Уиллисон пишет сам: если ты не читаешь код, то не знаешь, где ещё спрятался свой "Math.sin". Тест-запреты и хеш мира - это ровно та страховка, которая позволяет вайбкодить игру и не превратить проект в чёрный ящик. Про момент, когда без чтения кода уже никак, у меня есть отдельный текст: вайб-кодинг довёл до чёрного ящика.
Частые вопросы
Почему Math.sin даёт разный результат в Chrome и Safari?
Стандарт ECMA-262 не фиксирует точный результат Math.sin, cos, atan2, hypot и pow, каждый движок считает их по-своему. По замеру в репозитории Battle Drones V8 и JavaScriptCore расходятся в последнем бите в 3% случаев для sin и в 35% для hypot. Для детерминированной игры нужны свои версии функций по fdlibm.
На каком движке делать браузерную игру с ИИ-агентом?
Battle Drones начиналась на Phaser (2D), а с пятой версии переехала на three.js, всё на TypeScript и Vite. Важнее движка - чтобы игровая логика жила отдельно от рендера, без импортов движка: тогда агент гоняет матчи без экрана и проверяет свои правки сам.
Сколько стоит сервер для онлайн-мультиплеера браузерной RTS?
При детерминированной симуляции сервер не считает игру, а только пересылает команды игроков. У Battle Drones это один маленький инстанс Fly.io shared-cpu-1x, по оценке автора игры не дороже $2.19 в месяц (октябрь 2026).
Как готовился материал: черновик собран моим AI-конвейером по темам и заметкам из практики, факты сверены с первоисточниками, финальный текст я прочитал, поправил и утвердил перед публикацией. Обложку к статье тоже рисует нейросеть. Про сам конвейер — в разделе обо мне.
