jueves, 19 de marzo de 2026

Head On / Crash: Predecir los movimientos de la CPU

 Si eres viejo, como yo, es posible que recuerdes el juego Arcade "Head On" (Gremlin) o "Crash" (Exidy) de 1979. Básicamente los dos son una copia el uno del otro. Es un juego simple y adictivo, y que sirvió de inspiración para Pac-Man unos años más adelante, es el primero de la historia con el objetivo de recoger todos los "puntitos". En él llevas un cochecito que va en sentido anti-horario en un circuito de 4 carriles, mientras que otro cochecito va en dirección contraria en sentido horario intentando chocarse contigo, y si lo consigue: GAME OVER

Aquí en este video podeis ver el gameplay de la versión GameBoy, que es la que voy a desensamblar en esta ocasión.
Y pensareis... Pues el cochecito de la CPU intentará moverse siempre al mismo carril donde estemos nosotros para chocar. ¡Que fácil! ... Pues no. El cochecito de la CPU a veces hará cosas raras como moverse 2 carriles cuando solo estamos a 1 de distancia. ¡Incluso se puede dejar el juego en bucle infinito sin que choque nunca!
Más raro aun, en algunas ocasiones incluso se puede ver al cochecito de la CPU sin cambiar de carril pasando totalmente de nosotros. WTF?

Pues bien, llegó la hora de desensamblar, y por suerte el algoritmo que dicta al cochecito si debe girar o no esta contenida en una única función en 0x3efc que se llama cada frame
Si reemplazamos la llamada por NOPs, el cochecito de la CPU no girará nunca
Os voy a ahorrar leer ensamblador, así que os pondré las direcciones de memoria con las variables del juego que nos importan, y a continuación una traducción a lenguaje C:


0xC024 -> CPU_Y (Posicion Y del cochecito de la CPU)
0xC025 -> CPU_X (Posicion X del cochecito de la CPU)
0xC132 -> dir (Dirección del cochecito de la CPU 0-3. 0=IZQUIERDA,...)
0xC127 -> frame_giro (contador de frames que llevamos sin girar el cochecito de la CPU)
0xC138 -> velocidad (velocidad del cochecito. Incrementa al recolectar puntitos)
0xC13A -> flag_mov (¡este es importante! su valor {0x10, 0x20, 0x40, 0x80} cambia cada vez que los cochecitos del jugador, o de la CPU salen de una intersección entrando en los carriles, y cuando el cochecito de la CPU cambia de dirección "dir". Indica hacia donde debe moverse el cochecito de la CPU para girar y acercarse al cochecito del jugador. Si están en el mismo carril, valdrá 0x00)

if (velocidad==1) {
  frame_giro++;
  if (frame_giro!=4) return;
  frame_giro=0;
} else {
  frame_giro++;
  if (frame_giro!=2) return;
  frame_giro=0;
}

switch (dir) {
  case IZQUIERDA:
    if (CPU_X<0x48 || CPU_X>0x60) return;
    if (flagmov & 0x40) {
      CPU_Y=max(CPU_Y-1, 0X6F);
    } else if (flagmov & 0x80) {
      CPU_Y=min(CPU_Y+1, 0X93);
    }
    return;
......................................

Como veis el código es muy muy simple. Se divide en 2 bloques: el primero limita el giro a cada 2/4 frames dependiendo de la velocidad que tenga el cochecito de la CPU, y el segundo simplemente lo mueve en la dirección indicada por flagmov cuando se encuentre en una intersección sin salirnos de los limites. Realmente nunca controla si quiere moverse 1 o 2 carriles. Entonces... ¿que pasa? Centremonos en el primer bloque:
  • En velocidad 1 se desplaza lateralmente cada 4 frames, y así le da tiempo a desplazarse 2 carriles, aunque el jugador este solo a 1 de distancia.
  • En velocidad 2 se desplaza lateralmente cada 2 frames, y así también le da tiempo a desplazarse 2 carriles, aunque el jugador este solo a 1 de distancia.
  • En velocidad 3 y en adelante, se desplaza lateralmente cada 2 frames y solo le da tiempo a desplazarse 1 carril, aunque el jugador este a 2 o más de distancia.
Resumiendo, el coche de la CPU saltará siempre o 1 o 2 carriles dependiendo únicamente de la velocidad que tenga, moviendose siempre en dirección hacia el carril del jugador (esta dirección cambia cuando los coches salen de las intersecciones y entran de nuevo en los carriles)

Pero entonces... ¿Que ocurria cuando el cochecito de la CPU no cambiaba de carril y nos ignoraba?
Si veis el GIF detenidamente, veréis que justo antes de entrar al cruce, cambiaba de velocidad (1 -> 2)
⚠ ¡Alerta, bug! ⚠ si el valor de 0xC127 -> frame_giro en ese momento de cambiar velocidad vale 2 o 3, se seguirá incrementando sin control sin que llegue a valer de nuevo 2, y por lo tanto sin ejecutar ninguna lógica de giro durante los siguientes 254 frames, que es cuando el numero de 8bit hará overflow.

¿Que os ha parecido? Ahora que sabéis la lógica sigue el cochecito de la CPU, ya nunca veréis el juego de la misma forma. ¿preguntas? ¿dudas?...

No hay comentarios:

Publicar un comentario