Zurück zum Archiv
#ai#agents#coding#llm#aigen

Ich gab einem KI-Agenten Amiga-Disketten. Nach einem Abend lief Pirates! in Unity

Ich habe einem Coding-Agenten vier Diskettenabbilder der Amiga-Version von Sid Meier's Pirates! aus dem Jahr 1990 gegeben und ihn gebeten, das Spiel nach Unity zu übertragen. Gearbeitet hat Claude Opus 5.5 in Claude Code. Nach einem Abend ließ sich das Spiel vom Titelbildschirm über ein Duell, eine Stadt und die Fahrt über die Karte bis zu einer Seeschlacht durchspielen, mit der originalen Grafik und dem originalen Ton. Wichtiger als das Tempo ist die Art, wie das Ergebnis geprüft wurde: Dasselbe Skript von Entscheidungen lief auf dem C#-Port und auf dem originalen Maschinencode, der in einem Prozessoremulator ausgeführt wurde. In 802 Durchläufen, etwa 6 Millionen verglichenen Zeilen, zeigten beide Versionen dem Spieler dasselbe.

Ich beschreibe hier die Etappen dieser Arbeit, weil sie zusammen eine Methode ergeben, die sich mit einem anderen Spiel und einem anderen Modell wiederholen lässt. Am Ende zeige ich ein zweites Projekt nach demselben Schema, den Port von Centurion: Defender of Rome von DOS nach JavaScript. Beide sind private Projekte zum Lernen: Die Originalspiele, ihre Grafik und ihr Ton bleiben urheberrechtlich geschützt, und ich verbreite sie nicht.

Der Titelbildschirm von Pirates! im Unity-Port: eine Galeone mit Piratenflagge im Sonnenuntergang, gezeichnet mit der Originalgrafik von 1990.

Der Titelbildschirm in Unity. Das Bild wird zur Laufzeit aus den Originaldateien des Spiels dekodiert.

Vom Diskettenabbild zum dekompilierten Code

Der Agent begann damit, die Dateien zu bestimmen. Ein Amiga-Diskettenabbild (ADF) ist eine rohe Kopie der Sektoren, daher genügten einige Dutzend Zeilen Python, um den Inhalt zu entpacken. Das Spielprogramm erwies sich als Datei mit Overlays: Ein Kern bleibt im Speicher, und drei weitere Codesegmente werden bei Bedarf von der Diskette nachgeladen. Ghidra, ein kostenloser Dekompilierer, unterstützt dieses Format nicht von Haus aus. Der Agent schrieb ein Skript, das alle Segmente an festen Adressen in einem einzigen Abbild anordnet und 45 indirekte Sprünge in die Overlays in gewöhnliche Aufrufe umwandelt. Damit sah der Dekompilierer rund 630 Funktionen als ein Programm.

Die Spieldaten brachten eine Überraschung. Die Dateien mit der Endung .rez haben den Aufbau von Macintosh-Ressourcen, denn die Amiga-Version war eine Portierung vom Mac. Nachdem das Format erkannt war, holte der Agent 210 Texte, 82 Zeichenkettenlisten, die Stadtdaten für sechs Epochen, Bilder, Animationen, Sprites und Klänge heraus. Die Texte erwiesen sich als beste Karte des Codes: Die Nummer einer Zeichenkette, die an die Routine „Text anzeigen“ übergeben wird, verrät, welchen Bildschirm des Spiels eine Funktion bedient.

Dreizehn Agenten lesen den Code, neun schreiben ihn um

Der dekompilierte Code umfasste knapp 29 Tausend Zeilen ohne Namen für Funktionen und Variablen. Der Hauptagent zerlegte ihn in Stücke von etwa 2 Tausend Zeilen und gab jedes einem eigenen Agenten mit demselben schriftlichen Auftrag. Jeder sollte in einer Datei alle Funktionen seines Abschnitts beschreiben: genaue Formeln, Konstanten, Zufallsbereiche, die Nummern der angezeigten Texte sowie gelesene und geschriebene globale Variablen. Der Auftrag enthielt einen Satz, der sich als wichtig herausstellte: Eine falsche Formel ist schlimmer als eine markierte Lücke. Die Agenten sollten also kennzeichnen, was sie nicht klären konnten, und Unklares im Assembler nachprüfen. Der Dekompilierer verlor hier viele Funktionsargumente, weil der 35 Jahre alte Compiler einen 16-Bit-Typ int verwendete.

