26. Avanzando Columnas: segunda parte de la lógica de juego.(*)

La entrega anterior ya permitía que cayeran las piezas, y que pudiéramos "rotarlas" mientras caían. Aun así, quedaban varias cosas para que el juego realmente fuera jugable, como por ejemplo:

  • Las piezas "caían" siempre hasta el fondo de la pantalla, aunque ya hubiera otras piezas por debajo, en vez de depositarse sobre ellas.
  • Las piezas se podían mover a un lado y a otro, aunque existieran otras piezas con las que "chocaran", en vez de quedar bloquedas por ellas.
  • Cuando las piezas habían terminado de caer, no se eliminaban los fragmentos de el mismo color que estuvieran en línea.

 
Vayamos por partes...

Para que la pieza no caiga siempre hasta el fondo, sino que se compruebe si en algún momento toca alguna otra pieza anterior, deberemos cambiar un poco la comprobación de si debe dejar de moverse. Antes era:

 
if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA)
  colocarPiezaEnFondo(); 
 

Y ahora en su lugar podríamos crear una función más completa, que además de comprobar si ha llegado a la parte inferior la pantalla, viera si existe ya alguna pieza en el fondo ("tablero"), debida a algún movimiento anterior. Para eso calculamos la posición X y la posición Y de la parte inferior de nuestra pieza, y miramos si en el tablero

 
// -- Devuelve "true" si toca fondo y no puede bajar mas
bool tocadoFondo() {
  bool colision = false;
  // Si llega abajo del todo
  if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA)
    colision = true;
  // o si toca con una pieza interior
  int posX = (x-MARGENIZQDO)/TAMANYOPIEZA;
  int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3;
  if (tablero[posX][posYfinal] != -1)
    colision = true;
  return colision;   
}
 

De modo que ahora "intentarBajar" quedaría así, usando esta nueva función:

 
// -- Intenta mover la "pieza" hacia abajo
void intentarBajar() {
  y += incrY;
  if ( tocadoFondo() )
    colocarPiezaEnFondo(); 
}
 

De igual modo, al mover hacia los lados, podríamos comprobar si colisiona con:

 
// -- Intenta mover la "pieza" hacia la izquierda
void intentarMoverIzquierda() {
  if (x > MARGENIZQDO) // Si no he llegado al margen
  {
    // Y la casilla no esta ocupada
    int posX = (x-incrX-MARGENIZQDO)/TAMANYOPIEZA;
    int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3;
    if (tablero[posX][posYfinal] == SINPIEZA)
      x -= incrX;
  }
}
 

Y además, despues de colocar la pieza como parte del fondo deberemos eliminar los bloques de piezas iguales que estén conectados:

 
// -- Coloca una pieza como parte del fondo, cuando
// llegamos a la parte inferior de la pantalla
// o tocamos una pieza inferior colocada antes
void colocarPiezaEnFondo(){
  int i;
  for (i=0; i<3; i++) {
    tablero
      [ (x-MARGENIZQDO)/TAMANYOPIEZA ]
      [ (y-MARGENSUP)/TAMANYOPIEZA + i] 
        = tipoFragmento[i];
  }
  eliminarBloquesConectados();
  crearNuevaPieza();
}
 
 

Una forma sencilla (pero no totalmente fiable) de comprobar si 3 piezas están alineadas en horizontal podría ser

 
for (i=0; i<ANCHOTABLERO-2; i++)
  for (j=0; j<ALTOTABLERO; j++)
    if (tablero[i][j] != SINPIEZA) // Si hay pieza
      if ((tablero[i][j] == tablero[i+1][j]) && (tablero[i][j] == tablero[i+2][j]))
      {
        tablero[i][j] = PIEZAEXPLOSION;
        tablero[i+1][j] = PIEZAEXPLOSION;
        tablero[i+2][j] = PIEZAEXPLOSION;
        hayQueBorrar = true;
      }  
 

