
Любите ли вы Heroes of Might and Magic II так, как люблю его я? Я знаю, что большинство олдскулов скажут, что Heroes 3 гораздо лучше. Да, возможно, лучше, но я начал играть в эту игру в школе в 7-8 классе. Это был 97-98 год, и тогда еще Heroes 3 не было. А эмоции от побед, прохождения кампаний, от знания чит-кодов, чтобы сделать себе черных драконов или раскрыть всю карту, от ссор с друзьями за то, что кто-то подсмотрел, как ты ходил и сыграл против тебя, уже были. И эти эмоции остались со мной навсегда. И поэтому я до сих пор играю в эту игру и даже научил играть в нее свою десятилетнюю дочь Варю.
Учитывая прогресс, на который вышли современные агенты по кодированию, я не мог не создать новой расы в этой игре, чтобы нам с дочкой было интереснее играть. В этой статье я решил подробно рассказать, как и что я сделал, возможно кого-то это вдохновит на создание своих рас в аналогичных играх.
Итак! Код писал Claude Code, рисовал агент ChatGPT, музыку сочинил Suno. Я придумывал, проверял и присылал скриншоты с красными кружочками.
Как всё это выглядит и звучит в игре, я показал в шестиминутном ролике:
С чего началось
Вот мой первый промпт, дословно, вместе с оговорками диктовки:
“/Applications/fheroes2.app” Я хочу дополнить эту игру двумя новыми героями и двумя новыми мирами. Один будет принадлежать мне, будет Айванс Касл, и другой Варварас Касл. И главные герои в нем будут мой аватар и Варин аватар, сделанный на основе наших фотографий. Также там будут новые прикольные звери или там твари, которые будут принадлежать нашим войскам. У Вари, например, будет Винсент (это наша собака той-пудель), у меня будет Семен (это наша морская свинка Семен Семёныч). […] Давай подумаем, почитай код этой программы и подумай, как дополнить ее двумя новыми героями.
Я отправил его 5 ноября в 19:47, а последний коммит в движок появился 10 ноября. Всего было три захода: вечер 5-го, утро 6-го с восьми до полудня и утро 10-го, с 10:20 до 11:40.
Почему fheroes2
fheroes2 — открытый движок «Героев II» на C++ и SDL. Графики и музыки в нём нет, он читает файлы оригинальной игры (у меня GOG-версия). Раз код открыт, туда можно добавить свои расы, а свои ресурсы положить рядом в отдельный архив, не трогая оригинальные.
Claude первым делом прочитал код и вернулся с плохой новостью: расы в движке хранятся битовой маской в одном байте, и все 8 бит заняты шестью расами, «смешанной» и «случайной». Сама раса упоминается в 815 местах в 51 файле. Он предложил три варианта, от косметики до полноценных рас, и посоветовал идти по шагам. Я выбрал полноценные замки.
Кто что делал
Я придумывал, утверждал и тестировал, а ещё приносил фотографии для героев.
Claude Code (Opus 5.5) отвечал за архитектуру, код движка, конвейер графики и сборку. Обычные задачи он раздавал субагентам на Sonnet, Opus и Haiku.
Агент ChatGPT работал в той же папке проекта и нарисовал всю графику по техзаданию, которое написал Claude.
Suno сочинил музыку по промптам, которые тоже подготовил Claude.
Звуки существ Claude сам скачал с Pixabay через расширение для Chrome.
Правила работы лежат в CLAUDE.md. Нетривиальные задачи начинаются с режима планирования. В плане каждой подзадаче назначена модель. План и уроки ведутся в tasks/todo.md и tasks/lessons.md, а готовой задачу можно называть только после проверки.
Техзадание для художника
Вторым сообщением я попросил Claude не рисовать самому, а разложить папки и написать задание для ChatGPT. И подсказал идею: рисовать не каждую картинку отдельно, а по нескольку кадров на одном большом листе, потом резать.
Claude вытащил из архива оригинальной игры образцы спрайтов, чтобы художнику было с чем сравнивать, написал резак для листов и составил техзадание ART_BRIEF_FOR_CHATGPT.md почти на 600 строк. В нём одиннадцать разделов, среди них карта папок, единый стиль, правила листов, мир и персонажи, список ассетов с размерами, шаблоны промптов, этапы и чек-лист приёмки.
Стиль задан одним блоком, который повторяется в каждом промпте:
STYLE LOCK: hand-painted 1990s 256-colour fantasy strategy-game pixel art, crisp 1-pixel dark outline (#1E140C), 3–4 flat tones per material with hard-edged shading, light from the upper left, warm saturated colours slightly muted like an old CRT screen, chunky readable silhouette, cheerful storybook mood, no gradients, no soft blur.
Правила листа заранее запрещают сетку, номера и подписи:
1. Канвас делится на равные невидимые ячейки (сетка C колонок × R рядов). 2. В каждой ячейке ровно одна фигура целиком, с полями вокруг. Ничто не пересекает границы ячеек. 3. Во всех ячейках один и тот же масштаб и одна точка опоры: ноги стоят на воображаемой линии… … 5. Не рисуй линии сетки, номера, подписи и рамки.
Обычный лист — это сетка 4×3, двенадцать кадров. Ячейка маленького существа 448×384 пикселя, в игре из неё получается кадр 112×96. Одно существо занимает три-четыре листа: стойка и ходьба, атаки в три стороны, ранение и обморок, у стрелков ещё выстрелы. Вот лист Семёна, каким его вернул ChatGPT:

Мир в задании добрый: существа в бою не умирают, а падают в комичный обморок со звёздочками. В разделе «чего не делать» есть такой пункт:
Не пиши в промптах оценочные слова вроде «stunning, epic, masterpiece»: они ухудшают результат GPT Image. Описывай конкретные цвета, формы и материалы.
Первая правка: тема замков
Первый состав существ Claude придумал сам: гусь, ёжик-стрелок, бобр-прораб, медведь-лесоруб, филин-волхв и баня в городе Ивана. Я прислал правку: мой замок технорыбацкий, с пирсом, маяком и эхолотами, у Вари девичий и розовый. Claude переписал задание целиком, а в tasks/lessons.md появилось правило спрашивать тему и настроение фракции до техзадания и придумывать самому только детали внутри неё.
Как работал ChatGPT
Задание делило работу на этапы: сначала 16 концептов и четыре портрета, потом остановка до моего «да», и только после этого производство.


Где-то по дороге «Семён, свинка» превратился в кабана: в задании он записан кабаном-рыцарем в рыбацких латах. ChatGPT посмотрел на фотографию, увидел морскую свинку с чёрной головой и белой полосой на мордочке и нарисовал морскую свинку. В отчёте отдельно написал, что выбрал внешность с фото, а не текст задания.


От меня потребовалось только «да» на концепты. К утру 6 ноября агент сдал 567 игровых PNG и 115 GIF-превью анимаций: 12 существ с полными анимациями, Иван и Варя на карте и в бою, портреты, по 29 построек на город, фоны городов, замки на карте и гербы.

Что живёт в замках
Ур. | Айванс Касл | Варварас Касл |
|---|---|---|
1 | Карасики-матросы | Котёнок-шалун |
2 | Рак-спиннингист → Рак-снайпер | Белка-орехометчица → Белка-снайпер |
3 | Бобр-лодочник → Бобр-капитан | Лиса-плутовка → Лиса-иллюзионистка |
4 | Щука-торпеда → Щука-торпедоносец | Шмель-рыцарь → Шмель-паладин |
5 | Робо-чайка → Чайка-разведчик | Кот-учёный → Кот-архимаг |
6 | Семён → Семён Великолепный | Винсент → Винсент Великий |
Всего 22 существа: 12 базовых и 10 улучшенных. Улучшенных отдельно не рисовали, сборщик получает их перекраской базовых. У Семёна 55 очков здоровья, у Семёна Великолепного 70, и он бьёт дважды. Рак-снайпер стреляет два раза за ход.
Здания тоже по теме. У меня гильдия магов стоит в маяке, таверна называется «Уха у Семёна», а особое здание называется «Эхолотная башня». У Вари есть Чайная, Цветочный рынок, Радужный сад и Дворец Винсента.


Как картинки попадают в игру
PNG движок не читает. У «Героев II» свои форматы, и их пришлось воспроизвести по исходникам движка:
ICN — кадры спрайтов в RLE со вставками прозрачности и теней;
BIN — анимация существа: 821 байт, 34 типа анимации, смещения, скорость, точка вылета снаряда;
82M — звук, сырой PCM 22 050 Гц, 8 бит, моно;
AGG — архив с таблицей хешей;
палитра у игры одна, на 256 цветов.
В палитре есть подвох. Часть индексов в игре «мигает», на них держится анимация воды и лавы. Если покрасить таким цветом плащ героя, плащ будет переливаться. Поэтому квантование их обходит:
# Palette indices that cycle in-game; never emit them. FORBIDDEN_INDICES = frozenset(list(range(214, 222)) + list(range(231, 236)) + list(range(238, 242)))
Цвет подбирается в пространстве CIELAB с дизерингом Флойда — Стейнберга. Весь конвейер выглядит так:
art/*.png (RGBA от ChatGPT) → масштаб под габариты оригинальных спрайтов → квантование в палитру игры (CIELAB + Floyd–Steinberg, без «мигающих» индексов) → ICN / BIN / 82M → FAMILY.AGG (сейчас 205 файлов, 4,6 МБ) → движок ищет файл сначала в family.agg, потом в оригинальных архивах
Заодно сборщик генерирует кусок C++: области кликов по зданиям, порядок их отрисовки, раскладку домиков на карте. Скриптов в tools/ набралось около 3 400 строк. Проверяли их на оригинале: 18 тестов читают архив оригинальной игры как эталон, кодировщик пересобирает 46 оригинальных BIN байт в байт, а ICN после распаковки и обратной упаковки совпадает с исходником до пикселя.
День кода
Утром 6 ноября я передал Claude отчёт ChatGPT: «картинки готовы, можно расставлять». Сначала пять субагентов на Sonnet параллельно разбирали движок: расы, монстров, замки, героев, форматы и сборку. Потом Claude собрал план, а отдельный субагент на Opus его раскритиковал и нашёл четыре серьёзные ошибки. Одна из них сломала бы игру молча, другая уронила бы её при запуске.
Ошибка на единицу. Монстр на карте записан как номер кадра id - 1 в одном байте. По плану новые существа получали номера с 71, но 71 уже занят «случайным монстром 4-го уровня». Со сдвигом номеров существа на картах без всяких сообщений превращались бы в соседей. Правильные номера 72–93.
Таблицы, которые заполняются нулями. Характеристики монстров в движке лежат в обычных C-массивах на MONSTER_COUNT элементов. Если забыть строку, существо получит 0 HP и пустое имя файла, и игра упадёт. Массивы заменили на std::array со static_assert на размер.
Новые расы получили значения за пределами байта:
// Additional races of this fork (see family/family.h). They do not fit into 8 bits. IVAN = 0x100, VARV = 0x200,
Отсюда пошли правки в сохранения и формат карт. В заголовке карты раса игрока лежит в несжатой части, которую список карт читает ещё до проверки версии. Поэтому новые поля дописаны в конец, а версия карт помечена отдельным битом, 0x4000 | 14, чтобы не столкнуться с будущей версией 14 у авторов движка.
С сохранениями вышла похожая история. Сначала Claude взял следующий свободный номер версии, 10033, а код-ревью нашло, что этот номер уже занят версией 1.1.15 настоящего fheroes2. Её сохранения наша игра приняла бы за свои. Теперь у форка своя нумерация: 0x7F01, а после добавления героев 0x7F02.
Ещё одна защита касается файла с графикой. Новые расы появляются в игре, только если рядом лежит family.agg, а в кампаниях их нет совсем. Без архива движок работает как обычный fheroes2.
Музыка
Сам план я один раз отклонил: в нём не было музыки. Я попросил промпты для Suno: ренессанс с технологиями для Ивана и романтичный ренессанс для Вари, для темы героя, осады и капитана. Через две минуты план был утверждён, а ещё через минуту промпты лежали в SUNO_PROMPTS.md. Во lessons.md это стало вторым уроком: контент фракции включает графику, звуки, музыку и тексты сразу.
Музыку я генерировал в Suno сам, по три варианта стиля на каждый из шести треков. Вот один из вариантов темы Ивана:
🎛 ВЕРСИЯ 1 — «Пирс на рассвете» STYLE: Renaissance pavane, stately slow tempo, lute and recorder consort, viola da gamba drone, soft analog synth arpeggios beneath, D dorian, calm dawn over a lake EXCLUDE: vocals, choir, modern drum kit
У Ивана лютня и блок-флейта звучат поверх аналоговых синтезаторов и пингов эхолота, у Вари арфа и клавесин, гальярды и паваны. Claude обрезал тишину в конце треков, чтобы они крутились по кругу без паузы, и подключил их вместо MIDI оригинальных замков.
Тринадцать коммитов
С 09:29 до 11:50 в ветке появилось 13 коммитов: ядро рас, 22 существа, герои, города, карта и редактор, перевод, звуки, музыка. Ядро и формат карт писал субагент на Opus, существ и города писал Sonnet, базовую сборку поднял Haiku.
Чтобы не сломать мою обычную игру, «семейная» версия ставится отдельным приложением fheroes2 Family.app со своими настройками и сохранениями, а оригинальные данные берёт из общей папки. После каждой установки Claude проверял, что подпись оригинального приложения не изменилась.
Первый запуск
Первая сборка упала сразу. Оказалось, что SDL2 из Homebrew на самом деле прослойка, которая в рантайме грузит SDL3, а SDL3 в пакете не было. Потом macOS убила процесс за неподписанную библиотеку. Когда игра наконец запустилась, бой прошёл мгновенно: в свежей копии настроек все бои считались автоматически, и пришлось перенести мои настройки.
Дальше Claude с моего разрешения взял управление экраном и сам прошёл основной сценарий: новая игра за Рыболова, город, стройка, найм, герой на карте, сохранение и загрузка, бой Иван против Вари. Когда подключилось расширение Chrome, он за минуту скачал с Pixabay 39 звуков: атаки, ранения, обмороки и выстрелы. Рак стреляет звуком катушки спиннинга, а Винсент при ранении скулит. По дороге случился мелкий баг: в скрипте скачивания переменная цикла называлась path, а в zsh она связана с PATH, так что curl и ls вдруг стали «not found».

Ещё 16 героев
Если купить в таверне второго героя, он будет из чужого замка: у наших рас были только Иван и Варя. Claude написал второе задание для ChatGPT, EXTRA_HEROES_BRIEF.md, на 8 героев каждой расы, по четыре портрета на листе 2×2. Минут через десять портреты были готовы, ещё минут через десять они уже были в игре.


У меня служат Боцман Кузьмич, Инженер Лада, Сонарщик Тимофей, Рыбнадзор Степан и Атаманша Белуга, у Вари — Фея Зефирка, Гадалка Сирень, Кондитерша Карамель и Бабушка Пелагея. Теперь у каждого замка по девять героев, как у оригинальных классов.
Плейтест: красные кружочки
Дальше я играл сам и присылал скриншоты. В первом раунде было три правки.

На экране новой игры у наших классов были гербы со щитами, а у оригинальных классов там портреты. Я хотел видеть наши лица. Claude собрал значки из бюстов Ивана и Вари. В первый вариант попала удочка соседней фигуры, во втором у Вари обрезался подбородок, третий подошёл.
Первым героем игрока за Ивана должен быть Иван, а за Варю — Варя. Остальные герои в таверне приходят как обычно, свои и чужие.
Семён и Винсент ходили по карте беззвучно. Я написал: «Сам прими решение, что лучше, но звуки должны появиться». Claude собрал шаги из записей Pixabay: топот маленького зверька, у Семёна с писком, у Винсента со звоном жетонов на ошейнике. Громкость он выровнял по оригинальным звукам шагов.
После этого я написал «все работает! супер!». Второй раунд случился через четыре дня, 10 ноября: пять правок и четыре скриншота.




Claude сравнил наши спрайты с оригиналами по размерам и точкам опоры. Почти всё свелось к одному: нашу графику не сверили с оригинальной.
Симптом | Причина | Исправление |
|---|---|---|
Существа вылезают из ячеек армии в городе | Картинки больше ячейки 82×93 | Вписываются в 76×88 и стоят по нижнему краю |
Существа в бою наслаиваются | Наши почти вдвое шире оригиналов: у тех 34–49 px на одну клетку и 70–76 px на две | Ограничение 54×76 и 90×95, масштаб 0,61–0,79 |
Варя висит над землёй | Героя ровняли по верхушке посоха, где игра вешает флаг игрока | Ровняем по линии ног рыцаря, а посох удлиняем до точки флага |
Мусор на иконке левой башни после окна покупки | Кусок фона для иконки выходил за край картинки, и в прозрачные пиксели просвечивал старый экран | Кусок зажат внутри фона, сборщик проверяет, что в нём нет прозрачных пикселей |
Ещё я попросил верфь со стандартным кораблём, её в наших замках не было вовсе. Субагент на Sonnet включил верфь в движке, а Claude тем временем нарисовал причал в стиле каждого замка, корабль взял из оригинальной игры, тот же, что продаёт верфь. Субагент забыл про верфь Вари, Claude это заметил и дописал сам.
В lessons.md из этого раунда ушло правило: любой новый спрайт сравнивать с оригиналом того же назначения по ширине, высоте, точке опоры и ячейке.
Окно игры не принимает клики в фоне, а забирать мой экран второй раз Claude не стал. Поэтому правки 10 ноября он проверил по превью, сборке и стенду, а в игре их смотрел уже я. Существа теперь помещаются в ячейки армии, в бою каждое стоит в своей клетке, а иконка левой башни чистая.



Домик белочек
Потом я вспомнил, что забыл спросить про жилища. По карте разбросаны нора хоббитов, лагерь фургонов и другие домики: герой заходит туда и пополняет армию. А есть ли где-нибудь домик белочек?
Домика не было. Сначала обсуждали, не заказать ли ChatGPT новые картинки, но я спросил, нельзя ли взять жилища из наших городов. Claude уменьшил все 12 городских жилищ до размера объекта карты, добавил тени и показал превью. Я ответил: «Да, из городских».

Домики первого и второго уровня работают как нора хоббитов, существа приходят бесплатно. С третьего по шестой уровень работают как лагерь фургонов, там существ нанимают за золото. Каждую неделю их становится больше. ИИ про эти домики тоже знает, а в редакторе карт они есть в списке жилищ.
На старых картах их никто не расставлял. Поэтому в новой игре часть оригинальных жилищ подменяется нашими того же уровня. Подменяется только то, что помещается на место и не перекрывает проход, а отдельная проверка следит, чтобы сундук или ресурс по соседству не оказался замурован. Подмена детерминирована по зерну карты, так что одна и та же карта даёт один и тот же результат.
Сначала подмена срабатывала редко, на 3–4% жилищ. Claude спросил, сколько подменять, и я выбрал «все подходящие, но не больше 35%». На стенде без интерфейса проверили 103 карты по три прогона. Из 2 325 подходящих жилищ подменилось 226, на 64 картах. На Beltway не подменилось ни одного: там жилища зажаты между горами, и наши домики не влезают.
Как это выглядит сейчас
В окне колодца видны все существа замка и их недельный прирост:


Иван на Семёне у своего замка:

Найм в городе Вари:

Бой Ивана в болоте. Сам Иван верхом на Семёне, а в армии ещё два Семёна, раки-снайперы, бобры-лодочники, щуки-торпеды и одна робо-чайка.

Что ещё не доделано
В окне колодца Семён великоват и залезает на свои характеристики. Окна гильдии магов в городах мелковаты. Удлинённое удилище Ивана в двух кадрах колдовства отстаёт от руки. Длинные имена героев в списке редактора, возможно, обрезаются: это ещё не проверяли.
Цифры
19 коммитов в движок: 77 файлов, +3 616 и −213 строк.
Около 3 400 строк скриптов в конвейере ассетов.
567 игровых PNG от ChatGPT на первом заходе и 16 портретов на втором.
22 существа, 18 героев, по 29 зданий на город, верфь, 12 домиков на карте.
42 звука с Pixabay и 6 треков из Suno.
32 запуска субагентов. Модель явно указана у 25 из них: Sonnet 17 раз, Opus 7 раз, Haiku 1 раз.
Сам я написал в чат около дюжины сообщений, не считая ответов на вопросы.
Что я вынес
Задание для ChatGPT писал Claude, и это сработало лучше, чем если бы писал я. Он знал, какие размеры нужны движку и где у спрайта точка опоры, и заранее решил, что тени, зеркальные кадры и улучшенные версии сборщик сделает сам. Художнику остались персонажи и здания.
Скриншот с красным кружком оказался лучшим баг-репортом. Мне не пришлось объяснять, что такое ячейка армии или точка флага: Claude сам нашёл общую причину по четырём картинкам.
Отдельное приложение я попросил с самого начала, и оно себя оправдало: оригинальная игра всё это время работала как раньше, у экспериментов были свои настройки и сохранения.
Режим планирования из CLAUDE.md дал мне отклонить план из-за музыки ещё до того, как написан код. А ревью отдельным субагентом нашло сдвиг номеров монстров и чужой номер версии сохранений. Обе ошибки я сам не заметил бы: игра бы не упала, просто работала бы неправильно.
Код выложу на GitHub позже. Для игры понадобятся оригинальные данные «Героев II», как и для обычного fheroes2.
