In 1993, in Baghdad, I finished 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. We were under sanctions: no internet, no Amiga manuals — just one copy of the Amiga Hardware Reference Manual, which I used to program the hardware directly — electricity a few hours a day. The 50°C summers killed my disk drive three times.
“By hand” meant something specific on the Amiga. The game does not ask the operating system for anything while it runs: at startup it saves the machine’s interrupt vectors, switches the OS’s interrupts off, and takes the hardware for itself —
move.l #$dff000,a0 ;Base for hardware registers
lea save(pc),a1 ;Get the system
move.w #$4000,intena(A0) ;from the AMIGA
“Get the system from the AMIGA”: my comment, from 1993. From that point on the game is the machine. The display is its own copper list — the Amiga’s programmable video coprocessor — with the game rewriting entries on the fly for sprites and sky colors. Tiles are moved by writing the blitter’s registers directly and waiting on its done flag. The joystick is read from the hardware port; the fire button is one pin on a CIA chip. The operating system comes back only between levels, borrowed to fetch the next level’s files off the disk, then switched off again. There is no engine underneath; the game is the engine.
It was the first commercial game made in Iraq. It was also, for a long time, a game almost nobody got to play. Commodore collapsed, sanctions scared off publishers, and a 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.
This game has been ported once before, by hand, in 2010. That is the measuring stick for everything below. The 1993 team reunited (same artist, same musician, same coder) and rebuilt it for the iPhone on an engine written from scratch — about 34,000 lines of C++, a custom sprite batcher on OpenGL ES 1.1, hand-written pixel-perfect collision, because no engine of the day would hold a steady 60 fps for a 2D platformer of this kind. Months of nights and weekends. Apple featured it; when the Android build followed, Google did too. It reached over two million players. The game reached the top of Hacker News before it even launched, and years later Edge magazine told the whole story in “Made in Iraq”. (The full arc is here.)
When a port below takes an evening, it is being measured against those months, on the same game, at the same bar.
Let me be straight about my part, because it changes how you should read the rest. I did not do this port. I asked for it. I played the result every night and said what felt wrong. And I made the handful of calls that needed somebody who was actually there in 1993. The port itself — the file formats decoded, the assembly read, the decisions about how to carry thirty-year-old code across — was the AI’s work, and it happened faster than I could follow. This is what I found when I finally sat down and read what had been done to my own game: which parts turned out to be genuinely clever, and which parts were confidently wrong.
Why I tried again, and what I asked for first
I had tried this before. About a year ago I gave an earlier model the same old 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. I was the one supplying the insight; it was doing the typing. Useful, but not surprising, and I put the whole idea down again.
Then Claude Fable 5 shipped, and I gave it the same files.
The choice of test was deliberate. My assumption — I may be wrong about it; I have no way to check — was that there is very little 68000 game source in any training set. If the model was genuinely better at working things out rather than at recalling them, this is where the difference would show.
The July 4th long weekend was coming up, so there was time to go in three steps, each harder than the last, 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: I knew exactly what this task costs, because building the thing being ported had eaten months of my life.
Step two: the unfair ask. The original: 72,758 lines of 68000 assembly, for a machine that had already gone out of production. No comments to speak of. Nothing in common with the C++ whatsoever. Rebuild that in Godot too, running at the Amiga’s original 50 Hz.
Step three: the greedy ask. Now put the second one inside the first, so that buying the modern game gets you the 1993 original as a second thing you can launch, running as it always did.
All three worked. Step two is the one that matters: the level format that had taken several attempts and my corrections a year earlier came out in a single pass, with no hints from me. That is why I wrote this post. Not the speed. The fact that I was no longer the smartest part of the loop on my own material.
How it was actually run, because this matters
This was not files pasted into a chat window. I ran it in Claude Code, which means 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, not by describing how one might.
The part that stopped me is that it built its own instrument. Very early on it added a set of command-line flags to the game so that it could play it without me:
--level=<name> load a level directly
--pose=<spec> put the twins at exact positions
--drive=<spec> press buttons on a script, frame by frame
--probe dump switch / gate / door / key state
--screenshot=<path> render a frame and quit
Which turns “does the jump feel right” into something a machine can read:
drive[btw_jump:2.2] pos=(25.44, 24.04) vel=(0.00, -14.51) ground=false apex_y=22.48
Alongside that, 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. To be precise about what was not automated: there was no image comparison on the modern port — it took screenshots, and I looked at them — and no tooling covered whether the game felt right, which is half this post.
Step one: 34,000 lines of C++ in an evening
Wednesday night, the safe ask. It went absurdly fast. Timestamps, unedited:
22:23 Godot 4 project scaffold, asset sync, TMX level pipeline
22:44 both twins playable — collision, physics, camera, switching
23:19 all 38 entity types ported — full object roster live
00:35 full screen flow — menus, map, story, save, game flows
02:15 exporting to macOS, iOS and Android
Twenty-one minutes from empty project to a playable character. Every line it moved that night was a line I had written, over months, in 2010. I went to bed confused.
Getting it to feel right took about three days after that — jump arcs, trampoline timing, hit detection that rewards mashing, fixed in batches on July 2nd, 3rd and 4th. What took the two months was something else entirely, and I will come back to it.
I was not testing alone. My thirteen-year-old son played every build with me, with real excitement. He has always known I made this game — it is a fact about his father he grew up with — but he had never seen me working on it before. The testing turned into a father-and-son experience I did not plan, and it is one of my favorite parts of the whole project.
Reading it back, two decisions explain why an evening’s work was correct rather than merely fast.
Same units, same tick
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 sounds like a detail and is actually the whole ballgame, because the original applies drag multiplicatively, every frame:
static const float GROUND_DRAG_FACTOR = 0.85f;
this->velocity.x *= GROUND_DRAG_FACTOR; // every tick!
Multiply by 0.85 sixty times a second and you get one amount of friction; multiply fifty times a second and you get another. Same line of code, different feel. Port it to a different tick rate and every acceleration curve in the game silently changes. Nothing crashes, nothing looks broken, it just feels wrong forever and you will never 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 different games, two sets of hand-tuned numbers, each only correct at its own tick. It kept both clocks. I would have been tempted to tidy them into one.
It refused the engine it was porting to
Godot ships CharacterBody2D and move_and_slide(), and every tutorial tells
you to use them — they are overwhelmingly what the training data says a Godot
platformer looks like. The port used none of it 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 tiny fudge numbers I picked by feel fifteen years
ago, and the comments I wrote to my future self:
# Add 0.5 because we want the character's feet to be in the middle of the tile.
var bottom := pos.y + dim.y / 2 + 0.5 + i + fraction
if int(bottom) == int(pos.y + dim.y / 2 + 0.49):
continue
var right := pos.x
var left := pos.x - dim.x / 4 # asymmetric probes!
There is nothing an autocomplete likes more than tidying up a stray 0.49. It
left them exactly where they were, comments and all — in a codebase with no
tests and no documentation, those comments were the only specification that
existed.
The 38 entity types were then translated in parallel against one shared conventions document and a single hand-reviewed reference port. If anything here is worth stealing, it is that: do one by hand, extract the conventions, then fan out.
Step two: the unfair ask, and the first thing it did with it
By Sunday afternoon, July 5th, I was confident enough to hand over the thing I actually wanted to test: the original 68000 assembly. 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. There is 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 is no other copy.
It did not start porting. It started by making 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 — which survive as files saved out of the assembler’s memory in 1994. That detail will matter in a moment.
14:34 import the Amiga sources, assets, references
14:49 vasm toolchain reproduces the shipped binaries byte-identically
15:20 disk images rebuilt
15:42 the rebuilt demo boots and plays in FS-UAE
Fifteen minutes from a folder of files to the first rebuild that matched the
shipped bytes. Byte-identity out of thirty-year-old sources is not a matter of
typing the right command: 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 the CMPI
instruction, 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, because nothing would assemble until it
did.
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:
org $6a000 ; this section lives at address $6a000
Mapadd:
incbin"btwins:binary/L1/Map1.b" ;Game Map
org mapadd+73*1024 ; skip to 73 KB past the map's start
GLBtable:
dc.w $3333,50,20,100 ; one object record begins
dc.w SahamR-grb,26 ;Routine,Length
...
org glbtable+2*1024 ; the object table gets exactly 2 KB
Everything is a budget somebody once counted: the level-one map uses 74,400 of
those 74,752 bytes, a margin of 352, with nothing checking it. The only guard
was me, in 1993, counting. (SahamR-grb attaches an object’s behaviour as a
named offset — saham is Arabic for arrow. My labels were bilingual before the
game was.) And ASM-One’s org can also move the location counter backwards,
which vasm cannot; getting the workaround subtly wrong shifted every later
label by 0x3b0 bytes and quietly displaced the copper list. That is the kind of
error that produces a game which runs and is wrong.
Which is how the strangest thing in the whole rebuild surfaced. The check is signature-anchored rather than a checksum, and some chunks would not match, by about 108 bytes. Those bytes turned out to be runtime variables: the shipped files are not clean assembler output at all, they are snapshots of memory taken after the game had been running. Where a fresh assembly has zeros, the 1994 disk has whatever the game happened to be thinking at the moment someone saved it.
At the time I read the line, moved on, and waited for the actual game. It took me weeks to understand that this was the most important thing in the whole project, and that nobody had asked for it. From that moment on, every claim about this game could be settled by comparing bytes instead of by opinion. The warning everyone gives about AI is that it produces answers that sound right but are wrong, faster than anyone can check them. What I did not expect was for the thing itself to build the checking apparatus first, unprompted — I would not have done it myself: for eighteen years there was never an afternoon where rebuilding binaries I already had was the best use of the afternoon.
What the assembly still remembered
Reading back through what it did, one habit shows up everywhere, and I think it is the whole reason this worked: it never guessed at a format from the bytes. It went and found the code that reads them, and turned that code inside out. That is the obvious method, and any engineer knows the loader is the answer key; people reach for a hex editor instead because reading 1,652 lines of uncommented 68000 costs an afternoon you do not have. What changed is the price. When the tedious-but-correct method costs nothing, you take it every time, on every format, without the small negotiation a tired human has with themselves at 1am. That is not a smarter idea than mine. It is the same idea, executed without the discount I would have taken.
The levels, the exact thing I had failed at a year earlier
A level in this game is a grid of tiles: a long list of numbers, where each number means “put picture number 47 here”, stored in my own private 1993 layout that nothing else in the world understands. This is exactly the format the older model and I had ground through a year earlier — several rounds, my corrections at each one.
Here is the whole alphabet a level is built from — 256 tiles, 16×16 pixels each, for level one:
And here is a slice of level one, assembled from those tiles:
That picture is the output. The input is a list of numbers with no header, no dimensions and no documentation, inside a compressed chunk. This time I did not 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. First attempt. While I was doing something else.
Then it did something I had not thought to request. It re-rendered each level from its own extracted data and compared the result, pixel by pixel, against full-level reference images that had survived in my own archive — renders made years earlier with different tools. When they did not match, it did not adjust the maps to fit. It went and found the two missing display effects that explained the difference. Once those were in: five full-level images, every pixel of every one, zero differences. Level one alone is 600 tiles wide (9,600 pixels), so this is not a spot check.
That is the single clearest before-and-after I have. Same person, same files, same question. One year apart. The first time I supplied the insight and it supplied the labour. The second time I supplied the files.
The bits that know what hurts you
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:
one map cell, 16 bits:
bits 15..10 the property: what this square DOES (6 bits)
bit 8 which of the two tile banks to use (1 bit)
bits 7..0 which of the 256 tile pictures to draw (8 bits)
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 separate codes, because knockback needs a direction. 14 kills outright. 63 is a door. None of this is written down anywhere, and a hex dump will not give it up. It was recovered because two different routines read the same word and each one testifies to its own half: the draw loop masks off the low byte and ignores the rest, and the collision check does the exact opposite —
move.w (a1),d6 ; the same cell
and.w #$fc00,d6 ; keep the top 6 bits
lsr.w #2,d6
lsr.w #8,d6 ; d6 = the property, 0..63
bsr cbCheck ; 2 or 3? you can climb this
bsr Checkrmh ; 10..13? this hurts, and from which side
And one detail in there I had completely forgotten: Checkrmh hands the
painful cases to a label called rmhEnjury: 1993 me spelling “injury”. The
sources never let me forget it.
Those bits did not start life in a hex editor — they were painted. 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:
; ***********************************************************************
; * This Program was written in four days *
; * 1993-2-8/7/6/5 *
; * I made it to help me to make a map to my first serious *
; * Game *
; ***********************************************************************
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, so the invisible physics were visible while you worked. While this post was being written, the model brought the editor back: it assembled the 1993 source with a modern assembler, laid the shipped Level 2 data out in memory where the editor looks for 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 shows marked while the decoration you walk through does not. These bits are the same numbers I once clicked onto these cells, one at a time, with this mouse pointer.
The other name on the panel, Udai, was my partner in crime in Mesopotamia Software, which is what we called ourselves. He was building a game of his own at the time. I wrote the editor myself, for both of us, but its design was worked out between us so one tool could serve both games — that is what the credit is for. His game, unfortunately, was never finished. Mine is the one you are reading about.
Where the enemies live
The map knows what a tile does. It does not know where the enemies are. The world is stored one screen at a time, 25 tiles by 20, and every screen has a small table of the objects that live on it. My 1993 comments explain the markers, in my 1993 English:
; $1111=this is a Screen but it contain nothing or(End of Screen)
; $2222=this is an object but do not draw it (dead)go to next
; other=this is an object,draw it and go to the next
Scr0: dc.w $3333,50,23,17 ; a live object: frame, then x, y
dc.w hiddenwallR-lrb,20 ; its behaviour: a routine, as an offset
dc.w 0
dc.w 0
dc.w 7
dc.w 10 ; parameters only that routine understands
dc.w $3333,50,12,14
dc.w GreatTR-LRb,16,GkeyT-GTT,1
dc.w $1111 ; end of this screen
An enemy is not a thing. It is a row of words: a marker, a frame, a position
inside its screen, and 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 the extraction followed the same habit as everything else: find the
routine that walks these tables every frame, and let it name the fields.
Then the part I could not have kept in my head: an object’s place in the
world is its screen number unfolded into a grid — (screen % screens_per_row) * 25 + x — and screens-per-row is different in every
level. It worked that out from the drawing code, converted every object in
all five levels to world coordinates, and verified them against the
rendered maps.
The file that never caught up
Most of the data files scramble their 16-byte headers with a key stored
inside the file itself — a 1993 trick to keep disk editors out. The retail
loader, GAME.S, has no unscrambling step at all, and instead of “fixing”
the inconsistency, it was read as evidence: GAME.S predates the
scrambling; the format changed underneath it and that one file never caught
up. The source folder has layers the way an archaeological site does — a
file that speaks an older dialect of the format is an older file — and that
single inference is what later made the lost retail two-disk set recoverable
from a sector map buried inside it.
The doors are not in the map
I was sure they were — it is my game. Being wrong about it is what this section is about.
Load a level’s tile map and there are holes where every door should be. Not closed doors. Nothing at all. An 18-byte object record stamps them onto the map at runtime, a 1×4 tile column, from a table:
closed $528 $53C $550 $564 ; solid, blocks the way
open $129 $13D $151 $165 ; passable — exactly one sheet-column right
The map data says there is no door. The level code says there is a door. Those two facts contradict each other, and one of them has to be wrong. I know what I would have concluded, because I did conclude it, for thirty-three years: the map is the source of truth, doors are map data, and anything else is a detail. My certainty was the whole obstacle. It is very hard to look closely at something you already know the answer to.
It had no such certainty to defend. It held both facts, found the routine that reconciles them, and came back with the actual design: doors are drawn by code, not authored into the level. That is why the “obvious” port of the level data produces a castle with doorways full of sky.
Nobody remembered this. Not me, and I wrote it.
The sky that nothing paints
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, the Amiga’s little display co-processor, 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:
backgndcol:
col1: dc.w $09FF,$09FF,$09FF,$09FF,$09EF,$09EF,$0ADF,$0ADF
dc.w $0ACF,$0ACF,$0ABF,$0BBF,$0BBF,$0CBF,$0CBF,$0DCF
dc.w $0DCF,$0ECF,$0DCF,$0DCF,$0CCF,$0CCF,$0CDF,$0CDF
Read down the list and you can watch the sky go from pale blue to warm near the horizon. Nobody drew that. It is a table.
The same 24 words, rendered. Left is the top of the screen.
Finding a thing that is there is a search problem. Finding a thing that is not there — a colour slot that every level reserves and no level ever uses — means noticing that a gap is suspicious rather than empty. The first rebuild missed it, and the levels looked fine. Flat, in a way nobody could name, including me. The pixel comparison refused to go green, and the gradient went back in. This is where that first afternoon pays for itself.
The diff that would not go green: white is every pixel the first rebuild got wrong — the copper's sky and water.
The question it could not answer
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. Then it hit something that
could not be derived. 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 are consistent with every byte in
the file. It is two facing rows. I know because I chose it in 1993.
Two stacked rows, one per facing direction — drawn frame by frame, not mirrored.
It did not pick one. It flagged the ambiguity and asked, and that did more for my confidence than any of the successes above: where the information genuinely was not in the files, it knew the difference between a problem that is hard and a problem that is unanswerable. Knowing which one you are looking at is most of the skill.
The same twin, the same six frames: 1993 above, 2026 below.
That was the last format. From there the 1993 game went into Godot the same way the C++ had, behaviour rewritten in GDScript at the original 50 Hz, and the bugs it picked up on the way are catalogued below.
Step three: the old game inside the new one
The greedy ask took one evening, 21:58 to 23:43. The retro game runs as a guest: its own private namespace, its own scene host, and — the bit I like — the engine switches to 50 Hz on the way in and back to 60 on the way out. Nothing here is a discovery; it is engineering, tedious and full of small traps, done in one sitting, for a feature I had assumed would eat a week and probably get cut. It is the reason the Steam version ships with the 1993 game inside it: it turned out to be cheap.
The same doorway in both games, running in the same program. Left: 1993. Right: 2026.
One clarification, because it is the question I would ask. None of this is emulation. The data — the packed chunks, the planar graphics, the music — was decoded once, on my machine, by Python scripts, into ordinary PNG, WAV and JSON. The behaviour — how a guard patrols, when a door opens, the 50 Hz tick — was rewritten in the modern engine’s own language, and that is the only part running when you play. Not one line of code that understands an Amiga format survives into the game you download. If you want the real thing, that is the free disk image at the end of this post and an emulator.
Where it was confidently wrong
These posts usually skip this part, which is a shame, because the failures turn out to be more informative than the successes.
The guard you could not see
In level 2 you walk along a corridor. Waterfall to your left, stone pillar ahead, nothing else. No enemy on the screen, nothing approaching, and you take a hit. What hit you was a spear-carrying soldier standing thirteen tiles above you, on a grass ledge next to a palm tree, with a solid rock floor in between.
He is a doorman. He shoves whoever stands at his feet. In the original that check is fenced on both sides:
sub.w d1,d4 ; d4 = vertical distance to the kid
cmp.w #4,d4
bpl Sg.Far ; 4 or more rows below? not my problem
cmp.w #-2,d4
bmi SG.far ; too far above? also not my problem
The port kept the lower bound and dropped the upper one. A shove meant to cover the guard’s own three rows now ran the entire height of the map column beneath him, through the floor, into a corridor he does not appear in. Nothing looks wrong in the ported routine — every line that survived is a faithful translation. It is the line that did not survive that hurts you, and it hurts you somewhere the guard isn’t, so no amount of staring at the guard would ever have found it.
The doorman himself, from the 1993 sheet.
The others, quickly. The layer that must not be drawn: every level has a
second tile layer the original never renders — it is the hidden artwork
revealed when a door opens or a fake wall crumbles. Render it “faithfully” and
every secret passage stands open from the start. The order things update in
is not a detail: move enemies before players instead of after, and a
trampoline jump gets counted twice — twenty tiles into the air. One read too
literally: a door listed "p1,p2,p3,p4", meaning all four palms; read as a
single key with a strange name, the tutorial exit never opened. A level nobody
could finish, because of one comma. Maths on a wrong assumption: a
sound-loop length computed as stereo when the effects are mono; every sound
cut off halfway and restarted.
And one bigger one. I asked for something new in the 1993 build — let the twins swap places at any distance — so the proximity check came out, and the statues started corrupting. It went back into the routine and came out with the real answer, which was not the one either of us had assumed: that check was never a distance limit. The idle twin is stamped into the map itself as a statue, and two statues stamped on top of each other eat each other’s tiles. Not overlapping was the rule; being close was just how the code expressed it. The guard went back in, and I killed the feature on the 1993 build.
None of these are “AI is dumb” stories. They are exactly the mistakes a careful human porter makes; I made the same kind of mistakes myself, in 2010, slowly, over months. What changed is the clock: they all arrived in a single evening — and, because the checking apparatus already existed, so did finding them.
What the two months actually went on
The feel was right in three days. The two months went on fifteen years of complaints — every review I had never had the evenings to answer: proper saves with cloud sync and checkpoints, full gamepad support with rebinding, customizable touch controls. And eleven languages, which was hard for a reason worth one sentence: the 2010 menus were pictures, words painted into the artwork as pixels, so every screen had to be pulled apart and its text lifted out before anything could be translated — then Arabic, right-to-left with joined letters, in a game set in Babylon that had never been playable in Arabic. The Arabic I reviewed myself: I am the native speaker here. Friends reviewed the Chinese and the French; if you play in one of the other languages and something reads wrong, I want to hear about it. And the oldest debt of all: anyone who bought this game in 2011 gets the new one free, because the purchase is still looked up under the original 2011 product identifier, kept intact through every rebuild.
And then it shipped it, which was never the plan
Everything above is what I hired it for: reading code. Then the other 80% of releasing a game went the same way, and honestly that mattered more to me, because it is the part of the job I have always hated: screenshots at five pixel sizes in eleven languages, a preview video, store text, icons in six shapes, all uploaded to three stores that disagree about everything. I have shipped this game before. I know exactly how many evenings that costs.
The screenshots come out of the game itself — it launches the real game at each store’s exact pixel size, in the language it needs, walks the character to a chosen spot, and takes the shot, then draws the caption band with the game’s own fonts. There is a rule sitting in that pipeline that I keep thinking about: no AI ever renders the text in a store image. Real fonts, real translated strings, or it doesn’t ship. A rule about knowing what you are bad at, written by the thing it constrains — and when it broke, Russian and Korean captions coming out as rows of empty boxes, it broke visibly, before anything reached a store.
Steam has no publishing API. For iOS and Android there are proper APIs and it used them. Steam has a web dashboard and that is all, so it drove the browser: store page fields, achievements, the demo checklist, uploading artwork — clicking through Steamworks like a person. I do the login, and I press anything that submits, publishes, prices or releases. It fills in the forms.
And it reads my reviews. Google Play’s official API only returns the last seven days, which is useless for a game with fifteen years of history, so it pulls the rest through the public scraper, language by language, and sorts for the ones people actually see. Then it turned fifteen years of complaints into a plan: which reviews were describing real defects, and what would fix each one. I approved that plan, and most of it is the section above.
That is where the best bug of my year came from. A one-star review on Google Play. Bad spelling, the kind you scroll past:
“cant get through door on level one. opens but level dowsnt end”
That is not a complaint. That is an exact bug report, and it was right. In my game, opening the exit and walking through it are two separate actions, and the prompt that says so exists in all eleven languages — placed in exactly two of my eighteen levels. One of the levels missing it was the last free one. Which means the single player I most need to convince — the one deciding whether this game is worth paying for — was standing in front of an open door, with no way to know what to do, and concluded the game was broken. Reasonably!
Nobody found that in fifteen years — not me, not my testers, not two rebuilds. It took a stranger’s angry one-liner, and something willing to read it literally instead of dismissing it. The fix went out as 2.0.3 on both stores. I keep that review. It cost me a decade and a half of players, and the machine that found it did not know it was important. It just took the sentence seriously.
The one thing no model had
The bug I think about most did not look like a bug. “The trampoline feels too high.” That was the whole report, from me, playing the build at night. The constants checked out numerically: a twenty-line simulation of the original’s integrator predicted 19.1 tiles, and the build measured 19.5. The physics was right.
It was input semantics. The 2010 build was event-driven, and because of a workaround for a tvOS quirk we shipped years ago, a held jump button read as released until you physically pressed again. Godot polls input, and dutifully kept reporting the hold. Reproducing that one accident is what makes the high bounce require a fresh, well-timed press, which is exactly how the game played on a phone, and exactly what my hands were expecting.
That fact exists in no file. It is not in the 2010 source, because from the source’s point of view nothing unusual is happening. You would have to have been there, holding the phone, working around a bug in a television. No retrieval strategy reaches that. No context window is big enough, because the context is a memory. The model moved the code. The judgment about what makes it feel right was human, from 1993 and from 2010 — and so is every pixel of art (Murtadha Salman) and every note of music (Mahir AlSalman).
When I started trusting it
I did not trust it at the start. The first weeks I checked everything — I ran the disks, I read the diffs — because a machine claiming to have understood code I wrote as a student in Baghdad seemed like exactly the sort of claim that falls apart under examination.
What changed my mind was noticing what kind of mistakes it made. Everything it got wrong was wrong in a way that showed: a door that wouldn’t open, a level nobody could finish, a sound cut in half. What it did not do, not once, across two ports and a hundred thousand lines of source, was quietly produce something that looked finished and was hollow underneath. The failures were loud, and loud failures are survivable. And against them sat work I could not have done at all: the doors I was certain about, the sky I had forgotten the copper painted.
So my honest position, after a summer of this: I trust it the way I would trust a very fast colleague who has read everything, but has never played my game and never will. Enormous range. No taste. Absolutely no memory of what any of it was for. And worth checking, not because it is unreliable, but because checking it is cheap and the two of us are wrong about completely different things.
The original, released
After thirty-three years, the full original is out — free on itch.io — boot it in FS-UAE, WinUAE, or on real hardware. The Definitive Edition is on iOS and Android now, has a free demo on Steam, and the full Steam release (Win/Mac/Linux) lands this fall — including the original 1993 game as a fully playable second launch option.
One slice from each of the five levels, rendered from the extracted map data.
I don’t have a grand thesis about AI here. Just the honest shape of it: I asked for something, it was done faster than I could watch, and then I spent weeks reading back through the work to understand what had happened to my own game. Some of what I found was wrong. A lot of it was better than I would have managed. And a few things in there — the byte-identical rebuild nobody asked for, the doors nobody remembered, the one-star review nobody had taken literally in fifteen years — I could not have recovered on my own at any speed.
For the 1993 original — finished under sanctions, remade for phones in 2010, and only now released in full — that felt less like automation and more like somebody finally helping me carry it the last mile.
If you have old code of your own sitting somewhere, and if you are reading this, you probably do — the thing I would say is not that you should port it. It is that the reason you haven’t is probably not the reason you think. Mine sat in a drawer for three decades, and I always assumed the obstacle was the work. It took an afternoon to find out otherwise.
Babylonian Twins: Definitive Edition comes to Steam this fall — wishlist it here. Free demo available now · live today on iOS and Android · the original 1993 ADF is free on itch.