Promptima IV

DEVLOG · OCTOBRE 2026

Rejouer 1987 en lisant le binaire

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 de l'agent, de la question du joueur jusqu'à la vérification Humain : « que se passe-t-ilquand on fuit un combat ? » REA : décompiler, référencescroisées, lire les tables Nommer et commenter dans la base,noter dans RE_NOTES.md Porter en TypeScript en citant1000:xxxx et DS:xxxx Tests Vitest, scénarios headlessà graine fixe Humain : jouer, juger, choisirla direction suivante écart → on relit En doré, les étapes humaines ; le reste est enchaîné par l'agent.
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

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.

Promptima IV est une réinterprétation non officielle d'un jeu de 1987, sans lien avec ses ayants droit · Mentions légales

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 agent's working loop, from the human's question to verification Human: "what happens whenthe party flees a fight?" REA: decompile, cross-reference,read the tables Name and comment in the database,write it down in RE_NOTES.md Port to TypeScript, citing1000:xxxx and DS:xxxx Vitest tests, seeded headlessscenarios Human: play, judge, pickthe next direction mismatch → read again Gold: human steps; the rest is chained by the agent.
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 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.