Si hay 3 piezas alineadas, las convertimos en "piezas que explotan" (PIEZAEXPLOSION), como paso previo a que desaparezcan. Esta forma de comprobar si están en línea no es la mejor: fallará cuando haya 4 piezas iguales en horizontal, y sólo señalará 3 de ellas. Aun así, como primera aproximación puede servir, y la terminaremos de corregir en la siguiente entrega. La forma de comprobar si hay 3 piezas en vertical sería básicamente igual:

 
  for (i=0; i<ANCHOTABLERO; i++)
    for (j=0; j<ALTOTABLERO-2; j++)
      if (tablero[i][j] != SINPIEZA) // Si hay pieza    
        if ((tablero[i][j] == tablero[i][j+1]) && (tablero[i][j] == tablero[i][j+2]))
        {
          tablero[i][j] = PIEZAEXPLOSION;
          tablero[i][j+1] = PIEZAEXPLOSION;
          tablero[i][j+2] = PIEZAEXPLOSION;
          hayQueBorrar = true;
        }
 

Y tampoco hay grandes cambios si están alineadas en diagonal, caso en el que aumentaremos X e Y a la vez, o disminuiremos una mientras aumentamos la otra:

 
// Diagonal 1
for (i=0; i<ANCHOTABLERO-2; i++)
  for (j=0; j<ALTOTABLERO-2; j++)
    if (tablero[i][j] != SINPIEZA) // Si hay pieza    
      if ((tablero[i][j] == tablero[i+1][j+1]) && (tablero[i][j] == tablero[i+2][j+2]))
      {
        tablero[i][j] = PIEZAEXPLOSION;
        tablero[i+1][j+1] = PIEZAEXPLOSION;
        tablero[i+2][j+2] = PIEZAEXPLOSION;
        hayQueBorrar = true;
      }
 
// Diagonal 2
for (i=2; i<ANCHOTABLERO; i++)
  for (j=0; j<ALTOTABLERO-2; j++)
    if (tablero[i][j] != SINPIEZA) // Si hay pieza    
      if ((tablero[i][j] == tablero[i-1][j+1]) && (tablero[i][j] == tablero[i-2][j+2]))
      {
        tablero[i][j] = PIEZAEXPLOSION;
        tablero[i-1][j+1] = PIEZAEXPLOSION;
        tablero[i-2][j+2] = PIEZAEXPLOSION;
        hayQueBorrar = true;
      }
 