Die zweite Etappe war genauso aufgeteilt. Neun Agenten schrieben die Spielregeln in C# um, jeder seinen Adressbereich. So entstanden rund 16 Tausend Zeilen Code, und der Spielzustand kam in ein Byte-Array, das genauso adressiert wird wie der Speicher des Originals. Elegant sieht das nicht aus, aber es beseitigt eine ganze Klasse von Fehlern. Der alte Code legt Strukturen frei übereinander und liest Tabellen ab dem Byte vor ihrem Anfang. Ein Port auf einem Speicherabbild lässt sich Zeile für Zeile übersetzen und mit dem Assembler vergleichen, und die Konstantentabellen bleiben dort, wo der Code sie sucht. Zeichnen, Ton und Steuerung wurden durch Aufrufe einer Schnittstelle ersetzt, die die Engine getrennt implementiert.

Das Orakel: der Originalcode in einem Prozessoremulator

Dekompilierten Code zu lesen beweist nicht, dass sich der Port wie das Original verhält. Der Agent baute deshalb ein Vergleichswerkzeug. Der originale Maschinencode für den Prozessor Motorola 68000 läuft im Emulator Unicorn. Die Systemroutinen, also Menüs, Textfenster, das Lesen der Steuerung und das Zeichnen, wurden durch Python-Funktionen ersetzt. Diese Funktionen beantworten jede Frage des Spiels nach einem einfachen Entscheidungsgenerator und protokollieren alles, was das Spiel gezeigt hat. Der C#-Port erhält dieselbe Folge von Entscheidungen über einen „Skriptspieler“, der außerhalb von Unity läuft. Am Ende werden beide Protokolle Zeile für Zeile verglichen. Die Texte enthalten Gold, Mannschaftsstärke, Daten und Städtenamen, sodass übereinstimmende Protokolle ein starker Test der Regeln sind.

Die erste vollständige Messung ergab 800 übereinstimmende Durchläufe von 802. Beide Abweichungen hatten eine Ursache: die Gleitkommaarithmetik. Das Spiel nutzt eine Softwarebibliothek, die nicht so rundet wie heutige Prozessoren. Ihre Quadratwurzel aus 81 beträgt 8,999999999999998, und das Spiel schneidet Ergebnisse auf ganze Zahlen ab, sodass die Moral der Mannschaft im Port bei bestimmten Verhältnissen von Gold zu Mannschaft um einen Punkt zu hoch ausfiel. Die Korrektur bestand darin, die Routinen der Bibliothek Befehl für Befehl umzuschreiben: 1572 Befehle, 43 Routinen und 21 701 vom Originalcode berechnete Testvektoren, ohne jede Abweichung. Nach dieser Änderung stimmen alle 802 Durchläufe überein, und in 120 weiteren wurde der Spielzustand Bild für Bild verglichen, im Duell, bei der Fahrt sowie in der See- und der Landschlacht.

Der Vergleich deckte außerdem zwei Fehler des Spiels von 1990 selbst auf: einen Zugriff auf die Karte über einen Nullzeiger beim diagonalen Angriff auf eine Stadt und eine Division durch null, wenn die Mannschaft auf null fällt. An solchen Stellen muss der Port ein Verhalten wählen, und das Vergleichswerkzeug hat einen Schalter, der das Original denselben Weg gehen lässt. Das Ergebnis hat auch Grenzen. Das Werkzeug vergleicht weder das Zeichnen noch den Ton, und seltene Pfade wie die Heirat, eine vollständige Schatzkarte oder die höchsten Ränge wurden nur schwach durchlaufen.

Darstellung und Ergänzungen, die es 1990 nicht gab

Die Schicht, die der Spieler sieht, wurde in Unity aus Code gebaut, ohne fertige Szenen. Bilder und Animationen werden aus den Originalressourcen dekodiert, Sprites erhalten ihre Farben aus der Palette des aktuellen Bildschirms, und die Musik wurde durch Emulation des vierkanaligen Sequenzers des Spiels in Audiodateien gerendert. Der Agent steuerte das laufende Spiel von der Kommandozeile aus, machte Bildschirmfotos und sah sie sich an. So zeigten sich mehrere Mängel, die kein Regeltest findet, zum Beispiel fehlender Ton in der Browserversion, weil die Audiokonvertierung das Programm ffmpeg brauchte, das nicht installiert war.

Die Karibikkarte im Unity-Port: Inseln mit Städten, das Schiff des Spielers mit Kurszeiger und Gewitterwolken.

Die Fahrt über die Karte. Der Hintergrund entsteht aus einem Geländeraster von 160 mal 200 Feldern und den originalen Kacheln.

