Статьи

Статья · · 12 мин чтения

Я дал ИИ-агенту дискеты от Amiga. Спустя один вечер Pirates! работала в Unity

Я дал агенту-программисту четыре образа дискет с версией Sid Meier's Pirates! для Amiga 1990 года и попросил перенести игру в Unity. Работал Claude Opus 5.5 в Claude Code. Спустя один вечер игру можно было пройти от титульного экрана через дуэль, город и плавание по карте до морского сражения — с оригинальной графикой и звуком. Важнее темпа то, как проверялся результат: один и тот же сценарий решений выполнили на порте на C# и на оригинальном машинном коде, запущенном в эмуляторе процессора. В 802 прогонах, то есть примерно в 6 миллионах сравнённых строк, обе версии показали игроку одно и то же.

Я описываю здесь этапы этой работы по порядку, потому что вместе они складываются в метод, который можно повторить с другой игрой и другой моделью. В конце показываю второй проект, сделанный по той же схеме, — порт Centurion: Defender of Rome с DOS на JavaScript. Оба проекта частные и учебные: оригинальные игры, их графика и звук остаются под защитой авторского права, и я их не распространяю.

Титульный экран Pirates! в порте на Unity: галеон под пиратским флагом на закате, нарисованный оригинальной графикой 1990 года.

Титульный экран в Unity. Изображение декодируется во время работы из оригинальных файлов игры.

От образа дискеты до декомпилированного кода

Агент начал с того, что разобрался в файлах. Образ дискеты Amiga (ADF) — это сырая копия секторов, поэтому для распаковки содержимого хватило нескольких десятков строк на Python. Программа игры оказалась файлом с оверлеями (overlay): ядро постоянно сидит в памяти, а ещё три сегмента кода подгружаются с диска по мере надобности. В Ghidra, бесплатном декомпиляторе, встроенной поддержки этого формата нет. Агент написал скрипт, который размещает все сегменты по фиксированным адресам в одном образе и заменяет 45 косвенных переходов в оверлеи обычными вызовами. Благодаря этому декомпилятор увидел около 630 функций как одну программу.

Данные игры преподнесли сюрприз. Файлы с расширением .rez устроены как ресурсы компьютеров Macintosh, потому что версия для Amiga была портом с Mac. Распознав формат, агент извлёк 210 текстов, 82 списка строк, данные городов для шести эпох, изображения, анимации, спрайты и звуки. Тексты оказались лучшей картой кода: номер строки, передаваемый в процедуру «показать текст», говорит о том, какой экран игры обслуживает данная функция.

Тринадцать агентов читают код, девять его переписывают

Декомпилированный код насчитывал почти 29 тысяч строк без имён функций и переменных. Главный агент разрезал его на куски примерно по 2 тысячи строк и передал каждый отдельному агенту с одним и тем же письменным заданием. Каждый должен был записать в файл описание всех функций своего фрагмента: точные формулы, константы, диапазоны случайных чисел, номера выводимых текстов, а также глобальные переменные, которые читаются и записываются. В задании была фраза, которая оказалась существенной: неверная формула хуже, чем отмеченный пробел. Поэтому агенты должны были помечать то, что не удалось установить, и проверять неясные места по ассемблеру. Декомпилятор терял здесь многие аргументы функций, поскольку компилятор 35-летней давности использовал 16-битный тип int.

На втором этапе разделение было тем же. Девять агентов переписали правила игры на C#, каждый — свой диапазон адресов. Получилось около 16 тысяч строк кода, а состояние игры попало в массив байтов, который адресуется так же, как память оригинала. Выглядит это не слишком изящно, зато устраняет целый класс ошибок. Старый код свободно накладывает структуры друг на друга и читает таблицы с байта, стоящего перед их началом. Порт на образе памяти можно переводить строка за строкой и сравнивать с ассемблером, а таблицы констант остаются там, где их ищет код. Отрисовку, звук и управление агенты заменили вызовами интерфейса, который движок реализует отдельно.

Оракул: оригинальный код в эмуляторе процессора

