In 1993, in Baghdad, I built a game called Babylonian Twins on an Amiga 500: 512KB of RAM, no hard drive, plugged into a TV. I was an engineering student in my twenties. Pure 68000 assembly, every sprite and every scanline by hand. Murtadha Salman drew the art and Mahir AlSalman composed the music. We were under sanctions. No internet, no game development resources, just one copy of the Amiga Hardware Reference Manual, which I used to program the hardware directly, and electricity a few hours a day. The constant floppy disk swapping (because of the small memory) and the 50°C summers killed my disk drive three times.
Left: 1993, on the Amiga. Right: 2026, the same gateway.
On the Amiga, “by hand” means the game doesn’t ask the operating system for anything while it runs. At startup it saves the interrupt vectors, switches the OS interrupts off and takes the whole machine:
“Get the system from the AMIGA” is my comment, from 1993. From that point on the display is the game’s own copper list (the Amiga’s programmable video coprocessor), rewritten on the fly for sprites and sky colours. Tiles move by writing the blitter’s registers directly and waiting on its done flag. The joystick is read straight from the hardware port, and the fire button is one pin on a CIA chip. The OS comes back only between levels, to load the next level’s files from the disk, and then it’s switched off again.
It was the first commercial game made in Iraq, and for a long time a game very few people got to play. Commodore collapsed and sanctions scared off publishers, so the finished game sat on a shelf. An Amiga forum found it in 2008 from my brother’s YouTube uploads and hunted me down for the disks; the thread is still there:https://eab.abime.net/showthread.php?t=39120.
The game has been ported once before, by hand, in 2010. The same team rebuilt it for the iPhone on an engine written from scratch, about 34,000 lines of C++, over months of nights and weekends. Apple and Google featured it, and it reached over two million downloads. That story is here:https://babyloniantwins.com/story.
I didn’t do this port. I asked for it, played the result every night, said what felt wrong, and made the few decisions that needed somebody who was there in 1993. The file formats and the assembly reading were the AI’s work, and so were the decisions about how to carry thirty-year-old code across, and it went faster than I could follow. This post is what I found when I sat down weeks later and read what had been done to my own game. Some of it was wrong, and I didn’t notice for weeks.
I’d tried this before. About a year ago I gave an earlier model the same Amiga material and asked it to make sense of my binary level maps. It got there in the end, but it took several rounds and a lot of hints from me.
Then Claude Fable 5 shipped, and I gave it the same files.
The test was deliberate. My guess was that there is little Amiga assembly code in LLM training sets. If the model was better at working things out rather than recalling them, this is where it would show.
The July 4th weekend was coming up, so I planned three steps, each one conditional on the previous working.
Step one, the safe ask: my own 2010 engine, the 34,000 lines of C++, moved into Godot 4. This was the control.
Step two, the unfair ask: the original 72,758 lines of 68000 assembly, for a machine that had gone out of production, with no comments to speak of and nothing in common with the C++. Rebuild that in Godot too, at the Amiga’s original 50 Hz.
Step three, the greedy ask: put the second one inside the first, so buying the modern game gets you the 1993 original as a second thing you can launch.
All three worked. The level format that had taken several rounds and my corrections a year earlier came out in a single pass, with no hints from me.
I ran it in Claude Code, so it had a terminal and my filesystem. It could edit files, run the assembler, build the game, launch the game and read what came back. When I say below that it rebuilt my 1993 binaries and checked them, it did that by running vasm and diffing the output.
Early on it added a set of command-line flags to the game so it could play without me:
Which turns “does the jump feel right” into something a machine can read:
It also had two headless checks it could run before showing me anything: one that compiles every script, and one that builds every level and reports failures. On the Amiga side it drove the real toolchain, vasm to assemble and FS-UAE to boot the result. What wasn’t automated: there was no image comparison on the modern port (it took screenshots, I looked at them), and nothing checked whether the game felt right.
Wednesday night, the safe ask. Timestamps, unedited:
Twenty-one minutes from empty project to a playable character. Every line it moved that night was a line I’d written, over months, in 2010. I went to bed confused.
Getting it to feel right took about three days after that: jump arcs and trampoline timing, and hit detection that rewards mashing, fixed in batches on July 2nd, 3rd and 4th.
I wasn’t testing alone. My thirteen-year-old son played every build with me. He’s always known I made this game, it’s a fact about his father he grew up with, but he’d never seen me working on it. The testing turned into a father-and-son thing I didn’t plan, and it’s one of my favourite parts of the whole project.
All the gameplay state lives in tile units (1.0 = one 48px tile), and the update runs at a fixed 60 Hz, because the 2010 iOS build ran at 60 Hz. That matters because the original applies drag multiplicatively, every frame:
Multiply by 0.85 sixty times a second and you get one amount of friction; multiply fifty times a second and you get another. Port it to a different tick rate and every acceleration curve in the game changes. Nothing crashes, it just feels wrong forever, and you won’t find it by reading the diff. At 60 Hz the constant transplants verbatim. This is also why the 1993 rebuild runs at 50 Hz and the modern one at 60: two sets of hand-tuned numbers, each only correct at its own tick. It kept both clocks. I’d have been tempted to tidy them into one.
Godot ships CharacterBody2D and move_and_slide() , and every tutorial tells you to use them. The port used neither for the player. The original has its own hand-written movement code, and rebuilding that on somebody else’s physics would feel slightly wrong in ways that are miserable to track down. The player is a plain Node2D , and the 150-line collision routine came across line for line, including the fudge numbers I picked by feel fifteen years ago and the comments I wrote to my future self:
Nothing tidied up the stray 0.49 . There are no tests and no docs; those comments are the spec.
By Sunday afternoon, July 5th, I handed over the thing I actually wanted to test. 72,758 lines across 26 files, written for a machine with 512 KB of memory, by me, for me, with the commenting habits of somebody who never expected another person to read it. No documentation. A 2008 transfer to modern storage had shortened every long filename, so every include pointed at names that no longer existed. One of the five level source files is cut off partway through a data table. There’s no other copy.
Before porting anything, it made the 1993 sources assemble again, using vasm on an Apple Silicon Mac, and kept going until the output was byte-identical to the binaries that shipped.
Fifteen minutes from a folder of files to the first rebuild that matched the shipped bytes. I wrote these in ASM-One, whose dialect differs from vasm’s in ways that change the bytes: ASM-One encodes cmp #4,d0 as CMPI, vasm picks a different, equally valid encoding, so telling it not to optimise is necessary and not sufficient. Rather than edit my sources it wrote a preprocessing pass that bridges five such differences, and rebuilt the broken filename mapping file by file.
The expensive one was org . With no linker and no relocation, the level source lays out the Amiga’s memory by hand, address by address:
The level-one map uses 74,400 of those 74,752 bytes, a margin of 352, and nothing checked it except me, in 1993. ( SahamR-grb attaches an object’s behaviour as a named offset; saham is Arabic for arrow.) ASM-One’s org can also move the location counter backwards, which vasm can’t. The first workaround got one case wrong: a ds.b 800 inside a rewound block, which ASM-One treats as “skip 800 bytes”, was written out as 800 bytes of zeros. Everything after that point in the file, the copper list included, sat 944 bytes away from where the shipped binary had it. The game assembled and booted, and drew the wrong thing.
Even after that, some chunks still wouldn’t match, by about 108 bytes scattered through the variable area. Those bytes explained where the shipped files came from. ASM-One assembles into memory, and the game got onto disk by saving that memory out, after the game had been run. So the shipped files are a snapshot of a game that had already been running, not clean assembler output. A fresh assembly has zeros in those variables, because nothing has set them yet; the shipped disk has whatever they held on the machine when it was saved. The code writes them before it reads them, so the zeros are harmless.
At the time I read that line, moved on, and waited for the actual game. It took me weeks to see that this was the most important thing in the project, and that nobody had asked for it. From then on, every claim about this game could be settled by comparing bytes. I wouldn’t have done it myself. I already had the binaries, and in eighteen years rebuilding them from source never seemed worth an afternoon.
For every format it went to the code that reads the bytes and worked backwards from that. The level loader is 1,652 lines of uncommented 68000, which is why I’d always reached for a hex editor instead.
A level is a grid of tiles: a long list of numbers, where each number means “put picture 47 here”, in my own private 1993 layout. This is the format the older model and I had ground through a year earlier.
Here is the full set of tiles a level is built from, 256 of them, 16×16 pixels each, for level one:

