Verificación · qué se ha comprobado, y qué no
Un emulador ejecuta el binario de 1988 y no necesita entenderlo. Una reimplementación sí: cada regla del juego hay que leerla del ensamblador y luego demostrar que se leyó bien. Esta página cuenta cómo se demuestra, con los comandos que producen cada número — y dedica la misma prominencia a lo que hoy no está verificado.
Cada cifra de esta página lleva el árbol de código y la fecha de su propia medición, y no hay una fecha global: se producen corriendo el comando que aparece a su lado. Las cifras que cuestan minutos de máquina se sellan al cerrar cada tren de cambios, y cada una lleva el árbol y la fecha de su última medición: los tests unitarios del motor, los que corren sin ninguna copia del juego, la paridad modelo↔motor, los mismos, medidos en el repositorio privado, los tests del desenlace, el recuento del árbol que se publicaría y los jobs del CI público sobre ese árbol sobre 9412d99a1, el 1 de octubre de 2026. Las demás se derivan del árbol al construir esta página, así que llevan la fecha del sitio que estás leyendo. Las tres que NO son medición propia están marcadas como tales, con su fuente.
Cada regla portada vive en uno de tres niveles, y el nivel se declara siempre. Ninguna afirmación de esta página vale más que el nivel que tiene detrás.
Antes del detalle
La mayoría de las recreaciones de juegos antiguos se escriben de memoria: alguien juega al original, recuerda cómo se comportaba y programa algo que se le parece. Aquí no. El programa de 1988 sigue existiendo, y cada regla de OpenU5 se ha leído de dentro de ese programa — no de una guía, no del recuerdo — y después se ha comprobado contra él, con el grado de cada comprobación declarado más abajo.
La diferencia se nota en las cosas pequeñas. Los dados del juego original estaban ligeramente cargados: ciertos números salían el doble que otros — algo que nadie percibe en una tarde de juego, pero que decide miles de combates a la larga. Una recreación hecha de memoria pondría dados normales y jugaría «casi» igual. OpenU5 usa los dados cargados de 1988, porque los leyó donde estaban escritos.
Y además del programa están los testigos: cuatro partidas completas de Ultima V, jugadas y grabadas en vídeo por cuatro personas distintas — aulddragon, Alex Diener, Lord Fenton y un let's play en español. OpenU5 recorre esos mismos caminos y compara lo que va diciendo su pantalla con lo que decía la de ellos, momento a momento. Cuando algo no cuadra, no se ajusta a ojo: se vuelve al programa de 1988, se lee qué hacía de verdad, y gana el original.
Eso es lo que separa este port de una imitación: no pide que te fíes de nadie. Cada afirmación de esta página lleva al lado el comando que la reproduce, y lo que aún no está comprobado está contado más abajo con el mismo tamaño de letra que los logros. El resto de la página es el detalle técnico de todo esto. No hace falta leerlo para jugar; está para que no haga falta creer.
El punto de partida
La diferencia entre emular y reimplementar es dónde vive el conocimiento. En un emulador vive en el binario; aquí tiene que vivir en el código, escrito, y ser comprobable.
Un ejemplo pequeño y real. Cuando el juego decide si tu golpe acierta, tira un dado y lo compara con un umbral: acierta si tirada ≥ (defensa − ataque + 30) / 2, con la división redondeada hacia cero (COMBAT.OVL:0x14D6). Copiar eso es fácil. Lo que separa una reimplementación de una imitación es el dado.
Parece un rand(1,30) corriente y no lo es: el kernel lo calcula como max(1, rand(0,60) >> 1), así que no es uniforme — el 1 sale 4 veces de cada 61 y el 30 sólo 1, la mitad de probable que cualquier número de en medio. Un port escrito «de memoria» pondría un dado uniforme, jugaría parecido y sería incorrecto en cada tirada. Ese sesgo no se adivina: se lee.
Leerlo tampoco basta: hay que demostrar que se leyó bien. Por eso cada regla portada vive en uno de tres niveles, y el nivel se declara siempre. Los tres son comprobables por cualquiera que tenga una copia del juego y una tarde libre.
Derivada del ensamblador y cotejada valor a valor contra el binario corriendo, dentro de un DOSBox sin interfaz al que se le siembran valores en RAM y se le ponen puntos de ruptura. El nivel más alto y el más caro. Lo alcanzan seis subsistemas: combate, generador de aleatorios, viento, fases lunares, semilla de la gitana y reloj/comida/movimiento.
La regla se implementa dos veces: una predicción en Python escrita directamente desde el ensamblador, y el motor de verdad en TypeScript. Las dos tienen que dar la misma secuencia de tiradas y el mismo estado final. No es una aproximación, es una segunda derivación independiente — y destapó al menos tres errores reales que una sola implementación habría escondido.
Todo lo demás: valores del original sin medir, presentación que no es lógica de juego, decisiones de alcance. Catalogado por clase y con su vía de cierre escrita. No está escondido: está en un fichero que se publica, y más abajo en esta misma página.
El mapa del binario
El ledger de cobertura es la contabilidad del binario original: cada byte del ejecutable y de sus veinticuatro ficheros de overlay tiene que pertenecer a un segmento con nombre y con nota.