Чтение декомпилированного кода не доказывает, что порт работает как оригинал. Поэтому агент построил инструмент сравнения. Оригинальный машинный код процессора Motorola 68000 выполняется в эмуляторе Unicorn. Системные процедуры, то есть меню, текстовые окна, чтение ввода и отрисовка, были подменены функциями на Python. Эти функции отвечают на каждый вопрос игры по простому генератору решений и записывают всё, что игра показала. Порт на C# получает идентичную последовательность решений через «скриптового игрока», который работает вне Unity. В конце обе записи сравниваются строка за строкой. Тексты содержат золото, численность команды, даты и названия городов, так что совпадение записей — сильный тест правил.

Первое полное измерение дало 800 совпавших прогонов из 802. У обоих расхождений была одна причина: арифметика с плавающей запятой. Игра пользуется программной библиотекой, которая округляет не так, как сегодняшние процессоры. Её квадратный корень из 81 равен 8,999999999999998, а игра отбрасывает дробную часть результатов, поэтому боевой дух команды при некоторых соотношениях золота и численности команды получался в порте на один пункт выше, чем нужно. Исправление состояло в том, чтобы переписать процедуры библиотеки инструкция за инструкцией: 1572 инструкции, 43 процедуры и 21 701 тестовый вектор, вычисленный оригинальным кодом, — без единого различия. После этого изменения совпадают все 802 прогона, а ещё в 120 дополнительных состояние игры сравнивалось кадр за кадром в дуэли, в плавании, в морском и сухопутном сражениях.

Сравнение выявило и две ошибки самой игры 1990 года: чтение карты через нулевой указатель при атаке на город по диагонали и деление на ноль, когда численность команды падает до нуля. В таких местах порту приходится выбирать поведение, а у инструмента сравнения есть переключатель, который заставляет оригинал идти тем же путём. У результата есть и границы. Инструмент не сравнивает ни отрисовку, ни звук, а редкие пути, такие как женитьба, полностью собранная карта сокровищ или высшие звания, были проверены слабо.

Представление и дополнения, которых в 1990 году не было

Слой, который видит игрок, агенты построили в Unity из кода, без готовых сцен. Изображения и анимации декодируются из оригинальных ресурсов, спрайты получают цвета из палитры текущего экрана, а музыку отрендерили в звуковые файлы, эмулируя четырёхканальный секвенсор игры. Агент управлял запущенной игрой из командной строки, делал снимки экрана и рассматривал их. Так обнаружилось несколько ошибок, которые не ловит ни один тест правил, например отсутствие звука в браузерной версии: конвертация аудио требовала программы ffmpeg, которой в системе не было.

Карта Карибского моря в порте на Unity: острова с городами, корабль игрока с указателем курса и грозовые тучи.

Плавание по карте. Фон строится из сетки местности 160 на 200 клеток и оригинальных тайлов.

После первых пробных партий я попросил о трёх изменениях. Вопрос о дате прибытия флота с сокровищами был в оригинале защитой от копирования и требовал бумажного руководства; теперь викторина остаётся, но в «записках капитана» появляется правильный ответ. Список городов показывал только данные о городе, потому что игра предполагала, что у игрока есть печатная карта; теперь выбранный город отмечен на карте Карибского моря. Экран дуэли получил таблицу с силой, боевым духом и оружием обеих сторон, оформленную как в оригинале. Дополнения можно отключить, и сравнительные тесты работают без них, чтобы записи по-прежнему совпадали с кодом 1990 года.

Снимок экрана оригинальной игры на Amiga: два фехтовальщика перед стеной, ниже — заголовок дуэли и таблица Force, Morale, Weapon.

Дуэль в оригинале. Источник снимка экрана: OpenRetro Game Database.

Дуэль в порте на Unity: фехтовальщики на палубе корабля, ниже — заголовок и таблица с силой, боевым духом и оружием обеих сторон.

Тот же экран в порте. Числа в таблице читаются на лету из состояния дуэли.

Карта Карибского моря с отмеченным городом Port-de-Paix, позицией игрока и рамкой с данными города и французским флагом.

Дополнение к оригиналу: выбранный город на карте Карибского моря.

Centurion: та же схема на JavaScript и с расширенным геймплеем

Второй проект — Centurion: Defender of Rome, игра для DOS 1990 года, перенесённая на JavaScript и элемент Canvas, без эмулятора. Шаги были те же, только под другими названиями. Исполняемый файл сначала пришлось распаковать, потому что он был сжат утилитой EXEPACK. Пять модулей игры (сухопутное сражение, арена гладиаторов, гонка колесниц, морское сражение и заставка) лежали в файле оверлеев и пользовались одним общим сегментом данных. Состояние кампании работает на живой копии 64 килобайт этого сегмента, потому что движок игры полагается на 16-битное переполнение, целочисленное деление и байты со знаком, а числа с плавающей запятой в JavaScript сами этого не воспроизводят. Сохранение игры совпадает с оригинальным форматом байт в байт, что проверяют тесты.

