메뉴
HN
Hacker News • 22일 전

LLM으로 1993년 아미가 게임 어셈블리를 고돗(Godot)에 이식하기

IMP
7/10
핵심 요약

이라크 제재 시기 바그다드에서 순수 68000 어셈블리로 만든 아미가 게임 'Babylonian Twins'의 원작자가 Claude Code를 활용해 7만 줄이 넘는 어셈블리 원본을 분석하고 고돗 4로 이식한 사례입니다. 1년 전 모델은 레벨 포맷 해석에 여러 차례 힌트가 필요했지만, 최신 모델은 한 번에 힌트 없이 해석해내며 추론 능력의 발전을 보여줍니다. 에이전트형 코딩 도구가 레거시 코드 복원·이식에 실제로 활용 가능함을 입증한 사례로 주목받습니다.

번역된 본문

1993년, 바그다드에서 나는 Amiga 500으로 Babylonian Twins라는 게임을 만들었다. RAM 512KB, 하드디스크 없이 TV에 연결해서 쓰는 기계였다. 당시 나는 스무 살대의 공학도였다. 순수 68000 어셈블리로, 모든 스프라이트와 모든 스캔라인을 손수 만들었다. Murtadha Salman이 그림을 그렸고 Mahir AlSalman이 음악을 작곡했다. 우리는 제재 하에 있었다. 인터넷도, 게임 개발 자료도 없었고, Amiga 하드웨어 레퍼런스 매뉴얼 한 권이 전부였다. 나는 그 매뉴얼로 하드웨어를 직접 프로그래밍했고, 전기는 하루 몇 시간만 들어왔다. (메모리가 작아서) 끊임없는 플로피 디스크 교체와 섭씨 50도의 여름 때문에 디스크 드라이브를 세 번이나 망가뜨렸다. 왼쪽: 1993년, Amiga에서. 오른쪽: 2026년, 같은 관문 장면. Amiga에서 '손수 만든다'는 건 게임이 실행 중에 운영체제에 아무것도 요청하지 않는다는 뜻이다. 시작할 때 인터럽트 벡터를 저장하고, OS 인터럽트를 끄고, 기계 전체를 차지한다:

move.l #$dff000,a0 ; 하드웨어 레지스터 베이스 lea save(pc),a1 ; 시스템을 확보 move.w #$4000,intena(A0) ; AMIGA로부터

'AMIGA로부터 시스템을 확보'는 1993년에 내가 쓴 주석이다. 그 순간부터 화면은 게임 자체의 코퍼 리스트(Amiga의 프로그래밍 가능한 비디오 보조프로세서)로 처리되며, 스프라이트와 하늘 색상에 맞춰 실시간으로 다시 작성된다. 타일은 블리터(blitter)의 레지스터에 직접 값을 쓰고 완료 플래그를 기다리며 이동한다. 조이스틱은 하드웨어 포트에서 직접 읽고, 발사 버튼은 CIA 칩의 핀 하나다. OS는 레벨 사이에만 잠깐 돌아와서 디스크에서 다음 레벨 파일을 읽고, 곧 다시 꺼진다.

이것은 이라크에서 만든 최초의 상업용 게임이었고, 오랫동안 거의 아무도 플레이할 수 없는 게임이었다. Commodore가 무너지고 제재가 퍼블리셔들을 내쫓아서, 완성된 게임은 선반에 쌓여 있었다. 2008년에 한 Amiga 포럼이 내 동생의 유튜브 업로드에서 이 게임을 발견하고 디스크를 찾아내려고 나를 추적했다. 그 스레드는 아직도 남아 있다.

게임은 이전에 한 번, 2010년에 손으로 이식된 적이 있다. 같은 팀이 처음부터 직접 쓴 엔진으로 iPhone용을 다시 만들었는데, C++ 약 34,000 줄, 몇 달간의 밤과 주말이 걸렸다. Apple과 Google이 피처했고 200만 다운로드를 넘겼다. 그 이야기는 여기에 있다.

이번 이식은 내가 한 것이 아니다. 나는 요청만 했고, 결과물을 매일 밤 플레이했고, 어색한 점을 말했고, 1993년에 그 자리에 있던 사람만 내릴 수 있는 몇 가지 결정을 했다. 파일 포맷 해석과 어셈블리 분석은 AI의 작업이었고, 30년 된 코드를 어떻게 옮길지에 대한 결정들도 그랬다. 그리고 그 속도는 내가 따라가지 못할 정도였다. 이 글은 몇 주 뒤에 앉아서 내 게임에 무슨 일이 벌어졌는지 읽어본 기록이다. 일부는 틀렸고, 나는 몇 주 동안 눈치채지 못했다.

왜 다시 시도했나

예전에 시도한 적이 있다. 1년쯤 전에 이전 모델에게 같은 Amiga 자료를 주고 바이너리 레벨 맵을 해석해달라고 했다. 결국 해내긴 했지만 여러 라운드와 내 힌트가 많이 필요했다. 그러다 Claude Fable 5가 나와서 같은 파일을 줬다. 이건 의도된 테스트였다. 내 추측은 LLM 학습 데이터에 Amiga 어셈블리 코드가 거의 없으리라는 것이었다. 모델이 단순히 기억해내는 게 아니라 추론해서 풀어내는 능력이 더 좋아졌다면, 바로 여기서 드러날 것이다.

7월 4일 연휴가 다가오고 있어서, 이전 단계가 성공해야 다음 단계로 가는 조건부 세 단계를 계획했다. 1단계, 안전한 요청: 내 2010년 엔진, C++ 34,000 줄을 Godot 4로 옮기기. 이것이 대조군이다. 2단계, 불공평한 요청: 생산이 끊긴 기계를 위한 원본 68000 어셈블리 72,758 줄을 — 주석이라곤 거의 없고 C++과 공유점도 없는 — Amiga 원래의 50Hz로 Godot에서 다시 만들기. 3단계, 욕심 많은 요청: 두 번째 것을 첫 번째 안에 넣어서, 현대 게임을 사면 1993년 원작을 실행할 수 있는 보너스로 얻게 하기. 세 가지 모두 성공했다. 1년 전에는 여러 라운드와 내 수정이 필요했던 레벨 포맷이 이번엔 힌트 없이 한 번에 풀려나왔다.

어떻게 실행했나

Claude Code에서 실행해서 터미널과 내 파일시스템에 접근할 수 있게 했다. 파일을 편집하고, (프로그램을) 실행할 수 있었다.

원문 보기
원문 보기 (영어)
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: 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” 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 . 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 . 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. Why I tried again 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. How it was run 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: --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 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. Step one: 34,000 lines of C++ in an evening Wednesday night, the safe ask. 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’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. 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 matters because the original applies drag multiplicatively, every frame: static const float GROUND_DRAG_FACTOR = 0.85 f ; 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. 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. It didn’t use CharacterBody2D 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: # 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! Nothing tidied up the stray 0.49 . There are no tests and no docs; those comments are the spec. Step two: the 68000 assembly By Sunday afternoon, July 5th, I handed o