El interior de los juegos retro: Punch-Out para NES

imagen

Parte 1. Contraseñas


El juego NES Mike Tyson Punch-Out utiliza un sistema de contraseña que permite a los jugadores continuar el juego desde un punto específico. Cada contraseña consta de 10 dígitos, que pueden variar de 0 a 9. El juego puede aceptar dos tipos de contraseñas, que llamo contraseñas "normales" y "especiales". Las contraseñas especiales son ciertas combinaciones de 10 dígitos, cuya entrada el juego reacciona de una manera única. Una lista completa de contraseñas especiales se ve así:

  • 075 541 6113 - señal de ocupado 1
  • 800 422 2602 - señal de ocupado 2
  • 206882 2040 - señal de ocupado 3
  • 135 792 4680 - juego en un torneo oculto: "Otro circuito mundial" (para que se acepte la contraseña, debe mantener presionado el botón Seleccionar y presionar A + B)
  • 106 113 0120 - visualización de títulos (para que se acepte la contraseña, debe mantener presionado el botón Seleccionar y presionar A + B)
  • 007373 5963 - transfiere al jugador a la batalla con Mike Tyson

El segundo tipo de contraseñas aceptadas por el juego son las contraseñas normales. Las contraseñas regulares codifican el progreso que el jugador ha hecho en el juego. Los siguientes datos del juego están codificados en una contraseña normal:

  • Número de victorias profesionales
  • Número de pérdidas profesionales
  • Knockout gana
  • Siguiente oponente

Codificación de contraseña


Como ejemplo, para estudiar la generación de contraseñas, utilizamos un juego con 24 victorias, 1 derrota, 19 nocauts y comenzamos en el torneo mundial con una batalla contra Super Macho Man.

El proceso de codificar el estado de un juego en una contraseña comienza con la recopilación del número de victorias, pérdidas y nocauts en el búfer. El juego presenta cada número en forma de un código decimal binario formado por 8 bits por dígito y 2 dígitos para cada valor. Es decir, para 24 victorias, necesita un byte con un valor de 2 y un segundo byte con un valor de 4. Lo mismo sucede con pares de bytes para pérdidas y nocauts, es decir, se obtiene un total de 6 bytes de datos. En el diagrama a continuación, estos 6 bytes se indican con valores tanto en decimal como binario.


El siguiente paso es generar una suma de verificación para estos 6 bytes. El byte de suma de comprobación se calcula sumando 6 bytes separados y restando el resultado de 255. En nuestro caso, 2 + 4 + 0 + 1 + 1 + 9 = 17, es decir, 255-17 = 238.

Luego escribimos unos bits de 6 bytes en un nuevo búfer. Este búfer se puede interpretar como un valor intermedio de 28 bits, que completaremos paso a paso. Los bits del primer búfer se dividen en grupos de dos y se mueven a diferentes posiciones codificadas del segundo búfer. Este es el primero de varios pasos cuya única tarea es simplemente ofuscar los datos para complicar el proceso de generación de contraseñas para los jugadores.


Tenga en cuenta que no todos los bits del búfer original se transfieren al nuevo búfer intermedio. Estos bits se ignoran porque se sabe que siempre son 0. Gracias a las reglas del juego, es suficiente transmitir el número de pérdidas en la contraseña con solo 2 bits de información. Si la cantidad total de pérdidas llega a 3, entonces ocurre el juego y el jugador no recibe una contraseña. Por lo tanto, es suficiente describir el número de pérdidas con los números 0, 1 y 2, y para esto solo son suficientes 2 bits.

Luego escribimos otros pares de bits en el búfer intermedio. Los primeros cuatro pares se toman del valor de suma de control previamente calculado. Se toma otro par del valor del enemigo. El valor de un adversario es un número que indica contra qué adversario peleará un jugador después de ingresar una contraseña. Se pueden usar tres posibles valores enemigos:

0 - DON FLAMENCO (primera pelea del gran torneo)

1 - PISTON HONDA (primera pelea del torneo mundial)

2 - SUPER MACHO MAN (última pelea del torneo mundial)

Como queríamos generar una contraseña que nos confronte con Super Macho Man, usamos 2 como el valor del adversario. Luego, los bits de suma de comprobación y los valores del adversario se escriben en los bits intermedios de la siguiente manera:


