Final de la partida

Ya nos vamos acercando al final. Tanto, que el siguiente paso es detectar el final de la partida.

Hay dos formas de hacerlo. O bien detectar que las filas acumuladas han llegado hasta arriba, hasta la fila cero, o bien intentar sacar una ficha nueva y no poder hacerlo, porque colisiona con otra ya existente en el tablero.

Optaremos por la segunda variante, aunque es un poco más compleja, porque nos permite situaciones como que haya una torre de fichas hasta arriba y seguir jugando, porque hay otras zonas del tablero despejadas.

Ficheros tetris_ficha.h y tetris_ficha.c:

Aquí lo que aparece es una nueva función ficha_verifica_nueva(). Esta función es similar a las otras funciones ficha_verifica_*() ya comentadas anteriormente.:

El objetivo de esta función es detectar si una ficha nueva, una ficha recién activada, ya no cabe en el tablero, bien por salirse por encima o bien por pisar otra ficha (o restos).

Fichero tetris_main.c:

A la nueva función ficha_verifica_nueva() la llamaremos desde el bucle de juego:

Tras activar una ficha nueva, verificaremos si cabe y, en caso contrario, saldremos del bucle de juego con un break.

De momento, como se trata de verificar si esto funciona bien, tras salir del bucle de juego pintaremos los datos del tablero con tablero_pinta_datos(). Lógicamente, más adelante habrá que cambiar esto.

Resultado:

Si jugamos una partida apilando las fichas, buscando el final, vemos algo así:

Como se puede observar, la fila 0, que es la de más arriba, tiene estos datos:

999992333399999

Es decir, la fila 0 tiene cinco bloques vacíos (9), un bloque morado (2), cuatro bloques verdes (3), y otros cinco bloques vacíos (9). Sobre esta fila, y sobre el tablero en general, ha aparecido una ficha nueva, que hemos llegado a pintar, que tiene forma de ángulo recto y es de color naranja.

Lógicamente, esta ficha nueva pisa restos de otras fichas, concretamente, un bloque verde (3) en la fila 0 y otro morado (2) en la fila 1, y, por tanto, es el final de la partida.


Código de ejemplo: tetris10.zip

Colapsar filas llenas

La gracia de casi todo juego es ganar puntos y, en el Tetris, los puntos se ganan llenando filas. Esto nos lleva a la necesidad de detectar y colapsar las filas llenas y, asociado a esta necesidad, se deriva que necesitaremos algún tipo de contador o variable de líneas y puntos.

Es importante tener en cuenta, además, que en el Tetris se puede colapsar una línea, o dos, o tres o cuatro a la vez (más de cuatro no es posible) y, cuantas más líneas se llenen / colapsen de golpe, más puntos se ganan.

Variables de filas llenas y puntos:

A estas alturas del proyecto tenemos los ficheros:

  • tetris_ficha.h y tetris_ficha.c.
  • tetris_joystick.h y tetris_joystick.c.
  • tetris_tablero.h y tetris_tablero.c.
  • tetris_random.h y tetris_random.c.
  • tetris_main.c.

¿En cuál de estos ficheros encajarían mejor las nuevas variables para llevar control de las líneas llenas y los puntos? Pues no está muy claro… Con los ficheros relativos a la ficha no las veo, ya que no son características de la ficha, ni están directamente relacionadas con ella. Con el joystick por descontado que no. Con el tablero podría ser, aunque de forma un poco indirecta, por aquello de que todo se acaba pintando en el tablero. Con los números aleatorios nada que ver. Y con el programa principal podría ser…

En definitiva, hay dos candidatos para ubicar estas nuevas variables. Inicialmente (esta entrada), decidí ubicarlas en el tablero. Más adelante revisaré esa decisión.

Sea como fuere, aquí las tenéis en tetris_tablero.c:

La primera variable, líneas, servirá para contar las líneas llenas. Y la segunda, puntos, servirá para llevar control de los puntos ganados. La relación entre ellas no es directa, puesto que, si se llenan varias líneas de golpe, se ganarán más puntos que línea a línea.

Estas variables tienen su inicialización (función tablero_inicializa_puntos()) y su función de pintado (función tablero_pinta_puntos()).

Detectar filas llenas:

Ahora que tenemos dónde controlar las líneas llenas, lo siguiente es detectarlas. Las líneas no se pueden llenar en cualquier momento, sino sólo cuando cae y se acumula una ficha, ya sea sobre el fondo o sobre otras fichas o restos. Es decir, en esta parte del código (fichero tetris_main.c):

