Back to archive
#ai#agents#coding#llm#aigen

I gave an AI agent Amiga floppy disks. After one evening Pirates! was running in Unity

I gave a coding agent four floppy disk images of the 1990 Amiga version of Sid Meier's Pirates! and asked it to move the game to Unity. The work was done by Claude Opus 5.5 in Claude Code. After one evening the game could be played from the title screen through a duel, a town and sailing the map to a sea battle, on the original graphics and sound. More important than the pace is how the result was checked: the same script of decisions was run on the C# port and on the original machine code executing in a processor emulator. In 802 runs, about 6 million compared lines, both versions showed the player the same thing.

I describe the stages of this work here because together they form a method that can be repeated with another game and another model. At the end I show a second project built on the same scheme, a port of Centurion: Defender of Rome from DOS to JavaScript. Both are private projects for learning: the original games, their graphics and their sound remain under copyright, and I do not distribute them.

The Pirates! title screen in the Unity port: a galleon flying a pirate flag at sunset, drawn with the original 1990 graphics.

The title screen in Unity. The picture is decoded at run time from the original game files.

From a disk image to decompiled code

The agent began by identifying the files. An Amiga disk image (ADF) is a raw copy of sectors, so a few dozen lines of Python were enough to extract the contents. The game program turned out to be an overlay executable: a core stays in memory, and three further code segments are read from disk as needed. Ghidra, a free decompiler, has no built-in support for that format. The agent wrote a script that places all segments at fixed addresses in one image and turns 45 indirect jumps into overlays into ordinary calls. With that, the decompiler saw about 630 functions as a single program.

The game data held a surprise. The files with the .rez extension have the layout of Macintosh resources, because the Amiga version was a port from the Mac. Once the format was recognised, the agent pulled out 210 texts, 82 string lists, city data for six eras, pictures, animations, sprites and sounds. The texts proved to be the best map of the code: the number of a string passed to the "show text" routine tells which game screen a given function handles.

Thirteen agents read the code, nine rewrite it

The decompiled code ran to nearly 29 thousand lines with no function or variable names. The main agent cut it into pieces of about 2 thousand lines and gave each to a separate agent with the same written brief. Each had to write a file describing every function in its piece: exact formulas, constants, random ranges, the numbers of the texts shown, and the global variables read and written. The brief contained a sentence that turned out to matter: a wrong formula is worse than a flagged gap. So the agents had to mark what they could not determine and check unclear places in the assembly. The decompiler lost many function arguments here, because the 35-year-old compiler used a 16-bit int type.

The second stage had the same division. Nine agents rewrote the game rules in C#, each for its own address range. That produced about 16 thousand lines of code, and the game state went into a byte array addressed exactly like the original's memory. It does not look elegant, but it removes a whole class of errors. The old code freely overlays structures on one another and reads tables from the byte before their start. A port built on a memory image can be translated line by line and compared with the assembly, and the constant tables stay where the code looks for them. Drawing, sound and controls were replaced by calls to an interface that the engine implements separately.

The oracle: the original code in a processor emulator

Reading decompiled code does not prove that the port behaves like the original. So the agent built a comparison tool. The original machine code for the Motorola 68000 processor runs in the Unicorn emulator. The system routines, meaning menus, text boxes, input reading and drawing, were replaced with Python functions. Those functions answer every question the game asks according to a simple decision generator and record everything the game showed. The C# port receives the identical sequence of decisions through a "scripted player" that runs outside Unity. At the end the two records are compared line by line. The texts contain gold, crew size, dates and city names, so matching records are a strong test of the rules.

The first full measurement gave 800 matching runs out of 802. Both mismatches had one cause: floating-point arithmetic. The game uses a software library that does not round the way today's processors do. Its square root of 81 is 8.999999999999998, and the game truncates results to integers, so crew morale came out one point too high in the port at certain ratios of gold to crew. The fix was to rewrite the library's routines instruction by instruction: 1,572 instructions, 43 routines, and 21,701 test vectors computed by the original code, with no difference. After that change all 802 runs match, and in 120 additional ones the game state was compared frame by frame in the duel, in sailing, and in the sea and land battles.

The comparison also exposed two bugs in the 1990 game itself: a read of the map through a null pointer when attacking a town from a diagonal, and a division by zero when the crew falls to zero. In such places the port has to choose a behaviour, and the comparison tool has a switch that makes the original take the same path. The result also has limits. The tool does not compare drawing or sound, and rare paths such as marriage, a completed treasure map or the highest ranks were exercised only lightly.