El siguiente paso es realizar varias permutaciones cíclicas de los bits intermedios a la izquierda. Una permutación cíclica hacia la izquierda significa que todos los bits se desplazan una posición hacia la izquierda, y el bit que estaba más a la izquierda se mueve y se convierte en el más a la derecha. Para calcular el número de permutaciones a la izquierda, tomamos la suma del valor del oponente y el número de pérdidas, sumamos 1 y tomamos el resto de dividir por 3 este resultado. En nuestro caso, resulta que 2 + 1 + 1 = 4. Entonces el resto de 4/3 es 1, por lo que desplazamos cíclicamente los bits intermedios hacia la izquierda una vez.


En este punto, los bits intermedios ya están completamente mezclados y es hora de comenzar a romperlos para obtener los números que componen la contraseña. Las contraseñas deben constar de 10 dígitos, por lo que dividiremos 28 bits intermedios en 10 números separados, a los que llamaremos valores de contraseña P0, P1, P2, etc. Cada uno de los primeros nueve valores de contraseña recibe 3 bits de datos, y el último obtiene solo uno de los bits intermedios. Para completar el valor de la contraseña terminada, también incluiremos bits que indican el número de permutaciones realizadas en el paso anterior.


Finalmente, agregamos a cada valor de contraseña un desplazamiento único y codificado. El dígito de contraseña terminado será el resto de esta suma dividida por 10. Por ejemplo, en la séptima posición usamos el desplazamiento 1, es decir, obtenemos 5 + 1 = 6, y el último dígito es el resto de 6/10, es decir 6. En la cuarta posición usamos desplazamiento 7, es decir, obtenemos 5 + 7 = 12, y la cifra final es igual al resto 12/10, es decir 2.


Entonces, obtuvimos dígitos de contraseña listos para usar que se pueden verificar en el juego.


Decodificación de contraseña


El proceso de decodificar las contraseñas de vuelta al número de victorias / pérdidas / nocauts y el valor del oponente es una implementación simple en el orden inverso de todos los pasos descritos anteriormente. Lo dejaré como tarea para los lectores. Sin embargo, el juego tiene dos errores notables que comete al decodificar y verificar las contraseñas ingresadas por el jugador.

El primer error ocurre en el primer paso de decodificar la contraseña, es decir, al restar las compensaciones para volver a los valores de la contraseña. Los valores de contraseña iniciales contenían 3 bits de datos cada uno, es decir, sus valores antes de aplicar compensaciones deberían estar en el rango 0-7. Sin embargo, el jugador puede ingresar una contraseña, que después de restar el desplazamiento da como resultado un valor de contraseña de 8 o 9 (dividiendo entre 10 con el resto). En lugar de rechazar inmediatamente dicha contraseña, el juego por error no verifica este caso y le permite agregar un bit de datos adicional al valor de la contraseña que puede contaminar el conjunto de bits intermedios de tal manera que las contraseñas ya no serán únicas. Dado que ciertos bits intermedios se pueden establecer con el dígito correspondiente de la contraseña, O con un bit adicional del valor de la contraseña vecina, hay muchas contraseñas que se pueden convertir al mismo conjunto de bits intermedios. Es por eso que puedes encontrar diferentes contraseñas que dan el mismo resultado en el juego, aunque deberían haber sido únicas.

El segundo error es un error en la lógica que utiliza el juego para verificar los datos después de decodificar la contraseña. El juego está tratando de aplicar las siguientes condiciones:

  • la suma de verificación almacenada en la contraseña corresponde a la suma de verificación que debe obtenerse teniendo en cuenta el número de victorias / pérdidas / nocauts almacenados en la contraseña
  • el valor de pérdida es 0, 1 o 2
  • el valor del enemigo es 0, 1 o 2
  • El número de permutaciones cíclicas almacenadas en la contraseña es el número correcto teniendo en cuenta el valor de las pérdidas y el valor del oponente almacenado en la contraseña.
  • todos los números ganadores / perdidos / eliminados almacenados en la contraseña están en el rango 0-9
  • gana> = 3
  • gana> = nocauts