Por eso ahora, cuando la ficha ya no puede caer más y se copia al tablero, además de lo anterior, se llama a la nueva función tablero_filas_llenas(), que detecta si hay filas llenas en el tablero (fichero tetris_tablero.c):

Lo que hace esta función es recorrer el tablero por filas y, dentro de cada fila, por columnas. Si una columna está vacía, entonces esa fila ya no puede estar llena, y pasa a la siguiente fila. Por el contrario, si todas las columnas de la fila están llenas, entonces la fila también lo está.

Además, si detecta que la fila está llena, colapsa la fila con la nueva función tablero_colapsa_fila() y vuelve a pintar el tablero.

Por último, incrementa las variables de líneas y puntos y las pinta. En principio, se reciben 10 puntos por línea, pero si se llenan / colapsan dos líneas de golpe, se reciben 40 puntos. Esto es así porque la fórmula implementada es:

puntos = 10 * líneas^2

Colapsar filas llenas:

Para colapsar las filas llenas se usa la nueva función tablero_colapsa_fila():

Esta función recibe como parámetro la fila a colapsar, y lo que hace es recorrer todas las filas desde esa hasta la primera, copiando en cada fila la fila que está justo encima. En resumen, las tira hacia abajo o colapsa.

Resultado:

El resultado es difícil de captar en una foto, porque es algo dinámico (se llena una fila y se colapsa). Se capta más fácilmente en un vídeo.

En todo caso, lo que sí es patente es que ya tenemos contador de líneas, de puntos, y que los vamos incrementando según se llenan filas.


Código de ejemplo: tetris09.zip

Colisiones entre fichas

En la entrada anterior, cuando las fichas caen, éstas se acumulan sobre el fondo del tablero. Sin embargo, si en el fondo ya hubiera otras fichas, de momento, no se acumulan encima, sino que se pisan o solapan. Esto hay que mejorarlo.

En el fondo, esto es una tarea sencilla ya que, si recordamos, ya metimos unos controles en los movimientos y rotaciones de la ficha para detectar si ésta se iba a salir del tablero. Se trata pues, simplemente, de mejorar esos controles.

Por ejemplo, a la hora de mover la ficha hacia abajo o hacerla caer (fichero tetris_ficha.c):

Es decir, ahora, además del control relativo al fondo del tablero (if de la línea 222), tenemos un nuevo control relativo a si la nueva posición de la ficha al caer estará vacía o no (if de la línea 225). En caso de no estar vacía la función devolverá 0 (o false), y el movimiento no podrá tener lugar, acumulándose la ficha en su posición actual, es decir, encima de la ficha que le impide caer.

Y lo mismo ocurre con el resto de funciones que verifican movimientos, las funciones ficha_verifica_*(), así como con las funciones que verifican rotaciones, que son las funciones ficha_verifica_rotacion_*().

Se trata de un cambio sencillo pero que, al acumular las fichas unas sobre otras, ya le da al juego un aspecto mucho más acabado:


Código de ejemplo: tetris08.zip

Fichas aleatorias

En esta entrada nos planteamos dos objetivos nuevos:

  • Que las fichas se pinten en su color desde que aparecen.
  • Que las fichas que van surgiendo sean aleatorias, tanto en lo que respecta al tipo como a su rotación y posición.

Ambos objetivos se describen a continuación.

Ficheros tetris_ficha.h y tetris_ficha.c:

El primer objetivo es fácil de conseguir. Simplemente consiste en mejorar la función ficha_pinta() para activar el color de la ficha, que depende de su tipo. Recordemos que hace ya varias entradas quitamos la variable ficha_color para vincular el color de la ficha a su tipo mediante la función ficha_color():

La activación del color se hace con la función textcolor() de Conio.

Por otro lado, la función ficha_color() es así:

Es decir, el número que nos da el color de una ficha es el tipo de la ficha (0 – 6) más dos. Por tanto, los colores que vamos a usar van del 2 al 8.

Si recordamos, la paleta de colores del C64 la tenemos en el header file c64.h:

Por tanto, vamos a usar desde el rojo hasta (2) hasta el naranja (8). El motivo de hacerlo así no es otro que evitar el negro y el blanco, que no quedan bien.

Ficheros tetris_random.h y tetris_random.c:

Estos ficheros son nuevos, y los vamos a usar para generar números aleatorios.

En el fichero tetris_random.h, como siempre, tenemos constantes, macros, prototipos de funciones, etc. En este caso sólo tenemos los prototipos de dos funciones nuevas:

  • random_min_max().
  • random_col().

La función random_mix_max() genera un número aleatorio en el rango min – max, que se pasan como parámetros a la función:

Se apoya en las funciones srand() y rand() de la librería estándar de C stdlib.h. La función srand() sirve para inicializar la semilla (la “s” viene de “seed”), que es el número a partir del cual se genera la serie aleatoria.

Obsérvese cómo la semilla la fijamos a partir del Jiffy Clock, que es el reloj del C64 formado por las posiciones de memoria $a0-$a1-$a2. Tomamos las posiciones $a2 y $a1, ya que la $a0 es la más significativa de las tres y, por tanto, la que varía más lentamente. Y como queremos la mayor aleatoriedad posible, en este caso nos interesan más las posiciones $a2 y $a1.

Por otro lado, la función rand() genera un valor aleatorio entre 0 y RAND_MAX que, al calcularle el resto de la división entera (%) entre (max – min) genera un número entre 0 y (max – min – 1). Por tanto, al sumarle min, al final genera un número entre min y (max – 1), que es el rango que buscamos.

Por último, la función random_col() es así:

Como se puede observar, random_col() se apoya directamente en la función random_min_max(), a la que pasa un valor mínimo y un valor máximo que dependen del tipo de ficha y su rotación. Estos valores se consultan en la tabla cols[tipo][rotacion][min/max].

Esta función nos vale para elegir aleatoriamente la columna del tablero en la que va a aparecer la ficha. En función de su tipo y rotación, la ficha puede ser un poco más ancha o un poco más estrecha y, por tanto, el rango min – max hay que ajustarlo un poco. Recordemos que la posición (columna, fila) de la ficha es la posición de su centro.

Fichero tetris_main.c:

Ya sólo queda llamar a las nuevas funciones random_min_max() y random_col() desde el fichero tetris_main.c, concretamente desde la función ficha_activa(), que es la que activa una nueva ficha:

Como se puede observar, ahora el tipo de ficha, la rotación y la columna se eligen aleatoriamente. La fila de la ficha sigue siendo siempre la fila 0.

Resultado:

El resultado es que ahora las fichas nuevas se eligen aleatoriamente, tanto el tipo, como la rotación y su posición, y que además se pintan en su color desde el comienzo:


Código de ejemplo: tetris07.zip

Caída y acumulación de las fichas

Ahora que ya hemos combinado el tablero y la ficha activa, el siguiente paso va a ser que la ficha caiga y que, al llegar abajo, se acumule sobre el fondo del tablero (y más adelante también sobre restos de otras fichas).

Hacer que la ficha caiga es fácil. Básicamente es lo mismo que mover la ficha hacia abajo, como veníamos haciendo hasta ahora. Sólo que hasta ahora, la ficha se movía hacia abajo si el jugador marcaba hacia abajo con el joystick 2, mientras que ahora la caída va a ser automática, sin intervención del jugador.

Relacionado con la caída, tenemos el estado de la ficha (variable ficha_estado). Hasta ahora no hemos prestado mucha atención a esta variable. A partir de ahora pasa a ser importante:

  • El juego empieza con la ficha en estado FICHA_ESTADO_INACTIVA.
  • Si el estado de la ficha es FICHA_ESTADO_INACTIVA el juego activa una nueva ficha, es decir, la pinta en la parte superior del tablero.
  • Y si el estado de la ficha es FICHA_ESTADO_ACTIVA el juego la hace caer hasta que se acumule en el fondo, donde volverá a pasar a inactiva, reiniciándose el todo proceso.

Los cambios necesarios son:

Ficheros tetris_ficha.h y tetris_ficha.c:

El primer cambio tiene que ver con la función ficha_inicializa(). En las entradas anteriores hemos elegido diferentes inicializaciones, por ejemplo, para pintar la ficha en el centro de la pantalla o en el centro de la parte superior del tablero. A partir de ahora, la función ficha_inciailiza() va a inicializar la ficha de verdad, es decir, va a dar valor inicial cero a todas las variables que controlan la ficha (ficha_tipo, ficha_rotacion, ficha_fila, ficha_columna, etc.). En particular, va a dar valor inicial cero, es decir, FICHA_ESTADO_INACTIVA, a la variable ficha_estado.

