Promptima IV est une réimplémentation complète d'Ultima IV: Quest of the Avatar (version DOS)
en TypeScript, rendue avec three.js, jouable dans un navigateur, sur mobile et en application de bureau. Le moteur
n'a pas été réécrit de mémoire ni d'après un wiki : il a été reconstruit en lisant l'exécutable original, fonction
par fonction, par un agent d'IA qui pilotait lui-même les outils de rétro-ingénierie.
339fonctions originales citées par adresse dans le code
222offsets de données (DS) cités
~14 400lignes de TypeScript dans le moteur
0fichier graphique du jeu original dans le dépôt
Le point de départ
Ultima IV est distribué gratuitement par GOG sous forme d'un jeu DOS : un exécutable principal,
AVATAR.EXE, compressé avec EXEPACK, un TITLE.EXE pour l'introduction, et une poignée de
fichiers de données aux formats maison (cartes .ULT, dialogues .TLK, donjons
.DNG, graphismes EGA). Aucun code source. Les règles du jeu (formules de combat, prix, IA des monstres,
karma, sorts) n'existent qu'en assembleur 8086, compilé par Microsoft C en 1986.
L'objectif : un portage fidèle à la règle près, mais moderne dans sa forme (écran adaptatif, tactile, français,
graphismes et sons nouveaux), et propre juridiquement (aucun contenu original dans le code publié).
La démarche
1. Ouvrir le binaire
La première étape a été de défaire EXEPACK : l'en-tête est retrouvé derrière le point d'entrée, la
décompression (un RLE à l'envers) est réécrite, et l'image décompressée devient la référence. Une leçon est
arrivée tôt : un premier désassemblage fait sur le fichier encore compressé donnait des offsets faux
au-delà du premier bloc. Tout ce qui suit s'appuie sur l'image décompressée, où 1000:xxxx est
directement l'offset dans l'image et DS:x vaut 0xF0D0 + x, segment de données retrouvé
dans le code de démarrage de la bibliothèque C.
2. Lire, nommer, comprendre, avec REA
L'exploration du binaire s'est faite à travers REA, un serveur MCP de rétro-ingénierie : un
ensemble d'outils (décompilation, pseudo-code, références croisées, recherche de chaînes, lecture d'octets,
renommage et commentaires) que l'agent appelle directement depuis la conversation. Plutôt que de copier-coller des
listings dans un chat, l'agent ouvre la fonction qui l'intéresse, remonte ses appelants, suit une table en mémoire,
donne un nom à ce qu'il a compris et passe à la suivante.
Exemple typique : pour porter la fin de combat, l'agent part de la boucle principale du combat, trouve la
fonction de sortie, lit ses branches (fuite face à des monstres maléfiques ou non, victoire) et en tire les
variations de vertus exactes, puis vérifie les routines d'incrément et de décrément du karma qu'elle appelle,
avec leurs plafonds et leurs cas limites.
3. Porter en citant ses sources
Chaque règle portée cite dans un commentaire l'adresse de la fonction originale et des données qu'elle lit.
C'est la convention centrale du projet : quiconque doute d'une formule peut rouvrir le binaire à cette adresse et
vérifier. Les découvertes s'accumulent dans docs/RE_NOTES.md : carte des données globales, table des
fonctions, formules en pseudo-code, questions encore ouvertes.
// fin de combat : 1000:837A
// karma_inc / karma_dec : 1000:09F8 / 1000:0A17
// générateur aléatoire 16 octets à DS:0x8732 : 1000:1771
Là où le portage interprète au lieu de copier (temporisations qui dépendaient de la vitesse du processeur,
rendu 3D des donjons), le code le dit explicitement.
4. Extraire une fois, ne plus jamais lire l'original
Le moteur ne lit jamais les fichiers d'origine. Un outil d'extraction les convertit une fois en formats
modernes (PNG, cartes et dialogues JSON, tables, catalogues de textes). Le dépôt public garde les données (cartes,
dialogues, textes) mais aucun graphisme original : le jeu se joue tel quel avec les graphismes de l'édition 2026, et
qui possède le jeu peut extraire les graphismes de 1987 depuis sa propre copie. Le code source, lui, ne contient
aucun texte original, pas même en commentaire : il déclare seulement où chaque texte se trouve dans
l'exécutable (un offset), et l'extracteur parcourt ce même registre.
5. Vérifier par l'outil, pas par la confiance
Le générateur aléatoire original est reproduit à l'identique et peut recevoir une graine. Cela rend possibles
des tests unitaires sur les décodeurs de formats (exécutés contre les vrais fichiers quand ils sont présents) et
surtout des scénarios headless : un Chrome sans fenêtre pilote le jeu (marcher, entrer en ville,
descendre dans un donjon), capture l'écran et le compare à une référence. Une régression visuelle est détectée
sans qu'un humain rejoue.
L'intérêt agentique
La rétro-ingénierie d'un jeu entier, c'est surtout un énorme volume de petites recherches : quelle fonction
écrit cet octet, qui appelle cette routine, que contient cette table, ce nombre est-il un offset ou une constante.
Chaque question est simple ; leur nombre est ce qui décourage. C'est exactement le terrain où un agent change la
donne.
La boucle de travail. Un test qui échoue renvoie à la lecture du binaire, pas à une supposition.
L'agent tient les outils, pas seulement la conversation
Avec un assistant classique, l'humain fait l'aller-retour : il copie un listing, colle la réponse, relance
l'outil. Ici, l'agent appelle REA, lit le résultat, décide de la question suivante, écrit le code, lance les tests,
démarre le serveur, prend une capture et la regarde. La boucle question → preuve → code → vérification tourne
sans intermédiaire, et l'humain intervient là où son jugement compte.
La traçabilité devient automatique
Citer l'adresse de chaque règle est fastidieux pour un humain ; pour l'agent, c'est un effet de bord naturel
puisqu'il vient de lire la fonction. Résultat : plus de 300 fonctions et 200 offsets cités dans le code. Chaque
formule peut être contestée et revérifiée, ce qui est la seule façon de faire confiance à du code produit à cette
vitesse.
La mémoire est écrite, donc partageable
Un agent oublie entre deux sessions. La réponse a été de tout écrire : docs/RE_NOTES.md pour ce que
dit le binaire, CLAUDE.md pour les règles du projet (commandes, conventions, interdits), des notes de
mémoire pour les décisions et les pièges rencontrés. Ces documents servent autant à l'agent suivant qu'à un
contributeur humain : la rétro-ingénierie n'est pas enfermée dans une tête.
Les garde-fous sont des règles, pas des intentions
La contrainte la plus importante du projet est juridique : ne jamais republier le contenu original. Elle est
écrite noir sur blanc dans les instructions du projet (aucun texte original dans le source, pas même en
commentaire ; graphismes originaux et packs dérivés ignorés par git et exclus du site ; un hook qui refuse tout push
qui les contiendrait). L'agent les applique à chaque modification, y compris dans ce devlog.
L'humain garde la direction et le goût
L'agent est rapide et méthodique ; il n'a pas de goût propre. Les choix qui comptent ont été humains : refuser
les tuiles générées par diffusion locale (formes perdues, animations incohérentes), écarter le simple
agrandissement pixel-art (fidèle, mais toujours 1987), choisir une refonte moderne en 32 px avec PixelLab, animer
soi-même les personnages, décider du nom, de l'écran adaptatif, du mobile. L'agent explore, propose, mesure ;
l'humain tranche et joue.
Ce qui reste difficile
Ce qui dépendait de la vitesse du processeur en 1987 n'a pas de « bonne » valeur : c'est interprété et
signalé comme tel.
Le décompilateur se trompe parfois (constantes négatives qui sont en fait des offsets, segments mal
résolus) : il faut relire l'assembleur.
Un agent peut affirmer avec aplomb une règle mal lue. Les tests et les scénarios existent pour ça ; les notes
gardent une liste de questions ouvertes plutôt que de les maquiller.
Et maintenant
Le jeu est complet, de la création du personnage à la fin du jeu, en anglais et en français, jouable sur
promptima.app, et son code est ouvert sous licence MIT sur
GitHub. Le prochain chantier repense l'interface sur
téléphone. Le journal raconte les étapes une par une.
DEVLOG · OCTOBER 2026
Replaying 1987 by reading the binary
Promptima IV is a complete reimplementation of Ultima IV: Quest of the Avatar (DOS
version) in TypeScript, rendered with three.js, playable in a browser, on a phone and as a desktop app. The engine
was not rewritten from memory or from a wiki: it was rebuilt by reading the original executable, function by
function, by an AI agent that drove the reverse-engineering tools itself.
339original functions cited by address in the code
222data (DS) offsets cited
~14,400lines of TypeScript in the engine
0original game graphics files in the repository
The starting point
Ultima IV is given away by GOG as a DOS game: a main executable, AVATAR.EXE, packed with EXEPACK, a
TITLE.EXE for the introduction, and a handful of data files in custom formats (.ULT maps,
.TLK dialogues, .DNG dungeons, EGA graphics). No source code. The rules of the game
(combat formulas, prices, monster AI, karma, spells) exist only as 8086 assembly, compiled by Microsoft C in
1986.
The goal: a port faithful down to the rule, but modern in form (responsive screen, touch, French, new graphics
and sounds), and legally clean (no original content in the published code).
The approach
1. Opening the binary
The first step was undoing EXEPACK: the header is found behind the entry point, the decompression (a backwards
RLE) is rewritten, and the unpacked image becomes the reference. A lesson came early: a first disassembly made on
the still packed file had wrong offsets past the first block. Everything after that relies on the unpacked
image, where 1000:xxxx is directly the image offset and DS:x is 0xF0D0 + x, a
data segment found in the C library's startup code.
2. Reading, naming, understanding, with REA
The binary was explored through REA, a reverse-engineering MCP server: a set of tools
(decompilation, pseudo-code, cross-references, string search, byte reads, renaming and comments) that the agent
calls directly from the conversation. Instead of pasting listings into a chat, the agent opens the function it
cares about, walks up its callers, follows a table in memory, names what it has understood and moves on to the
next one.
A typical example: to port the end of combat, the agent starts from the combat main loop, finds the exit
function, reads its branches (fleeing from evil or non-evil monsters, victory) and derives the exact virtue
changes, then checks the karma increment and decrement routines it calls, with their caps and edge cases.
3. Porting while citing sources
Every ported rule cites, in a comment, the address of the original function and of the data it reads. That is
the project's central convention: anyone who doubts a formula can reopen the binary at that address and check.
Findings pile up in docs/RE_NOTES.md: global data map, function table, formulas in pseudo-code,
questions still open.
// end of combat: 1000:837A
// karma_inc / karma_dec: 1000:09F8 / 1000:0A17
// 16-byte random generator at DS:0x8732: 1000:1771
Where the port interprets instead of copying (timings that depended on CPU speed, the 3D look of the dungeons),
the code says so explicitly.
4. Extract once, never read the original again
The engine never reads the original files. An extraction tool converts them once into modern formats
(PNG, JSON maps and dialogues, tables, text catalogs). The public repository keeps the data (maps, dialogues,
texts) but no original graphics: the game plays as is with the 2026 edition's art, and anyone who owns the game can
extract the 1987 graphics from their own copy. The source code holds no original text, not even in comments: it
only declares where each text lives in the executable (an offset), and the extractor walks that same
registry.
5. Verify with tools, not trust
The original random generator is reproduced exactly and can be seeded. That makes possible unit tests on the
format decoders (run against the real files when they are present) and above all headless
scenarios: a windowless Chrome drives the game (walk, enter a town, go down a dungeon), captures the screen
and compares it with a reference. A visual regression is caught without a human replaying.
Why the agentic part matters
Reverse engineering a whole game is mostly a huge volume of small lookups: which function writes this byte, who
calls this routine, what is in this table, is this number an offset or a constant. Each question is simple; their
number is what wears people down. That is exactly where an agent changes the game.
The working loop. A failing test sends the agent back to the binary, not to a guess.
The agent holds the tools, not just the conversation
With a classic assistant, the human does the round trips: copy a listing, paste the answer, rerun the tool.
Here, the agent calls REA, reads the result, decides the next question, writes the code, runs the tests, starts the
server, takes a screenshot and looks at it. The question → evidence → code → verification loop runs with no
middleman, and the human steps in where judgement matters.
Traceability comes for free
Citing the address of every rule is tedious for a human; for the agent it is a natural side effect, since it
has just read the function. The result: over 300 functions and 200 offsets cited in the code. Every formula can be
challenged and checked again, which is the only way to trust code produced at this pace.
Memory is written down, so it can be shared
An agent forgets between sessions. The answer was to write everything down: docs/RE_NOTES.md for
what the binary says, CLAUDE.md for the project rules (commands, conventions, prohibitions), memory
notes for decisions and pitfalls met along the way. These documents serve the next agent as much as a human
contributor: the reverse engineering is not locked in anyone's head.
Guardrails are rules, not intentions
The project's most important constraint is legal: never republish the original content. It is written down in
the project instructions (no original text in the source, not even in comments; original graphics and derived packs
ignored by git and left out of the site; a hook that refuses any push containing them). The agent applies them on
every change, this devlog included.
The human keeps direction and taste
The agent is fast and methodical; it has no taste of its own. The choices that mattered were human: rejecting
tiles generated by local diffusion (shapes lost, inconsistent animations), setting aside plain pixel-art upscaling
(faithful, but still 1987), choosing a modern 32 px redesign with PixelLab, animating the characters by hand,
deciding the name, the responsive screen, the phone layout. The agent explores, proposes and measures; the human
decides and plays.
What is still hard
What depended on CPU speed in 1987 has no "right" value: it is interpreted and flagged as such.
The decompiler is sometimes wrong (negative constants that are really offsets, badly resolved segments): the
assembly has to be read.
An agent can state a misread rule with confidence. Tests and scenarios exist for that; the notes keep a list
of open questions instead of papering over them.
What now
The game is complete, from character creation to the endgame, in English and French, playable on
promptima.app, and its code is open source under the MIT license on
GitHub. The next step rethinks the phone interface. The log tells the steps one by one.