Nach den ersten Spielversuchen habe ich um drei Änderungen gebeten. Die Frage nach dem Ankunftsdatum der Schatzflotte war im Original der Kopierschutz und verlangte das gedruckte Handbuch; jetzt bleibt das Quiz, aber die richtige Antwort steht in den „Notizen des Kapitäns“. Die Städteliste zeigte nur die Daten einer Stadt, weil das Spiel davon ausging, dass der Spieler eine gedruckte Karte hat; jetzt ist die gewählte Stadt auf einer Karte der Karibik markiert. Der Duellbildschirm bekam eine Tabelle mit Stärke, Moral und Waffe beider Seiten, angeordnet wie im Original. Die Ergänzungen lassen sich abschalten, und die Vergleichstests laufen ohne sie, damit die Protokolle weiterhin mit dem Code von 1990 übereinstimmen.

Ein Bildschirmfoto des Originalspiels auf dem Amiga: zwei Fechter vor einer Mauer, darunter der Titel des Duells und eine Tabelle mit Force, Morale und Weapon.

Das Duell im Original. Quelle des Bildschirmfotos: OpenRetro Game Database.

Das Duell im Unity-Port: Fechter auf einem Schiffsdeck, darunter der Titel und eine Tabelle mit Stärke, Moral und Waffe beider Seiten.

Derselbe Bildschirm im Port. Die Zahlen in der Tabelle werden laufend aus dem Zustand des Duells gelesen.

Eine Karte der Karibik mit der markierten Stadt Port-de-Paix, der Position des Spielers und einem Kasten mit den Stadtdaten und der französischen Flagge.

Eine Ergänzung gegenüber dem Original: die gewählte Stadt auf einer Karte der Karibik.

Centurion: dasselbe Schema in JavaScript und mit erweitertem Spielablauf

Das zweite Projekt ist Centurion: Defender of Rome, ein DOS-Spiel von 1990, übertragen nach JavaScript und auf das Canvas-Element, ohne Emulator. Die Schritte waren dieselben unter anderen Namen. Die ausführbare Datei musste zuerst entpackt werden, weil sie mit dem Werkzeug EXEPACK komprimiert war. Fünf Module des Spiels (Landschlacht, Gladiatorenarena, Wagenrennen, Seeschlacht und Vorspann) lagen in einer Overlay-Datei und teilten sich ein gemeinsames Datensegment. Der Zustand der Kampagne läuft auf einer lebenden Kopie der 64 Kilobyte dieses Segments, weil die Spiel-Engine auf 16-Bit-Überlauf, Ganzzahldivision und vorzeichenbehaftete Bytes angewiesen ist, die die Gleitkommazahlen von JavaScript nicht von selbst nachbilden. Der Spielstand stimmt Byte für Byte mit dem Originalformat überein, was die Tests prüfen.

Der Titelbildschirm von Centurion im Browser-Port: der Schriftzug CENTURION über einer Karte des Mittelmeers, eine Tafel mit dem Abspann und die Schaltflächen Start Game, Difficulty und Game Options.

Der Titelbildschirm von Centurion im Browser, mit der Originalgrafik.

Hier bin ich weiter gegangen als bei einem originalgetreuen Port. Ich habe einen Schalter für erweiterte Regeln hinzugefügt, der standardmäßig aus ist: das Zusammenlegen von Legionen und mehrere Legionen in einer Provinz, leichte Veliten, Kriegselefanten und Landungen von See. Den Code dieses Ports haben andere Modelle geschrieben als bei Pirates: überwiegend Gemini 3.8 Flash und zum Teil gpt-6.1-sol von OpenAI, das über Codex lief. Die Auswahl ergab sich aus den Werkzeugen, die ich zur Hand hatte, nicht aus einem Vergleich der Modelle.

Das Optionsmenü im Centurion-Port: der Eintrag Extra Features: OFF, der Schwierigkeitsgrad, Speichern und Laden, daneben das Senatsmandat mit Rang und Staatskasse.

Das Optionsmenü. Der erste Eintrag schaltet die erweiterten Regeln ein, die es im Original nicht gab.

Der Bildschirm der Provinz Italia im Centurion-Port: eine Ansicht der Stadt, das Spielemenü mit dem zu bauenden Amphitheater und der Zustand der Provinz: Stimmung, Tribut, Truppen.

Der Provinzbildschirm mit dem Spielemenü.

Die Probleme, auf die Gemini bei Centurion stieß