And a slice of level one, assembled from those tiles:

The input is a list of numbers with no header and no dimensions, inside a compressed chunk. This time I didn’t explain anything. It found the drawing routine, read how the grid was walked, worked out the width and height from constants elsewhere in the file, and produced correct maps for all five levels on the first attempt.
Then it re-rendered each level from its own extracted data and compared the result, pixel by pixel, against full-level captures I had made in 2020. Where they didn’t match, it went looking for the cause and found two copper effects: the sky gradient and the water colour cycle. With those two accounted for: five full-level images, zero differing pixels. Level one alone is 600 tiles wide, 9,600 pixels.
Drawing the level is only half of what a map cell does. Each cell is one 16-bit word, and the picture is the smaller part of it:
The property is the level’s invisible physics. 1 is solid ground. 2 and 3 can be climbed. 10 to 13 all mean “this hurts”, four codes because knockback needs a direction. 14 kills outright. 63 is a door. None of this is written down anywhere. It was recovered because two routines read the same word and each one reveals its own half: the draw loop masks off the low byte, and the collision check does the opposite:
Checkrmh hands the painful cases to a label called rmhEnjury , which is 1993 me spelling “injury”.
Those bits were painted in an editor. Before the game could be built I had to build the tool that builds it: MEDITOR.S , 1,254 lines of assembly, dated by its own header, in my 1993 English:
Four days in February 1993. Paint tiles with the mouse, pick a property number on the panel’s CURRENT FLAG counter, stamp it onto cells with PUT FLAG, and a flag view marks every cell carrying the selected number. While writing this post, I asked the model to run the map editor and get a screenshot. It assembled the 1993 source with a modern assembler, laid the shipped Level 2 data out in memory where the editor expects it, and booted the result in an emulator.