Presentation and additions that did not exist in 1990

The layer the player sees was built in Unity from code, without ready-made scenes. Pictures and animations are decoded from the original resources, sprites take their colours from the palette of the current screen, and the music was rendered to audio files by emulating the game's four-channel sequencer. The agent drove the running game from the command line, took screenshots and looked at them. That revealed several defects no rules test catches, for example missing sound in the browser version, because audio conversion needed the ffmpeg program, which was not installed.

The Caribbean map in the Unity port: islands with towns, the player's ship with its heading pointer, and storm clouds.

Sailing the map. The background is built from a terrain grid of 160 by 200 cells and the original tiles.

After the first play sessions I asked for three changes. The question about the arrival date of the treasure fleet was the original's copy protection and required the printed manual; now the quiz stays, but the correct answer appears in the "captain's notes". The city list showed only data about a city, because the game assumed the player had a printed map; now the chosen city is marked on a chart of the Caribbean. The duel screen received a table with the force, morale and weapon of both sides, laid out as in the original. The additions can be switched off, and the comparison tests run without them, so the records still match the 1990 code.

A screenshot of the original Amiga game: two fencers in front of a wall, with the duel title and a table of Force, Morale and Weapon below.

The duel in the original. Screenshot source: OpenRetro Game Database.

The duel in the Unity port: fencers on a ship's deck, with the title and a table of force, morale and weapon for both sides below.

The same screen in the port. The figures in the table are read live from the duel state.

A chart of the Caribbean with the town of Port-de-Paix marked, the player's position, and a box with the town's data and the French flag.

An addition to the original: the chosen city on a chart of the Caribbean.

Centurion: the same scheme in JavaScript, with extended gameplay

The second project is Centurion: Defender of Rome, a 1990 DOS game, moved to JavaScript and the Canvas element, with no emulator. The steps were the same under different names. The executable first had to be unpacked, because it was compressed with the EXEPACK tool. Five game modules (land battle, gladiator arena, chariot race, naval battle and the intro) sat in an overlay file and shared one data segment. The campaign state runs on a live copy of the 64 kilobytes of that segment, because the game engine relies on 16-bit overflow, integer division and signed bytes, which JavaScript's floating-point numbers do not reproduce by themselves. The saved game matches the original format byte for byte, which the tests verify.

The Centurion title screen in the browser port: the word CENTURION above a map of the Mediterranean, a credits panel, and the Start Game, Difficulty and Game Options buttons.

The Centurion title screen in the browser, on the original graphics.

Here I went further than a faithful port. I added a switch for extended rules, off by default: merging legions and several legions in one province, light velites, war elephants and landings from the sea. The code of this port was written by different models than in Pirates: mostly Gemini 3.8 Flash, and partly OpenAI's gpt-6.1-sol run through Codex. The choice came from the tools I had at hand, not from a comparison of models.

The game options menu in the Centurion port: the entry Extra Features: OFF, the difficulty level, save and load, and next to it the Senate mandate with rank and treasury.

The options menu. The first entry turns on the extended rules that were not in the original.

The Italia province screen in the Centurion port: a view of the city, the games menu with the amphitheatre to build, and the province status: mood, tribute, troops.

The province screen with the games menu.

The problems Gemini ran into with Centurion

Most of the time went into places where the decompiled code or the file names suggested something other than what the game actually does. Five examples from the project notes:

  • Overlays without data. The five game modules live in one file, OVL0.OVL, and are loaded at the same address, but all of them use a shared data segment. An overlay's code opened in Ghidra by itself therefore showed no strings or tables. A script joined each overlay with the data segment into five synthetic executables, and only then did the decompiler connect the minigames' code with the campaign structures.
  • Borders of conquered provinces. Outlining a province from the map of mouse-clickable regions gave jagged lines that did not fit the hand-drawn map. The code showed a different mechanism: in the map picture each province has a hidden path one pixel wide in a dark colour, and a table in the data gives one starting point for each of the 27 provinces. The game repaints that path in the empire's colour, beginning at the starting point, and the port does the same.
  • Frames stepping backwards. The original game loop ran at 9–12 Hz, and a browser draws 60 times a second. In three places a sprite stepped back by one frame for a moment. The globe in the intro rotates by shifting 14 palette colours over 10 steps, but the first step in the table has the value zero; shifting the palette on every step made the continents jerk once per rotation. In the marching cohorts and the gladiators' lunges the order had to be reproduced exactly: first the intermediate frame, then the change of position.
  • Misleading file names. The bitmaps BB1.CBM and BB2.CBM looked like battle banners, and drawn at the top of the screen they covered the whole battlefield. The code loads them only when the computer has no VGA card: they are full-screen backgrounds for the older EGA and CGA cards. The same routine revealed that deployment before a battle has two stages. Choosing a formation replaces the list of available tactics, and the Cannae manoeuvre appears only after the wedge is chosen.
  • Barely audible music. After 17 tracks and 24 instruments of the AdLib sound card driver were decoded, the melodies were right but played at about −55 dB. Disassembling the driver showed that on every note it subtracts a quarter of the note's velocity from the attenuation stored in the instrument. One line of code restored about 32 dB. A second defect was on the browser side: on very short notes, cancelling scheduled volume changes in the Web Audio API wiped out the attack that was still in progress and silenced the note.