Si alguna de estas condiciones no se cumple, el juego debe rechazar la contraseña. Sin embargo, hay un error en la implementación de la verificación final (es decir, cuando se verifican los números codificados por BCD). En lugar de verificar la victoria> = nocauts, el juego permite casos en los que el número más alto de victorias es 0, el número más bajo de victorias> = 3, y el número más alto de nocauts es menor que el número más bajo gana. Por ejemplo, el juego aceptará un récord con 3 victorias, 0 derrotas y 23 nocauts (lo que demuestra la contraseña 099 837 5823), aunque debe ser rechazado (ya que es imposible ganar 23 peleas por nocaut si gana en solo 3 peleas).

Conclusión


Los detalles particulares de tal esquema de codificación son exclusivos de Punch-Out, pero la idea general de obtener partes importantes del estado del juego, convertirlas con la posibilidad de restauración para ofuscar el estado inicial, y luego usarlas para generar un cierto número de caracteres para demostrarle al jugador como contraseña es un enfoque bastante universal. Puede usar sumas de verificación para que los cambios accidentales de contraseña (por ejemplo, si un jugador comete un error) conduzcan a su rechazo, en lugar de crear otra contraseña con un estado de juego aleatorio.

Parte 2. Descripción general de Punch-Out


¡Todos los luchadores en Punch-Out de Mike Tyson! controlado por uno o más scripts de bytecode interpretados. El personaje del jugador, Little Mac, ejecuta un guión simple que contiene la lógica de cada acción disponible para el jugador (evasión, bloqueos, puñetazos, etc.) Los personajes de los oponentes están controlados por 3 niveles de guiones independientes que juntos crean el comportamiento del personaje.


Match script


El script enemigo de más alto nivel se ejecuta a lo largo de las 3 rondas de batalla y controla los cambios más ambiciosos en el comportamiento del oponente. Llamaré a este script "script de coincidencia". Su tarea principal es elegir los comportamientos que el enemigo realizará en respuesta a varios eventos durante la batalla. Por ejemplo, un cierto comportamiento se activará instantáneamente después de que el oponente se levante después de una caída, o cuando el jugador se quede sin corazones y se canse. Estos comportamientos se escriben en la tabla y son llamados por el motor del juego en respuesta a los eventos correspondientes. El guión del partido también establece los valores iniciales para las opciones de configuración relacionadas con la complejidad de la batalla (por ejemplo, la cantidad de tiempo que el oponente permanece vulnerable después de un golpe fallido). Finalmente, el guión del partido comienza a esperar ciertos marcadores temporales durante la batalla para realizar cambios en los valores previamente establecidos. .

Script de comportamiento


El guión de un adversario de un nivel inferior es un "guión de comportamiento". Este nivel es responsable de la secuencia de ciertos golpes y ataques que el oponente debe realizar dentro del marco del comportamiento actual (establecido por el script de coincidencia). Los scripts de comportamiento ejecutan comandos como "aplicar el jab correcto, pausar durante 28 cuadros, aplicar aleatoriamente el uppercut izquierdo o derecho, repetir todo es 5 veces ". El script también tiene comandos para leer y escribir en cualquier dirección en la memoria del motor del juego, por lo que el comportamiento puede ser muy dinámico.

Guión de animación


El guión adversario de nivel más bajo es un "guión de animación". Tales guiones realizan los detalles de cada golpe individual, bloqueo o ataque especial como parte del comportamiento (definido por el guión de comportamiento). En este nivel, comandos como "asignar sprite 23 al cuadro actual de la animación del enemigo, moverlo hacia abajo y hacia la derecha 1 píxel por segundo cuadro en cada segundo cuadro". para los próximos 10 cuadros, cambie el cuadro de animación al sprite 24, reproduzca el efecto de sonido 7 ". Además de los comandos de animación, los guiones de animación también realizan secuencias de varios cambios en los estados de juego que están estrechamente relacionados con los movimientos enemigos. Por ejemplo, en una animación larga de un ataque especial, un guión de animación puede insertar comandos que hacen que el enemigo sea vulnerable a derribos con un solo golpe en un período de tiempo muy corto. Al igual que los guiones de comportamiento, los guiones de animación pueden leer y escribir direcciones de memoria arbitrarias en el motor del juego para lograr efectos más dinámicos.