My own tool at thirty-three years old, editing the real Level 2, flag view on. CURRENT FLAG reads 0001, solid, and the ground you can stand on is marked while the decoration you walk through isn’t. The panel says 1994: the panel artwork is a separate bitmap file the editor loads, and the copy that survived is a later one than the February 1993 code.
The other name on the panel, Udai, was my partner in Mesopotamia Software, which is what we called ourselves. He was building a game of his own at the time. I wrote the editor, for both of us, but its design was worked out between us so one tool could serve both games. His game was never finished.
Enemies aren’t in the map. The world is stored one screen at a time, 25 tiles by 20, and every screen has a small table of the objects on it. My 1993 comments explain the markers:
An enemy is a row of words: a marker, a frame, a position inside its screen, then its behaviour. hiddenwallR-lrb is the crumbling-wall routine, attached as an offset from a base label, the same trick as the arrow thrower earlier. The words after it are parameters that mean whatever that routine wants them to mean. Nothing in the file says which word is which, so it found the routine that walks these tables every frame and let it name the fields, then converted every object in all five levels to world coordinates and checked them against the rendered maps.
Most of the data files scramble their 16-byte headers with a key stored inside the file, a 1993 trick to keep disk editors out. The retail loader, GAME.S , has no unscrambling step at all. It read that as a clue: GAME.S was written before the scrambling was added, so it is an older file. That clue is what later let it recover the lost two-disk retail set, from a sector map inside that same file.
Load a level’s tile map and there are holes where every door should be, with no door tile in them, open or closed. An 18-byte object record stamps them onto the map at runtime, a 1×4 tile column, from a table:
The map data says there’s no door. The level code says there is. For thirty-three years I’d have told you the map is the source of truth and doors are map data, and I’d never have looked. It held both facts, found the routine that reconciles them, and came back with the design: doors are drawn by code at runtime; they were never painted into the map in the editor. That’s why the obvious port of the level data produces a tower with doorways full of sky.
In every level, colour index 31 is the sky, and nothing in the tile art ever paints it. The tile atlas renders it transparent, and behind it the copper repaints the background colour on chosen scanlines to make a vertical gradient. The gradient sits in the level source as a plain list of colours. This is the entire sky of the second level:
Read down the list and the sky goes from pale blue to warm near the horizon.
The same 24 words, rendered. Left is the top of the screen.
The first rebuild missed it, and the levels looked fine. Flat, in a way I couldn’t name. The pixel comparison refused to go green, and the gradient went back in.

The diff that would not go green: white is every pixel the first rebuild got wrong, the copper's sky and water.
Amiga sprite sheets are planar (five separate 1-bit bitplanes in plane-major strips, plus a transparency mask), and all of that was worked out from the draw routines and the org arithmetic. Sheet sizes of the form frames * width * height * 2 * 5 are ambiguous: that 2 could mean double-width frames, or two stacked rows, one per facing direction. Both readings fit every byte in the file. It’s two facing rows; that was my choice in 1993.

Two stacked rows, one per facing direction, drawn frame by frame, not mirrored.
It flagged the ambiguity and asked.