A land battle in the Centurion port: a list of five tactics for the wedge formation, including the Cannae Tactic, and units deployed on the battlefield.

The second stage of deployment before a battle: the tactics available for the wedge formation.

The chariot races in the Circus Maximus were a separate piece of work. A chariot there is not a single picture. Separate bitmaps for every combination of three chariot classes, four driver colours, four actions, sixteen lean angles in a turn and three gallop frames would have meant over 2,300 frames, so the game's authors built a tree of 16-byte nodes in memory. Each node points to the next element on the same level and to its first child, and holds a frame index, a table of offsets for the successive animation frames, and a visibility flag. The root is the four horses. The chariot's hull has an extra horizontal offset into which the cornering physics writes a lean of 0 to 15 pixels, and the driver is its child and inherits that position. That is why in a bend the driver leans together with the hull while the horses keep running along the track's axis. The branches with the breaking axle and the tumbling wreck exist all the time; they only have their visibility flag turned off.

A diagram of the chariot sprite tree in Centurion: a node with four horses, the hull node in three classes, the crash-state nodes, four driver animations, and the assembled chariot with no lean and with a lean of 12 pixels.

The chariot sprite tree reconstructed from the game data. The diagram was made in the project from the original, decoded resources.

The track is not built from tiles either. The game expands 72 stored coordinate pairs into 368 control points and divides the position across the track into 16 lanes. The lane determines the safe speed in a turn, computed as (lane number + 15) × 4: at the inner wall it is 60, at the outer barrier 116–120. The inner lane is the shortest way round, but entering the hairpin above that threshold leans the hull, pushes the chariot outwards and triggers a roll for a crash, which becomes more likely the larger the excess. The red marker on the speed bar shows the player that threshold.

The geometry of the Circus Maximus track: straights, braking zones and hairpins marked in colour on the stadium picture, and a chart of the safe speed rising from 60 in the inner lane to 120 in the outer one.

The 368 track points and the safe cornering speed depending on the lane.

What remains from this work besides the games

I wrote the method down as a skill, a set of instructions that an agent loads when it receives a similar task. It contains three principles from both projects. The game rules are ported faithfully, and the presentation is replaced with an interface. The port keeps the original's memory layout. The result is not trusted until it passes a comparison with the original code. The skill also includes the briefs for the analysing and porting agents and the scripts from the Pirates project.

The most interesting conclusion concerns fans, not agents. Anyone who has a game they like and has missed something in it for years can now rework it their own way, quickly and cheaply, without the source code. In Pirates the quiz that needed the printed manual now gives the answer, and a map the game never had has appeared. In Centurion I added rules the original did not have: merging legions, war elephants and landings from the sea. Once the game's rules are ordinary code in C# or JavaScript, the next change is an ordinary code change, not a binary patch. A game can be moved to a faster engine or another platform in the same way: Centurion runs in a browser, and Pirates builds for Linux and for the browser. Nor is the method tied to old games. A decompiler, a processor emulator and the comparison of records work the same for a newer program; more code means more work, not a different approach.

Sources

  • My own materials from both projects: code analysis notes, briefs for the agents, the comparison tool's report (802 runs, the list of differences found), and the description of the Centurion port with its specifications. They are not published.
  • National Security Agency, Ghidra, the reverse-engineering suite used to decompile both games.
  • Unicorn Engine, the processor emulator used to run the original 68000 code.
  • OpenRetro Game Database, the page for Pirates! on the Amiga, source of the screenshot of the original game.

I use AI-generated content as part of my daily learning process.