Script Little Mac


El guión ejecutado por el personaje del reproductor Little Mac es muy similar a los guiones de animación del enemigo. Cambia el cuadro actual de la animación reflejada y mueve al jugador por la pantalla. Al igual que los guiones de animación, el guión de Little Mac ejecuta secuencias de ciertos eventos de juego, por ejemplo, en qué momento en particular el Mac debería golpear al enemigo, o cuándo debería ejecutar un bloqueo o evasión. La secuencia de comandos Little Mac controla la entrada del jugador, de forma similar a cómo las secuencias de comandos de comportamiento controlan las secuencias de comandos de animación enemigas.

Cada uno de estos cuatro guiones es procesado por su propio intérprete. Aunque muchos de ellos tienen la misma funcionalidad, por ejemplo, control de control básico y acceso directo a la memoria, cada sistema implementa su propia versión y no comparte el código común (o el espacio del código de operación) con otros sistemas. Esto permite que cada tipo de script sea muy específico y use efectivamente un pequeño conjunto de comandos de destino. Los datos de script representan aproximadamente el 22% de los datos no gráficos del cartucho del juego (el código de máquina para el motor del juego solo ocupa el 17%), por lo que era muy importante que los scripts tuvieran un aspecto compacto.

Parte 3. Secuencia de comandos de puñetazo


El guión del partido controla el comportamiento del oponente al más alto nivel. La operación principal que realiza una y otra vez es esperar un cierto momento de la ronda y realizar cambios en los datos de configuración del oponente en ese momento. El video muestra la primera ronda de la primera batalla contra Bald Bull, junto con un guión de partido que controla su comportamiento general.


Hay tres operaciones principales que puede realizar un script de coincidencia. El primero es esperar hasta que el temporizador de ronda alcance un cierto valor. El segundo es preguntar si el comportamiento actual del adversario ha cambiado. Los comportamientos se registran en la tabla de configuración de la batalla en la memoria, y luego son llamados en diferentes momentos por los guiones del partido y el motor del juego. La tabla tiene dos segmentos de comportamiento utilizados por scripts de coincidencia. Los llamo comportamiento "básico" y comportamiento "especial". Los comportamientos especiales son, por ejemplo, un golpe de Bull Charge de un oponente de Bald Bull o Honda Rush de un oponente de Piston Honda, y los comportamientos principales son los golpes habituales que el oponente entrega por el resto del tiempo. Los scripts de comportamiento específicos utilizados para implementar este tipo de comportamientos pueden ser cambiados por el script de combate directamente durante la batalla, por lo que los luchadores pueden comenzar con un comportamiento principal y luego cambiar a otro (como se nota en el video, Bald Bull hace esto cuando el temporizador llega a 0 : 20.)

Una característica de los comportamientos cambiantes realizados por los scripts de coincidencia es que pueden ser reemplazados por cambios de comportamiento solicitados por el motor del juego. El motor del juego usa cuatro segmentos de comportamiento para solicitar nuevos comportamientos cuando la Mac pierde todos sus corazones y se cansa, y también cuando el oponente se levanta después de una caída. Si el guión del partido cumplió con la solicitud de cambiar el comportamiento, pero uno de estos cuatro eventos del motor del juego ocurre antes de que se procese la solicitud (las solicitudes no se pueden procesar hasta que el oponente entre en el estado de espera), el motor del juego establece el comportamiento deseado y la solicitud del guión del partido se rechaza. Algunos luchadores, como Bald Bull, solicitan un comportamiento especial varias veces durante un corto período de tiempo. Parece que esto solo es necesario para reducir la probabilidad de que cualquiera de estas solicitudes se descarte accidentalmente.

La tercera operación principal del script de coincidencia es el parcheo de memoria. La mayoría de los parches de memoria afectan la tabla de configuración de batalla, donde se graban los scripts de comportamiento. Además de los conjuntos de comportamientos, la tabla contiene datos relacionados con la complejidad de la batalla. Por ejemplo, cuando el temporizador del video llega a las 0:30, Bald Bull cambia su configuración de seguridad. Esto lleva al hecho de que el jugador ya no puede engañarlo presionando hacia arriba y luego golpeando el cuerpo. Además, los guiones de coincidencia tienen la capacidad de parchear direcciones de memoria arbitrarias, pero esta función se usa solo una vez, al comienzo de la segunda ronda con Mike Tyson, de modo que el jugador recibe una estrella por primera vez cuando la golpea, que está en modo de espera.

