Created
October 14, 2021 12:01
-
-
Save yves-chevallier/ff253d90a0ece9633ea34d301259efa2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define _POSIX_SOURCE | |
#include <stdlib.h> | |
#include <curses.h> | |
#include <signal.h> | |
#include <sys/time.h> | |
#include <errno.h> | |
#include <time.h> | |
#include <string.h> | |
#include <stdbool.h> | |
#define ASSERT(N, X) \ | |
{ \ | |
if (X != OK) \ | |
{ \ | |
fprintf(stderr, "%s call returned error: %d\n", N, X); \ | |
end_game(&game, timer); \ | |
exit(1); \ | |
} \ | |
} | |
#define DRAW() \ | |
prefresh(board, 0, 0, BOARD_Y_OFF, BOARD_X_OFF, \ | |
BOARD_HEIGHT + BOARD_Y_OFF, BOARD_WIDTH + BOARD_X_OFF); \ | |
prefresh(score_board, 0, 0, SCORE_BOARD_Y_OFF, SCORE_BOARD_X_OFF, \ | |
BOARD_HEIGHT + SCORE_BOARD_Y_OFF, SCORE_BOARD_WIDTH + SCORE_BOARD_X_OFF) | |
#define MAX_DIR_MEMORY 10 | |
#define BOARD_HEIGHT 15 | |
#define BOARD_WIDTH 30 | |
#define BOARD_X_OFF 8 | |
#define BOARD_Y_OFF 1 | |
#define SCORE_BOARD_WIDTH 20 | |
#define SCORE_BOARD_HEIGHT 10 | |
#define SCORE_BOARD_X_OFF BOARD_WIDTH + BOARD_X_OFF * 2 | |
#define SCORE_BOARD_Y_OFF BOARD_Y_OFF + 1 | |
#define DEFAULT_SNAKE_BODY_ARRAY_SIZE 100 | |
bool walk = false; | |
typedef struct | |
{ | |
int x; | |
int y; | |
} Point; | |
const Point S_DOWN = {0, 1}; | |
const Point S_UP = {0, -1}; | |
const Point S_LEFT = {-1, 0}; | |
const Point S_RIGHT = {1, 0}; | |
const Point S_NO_DIR = {0, 0}; | |
typedef struct | |
{ | |
Point *body; | |
int __b_size; | |
int __b_array_size; | |
int __b_begin; | |
Point facing; | |
} Snake; | |
typedef struct | |
{ | |
int board_width; | |
int board_height; | |
int speed; | |
int next_speed; | |
bool paused; | |
Point food; | |
Snake snake; | |
bool dead; | |
Point dir_memory[MAX_DIR_MEMORY]; | |
int __dm_size; | |
int __dm_begin; | |
uint64_t score; | |
} Game; | |
bool point_eq(Point a, Point b) | |
{ | |
return a.x == b.x && a.y == b.y; | |
} | |
void stop_alarm(struct itimerval *it) | |
{ | |
struct timeval tv = {0, 0}; | |
it->it_value = tv; | |
setitimer(ITIMER_REAL, it, NULL); | |
free(it); | |
} | |
void end_game(Game *game, struct itimerval *timer) | |
{ | |
free(game->snake.body); | |
stop_alarm(timer); | |
} | |
void handle_sigalrm(int sig_count) | |
{ | |
walk = true; | |
} | |
Point point_add(Point a, Point b) | |
{ | |
a.x += b.x; | |
a.y += b.y; | |
return a; | |
} | |
bool changed_direction(Point a, Point b) | |
{ | |
return !( | |
(a.x == b.x && a.y == b.y) || | |
(a.x != 0 && a.x == -b.x) || | |
(a.y != 0 && a.y == -b.y) | |
); | |
} | |
void render_snake(WINDOW *board, Snake *snake, bool remove) | |
{ | |
for (size_t i = 0; i < snake->__b_size; i++) | |
{ | |
Point pos = snake->body[(snake->__b_begin + i) % snake->__b_array_size]; | |
wmove(board, pos.y + 1, pos.x + 1); | |
waddch(board, remove ? ' ' : i == snake->__b_size - 1 ? '0' | |
: '#'); | |
} | |
} | |
Point to_dir(int key) | |
{ | |
Point dir = S_NO_DIR; | |
switch (key) | |
{ | |
case KEY_LEFT: | |
case 'h': | |
dir = S_LEFT; | |
break; | |
case KEY_DOWN: | |
case 'j': | |
dir = S_DOWN; | |
break; | |
case KEY_UP: | |
case 'k': | |
dir = S_UP; | |
break; | |
case KEY_RIGHT: | |
case 'l': | |
dir = S_RIGHT; | |
break; | |
} | |
return dir; | |
} | |
void add_dir(Game *game, Point dir) | |
{ | |
if (game->__dm_size < MAX_DIR_MEMORY && !point_eq(dir, S_NO_DIR) && ( | |
( | |
game->__dm_size == 0 && | |
changed_direction(dir, game->snake.facing) | |
) || | |
( | |
game->__dm_size > 0 && changed_direction( | |
dir, | |
game->dir_memory[ | |
(game->__dm_begin + game->__dm_size - 1) % MAX_DIR_MEMORY | |
] | |
) | |
) | |
)) | |
{ | |
game->dir_memory[(game->__dm_begin + game->__dm_size) % MAX_DIR_MEMORY] = dir; | |
game->__dm_size++; | |
} | |
} | |
void snake_go(Snake *snake) | |
{ | |
snake->body[(snake->__b_begin + snake->__b_size) % snake->__b_array_size] = point_add( | |
snake->body[(snake->__b_begin + snake->__b_size - 1) % snake->__b_array_size], | |
snake->facing); | |
snake->__b_begin = (snake->__b_begin + 1) % snake->__b_array_size; | |
} | |
int rand_range(int upper_bound) | |
{ | |
return ((rand() - 1) / (double)RAND_MAX) * upper_bound; | |
} | |
int compare_int(const void *int1, const void *int2) | |
{ | |
return *((int *)int1) - *((int *)int2); | |
} | |
Point gen_food(Game *game) | |
{ | |
int w = BOARD_WIDTH - 2; | |
int h = BOARD_HEIGHT - 2; | |
int len = game->snake.__b_size; | |
int tile_count = w * h - len; | |
int used_tiles[len]; | |
for (size_t i = 0; i < len; i++) | |
{ | |
Point p = game->snake.body[(game->snake.__b_begin + i) % game->snake.__b_array_size]; | |
used_tiles[i] = p.y * w + p.x + 1; | |
} | |
qsort((void *)used_tiles, len, sizeof(int), compare_int); | |
int pick = rand_range(tile_count) + 1; | |
for (size_t i = 0; i < len; i++) | |
{ | |
if (used_tiles[i] <= pick) | |
{ | |
pick++; | |
} | |
else | |
{ | |
break; | |
} | |
} | |
Point food; | |
food.x = (pick - 1) % w; | |
food.y = (pick - 1) / w; | |
return food; | |
} | |
void render_food(WINDOW *board, Game *game) | |
{ | |
wmove(board, game->food.y + 1, game->food.x + 1); | |
waddch(board, '*'); | |
} | |
void walk_snake(Game *game) | |
{ | |
Snake *snake = &game->snake; | |
if (game->__dm_size > 0) | |
{ | |
game->snake.facing = game->dir_memory[game->__dm_begin]; | |
game->__dm_size--; | |
game->__dm_begin = (game->__dm_begin + 1) % MAX_DIR_MEMORY; | |
} | |
snake_go(snake); | |
Point head = game->snake.body[(snake->__b_begin + snake->__b_size - 1) % snake->__b_array_size]; | |
if (head.x < 0 || head.y < 0 || head.x > BOARD_WIDTH - 3 || head.y > BOARD_HEIGHT - 3) | |
{ | |
game->dead = true; | |
} | |
else | |
{ | |
for (size_t i = 0; i < snake->__b_size - 1; i++) | |
{ | |
Point segment = snake->body[(snake->__b_begin + i) % snake->__b_array_size]; | |
if (segment.x == head.x && segment.y == head.y) | |
{ | |
game->dead = true; | |
break; | |
} | |
} | |
} | |
if (head.x == game->food.x && head.y == game->food.y) | |
{ | |
snake->__b_begin = (snake->__b_begin + snake->__b_array_size - 1) % snake->__b_array_size; | |
snake->__b_size++; | |
if (snake->__b_size == snake->__b_array_size) | |
{ | |
Point *old_body = snake->body; | |
int newsize = 2 * snake->__b_array_size; | |
snake->body = (Point *)malloc(sizeof(Point) * newsize); | |
memcpy(snake->body, old_body + snake->__b_begin, (snake->__b_array_size - snake->__b_begin) * sizeof(Point)); | |
memcpy(snake->body + (snake->__b_array_size - snake->__b_begin), old_body, snake->__b_begin * sizeof(Point)); | |
free(old_body); | |
snake->__b_begin = 0; | |
snake->__b_array_size = newsize; | |
} | |
game->score += game->speed; | |
game->food = gen_food(game); | |
} | |
} | |
struct itimerval *start_alarm() | |
{ | |
struct timeval tv = {0, 0}; | |
struct itimerval *itp = (struct itimerval *)malloc(sizeof(struct itimerval)); | |
struct itimerval it = {tv, tv}; | |
*itp = it; | |
setitimer(ITIMER_REAL, itp, NULL); | |
return itp; | |
} | |
void pause_alarm(struct itimerval *it) | |
{ | |
struct timeval tv = {0, 0}; | |
it->it_value = tv; | |
setitimer(ITIMER_REAL, it, NULL); | |
} | |
void resume_alarm(struct itimerval *it) | |
{ | |
it->it_value = it->it_interval; | |
setitimer(ITIMER_REAL, it, NULL); | |
} | |
void set_alarm_speed(struct itimerval *it, int speed) | |
{ | |
struct timeval tv; | |
if (speed == 1) | |
{ | |
tv.tv_sec = 1; | |
tv.tv_usec = 0; | |
} | |
else | |
{ | |
tv.tv_sec = 0; | |
tv.tv_usec = 1000000 / speed; | |
} | |
it->it_value = tv; | |
it->it_interval = tv; | |
setitimer(ITIMER_REAL, it, NULL); | |
} | |
void set_alarm_handler() | |
{ | |
struct sigaction sa; | |
sa.sa_handler = handle_sigalrm; | |
sigaction(SIGALRM, &sa, NULL); | |
} | |
void render_num(WINDOW *wd, uint64_t number, int x_off, int y_off) | |
{ | |
if (number == 0) | |
{ | |
wmove(wd, y_off, x_off); | |
waddch(wd, '0'); | |
} | |
else | |
{ | |
while (number > 0) | |
{ | |
wmove(wd, y_off, x_off); | |
x_off--; | |
waddch(wd, '0' + (number % 10)); | |
number /= 10; | |
} | |
} | |
} | |
void render_score(WINDOW *score_board, uint64_t score) | |
{ | |
render_num(score_board, score, SCORE_BOARD_WIDTH - 1, 0); | |
} | |
void render_speed(WINDOW *score_board, uint64_t speed) | |
{ | |
render_num(score_board, speed, SCORE_BOARD_WIDTH - 1, 2); | |
} | |
void init_score_board(WINDOW *score_board) | |
{ | |
wmove(score_board, 0, 0); | |
waddstr(score_board, "Score:"); | |
wmove(score_board, 2, 0); | |
waddstr(score_board, "Speed:"); | |
} | |
Snake init_snake() | |
{ | |
Snake snake; | |
snake.body = (Point *)malloc(sizeof(Point) * DEFAULT_SNAKE_BODY_ARRAY_SIZE); | |
Point pos = {0, BOARD_HEIGHT - 3}; | |
snake.body[0] = pos; | |
pos.x++; | |
snake.body[1] = pos; | |
pos.x++; | |
snake.body[2] = pos; | |
snake.__b_array_size = DEFAULT_SNAKE_BODY_ARRAY_SIZE; | |
snake.__b_size = 3; | |
snake.__b_begin = 0; | |
snake.facing = S_RIGHT; | |
return snake; | |
} | |
void init_board(WINDOW *board) | |
{ | |
wmove(board, 0, 0); | |
for (size_t i = 0; i < BOARD_WIDTH; i++) | |
{ | |
waddch(board, '_'); | |
} | |
wmove(board, BOARD_HEIGHT - 1, 0); | |
for (size_t i = 0; i < BOARD_WIDTH; i++) | |
{ | |
waddch(board, 'T'); | |
} | |
for (size_t i = 0; i < BOARD_HEIGHT - 2; i++) | |
{ | |
wmove(board, i + 1, 0); | |
waddch(board, '\\'); | |
wmove(board, i + 1, BOARD_WIDTH - 1); | |
waddch(board, '/'); | |
} | |
} | |
Game init_game() | |
{ | |
Game game; | |
game.board_width = BOARD_WIDTH; | |
game.board_height = BOARD_HEIGHT; | |
game.speed = 5; | |
game.next_speed = game.speed; | |
game.paused = false; | |
game.snake = init_snake(); | |
game.dead = false; | |
game.__dm_size = 0; | |
game.__dm_begin = 0; | |
game.score = 0; | |
game.food = gen_food(&game); | |
return game; | |
} | |
int main(void) | |
{ | |
srand(time(NULL)); | |
set_alarm_handler(); | |
Game game = init_game(); | |
struct itimerval *timer = start_alarm(); | |
set_alarm_speed(timer, game.speed); | |
WINDOW *stdscr = initscr(); | |
WINDOW *board = newpad(BOARD_HEIGHT, BOARD_WIDTH); | |
WINDOW *score_board = newpad(SCORE_BOARD_HEIGHT, SCORE_BOARD_WIDTH); | |
ASSERT("cbreak", cbreak()); | |
ASSERT("noecho", noecho()); | |
ASSERT("nonl", nonl()); | |
ASSERT("initrflush", intrflush(stdscr, false)); | |
ASSERT("keypad", keypad(stdscr, true)); | |
wrefresh(stdscr); | |
init_board(board); | |
init_score_board(score_board); | |
render_snake(board, &game.snake, false); | |
render_food(board, &game); | |
render_score(score_board, game.score); | |
render_speed(score_board, game.next_speed); | |
wmove(stdscr, LINES - 1, COLS - 1); //get cursor out of the way; | |
DRAW(); | |
int ch; | |
do | |
{ | |
if (walk && !game.dead) | |
{ | |
walk = false; | |
render_snake(board, &game.snake, true); | |
walk_snake(&game); | |
render_snake(board, &game.snake, false); | |
render_food(board, &game); | |
render_score(score_board, game.score); | |
wmove(stdscr, LINES - 1, COLS - 1); //get cursor out of the way; | |
DRAW(); | |
} | |
errno = 0; | |
ch = wgetch(stdscr); | |
if (errno == 0 && ch != ERR) | |
{ | |
if (ch == 'r') | |
{ | |
free(game.snake.body); | |
int next_speed = game.next_speed; | |
game = init_game(); | |
game.speed = next_speed; | |
game.next_speed = next_speed; | |
set_alarm_speed(timer, game.speed); | |
delwin(board); | |
delwin(score_board); | |
board = newpad(BOARD_HEIGHT, BOARD_WIDTH); | |
score_board = newpad(SCORE_BOARD_HEIGHT, SCORE_BOARD_WIDTH); | |
init_board(board); | |
init_score_board(score_board); | |
render_snake(board, &game.snake, false); | |
render_score(score_board, game.score); | |
render_speed(score_board, game.next_speed); | |
render_food(board, &game); | |
wmove(stdscr, LINES - 1, COLS - 1); //get cursor out of the way; | |
DRAW(); | |
} | |
else if (ch == 'p') | |
{ | |
if (game.paused) | |
{ | |
resume_alarm(timer); | |
} | |
else | |
{ | |
pause_alarm(timer); | |
} | |
game.paused = !game.paused; | |
} | |
else if (ch == '+' || ch == '-') | |
{ | |
if (ch == '+') | |
{ | |
game.next_speed++; | |
} | |
else if (game.next_speed > 1) | |
{ | |
game.next_speed--; | |
} | |
delwin(score_board); | |
score_board = newpad(SCORE_BOARD_HEIGHT, SCORE_BOARD_WIDTH); | |
init_score_board(score_board); | |
render_score(score_board, game.score); | |
render_speed(score_board, game.next_speed); | |
wmove(stdscr, LINES - 1, COLS - 1); //get cursor out of the way; | |
DRAW(); | |
} | |
else if (!game.dead) | |
{ | |
add_dir(&game, to_dir(ch)); | |
} | |
} | |
} while (ch != 'q'); | |
ASSERT("endwin", endwin()); | |
end_game(&game, timer); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment