Caída a plomo de las fichas

Como última mejora del juego, vamos a hacer que las fichas puedan caer a plomo.

La caída a plomo se produce cuando el jugador cree que ya ha colocado la ficha en la posición y rotación correctas. En ese momento, si pulsa el joystick hacia abajo, la ficha cae rápidamente, sin posibilidad de moverla o rotarla más.

Hasta ahora, la ficha podía estar en uno de dos estados:

  • O inactiva (FICHA_ESTADO_INACTIVA), en cuyo caso se activaba y pintaba una ficha nueva.
  • O activa (FICHA_ESTADO_ACTIVA), en cuyo caso la ficha caía, se podía mover y rotar, si el jugador así lo quería, y se acumulaba al llegar al fondo del tablero.

Pues bien, ahora vamos a definir un tercer estado FICHA_ESTADO_PLOMO, que no deja de ser una especie de variante del estado FICHA_ESTADO_ACTIVA. Cuando la ficha esté en este nuevo estado caerá sin retardo, es decir, rapidito, y sin posibilidad de que el jugador la mueva o la rote.

Los cambios se concentran en el fichero tetris_main.c.

Fichero tetris_main.c:

En primer lugar, tenemos que detectar que el jugador ha tirado hacia abajo del joystick:

Cuando ocurre lo anterior, la ficha deja de estar en estado FICHA_ESTADO_ACTIVA y pasa a estar en estado FICHA_ESTADO_PLOMO. El nuevo estado se reconoce y se trata en el bucle de juego:

Y, por último, tenemos la nueva función ficha_plomo() que viene a ser equivalente a ficha_actualiza(), pero sin retardo y sin posibilidad de mover ni rotar:

Resultado:

El resultado es que el jugador ya puede lanzar la ficha a plomo, una vez que piensa que ya la tiene correctamente colocada. Esta función está disponible en la mayoría de implementaciones de Tetris.


Código de ejemplo: tetris13.zip

Diferentes niveles de dificultad

Cuando diseñamos la caída de las fichas vimos que, para que la caída no fuera muy rápida, convenía introducir un retardo (recordemos la variable ret):

Pues bien, lo que vamos a hacer ahora es complicar un poco el juego para que ese retardo sea mayor o menor en función de un nivel que elige el jugador. Lógicamente, a mayor nivel menor retardo o mayor velocidad.

De paso, vamos a aprovechar también para mover las variables líneas, puntos y max_puntos desde el fichero tetris_tablero.c al fichero tetris_main.c.

Líneas, puntos y max_puntos:

Las variables líneas, puntos y max_puntos las pusimos en su momento, no sin dudas, en el fichero tetris_tablero.c. Recapacitando, lo cierto es que esas variables tienen más que ver con el proceso principal del juego y su control que con el tablero.

Por este motivo, estas variables las movemos desde el fichero tetris_tablero.c al fichero tetris_main.c. Y algo similar hacemos con las funciones asociadas a estas variables:

  • La anterior función tablero_inicializa_puntos() se diluye en la función inicializa_partida().
  • Y la anterior función tablero_pinta_puntos() se convierte en la función pinta_puntos().

Bueno, esto no es un cambio muy profundo. Es más bien una reubicación de funcionalidad.

Nivel y retardo:

Este es el cambio importante. Consiste en que el jugador elige un nivel y, en función de ese nivel, así será el retardo de la caída.

Todo esto lo controlamos con las nuevas variables nivel y ret_max:

La variable nivel la inicializamos a 1 en la inicialización del juego, pero luego el jugador podrá cambiar ese valor:

La forma de cambiar el nivel es mediante una nueva implementación de la función joystick_empezar():

Ahora, además de pulsar disparo para empezar el juego, el jugador podrá mover el joystick arriba para subir el nivel (hasta 5) o abajo para bajarlo (hasta 1). Posteriormente, el retardo (nueva variable ret_max) se define como:

ret_max = 10 – nivel

Y ya sólo que cambiar la función ficha_actualiza() para que, en vez de usar un retardo fijo de 10 iteraciones, utilice un retardo variable ret_max que depende del nivel elegido:

Resultado:

El resultado es que ahora el jugador puede elegir el nivel y, en función del nivel, así es el retardo con que ocurre la caída de las fichas (nivel 1 => retardo de 9 iteraciones; nivel 5 => retardo de 5 iteraciones):


Código de ejemplo: tetris12.zip

Juego multipartida

Ahora que ya somos capaces de detectar el final de partida, el siguiente paso es que el juego sea multipartida, es decir que el jugador pueda jugar varias partidas seguidas antes de terminar el juego o apagar el C64.

De paso, suele ser habitual llevar cuenta de la puntuación máxima (recordemos que hace un par de entradas empezamos a controlar líneas y puntos). No es algo tan completo como llevar un ranking de jugadores y puntuaciones, pero algo es algo.

Para conseguir lo anterior, los cambios se focalizan en el programa principal, es decir, en tetris_main.c. No obstante, también hay algún pequeño retoque en tetris_tablero.c.

Fichero tetris_tablero.c:

En este fichero complementamos las variables líneas y puntos con una nueva variable max_puntos:

Ya dijimos en algún apartado anterior que estas variables las reubicaremos más adelante.

Por otro lado, también modificamos la función tablero_pinta_puntos() para que, además de pintar las líneas y los puntos, también pinte el máximo de puntos:

Por último, en la función tablero_filas_llenas(), que sirve para detectar las filas llenas y colapsarlas, si la puntuación actual supera el máximo, tenemos que actualizar el máximo:

Esto también podría hacerse al final de cada partida, pero casi está mejor hacerlo sobre la marcha.

Fichero tetris_main.c:

Los cambios fuertes están en este segundo fichero. Para empezar, dejamos de tener una inicialización única (función inicializa()) para tener una inicialización del juego (nueva función inicializa_juego()) y una inicialización de la partida (nueva función inicializa_partida()).

La primera función inicializa todo aquello que tiene que inicializarse una única vez:

  • Carga el driver del joystick.
  • Cambia los colores del fondo y del borde.
  • Y pone max_puntos a cero.

La segunda, por su lado, inicializa todo aquello que hay que inicializar en cada partida (recordemos que ahora vamos a tener varias partidas):

  • Borra la pantalla.
  • Inicializa el tablero vacío y lo pinta.
  • Inicializa los puntos y los pinta.
  • Inicializa la ficha.

Por otro lado, el cambio principal está en el bucle de juego. Si antes era así:

Ahora pasa a ser así:

Es decir, ahora tenemos:

  • Un bucle interior que es propiamente el bucle de juego.
  • Y un bucle exterior que es el bucle de las diferentes partidas.

Si nos fijamos en el bucle exterior, que es el nuevo:

  • Inicializa la partida con inicializa_partida().
  • Con joystick_empezar() espera a que el jugador pulse el disparo para empezar la partida.
  • Tiene lugar la partida, es decir, el bucle de juego.
  • Cuando termina la partida, con joystick_continuar() espera a que el jugador pulse el disparo para continuar con otra partida.

Las funciones joystick_empezar() y joystick_continuar() son nuevas, y básicamente muestran un mensaje, que es diferente en cada función, esperan a que el jugador pulse el disparo, y borran el mensaje:

Resultado:

El resultado es que ahora ya es posible jugar varias partidas seguidas. Antes de cada partida, el jugador tiene que pulsar el disparo, y al terminar la partida también, para así continuar con otra partida.


Código de ejemplo: tetris11.zip

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