25. Avanzando Columnas: primera parte de la lógica de juego.(*)

La entrega anterior mostraba la apariencia que suele tener el "bucle de juego", y lo aplicaba para un primer acercamiento al nuestro juego "Columnas". Hasta entonces, una pieza ficticia se podía mover de lado a lado. Ahora llega el momento de ampliar ese "esqueleto" con parte de la lógica que corresponde realmente a este juego.

Tendremos que hacer cosas como:

  • La pieza no será siempre igual: cada vez que aparece una nueva pieza, ésta estará formada por 3 componentes elegidos al azar.
  • Además, podemos "rotar" la pieza, con lo que la distribución de colores cambiará.
  • La pieza no se podrá mover tan libremente como en la primera aproximación, sino que sólo habrá unas pocas columnas verticales en las que podrá estar (por ejemplo, 6).
  • La pieza va bajando poco a poco.
  • Cuando toca el suelo, deberemos comprobar si hay 3 o más piezas del mismo color en línea (horizontal, vertical o diagonal), y si es así, deberán eliminarse.
  • Cuando ya han caido piezas anteriores, nuerstra pieza no deberá llegar hasta la parte inferior de la pantalla, sino que sólo caerá hasta tocar esas piezas; de igual modo, si hay una pieza a nuestra derecha o izquierda, no podremos movernos en esa dirección.
  • La partida acaba cuando una de las columnas se queda ocupada en toda su altura (que puede ser, por ejemplo, de 14 filas).
  • También hay otra pequeña ampliación que tenemos que incluir: que se pueda pulsar la flecha hacia abajo para que la pieza caiga un poco más rápido.

 
Vamos a ver cómo se podría hacer cada una de esas cosas:

Para que la pieza esté formada por tres fragmentos al azar, podemos leer la imagen que contiene todos los fragmentos posibles y "trocearla": columnas_pieza0.bmp

(Realmente, esta imagen contiene un séptimo fragmento, que no usaremos... todavía...)

 
#define NUMFRAGMENTOS 6
BITMAP *pieza[NUMFRAGMENTOS];
imagenFragmentos = load_bmp("columnas_piezas.bmp", pal);
// 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             // posición de destino
    , 32, 32);         // anchura y altura
}
 

Y luego podemos guardar en un "array" los 3 tipos de fragmento que contiene la pieza actualmente

 
// Tipo de imagen que corresponde a cada fragmento de pieza
int tipoFragmento[3];
 

Cuando se crea una pieza nueva, esos fragmentos se escogen al azar:

 
for (i=0; i<3; i++)
  tipoFragmento[i] = rand() % NUMFRAGMENTOS;
 

Y se consulta este array para dibujar la pieza:

 
  for (i=0; i<3; i++)
    draw_sprite(pantallaOculta, 
      pieza[ tipoFragmento[i] ], x, y+32*i);
 

Por otra parte, la forma de rotar colores sería asignar a la segunda casilla el color que tenía la primera, asignar a la tercera e que tenía la segunda y así sucesivamente:

 
  int auxiliar = tipoFragmento[0];
  tipoFragmento[0] = tipoFragmento[1];
  tipoFragmento[1] = tipoFragmento[2];
  tipoFragmento[2] = auxiliar;
 

Para que la pieza no se podrá mover tan libremente como en la primera aproximación, sino que sólo habrá unas pocas columnas verticales, los saltos (incrementos) verticales deberán ser algo mayores, y deberemos comprobar que no se salga por ninguno de los dos extremos de la "pantalla de juego":

 
#define TAMANYOPIEZA 32
int incrX = TAMANYOPIEZA;
...
void intentarMoverIzquierda() {
  if (x > MARGENIZQDO)
    x -= incrX;
}
 

La pieza debe ir bajando poco a poco.... asío que nuestro "moverElementos" ya tendrá que hacer algo aunque no pulsemos ninguna tecla. Si hacemos "y += incrY;" en cada pasada, la pieza se moverá demasiado deprisa, así que será ás jugable si llevamos un contador de fotogramas dibujados, de modo que sólo avance una vez cada cierto número de fotogramas (por ejemplo, 10):

 
void moverElementos() {
  contadorFotogramas ++;
  if (contadorFotogramas >= 10) {   
    y += incrY;
    contadorFotogramas = 0;
  }
}
 

También queremos que se pueda pulsar la flecha hacia abajo para que la pieza caiga un poco más rápido. Eso es muy fácil:

 
if ( tecla == KEY_DOWN )
  intentarBajar();
...
void intentarBajar() {
  y += incrY;
  if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA)
    colocarPiezaEnFondo(); 
}
 

Cuando toca el suelo, haremos que las figuras que al componen pasen a ser parte del fondo, pero aplazamos para la siguiente entregar el comprobar si hay 3 o más piezas del mismo color en línea para eliminarlas. También aplazaremos por ahora la comprobación de choques en vertical y en horizontal, y el final de la partida (seguiremos teniendo que pulsar ESC). Para comprobar si ha llegado al suelo, podríamos añadir estas líneas al final de "intentarBajar" y de "moverElementos":

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

Y esa función "colocarPiezaEnFondo" se encargaría de guardar en un array de dos dimensiones cada fragmento de la pieza actual, calculando la posición del "tablero" que le corresponde a cada fargmento (para lo que habrá que restar el margen y dividir entre el tamaño de la pieza):

 
  for (i=0; i<3; i++) {
    tablero
      [ (x-MARGENIZQDO)/TAMANYOPIEZA ]
      [ (y-MARGENSUP)/TAMANYOPIEZA + i] 
        = tipoFragmento[i];
  }
 

Con todo esto, la nueva versión del fuente podría ser así

/*------------------------------*/
/*  Intro a la programac de     */
/*  juegos, por Nacho Cabanes   */
/*                              */
/*    ipj25c.c                  */
/*                              */
/*  Ejemplo:                    */
/*    Segundo 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 6
 
// Ancho y alto de cada pieza
#define TAMANYOPIEZA 32
 
 
/* -------------- 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() {
  if (x < ANCHOPANTALLA-MARGENDCHO-TAMANYOPIEZA)
    x += incrX;
}
 
 
// -- Intenta mover la "pieza" hacia la izquierda
void intentarMoverIzquierda() {
  if (x > MARGENIZQDO)
    x -= incrX;
}
 
 
// -- Intenta mover la "pieza" hacia abajo
void intentarBajar() {
  y += incrY;
  if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA)
    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() {
  contadorFotogramas ++;
  if (contadorFotogramas >= 10) {   
    y += incrY;
    contadorFotogramas = 0;
    if (y >= ALTOPANTALLA-MARGENINF-3*TAMANYOPIEZA)
      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] != -1)
          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;
  x = MARGENIZQDO + TAMANYOPIEZA*5;
  y = MARGENSUP;  
}
 
 
// -- Coloca una pieza como parte del fondo, cuando
// llegamos a la parte inferior de la pantalla
void colocarPiezaEnFondo(){
  int i;
  for (i=0; i<3; i++) {
    tablero
      [ (x-MARGENIZQDO)/TAMANYOPIEZA ]
      [ (y-MARGENSUP)/TAMANYOPIEZA + i] 
        = tipoFragmento[i];
  }
  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] = -1;
 
    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();