Created
January 15, 2022 14:30
-
-
Save SocketByte/4330574d1b3c4c3b4469729bdee4384a to your computer and use it in GitHub Desktop.
Simple bare-bones C snake implementation for POSIX.
This file contains 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
#include <poll.h> | |
#include <signal.h> | |
#include <stdarg.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <termios.h> | |
#include <time.h> | |
#include <unistd.h> | |
/* Base configuration definitions to avoid magic values */ | |
#define RESOLUTION 40 | |
#define BORDER_FRAME '#' | |
#define BORDER_WIDTH 1 | |
#define GRID_WIDTH ((RESOLUTION / 2) - BORDER_WIDTH) | |
#define GRID_HEIGHT ((RESOLUTION / 2) - BORDER_WIDTH) | |
#define DISPLAY_OFFSET_X 3 | |
#define DISPLAY_OFFSET_Y 2 | |
#define HEAD 'O' | |
#define BODY 'o' | |
#define FOOD '*' | |
#define BOMB '@' | |
#define TIME '$' | |
/* Color definitions */ | |
#define COLOR_NC "0" | |
#define COLOR_BLACK "0;30" | |
#define COLOR_RED "0;31" | |
#define COLOR_GREEN "0;32" | |
#define COLOR_YELLOW "0;33" | |
#define COLOR_BLUE "0;34" | |
#define COLOR_MAGENTA "0;35" | |
#define COLOR_CYAN "0;36" | |
#define COLOR_WHITE "0;37" | |
#define COLOR_LIGHT_GREEN "1;32" | |
#define COLOR_LIGHT_RED "1;31" | |
/* Key ASCII hex codes */ | |
#define KEY_ESC 0x1B | |
#define KEY_W 0x77 | |
#define KEY_A 0x61 | |
#define KEY_S 0x73 | |
#define KEY_D 0x64 | |
/* Global game states */ | |
static int SCORE = 0; | |
static int HIGH_SCORE = 0; | |
/* A simple grid buffer structure that holds pixel character data */ | |
static char GRID[GRID_WIDTH][GRID_HEIGHT]; | |
/* A simple grid buffer structure that holds pixel color data */ | |
static const char* GRID_COLORS[GRID_WIDTH][GRID_HEIGHT]; | |
/* Keyboard polling utilities */ | |
static void | |
set_non_canonical_mode(void); | |
static int | |
poll_key(void); | |
/* Screen buffer and graphics utilities */ | |
static void | |
set_cursor_position(int x, int y); | |
static void | |
set_cursor_state(int state); | |
static void | |
set_color(const char* color); | |
static void | |
set_data_text(const char* text, ...); | |
static void | |
put_char(int x, int y, const char* color, char c); | |
static void | |
put_grid(int x, int y, const char* color, char c); | |
static void | |
clear_grid(void); | |
static void | |
clear_screen(void); | |
static void | |
put_top_border(void); | |
static void | |
put_bottom_border(void); | |
static void | |
put_left_right_border(void); | |
/* Spawners */ | |
static void | |
random_xy(int* x, int* y); | |
static void | |
spawn_food(void); | |
static void | |
spawn_bomb(void); | |
static void | |
spawn_time(void); | |
/* Handlers */ | |
static void | |
handle_interrupt(int sig); | |
static void | |
handle_game(float* time_modifier); | |
/* Turns the terminal into non-canonical (raw) mode */ | |
/* This makes the terminal stop echoing typed characters */ | |
static void | |
set_non_canonical_mode(void) | |
{ | |
struct termios term; | |
tcgetattr(STDIN_FILENO, &term); | |
term.c_lflag &= ~(ICANON | ECHO); | |
tcsetattr(STDIN_FILENO, TCSANOW, &term); | |
} | |
/* A simple utility for polling keys */ | |
/* Returns an ASCII keycode or EOF if none was available */ | |
static int | |
poll_key(void) | |
{ | |
struct pollfd pfds[1]; | |
int available; | |
char key; | |
pfds[0].fd = 0; | |
pfds[0].events = POLLIN; | |
available = poll(pfds, 1, 0); | |
if (available > 0) { | |
read(0, &key, 1); | |
return key; | |
} | |
return EOF; | |
} | |
static void | |
set_cursor_position(int x, int y) | |
{ | |
printf("\033[%d;%dH", y, x); | |
} | |
static void | |
set_cursor_state(int state) | |
{ | |
printf("\033[?25%c", state ? 'h' : 'l'); | |
} | |
static void | |
set_color(const char* color) | |
{ | |
printf("\e[%sm", color); | |
} | |
static void | |
set_data_text(const char* text, ...) | |
{ | |
va_list args; | |
va_start(args, text); | |
set_cursor_position(0, RESOLUTION / 2 + 2); | |
vprintf(text, args); | |
va_end(args); | |
} | |
static void | |
put_char(int x, int y, const char* color, char c) | |
{ | |
set_cursor_position(x, y); | |
set_color(color); | |
printf("%c", c); | |
} | |
static void | |
put_grid(int x, int y, const char* color, char c) | |
{ | |
GRID[x][y] = c; | |
GRID_COLORS[x][y] = color; | |
} | |
static void | |
clear_grid(void) | |
{ | |
for (int i = 0; i < GRID_WIDTH; i++) { | |
for (int j = 0; j < GRID_HEIGHT; j++) { | |
put_grid(i, j, COLOR_BLACK, ' '); | |
} | |
} | |
} | |
static void | |
clear_screen(void) | |
{ | |
printf("\033[2J"); | |
} | |
static void | |
put_top_border(void) | |
{ | |
for (int i = 1; i <= RESOLUTION; i += 2) { | |
put_char(i, 0, COLOR_WHITE, BORDER_FRAME); | |
} | |
} | |
static void | |
put_bottom_border(void) | |
{ | |
for (int i = 1; i <= RESOLUTION + 1; i += 2) { | |
put_char(i, RESOLUTION / 2 + 1, COLOR_WHITE, BORDER_FRAME); | |
} | |
} | |
static void | |
put_left_right_border(void) | |
{ | |
for (int i = 1; i <= RESOLUTION / 2; i++) | |
put_char(1, i, COLOR_WHITE, BORDER_FRAME); | |
for (int i = 1; i <= RESOLUTION / 2; i++) | |
put_char(RESOLUTION + 1, i, COLOR_WHITE, BORDER_FRAME); | |
} | |
static void | |
random_xy(int* x, int* y) | |
{ | |
do { | |
*x = rand() % GRID_WIDTH; | |
*y = rand() % GRID_HEIGHT; | |
} while (GRID[*x][*y] != ' '); | |
} | |
static void | |
spawn_food(void) | |
{ | |
int x, y; | |
random_xy(&x, &y); | |
put_grid(x, y, COLOR_YELLOW, FOOD); | |
} | |
static void | |
spawn_bomb(void) | |
{ | |
int x, y; | |
random_xy(&x, &y); | |
put_grid(x, y, COLOR_RED, BOMB); | |
} | |
static void | |
spawn_time(void) | |
{ | |
int x, y; | |
random_xy(&x, &y); | |
put_grid(x, y, COLOR_BLUE, TIME); | |
} | |
static void | |
handle_interrupt(int sig) | |
{ | |
set_cursor_state(1); | |
clear_screen(); | |
set_color(COLOR_LIGHT_RED); | |
printf("\nGame over!\n"); | |
set_color(COLOR_WHITE); | |
printf("Your score: %d\n", SCORE); | |
printf("Your high score: %d\n\n", HIGH_SCORE); | |
exit(sig); | |
} | |
static void | |
handle_game(float* time_modifier) | |
{ | |
static int x_head = GRID_WIDTH / 2; | |
static int y_head = GRID_HEIGHT / 2; | |
static int x_dir = -1; | |
static int y_dir = 0; | |
static int body_length = 0; | |
static int x_body[GRID_WIDTH * GRID_HEIGHT]; | |
static int y_body[GRID_WIDTH * GRID_HEIGHT]; | |
static int x_tail, y_tail; | |
static int x_food, y_food; | |
static int bomb_body_radius = 1; | |
static int bomb_exists = 0; | |
static int food_amount_on_map = 1; | |
int key = poll_key(); | |
if (key != EOF) { | |
switch (key) { | |
case KEY_ESC: | |
handle_interrupt(0); | |
break; | |
case KEY_W: | |
x_dir = 0; | |
y_dir = -1; | |
break; | |
case KEY_A: | |
x_dir = -1; | |
y_dir = 0; | |
break; | |
case KEY_S: | |
x_dir = 0; | |
y_dir = 1; | |
break; | |
case KEY_D: | |
x_dir = 1; | |
y_dir = 0; | |
break; | |
} | |
} | |
x_head += x_dir; | |
y_head += y_dir; | |
if (x_head < 0) | |
x_head = GRID_WIDTH - 1; | |
else if (x_head >= GRID_WIDTH) | |
x_head = 0; | |
if (y_head < 0) | |
y_head = GRID_HEIGHT - 1; | |
else if (y_head >= GRID_HEIGHT) | |
y_head = 0; | |
if (GRID[x_head][y_head] == BODY) { | |
x_head = GRID_WIDTH / 2; | |
y_head = GRID_HEIGHT / 2; | |
HIGH_SCORE = SCORE > HIGH_SCORE ? SCORE : HIGH_SCORE; | |
SCORE = 0; | |
body_length = 0; | |
bomb_exists = 0; | |
food_amount_on_map = 1; | |
*time_modifier = 1.0; | |
clear_grid(); | |
spawn_food(); | |
return; | |
} | |
/* Set the tail end to the first body position */ | |
x_tail = x_body[0]; | |
y_tail = y_body[0]; | |
/* Move the body */ | |
for (int i = 0; i < body_length; i++) { | |
x_body[i] = x_body[i + 1]; | |
y_body[i] = y_body[i + 1]; | |
} | |
if (GRID[x_head][y_head] == FOOD) { | |
food_amount_on_map--; | |
body_length++; | |
SCORE++; | |
*time_modifier += 0.007f; | |
if (food_amount_on_map == 0) { | |
spawn_food(); | |
food_amount_on_map++; | |
// 20% chance | |
if (rand() % 5 == 0) { | |
spawn_food(); | |
food_amount_on_map++; | |
} | |
} | |
if (body_length > bomb_body_radius * 3) { | |
if (!bomb_exists) { | |
spawn_bomb(); | |
bomb_exists = 1; | |
} | |
bomb_body_radius++; | |
} | |
if (body_length > 10 && rand() % 7 == 0) { | |
spawn_time(); | |
} | |
} | |
if (GRID[x_head][y_head] == BOMB) { | |
for (int i = 0; i < bomb_body_radius; i++) | |
put_grid(x_body[i], y_body[i], COLOR_BLACK, ' '); | |
for (int i = bomb_body_radius; i < body_length; i++) { | |
x_body[i - bomb_body_radius] = x_body[i]; | |
y_body[i - bomb_body_radius] = y_body[i]; | |
} | |
body_length -= bomb_body_radius; | |
bomb_exists = 0; | |
} | |
if (GRID[x_head][y_head] == TIME) { | |
*time_modifier -= 0.03; | |
} | |
/* Set the top body position to head */ | |
x_body[body_length] = x_head; | |
y_body[body_length] = y_head; | |
/* Draw everything */ | |
for (int i = 0; i < body_length; i++) { | |
put_grid(x_body[i], y_body[i], COLOR_GREEN, BODY); | |
} | |
put_grid(x_head, y_head, COLOR_LIGHT_GREEN, HEAD); | |
put_grid(x_tail, y_tail, COLOR_BLACK, ' '); | |
set_data_text("SCORE: %d, %d", SCORE, body_length); | |
set_data_text("\nTIME: x%.2f", *time_modifier); | |
} | |
int | |
main(void) | |
{ | |
static float time_modifier = 1.0; | |
srand(time(NULL)); | |
set_non_canonical_mode(); | |
signal(SIGINT, handle_interrupt); | |
clear_grid(); | |
clear_screen(); | |
spawn_food(); | |
for (;;) { | |
handle_game(&time_modifier); | |
set_cursor_state(0); | |
put_top_border(); | |
put_left_right_border(); | |
for (int x = 0; x < GRID_WIDTH; x++) { | |
for (int y = 0; y < GRID_HEIGHT; y++) { | |
put_char(x * 2 + DISPLAY_OFFSET_X, | |
y + DISPLAY_OFFSET_Y, | |
GRID_COLORS[x][y], | |
GRID[x][y]); | |
} | |
} | |
put_bottom_border(); | |
usleep(120000 / time_modifier); | |
} | |
handle_interrupt(0); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment