software 2026 · Windows · .NET 10 · GPL-3.0

PakuPort: portar juegos DOS a .NET y rehacerlos pieza a pieza con un juez que dice sí o no

🌐 Read this page in English →

Casi ningún juego de DOS tiene su código fuente disponible: se perdió, o nunca salió de la empresa que lo hizo. Lo único que queda es el ejecutable. PakuPort coge ese ejecutable y lo convierte en un programa .NET que juega al juego tal cual, sin tocarle un byte. Y después te ayuda a rehacerlo en C#, pieza a pieza, con un juez que compara cada pieza tuya con la original byte a byte y dice sí o no. Sirve para portar juegos DOS a .NET con IA o sin ella, es gratis y es libre (GPL-3.0). El proyecto vive en github.com/unmateria/PakuPort.

portar juegos DOS a .NET: un pequeño juego DOS de ejemplo corriendo como programa .NET después de un solo comando de PakuPort
CATCH, el juego de ejemplo que viene con PakuPort, corriendo como programa .NET después de un solo comando. Es un juego DOS escrito a propósito para el proyecto, así que se puede repartir sin problemas de derechos. Pincha para verlo grande.
Probar un juego porteado — gratis

Windows x64 · 34 MB · el juego de ejemplo porteado por PakuPort: se descomprime y doble clic en JugarCatch.exe, sin instalar nada

Descargar PakuPort — gratis

Windows x64 · 8 MB · el kit completo con la línea de comandos ya compilada · necesita el SDK de .NET 10 y Python 3.10

Código fuente, documentación y todas las versiones en GitHub · página de descargas (releases) · tutorial.

01 — Qué hace PakuPort: portar juegos DOS a .NET

PakuPort hace dos cosas, y conviene separarlas porque sirven para cosas distintas.

La primera es el port. Le das el .EXE o el .COM de un juego y te devuelve un proyecto .NET que lo juega. Por dentro, ese proyecto lleva un procesador x86 escrito en C# que ejecuta los bytes originales del juego, y alrededor un PC de la época entero: el DOS, la BIOS, la tarjeta gráfica (VGA, EGA, CGA, modo texto, VESA), el teclado, el ratón, el joystick, el temporizador, el altavoz, la Sound Blaster y el chip de música FM. El resultado puede empaquetarse como un .exe de Windows de doble clic, sin instalar nada.

La segunda es el remake. Una vez que el juego corre dentro de PakuPort, PakuPort lo puede observar: qué funciones se llaman, con qué datos entran y con qué salen, qué código se ejecuta de verdad, quién pinta cada píxel y quién hace sonar cada sonido. Con todo eso prepara una carpeta por cada función del juego, con lo medido y un esqueleto en C# para rellenar. Tú escribes la función en C# (o se la encargas a una IA), y un juez la pone a prueba contra llamadas reales grabadas del juego original. Si sale verde, se enchufa al juego y a partir de ese momento esa parte la hace tu código; lo que todavía no has traducido lo sigue haciendo el original.

La mejor imagen que tengo para explicarlo es la de cambiar las piezas de un motor mientras está en marcha, con un banco de pruebas para cada pieza que se ha grabado del motor de verdad. No hace falta tener el remake entero para poder jugarlo: se juega desde el primer día, y cada pieza nueva que entra está comprobada.

02 — Por qué no basta con traducir el ensamblador

PakuPort no empezó así. Pasó por dos caminos que parecen obvios y que no funcionan, y entender por qué explica casi todas sus decisiones.

El primer camino fue traducir el ejecutable a otro lenguaje de una vez: leer el binario, separar el código de los datos y convertir cada instrucción. En los juegos de DOS eso se da contra un muro que no es de esfuerzo sino de teoría: saber qué bytes son código es indecidible. Hay tablas de saltos que sólo se resuelven al ejecutarse, código que se modifica a sí mismo, overlays que se cargan del disco a mitad de partida, ejecutables comprimidos y protecciones anticopia que se descifran solas al arrancar. Ningún análisis estático lo resuelve en general. Por eso el motor de PakuPort es un intérprete: ejecuta lo que ejecutaría la CPU, y todo eso simplemente funciona.