Die meiste Zeit kosteten Stellen, an denen der dekompilierte Code oder die Dateinamen etwas anderes nahelegten, als das Spiel tatsächlich tut. Fünf Beispiele aus den Projektnotizen:

  • Overlays ohne Daten. Die fünf Module des Spiels liegen in einer Datei, OVL0.OVL, und werden an dieselbe Adresse geladen, nutzen aber alle ein gemeinsames Datensegment. Der Code eines Overlays, für sich in Ghidra geöffnet, zeigte deshalb weder Zeichenketten noch Tabellen. Ein Skript fügte jedes Overlay mit dem Datensegment zu fünf synthetischen ausführbaren Dateien zusammen, und erst dann verband der Dekompilierer den Code der Minispiele mit den Strukturen der Kampagne.
  • Grenzen eroberter Provinzen. Eine Provinz nach der Karte der mit der Maus anklickbaren Bereiche zu umranden ergab kantige Linien, die nicht zur handgezeichneten Karte passten. Der Code zeigte einen anderen Mechanismus: Im Kartenbild hat jede Provinz einen verborgenen, ein Pixel breiten Pfad in einer dunklen Farbe, und eine Tabelle in den Daten gibt für jede der 27 Provinzen einen Startpunkt an. Das Spiel färbt diesen Pfad vom Startpunkt aus in der Farbe des Imperiums um, und der Port tut dasselbe.
  • Rückwärts springende Bilder. Die originale Spielschleife lief mit 9–12 Hz, ein Browser zeichnet 60-mal pro Sekunde. An drei Stellen sprang ein Sprite für einen Moment um ein Bild zurück. Der Globus im Vorspann dreht sich, indem 14 Palettenfarben in 10 Schritten verschoben werden, doch der erste Schritt in der Tabelle hat den Wert null; die Palette in jedem Schritt zu verschieben ließ die Kontinente einmal pro Umdrehung rucken. Beim Marsch der Kohorten und den Ausfällen der Gladiatoren musste die Reihenfolge genau nachgebildet werden: erst das Zwischenbild, dann der Positionswechsel.
  • Irreführende Dateinamen. Die Bitmaps BB1.CBM und BB2.CBM sahen nach Schlachtbannern aus, und oben auf den Bildschirm gezeichnet verdeckten sie das ganze Schlachtfeld. Der Code lädt sie nur, wenn der Rechner keine VGA-Karte hat: Es sind bildschirmfüllende Hintergründe für die älteren Karten EGA und CGA. Dieselbe Routine zeigte, dass die Aufstellung vor der Schlacht zwei Stufen hat. Die Wahl einer Formation ersetzt die Liste der verfügbaren Taktiken, und das Cannae-Manöver erscheint erst, nachdem der Keil gewählt wurde.
  • Kaum hörbare Musik. Nachdem 17 Stücke und 24 Instrumente des Treibers der AdLib-Soundkarte dekodiert waren, stimmten die Melodien, spielten aber mit etwa −55 dB. Das Disassemblieren des Treibers zeigte, dass er bei jeder Note ein Viertel ihrer Anschlagstärke von der im Instrument gespeicherten Dämpfung abzieht. Eine Codezeile brachte etwa 32 dB zurück. Ein zweiter Mangel lag auf der Seite des Browsers: Bei sehr kurzen Noten löschte das Abbrechen geplanter Lautstärkeänderungen in der Web Audio API den noch laufenden Anschlag und machte die Note stumm.

Eine Landschlacht im Centurion-Port: eine Liste von fünf Taktiken für die Keilformation, darunter Cannae Tactic, und auf dem Schlachtfeld aufgestellte Einheiten.

Die zweite Stufe der Aufstellung vor der Schlacht: die für die Keilformation verfügbaren Taktiken.

Eine eigene Arbeit waren die Wagenrennen im Circus Maximus. Ein Wagen ist dort kein einzelnes Bild. Eigene Bitmaps für jede Kombination aus drei Wagenklassen, vier Farben der Lenker, vier Aktionen, sechzehn Neigungen in der Kurve und drei Galoppbildern hätten über 2300 Bilder bedeutet, also bauten die Autoren des Spiels im Speicher einen Baum aus 16 Byte großen Knoten. Jeder Knoten verweist auf das nächste Element derselben Ebene und auf sein erstes Kind und enthält einen Bildindex, eine Tabelle von Verschiebungen für die aufeinanderfolgenden Animationsbilder und ein Sichtbarkeitsflag. Die Wurzel sind die vier Pferde. Der Wagenkasten hat eine zusätzliche horizontale Verschiebung, in die die Kurvenphysik eine Neigung von 0 bis 15 Pixeln schreibt, und der Lenker ist sein Kind und erbt diese Position. Deshalb neigt sich der Lenker in der Kurve zusammen mit dem Kasten, während die Pferde weiter in der Achse der Bahn laufen. Die Zweige mit der brechenden Achse und dem sich überschlagenden Wrack existieren die ganze Zeit; bei ihnen ist nur das Sichtbarkeitsflag ausgeschaltet.

