Definición de una ficha y sus rotaciones

Programación retro del Commodore 64

  • Programación retro del Commodore 64
  • “Programación Retro del Commodore 64” es un blog sobre el hardware, el sistema operativo, y la programación del Commodore 64. Y más específicamente sobre programación en ensamblador. Pretende ser un blog con información de calidad, y referencia en español de la programación retro de esta maravillosa máquina.
    • Mi blog
« Publicado el: 27/09/2023 »

Nuestro primer objetivo va a ser definir una ficha y permitir rotarla en pantalla con el joystick.

El Tetris clásico tiene siete tipos de ficha, cada una de ellas con cuatro rotaciones, y todas estas 7 x 4 = 28 combinaciones se definen en matrices de 5 x 5 posiciones.

Empecemos con una de las siete fichas (la que tiene forma de “T”):

Para codificar esta información una primera opción sería guardar vectores de 25 posiciones, o matrices de 5 x 5, que es lo mismo, con un 0 para las posiciones inactivas y un 1 para las posiciones activas. También se podría guardar un valor 1, 2, 3, …, en función del color, ya que las fichas tienen diferentes colores.

Sin embargo, esto no es lo más práctico ni lo más compacto. Harían falta 25 valores por cada ficha o rotación cuando, en realidad, con guardar sólo las cuatro posiciones activas es más que suficiente.

Si observamos bien, podemos llamar (0, 0) a la posición central de la ficha y, a partir de ahí, guardar las posiciones relativas de las otras posiciones activas. Algo así:

De este modo, guardando sólo estas cuatro posiciones relativas, que en el fondo son 8 valores, porque cada posición tiene una X y una Y, podemos guardar la forma de la ficha. Además, el orden de esas cuatro posiciones relativas da un poco igual.

Así, en C es posible definir una ficha con una tabla de este tipo:

   {+0, +0, -1, +0, -1, -1, -1, +1}

lo que viene a indicar que la ficha tiene activas:

  • El centro, es decir, la posición (0, 0).
  • La posición a la izquierda del centro, es decir, la (-1, 0).
  • La posición arriba de la anterior, es decir, la (-1, -1).
  • Y la posición inferior también, es decir, la (-1, +1).

Esto nos da la ficha base, pero luego necesitamos las otras tres rotaciones. ¿Qué hacemos? ¿Las calculamos sobre la marcha? ¿Las almacenamos en tablas?

Desde luego, si estuviéramos hablando de ensamblador la respuesta estaría clara: las almacenamos en tablas. En ensamblador hacer cálculos es complejo; es mucho más sencillo consultar tablas.

En este caso estamos programando en C. Por tanto, hacer cálculos es más sencillo que en ensamblador. Aun así, no merece la pena, y menos en un ordenador con pocos recursos como el C64. Es mucho más práctico almacenar la definición de las cuatro rotaciones de la ficha. Algo así:

int fichas_def[][] = {

   {+0, +0, -1, +0, -1, -1, -1, +1},

   {+0, +0, -1, -1, +0, -1, +1, -1},

   {+0, +0, +1, -1, +1, +0, +1, +1},

   {+0, +0, -1, +1, +0, +1, +1, +1}

};

De este modo, con un índice podemos recorrer las cuatro rotaciones (de la rotación 0 a la rotación 3) y, dentro de cada rotación, con otro índice recorremos las 4 posiciones activas (que son dobles porque tienen X e Y).

Posteriormente, cuando queramos definir las siete fichas bastará con repetir este mismo proceso siete veces.

Una vez entendida la definición o codificación de las fichas y sus rotaciones, el resto del apartado es bastante rutinario:

Ficheros tetris_ficha.h y tetris_ficha.c:

En el fichero tetris_ficha.h definimos las constantes, las variables externas y las funciones que vamos a necesitar para gestionar la ficha activa.

Entre las constantes tenemos FICHA_CHAR y FICHA_ESPACIO. La primera vale para pintar los bloques de la ficha; la segunda para borrarlos. Y como vamos a usar Conio, lo que necesitamos son los códigos PETSCII, no los códigos de pantalla.