El segundo camino fue pedirle a una IA que reescribiera el juego en C# a partir del desensamblado. Falla siempre igual: inventa comportamiento y se salta partes sin decirlo. El código que devuelve tiene buena pinta, compila, y no hace lo mismo que el original. Lo peor no es que falle: es que no hay manera de saber dónde.

De ahí sale la regla central del proyecto: a quien traduce se le dan hechos medidos, no conjeturas, y cada respuesta la comprueba un juez. Y unas cuantas reglas más que se han ido aprendiendo a golpes: ningún juez vale hasta que se le ha visto decir que no; una comparación sin nada que comparar es un fallo, no un aprobado; y los fallos se arreglan en la plataforma, siguiendo la especificación del hardware, nunca con un parche para un juego concreto.

03 — Lo que hace bien

Y la prueba de que todo esto encaja: con uno de mis juegos de prueba, un arcade comercial de principios de los 80, el juego entero está rehecho en C# con este método, en 61 piezas. Una partida grabada de 398 millones de instrucciones —título, menú, todas las fases, fin de partida y tabla de récords— corre sin que el intérprete ejecute ni una sola instrucción del original, y la memoria y la pantalla salen idénticas byte a byte a las del juego original.

04 — Cómo funciona por dentro

El recorrido completo, de un ejecutable de DOS a un juego rehecho en C#, son seis pasos. Los cuatro del medio se repiten función a función tantas veces como quieras:

1 · Portar pakuport port JUEGO.EXE 2 · Grabar llamadas, capas, cobertura 3 · Tablero una carpeta por función 4 · Traducir tú, o una IA: respuesta.cs 5 · Juez VERDE o ROJO, byte a byte verde 6 · Enchufar A/B: 0 bytes de diferencia rojo: el juez dice dónde falla, y se vuelve a intentar lo no traducido lo sigue haciendo el original
El recorrido de PakuPort. El rojo no es un fracaso: es el juez señalando el byte exacto que no cuadra.

Grabar. Se juega una partida (o se deja que el juego juegue solo, que muchos tienen modo demostración) con tres grabadoras encendidas a la vez: una apunta las llamadas reales a cada función, otra apunta qué hace cada función con el hardware (si pinta, si suena, si lee el teclado o si es lógica pura) y la tercera apunta qué bytes de código se ejecutaron de verdad. Esa última es la frontera de lo desconocido: lo que nunca se ejecutó no tiene prueba posible, y el tablero lo dice en vez de disimularlo.

El tablero. Una carpeta por función. Dentro va el código original desensamblado, un esqueleto en C# y, en su cabecera, todo lo medido. Así es la cabecera de una función pequeña del juego de ejemplo (traducida y recortada):

// FUN_1000_189  —  entra en 0814:0189
// CÓMO SE LLAMARÍA:  SIN NOMBRE: no hay pruebas de lo que hace. Es la respuesta honesta.
// QUÉ HACE:  Capa: LÓGICA.  Código ejecutado de verdad: 17 de 17 bytes (100,0 %)
// CONVENCIÓN DE LLAMADA (observada en 8 invocaciones)
//   Conserva: BX CX DX SI DI BP DS ES     Cambia: AX     Vuelve con ret
// GLOBALES QUE TOCA
//   ds:[044Ch] 2 bytes ESCRITOS  vistos: 2F1D->F4BA, F4BA->ABDB, ABDB->1BD0, ...
// LA PILA, DONDE SE COMETEN CASI TODOS LOS ERRORES ...
// EL RITMO: CUÁNTAS INSTRUCCIONES HABRÍA GASTADO EL ORIGINAL ...
// SI DUDAS, PREGÚNTALE AL ORIGINAL:  pakuport oracle-run --game ... --func FUN_1000_189 ...

Fíjate en el nombre: ninguno. Una función que no llama a ningún servicio del sistema ni toca ningún puerto no deja pistas de para qué sirve, y PakuPort no se inventa una. Un nombre inventado lo lee como un hecho el siguiente que llega, persona o IA. Los nombres sólo se proponen con la prueba citada al lado: el servicio del DOS que llama, el puerto que toca, el texto que usa.