python3 re/tools/ledger.py. La última línea es la cifra: 202.800 de 202.800 bytes, 0 pendientes.Son 25 módulos: el ejecutable y sus veinticuatro overlays. Dentro hay código de funciones identificadas, tablas, depósitos de texto, estado de ejecución — y 146 bytes de relleno que el enlazador Phoenix dejó entre funciones, que también están adjudicados, porque «relleno del enlazador» es una respuesta y «no lo sé» no lo es.
Las cifras, con su comando
Las de la primera tabla se re-miden al cerrar cada tren de cambios, y una guarda automática las carea contra el árbol en cada aterrizaje: si una se queda atrás, el aterrizaje no entra. (Hasta el 1 de septiembre de 2026 esta tabla era una foto del 9 de agosto sobre el árbol fb1570cb, y tres de sus filas decían 6.085, 4.310 y 203 — cifras que ya no eran las del árbol.) Las de navegador y las de notas se derivan en cada construcción de esta página, así que llevan la fecha del sitio que estás leyendo. Sobre un árbol posterior darán otros números, que es exactamente lo que tienen que hacer.
| Qué | Cuánto | Comando |
|---|---|---|
| Cobertura del binario original | 202.800 / 202.800 B — 100 % | python3 re/tools/ledger.py |
| Tests unitarios del motor | 9.887 pasan · 20 omitidos · 730 ficheros | npx vitest run (en game/) |
| …de ellos, los que corren SIN ninguna copia del juego | 6.886 pasan · 652 omitidos (sin datos del juego) · 559 ficheros | npm run test:pure -w game (en el árbol público generado, node 22) |
| Tests del extractor de datos | 252 pasan · 29 ficheros | npm test (en extractor/) |
| Paridad modelo↔motor (nivel 2) | 275 pasan · 12 deseleccionados · ~80 s | python3 re/tools/parity_all.py |
| Pruebas de navegador DECLARADAS (censo con --list; el resultado, en los huecos) | 826 en 104 ficheros (4 configuraciones) | npx playwright test -c <config> --list (las 4, en game/) |
| Salas de mazmorra selladas | 112 / 112 · 0 en cola (7 mazmorras × 16) | python3 tools/estado-plan.py |
| Notas de ingeniería inversa que se publican | 562 de 1402 | python3 docs/publicacion/web/censo_notas.py |
La tercera fila es la interesante para quien no tenga el juego: esa suite corre sin un solo byte de datos de EA, y es la que puede correr una integración continua pública.
Estas cifras no las he corrido yo: se leen de re/ledger/frontier.json, el artefacto del censo, y se distinguen de las de arriba a propósito. La unidad es la fila y no la función —una fila cubre un rango de direcciones y lleva un nombre, y algunas tienen más de un punto de entrada—; llamarlas «rutinas» sería la cifra correcta con la etiqueta equivocada. Las mismas cifras, derivadas en cada construcción, están en el estado del port.
| Del censo de filas | Cuántas |
|---|---|
| Filas de código censadas en el binario | 885 |
| Con el cuerpo leído y verificado, instrucción a instrucción | 722 |
| De ellas, filas de JUEGO con el cuerpo leído (el resto del censo son drivers) | 721 / 722 |
| Filas de juego pendientes de esa lectura — y esa 1 no cierra nunca por estructura, así que el trabajo real pendiente es 0 | 1 |
| Drivers de hardware aplazados con razón declarada (vídeo EGA, altavoz, disco) | 163 |
| Verificadas cuya cita NO acredita lectura de cuerpo (banda, ver el hueco 3) | 20 – 29 |
| Con lectura PARCIAL declarada · con bloqueo declarado | 25 · 2 |
| Sin explicación · sin nombre · con nombre inventado | 0 · 0 · 0 |
Las tres últimas filas son la propiedad que de verdad importa: donde no se sabe algo, hay una línea diciendo qué no se sabe.
Nivel 3, con nombre y apellidos
Todo lo que el port hace distinto del original, o todavía no hace, está catalogado en un fichero del repositorio y clasificado por UNA pregunta: qué falta exactamente para cerrarlo. Una divergencia sin vía de cierre sería una excusa; con ella es una tarea.
| Clase | Qué es, y qué falta para cerrarla |
|---|---|
| A | Paridad de stream, no de ejecución. Derivada del ensamblador y cruzada entre dos modelos, pero nunca careada contra el binario corriendo: magia, mazmorra, NPC, tiendas, transporte, santuarios, comandos, bucles de turno. Cerrarla es trabajo de instrumental, no de re-derivación: la regla ya está fijada. |
| B | Cableado interactivo pendiente. Reglas ya portadas como funciones puras, con sus tests en verde, que todavía no cuelgan de una tecla: existen y funcionan, pero un jugador no las dispara aún. Falta un paso de interfaz por comando. No hay que volver al binario. |
| C | Preguntas de oráculo. Valores que el port fija desde una fuente derivada pero que nadie ha leído en vivo del original: cuántos turnos dura una puerta abierta, cómo escala el precio de los reactivos con el karma. Cada una lleva anotada la dirección del punto de ruptura. Aquí falta una medición, no un cableado. |
| D | Fronteras de alcance conscientes. Comportamiento del original que se decidió no portar, cada uno con su motivo escrito; el caso mayor es la importación de personajes de Ultima IV, derivada y documentada pero no implementada. Es una decisión, no un hallazgo. |
| E | Marcadores de trama. La capa de misión se construyó con ubicaciones jugables derivadas en vez de las canónicas del binario; las piezas mayores —los fragmentos, el amuleto— ya se sustituyeron. Falta re-derivar el resto: el juego se puede terminar de principio a fin con esas ubicaciones derivadas, no con las canónicas. |