Finalmente, cuando ya hemos marcado con la "explosión" todas las piezas que debemos borrar, las mostramos un instante:

 
if (hayQueBorrar) {
   dibujarElementos();
   rest(200);
   ...
 

y después las borramos, dejando "caer" las piezas que tuvieran por encima:

 
 // Mientras haya "explosiones", recoloco
 algoHaCambiado = false;
 for (i=0; i<ANCHOTABLERO; i++)
   for (j=ALTOTABLERO-1; j>=0; j--) // Abajo a arriba
     if (tablero[i][j] == PIEZAEXPLOSION)
     {
         // Bajo las superiores
         for (k=j; k>0; k--)
           tablero[i][k] = tablero[i][k-1];
         tablero[i][0] = SINPIEZA;
     }
 

Y finalmente, si realmente hemos borrado alguna pieza, hacemos una pequeña pausa y mostramos cómo queda la pantalla de juego. Además, en ese caso deberemos comprobar si al caer las piezas se ha formado algún nuevo bloque que hay que eliminar:

 
     if (algoHaCambiado) {
       dibujarElementos();
       rest(200);
       eliminarBloquesConectados();
     }
 

Por supuesto, quedan cosas por hacer: la rutina de marcado de piezas no es buena (falla si hay 4 o más en línea), la presentación es muy pobre, y no se lleva cuenta de la puntuación. Por tanto, prepararemos una cuarta entrega que borre correctamente, que dibuje los límites de la pantalla de juego y que calcule y muestre la puntuación de la partida.

El fuente completo de esta tercera entrega podría ser;

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*    ipj26c.c                  */
/*                              */
/*  Ejemplo:                    */
/*    Tercer acercamiento a     */
/*    Columnas                  */
/*                              */
/*  Comprobado con:             */
/*  - DevC++ 4.9.9.2(gcc 3.4.2) */
/*    y Allegro 4.03 - WinXP    */
/*------------------------------*/
 
 
 
#include <allegro.h>
 
/* -------------- Constantes globales ------------- */
#define ANCHOPANTALLA 640
#define ALTOPANTALLA 480
 
#define ANCHOTABLERO 10
#define ALTOTABLERO  14
 
#define MARGENSUP   16
#define MARGENDCHO  160
#define MARGENIZQDO 160
#define MARGENINF   16
 
// Nmero de fragmentos distintos con los que
// formar las piezas
#define NUMFRAGMENTOS 7
 
// Ancho y alto de cada pieza
#define TAMANYOPIEZA 32
 
// Pieza especial como paso previo cuando explotan
#define PIEZAEXPLOSION 6
#define SINPIEZA -1
 
/* -------------- Variables globales -------------- */
PALETTE pal;
BITMAP *imagenFragmentos;
BITMAP *pantallaOculta;
 
BITMAP *pieza[NUMFRAGMENTOS];
 
int partidaTerminada;
int x = MARGENIZQDO + TAMANYOPIEZA*5;
int y = MARGENSUP;
int incrX = TAMANYOPIEZA;
int incrY = 4;
int tecla;
 
// Tipo de imagen que corresponde a cada fragmento de pieza
int tipoFragmento[3];
 
// El tablero de fondo
int tablero[ANCHOTABLERO][ALTOTABLERO];
 
// Contador de fotogramas, para regular la velocidad de juego
int contadorFotogramas = 0;
 
// Prototipos de las funciones que usaremos
void comprobarTeclas();
void moverElementos();
void comprobarColisiones();
void dibujarElementos();
void pausaFotograma();
void intentarMoverDerecha();
void intentarMoverIzquierda();
void intentarBajar();
void rotarColores();
void colocarPiezaEnFondo();
 
 
 
// --- Bucle principal del juego -----
void buclePrincipal() {
  partidaTerminada = FALSE;
  do {
    comprobarTeclas();
    moverElementos();
    comprobarColisiones();
    dibujarElementos();
    pausaFotograma();
  } while (partidaTerminada != TRUE);
}
 
 
// -- Comprobac de teclas para mover personaje o salir
void comprobarTeclas() {   
 
  if (keypressed()) {
    tecla = readkey() >> 8;
    if ( tecla == KEY_ESC )
      partidaTerminada = TRUE;
    if ( tecla == KEY_RIGHT )
       intentarMoverDerecha();   
    if ( tecla == KEY_LEFT )
       intentarMoverIzquierda();
    if ( tecla == KEY_DOWN )
       intentarBajar();
    if (( tecla == KEY_SPACE ) || ( tecla == KEY_UP ))
       rotarColores();
     clear_keybuf();
   }
}
 
 
// -- Intenta mover la "pieza" hacia la derecha
void intentarMoverDerecha() {
   // Si no he llegado al margen
  if (x < ANCHOPANTALLA-MARGENDCHO-TAMANYOPIEZA)
  {
    // Y la casilla no esta ocupada
    int posX = (x+incrX-MARGENIZQDO)/TAMANYOPIEZA;
    int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3;
    if (tablero[posX][posYfinal] == SINPIEZA)
      x += incrX;
  }
}
 
 
// -- Intenta mover la "pieza" hacia la izquierda
void intentarMoverIzquierda() {
  if (x > MARGENIZQDO) // Si no he llegado al margen
  {
    // Y la casilla no esta ocupada
    int posX = (x-incrX-MARGENIZQDO)/TAMANYOPIEZA;
    int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3;
    if (tablero[posX][posYfinal] == SINPIEZA)
      x -= incrX;
  }
}
 
 
// -- Devuelve "true" si toca fondo y no puede bajar mas
bool tocadoFondo() {
  bool colision = false;
  // Si llega abajo del todo
  if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA)
    colision = true;
  // o si toca con una pieza interior
  int posX = (x-MARGENIZQDO)/TAMANYOPIEZA;
  int posYfinal = (y-MARGENSUP)/TAMANYOPIEZA + 3;
  if (tablero[posX][posYfinal] != SINPIEZA)
    colision = true;
  return colision;   
}
 
