ASCII snake in C
Introduction
I've got some small/tiny gamedev projects on my pocket that I haven't documented here in my Bearblog account. The first one is an ASCII Snake game only using the curses library. There will be another similar post on making Snake game using Raylib.
#1 Libraries
#include <curses.h>
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#2 Consts, structs and global vars
#define MAX_SCORE 256
#define FRAME_TIME 180000
typedef struct {
int x;
int y;
} Vec2;
int score = 0;
char score_message[32];
bool skip = false;
bool is_running = true;
int screen_width = 25;
int screen_height = 20;
// initialize screen
WINDOW *win;
// snake
Vec2 head = {0, 0};
Vec2 segments[MAX_SCORE + 1];
Vec2 dir = {1, 0};
// berry
Vec2 berry;
#3 Function declaration
void game_over();
void init();
void quit_game();
void restart_game();
bool collide(Vec2 a, Vec2 b);
bool collide_snake_body(Vec2 point);
Vec2 spawn_berry();
void process_input();
void draw_border(int y, int x, int width, int height);
void draw();
void update();
void game_over();
#4 Collision: the collide function
- collide: Checks if two points have the same coordinates.
- collide_snake_body: Checks if a point collides with the snake's body.
bool collide(Vec2 a, Vec2 b)
{
if (a.x == b.x && a.y == b.y) {
return true;
}
else return false;
}
bool collide_snake_body(Vec2 point)
{
for (int i = 0; i < score; i++) {
if (collide(point, segments[i])) {
return true;
}
}
return false;
}
#5 Spawning berries
- Generates a random position for a "berry" on the screen, ensuring it's not on the snake's head or body, and has a 1-pixel padding from the edges
Vec2 spawn_berry()
{
// spawn a new berry with 1 pixel padding from edges and not inside of the snake
Vec2 berry = { 1 + rand() % (screen_width - 2), 1 + rand() % (screen_height - 2) };
while (collide(head, berry) || collide_snake_body(berry)) {
berry.x = 1 + rand() % (screen_width - 2);
berry.y = 1 + rand() % (screen_height - 2);
}
return berry;
}
#6 Game Area: draw border
- Draws a border on the screen using ASCII characters, with specified width and height, using ncurses library functions.
void draw_border(int y, int x, int width, int height)
{
// top row
mvaddch(y, x, ACS_ULCORNER);
mvaddch(y, x + width * 2 + 1, ACS_URCORNER);
for (int i = 1; i < width * 2 + 1; i++) {
mvaddch(y, x + i, ACS_HLINE);
}
// vertical lines
for (int i = 1; i < height + 1; i++) {
mvaddch(y + i, x, ACS_VLINE);
mvaddch(y + i, x + width * 2 + 1, ACS_VLINE);
}
// bottom row
mvaddch(y + height + 1, x, ACS_LLCORNER);
mvaddch(y + height + 1, x + width * 2 + 1, ACS_LRCORNER);
for (int i = 1; i < width * 2 + 1; i++) {
mvaddch(y + height + 1, x + i, ACS_HLINE);
}
}
#7 Quit and Restart
- quit_game: Exits the game cleanly, restoring the terminal state.
- restart_game: Resets game state to initial values.
void quit_game()
{
// exit cleanly from application
endwin();
// clear screen, place cursor on top, and un-hide cursor
printf("\e[1;1H\e[2J");
printf("\e[?25h");
exit(0);
}
void restart_game()
{
head.x = 0;
head.y = 0;
dir.x = 1;
dir.y = 0;
score = 0;
sprintf(score_message, "[ Score: %d ]", score);
is_running = true;
}
#8 Init function
This code initializes the game by:
- Seeding the random number generator
- Setting up the terminal window with ncurses
- Initializing colors and color pairs
- Randomly placing the "berry" on the screen
- Initializing the score message
void init()
{
srand(time(NULL));
// initialize window
win = initscr();
// take player input and hide cursor
keypad(win, true);
noecho();
nodelay(win, true);
curs_set(0);
// initialize color
if (has_colors() == FALSE) {
endwin();
fprintf(stderr, "Your terminal does not support color\n");
exit(1);
}
start_color();
use_default_colors();
init_pair(1, COLOR_BLUE, -1);
init_pair(2, COLOR_GREEN, -1);
init_pair(3, COLOR_YELLOW, -1);
berry.x = rand() % screen_width;
berry.y = rand() % screen_height;
// update score message
sprintf(score_message, "[ Score: %d ]", score);
}
#9 Game Over state
game_over: Displays a "Game Over" message and waits for user input to restart or quit.
void game_over()
{
while (is_running == false) {
process_input();
mvaddstr(screen_height / 2, screen_width - 16, " Game Over ");
mvaddstr(screen_height / 2 + 1, screen_width - 16, "[SPACE] to restart, [ESC] to quit ");
attron(COLOR_PAIR(3));
draw_border(screen_height / 2 - 1, screen_width - 17, 17, 2);
attroff(COLOR_PAIR(3));
usleep(FRAME_TIME);
}
}
#10 Movement - process_input
This code processes user input by:
- Reading a key press from the terminal window
- Updating the snake's direction based on arrow key presses
- Preventing the snake from reversing direction immediately
- Restarting the game when the space bar is pressed
- Quitting the game when the escape key is pressed
void process_input()
{
int pressed = wgetch(win);
if (pressed == KEY_LEFT) {
if (dir.x == 1) {
return;
skip = true;
}
dir.x = -1;
dir.y = 0;
}
if (pressed == KEY_RIGHT) {
if (dir.x == -1) {
return;
skip = true;
}
dir.x = 1;
dir.y = 0;
}
if (pressed == KEY_UP) {
if (dir.y == 1) {
return;
skip = true;
}
dir.x = 0;
dir.y = -1;
}
if (pressed == KEY_DOWN) {
if (dir.y == -1) {
return;
skip = true;
}
dir.x = 0;
dir.y = 1;
}
if (pressed == ' ') {
if (!is_running)
restart_game();
}
if (pressed == '\e') {
is_running = false;
quit_game();
}
}
#11 Update game state and Draw function
update:
- Updates the snake's segments and position
- Checks for collisions with the body or walls
- Handles eating a berry and updates the score
- Waits for a short time to control the game's frame rate
draw:
- Clears the screen and redraws the game elements
- Draws the berry, snake, and border using different colors
- Displays the score message
void update()
{
// update snake segments
for (int i = score; i > 0; i--) {
segments[i] = segments[i - 1];
}
segments[0] = head;
// move snake
head.x += dir.x;
head.y += dir.y;
// collide with body or walls
if (collide_snake_body(head) || head.x < 0 || head.y < 0 \
|| head.x >= screen_width || head.y >= screen_height) {
is_running = false;
game_over();
}
// eating a berry
if (collide(head, berry)) {
if (score < MAX_SCORE) {
score += 1;
sprintf(score_message, "[ Score: %d ]", score);
}
else {
// WIN!
printf("You Win!");
}
berry = spawn_berry();
}
usleep(FRAME_TIME);
}
void draw()
{
erase();
attron(COLOR_PAIR(1));
mvaddch(berry.y+1, berry.x * 2+1, '@');
attroff(COLOR_PAIR(1));
// draw snake
attron(COLOR_PAIR(2));
for (int i = 0; i < score; i++) {
mvaddch(segments[i].y+1, segments[i].x * 2 + 1, ACS_DIAMOND);
}
mvaddch(head.y+1, head.x * 2+1, 'O');
attroff(COLOR_PAIR(2));
attron(COLOR_PAIR(3));
draw_border(0, 0, screen_width, screen_height);
attroff(COLOR_PAIR(3));
mvaddstr(0, screen_width - 5, score_message);
}
#12 Main
This code is the main entry point of the game:
- Processes command-line arguments to set the screen dimensions
- Initializes the game state
int main(int argc, char *argv[])
{
// process user args
if (argc == 1) {}
else if (argc == 3) {
if (!strcmp(argv[1], "-d")) {
if (sscanf(argv[2], "%dx%d", &screen_width, &screen_height) != 2) {
printf("Usage: snake [options]\nOptions:\n -d [width]x[height]"
"\tdefine dimensions of the screen\n\nDefault dimensions are 25x20\n");
exit(1);
}
}
}
else {
printf("Usage: snake [options]\nOptions:\n -d [width]x[height]"
"\tdefine dimensions of the screen\n\nDefault dimensions are 25x20\n");
exit(1);
}
init();
while(true) {
process_input();
if (skip == true) {
skip = false;
continue;
}
// ---------- update ----------
update();
// ------------ draw ------------
draw();
}
quit_game();
return 0;
}