En cuanto a las variables, vamos a tener variables para el estado de la ficha (activa o inactiva), para su tipo (7 tipos de fichas o 7 formas básicas), para su rotación (4 rotaciones por tipo), para la posición (X, Y) del centro de la ficha, y para el color. Más adelante vincularemos el color al tipo de ficha, prescindiendo de esta última variable. Las variables las hacemos externas, es decir, globales, porque se van a usar con frecuencia y desde casi cualquier fichero del programa.

Por último, respecto a las funciones, las operaciones básicas que vamos a poder hacer con las fichas son inicializarlas, pintarlas, borrarlas, rotarlas a la derecha / izquierda, moverlas a la derecha / izquierda, bajarlas, subirlas (aunque no se va a usar no está mal dotar e implementar la función), y pintar sus datos para depurar.

La mayoría de estas funciones son muy sencillas, ya que lo único que hacen es dar o modificar el valor de todas o parte de las variables. Por ejemplo, la función de inicialización de un valor inicial a todas las variables. Las funciones de rotado modifican la variable que controla la rotación, y las de movimiento las que controlan la posición. Fácil.

Las únicas funciones que merece un poco de explicación son la que pinta una ficha y la que la borra, aunque en el fondo son muy parecidas. La primera pinta un bloque por cada posición activa de la ficha; la segunda lo borra, lo cual equivale a pintar un espacio. Por tanto, vista una función, vista la otra.

La implementación de la función ficha_pinta () es así:

Es decir, con un índice i recorre la tabla de definición de la ficha. Para cada posición o bloque activo obtiene su (x, y) que, al ser posiciones relativas, es decir, diferencias respecto al centro de la ficha, llama (delta_x, delta_y). Luego suma a esas diferencias la posición del centro, que es (ficha_x, ficha_y) y así obtiene la posición absoluta. Pues bien, en esa posición absoluta pinta un FICHA_CHAR con la función cputcxy() de Conio.

Y el borrado es lo mismo, pero pintando un FICHA_ESPACIO.

Ficheros tetris_joystick.h y tetris_joystick.c:

El fichero tetris_joystick.h define las funciones que vamos a hacer con el joystick, es decir, inicializarlo y leerlo.

El fichero tetris_joystick.c implementa esas funciones. En el caso de la inicialización, se trata de cargar el driver. Y en el caso de la lectura, se trata de leer el joystick conectado al puerto 2.

Nada nuevo si se recuerda la entrada dedicada al joystick en cc65.

Fichero tetris_main.c:

Este es el programa principal, el que junta todas las piezas. La función main() primero hace una inicialización y luego tiene el típico bucle de juego.

La inicialización inicializa el joystick, es decir, carga su driver, borra la pantalla, inicializa la ficha y la pinta por primera vez.

El bucle de juego, que es un bucle infinito, lee el joystick 2 y, en función de que se haya pulsado derecha o izquierda, borra la ficha, la rota en esa dirección, y la vuelva a pintar. También pinta sus datos o variables para mejor depuración. Finalmente, mete un retardo para que la ejecución no sea muy rápida.

Fichero tetris.bat:

El fichero tetris.bat es un fichero por lotes (*.bat) de MS-DOS para facilitar la compilación con cc65. Su contenido es así de sencillo:

cl65 -O tetris_main.c tetris_ficha.c tetris_joystick.c

Según se vaya complicando el juego, y vayan apareciendo más ficheros *.c habrá que ir recogiéndolos en esa lista para que se compilen.

Resultado:

El resultado de la compilación es un fichero tetris_main que, si se arrastra a VICE, muestra algo así:

Si se configura el joystick de VICE con el menú Settings > Joystick Settings > Joystick Settings…, de modo que se vinculen unas teclas del PC con el puerto 2 de VICE, es posible rotar la ficha a derecha e izquierda.

Es un buen paso para empezar…

Código de ejemplo: tetris01.zip