Traducir y juzgar. Esa función resulta ser el generador de números aleatorios del juego. Su traducción a C# es esto:

// random: seed = seed * 25173 + 13849, devuelto en AX.
public static void Ejecuta(Cpu c, Memory m, Original o)
{
    c.SP -= 2; m.WrU16(c.SS, c.SP, c.DX);      // push dx
    c.AX = m.RdU16(c.DS, Seed);
    c.DX = 25173;
    c.Mul16(c.DX);                              // DX:AX = AX * DX, con sus banderas
    c.AX = c.Add16(c.AX, 13849);
    m.WrU16(c.DS, Seed, c.AX);
    c.DX = m.RdU16(c.SS, c.SP); c.SP += 2;      // pop dx
    c.SP += 2;                                  // ret
    o.Instrucciones(8);
}

Y el juez la pone a prueba contra las ocho llamadas reales que se grabaron del juego:

  FUN_1000_189: VERDE 8/8 | ritmo EXACTO

Para comprobar que el juez sabe decir que no, se cambia una sola unidad de la constante (13849 por 13848) y se vuelve a juzgar:

  FUN_1000_189: rojo 0/8 (caso 0: memoria: 1 tramo(s), el primero @0x044C len 1: B9 -> BA)

Rojo en los ocho casos, y señalando el byte exacto: la semilla del generador, en la dirección 044C, vale BA donde tenía que valer B9. Ese mensaje es lo que se le devuelve a quien traduce, tal cual, para que lo arregle.

Enchufar. Las funciones en verde se meten en el juego. Que una función acierte en las llamadas grabadas no demuestra que el juego siga igual con ella dentro, así que el control A/B juega dos veces la misma partida, con y sin tus traducciones, la corta en la misma instrucción y compara la memoria entera y la pantalla:

VERDE  el arnes es determinista: 2 corridas identicas, 0 bytes de diferencia
VERDE  FUN_1000_189       se ejecuto 14 veces dentro del juego
VERDE  la memoria del invitado sale igual: 0 bytes cambiados
VERDE  la pantalla sale igual: 0 bytes cambiados
== CONTROL DE LAS PORTADAS: VERDE ==

Con la constante cambiada a propósito, el mismo control sale en rojo: 15 bytes de memoria y 294 de pantalla distintos, porque las estrellas caen en otras columnas. Eso es el método entero a escala de una función: hechos medidos a la entrada, un juez que sabe decir que no, y una prueba de que el juego no ha cambiado.

05 — Cómo se usa, paso a paso

Hace falta Windows, el SDK de .NET 10 (los ports se compilan) y Python 3.10 o posterior (las herramientas del remake). Hay dos maneras de tenerlo:

En los dos casos, lo primero es abrir un terminal en la carpeta y pasar la autoprueba, que recorre la cadena entera sobre el juego de ejemplo y tiene que acabar en verde:

python tools\selftest.py

Todas las órdenes se lanzan desde esa carpeta con .\pakuport (en cmd, pakuport a secas): usa la línea de comandos ya compilada si está, y si no, la compila al vuelo.

Paso 1 — portar juegos DOS a .NET con un solo comando

Se pone el juego en una carpeta que contenga sólo el juego (PakuPort copia la carpeta entera como datos) y:

.\pakuport port C:\juegos\MIJUEGO\JUEGO.EXE

El comando analiza el ejecutable, genera el proyecto .NET en export\<id>\, copia los datos, lo compila, lo arranca unos segundos sin ventana para comprobar que pinta algo, y lo empaqueta en un Jugar<Juego>.exe de doble clic. Al jugar, Ctrl+F1 y Ctrl+F2 bajan y suben la velocidad de la CPU emulada, F11 pone pantalla completa y F12 sale (Esc es una tecla más del juego).

Paso 2 — fijar la velocidad

Cuando se ha encontrado la velocidad buena con las teclas de antes, se apunta en games\<id>\game.json (por ejemplo "cyclesPerMs": 3000) y se vuelve a exportar. Grabar, comparar y repetir partidas exige una velocidad fija, porque en PakuPort el reloj del juego es el contador de instrucciones.

Paso 3 — grabar lo que hace

