OPENU5

Verificación · qué se ha comprobado, y qué no

Cómo se comprueba que esto es Ultima V, y no algo que se le parece

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.

Todas las cifras de esta página se midieron el 4 de agosto de 2026 sobre el árbol de código d48066e0, corriendo el comando que aparece a su lado. Las tres que NO son medición propia están marcadas como tales, con su fuente.

El punto de partida

«Verificado contra el binario» no es una forma de hablar

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.

Ese 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 de cada 61 — el 30 es 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.

Nivel 1 · Verificado en ejecución

La regla se derivó del ensamblador y además se cotejó valor a valor contra el binario corriendo de verdad, dentro de un DOSBox sin interfaz al que se le siembran valores en RAM y se le ponen puntos de ruptura. Es el nivel más alto y el más caro. Lo alcanzan seis subsistemas: la fórmula de combate, la órbita del generador de aleatorios, el viento turno a turno, las fases lunares, la semilla de la gitana y el reloj/comida/movimiento.

Nivel 2 · Paridad de dos modelos

La regla se implementa dos veces: una predicción en Python escrita directamente desde el ensamblador, y el motor de verdad en TypeScript. Ambas tienen que producir 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.

Nivel 3 · Divergencia deliberada

Todo lo demás: valores del original que aún no se han medido, presentación que no es lógica de juego, decisiones de alcance. Catalogado, clasificado por clase y con su vía de cierre escrita. No está escondido: está en un fichero que se publica, y está más abajo en esta misma página.

La diferencia entre esto y «lo probé y parece que va» es que los tres niveles son comprobables por cualquiera que tenga una copia del juego y una tarde libre.

El mapa del binario

202.800 bytes de 202.800: cero sin adjudicar

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.

Salida del comando «python3 re/tools/ledger.py» en una terminal: una línea por módulo del binario original — NPC.OVL 4912/4912, SHOPPES.OVL 5936/5936, SJOG.OVL 8800/8800, TOWN.OVL 6256/6256, ULTIMA.EXE 34544/34544, ZSTATS.OVL 4880/4880 — todas al 100,0 %, y una línea final TOTAL 202800 / 202800 100.0% (0 bytes pendientes).
Salida real de 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 ficheros de overlay. Dentro hay código de funciones identificadas, tablas de datos, depósitos de texto, estado de ejecución — y 147 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.

Esta cifra es la que más se malinterpreta, así que va con su límite pegado: el 100 % no significa «lo entendemos todo». Significa que no queda ni un byte sin clasificar. Es el mapa completo del territorio, no la conquista completa del territorio — y la conquista es lo primero de la lista de huecos del final de esta página.

Las cifras, con su comando

Cada número de aquí se produjo corriendo la columna de la derecha