Parte 4. Script de Comportamiento Punch-Out


Ahora observamos los scripts de comportamiento que están directamente involucrados en la implementación de comportamientos.

El video muestra una interpretación de cómo se vería el guión de comportamiento rival del Piston Honda 1 en los equipos ingleses.


Comandos de animacion


Los guiones de comportamiento son responsables del lanzamiento secuencial de animaciones de la misma manera que los guiones de coincidencia fueron responsables del lanzamiento de animaciones. El comando anim reproduce una animación específica y el comando anim_rnd realiza una animación seleccionada al azar de una lista de 8 opciones. En el video anterior, en el momento de una selección aleatoria de una lista de opciones, la opción seleccionada se resalta momentáneamente en rojo. Cuando Piston Honda aplica sus dos primeros golpes, anim usa anim para cada uno de ellos. Después de eso, usa anim_rnd para seleccionar aleatoriamente de un conjunto que contiene 6 animaciones de gancho y 2 animaciones vacías. Como resultado de esto, engancha el 75% del tiempo y no hace nada el 25% del tiempo.

Desde el punto de vista del guión, el comportamiento de las animaciones se reproduce sincrónicamente, porque cuando el sistema de animación no está en modo inactivo, el intérprete del guión se detiene.

Comandos de control de ejecución


Hay varios comandos que modifican la ejecución del script de comportamiento en sí. pause comandos de pausa pueden pausar la ejecución del script para un cierto número de cuadros, o para el número de cuadros seleccionados al azar de una lista de 2 opciones.

Hay varios comandos de ramificación que, bajo ciertas condiciones, opcionalmente cambian a diferentes partes del script de comportamiento. El branch_rnd tiene una probabilidad dada de que se produzca una rama cada vez que se ejecuta. Un caso especial de ramificación probabilística es el comando branch_always , que tiene un 100% de probabilidad de ramificación.

Un mecanismo de bucle simple está integrado en el intérprete de script de comportamiento. El comando set_loop_count establece el valor actual del contador de bucles. Cada vez que se branch_while_loop comando branch_while_loop disminuye el valor del contador del bucle en uno y realiza una ramificación solo si el valor del contador es mayor que cero.

El último tipo de ramificación verifica el contenido de la memoria para tomar una decisión sobre la ramificación. Piston Honda usa este comando branch_mem_test para verificar si su último golpe en un comportamiento particular fue exitoso. Si el golpe alcanza el objetivo, se bifurca para el siguiente golpe. Si el golpe no fue exitoso, entonces usa el comando branch_while_loop para continuar golpeando solo cuando se acumulan 5 golpes fallidos.

Comandos de comportamiento


Hay dos comandos mediante los cuales los scripts de comportamiento pueden controlar el sistema de comportamiento en sí. El comando begin_behavior_main usa para finalizar el comportamiento ejecutable actual e iniciar el comportamiento principal. Esto difiere de la ramificación en el script de comportamiento, porque la parte del script que se considera el comportamiento "principal" actual se puede cambiar durante el partido mediante el script de coincidencia (consulte la parte anterior del artículo sobre los scripts de coincidencia).

Otro comando relacionado con el comportamiento es enable_behavior_change . Cuando se inicia un nuevo comportamiento, comienza con un estado de bloqueo, cuando se bloquean todas las solicitudes adicionales de cambio de comportamiento. Usando el comando enable_behavior_change script indica que está listo para permitir otros comportamientos. Por ejemplo, en el comportamiento especial de Piston Honda, el comando enable_behavior_change nunca se ejecuta, por lo que si la Mac está cansada durante este tiempo, el comportamiento especial continúa. Sin embargo, los eventos de caída omiten este sistema, por lo que si durante el comportamiento especial del Piston Honda el personaje principal es derribado, el comportamiento cambiará en cualquier caso.

Source: https://habr.com/ru/post/es434550/


All Articles