El segundo cambio es la nueva función ficha_copia_a_tablero(). Esta función será llamada cuando el juego detecte que la ficha ya no puede bajar más por estar sobre el fondo del tablero (o sobre restos de otras fichas más adelante). En ese momento hay que copiar la ficha activa al tablero, es decir, acumularla.

Esta nueva función es así:

Es decir, tras detectarse que la ficha activa ya no puede caer más, la función ficha_copia_a_tablero() recorre los cuatro bloques de la ficha, calcula la posición (columna, ficha) de cada bloque, y mete el tipo de ficha (ficha_tipo) en la posición equivalente de tablero. En resumen, copia la ficha al tablero.

Fichero tetris_main.c:

En este fichero hay bastantes cambios, ya que el juego se va complicando.

El primer cambio es en el bucle de juego. Antes era así:

Es decir, antes el bucle de juego era actualizar la ficha o, lo que es lo mismo, moverla y rotarla. Ahora pasa a ser así:

Por tanto, ahora el bucle de juego consiste en:

  • Si la ficha está inactiva, la activa.
  • Y si la ficha está activa, actualiza la ficha.

Activar la ficha se hace en la nueva función ficha_activa():

Esta función activa la ficha (ficha_estado = FICHA_ESTADO_ACTIVA) y, además, la coloca en el centro (ficha_c = TABLERO_MAX_C/2) de la parte superior del tablero (ficha_f = 0).

Por su parte, la función que actualiza la ficha ahora se llama ficha_actualiza(). Antes se llamaba actualiza_ficha(). Viene a ser lo mismo; sólo es una unificación de la nomenclatura.

Lo importante es que ahora ficha_actualiza() es así:

Es decir, esta función hace caer la ficha de forma automática, sin intervención del jugador, y luego la mueve, entendiendo por “mover” moverla a derecha o izquierda y rotarla.

La caída se hace con la función ficha_cae(). Y para que la caída no sea muy rápida, ficha_cae() no se llama en cada iteración del bucle de juego, sino cada vez que la variable ret llega a diez. Es decir, se invoca una de cada diez iteraciones del bucle de juego.

Este tipo de retardo es muy diferente al de la función retardo(), porque esta función retarda todo el programa que la llama, por ejemplo, todo el bucle de juego. Sin embargo, el retardo conseguido con la variable ret sólo retarda una función específica, por ejemplo, la caída. Es un retardo selectivo.

Por otro lado, el movimiento se consigue ahora con la función ficha_mueve(), que viene a ser equivalente a la actualiza_ficha() de la entrada anterior, ya que lee el joystick 2 y actúa en consecuencia, moviendo la ficha a derecha o izquierda o rotándola. Ahora ya no se permite el movimiento hacia arriba ni hacia abajo, ya que la caída es automática.

La función ficha_mueve() no tiene nada nuevo que merezca la pena comentar, pero la función ficha_cae() sí:

Es decir, esta función verifica si la ficha puede seguir bajando y:

  • Si todavía puede bajar más, la baja de forma automática, sin intervención del jugador.
  • Si ya no puede bajar más, copia la ficha al tablero con la ya comentada función ficha_copia_a_tablero(), pone la ficha inactiva, de modo que en la siguiente iteración se active otra, y pinta el tablero con las novedades.

Resultado:

El resultado es que ya tenemos un juego bastante completo. Van apareciendo fichas en la parte superior del tablero (todavía no son aleatorias). Las fichas caen y, mientras caen, se pueden mover a izquierda o derecha y rotar. Cuando las fichas llegan al fondo del tablero se acumulan.

No obstante, todavía quedan bastantes detalles que pulir:

  • Que las fichas que surgen sean aleatorias.
  • Que las fichas se pinten en su color mientras caen.
  • Que las fichas no se acumulen sólo sobre el fondo, sino también sobre otras fichas.
  • Que las filas completas se colapsen y otorguen puntos.
  • Que se detecte el final del juego cuando se llena el tablero.
  • Y algunos detalles más.

Todo esto lo iremos viendo en las entradas que siguen.


Código de ejemplo: tetris06.zip

Combinación del tablero y la ficha activa