Una partida sin ventana con las tres grabadoras encendidas, y después pakuport corpus-diag para comprobar que cada caso grabado se reproduce. En el juego de ejemplo salen 146 casos de 20 funciones, los 146 en verde.

Paso 4 — generar el tablero

.\pakuport expediente tablero --game games\catch\game.json --corpus games\catch\corpus

Paso 5 — traducir y juzgar

Se escribe respuesta.cs en la carpeta de la función, y se juzga todo el tablero de una vez. El avance del remake es un número:

.\pakuport banco juzga --banco games\catch\expediente --exige-ritmo

Paso 6 — enchufarlo al juego y demostrar que no cambia nada

.\pakuport export --game games\catch\game.json --portadas games\catch\expediente
python tools\control_portadas.py --export export\catch --cinta <cinta> --insn <N>

El repositorio trae un tutorial completo (en inglés) que hace todo esto sobre el juego de ejemplo, con la salida exacta que tiene que dar cada paso, y una guía para hacer el mismo trabajo a mano, sin ninguna IA.

06 — Cómo usarlo con Claude Code

PakuPort está hecho para trabajar con un agente de programación al lado, y en buena parte se ha desarrollado así. Con Claude Code el arranque es abrirlo en la carpeta del proyecto:

cd pakuport
claude

El repositorio trae un CLAUDE.md que Claude Code lee solo al arrancar, y que carga las reglas de la casa para cualquier agente (AGENTS.md): las corridas de medida van sin ventana y sin sonido, nunca se arregla un juego exportado sino PakuPort, ningún juez se debilita para que algo salga verde, y no se suben juegos al repositorio.

Para portar un juego basta con pedirlo en lenguaje normal:

Portea el juego que tengo en C:\juegos\MIJUEGO\JUEGO.EXE. Si no arranca,
mira los logs y dime en qué instrucción se para y por qué.

Cuando un juego no funciona, esto es donde un agente aporta más: PakuPort tiene decenas de interruptores PAKU_* que registran cada llamada al DOS, cada interrupción, cada acceso a un puerto y las últimas instrucciones antes de un fallo, y leer esos registros es un trabajo largo y mecánico que a un agente se le da muy bien.

Para el remake, la regla es una carpeta del tablero por tarea. Algo así funciona:

Traduce a C# la función games\catch\expediente\FUN_1000_19a.
Lee primero ..\COMO-ESCRIBIR.md, luego PLANTILLA.cs y desensamblado.asm.
Escribe sólo respuesta.cs en esa carpeta. No toques el corpus, banco.json,
game.json ni src\. Cuando acabes, juzga con
  pakuport banco juzga --banco games\catch\expediente --exige-ritmo
y si sale rojo, arréglalo con el mensaje del juez. Como mucho tres intentos.

Como cada carpeta es independiente, se pueden lanzar varios subagentes a la vez, cada uno con su función, y juzgar el tablero entero al final. Tres consejos que salen de haberlo hecho muchas veces:

La guía completa, con los fallos medidos que conviene avisarle al modelo y un banco ciego para medir cuánto ayuda el material, está en docs/REMAKE-WITH-AI.md. Vale para cualquier agente que sepa leer ficheros y lanzar comandos, no sólo para Claude Code.

07 — Un port en HD con Claude Code

Un port en HD tiene dos niveles, y conviene empezar siempre por el primero porque sale gratis.

Nivel 1: más nitidez sin dibujar nada

Cualquier juego porteado se puede ampliar respetando los bordes del pixel art en lugar de estirarlo y emborronarlo. Son dos interruptores: PAKU_SCALE3X=1 (Scale3x: bordes nítidos; trabaja sobre los índices de la paleta para no inventarse bordes entre dos colores parecidos) y PAKU_XBRZ=2 a 6 (xBRZ: diagonales redondeadas y letras suaves). Se le pide así:

Lanza el port de MIJUEGO con xBRZ a x4, y luego con Scale3x,
para que compare cuál le sienta mejor.

Nivel 2: arte nuevo, gráfico a gráfico