El tamaño de las cosas que se catalogan: en el original, mirar al sol de día le quita un punto de vida al personaje que mira, y puede matarlo si le quedaba uno. Está portado. Lo que no se pudo derivar es a quién le toca el daño cuando no hay personaje activo — ahí el ensamblador copia un byte de trabajo del combate que no es un índice de personaje, y su sentido es indecible desde el volcado. El port usa el primero del grupo y lo declara como aproximación.
Determinismo
El núcleo del motor es puro: no lee el reloj, no lee el azar del sistema, no lee nada de fuera. Recibe un estado y una tecla, y devuelve un estado.
El azar del juego sale de un generador cuya semilla es parte de ese estado, calcado del original — que no es el generador lineal estándar del compilador de la época, sino uno de suma-rotación-xor sobre una palabra de dieciséis bits: x = ror16(semilla + 0x9248, 3) ^ 0x9248 + 0x11. De ahí sale una propiedad fuerte: mismo estado inicial + mismas teclas ⇒ misma partida, tecla por tecla y tirada por tirada. No «parecida»: idéntica.
Con su límite, que aquí también toca: el determinismo está medido, no demostrado. Lo que se comprobó es que la misma partida con la misma lista de teclas, corrida dos veces, da un estado idéntico byte a byte, el mismo turno y la misma semilla. Eso es una medición sobre una partida, no una prueba sobre todas.
Sobre ese determinismo descansa el aparato entero: el criterio con el que se da por bueno el gran recorrido automatizado es que sus ficheros de sello, regenerados dos veces, salgan byte a byte iguales. Está documentado en el proyecto y no se ha vuelto a medir hoy: exige la misma ventana con exclusión que la suite de navegador (ver el hueco 5).
La propiedad ya está en el juego: se puede grabar una partida y volver a reproducirla, con pausa, velocidad y salto. Una grabación es la lista de teclas más un ancla del estado inicial, y todo eso vive en tu navegador — no es un vídeo de la partida, es la partida, que se puede volver a ejecutar, o retomar desde el minuto diecisiete con otro final.
huella del estado inicial + [tecla, nº de turno] × N = la partida entera
Lo que de verdad importa no es el tamaño: es que «mándame tu registro» significa reproducir tu fallo exacto. No «no consigo reproducirlo». No «¿qué llevabas equipado?». Tu partida, en otra máquina, paso por paso. Y por el mismo hilo, las tablas de récords se pueden verificar entre pares: un servidor no puede comprobar tu récord porque comprobarlo exigiría ejecutar el motor, y ejecutarlo exige los datos del juego que el servidor no tiene ni tendrá. Se publica la partida y cualquiera con su copia la reproduce.
Lo que NO está verificado
Esta sección no es una nota al pie ni un apéndice: es lo que hace creíble el resto de la página. Un proyecto que sólo publica sus verdes no ha publicado nada.
El port arranca el final si llevas las tres regalías y llegas a la planta. Es razonable y no es la regla del binario. La del binario, leída instrucción a instrucción: quien pisa la fila 2 del combate teniendo un alma atrapada justo al norte, en su misma columna, es absorbido — y es esa absorción la que arma el centinela que carga la escena final. El combate no es un trámite antes del desenlace: es lo que lo causa.
Qué falta: portarlo. La regla ya está derivada y escrita; lo que el port no tiene es el combate de la celda, la absorción ni el centinela, y eso es trabajo de motor de combate que mueve el orden de los dados y por tanto exige su propia ventana. Falta además una medición pequeña y honesta: que las almas estén en la fila 1 del mapa de esa celda lo exige la regla y encaja con la grabación, pero el mapa no se ha abierto todavía.
Once instantes de la secuencia final —la sala re-teñida de verde, el diálogo del trono, la puerta lunar roja, la disolución, las seis pantallas de historia y el pergamino— se capturan del port y se carean, región por región, contra los fotogramas de la grabación del original. Siete salen limpios; los cuatro restantes quedan anotados como divergencia con su causa a la vista: el original no borra la banda de texto al pasar de página y deja restos de la anterior, y el port pinta la página limpia.
La comparación se corre a mano, porque necesita la grabación original, que no viaja con el código. Lo que sí corre en cada aterrizaje es su guarda: comprueba que cada instante apunta a una escena que el juego todavía produce, que ninguna región mide donde la grabación no llega, y —el diente— que los veredictos guardados llevan la huella de la definición con la que se midieron. Cambiar qué se compara sin volver a medirlo pone la puerta en rojo.
Qué falta: el careo píxel a píxel. La referencia es una grabación de pantalla, y se comprobó que no es una copia exacta de la imagen del juego sino una versión reescalada y comprimida: exigirle igualdad de píxeles compararía el ruido del compresor de vídeo. Para eso hace falta un oráculo que llegue al desenlace dentro del binario, y eso sigue bloqueado por lo mismo que el hueco 1. Lo que hoy se afirma es que el desenlace se pinta con el contenido, el color y la estructura del original; no que sea idéntico byte a byte. Y la comprobación empieza después de que el final arranque: cómo arranca es el hueco 1, y queda deliberadamente fuera de este marco.