Schema des Sprite-Baums des Wagens in Centurion: ein Knoten mit vier Pferden, der Knoten des Wagenkastens in drei Klassen, die Knoten der Unfallzustände, vier Animationen des Lenkers und der zusammengesetzte Wagen ohne Neigung und mit einer Neigung von 12 Pixeln.

Der Sprite-Baum des Wagens, aus den Spieldaten rekonstruiert. Das Schema entstand im Projekt aus den originalen, dekodierten Ressourcen.

Auch die Bahn ist nicht aus Kacheln gebaut. Das Spiel entfaltet 72 gespeicherte Koordinatenpaare zu 368 Kontrollpunkten und teilt die Position quer zur Bahn in 16 Spuren. Von der Spur hängt die sichere Geschwindigkeit in der Kurve ab, berechnet als (Spurnummer + 15) × 4: an der inneren Mauer beträgt sie 60, an der äußeren Bande 116–120. Die innere Spur ist der kürzeste Weg, aber wer oberhalb dieser Schwelle in die Kehre fährt, neigt den Kasten, wird nach außen gedrückt und löst eine Auslosung für einen Sturz aus, der umso wahrscheinlicher wird, je größer die Überschreitung ist. Die rote Markierung auf der Geschwindigkeitsleiste zeigt dem Spieler diese Schwelle.

Die Geometrie der Bahn des Circus Maximus: Geraden, Bremszonen und Kehren farbig auf dem Bild des Stadions markiert, dazu ein Diagramm der sicheren Geschwindigkeit, die von 60 auf der inneren Spur auf 120 auf der äußeren steigt.

Die 368 Punkte der Bahn und die sichere Kurvengeschwindigkeit je nach Spur.

Was von dieser Arbeit außer den Spielen bleibt

Die Methode habe ich als Skill aufgeschrieben, also als Satz von Anweisungen, den ein Agent lädt, wenn er eine ähnliche Aufgabe bekommt. Er enthält drei Grundsätze aus beiden Projekten. Die Spielregeln werden originalgetreu portiert, und die Darstellung wird durch eine Schnittstelle ersetzt. Der Port behält den Speicheraufbau des Originals bei. Dem Ergebnis wird erst vertraut, wenn es den Vergleich mit dem Originalcode besteht. Zum Skill gehören auch die Aufträge für die analysierenden und portierenden Agenten sowie die Skripte aus dem Pirates-Projekt.

Die interessanteste Folgerung betrifft die Fans, nicht die Agenten. Wer ein Spiel mag und darin seit Jahren etwas vermisst, kann es heute schnell und günstig nach eigenen Vorstellungen umbauen, ohne den Quellcode. In Pirates gibt das Quiz, das das gedruckte Handbuch verlangte, jetzt die Antwort vor, und eine Karte ist hinzugekommen, die das Spiel nie hatte. In Centurion habe ich Regeln ergänzt, die das Original nicht hatte: das Zusammenlegen von Legionen, Kriegselefanten und Landungen von See. Sobald die Spielregeln gewöhnlicher Code in C# oder JavaScript sind, ist die nächste Änderung eine gewöhnliche Codeänderung und kein Patchen der Binärdatei. Genauso lässt sich ein Spiel auf eine schnellere Engine oder eine andere Plattform bringen: Centurion läuft im Browser, und Pirates wird für Linux und für den Browser gebaut. Die Methode ist auch nicht an alte Spiele gebunden. Ein Dekompilierer, ein Prozessoremulator und der Vergleich der Protokolle funktionieren bei einem neueren Programm genauso; mehr Code bedeutet mehr Arbeit, kein anderes Vorgehen.

Quellen

  • Eigene Materialien aus beiden Projekten: Notizen zur Codeanalyse, Aufträge für die Agenten, der Bericht des Vergleichswerkzeugs (802 Durchläufe, Liste der gefundenen Abweichungen) und die Beschreibung des Centurion-Ports mit seinen Spezifikationen. Sie sind nicht veröffentlicht.
  • National Security Agency, Ghidra, das Reverse-Engineering-Paket, mit dem beide Spiele dekompiliert wurden.
  • Unicorn Engine, der Prozessoremulator, mit dem der originale 68000-Code ausgeführt wurde.
  • OpenRetro Game Database, die Seite zu Pirates! für den Amiga, Quelle des Bildschirmfotos aus dem Originalspiel.

Ich nutze KI-generierte Inhalte als Teil meines täglichen Lernprozesses.