Ha llegado el gran momento: combinar el tablero y la ficha activa. Esto implica fundamentalmente dos cosas:

  • A la hora de pintar y borrar la ficha activa hay que tener en cuenta que su posición (columna, fila) es con relación al tablero, es decir, para conocer su posición absoluta con relación a la pantalla habrá que tener en cuenta la posición del tablero dentro de la pantalla.
  • A la hora de mover y rotar la ficha activa, antes hay que verificar si ese movimiento o rotación es posible, ya que ahora podría ocurrir que nos saliéramos de los límites del tablero (y más adelante que pisáramos otras fichas o restos de fichas).

Pues bien, esto se plasma en los siguientes cambios:

Ficheros tetris_ficha.h y tetris_ficha.c:

El primer cambio tiene que ver con la función ficha_incializa(). Ya no vamos a inicializar la ficha al centro de la pantalla, sino al centro de la parte superior del tablero (ficha_c = TABLERO_MAX_C/2, ficha_f = 1).

El segundo cambio tiene que ver con el pintado y el borrado de las fichas. Como la posición (ficha_c, ficha_f) es relativa al tablero, hay que tener en cuenta que el tablero se pinta en (TABLERO_X0, TABLERO_Y0) para hacer la conversión de (columna, fila) a (x, y). Esto se puede ver en ficha_pinta() y en ficha_borra():

Por último, el cambio más importante es el que tiene que ver con la aparición de las funciones:

  • ficha_verifica_izquierda().
  • ficha_verifica_derecha().
  • ficha_verifica_abajo().
  • ficha_verifica_arriba().
  • ficha_verifica_rotacion_derecha().
  • ficha_verifica_rotacion_izquierda().

Las cuatro primeras tienen que ver con verificar si el movimiento es posible. Y las dos últimas tienen que ver con verificar si la rotación es posible. Pero, en el fondo, todas siguen el mismo esquema:

  • Recorrer los cuatro bloques de la ficha.
  • Calcular su nueva posición si el movimiento o rotación tuviera lugar.
  • Verificar que la nueva (columna, fila) del bloque caería dentro de los límites del tablero.

Más adelante mejoraremos estas funciones para tener en cuenta también si el movimiento o rotación implicarían pisar otras fichas o restos de fichas. Pero de momento nos vale con esto.

A modo de ejemplo, ficha_verifica_izquierda() es así:

Ficheros tetris_tablero.h y tetris_tablero.c:

En estos ficheros apenas hay cambios. Se trata, simplemente, de dejar de pintar el borde superior del tablero (se comenta esa línea):

Fichero tetris_main.c:

En este fichero hay varios cambios. En primer lugar, en la función inicializa() el tablero ya se inicializa vacío, no con contenido, y se recupera la inicialización y el pintado de la ficha activa.

En segundo lugar, en la función actualiza_ficha(), los movimientos y rotaciones van precedidos por las verificaciones oportunas, es decir, por llamadas a las funciones ficha_verifica_*() correspondientes.

Por último, en el bucle de juego recuperamos la llamada a actualiza_ficha(), porque ahora volvemos a querer que se lea el joystick 2 y que se mueva y rote la ficha según corresponda (previa verificación, claro).

Resultado:

El resultado es algo así:

Es decir, ya es posible mover y rotar la ficha dentro del tablero. De momento, sólo se respetan los límites del tablero. Más adelante habrá que respetar otras limitaciones, como no pisar fichas o restos de fichas, pero para hacer eso antes será preciso empezar a acumular las fichas.


Código de ejemplo: tetris05.zip

Definición del tablero

Ahora vamos a dar otro paso importante: meter el tablero. El tablero es el “campo de juego” en el que aparecen, caen y se acumulan las fichas.

Desde el punto de vista gráfico, el tablero es un rectángulo de N x M casillas. Concretamente, en nuestro caso lo vamos a hacer de 20 filas x 15 columnas. Sin embargo, desde el punto de vista de la programación, ¿qué estructura le damos? ¿Qué estructura de datos utilizamos?

En el tablero va a haber casillas vacías y fichas acumuladas. Por tanto, podríamos pensar en una lista de fichas con su tipo, su rotación y su posición, por ejemplo. Sin embargo, cuando una fila se llena y se colapsa, algunas de sus fichas desaparecen parcialmente, quedando sólo algunos de sus bloques a modo de restos.

Por tanto, la mejor opción es que el tablero sea una tabla de 20 x 15 casillas. Estas casillas empezarán el juego vacías y, según vayan cayendo y acumulándose fichas, iremos copiando esas fichas “acumuladas” al tablero. Pero no como fichas, sino como bloques individuales con su color correspondiente. De este modo, cuando una fila se llene y se colapse, será tan fácil como hacer caer o mover hacia abajo en la tabla todas las filas y bloques que están por encima.