// -- Intenta mover la "pieza" hacia abajo
void intentarBajar() {
  y += incrY;
  if ( tocadoFondo() )
    colocarPiezaEnFondo(); 
}
 
 
// -- Rotar los colores de la "pieza"
void rotarColores() {
  int auxiliar = tipoFragmento[0];
  tipoFragmento[0] = tipoFragmento[1];
  tipoFragmento[1] = tipoFragmento[2];
  tipoFragmento[2] = auxiliar;
}
 
 
// -- Mover otros elementos del juego 
void moverElementos() {
  // Las piezas bajan solas, pero unicamente un
  // fotograma de cada 10
  contadorFotogramas ++;
  if (contadorFotogramas >= 10) {   
    y += incrY;
    contadorFotogramas = 0;
    if ( tocadoFondo() )
      colocarPiezaEnFondo(); 
  }
}
 
 
// -- Comprobar colisiones de nuestro elemento con otros, o disparos con enemigos, etc
void comprobarColisiones() {
  // Por ahora, no hay colisiones que comprobar
}
 
 
// -- Dibujar elementos en pantalla
void dibujarElementos() {
  int i,j;
 
  // Borro la pantalla y dibujo la pieza
  clear_bitmap(pantallaOculta);
 
  // Dibujo el "fondo", con los trozos de piezas anteriores
  for (i=0; i<ANCHOTABLERO; i++)
      for (j=0; j<ALTOTABLERO; j++)
        if (tablero[i][j] != SINPIEZA)
          draw_sprite(pantallaOculta, 
            pieza[ tablero[i][j]  ], 
            MARGENIZQDO + TAMANYOPIEZA*i, 
            MARGENSUP + TAMANYOPIEZA*j);      
 
  // Dibujo la "pieza"
  for (i=0; i<3; i++)
    draw_sprite(pantallaOculta, 
      pieza[ tipoFragmento[i] ], x, y+32*i);
 
  // Sincronizo con el barrido para evitar parpadeos
  // y vuelco la pantalla oculta
  vsync();
  blit(pantallaOculta, screen, 0, 0, 0, 0,
    ANCHOPANTALLA, ALTOPANTALLA);
 
}
 
 
// -- Pausa hasta el siguiente fotograma
void pausaFotograma() {
  // Para 25 fps: 1000/25 = 40 milisegundos de pausa
  rest(40);
}
 
 
// -- Crea una nueva pieza con componentes al azar
void crearNuevaPieza() {
  int i;
  for (i=0; i<3; i++)
    tipoFragmento[i] = rand() % (NUMFRAGMENTOS-1);
  x = MARGENIZQDO + TAMANYOPIEZA*5;
  y = MARGENSUP;  
}
 
 
// -- Revisa el fondo para comprobar si hay varias
// piezas iguales unidas, que se puedan eliminar
void eliminarBloquesConectados() {     
  int i,j,k;
  bool hayQueBorrar = false;
  // Busco en horizontal
  for (i=0; i<ANCHOTABLERO-2; i++)
    for (j=0; j<ALTOTABLERO; j++)
      if (tablero[i][j] != SINPIEZA) // Si hay pieza
        if ((tablero[i][j] == tablero[i+1][j]) && (tablero[i][j] == tablero[i+2][j]))
        {
          tablero[i][j] = PIEZAEXPLOSION;
          tablero[i+1][j] = PIEZAEXPLOSION;
          tablero[i+2][j] = PIEZAEXPLOSION;
          hayQueBorrar = true;
        }  
 
  // Busco en vertical
  for (i=0; i<ANCHOTABLERO; i++)
    for (j=0; j<ALTOTABLERO-2; j++)
      if (tablero[i][j] != SINPIEZA) // Si hay pieza    
        if ((tablero[i][j] == tablero[i][j+1]) && (tablero[i][j] == tablero[i][j+2]))
        {
          tablero[i][j] = PIEZAEXPLOSION;
          tablero[i][j+1] = PIEZAEXPLOSION;
          tablero[i][j+2] = PIEZAEXPLOSION;
          hayQueBorrar = true;
        }
 
  // Diagonal 1
  for (i=0; i<ANCHOTABLERO-2; i++)
    for (j=0; j<ALTOTABLERO-2; j++)
      if (tablero[i][j] != SINPIEZA) // Si hay pieza    
        if ((tablero[i][j] == tablero[i+1][j+1]) && (tablero[i][j] == tablero[i+2][j+2]))
        {
          tablero[i][j] = PIEZAEXPLOSION;
          tablero[i+1][j+1] = PIEZAEXPLOSION;
          tablero[i+2][j+2] = PIEZAEXPLOSION;
          hayQueBorrar = true;
        }
 
  // Diagonal 2
  for (i=2; i<ANCHOTABLERO; i++)
    for (j=0; j<ALTOTABLERO-2; j++)
      if (tablero[i][j] != SINPIEZA) // Si hay pieza    
        if ((tablero[i][j] == tablero[i-1][j+1]) && (tablero[i][j] == tablero[i-2][j+2]))
        {
          tablero[i][j] = PIEZAEXPLOSION;
          tablero[i-1][j+1] = PIEZAEXPLOSION;
          tablero[i-2][j+2] = PIEZAEXPLOSION;
          hayQueBorrar = true;
        }
 
  // Borro, si es el caso
  if (hayQueBorrar) {
     dibujarElementos();
     rest(200);
     bool algoHaCambiado;
     do {
       // Mientras haya "explosiones", recoloco
       algoHaCambiado = false;
       for (i=0; i<ANCHOTABLERO; i++)
         for (j=ALTOTABLERO-1; j>=0; j--) // Abajo a arriba
           if (tablero[i][j] == PIEZAEXPLOSION)
           {
             algoHaCambiado = true;
               // Bajo las superiores
               for (k=j; k>0; k--)
                 tablero[i][k] = tablero[i][k-1];
               tablero[i][0] = SINPIEZA;
           }
     } while (algoHaCambiado);
     // Y vuelvo a comprobar si hay nuevas cosas que borrar   
     if (algoHaCambiado) {
       dibujarElementos();
       rest(200);
       eliminarBloquesConectados();
     }
  }
 
}
 
