Movimiento de una ficha

Recordemos cómo se rotaba la ficha en la entrada anterior. Desde el bucle de juego:

  • Primero se capturaba el evento de movimiento a la izquierda o derecha del joystick 2.
  • Ante esos eventos, se borraba la ficha, se actualizaba la variable que controla la rotación, y se volvía a pintar la ficha.

Pues bien, con el movimiento viene a ser lo mismo, sólo que en vez de modificar la variable que controla la rotación vamos a modificar la variable que controla la posición X o la posición Y de la ficha.

Y para no perder la rotación de la ficha, lo que vamos a hacer es:

  • Al mover a izquierda o derecha el joystick 2, cambiaremos la variable X.
  • Al mover arriba o abajo el joystick 2, cambiaremos la variable Y.
  • Al pulsar disparo en el joystick 2, rotaremos a derechas.

Cuando el juego esté más avanzando no será posible mover la ficha arriba o abajo, sólo a la izquierda o a la derecha, así como rotar la ficha, que además caerá por su propio peso. No obstante, como de momento estamos probando cosas y mejorando el API de programación, no está de más que preveamos esas opciones, que luego pueden usarse o no.

En realidad, de momento sólo hacer falta meter cambios en el fichero principal, el tetris_main.c. Para ir preparando este fichero para un programa cada vez más complejo, definimos una nueva función actualiza_ficha(), que es donde ahora estará el código que lee el joystick 2 y actuará en consecuencia:

Bueno, pues con poco cambio ya somos capaces de rotar la ficha (disparo) y moverla a izquierda / derecha y arriba / abajo, sabiendo que esto último no será necesario en el juego real.

Aquí vemos, por ejemplo, que el jugador ha movido la ficha a la esquina inferior derecha, y que la ha rotado varias veces, quedando todo ello reflejado en las variables de la ficha, así como en las trazas que se ven al pie de la pantalla:

Poquito a poco se va haciendo el camino…


Código de ejemplo: tetris02.zip

Definición de una ficha y sus rotaciones

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

Desarrollo de un proyecto en C para el C64: Tetris

Hace algunos meses que no puedo dedicarle mucho tiempo al blog. Quisiera retomarlo con algún proyecto sencillo que nos permita poner en práctica todo lo aprendido sobre C y cc65.

Posibilidades hay muchas. Recientemente he leído que están a punto de cumplirse los 40 años desde la publicación del videojuego clásico por antonomasia: el Tetris. Así que me ha parecido una buena opción.

Para quien no lo conozca, si esto fuera posible, el juego aparece descrito en Wikipedia:

https://es.wikipedia.org/wiki/Tetris

Resumiendo, se trata de algo así:

  • Tenemos un tablero de N x M casillas.
  • En la parte superior del tablero van apareciendo fichas de 4 bloques, aunque con diferentes formas.
  • Las fichas aparecen de una en una y van cayendo desde la parte superior hasta la parte inferior del tablero, donde se acumulan.
  • El jugador puede rotar y mover las fichas, siempre dentro de los límites del tablero y las limitaciones que imponen las otras fichas, o los restos de ellas.
  • El objetivo del jugador es completar filas. Cuando una fila se completa, ésta se colapsa y el jugador recibe puntos.
  • Las filas pueden colapsarse de una en una, de dos en dos, de tres en tres, etc. Cuantas más filas se colapsen de golpe, más puntos recibe el jugador.
  • El juego se acaba cuando las fichas acumuladas llegan hasta la parte superior del tablero y ya no pueden entrar fichas nuevas.
  • Cuanto más rápido sea el juego, lógicamente, más difícil se vuelve.

Y gráficamente sería algo así:

Este juego es relativamente fácil de programar. La clave está en dar con las estructuras de datos más adecuadas para definir las diferentes fichas y sus rotaciones, así como el tablero.

Algunas dudas normales serían: ¿cómo definimos las fichas? ¿Definimos una forma básica y a partir de ahí calculamos sus rotaciones? ¿O mejor definimos las fichas con todas sus rotaciones? ¿Cómo definimos el tablero? ¿Como un conjunto de fichas acumuladas en ciertas posiciones? ¿Como un conjunto de bloques o restos de fichas? ¿Debemos mezclar el tablero y la ficha actual en una misma estructura de datos? ¿Mejor usamos estructuras separadas?

A todo esto, iremos dando respuesta. En particular, veremos los siguientes puntos:

  • Definir una ficha y sus rotaciones.
  • Mover una ficha.
  • Definir todos los tipos de fichas.
  • Definir el tablero.
  • Combinar el tablero y la ficha actual.
  • Hacer que las fichas caigan.
  • Hacer que las fichas que aparecen sean aleatorias.
  • Controlar las colisiones con otras fichas.
  • Colapsar las filas llenas.
  • Controlar el final de la partida.
  • Hacer que el juego sea multipartida.
  • Definir diferentes niveles de dificultad.
  • Una vez colocada, que la ficha caiga a plomo.

Y todo esto programado en C con cc65 y para el C64, claro. A disfrutarlo.