Como podéis ver, esto significa que la ficha activa y el tablero se van a manejar como estructuras de datos independientes, aunque la ficha activa luego se pinte sobre el tablero. Ambas estructuras sólo se relacionan cuando la ficha activa cae y se acumula. En ese momento habrá una copia de datos desde la ficha activa hasta el tablero. Y vuelta a empezar con otra ficha.

También es interesante observar que vamos a separar claramente entre las estructuras de datos para guardar la información de la ficha activa y el tablero, y la pantalla y su memoria ($0400 – $07e7). Es decir, el juego controlará el tipo de ficha, su rotación, su posición y el contenido del tablero mediante unas estructuras de datos en memoria, y luego pintará esa información a pantalla cuando sea necesario, pero ambas informaciones, variables y pantalla, no van a estar mezcladas ni van a ser la misma.

Aclaro esto porque esta separación tan clara entre “capa de datos” y “capa de presentación” no se estilaba tanto en los 80, sino que es una técnica de programación que empezó a utilizarse bastante más tarde, por ejemplo, en aplicaciones web. En los 80, en ordenadores como el C64 y similares, era bastante habitual usar la memoria de pantalla tanto para pintar y comunicar información al usuario, como para guardar información de estado del programa y hacer controles sobre ella.

Me refiero a cosas del tipo, si un jugador está en la posición (X = 20, Y = 12), que equivale a la posición de pantalla $05f4, y la posición de al lado, que es la $05f5 está libre, entonces el jugador puede moverse a la derecha. Obsérvese que este control se hace directamente sobre la pantalla (posición $05f5), no sobre una estructura de datos con una copia del contenido de la pantalla.

En nuestro caso, los controles no los haremos directamente contra la pantalla, sino contra la estructura de datos del tablero, que luego copiaremos a la pantalla cuando toque.

Ficheros tetris_ficha.h y tetris_ficha.c:

El punto anterior (que una cosa es la estructura de datos del tablero y otra la pantalla), tiene una implicación muy importante:

Hasta ahora, la ficha tenía una posición (x, y) que venía marcada por las variables ficha_x y ficha_y. Pero ahora, nuestro interés no es tanto la posición (x, y) de pantalla donde está o se va a pintar la ficha, sino la posición (fila, columna) de la ficha con relación al tablero.

Si el tablero lo pintáramos directamente en la esquina superior izquierda de la pantalla, es decir, en la posición (0, 0), ambas cosas coincidirían, pero en general querremos pintar el tablero en una zona central de la pantalla.

Por tanto, para distinguir claramente entre la posición que ocupa la ficha dentro del tablero (este es el dato más importante a efectos del juego y los controles asociados) y la posición de pantalla donde se pinta la ficha (este dato es menos relevante), usaremos la notación (fila, columna) para el primer caso, y la notación (x, y) para el segundo.

Y este es el principal cambio en los ficheros tetris_ficha.h y tetris_ficha.c: donde antes se hablaba de ficha_x y ficha_y ahora se habla de ficha_f y ficha_c, porque el dato que más nos interesa es la (fila, columna) que ocupa la ficha respecto al tablero.

Por otro lado, también desaparece la variable ficha_color, ya que el color ahora lo vamos a vincular al tipo de ficha mediante la nueva función ficha_color().

Ficheros tetris_tablero.h y tetris_tablero.c:

En el fichero tetris_tablero.h tenemos la definición de constantes importantes, como el tamaño del tablero (TABLERO_MAX_F y TABLERO_MAX_C), la posición del tablero respecto a la pantalla (TABLERO_X0 y TABLERO_Y0), es decir, dónde se va a empezar a pintar el tablero dentro de la pantalla, y cómo vamos a codificar las posiciones vacías (TABLERO_VACIO).

Respecto a las posiciones vacías, parece que lo que le pide a uno el cuerpo es codificar la posición vacía con un cero pero, como cuando caigan las fichas y se acumulen en la parte inferior las copiaremos al tablero, y como los tipos de fichas van del cero al seis (siete tipos de fichas), resulta práctico identificar las fichas (o más bien sus bloques) mediante su tipo, que de paso nos servirá para deducir el color a pintar. Por eso se toma TABLERO_VACIO = 9, ya que el 9 no es un tipo de ficha válido, por mucho que pueda resultar extraño u original…

