Skip to content

Instantly share code, notes, and snippets.

@arget13
Last active March 22, 2023 12:17
Show Gist options
  • Save arget13/07d55f6986a6ec776982076592fdb35b to your computer and use it in GitHub Desktop.
Save arget13/07d55f6986a6ec776982076592fdb35b to your computer and use it in GitHub Desktop.
A simple implementation of asteroids game for modern terminals.
/**
Name : Asteroids
Author : Arget
Version : 1.4
Date : 16/11/2017
Description : A simple implementation of asteroids game for modern terminals.
Notes : Compile with `gcc -o asteroids asteroids.c'
*/
#include <stdio.h> /* printf(), putchar() */
#include <termios.h> /* tcgetattr(), tcsetattr(), struct termios,
defs. of ECHO, ICANON, VTIME, VMIN, TCSANOW */
#include <string.h> /* memcpy() */
#include <stdlib.h> /* srandom(), random(), malloc(), realloc(), free() */
#include <unistd.h> /* usleep(), read(), def. of STDIN_FILENO */
#include <signal.h> /* sigaction(), struct sigaction, defs. of SIGTERM,
SIGINT */
#include <time.h> /* time(), struct timeval, time_t */
#include <sys/time.h> /* gettimeofday() */
#include <sys/ioctl.h>
/* Valores números asociados a
las teclas de los cursores */
#define UP 65
#define DOWN 66
#define RIGHT 67
#define LEFT 68
/* Facilitan la comprensión de qué
dato guarda cada elemento en los
arrays empleados para los asteroides */
#define X 0
#define Y 1
/*** VARIABLES GLOBALES ***/
/* Maneja la ejecución del bucle while en el que
se basa todo el juego. En el momento en que keeprunning
vale 0 el juego termina.
Esta variable la manipula la funcion inthandler() */
int keeprunning;
/* Manejan la posición de la nave */
unsigned int x, y;
unsigned int oldx, oldy;
/* Guardan la posición de la bala */
unsigned int bulletx = 0;
unsigned int bullety = 0;
/* Almacenará la puntuación */
unsigned int score;
/* Contiene el nivel en el que se encuentra el usuario */
int level;
/* Número de vidas inicial */
#ifndef INITLIVES
# define INITLIVES 5
#endif
/* Almacena el número de vidas a cada momento */
int lives;
/* Almacena el número de asteroides
simultáneos en el campo de juego */
unsigned int numast;
/* Una tabla que relaciona cada
asteroide con su posición */
unsigned int **asteroids;
/* Empleadas para mostrar el tiempo de
juego en la función gametime() */
time_t init_time, last_time;
/* Manejan las coordenadas de la nueva vida */
int livex, livey;
/* En esta estructura obtendremos el tamaño de la terminal */
struct winsize ws;
/*** FUNCIONES ***/
/* Dibuja la nave en la nueva posición tras borrarla
de la antigua */
void draw_aircraft(int x, int y, int oldx, int oldy)
{
if(oldx && oldy)
printf("\033[%d;%dH ", oldy, oldx - 1);
printf("\033[%d;%dH╔█╗", y, x - 1);
}
/*
void draw_aircraft(int x, int y, int oldx, int oldy)
{
if(oldx && oldy)
printf("\033[%d;%dH \033[%d;%dH \033[%d;%dH ",
oldy - 1, oldx - 1, oldy, oldx - 1, oldy + 1, oldx - 2);
printf("\033[%d;%dH═╩═\033[%d;%dH███\033[%d;%dH/]x[\\",
y - 1, x - 1, y, x - 1, y + 1, x - 2);
}*/
/* Dibuja en pantalla un campo de juego */
void draw_field()
{
int i;
printf("\033[1;1H╔\033[%1$u;1H╚\033[1;%2$uH╗\033[%1$u;%2$uH╝", ws.ws_row, ws.ws_col);
for(i = 2; i < ws.ws_row; i++)
printf("\033[%1$u;1H║", i);
for(i = 2; i < ws.ws_row; i++)
printf("\033[%1$u;%2$uH║", i, ws.ws_col);
for(i = 2; i < ws.ws_col; i++)
printf("\033[1;%1$uH═", i);
for(i = 2; i < ws.ws_col; i++)
printf("\033[%2$u;%1$uH═", i, ws.ws_row);
printf("\033[1;%uH Tiempo: ", ws.ws_col / 2 - 7);
}
/* Muestra la "mira" para apuntar */
void show_sight(int x, int oldx)
{
if(y == 2) return;
if(oldx)
printf("\033[%u;%uH \x1b[1;41m\033[%1$u;%3$uH \x1b[0;0m", 2, oldx, x);
else
printf("\x1b[1;41m\033[%u;%uH \x1b[0;0m", 2, x);
}
/* Sitúa en el campo de juego la bala en su posición */
void draw_bullet()
{
/* Cambiamos la posición de la bala */
bullety--;
/* Borramos la bala y la redibujamos en la nueva posición */
if(bullety > 1) printf("\033[%d;%3$dH \033[%2$d;%3$dH_", bullety + 1, bullety, bulletx);
else if(bullety == 1) printf("\033[%d;%dH ", bullety + 1, bulletx);
else
show_sight(x, 0);
}
/* Muestra en pantalla la puntuación */
void draw_score()
{
printf("\033[0;%1$uH \033[0;%1$uH Puntos: %2$07u ", ws.ws_col - 20, score);
}
void draw_lives()
{
int i;
printf("\033[1;4H ");
printf("\033[1;11H");
for(i = 0; i < INITLIVES; i++)
putchar(' ');
printf("\033[1;3H Vidas: ");
for(i = 0; i < lives; i++)
printf("♥");
for(; i < INITLIVES; i++)
printf("×");
putchar(' ');
}
/* Sitúa en el campo cada asteroide en su lugar
correspondiente */
void draw_asteroids()
{
int i;
/* Borramos de su antigua posición todos los asteroides */
for(i = 0; i < numast; i++)
printf("\033[%u;%uH ", asteroids[i][Y], asteroids[i][X]);
/* Comprobamos si alguno tiene su y abajo del todo,
en ese caso los que hayan llegado abajo del todo
colocamos su y arriba del todo, si no están abajo
del todo simplemente incrementamos su y en 1 */
for(i = 0; i < numast; i++)
asteroids[i][Y] = (asteroids[i][Y] > ws.ws_row - 2) ? 2 : asteroids[i][Y] + 1;
/* Los que se encuentren arriba del todo es que los
acabamos de colocar ahí en las instrucciones anteriores,
luego es necesario cambiar su x por un valor aleatorio dentro del
rango del campo, si no se encuentran arriba del todo mantenemos
su x sin variar */
for(i = 0; i < numast; i++)
asteroids[i][X] = (asteroids[i][Y] == 2) ? (random() % (ws.ws_col - 2)) + 2 : asteroids[i][X];
/* Imprimimos cada asteroide en su posición actual */
for(i = 0; i < numast; i++)
printf("\033[%u;%uH*", asteroids[i][Y], asteroids[i][X]);
}
/* Muestra el tiempo de juego en segundos */
void gametime(time_t t)
{
last_time = t;
printf("\033[1;%2$uH%06u ", last_time - init_time, ws.ws_col / 2 + 2);
}
/* Imprime el nivel al que se ha llegado */
void gamelevel(int level)
{
printf("\033[%2$u;%3$uH Nivel %1$02u \033[%4$u;%5$uHNIVEL %1$02u", level, ws.ws_row,
ws.ws_col - 20, ws.ws_row / 2, ws.ws_col / 2 - 5);
sleep(1);
printf("\033[%u;%uH ", ws.ws_row / 2, ws.ws_col / 2 - 5);
}
/* Muestra el mensaje de "GAMEOVER" */
void gameover()
{
/* Limpiamos la pantalla de la terminal */
printf("\033[H\033[J");
/* Pintamos en pantalla el campo de juego */
draw_field();
/* Pintamos la puntuación */
draw_score();
printf("\033[%u;%uHGAMEOVER", ws.ws_row / 2, ws.ws_col / 2 - 4);
gametime(time(NULL));
}
/* Establece la terminación del juego */
void inthandler()
{
gameover();
printf("\033[%u;%uH Nivel %02u ", ws.ws_row, ws.ws_col - 20, level);
char c;
while(read(fileno(stdin), &c, 1) == 0 || c != ' ');
keeprunning = 0;
keeprunning = 0;
}
/* Maneja el movimiento de "vidas" nuevas que caen */
void draw_new_live(int a)
{
livey++;
if(livey == ws.ws_row)
{
printf("\033[%u;%uH ", livey - 1, livex);
livey = 1;
livex = random() % (ws.ws_col - 2) + 2;
if(!a) return;
printf("\033[%u;%uH♥", livey, livex);
}
else
printf("\033[%1$u;%2$uH \033[%3$u;%2$uH♥", livey - 1, livex, livey);
}
/* Maneja la pausa en el juego */
void gamepause()
{
char c;
/* Indicamos la pausa */
printf("\033[%u;%uHPAUSE", ws.ws_row / 2, ws.ws_col / 2 - 3);
/* y esperamos a que se presione p */
while(keeprunning && (read(fileno(stdin), &c, 1) == 0 || c != 'p'));
printf("\033[%u;%uH ", ws.ws_row / 2, ws.ws_col / 2 - 3);
}
/* Se ejecuta si se cambia de tamaño la terminal */
void winch()
{
// ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
keeprunning = 0;
}
int main(void)
{
/* Un simple contador */
unsigned int i;
/* Funcionarán para la temporización correcta
de cada evento del juego: movimiento de
asteroides, de bala, de vida nueva e incremento
de dificultad */
struct timeval clk_ast, clk_ast_;
struct timeval clk_bul, clk_bul_;
time_t level_time, level_time_;
unsigned int level_time_diff;
time_t add_asteroid_time, add_asteroid_time_;
unsigned int add_asteroid_time_diff;
time_t live_time, live_time_;
unsigned int live_time_diff;
unsigned int live_last_time;
/* Participarán en el manejo de señales */
struct sigaction act;
struct sigaction act2;
/* Servirán para el manejo de características de la terminal */
struct termios term_attr;
struct termios old_term_attr;
/* Almacenará el valor de las teclas presionadas */
char k;
/* Almacena el tiempo en microsecundos necesario para que
los asteroides avancen una posición, en cada nivel
este valor se decrementa en una 0,01s, comienza valiendo
0,1s */
int ast_speed;
/* Si recibimos alguna señal de terminación
ejecutamos inthandler(), quien pondrá a 0
la variable keeprunning, lo que hará que
el bucle while termine y se ejecuten funciones
de limpieza y terminemos */
memset(&act, 0, sizeof(act));
act.sa_handler = &inthandler;
sigaction(SIGINT, &act, NULL);
sigaction(SIGTERM, &act, NULL);
srandom(time(NULL));
/* Obtenemos el tamaño de la terminal */
ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
/* Si cambia de tamaño la terminal ejecutamos winch() */
memset(&act2, 0, sizeof(act2));
act2.sa_handler = &winch;
sigaction(SIGWINCH, &act2, NULL);
/* Establecemos los valores iniciales de ciertas variables del juego */
ast_speed = 100000;
/* Obtenemos tiempo inicial para distintos parámetros */
gettimeofday(&clk_ast, NULL);
memcpy(&clk_bul, &clk_ast, sizeof(struct timeval));
level_time = time(NULL);
level_time_diff = 20;
add_asteroid_time = level_time;
add_asteroid_time_diff = 5;
live_time = level_time;
live_time_diff = random() % 60;
live_last_time = level_time;
live_time_diff = 5;
/* Deshabilitamos el búfer para stdout */
setvbuf(stdout, NULL, _IONBF, 0);
/* Ocultamos el cursor */
printf("\x1b[?25l");
/* Limpiamos la pantalla de la terminal */
printf("\033[H\033[J");
/* Ponemos el stdin en modo raw, deshabilitamos el
echo de la terminal y leemos directamente las
pulsaciones del teclado */
tcgetattr(STDIN_FILENO, &old_term_attr);
memcpy(&term_attr, &old_term_attr, sizeof(struct termios));
term_attr.c_lflag &= ~(ECHO | ICANON);
term_attr.c_cc[VTIME] = 0;
term_attr.c_cc[VMIN] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &term_attr);
lives = INITLIVES;
score = 0;
x = ws.ws_col / 2;
y = ws.ws_row - 1;
keeprunning = 1;
numast = 1;
last_time = 0;
init_time = time(NULL);
livex = random() % ws.ws_col + 2;
livey = 1;
/* Mostramos el puntero de la mira, debe de ser antes de pintar
el campo, si no, al ser 0 el segundo argumento,
superior izquierda quedaría con un bloque blanco */
show_sight(x, 0);
/* Pintamos en pantalla el campo de juego */
draw_field();
/* Mostramos las vidas */
draw_lives();
/* Mostramos la puntuación */
draw_score();
/* Colocamos la nave en su posición inicial */
draw_aircraft(x, y, 0, 0);
/* Asignamos a asteroids espacio para apuntar a la instancia de cada
uno de los `numast' asteroides (numast empieza definiendo la cantidad
de asteroides iniciales, más adelante este valor se incrementará para
añadir más asteroides) */
asteroids = malloc(numast * sizeof(void*));
/* A cada instancia le asignamos espacio para almacenar
las coordenadas del asteroide correspondiente */
for(i = 0; i < numast; i++)
asteroids[i] = malloc(2 * sizeof(int));
for(i = 0; i < numast; i++)
{
/* Cada asteroide lo inicializamos a una posición */
asteroids[i][X] = (random() % (ws.ws_col) - 2) + 2;
asteroids[i][Y] = 2;
}
/* Comenzamos por el nivel 0 */
level = 0;
gamelevel(level);
/* Este bucle constituye todo el juego
tras haberlo inicializado, se ejecuta hasta
recibir una señal SIGINT (ctrl-c) o SIGTERM,
o hasta que se terminen las vidas del jugador */
while(keeprunning)
{
/* Además de asignar a level_time_ el valor
de time(), que se comprobará más adelante
para aumentar el nivel de dificultad del juego,
este código comprueba si ha transcurrido
al menos 1 segundo desde la última llamada a gametime(),
quien imprime la cantidad de segundos transcurridos desde
el comienzo del juego */
if(time(&level_time_) > last_time)
gametime(level_time_);
/* También asignamos el tiempo actual a add_asteroid_time_ y
a live_time_ , que se comprobarán más adelante para añadir
un asteroide o para introducir en el campo de juego una vida
que podrá recoger el jugador */
add_asteroid_time_ = level_time_;
live_time_ = level_time_;
if(live_time_ > live_last_time && livey > 1)
{
live_last_time = live_time_;
draw_new_live(0);
}
/* Obtenemos la hora actual para comparar con una anterior
y saber si ya toca ejecutar algún evento del juego: mover
asteroides o balas*/
gettimeofday(&clk_ast_, NULL);
memcpy(&clk_bul_, &clk_ast_, sizeof(struct timeval));
/* Se comprueba si hay
presionada alguna tecla */
if(read(fileno(stdin), &k, 1) == 1)
{
oldy = y;
oldx = x;
/* Efectuamos las acciones respectivas
a cada tecla */
switch(k)
{
case 's':
if(y < ws.ws_row - 1) /* Terminal de ws.ws_col x ws.ws_row */
y++;
break;
case DOWN:
if(y < ws.ws_row - 1)
y++;
break;
case 'w':
if(y > 2)
y--;
break;
case UP:
if(y > 2)
y--;
break;
case 'a':
if(x > 3)
x--;
break;
case LEFT:
if(x > 3)
x--;
break;
case 'd':
if(x < ws.ws_col - 2)
x++;
break;
case RIGHT:
if(x < ws.ws_col - 2)
x++;
break;
case ' ':
if(!bullety) /* Se dispara una bala solo si
no hay otra todavía en el campo */
{
bullety = y - 1;
bulletx = x; /* La bala se mueve en vertical */
}
break;
case 'p':
gamepause();
break;
}
/* Redibujamos la nave en la nueva posición */
draw_aircraft(x, y, oldx, oldy);
if(oldx != x) show_sight(x, oldx);
}
/* Dibujamos la bala en la nueva posición, si no ha
sido disparada no se hace nada
Avanza a una velocidad de una posición cada 0,05 segundos */
if(bullety && ((((long long)(clk_bul_.tv_sec * 1000000) + clk_bul_.tv_usec)
- ((long long)(clk_bul .tv_sec * 1000000) + clk_bul .tv_usec)) >= 50000))
{
draw_bullet();
memcpy(&clk_bul, &clk_bul_, sizeof(struct timeval));
}
/* Comprobamos para cada asteroide si sus coordenadas coinciden con las de
la bala, en ese caso borramos ambos y reiniciamos sus posiciones. Esta comprobación
se realiza antes de mover los asteroides para evitar que se crucen
bala y asteroide sin detectarse. */
for(i = 0; i < numast; i++)
{
if(bulletx == asteroids[i][X] && bullety == asteroids[i][Y])
{
asteroids[i][X] = (random() % (ws.ws_col - 2)) + 2;
asteroids[i][Y] = 2;
printf("\033[%d;%dH ", bullety, bulletx);
printf("\033[%d;%dH+10", bullety, bulletx - 1);
usleep(500000);
printf("\033[%d;%dH ", bullety, bulletx - 1);
bullety = 0;
show_sight(x, oldx);
/* Actualizamos la puntuación */
score += 10;
draw_score();
}
}
/* Dibujamos cada asteroide en su nueva posición respectiva,
avanzan todos a la vez a una velocidad de una posición cada
0,1 segundos */
if((((long long)(clk_ast_.tv_sec * 1000000 + clk_ast_.tv_usec))
- ((long long)(clk_ast .tv_sec * 1000000 + clk_ast .tv_usec))) >= ast_speed)
{
memcpy(&clk_ast, &clk_ast_, sizeof(struct timeval));
draw_asteroids();
}
/* Comprobamos si un asteroide ha tocado alguna parte de la nave */
for(i = 0; i < numast; i++)
{
if(asteroids[i][X] >= x - 1 && asteroids[i][X] <= x + 1)
{
if(asteroids[i][Y] == y)
{
lives--;
draw_lives();
putchar('\a');
if(lives == 0)
{
gameover();
printf("\033[%u;%uH Nivel %02u ", ws.ws_row, ws.ws_col - 20, level);
char c;
while(read(fileno(stdin), &c, 1) == 0 || c != ' ');
keeprunning = 0;
}
else
{
asteroids[i][X] = (random() % (ws.ws_col - 2)) + 2;
asteroids[i][Y] = 2;
draw_aircraft(x, y, 0, 0);
}
}
}
}
/* Comprobamos si la vida ha tocado la nave */
if(livex >= x - 1 && livex <= x + 1)
{
if(livey == y)
{
/* Normalmente se añade al jugador una vida, */
if(lives < INITLIVES)
{
lives++;
draw_lives();
}
else /* pero si ya se tiene el max de vidas, se dan 50 puntos */
{
printf("\033[%u;%uH+50", y, x - 1);
score += 50;
draw_score();
usleep(500000);
printf("\033[%u;%uH ", y, x - 1);
}
livey = 1;
livex = random() % (ws.ws_col - 2) + 2;
draw_aircraft(x, y, 0, 0);
}
}
/* Si un asteroide toca la vida, esta desaparece y el asteroide sigue
(balas y asteroides van mucho más rápido que las vidas) */
for(i = 0; i < numast; i++)
{
if(livex == asteroids[i][X] && livey == asteroids[i][Y])
{
livey = 1;
livex = random() % (ws.ws_col - 2) + 2;
}
}
/* Si la bala toca la vida, esta desaparece y la bala sigue */
if(livex == bulletx && livey == bullety)
{
livey = 1;
livex = random() % (ws.ws_col - 2) + 2;
}
/* Si se ha cumplido el tiempo necesario para añadir un asteroide, lo añadimos */
if((add_asteroid_time_ - add_asteroid_time) >= add_asteroid_time_diff)
{
/* Incrementamos el tiempo necesario para añadir un asteroide */
add_asteroid_time = add_asteroid_time_;
add_asteroid_time_diff += 10;
/* Incrementamos el contador de asteroides */
numast++;
/* Primero al puntero asteroids le asignamos suficiente espacio
para apuntar a una nueva "instancia de asteroide" */
asteroids = realloc(asteroids, numast * sizeof(int*));
/* Luego asignamos a esta última entrada espacio
suficiente para almacenar el x e y del nuevo asteroide */
asteroids[numast - 1] = malloc(2 * sizeof(int));
/* Inicializamos la posición del nuevo asteroide */
asteroids[numast - 1][X] = (random() % (ws.ws_col - 2)) + 2;
asteroids[numast - 1][Y] = 2;
}
/* Cada level_time_diff segundos, se aumenta la dificultad,
se aumenta 10 veces la velocidad de los asteroides */
if((level_time_ - level_time) >= level_time_diff)
{
level++;
gamelevel(level);
draw_aircraft(x, y, 0, 0);
/* Subimos el nivel aumentando la velocidad de los asteroides */
level_time = level_time_;
level_time_diff += 10;
/* Ahora tarda una centésima menos en avanzar una posición */
ast_speed -= 10000;
}
/* Cuando se cumpla el tiempo especificado por live_time_diff
se añadirá (si no está ya) una vida al campo de juego */
if((live_time_ - live_time) >= live_time_diff)
{
live_time = live_time_;
live_time_diff = random() % 60;
if(livey == 1)
{
livey = 2;
draw_new_live(1);
}
}
}
/* Liberamos toda la memoria reservada dinámicamente */
for(i = 0; i < numast; i++)
free(asteroids[i]);
free(asteroids);
/* Restablecemos la configuración de la terminal a la original */
tcsetattr(STDIN_FILENO, TCSANOW, &old_term_attr);
/* Limpiamos la pantalla de la terminal */
printf("\033[H\033[J");
/* Mostramos el cursor de la terminal */
printf("\x1b[?25h");
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment