Kabosu - Creando cosas

Logo de la página. Gato esférico con colores verdoso.

Analizando ROMs de Game Boy: el famoso logo

Publicado: 2024-02-03 (actualizado 2024-03-13)

Etiquetas: game boy, python


Hace unos días estuve escribiendo un programa en Python para procesar ROMs de Game Boy para un proyecto que tengo entre manos. Ya hablaré de él en un futuro pero por ahora me ha parecido interesante explicar de dónde sale el logo de Nintendo que aparece al encender una Game Boy y cómo lo podemos extraer de una ROM usando unas pocas líneas de Python.

Las personas que hayan jugado alguna vez a una Game Boy recordarán que al encender la consola aparecía el logo de Nintendo en la parte de arriba de la pantalla y se desplazaba hasta el centro antes de iniciarse el juego. Es posible que también recuerden que a veces salía basura en vez del logo y el juego no arrancaba.

¿Por qué ocurría esto? La razón era que el logo de Nintendo estaba grabado en el cartucho. Al encender la consola se leían las posiciones 0104 a 0133 (en hexadecimal) de la memoria del cartucho y se dibujaban en pantalla. Si el resultado era el logo de Nintendo entonces la consola iniciaba el juego. Si en vez de el logo veíamos algún gráfico raro quería decir que no ha podido leer la información del cartucho correctamente. Seguramente por estar los conectores sucios o desgastados.

La secuencia de bytes que representa el logo de Nintendo y que la consola espera encontrar es:

CE ED 66 66 CC 0D 00 0B 03 73 00 83 00 0C 00 0D 00 08 11 1F 88 89 00 0E
DC CC 6E E6 DD DD D9 99 BB BB 67 63 6E 0E EC CC DD DC 99 9F BB B9 33 3E

Voy a explicar brevemente cómo se representa la consola esa cadena hexadecimal como una imagen y luego veremos cómo hacerlo en Python.

La Game Boy usa tiles de 8x8 píxeles para representar los gráficos. Cada uno de los tiles del logo se generan a partir de 2 bytes de la secuencia anterior. Por ejemplo el primer tile está formado por los bytes CE y ED, el segundo por 66 y 66, el tercero por CC y 0C, etc.

¿Cómo se convierten dos bytes (16 bits) en un tile de 8x8 (64 píxeles)? El proceso es el siguiente:

Separar el byte en dos nibbles (4 bits):

CD -> C y E
ED -> E y D

Cada uno de los bits de estos nibbles se duplica.

C -> 1 1 0 0 -> 11 11 00 00
E -> 1 1 1 0 -> 11 11 11 00
E -> 1 1 1 0 -> 11 11 11 00
D -> 1 1 0 1 -> 11 11 00 11

Cada una de estas secuencias de 8 bits se usa dos veces para formar el tile de 8x8 final. Por cada 1 se pone un pixel de color oscuro, por cada 0 se pone una pixel de color claro.

11 11 00 00
11 11 00 00
11 11 11 00
11 11 11 00
11 11 11 00
11 11 11 00
11 11 00 11
11 11 00 11

El siguiente gráfico muestra cómo se forman los tiles de manera más visual

Diagrama que muestra visualmente los pasos explicados anteriormente.

Haciendo el mismo proceso con el resto de parejas de bytes se completa el logo de Nintendo. Hay que tener en cuenta que hay 2 filas de tiles.

Ahora vamos a hacer un script en Python que extrae el logo de una ROM de Game Boy y la muestra por el terminal.

Primero leemos el contenido de la ROM (normalmente un fichero .gb). Abrir el fichero como binario es importante.

def loadRom(path):
	with open(path, 'rb') as fin:
    	  return fin.read()

He creado 2 funciones auxiliares:

def toBin(v):
	return bin(v)[2:].rjust(4, '0')

def duplicate(v):
	return str.join('', [c + c for c in v])

La función que hace la conversión es un poco más complicada pero simplemente realiza los pasos descritos más arriba

LOGO_START = 0x0104
LOGO_END = 0x0133

def printLogo(rom):
	logo = rom[LOGO_START:LOGO_END+1]
	rowSize = len(logo)//2

	output = ['']*8

	for i, c in enumerate(logo):
    	a = toBin(c//16)
    	b = toBin(c%16)

    	row = 0
    	if i%2 == 1:
        	row += 2
    	if i >= rowSize:
        	row += 4

    	output[row] += a
    	output[row+1] += b

	for r in output:
    	line = r.replace('0', ' ').replace('1', '#')
    	print(duplicate(line))
    	print(duplicate(line))

Primero me copia el otro de ROM que contiene el logo (desde LOGO_START hasta LOGO_END) y se crea una matriz capaz de albergar el logo.

Para cada byte de memoria se divide en dos trozos de 4 bits a y b (paso 1) que se convierten a binario (paso 2)

Con un par de if se calcula en qué fila de la matriz se han de colocar los dígitos.

Finalmente para cada fila de la matriz se duplican sus caracteres (paso 3) y se imprime 2 veces (paso 4). He reemplazado los 0 por un espacio y los 1 por # para que se vea mejor.

El logo final se puede ver a continuación

$ python3 extractor.py probotector.gb
####      ####  ####                                                          ####              
####      ####  ####                                                          ####              
######    ####  ####                ####                                      ####              
######    ####  ####                ####                                      ####              
######    ####                    ########                                    ####              
######    ####                    ########                                    ####              
####  ##  ####  ####  ####  ####    ####    ########    ####  ####      ##########    ########  
####  ##  ####  ####  ####  ####    ####    ########    ####  ####      ##########    ########  
####  ##  ####  ####  ######  ####  ####  ####    ####  ######  ####  ####    ####  ####    ####
####  ##  ####  ####  ######  ####  ####  ####    ####  ######  ####  ####    ####  ####    ####
####    ######  ####  ####    ####  ####  ############  ####    ####  ####    ####  ####    ####
####    ######  ####  ####    ####  ####  ############  ####    ####  ####    ####  ####    ####
####    ######  ####  ####    ####  ####  ####          ####    ####  ####    ####  ####    ####
####    ######  ####  ####    ####  ####  ####          ####    ####  ####    ####  ####    ####
####      ####  ####  ####    ####  ####    ##########  ####    ####    ##########    ########  
####      ####  ####  ####    ####  ####    ##########  ####    ####    ##########    ########  

Espero que haya resultado interesante. Estoy utilizando un proceso similar para extraer los gráficos de algunos juegos de Game Boy. Si te interesa, el siguiente artículo de esta serie se encuentra aquí.


Artículo siguiente: Paella Pixel Dungeon (parte 1): añadiendo valencianía a un juego
Artículo anterior: Generador estático del blog: cómo lo he hecho