// -- Coloca una pieza como parte del fondo, cuando
// llegamos a la parte inferior de la pantalla
// o tocamos una pieza inferior colocada antes
void colocarPiezaEnFondo(){
  int i;
  for (i=0; i<3; i++) {
    tablero
      [ (x-MARGENIZQDO)/TAMANYOPIEZA ]
      [ (y-MARGENSUP)/TAMANYOPIEZA + i] 
        = tipoFragmento[i];
  }
  eliminarBloquesConectados();
  crearNuevaPieza();
}
 
 
/* -------------- Rutina de inicializacin -------- */
int inicializa()
{
    int i,j;
 
    allegro_init();        // Inicializamos Allegro
    install_keyboard();
    install_timer();
 
                           // Intentamos entrar a modo grafico
    set_color_depth(32);
    if (set_gfx_mode(GFX_SAFE,ANCHOPANTALLA, ALTOPANTALLA, 0, 0) != 0) {
        set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
        allegro_message(
            "Incapaz de entrar a modo grafico\n%s\n",
            allegro_error);
        return 1;
    }
 
                           // e intentamos abrir imgenes
    imagenFragmentos = load_bmp("columnas_piezas.bmp", pal);
    if (!imagenFragmentos) {
        set_gfx_mode(GFX_TEXT, 0, 0, 0, 0);
        allegro_message("No se ha podido abrir la imagen\n");
        return 1;
    }
 
        // Ahora reservo espacio para los otros sprites
    for (i=0; i<NUMFRAGMENTOS; i++)    
    {
      pieza[i] = create_bitmap(32, 32);
 
      // Y los extraigo de la imagen "grande"
      blit(imagenFragmentos, pieza[i]    // bitmaps de origen y destino
        , 32*i, 0           // coordenadas de origen
        , 0, 0             // posicin de destino
        , 32, 32);         // anchura y altura
    }
 
    set_palette(pal);
 
    srand(time(0));
 
    // Pantalla oculta para evitar parpadeos
    // (doble buffer)
    pantallaOculta = create_bitmap(ANCHOPANTALLA, ALTOPANTALLA);
 
    // Vaco el tablero de fondo
    for (i=0; i<ANCHOTABLERO; i++)
      for (j=0; j<ALTOTABLERO; j++)
        tablero[i][j] = SINPIEZA;
 
    crearNuevaPieza();
 
   // Y termino indicando que no ha habido errores
   return 0;
}
 
 
 
/* ------------------------------------------------ */
/*                                                  */
/* -------------- Cuerpo del programa ------------- */
 
int main()
{
 
    int i,j;
 
    // Intento inicializar
    if (inicializa() != 0)
        exit(1);
 
    // Bucle principal del juego
    buclePrincipal();
 
    // Libero memoria antes de salir
    for (i=0; i<NUMFRAGMENTOS; i++)
      destroy_bitmap(pieza[i]);
    destroy_bitmap(pantallaOculta);
 
    rest(1000);
    return 0;
 
}
 
            /* Termino con la "macro" que me pide Allegro */
END_OF_MAIN();