Титульный экран Centurion в браузерном порте: надпись CENTURION над картой Средиземного моря, панель с титрами и кнопки Start Game, Difficulty и Game Options.

Титульный экран Centurion в браузере, на оригинальной графике.

Здесь я пошёл дальше, чем верный порт. Я добавил переключатель расширенных правил, по умолчанию выключенный: объединение легионов и несколько легионов в одной провинции, легковооружённые велиты, боевые слоны и морской десант. Код этого порта писали другие модели, не те, что в Pirates: больше всего — Gemini 3.8 Flash, а часть — gpt-6.1-sol от OpenAI, запущенный через Codex. Выбор определялся инструментами, которые были у меня под рукой, а не сравнением моделей.

Меню настроек игры в порте Centurion: пункт Extra Features: OFF, уровень сложности, сохранение и загрузка, рядом — мандат сената со званием и состоянием казны.

Меню настроек. Первый пункт включает расширенные правила, которых не было в оригинале.

Экран провинции Italia в порте Centurion: вид города, меню игр со строительством амфитеатра и состояние провинции: настроения, дань, войска.

Экран провинции с меню игр.

Проблемы, с которыми столкнулся Gemini в Centurion

Больше всего времени ушло на места, где декомпилированный код или имена файлов подсказывали не то, что игра делает на самом деле. Пять примеров из заметок проекта:

  • Оверлеи без данных. Пять модулей игры лежат в одном файле OVL0.OVL и подгружаются по одному и тому же адресу, но все они пользуются общим сегментом данных. Поэтому сам по себе код оверлея, открытый в Ghidra, не показывал ни строк, ни таблиц. Скрипт склеил каждый оверлей с сегментом данных в пять синтетических исполняемых файлов, и только тогда декомпилятор связал код мини-игр со структурами кампании.
  • Границы завоёванных провинций. Обводка провинции по карте областей, кликабельных мышью, давала угловатые линии, не подходившие к нарисованной от руки карте. Код показал другой механизм: в изображении карты у каждой провинции есть скрытая дорожка шириной в один пиксель тёмного цвета, а таблица в данных задаёт по одной стартовой точке для каждой из 27 провинций. Игра перекрашивает эту дорожку в цвет империи, начиная со стартовой точки, и порт делает то же самое.
  • Кадры, шагающие назад. Оригинальный игровой цикл работал с частотой 9–12 Гц, а браузер рисует 60 раз в секунду. В трёх местах спрайт на мгновение отступал на один кадр назад. Глобус в заставке вращается за счёт сдвига 14 цветов палитры за 10 шагов, но первый шаг в таблице имеет значение ноль; сдвиг палитры на каждом шаге давал рывок континентов раз за оборот. В марше когорт и выпадах гладиаторов пришлось точно воспроизвести порядок: сначала промежуточный кадр, потом смена позиции.
  • Обманчивые имена файлов. Битмапы BB1.CBM и BB2.CBM выглядели как баннеры сражения, а нарисованные вверху экрана закрыли всё поле боя. Код загружает их только тогда, когда в компьютере нет карты VGA: это полноэкранные фоны для более старых карт EGA и CGA. В той же процедуре выяснилось, что расстановка перед сражением состоит из двух этапов. Выбор построения подменяет список доступных тактик, а манёвр «Канны» появляется только после выбора клина.
  • Едва слышная музыка. После декодирования 17 композиций и 24 инструментов драйвера звуковой карты AdLib мелодии были правильными, но звучали на уровне около −55 дБ. Дизассемблирование драйвера показало, что на каждой ноте он вычитает четверть её громкости из затухания, записанного в инструменте. Одна строка кода вернула около 32 дБ. Вторая неисправность была на стороне браузера: на очень коротких нотах отмена запланированных изменений громкости в Web Audio API стирала ещё длящуюся атаку звука и заглушала ноту.

Сухопутное сражение в порте Centurion: список из пяти тактик для построения клином, включая Cannae Tactic, и отряды, расставленные на поле боя.