Aquí entra el kit de remakes. Su regla de oro es que el arte nunca decide nada: qué se dibuja, dónde, a qué tamaño, en qué orden y cuánto tiempo se queda lo mide PakuPort y lo deja escrito. Quien dibuja, sea una persona o un modelo de imagen, sólo entrega ficheros PNG en las rutas que el kit le dice. Y el juego no se toca: con decenas de gráficos sustituidos, la memoria de vídeo del juego sigue idéntica byte a byte.

La diferencia importante con otros intentos de remasterizar gráficos está en cómo se reconoce cada gráfico. Lo habitual es buscar en la pantalla píxeles que coincidan con un sprite conocido, y eso falla en cuanto dos sprites se solapan, uno se recorta en el borde o un fotograma de una animación no se capturó: el resultado es una mezcla de gráficos nuevos y viejos que parece que le faltan sprites. PakuPort identifica cada dibujo en el momento en que el juego lo pinta, por qué código lo pinta y desde dónde, y reconstruye la figura entera aunque el juego la dibuje fila a fila.

Con Claude Code, el recorrido completo son cuatro peticiones. La primera prepara el trabajo:

Vamos a hacer el port HD de MIJUEGO. Fija su velocidad en game.json si
no la tiene, graba una partida con "pakuport remake capture" y luego
"pakuport remake study". Hazme la hoja con "pakuport remake hoja" y
resúmeme qué gráficos hay y cuáles se ven más.

Para grabar, o juegas tú un rato en la ventana que se abre, o le dices qué teclas pulsar y la partida se graba sola y sin ventana. El estudio deja en games\<id>\remake\ el encargo (pendiente.json): una entrada por gráfico con su imagen de referencia recortada con la paleta real, su tamaño, si necesita transparencia, si es una animación y con cuántos fotogramas, y la ruta exacta donde hay que guardar la versión nueva. La hoja es ese mismo encargo en una sola página HTML, para verlo de un vistazo.

La segunda pide el arte. Claude Code no dibuja, pero sabe llevar el encargo a quien dibuja: a ti, a un modelo de imagen local o a uno de pago por API. Si usas un modelo, que Claude escriba el script que le pasa cada entrada con su referencia:

Lee games\mijuego\remake\pendiente.json. Para los 3 gráficos que más se
repiten, escribe un script que pida a [tu modelo de imagen] la versión x4
a partir de su imagen de referencia: que la VUELVA A DIBUJAR respetando
la rejilla, la silueta y la paleta, no que la "remasterice". Si es una
animación, todos los fotogramas en una sola hoja. Guarda cada imagen en
su "destino". Después pasa "pakuport remake check mijuego" y, de cada
una que pase, enséñame el antes y el después con "pakuport remake try".

check es la aduana: revisa cada imagen entregada sin arrancar el juego (tamaño o múltiplo exacto, transparencia donde hace falta, restos del fondo de croma, animaciones a medias) y rechaza, con el motivo, lo que rompería el juego. try mete una sola imagen en el juego y saca el antes y el después lado a lado, congelando el juego en la misma instrucción en las dos mitades para que sean el mismo instante.

La tercera lo comprueba:

Pasa "pakuport remake judge mijuego". Si la comprobación de que el
remake no perturba al juego no sale verde, para y dime qué ha cambiado.

El juez juega la misma partida dos veces, con y sin el arte nuevo, y exige que el juego dibuje exactamente lo mismo en las dos: el remake no puede cambiar el juego, nunca. Además dice qué parte de lo que se dibuja ya tiene arte nuevo y cuáles son los huecos más visibles, que es la lista de lo siguiente que hay que encargar.

Y la cuarta es jugar: .\pakuport remake play mijuego.

Cuatro avisos para el agente, que salen de haberlo hecho:

La guía completa del kit está en docs/HD-ART.md.

08 — Lo que no hace (todavía)

Prefiero contarlo a que lo descubras tú:

Y lo legal: PakuPort no incluye ningún juego. Un port lleva dentro el código y los datos del juego original, y es para uso propio con una copia que tengas tú.

09 — Para quién es esto

El proyecto está en github.com/unmateria/PakuPort, con el código, la documentación en inglés y el juego de ejemplo, y las descargas en su página de releases. Si lo pruebas con un juego tuyo y se para en algún sitio, cuéntamelo: cada juego que falla es una pieza de la plataforma que todavía no estaba bien.