Medidas el 4 de agosto de 2026 sobre el árbol d48066e0. Si alguna está mal, el comando lo dice — y 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 5.357 pasan · 15 omitidos · 388 ficheros · 39 s npx vitest run
…de ellos, los que corren SIN ninguna copia del juego 3.907 pasan · 290 ficheros · 28 s npx vitest run -c vitest.pure.config.ts
Tests del extractor de datos 203 pasan · 25 ficheros npm test (en extractor/)
Paridad modelo↔motor (nivel 2) 273 pasan · 11 deseleccionados · 150 s python3 re/tools/parity_all.py
Pruebas de navegador DECLARADAS (no ejecutadas hoy — ver los huecos) 259 en 67 ficheros + 112 del gran recorrido en 47 npx playwright test --list
Salas de mazmorra selladas 112 / 112 · 0 en cola (7 mazmorras × 16) python3 tools/estado-plan.py
Notas de ingeniería inversa 651 documentos ls re/notes/*.md | wc -l

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.

El censo de rutinas (leído de un artefacto, no medido aquí)

Estas seis cifras NO las he corrido yo: las he leído de re/ledger/frontier.json, el artefacto que genera la herramienta de censo. Se distinguen a propósito de las de arriba.

Del censo de rutinas Cuántas
Rutinas de código en el binario885
Leídas cuerpo a cuerpo, instrucción a instrucción422
Rutinas de juego pendientes de esa lectura298
Drivers de hardware aplazados con razón declarada (vídeo EGA, altavoz, disco)163
Bloqueadas para verificar, con el bloqueo escrito5
Sin explicación · sin nombre · con nombre inventado0 · 0 · 0

Las tres últimas son la propiedad que de verdad importa: donde no se sabe algo, hay una línea diciendo qué no se sabe. Las 298 tienen su propio apartado abajo.

Nivel 3, con nombre y apellidos

Las cinco clases de divergencia deliberada

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.

Fuente: re/deliberate-divergences.md, el documento que viaja con el código.
Clase Qué es Qué falta para cerrarla
A Paridad de stream en vez de ejecución. La regla está derivada del ensamblador y cruzada entre dos modelos independientes, pero nunca se careó contra el binario corriendo. Aquí están la magia, la mazmorra, los NPC, las tiendas, el transporte, los santuarios, los comandos, la gitana y los bucles de turno. Para cada subsistema, una de dos: poder sembrar sus estructuras en la RAM del DOSBox sin interfaz, o haber leído el protocolo de teclas del comando que lo dispara. 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. Un paso de integración de interfaz por comando, cada uno guiado por su función pura ya verificada. No hay que volver al binario.
C Preguntas de oráculo. Valores concretos que el port fija con una fuente derivada —fórmula de diseño, ensamblador parcial— pero que aún no se han leído en vivo del original. Ejemplos: cuántos turnos dura una puerta abierta, o cómo escala el precio de los reactivos con el karma. Una medición. Cada una lleva anotada la dirección exacta donde poner el punto de ruptura; falta navegar el juego hasta ese estado y capturarlo. Aquí sí hay una medida pendiente, no sólo un cableado.
D Fronteras de alcance conscientes. Comportamiento del original que se decidió no portar, cada uno con su motivo escrito. El caso mayor: la importación de personajes de Ultima IV, derivada y documentada pero no implementada. Una decisión, no un hallazgo. Se cierran sólo si alguien quiere ese alcance; el trabajo de lectura del binario ya está hecho en los casos principales.
E Marcadores de trama. La capa de misión global se construyó con ubicaciones jugables derivadas en vez de las canónicas del binario, porque la migración se centró en el motor y las reglas. Las piezas mayores —las coordenadas reales de los fragmentos y del amuleto— ya se sustituyeron por las del binario. Re-derivar del binario lo que queda de la capa de trama, que es contenido de datos más que lógica. El juego es completable con lo que hay.
Índice del documento re/deliberate-divergences.md con las cinco clases de divergencia deliberada, cada una con lo que falta para cerrarla: Clase A, paridad de stream, no de ejecución (falta el careo contra el juego vivo); Clase B, cableado interactivo pendiente (falta conectarla al comando); Clase C, preguntas de oráculo (valores que el binario no fija: sólo se saben midiendo el original); Clase D, fronteras de alcance conscientes (decidimos no portarlo, y está dicho por qué); Clase E, marcadores de trama aún no re-derivados desde el binario.
El índice del documento tal cual está en el repositorio. La tabla de arriba es su contenido completo: la tabla informa, la imagen ilustra.

Un ejemplo de lo que hay dentro, para que se vea el nivel de detalle: en el original, mirar al sol de día 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 seleccionado: el ensamblador, en ese caso, 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 primer miembro del grupo y lo declara como aproximación. Ese es el tamaño de las cosas que se catalogan.

Determinismo

La lista de teclas es la partida

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. Eso está documentado en el proyecto y no lo he vuelto a medir hoy: exige la misma ventana con exclusión que la suite de navegador, y estaba ocupada (ver el hueco 5).

Esa propiedad ya está construida y en el juego: se puede grabar una partida y volver a reproducirla, con pausa, velocidad y salto. La grabación es la lista de teclas y un ancla del estado inicial, y todo eso vive en tu navegador.

huella del estado inicial  +  [tecla, nº de turno] × N  =  la partida entera

Lo que eso pesa, medido y no estimado: la codificación cuesta 2,02 bytes por tecla sobre una mezcla realista de movimiento, comandos y texto escrito — 7,9 KB por hora a cuatro mil teclas por hora. Para comparar con lo obvio: el clip de medio minuto que hay en la portada de este sitio pesa 2,9 MB por 34 segundos, o sea unos 300 MB por hora al mismo ritmo. Y la lista de teclas no es un vídeo: es la partida, que se puede volver a ejecutar, pausar, acelerar, o retomar desde el minuto diecisiete con otro final.

Lo que de verdad importa de esto 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.

Una advertencia que este proyecto se hizo a sí mismo: la primera prueba que comprobaba el determinismo salió verde con una tecla alterada. No era un fallo del motor: las dos partidas divergían durante quince pasos y volvían a converger, con la misma posición, el mismo turno y la misma semilla — el generador del original es una biyección de dieciséis bits y sus órbitas se reencuentran. Comparar el estado final era un verde falso. La prueba compara ahora la trayectoria entera, huella a huella, y exige que la divergencia aparezca exactamente en la tecla que se cambió.

Hay una consecuencia bonita en el mismo hilo: las tablas de récords de este juego 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á. Así que no hace falta confiar en un servidor: se publica la partida y cualquiera con su propia copia la reproduce y la confirma.

Lo que NO está verificado

Los huecos, con la misma letra que todo lo demás

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.

1 · El desenlace es el tramo con menos verificación de todo el proyecto

El estado del tramo, en una frase: el texto y la lógica del final están derivados y probados; el disparador real está derivado y el port no lo implementa; el aspecto visual no tiene ninguna comprobación automática.

Lo derivado y probado: el diálogo sale de los ficheros de texto del original; la aritmética de cuántos días llevas jugando, con el calendario britanniano de trece meses de veintiocho días, y la bifurcación por la caja de sándalo están en el arnés de paridad de nivel 2. Son 93 pruebas sobre esa lógica y ese texto (medidas hoy: npx vitest run tests/endgame*.test.ts tests/fiel-endgame*.test.ts).

El disparador, que es el fallo. El port arranca el final si llevas el amuleto, la corona y el cetro y llegas a la celda. Es una regla razonable y no es la del binario: el original lo arranca porque una sombra absorbe a un miembro del grupo —hay un centinela que se pone en ese momento—, lo que significa que el combate final no es un trámite antes de la escena, sino lo que causa la escena. El port todavía no lo hace así.

Sobre el crédito, porque la versión bonita de esta historia sería falsa: el empalme estaba identificado desde una auditoría previa; lo que faltaba era resolverlo y probar que es el único. El dato llevaba tiempo escrito, con el salto sin resolver, y un dato a medio resolver no llega a implementación. Lo que se hizo esta semana fue resolver el salto, encontrar un segundo llamador que no estaba, y demostrar que es el único punto de entrada y el único sitio de todo el binario que escribe ese centinela.

Es el mejor ejemplo que hay de por qué esta sección existe. El fallo llevaba meses ahí, verde en todas las pruebas, porque el recorrido automatizado comprueba que el port hace lo que el port cree que hay que hacer. Ninguna prueba mira si la regla que estás probando es la del binario. Eso sólo lo mira alguien volviendo al binario.

Qué falta: implementar el disparador real, con la condición que hace que la absorción arranque el final en esa celda y no en cualquier otro sitio. Esa condición se buscó y se demostró que no está en el código —ni en el llamador, que es único y genérico, ni en la rutina, leída entera, barriendo todo el overlay—, así que por eliminación vive en los datos del encuentro. Eso no cierra la pregunta: la mueve de «seguir mirando el desensamblado» a «hace falta el mapa real de esa celda». Un negativo comprobado no es un fracaso; es lo que evita que sigas buscando donde no está.

Procedencia, separada a propósito: «el port dispara por las regalías y por poder llegar a Doom» es medición propia, leída del código del port. «El original dispara por la absorción» es un hallazgo del trabajo de ingeniería inversa, citado como suyo, no re-verificado contra el binario por quien escribe esta página.

2 · Del aspecto visual del desenlace no hay ninguna comprobación automática

Ni una sola prueba compara lo que se pinta en el desenlace con lo que pintaba el original. La referencia son dos grabaciones de vídeo del juego de verdad, revisadas a mano —parándolas donde hacía falta— para levantar una lista de huecos que después se cerraron uno a uno contra el ensamblador. Eso es mucho mejor que adivinar y es estrictamente peor que un oráculo, porque un vídeo te dice lo que pasó una vez, no lo que pasa siempre.

La diferencia se ve con un contraste. Para verificar el viento se siembra un valor en la RAM del binario corriendo, se avanza un turno y se compara el número: si difiere, se sabe al instante. Con el desenlace eso no se puede hacer, porque para llegar ahí el binario tiene que haber poblado media docena de estructuras que sólo se llenan jugando.

Un ejemplo concreto de dónde se nota: el orden en que se deshacen los píxeles de la pantalla final es un calco exacto —polinomio 0xB400, un registro de desplazamiento de Galois, con el píxel (0,0) al final—, pero la velocidad a la que los ves está sacada con un cronómetro sobre ese vídeo, unos dos o tres segundos para los 64.000 píxeles. No hay constante que derivar: en el desenlace, el original va a la velocidad a la que le responde el hardware.

Qué falta: un oráculo capaz de llegar al desenlace en el binario, o una comparación automática de imagen contra frames del original. Hoy no existe ninguna de las dos, y por eso la respuesta honesta a «¿es idéntico?» aquí es «que yo sepa sí, y sé menos de esto que del resto».

El desenlace corriendo en OpenU5: la celda de Lord British en el octavo piso de Doom, con el suelo re-teñido de verde, Lord British en pie junto al trono y el grupo delante. El panel de texto dice «FOLLOW!» cries Lord British, as he extracts a small, red sphere from the wooden box. «Our worlds await!». Ninguna prueba automática compara esta pantalla con la del original.
Esta pantalla del port es exactamente la que no tiene red: se ha comparado a ojo contra un vídeo, nunca automáticamente.

3 · De 885 rutinas del binario, 422 están leídas cuerpo a cuerpo. Quedan 298

Que una rutina no esté en ese pase no significa que su comportamiento esté mal: significa que su regla se derivó por una vía más barata —por su llamador, por sus datos, por paridad de stream— y que nadie ha vuelto a leerla entera para confirmarlo. Es exactamente la clase de sitio donde se esconde un error sutil, y por eso el pase sigue.

Qué falta: 298 lecturas de rutina con cita exacta. Es trabajo lineal y sin misterio; lo que no se puede es dar por leído lo que no se ha leído.

4 · El Safari de verdad es un punto ciego

El arnés de pruebas de navegador corre sobre Chromium. Cuando emula un iPhone, emula un iPhone en Chromium. Un defecto que sólo ocurra en el Safari real hoy es invisible. Se sabe que el agujero existe; no se sabe qué hay dentro.

Qué falta: correr la suite contra WebKit real, en dispositivo o en un servicio de dispositivos, y ver qué sale.

5 · Las 371 pruebas de navegador de la tabla están DECLARADAS, no ejecutadas hoy

Esa suite necesita una ventana larga y exclusiva —levanta un servidor y juega partidas enteras—, y al medir para esta página estaba ocupada por otro frente de trabajo. Así que se contaron declaraciones (con --list), no verdes. Es una distinción diminuta, y es justo el tipo de distinción que hace que el resto de los números de esta página signifiquen algo.

Qué falta: nada estructural — una ventana de máquina. Se dice porque la alternativa era escribir «371 pruebas de navegador» a secas, que se habría leído como «371 en verde hoy».

Verificación de segunda mano

Cómo comprobar todo esto sin creerse nada

Estos son los comandos que producen las cifras de esta página. El primero y el sexto no necesitan navegador. 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              # sin datos del juego
npx vitest run                                       # 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 se publicará bajo licencia GPL-3.0; hasta ese día esta página describe un aparato de verificación que todavía no se puede correr desde fuera, y decirlo forma parte de describirlo con honestidad. Los datos del juego no se distribuyen ni se distribuirán: son de sus dueños.