Hace ya 4 años comencé a programar el juego de Tetris en Allegro 5 y hoy finalmente lo he terminado.

Primero hice el juego con JavaScript y como lo quería compilado para móviles me decidí a hacerlo primero en C con Allegro, porque si podía hacerlo en C podría portarlo para cualquier otra plataforma más adelante.
No es que me haya llevado 4 años, lo que pasa es que dejé el proyecto abandonado hasta que recientemente me compré una pantalla OLED para Arduino y decidí implementar Tetris en Arduino con dicha OLED, pero de nuevo: primero tenía que terminarlo en C
Así que aquí vengo a mostrar el juego de Tetris en C con Allegro 5, y en otro post voy a explicar su port para Arduino.
Características del juego
Tiene implementado lo de un juego de tetris común. Incorpora los 7 tetriminos originales.
- Puedes rotar el tetrimino
- Muestra la posición de caída del tetrimino
- Tetriminos aleatorios y mezclados correctamente
- Detección de 4 líneas eliminadas
- Muestra siguiente pieza
- Open source: puedes modificar y mejorar cualquier aspecto del juego
- Probado en Windows 11 y Elementary OS (basado en Linux Ubuntu)
- Puedes cambiar la música de fondo y el diseño reemplazando los archivos mp3, wav y png
Se juega con las flechas de dirección o hjkl. Usa Q
para la reserva y Espacio
para rotar la pieza.
Tetris en C con Allegro
He compilado este proyecto en Windows usando GCC. Para que funcione al menos en Windows necesitas tener los include de Allegro
La versión de GCC es:
C:\Users\parzibyte>gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/15.1.0/lto-wrapper.exe
Target: x86_64-w64-mingw32
Configured with: ../gcc-15.1.0/configure --prefix=/mingw64 --with-local-prefix=/mingw64/local --with-native-system-header-dir=/mingw64/include --libexecdir=/mingw64/lib --enable-bootstrap --enable-checking=release --with-arch=nocona --with-tune=generic --enable-mingw-wildcard --enable-languages=c,lto,c++,fortran,ada,objc,obj-c++,jit --enable-shared --enable-static --enable-libatomic --enable-threads=posix --enable-graphite --enable-fully-dynamic-string --enable-libstdcxx-backtrace=yes --enable-libstdcxx-filesystem-ts --enable-libstdcxx-time --disable-libstdcxx-pch --enable-lto --enable-libgomp --disable-libssp --disable-multilib --disable-rpath --disable-win32-registry --disable-nls --disable-werror --disable-symvers --with-libiconv --with-system-zlib --with-gmp=/mingw64 --with-mpfr=/mingw64 --with-mpc=/mingw64 --with-isl=/mingw64 --with-pkgversion='Rev5, Built by MSYS2 project' --with-bugurl=https://github.com/msys2/MINGW-packages/issues --with-gnu-as --with-gnu-ld --disable-libstdcxx-debug --enable-plugin --with-boot-ldflags=-static-libstdc++ --with-stage1-ldflags=-static-libstdc++
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 15.1.0 (Rev5, Built by MSYS2 project)
C:\Users\parzibyte>
Además del código fuente de Tetris que se encuentra en main.c
vas a necesitar los assets que son las imágenes, efectos de sonido,
música de fondo y la fuente arial.ttf.
Sobre arial.ttf
Todos los assets están
incluidos en el repositorio excepto por la tipografía arial.ttf
que no sé
si puedo distribuir así que no lo he hecho.
Basta con que la copies de C:\Windows\Fonts
o que
te descargues otra fuente de internet y la cambies en
el código.
También puedes cambiar la línea que usa arial.ttf por:
ALLEGRO_FONT *fuente = al_create_builtin_font();
De modo que usas la fuente incorporada. No la he probado en Linux, solo en Windows.
Compilando en Windows
Para compilar puedes ejecutar make
si cuentas con él
o:
gcc -Wall main.c -o main -lallegro -lallegro_font -lallegro_primitives -lallegro_ttf -lallegro_image -lallegro_audio -lallegro_acodec
Y luego ejecuta main.exe
Para Linux
También se puede compilar Tetris en C usando Allegro 5 en Linux. He logrado compilar exactamente el mismo código pero con algunas variaciones en Elementary OS (basado en Ubuntu).
Las variaciones son:
- Cambiado el tamaño de los mapas de bits de 32x32 a 20x20, ya que mi pantalla es pequeña (1024x768). Esto no debería ser problema si tu pantalla es de mayor resolución
- Cambiado el formato de la canción de fondo de mp3 a wav. Si usaba mp3 no se cargaba correctamente; desconozco la razón
- La librería de Allegro 5 se instala mucho más fácil en Linux
En código se ve así:
/*
La MEDIDA_CUADRO debe coincidir exactamente
con el tamaño en pixeles de las imágenes
*/
#define MEDIDA_CUADRO 20
#define NOMBRE_CANCION_FONDO "b.wav"
Veamos cómo lo hice. Primero instalé la dependencia con:
sudo apt install liballegro5-dev
Y luego compilé igualmente con:
gcc -Wall main.c -o main -lallegro -lallegro_font -lallegro_primitives -lallegro_ttf -lallegro_image -lallegro_audio -lallegro_acodec
Pero al momento de ejecutar el código me apareció el mensaje que decía: Error cargando audio_stream
Ese error era porque no se podía cargar la música de fondo. Obviamente la ruta y nombre de archivo era correcta, así que para probar puse un efecto de sonido en formato wav y con eso ya funcionó: el error era que, por alguna razón, no podía usar MP3.
Puedes usar cualquier convertidor pero ya que estamos en Ubuntu podemos hacer un:
sudo apt install ffmpeg
ffmpeg -i archivo.mp3 archivo.wav
También tuve que cambiar el tamaño de las imágenes, pero esto solo te lo cuento porque puede que en tu caso no lo necesites, ya que la computadora donde lo compilé todavía es un poco antigua así como su monitor.
Para convertir las imágenes usé imagemagick
con
el comando convert -resize
Explicación del código
Ya he dedicado un post muy extenso explicando cómo programar Tetris en cualquier lenguaje de programación y he usado justamente el lenguaje C para los ejemplos.
Código fuente
El código completo de Tetris en C con Allegro queda como se ve a continuación. Si actualizo, mejoro o limpio el código open source de Tetris en el futuro lo estaré anunciando en mi blog y canales oficiales.
#include <allegro5/allegro5.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_primitives.h>
#include <stdbool.h>
#include <stdio.h>
#include <allegro5/allegro_ttf.h>
#include <stdbool.h>
#include <allegro5/allegro_image.h>
#include <allegro5/allegro_audio.h>
#include <allegro5/allegro_acodec.h>
#define OFFSET_X 200
/*
La MEDIDA_CUADRO debe coincidir exactamente
con el tamaño en pixeles de las imágenes
*/
#define MEDIDA_CUADRO 32
#define ANCHO_CUADRICULA 2 // Recuerda que se multiplicará por BITS_EN_UN_BYTE así que si es 2 en realidad es 16
#define ALTO_CUADRICULA 30
#define BITS_EN_UN_BYTE 8
#define BITS_EN_UINT16 16
#define MAXIMO_INDICE_BIT_EN_BYTE 7
#define MAXIMO_INDICE_BIT_EN_UINT16 15
#define CUADRICULA_TETRIMINO 4
#define TOTAL_TETRIMINOS_DISPONIBLES 7
#define BITS_POR_FILA_PARA_TETRIMINO 4
#define MITAD_CUADRICULA_X ANCHO_CUADRICULA *BITS_EN_UN_BYTE / 2 - (BITS_POR_FILA_PARA_TETRIMINO / 2);
#define GROSOR_BORDE 10
#define NOMBRE_CANCION_FONDO "01. Bulby - Super Mario Bros. Wonder 8 Bit VRC6.mp3"
#define NOMBRE_SONIDO_UNA_LINEA "397819__swordmaster767__powerup.wav"
#define NOMBRE_SONIDO_4_LINEAS "528958__beetlemuse__level-up-mission-complete.wav"
#define NOMBRE_IMAGEN_MOVIMIENTO "bitmap.png"
#define NOMBRE_PIEZA_CAIDA "2.png"
#define NOMBRE_PIEZA_RESERVA "amarillo.png"
typedef enum
{
MENOS_DE_CUATRO_LINEAS,
CUATRO_LINEAS_O_MAS,
TOCADO_LIMITE, // Ya está a punto de ser parte de la cuadrícula
NADA, // Bajó normalmente y no eliminó líneas
ACOMODADA, // Ya es parte de la cuadrícula
CANTIDAD_ESTADOS_BAJAR
} ResultadoAlBajar;
struct Tetrimino
{
uint16_t cuadricula;
uint8_t x, y;
};
struct TetriminoParaElegir
{
uint16_t cuadricula;
};
uint8_t indiceGlobalTetrimino = 0;
void aleatorizarPiezas(struct TetriminoParaElegir piezas[TOTAL_TETRIMINOS_DISPONIBLES])
{
/*
Cantidad = Tamaño(Array)
Recorrer con k desde Cantidad-1 hasta 1 Regresivamente
az = Random(entre 0 y k)
tmp = Array(az)
Array(az) = Array(k)
Array(k) = tmp
Siguiente
*/
for (int k = TOTAL_TETRIMINOS_DISPONIBLES - 1; k > 0; k--)
{
uint8_t az = rand() % (k + 1);
struct TetriminoParaElegir tmp = piezas[az];
piezas[az] = piezas[k];
piezas[k] = tmp;
}
}
void elegirPiezaAleatoria(struct Tetrimino *destino, struct TetriminoParaElegir piezasDisponibles[TOTAL_TETRIMINOS_DISPONIBLES])
{
destino->cuadricula = piezasDisponibles[indiceGlobalTetrimino].cuadricula;
destino->x = MITAD_CUADRICULA_X;
destino->y = 0;
indiceGlobalTetrimino++;
if (indiceGlobalTetrimino > TOTAL_TETRIMINOS_DISPONIBLES - 1)
{
aleatorizarPiezas(piezasDisponibles);
indiceGlobalTetrimino = 0;
}
}
void elegirSiguientePieza(struct Tetrimino *actual, struct Tetrimino *siguiente, struct TetriminoParaElegir piezas[TOTAL_TETRIMINOS_DISPONIBLES])
{
actual->cuadricula = siguiente->cuadricula;
actual->x = siguiente->x = MITAD_CUADRICULA_X;
actual->y = siguiente->y = 0;
elegirPiezaAleatoria(siguiente, piezas);
}
void inicializarPiezas(struct TetriminoParaElegir piezas[TOTAL_TETRIMINOS_DISPONIBLES])
{
/*
Veamos la Z es
1100
0110
0000
0000
Que sería C6
*/
piezas[0].cuadricula = 0xC600;
/*
La L es
1000
1000
1100
0000
Que sería 88C0
*/
piezas[1].cuadricula = 0x88C0;
/*
Ahora una línea
1111
0000
0000
0000
Solo sería F000
*/
piezas[2].cuadricula = 0xF000;
/*
Ahora el cuadro
1100
1100
0000
0000
Que sería CC00
*/
piezas[3].cuadricula = 0xCC00;
/*
Ahora la T
Esa es
1110
0100
0000
0000
Que sería E400
*/
piezas[4].cuadricula = 0xE400;
/*
La L invertida que sería
1110
0010
0000
0000
E200
*/
piezas[5].cuadricula = 0xe200;
/*
La Z invertida que sería
0110
1100
0000
0000
6C00
*/
piezas[6].cuadricula = 0x6c00;
}
uint16_t rotar90CW(uint16_t pieza)
{
// (x', y') = ( y, 3 - x )
uint16_t rotado = 0;
for (int y = 0; y < 4; y++)
{
for (int x = 0; x < 4; x++)
{
int indiceBitOrigen = (y * 4 + x);
int bit = (pieza >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBitOrigen)) & 1;
int xPrima = y;
int yPrima = 3 - x;
int indiceBitDestino = (yPrima * 4 + xPrima);
rotado |= bit << (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBitDestino);
}
}
/*
Recordemos que 0xf000 es
1111
0000
0000
0000
Y le hacemos un AND con la pieza rotada. Va a devolver
0 siempre que la pieza tenga únicamente ceros en los primeros 4 bits
Por ejemplo, tenemos la pieza
0100
0110
0010
0000
Hacemos un AND:
0100011000100000
&
1111000000000000
El resultado es 0100000000000000, mismo que es distinto a 0000000000000000, es correcto
porque no tiene filas vacías al inicio.
Pero ahora veamos con la siguiente pieza:
0000
0110
0011
0000
Le hacemos un AND:
0000011000110000
&
1111000000000000
El resultado es
0000000000000000
Lo cual es exactamente a 0
Entonces cuando se cumple esta condición desplazamos 4 bits a la izquierda,
lo que es como subir la pieza
*/
while ((rotado & 0xF000) == 0)
{
rotado <<= 4;
}
/*
0x8888 sería:
1000
1000
1000
1000
Y hacemos lo mismo pero ahora para arrimar
la pieza a la izquierda
*/
while ((rotado & 0x8888) == 0)
{
rotado <<= 1;
}
return rotado;
}
/*
Recibe un apuntador al tetrimino y la cuadrícula del tetris
*/
bool tetriminoColisionaConCuadriculaAlRotar(struct Tetrimino *tetrimino, uint8_t cuadricula[ALTO_CUADRICULA][ANCHO_CUADRICULA])
{
/*
Nuevo código porque usamos uint16_t
*/
for (uint8_t indiceBit = 0; indiceBit < BITS_EN_UINT16; indiceBit++)
{
// Primero rotamos. No usaremos tetrimino->cuadricula sino lo rotado
uint16_t rotado = rotar90CW(tetrimino->cuadricula);
bool hayUnCuadroDeTetriminoEnLaCoordenadaActual = (rotado >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBit)) & 1;
if (!hayUnCuadroDeTetriminoEnLaCoordenadaActual)
{
continue;
}
// Llegados aquí sabemos que el "continue" no se ejecutó y que SÍ hay un tetrimino
// Coordenadas sobre la cuadrícula después de aplicar los modificadores
uint8_t xRelativoDentroDeCuadricula = indiceBit % BITS_POR_FILA_PARA_TETRIMINO;
uint8_t YRelativoDentroDeCuadricula = indiceBit / BITS_POR_FILA_PARA_TETRIMINO;
int xEnCuadriculaDespuesDeModificar = tetrimino->x + xRelativoDentroDeCuadricula;
int yEnCuadriculaDespuesDeModificar = tetrimino->y + YRelativoDentroDeCuadricula;
// Límites con anchos y altos de la cuadrícula
if (xEnCuadriculaDespuesDeModificar > ANCHO_CUADRICULA * BITS_EN_UN_BYTE - 1)
{
return true;
}
if (xEnCuadriculaDespuesDeModificar < 0)
{
return true;
}
if (yEnCuadriculaDespuesDeModificar < 0)
{
return true;
}
if (yEnCuadriculaDespuesDeModificar > ALTO_CUADRICULA - 1)
{
return true;
}
/*
Hasta este punto las coordenadas ya son seguras y ya las tenemos simuladas con el avance
*/
int xEnByteDeCuadricula = xEnCuadriculaDespuesDeModificar / BITS_EN_UN_BYTE;
int indiceBitDeByteEnCuadricula = xEnCuadriculaDespuesDeModificar % BITS_EN_UN_BYTE;
if ((cuadricula[yEnCuadriculaDespuesDeModificar][xEnByteDeCuadricula] >> (MAXIMO_INDICE_BIT_EN_BYTE - indiceBitDeByteEnCuadricula)) & 1)
{
return true;
}
}
return false;
}
/*
Recibe un apuntador al tetrimino, la cuadrícula del tetris y dos modificadores x e y.
La función aumentará las coordenadas del tetrimino a partir de los modificadores simulando un avance y
después va a devolver true si el tetrimino colisiona con una pared, suelo u otras piezas
*/
bool tetriminoColisionaConCuadriculaAlAvanzar(struct Tetrimino *tetrimino, uint8_t cuadricula[ALTO_CUADRICULA][ANCHO_CUADRICULA], int8_t modificadorX, int8_t modificadorY)
{
/*
Nuevo código porque usamos uint16_t
*/
for (uint8_t indiceBit = 0; indiceBit < BITS_EN_UINT16; indiceBit++)
{
bool hayUnCuadroDeTetriminoEnLaCoordenadaActual = (tetrimino->cuadricula >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBit)) & 1;
if (!hayUnCuadroDeTetriminoEnLaCoordenadaActual)
{
continue;
}
// Llegados aquí sabemos que el "continue" no se ejecutó y que SÍ hay un tetrimino
// Coordenadas sobre la cuadrícula después de aplicar los modificadores
uint8_t xRelativoDentroDeCuadricula = indiceBit % BITS_POR_FILA_PARA_TETRIMINO;
uint8_t YRelativoDentroDeCuadricula = indiceBit / BITS_POR_FILA_PARA_TETRIMINO;
int xEnCuadriculaDespuesDeModificar = tetrimino->x + xRelativoDentroDeCuadricula + modificadorX;
int yEnCuadriculaDespuesDeModificar = tetrimino->y + YRelativoDentroDeCuadricula + modificadorY;
// Límites con anchos y altos de la cuadrícula
if (xEnCuadriculaDespuesDeModificar > ANCHO_CUADRICULA * BITS_EN_UN_BYTE - 1)
{
return true;
}
if (xEnCuadriculaDespuesDeModificar < 0)
{
return true;
}
if (yEnCuadriculaDespuesDeModificar < 0)
{
return true;
}
if (yEnCuadriculaDespuesDeModificar > ALTO_CUADRICULA - 1)
{
return true;
}
/*
Hasta este punto las coordenadas ya son seguras y ya las tenemos simuladas con el avance
*/
int xEnByteDeCuadricula = xEnCuadriculaDespuesDeModificar / BITS_EN_UN_BYTE;
int indiceBitDeByteEnCuadricula = xEnCuadriculaDespuesDeModificar % BITS_EN_UN_BYTE;
if ((cuadricula[yEnCuadriculaDespuesDeModificar][xEnByteDeCuadricula] >> (MAXIMO_INDICE_BIT_EN_BYTE - indiceBitDeByteEnCuadricula)) & 1)
{
return true;
}
}
return false;
}
int8_t indiceYParaFantasma(struct Tetrimino *tetrimino, uint8_t cuadricula[ALTO_CUADRICULA][ANCHO_CUADRICULA])
{
for (uint8_t y = 0; y <= ALTO_CUADRICULA; y++)
{
if (tetriminoColisionaConCuadriculaAlAvanzar(tetrimino, cuadricula, 0, y - tetrimino->y))
{
return y - 1;
}
}
// TODO: tal vez no usar un uint8_t y devolver -1
return -1;
}
/*
Devuelve el primer índice de la fila llena comenzando desde abajo (desde ALTO_CUADRICULA - 1)
Si no hay ninguna fila llena devuelve -1
*/
int8_t indiceFilaLlena(uint8_t cuadricula[ALTO_CUADRICULA][ANCHO_CUADRICULA])
{
for (int y = ALTO_CUADRICULA - 1; y >= 0; y--)
{
bool filaLlena = true;
for (int x = 0; x < ANCHO_CUADRICULA; x++)
{
filaLlena = filaLlena && cuadricula[y][x] == 255;
}
if (filaLlena)
{
return y;
}
}
return -1;
}
void limpiarFilaYBajarFilasSuperiores(int8_t indiceFila, uint8_t cuadricula[ALTO_CUADRICULA][ANCHO_CUADRICULA])
{
// Bajamos las superiores
for (; indiceFila > 0; indiceFila--)
{
memcpy(cuadricula[indiceFila], cuadricula[indiceFila - 1], sizeof(cuadricula[indiceFila]));
}
// Y estamos seguros de que hasta arriba (y=0) se quedó una fila disponible, la dejamos en ceros
memset(cuadricula[0], 0, sizeof(cuadricula[0]));
}
ResultadoAlBajar bajarTetrimino(struct Tetrimino *tetrimino, struct Tetrimino *siguiente, uint8_t cuadricula[ALTO_CUADRICULA][ANCHO_CUADRICULA], bool *bandera, unsigned long *puntajeGlobal, bool *juegoTerminado, struct TetriminoParaElegir piezas[TOTAL_TETRIMINOS_DISPONIBLES], bool *haUsadoReserva)
{
if (!tetriminoColisionaConCuadriculaAlAvanzar(tetrimino, cuadricula, 0, 1))
{
tetrimino->y++;
*bandera = false;
return NADA;
}
else
{
// Ya te había avisado que te movieras. Esto significa que no te moviste y por lo tanto toca spawnear
// una nueva pieza
if (bandera)
{
/*
Otra vez código nuevo porque migramos a uint16_t
Primero toca copiar la pieza actual a la cuadrícula maestra
*/
for (uint8_t indiceBit = 0; indiceBit < BITS_EN_UINT16; indiceBit++)
{
bool hayUnCuadroDeTetriminoEnLaCoordenadaActual = (tetrimino->cuadricula >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBit)) & 1;
if (!hayUnCuadroDeTetriminoEnLaCoordenadaActual)
{
continue;
}
// Llegados aquí sabemos que el "continue" no se ejecutó y que SÍ hay un tetrimino
// Coordenadas sobre la cuadrícula después de aplicar los modificadores
uint8_t xRelativoDentroDeCuadricula = indiceBit % BITS_POR_FILA_PARA_TETRIMINO;
uint8_t YRelativoDentroDeCuadricula = indiceBit / BITS_POR_FILA_PARA_TETRIMINO;
int xEnCuadriculaDespuesDeModificar = tetrimino->x + xRelativoDentroDeCuadricula;
int yEnCuadriculaDespuesDeModificar = tetrimino->y + YRelativoDentroDeCuadricula;
int xEnByteDeCuadricula = xEnCuadriculaDespuesDeModificar / BITS_EN_UN_BYTE;
int indiceBitDeByteEnCuadricula = xEnCuadriculaDespuesDeModificar % BITS_EN_UN_BYTE;
cuadricula[yEnCuadriculaDespuesDeModificar][xEnByteDeCuadricula] = cuadricula[yEnCuadriculaDespuesDeModificar][xEnByteDeCuadricula] | (1 << (MAXIMO_INDICE_BIT_EN_BYTE - indiceBitDeByteEnCuadricula));
}
// Limpiar filas y bajar filas superiores hasta que ya no haya filas llenas
/*
Aquí puede que haya un "tetris", entonces calculamos un puntaje o cosas de esas, así que
calculamos el puntaje
*/
uint8_t lineasEliminadasConsecutivamente = 0;
while (1)
{
int8_t posibleIndiceFilaLlena = indiceFilaLlena(cuadricula);
// No hay filas llenas. Nada que limpiar
if (posibleIndiceFilaLlena == -1)
{
*puntajeGlobal += lineasEliminadasConsecutivamente;
break;
}
else
{
limpiarFilaYBajarFilasSuperiores(posibleIndiceFilaLlena, cuadricula);
lineasEliminadasConsecutivamente++;
}
}
elegirSiguientePieza(tetrimino, siguiente, piezas);
if (tetriminoColisionaConCuadriculaAlAvanzar(tetrimino, cuadricula, 0, 0))
{
*juegoTerminado = true;
}
// Llegados hasta este punto sabemos que podemos aparecer la nueva pieza y que sí hay
// espacio para ella.
*haUsadoReserva = false;
if (lineasEliminadasConsecutivamente > 0 && lineasEliminadasConsecutivamente < 4)
{
return MENOS_DE_CUATRO_LINEAS;
}
else if (lineasEliminadasConsecutivamente >= 4)
{
return CUATRO_LINEAS_O_MAS;
}
else
{
return ACOMODADA;
}
}
else
{
// No puedes bajar pero te doy un tiempo para que te puedas mover
*bandera = true;
return TOCADO_LIMITE;
}
}
}
int main()
{
srand(time(NULL));
al_init();
al_init_primitives_addon();
al_install_keyboard();
al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
al_set_new_display_option(ALLEGRO_SAMPLES, 8, ALLEGRO_SUGGEST);
al_set_new_bitmap_flags(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);
// Aquí se configura el tamaño de la pantalla
ALLEGRO_DISPLAY *disp = al_create_display((MEDIDA_CUADRO * ANCHO_CUADRICULA * BITS_EN_UN_BYTE) + OFFSET_X, (ALTO_CUADRICULA * MEDIDA_CUADRO) + GROSOR_BORDE * 2);
ALLEGRO_FONT *font = al_create_builtin_font();
if (!al_init_image_addon())
{
fprintf(stderr, "Error inicializando librería imagen");
}
if (!al_install_audio())
{
fprintf(stderr, "Error inicializando audio\n");
return -1;
}
if (!al_init_acodec_addon())
{
fprintf(stderr, "Error inicializando codec addon\n");
return -1;
}
if (!al_reserve_samples(2))
{
fprintf(stderr, "Error reserve_samples\n");
return -1;
}
ALLEGRO_AUDIO_STREAM *musica_fondo = NULL;
musica_fondo = al_load_audio_stream(NOMBRE_CANCION_FONDO, 4, 2048);
if (!musica_fondo)
{
fprintf(stderr, "Error cargando audio_stream\n");
return 1;
}
al_attach_audio_stream_to_mixer(musica_fondo, al_get_default_mixer());
al_set_audio_stream_playmode(musica_fondo, ALLEGRO_PLAYMODE_LOOP);
al_set_audio_stream_playing(musica_fondo, true);
ALLEGRO_SAMPLE *sonido_una_linea = NULL;
ALLEGRO_SAMPLE *sonido_4_lineas = NULL;
ALLEGRO_SAMPLE_ID id_sonido_una_linea;
ALLEGRO_SAMPLE_ID id_sonido_4_lineas;
sonido_una_linea = al_load_sample(NOMBRE_SONIDO_UNA_LINEA);
if (!sonido_una_linea)
{
fprintf(stderr, "Error cargando sonido linea\n");
return 1;
}
sonido_4_lineas = al_load_sample(NOMBRE_SONIDO_4_LINEAS);
if (!sonido_4_lineas)
{
fprintf(stderr, "Error cargando sonido 4 lineas\n");
return 1;
}
ALLEGRO_BITMAP *imagen_pieza_caida = NULL;
ALLEGRO_BITMAP *imagen_pieza_movimiento = NULL;
ALLEGRO_BITMAP *imagen_pieza_reserva = NULL;
imagen_pieza_reserva = al_load_bitmap(NOMBRE_PIEZA_RESERVA);
if(!imagen_pieza_reserva){
fprintf(stderr, "Error cargando imagen pieza reserva");
}
imagen_pieza_caida = al_load_bitmap(NOMBRE_PIEZA_CAIDA);
if (!imagen_pieza_caida)
{
fprintf(stderr, "Error cargando imagen");
}
imagen_pieza_movimiento = al_load_bitmap(NOMBRE_IMAGEN_MOVIMIENTO);
if (!imagen_pieza_movimiento)
{
fprintf(stderr, "Error cargando imagen");
}
ALLEGRO_TIMER *timer = al_create_timer(1.0 / 60.0);
ALLEGRO_EVENT_QUEUE *queue = al_create_event_queue();
/*Timers para bajar pieza automáticamente*/
ALLEGRO_TIMER *timer_bajar_pieza = al_create_timer(1);
al_register_event_source(queue, al_get_timer_event_source(timer_bajar_pieza));
al_start_timer(timer_bajar_pieza);
/*Terminan timers para bajar pieza automáticamente*/
al_register_event_source(queue, al_get_keyboard_event_source());
al_register_event_source(queue, al_get_display_event_source(disp));
al_register_event_source(queue, al_get_timer_event_source(timer));
bool redraw = true;
ALLEGRO_EVENT event;
al_start_timer(timer);
ALLEGRO_COLOR blanco = al_map_rgb(255, 255, 255);
ALLEGRO_COLOR negro = al_map_rgb_f(0, 0, 0);
ALLEGRO_COLOR rojo = al_map_rgb_f(255, 0, 0);
ALLEGRO_COLOR azul = al_map_rgb(0, 0, 255);
ALLEGRO_COLOR color_borde = al_map_rgb(136, 195, 191);
uint8_t cuadriculaJuego[ALTO_CUADRICULA][ANCHO_CUADRICULA] = {
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
{0, 0},
};
struct Tetrimino piezaActual;
struct Tetrimino piezaSiguiente;
struct Tetrimino reserva = {0};
struct TetriminoParaElegir piezas[TOTAL_TETRIMINOS_DISPONIBLES];
bool haUsadoLaReserva = false;
inicializarPiezas(piezas);
aleatorizarPiezas(piezas);
elegirPiezaAleatoria(&piezaActual, piezas);
elegirPiezaAleatoria(&piezaSiguiente, piezas);
al_init_font_addon();
al_init_ttf_addon();
ALLEGRO_FONT *fuente = al_load_font("arial.ttf", 20, 0);
bool banderaTocoSuelo = false;
bool juegoTerminado = false;
unsigned long puntajeGlobal = 0;
while (1)
{
al_wait_for_event(queue, &event);
if (event.type == ALLEGRO_EVENT_TIMER)
{
if (event.timer.source == timer)
{
redraw = true;
}
else if (event.timer.source == timer_bajar_pieza)
{
if (!juegoTerminado)
{
ResultadoAlBajar r = bajarTetrimino(&piezaActual, &piezaSiguiente, cuadriculaJuego, &banderaTocoSuelo, &puntajeGlobal, &juegoTerminado, piezas, &haUsadoLaReserva);
switch (r)
{
case CUATRO_LINEAS_O_MAS:
al_play_sample(
sonido_4_lineas,
1.0,
0.0,
1.0,
ALLEGRO_PLAYMODE_ONCE,
&id_sonido_4_lineas);
break;
case MENOS_DE_CUATRO_LINEAS:
al_play_sample(
sonido_una_linea,
1.0,
0.0,
1.0,
ALLEGRO_PLAYMODE_ONCE,
&id_sonido_una_linea);
break;
default:
break;
}
}
}
}
else if (event.type == ALLEGRO_EVENT_KEY_CHAR)
{
int teclaPresionada = event.keyboard.keycode;
if (!juegoTerminado)
{
if (teclaPresionada == ALLEGRO_KEY_UP || teclaPresionada == ALLEGRO_KEY_K)
{
piezaActual.y = indiceYParaFantasma(&piezaActual, cuadriculaJuego);
banderaTocoSuelo = true;
ResultadoAlBajar r = bajarTetrimino(&piezaActual, &piezaSiguiente, cuadriculaJuego, &banderaTocoSuelo, &puntajeGlobal, &juegoTerminado, piezas, &haUsadoLaReserva);
switch (r)
{
case CUATRO_LINEAS_O_MAS:
al_play_sample(
sonido_4_lineas,
1.0,
0.0,
1.0,
ALLEGRO_PLAYMODE_ONCE,
&id_sonido_4_lineas);
break;
case MENOS_DE_CUATRO_LINEAS:
al_play_sample(
sonido_una_linea,
1.0,
0.0,
1.0,
ALLEGRO_PLAYMODE_ONCE,
&id_sonido_una_linea);
break;
default:
break;
}
}
else if (teclaPresionada == ALLEGRO_KEY_J || teclaPresionada == ALLEGRO_KEY_DOWN)
{
ResultadoAlBajar r = bajarTetrimino(&piezaActual, &piezaSiguiente, cuadriculaJuego, &banderaTocoSuelo, &puntajeGlobal, &juegoTerminado, piezas, &haUsadoLaReserva);
switch (r)
{
case CUATRO_LINEAS_O_MAS:
al_play_sample(
sonido_4_lineas,
1.0,
0.0,
1.0,
ALLEGRO_PLAYMODE_ONCE,
&id_sonido_4_lineas);
break;
case MENOS_DE_CUATRO_LINEAS:
al_play_sample(
sonido_una_linea,
1.0,
0.0,
1.0,
ALLEGRO_PLAYMODE_ONCE,
&id_sonido_una_linea);
break;
default:
break;
}
}
else if (teclaPresionada == ALLEGRO_KEY_H || teclaPresionada == ALLEGRO_KEY_LEFT)
{
if (!tetriminoColisionaConCuadriculaAlAvanzar(&piezaActual, cuadriculaJuego, -1, 0))
{
piezaActual.x--;
}
}
else if (teclaPresionada == ALLEGRO_KEY_L || teclaPresionada == ALLEGRO_KEY_RIGHT)
{
if (!tetriminoColisionaConCuadriculaAlAvanzar(&piezaActual, cuadriculaJuego, 1, 0))
{
piezaActual.x++;
}
}
else if (teclaPresionada == ALLEGRO_KEY_SPACE)
{
if (!tetriminoColisionaConCuadriculaAlRotar(&piezaActual, cuadriculaJuego))
{
piezaActual.cuadricula = rotar90CW(piezaActual.cuadricula);
}
}
else if (teclaPresionada == ALLEGRO_KEY_Q)
{
if (reserva.cuadricula == 0)
{
reserva.cuadricula = piezaActual.cuadricula;
reserva.x = MITAD_CUADRICULA_X;
reserva.y = 0;
elegirSiguientePieza(&piezaActual, &piezaSiguiente, piezas);
}
else
{
if (!haUsadoLaReserva)
{
/*
Aquí no recuerdo si está bien reiniciar la x e y o dejarla como
está. Si la dejáramos como está sería más complejo para el jugador,
así que yo pongo la pieza en y=0, o sea, hasta arriba, así le da un respiro al jugador
*/
struct Tetrimino temporal = {0};
temporal.cuadricula = piezaActual.cuadricula;
piezaActual.cuadricula = reserva.cuadricula;
reserva.cuadricula = temporal.cuadricula;
piezaActual.x = MITAD_CUADRICULA_X;
piezaActual.y = 0;
haUsadoLaReserva = true;
}
}
}
}
else
{
puntajeGlobal = 0;
memset(cuadriculaJuego, 0, ANCHO_CUADRICULA * ALTO_CUADRICULA);
juegoTerminado = false;
elegirSiguientePieza(&piezaActual, &piezaSiguiente, piezas);
}
}
else if ((event.type == ALLEGRO_EVENT_DISPLAY_CLOSE))
{
break;
}
if (redraw && al_is_event_queue_empty(queue))
{
if (juegoTerminado)
{
al_clear_to_color(al_map_rgb(255, 255, 0));
al_draw_textf(
fuente,
rojo,
0,
0,
0,
"Juego terminado. Puntaje: %lu. Presiona una tecla para reiniciar",
puntajeGlobal);
redraw = true;
al_flip_display();
continue;
}
// Evitar que haya líneas azules o de colores raros
al_clear_to_color(al_map_rgb(0, 0, 0));
/*
=========================================
Comienza dibujo de la cuadrícula donde las piezas ya han caído
=========================================
*/
float xCoordenada = 0, yCoordenada = 0;
for (int y = 0; y < ALTO_CUADRICULA; y++)
{
for (int x = 0; x < ANCHO_CUADRICULA; x++)
{
for (int i = 0; i < BITS_EN_UN_BYTE; i++)
{
int encendido = (cuadriculaJuego[y][x] >> (MAXIMO_INDICE_BIT_EN_BYTE - i)) & 1;
// Si hay un 1 en este bit entonces dibujamos la imagen
if (encendido)
{
float dx = xCoordenada + GROSOR_BORDE;
float dy = yCoordenada + GROSOR_BORDE;
al_draw_bitmap(imagen_pieza_caida, dx, dy, 0);
}
else
{
// Si no, un rectángulo negro que coincida con el fondo
al_draw_filled_rectangle(xCoordenada + GROSOR_BORDE, yCoordenada + GROSOR_BORDE, xCoordenada + MEDIDA_CUADRO + GROSOR_BORDE, yCoordenada + MEDIDA_CUADRO + GROSOR_BORDE, encendido == 0 ? negro : rojo);
}
xCoordenada += MEDIDA_CUADRO;
}
}
xCoordenada = 0;
yCoordenada += MEDIDA_CUADRO;
}
/*
=========================================
Termina dibujo de la cuadrícula donde las piezas ya han caído
=========================================
*/
/*
=========================================
Empieza dibujo de la pieza (tetrimino) en movimiento
=========================================
*/
for (uint8_t indiceBit = 0; indiceBit < BITS_EN_UINT16; indiceBit++)
{
bool hayUnCuadroDeTetriminoEnLaCoordenadaActual = (piezaActual.cuadricula >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBit)) & 1;
if (hayUnCuadroDeTetriminoEnLaCoordenadaActual)
{
// Llegados aquí sabemos que el "continue" no se ejecutó y que SÍ hay un tetrimino
// Coordenadas sobre la cuadrícula después de aplicar los modificadores
uint8_t xRelativoDentroDeCuadricula = indiceBit % BITS_POR_FILA_PARA_TETRIMINO;
uint8_t YRelativoDentroDeCuadricula = indiceBit / BITS_POR_FILA_PARA_TETRIMINO;
int sumaX = piezaActual.x + xRelativoDentroDeCuadricula;
int sumaY = piezaActual.y + YRelativoDentroDeCuadricula;
int8_t indicePiezaFantasma = indiceYParaFantasma(&piezaActual, cuadriculaJuego);
// Este es el fantasma
al_draw_rectangle((sumaX * MEDIDA_CUADRO) + GROSOR_BORDE, ((YRelativoDentroDeCuadricula + indicePiezaFantasma) * MEDIDA_CUADRO) + GROSOR_BORDE, (sumaX * MEDIDA_CUADRO) + MEDIDA_CUADRO + GROSOR_BORDE, (((YRelativoDentroDeCuadricula + indicePiezaFantasma) * MEDIDA_CUADRO) + MEDIDA_CUADRO) + GROSOR_BORDE, azul, 2);
// Y esta es la pieza en movimiento
float dx = (sumaX * MEDIDA_CUADRO) + GROSOR_BORDE;
float dy = (sumaY * MEDIDA_CUADRO) + GROSOR_BORDE;
al_draw_bitmap(imagen_pieza_movimiento, dx, dy, 0);
}
}
/*
=========================================
Termina dibujo de la pieza (tetrimino) en movimiento
=========================================
*/
/*
=========================================
Empieza dibujo de la pieza (tetrimino) siguiente
=========================================
*/
al_draw_textf(
fuente,
blanco,
(ANCHO_CUADRICULA * BITS_EN_UN_BYTE * MEDIDA_CUADRO) + GROSOR_BORDE * 2,
GROSOR_BORDE,
ALLEGRO_ALIGN_LEFT,
"Siguiente");
for (uint8_t indiceBit = 0; indiceBit < BITS_EN_UINT16; indiceBit++)
{
bool hayUnCuadroDeTetriminoEnLaCoordenadaActual = (piezaSiguiente.cuadricula >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBit)) & 1;
if (hayUnCuadroDeTetriminoEnLaCoordenadaActual)
{
// Llegados aquí sabemos que el "continue" no se ejecutó y que SÍ hay un tetrimino
// Coordenadas sobre la cuadrícula después de aplicar los modificadores
uint8_t xRelativoDentroDeCuadricula = indiceBit % BITS_POR_FILA_PARA_TETRIMINO;
uint8_t YRelativoDentroDeCuadricula = indiceBit / BITS_POR_FILA_PARA_TETRIMINO;
int sumaX = 50 + (ANCHO_CUADRICULA * BITS_EN_UN_BYTE * MEDIDA_CUADRO) + GROSOR_BORDE * 2 + xRelativoDentroDeCuadricula * MEDIDA_CUADRO;
int sumaY = 50 + GROSOR_BORDE + YRelativoDentroDeCuadricula * MEDIDA_CUADRO;
al_draw_bitmap(imagen_pieza_movimiento, sumaX, sumaY, 0);
}
}
/*
=========================================
Empieza dibujo de la pieza (tetrimino) reserva
=========================================
*/
al_draw_textf(
fuente,
blanco,
(ANCHO_CUADRICULA * BITS_EN_UN_BYTE * MEDIDA_CUADRO) + GROSOR_BORDE * 2,
170,
ALLEGRO_ALIGN_LEFT,
"Reserva");
if (reserva.cuadricula != 0)
{
for (uint8_t indiceBit = 0; indiceBit < BITS_EN_UINT16; indiceBit++)
{
bool hayUnCuadroDeTetriminoEnLaCoordenadaActual = (reserva.cuadricula >> (MAXIMO_INDICE_BIT_EN_UINT16 - indiceBit)) & 1;
if (hayUnCuadroDeTetriminoEnLaCoordenadaActual)
{
// Llegados aquí sabemos que el "continue" no se ejecutó y que SÍ hay un tetrimino
// Coordenadas sobre la cuadrícula después de aplicar los modificadores
uint8_t xRelativoDentroDeCuadricula = indiceBit % BITS_POR_FILA_PARA_TETRIMINO;
uint8_t YRelativoDentroDeCuadricula = indiceBit / BITS_POR_FILA_PARA_TETRIMINO;
int sumaX = 50 + (ANCHO_CUADRICULA * BITS_EN_UN_BYTE * MEDIDA_CUADRO) + GROSOR_BORDE * 2 + xRelativoDentroDeCuadricula * MEDIDA_CUADRO;
int sumaY = 200 + GROSOR_BORDE + YRelativoDentroDeCuadricula * MEDIDA_CUADRO;
al_draw_bitmap(imagen_pieza_reserva, sumaX, sumaY, 0);
}
}
}
// Borde separador de puntaje y área de juego
al_draw_rectangle(
GROSOR_BORDE / 2,
GROSOR_BORDE / 2,
(ANCHO_CUADRICULA * BITS_EN_UN_BYTE * MEDIDA_CUADRO) + GROSOR_BORDE * 1.5,
(ALTO_CUADRICULA * MEDIDA_CUADRO) + GROSOR_BORDE * 1.5,
color_borde, GROSOR_BORDE);
// Y ahora el puntaje
al_draw_textf(
fuente,
blanco,
(ANCHO_CUADRICULA * BITS_EN_UN_BYTE * MEDIDA_CUADRO) + GROSOR_BORDE * 2,
ALTO_CUADRICULA * MEDIDA_CUADRO / 2,
ALLEGRO_ALIGN_LEFT,
"Puntaje: %lu",
puntajeGlobal);
al_flip_display();
redraw = false;
}
}
al_destroy_font(font);
al_destroy_display(disp);
al_destroy_timer(timer);
al_destroy_event_queue(queue);
al_destroy_bitmap(imagen_pieza_caida);
al_destroy_bitmap(imagen_pieza_movimiento);
al_destroy_audio_stream(musica_fondo);
al_destroy_sample(sonido_una_linea);
al_destroy_sample(sonido_4_lineas);
al_uninstall_audio();
return 0;
}
Y el repositorio completo se encuentra en GitHub