Второй этап расстановки перед сражением: тактики, доступные для построения клином.

Отдельной работой были гонки колесниц в Circus Maximus. Колесница там — не одна картинка. Отдельные битмапы для каждой комбинации трёх классов колесниц, четырёх цветов возничих, четырёх действий, шестнадцати углов наклона в повороте и трёх кадров галопа дали бы более 2300 кадров, поэтому авторы игры построили в памяти дерево из 16-байтовых узлов. Каждый узел указывает на следующий элемент того же уровня и на своего первого потомка, хранит индекс кадра, таблицу смещений для последовательных кадров анимации и флаг видимости. Корень — четвёрка лошадей. У кузова колесницы есть дополнительное горизонтальное смещение, в которое физика поворота записывает наклон от 0 до 15 пикселей, а возничий является его потомком и наследует эту позицию. Поэтому на дуге возничий наклоняется вместе с кузовом, тогда как лошади продолжают бежать по оси трассы. Ветви с ломающейся осью и кувыркающимися обломками существуют всё время, у них лишь выключен флаг видимости.

Схема дерева спрайтов колесницы в Centurion: узел с четырьмя лошадьми, узел кузова в трёх классах, узлы состояний аварии, четыре анимации возничего и собранная колесница без наклона и с наклоном 12 пикселей.

Дерево спрайтов колесницы, восстановленное по данным игры. Схема сделана в проекте из оригинальных декодированных ресурсов.

Трасса тоже построена не из тайлов. Игра разворачивает 72 записанные пары координат в 368 контрольных точек, а положение поперёк трассы делит на 16 полос. От полосы зависит безопасная скорость в повороте, которая считается как (номер полосы + 15) × 4: у внутренней стены она равна 60, у внешнего ограждения — 116–120. Внутренняя полоса даёт кратчайший путь, но вход в разворот выше этого порога наклоняет кузов, выносит колесницу наружу и запускает розыгрыш аварии, тем более вероятной, чем больше превышение. Красная метка на шкале скорости показывает игроку этот порог.

Геометрия трассы Circus Maximus: прямые, зоны торможения и развороты, отмеченные цветом на изображении стадиона, и график безопасной скорости, растущей от 60 на внутренней полосе до 120 на внешней.

368 точек трассы и безопасная скорость в повороте в зависимости от полосы.

Что осталось от этой работы, кроме игр

Метод я записал в виде скилла (skill), то есть набора инструкций, который агент загружает, когда получает похожую задачу. В нём три принципа из обоих проектов. Правила игры переносятся верно, а представление заменяется интерфейсом. Порт сохраняет раскладку памяти оригинала. Результату не доверяют, пока он не пройдёт сравнение с оригинальным кодом. В скилл вошли также задания для агентов, которые анализируют и портируют код, и скрипты из проекта Pirates.

Самый интересный вывод касается фанатов, а не агентов. Тот, у кого есть любимая игра, в которой ему годами чего-то не хватало, сегодня может быстро и дёшево переделать её по-своему, без исходного кода. В Pirates викторина, требовавшая печатного руководства, теперь сама подсказывает ответ, и появилась карта, которой в игре никогда не было. В Centurion я добавил правила, которых не было в оригинале: объединение легионов, боевых слонов и высадку с моря. Когда правила игры уже стали обычным кодом на C# или JavaScript, следующее изменение — это обычная правка кода, а не патч бинарника. Точно так же игру можно перенести на более быстрый движок или на другую платформу: Centurion работает в браузере, а Pirates собирается для Linux и для браузера. Метод не привязан и к старым играм. Декомпилятор, эмулятор процессора и сравнение журналов работают так же для более новой программы; больше кода означает больше работы, а не другой подход.

Источники

  • Собственные материалы обоих проектов: заметки по анализу кода, задания для агентов, отчёт инструмента сравнения (802 прогона, список найденных различий), описание порта Centurion и его спецификации. Они не опубликованы.
  • National Security Agency, Ghidra, пакет для обратной разработки, использованный для декомпиляции обеих игр.
  • Unicorn Engine, эмулятор процессоров, использованный для запуска оригинального кода 68000.
  • OpenRetro Game Database, страница игры Pirates! для Amiga, источник снимка экрана оригинальной игры.

Я использую контент, сгенерированный ИИ, как часть своего ежедневного процесса обучения.