Por lo demás, en tetris_tablero.h también tenemos los prototipos de las funciones, que lógicamente se implementan en tetris_tablero.c. Respecto a éstas, la función tablero_inicializa_vacio() inicializa el tablero vacío, lo cual será lo normal, y tablero_inicializa_contenido() lo inicializa con fichas de tipos variados, lo cual nos servirá para probar que otras funciones como tablero_pinta() funcionan bien.

De hecho, tablero_pinta() es la función estrella, ya que se encarga de pintar el tablero en pantalla (sin la ficha activa que se maneja y pinta aparte). En el fondo, es fácil; se trata de recorrer la matriz del tablero por columnas y filas y pintar espacios allí donde el tablero está vacío (el espacio no necesita un color, porque el espacio es transparente) y un cuadrado allí donde hay una ficha o bloque (el bloque sí necesita color):

Por lo demás, la función tablero_pinta_borde() y funciones asociadas sirven para dotar al tablero de un borde.

Y la función tablero_pinta_datos() es similar a tablero_pinta() pero, en vez de pintar espacio o un bloque de color en función del contenido del tablero, pinta el valor numérico de cada posición, de modo que se puede depurar el programa, ver si las fichas se copian bien al tablero al caer y acumularse, comprobar si la función tablero_pinta() pinta lo que corresponde, etc. En definitiva, vale para depurar el programa.

Fichero tetris_main.c:

Una vez que tenemos los mimbres nos falta tejer el cesto. Y el “cesto” es el tetris_main.c.

Los principales cambios respecto a la versión anterior son que, al inicializar el juego, además de borrar la pantalla vamos a ponerla en negro (tanto el borde como el fondo), y vamos a inicializar el tablero (de momento con contenido variado, no vacío) y pintarlo, incluyendo el tablero propiamente dicho, su borde, y sus datos para depurar.

De momento, el bucle de juego lo comentamos, ya que la ficha la meteremos en la siguiente iteración.

El resultado es algo así:

Cosas a observar:

  • A la izquierda tenemos el tablero con un contenido inicial, no vacío. En el juego real el tablero empezará vacío, y se irá llenando según caen y se acumulan las fichas.
  • Además del tablero propiamente dicho, tenemos el borde. En el juego real, quitaremos el borde superior porque es por donde “entran” las fichas.
  • A la derecha, tenemos el contenido del tablero. Es otra forma de verlo que nos vale para depurar. Obsérvese que donde hay un 9 vemos un cuadrado negro, es decir, un espacio. Y donde hay 0 – 6, vemos un bloque del color correspondiente.

Siguiente paso: empezar con un tablero vacío y añadirle la ficha.


Código de ejemplo: tetris04.zip

Definición de todos los tipos de fichas

Bueno, pues ya sabemos definir una ficha, incluidas sus cuatro rotaciones, rotarla y moverla con el joystick 2. El siguiente paso, por tanto, va a ser definir las siete fichas que tiene el Tetris clásico:

En el fondo, se trata tan sólo de modificar el fichero tetris_ficha.c, de modo que la anterior tabla fichas_def[][], que tenía las dimensiones [rotación][bloque], ahora pasa a ser de tres dimensiones, fichas_def[][][], que son [tipo][rotación][bloque]:

De este modo, al pintar una ficha con ficha_pinta() o borrarla con ficha_borra(), no sólo tenemos que tener en cuenta la rotación y recorrer los cuatro bloques activos, sino también tener en cuenta el tipo de ficha:

Dotamos, además, una nueva función ficha_inc_tipo(), que servirá para incrementar el tipo, es decir, pasar de un tipo al siguiente. Esta función la usaremos durante las pruebas del desarrollo, ya que en el juego real el tipo de las fichas se elige aleatoriamente.

Por último, sólo queda modificar la función actualiza_ficha() del fichero tetris_main.c:

Ahora el uso del joystick 2 será así:

  • Izquierda: La ficha se mueve a la izquierda.
  • Derecha: La ficha se mueve a la derecha.
  • Arriba: Cambia el tipo de ficha.
  • Abajo: La ficha baja.
  • Disparo: La ficha rota a derechas.

De este modo ahora, usando el joystick, podemos rotar la ficha, moverla –salvo hacia arriba– e, incluso, cambiar su tipo:


Código de ejemplo: tetris03.zip

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