El pase de lectura del binario está terminado. Hoy son 722 las filas con el cuerpo leído y queda 1 fila de juego pendiente, que no cierra nunca porque no tiene cuerpo que leer.
El hueco vivo es otro: «leída» es un sello, y el censo comprueba si su cita acredita una lectura instrucción a instrucción. Entre 20 y 29 de las 722 no la acreditan.
Qué falta: releerlas y dejar cita que acredite, o retirarles el sello — y no vale aflojar el criterio: el censo lleva un trinquete contra eso.
La suite móvil corre sobre Chromium y también sobre WebKit, el motor de Safari, con el mismo dispositivo y las mismas pruebas: la única variable es el motor, así que un fallo que salga ahí y no en Chromium es atribuible al motor y a nada más.
Un defecto real que esa red ya ha cazado, hoy abierto: la barra de modo apaisada pierde controles bajo WebKit que en Chromium sí monta.
Qué falta: el Safari de iOS entero, que es más que su motor. Fuera de esta red siguen: la barra del navegador que come alto y cambia al hacer scroll, las políticas de gesto que iOS exige para el sonido y la pantalla completa, y los cinco puntos de contacto que un iPhone declara y este arnés no. Eso pide un dispositivo de verdad.
El 2026-09-12, sobre el commit b083ad30f, se ejecutaron 820 de las 826 declaradas: 720 pasaron y 0 no.
La diferencia entre las 826 y las 820 no es un descuido: son las seis del arnés del espejo, que no es una suite de regresión. Compara una repetición del port contra grabaciones de partidas reales, y su umbral está por encima de la conformidad que el corpus alcanza de verdad — o sea que sin aflojarlo falla siempre, por instrumento. Sumarlas daría seis rojos que no significan nada, o seis verdes que tampoco. Se cuentan aparte porque son otra clase de prueba.
Y una cautela sobre ese 0, porque publicarlo a secas invitaría a leerlo como «0 defectos»: esta corrida tardó 144 minutos, y la misma suite sobre la misma máquina, sin nadie más trabajando, tarda 24. Bajo esa carga hay pruebas que caen por agotárseles el plazo y no por el defecto que vigilan — de hecho, entre las dos corridas una prueba dejó de fallar sola. Así que el 0 es una cota superior del número de defectos, no su recuento; separarlos exige releerlos uno a uno.
Qué falta: que esto deje de ser una foto. El recuento de arriba se deriva en cada construcción, pero el RESULTADO no puede: cuesta una ventana de máquina, así que caduca. Por eso va con su fecha y su commit, y por eso el ritual es repetible — node tools/suite-navegador.mjs desde game/ vuelve a producirlo entero.
Verificación de segunda mano
Los comandos que producen las cifras de arriba, juntos para copiar. El segundo es el único que corre sin ninguna copia del juego original: por eso es el que puede correr una integración continua pública.
python3 re/tools/ledger.py # cobertura del binario
npx vitest run -c vitest.pure.config.ts # (en game/) sin datos del juego
npx vitest run # (en game/) suite completa del motor
npm test # (en extractor/) extractor de datos
python3 re/tools/parity_all.py # paridad modelo↔motor
python3 tools/estado-plan.py # salas de mazmorra selladas
El código del motor está publicado bajo GPL-3.0-or-later en github.com/kokoima/openu5, así que este aparato de verificación se corre desde fuera: los comandos de arriba operan sobre el árbol real, no sobre una descripción de él. Los datos del juego no se distribuyen ni se distribuirán —son de sus dueños—, y por eso el segundo comando es el que puede correr cualquiera: es el único que no los necesita.