Project 10: Terminal Snake Game

Project 10: Terminal Snake Game

Create a classic snake game that runs in the terminal.

Learning Objectives

  • Terminal control (ANSI escape codes)
  • Asynchronous keyboard input handling
  • Game loop implementation
  • Timer and frame management

Prerequisites

  • Structures and pointers
  • Dynamic memory management
  • Linked lists (for snake body representation)

Step 1: Understanding ANSI Escape Codes

We use ANSI escape codes to display graphics in the terminal.

Basic ANSI Codes

// ansi_demo.c
#include <stdio.h>
#include <unistd.h>

// ANSI Escape Codes
#define CLEAR_SCREEN "\033[2J"
#define CURSOR_HOME "\033[H"
#define HIDE_CURSOR "\033[?25l"
#define SHOW_CURSOR "\033[?25h"

// Cursor movement: \033[row;colH
#define MOVE_CURSOR(row, col) printf("\033[%d;%dH", row, col)

// Colors
#define COLOR_RESET "\033[0m"
#define COLOR_RED "\033[31m"
#define COLOR_GREEN "\033[32m"
#define COLOR_YELLOW "\033[33m"
#define COLOR_BLUE "\033[34m"
#define COLOR_MAGENTA "\033[35m"
#define COLOR_CYAN "\033[36m"

int main(void) {
    // Clear screen
    printf(CLEAR_SCREEN);
    printf(CURSOR_HOME);

    // Hide cursor
    printf(HIDE_CURSOR);

    // Output at various positions
    MOVE_CURSOR(5, 10);
    printf(COLOR_RED "Red Text" COLOR_RESET);

    MOVE_CURSOR(7, 10);
    printf(COLOR_GREEN "Green Text" COLOR_RESET);

    MOVE_CURSOR(9, 10);
    printf(COLOR_BLUE "Blue Text" COLOR_RESET);

    // Draw box
    MOVE_CURSOR(12, 5);
    printf("┌────────────────────┐");
    for (int i = 13; i < 18; i++) {
        MOVE_CURSOR(i, 5);
        printf("│                    │");
    }
    MOVE_CURSOR(18, 5);
    printf("└────────────────────┘");

    MOVE_CURSOR(15, 10);
    printf(COLOR_YELLOW "Text in Box" COLOR_RESET);

    sleep(3);

    // Show cursor
    printf(SHOW_CURSOR);
    MOVE_CURSOR(20, 1);

    return 0;
}

Step 2: Asynchronous Keyboard Input

In games, execution must continue without waiting for key input.

Input Handling with termios

// input_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

// Store original terminal settings
static struct termios original_termios;

// Set terminal to raw mode
void enable_raw_mode(void) {
    tcgetattr(STDIN_FILENO, &original_termios);

    struct termios raw = original_termios;

    // Input flags: disable echo, disable line buffering
    raw.c_lflag &= ~(ECHO | ICANON);

    // Minimum input characters: 0 (non-blocking possible)
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 0;  // No timeout

    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

// Restore terminal settings
void disable_raw_mode(void) {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios);
}

// Check for key input (non-blocking)
int kbhit(void) {
    int ch = getchar();
    if (ch != EOF) {
        ungetc(ch, stdin);
        return 1;
    }
    return 0;
}

// Read key
int getch(void) {
    return getchar();
}

// Read arrow keys (handle escape sequence)
typedef enum {
    KEY_NONE = 0,
    KEY_UP,
    KEY_DOWN,
    KEY_LEFT,
    KEY_RIGHT,
    KEY_QUIT,
    KEY_OTHER
} KeyCode;

KeyCode read_key(void) {
    int ch = getchar();

    if (ch == EOF) return KEY_NONE;
    if (ch == 'q' || ch == 'Q') return KEY_QUIT;

    // Escape sequence (arrow keys)
    if (ch == '\033') {
        int ch2 = getchar();
        if (ch2 == '[') {
            int ch3 = getchar();
            switch (ch3) {
                case 'A': return KEY_UP;
                case 'B': return KEY_DOWN;
                case 'C': return KEY_RIGHT;
                case 'D': return KEY_LEFT;
            }
        }
    }

    // WASD keys support
    switch (ch) {
        case 'w': case 'W': return KEY_UP;
        case 's': case 'S': return KEY_DOWN;
        case 'a': case 'A': return KEY_LEFT;
        case 'd': case 'D': return KEY_RIGHT;
    }

    return KEY_OTHER;
}

int main(void) {
    enable_raw_mode();
    atexit(disable_raw_mode);  // Auto restore on exit

    printf("\033[2J\033[H");  // Clear screen
    printf("Move with arrow keys or WASD, Q to quit\n\n");

    int x = 10, y = 5;

    while (1) {
        KeyCode key = read_key();

        if (key == KEY_QUIT) break;

        // Erase previous position
        printf("\033[%d;%dH ", y, x);

        switch (key) {
            case KEY_UP:    if (y > 3) y--; break;
            case KEY_DOWN:  if (y < 20) y++; break;
            case KEY_LEFT:  if (x > 1) x--; break;
            case KEY_RIGHT: if (x < 40) x++; break;
            default: break;
        }

        // Display at new position
        printf("\033[%d;%dH@", y, x);
        fflush(stdout);

        usleep(50000);  // 50ms delay
    }

    printf("\033[22;1HExiting.\n");
    return 0;
}

Step 3: Basic Game Structure

Game Data Structure Definition

// snake_types.h
#ifndef SNAKE_TYPES_H
#define SNAKE_TYPES_H

#include <stdbool.h>

// Screen size
#define SCREEN_WIDTH 40
#define SCREEN_HEIGHT 20

// Game speed (microseconds)
#define GAME_SPEED 150000

// Direction
typedef enum {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
} Direction;

// Coordinates
typedef struct {
    int x;
    int y;
} Point;

// Snake body node
typedef struct SnakeNode {
    Point pos;
    struct SnakeNode* next;
} SnakeNode;

// Snake
typedef struct {
    SnakeNode* head;
    SnakeNode* tail;
    Direction dir;
    int length;
} Snake;

// Game state
typedef struct {
    Snake snake;
    Point food;
    int score;
    bool game_over;
    bool paused;
} GameState;

#endif

Snake Management Functions

// snake.c
#include <stdio.h>
#include <stdlib.h>
#include "snake_types.h"

// Create snake
Snake* snake_create(int start_x, int start_y) {
    Snake* snake = malloc(sizeof(Snake));
    if (!snake) return NULL;

    // Initial body of 3 segments
    snake->head = NULL;
    snake->tail = NULL;
    snake->length = 0;
    snake->dir = DIR_RIGHT;

    // Add from head to tail
    for (int i = 0; i < 3; i++) {
        SnakeNode* node = malloc(sizeof(SnakeNode));
        node->pos.x = start_x - i;
        node->pos.y = start_y;
        node->next = NULL;

        if (snake->head == NULL) {
            snake->head = node;
            snake->tail = node;
        } else {
            snake->tail->next = node;
            snake->tail = node;
        }
        snake->length++;
    }

    return snake;
}

// Free snake
void snake_destroy(Snake* snake) {
    SnakeNode* current = snake->head;
    while (current) {
        SnakeNode* next = current->next;
        free(current);
        current = next;
    }
    free(snake);
}

// Change direction (prevent opposite direction)
void snake_change_direction(Snake* snake, Direction new_dir) {
    // Can't go in opposite direction
    if ((snake->dir == DIR_UP && new_dir == DIR_DOWN) ||
        (snake->dir == DIR_DOWN && new_dir == DIR_UP) ||
        (snake->dir == DIR_LEFT && new_dir == DIR_RIGHT) ||
        (snake->dir == DIR_RIGHT && new_dir == DIR_LEFT)) {
        return;
    }
    snake->dir = new_dir;
}

// Calculate next head position
Point snake_next_head(Snake* snake) {
    Point next = snake->head->pos;

    switch (snake->dir) {
        case DIR_UP:    next.y--; break;
        case DIR_DOWN:  next.y++; break;
        case DIR_LEFT:  next.x--; break;
        case DIR_RIGHT: next.x++; break;
    }

    return next;
}

// Move snake (returns true if food eaten)
bool snake_move(Snake* snake, Point food) {
    Point next = snake_next_head(snake);

    // Create new head node
    SnakeNode* new_head = malloc(sizeof(SnakeNode));
    new_head->pos = next;
    new_head->next = snake->head;
    snake->head = new_head;
    snake->length++;

    // Check if food eaten
    if (next.x == food.x && next.y == food.y) {
        return true;  // Keep tail (grow)
    }

    // Remove tail if no food eaten
    SnakeNode* current = snake->head;
    while (current->next != snake->tail) {
        current = current->next;
    }
    free(snake->tail);
    snake->tail = current;
    snake->tail->next = NULL;
    snake->length--;

    return false;
}

// Collision check: wall
bool snake_hit_wall(Snake* snake, int width, int height) {
    int x = snake->head->pos.x;
    int y = snake->head->pos.y;
    return (x < 1 || x >= width - 1 || y < 1 || y >= height - 1);
}

// Collision check: self
bool snake_hit_self(Snake* snake) {
    SnakeNode* head = snake->head;
    SnakeNode* current = head->next;

    while (current) {
        if (head->pos.x == current->pos.x &&
            head->pos.y == current->pos.y) {
            return true;
        }
        current = current->next;
    }
    return false;
}

// Check if snake occupies position
bool snake_occupies(Snake* snake, int x, int y) {
    SnakeNode* current = snake->head;
    while (current) {
        if (current->pos.x == x && current->pos.y == y) {
            return true;
        }
        current = current->next;
    }
    return false;
}

Step 4: Complete Snake Game

Main Game Code

// snake_game.c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>

// ============ Config ============
#define WIDTH 40
#define HEIGHT 20
#define INITIAL_SPEED 150000  // microseconds

// ============ ANSI Codes ============
#define CLEAR "\033[2J"
#define HOME "\033[H"
#define HIDE_CURSOR "\033[?25l"
#define SHOW_CURSOR "\033[?25h"
#define MOVE(r,c) printf("\033[%d;%dH", r, c)

#define RESET "\033[0m"
#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define RED "\033[31m"
#define CYAN "\033[36m"
#define BOLD "\033[1m"

// ============ Direction ============
typedef enum { UP, DOWN, LEFT, RIGHT } Direction;

// ============ Coordinates ============
typedef struct {
    int x, y;
} Point;

// ============ Snake Node ============
typedef struct Node {
    Point pos;
    struct Node* next;
} Node;

// ============ Game State ============
typedef struct {
    Node* head;
    Node* tail;
    Direction dir;
    Point food;
    int score;
    int length;
    bool game_over;
    int speed;
} Game;

// ============ Terminal Setup ============
static struct termios orig_termios;

void disable_raw_mode(void) {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
    printf(SHOW_CURSOR);
}

void enable_raw_mode(void) {
    tcgetattr(STDIN_FILENO, &orig_termios);
    atexit(disable_raw_mode);

    struct termios raw = orig_termios;
    raw.c_lflag &= ~(ECHO | ICANON);
    raw.c_cc[VMIN] = 0;
    raw.c_cc[VTIME] = 0;

    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
    printf(HIDE_CURSOR);
}

// ============ Input Handler ============
Direction read_direction(Direction current) {
    int ch = getchar();
    if (ch == EOF) return current;

    // ESC sequence (arrow keys)
    if (ch == '\033') {
        getchar();  // '['
        switch (getchar()) {
            case 'A': return (current != DOWN) ? UP : current;
            case 'B': return (current != UP) ? DOWN : current;
            case 'C': return (current != LEFT) ? RIGHT : current;
            case 'D': return (current != RIGHT) ? LEFT : current;
        }
    }

    // WASD
    switch (ch) {
        case 'w': case 'W': return (current != DOWN) ? UP : current;
        case 's': case 'S': return (current != UP) ? DOWN : current;
        case 'a': case 'A': return (current != RIGHT) ? LEFT : current;
        case 'd': case 'D': return (current != LEFT) ? RIGHT : current;
        case 'q': case 'Q': return -1;  // Quit signal
    }

    return current;
}

// ============ Snake Functions ============
bool snake_at(Node* head, int x, int y) {
    for (Node* n = head; n; n = n->next) {
        if (n->pos.x == x && n->pos.y == y) return true;
    }
    return false;
}

void spawn_food(Game* g) {
    do {
        g->food.x = 1 + rand() % (WIDTH - 2);
        g->food.y = 1 + rand() % (HEIGHT - 2);
    } while (snake_at(g->head, g->food.x, g->food.y));
}

Game* game_init(void) {
    Game* g = malloc(sizeof(Game));

    // Initialize snake (length 3)
    g->head = NULL;
    for (int i = 0; i < 3; i++) {
        Node* n = malloc(sizeof(Node));
        n->pos.x = WIDTH / 2 - i;
        n->pos.y = HEIGHT / 2;
        n->next = g->head;
        g->head = n;
        if (i == 0) g->tail = n;
    }

    // Find tail
    Node* curr = g->head;
    while (curr->next) curr = curr->next;
    g->tail = curr;

    g->dir = RIGHT;
    g->score = 0;
    g->length = 3;
    g->game_over = false;
    g->speed = INITIAL_SPEED;

    spawn_food(g);
    return g;
}

void game_free(Game* g) {
    Node* n = g->head;
    while (n) {
        Node* next = n->next;
        free(n);
        n = next;
    }
    free(g);
}

bool game_update(Game* g) {
    // Calculate next head position
    Point next = g->head->pos;
    switch (g->dir) {
        case UP:    next.y--; break;
        case DOWN:  next.y++; break;
        case LEFT:  next.x--; break;
        case RIGHT: next.x++; break;
    }

    // Wall collision
    if (next.x <= 0 || next.x >= WIDTH - 1 ||
        next.y <= 0 || next.y >= HEIGHT - 1) {
        g->game_over = true;
        return false;
    }

    // Self collision
    if (snake_at(g->head, next.x, next.y)) {
        g->game_over = true;
        return false;
    }

    // Add new head
    Node* new_head = malloc(sizeof(Node));
    new_head->pos = next;
    new_head->next = g->head;
    g->head = new_head;

    // Check food
    if (next.x == g->food.x && next.y == g->food.y) {
        g->score += 10;
        g->length++;
        spawn_food(g);

        // Increase speed (minimum 50ms)
        if (g->speed > 50000) {
            g->speed -= 5000;
        }
        return true;
    }

    // Remove tail
    Node* curr = g->head;
    while (curr->next && curr->next->next) {
        curr = curr->next;
    }
    free(curr->next);
    curr->next = NULL;
    g->tail = curr;

    return false;
}

// ============ Draw Functions ============
void draw_border(void) {
    // Top
    MOVE(1, 1);
    printf(CYAN "╔");
    for (int i = 1; i < WIDTH - 1; i++) printf("═");
    printf("╗" RESET);

    // Sides
    for (int i = 2; i < HEIGHT; i++) {
        MOVE(i, 1);
        printf(CYAN "║" RESET);
        MOVE(i, WIDTH);
        printf(CYAN "║" RESET);
    }

    // Bottom
    MOVE(HEIGHT, 1);
    printf(CYAN "╚");
    for (int i = 1; i < WIDTH - 1; i++) printf("═");
    printf("╝" RESET);
}

void draw_game(Game* g) {
    printf(CLEAR HOME);

    draw_border();

    // Food
    MOVE(g->food.y + 1, g->food.x + 1);
    printf(RED "●" RESET);

    // Snake
    bool is_head = true;
    for (Node* n = g->head; n; n = n->next) {
        MOVE(n->pos.y + 1, n->pos.x + 1);
        if (is_head) {
            printf(BOLD GREEN "◆" RESET);
            is_head = false;
        } else {
            printf(GREEN "■" RESET);
        }
    }

    // Score
    MOVE(HEIGHT + 1, 1);
    printf(YELLOW "Score: %d  Length: %d" RESET, g->score, g->length);

    MOVE(HEIGHT + 2, 1);
    printf("Controls: ↑↓←→ or WASD, Q: Quit");

    fflush(stdout);
}

void draw_game_over(Game* g) {
    MOVE(HEIGHT / 2, WIDTH / 2 - 5);
    printf(BOLD RED "GAME OVER!" RESET);

    MOVE(HEIGHT / 2 + 1, WIDTH / 2 - 6);
    printf("Final Score: %d", g->score);

    MOVE(HEIGHT / 2 + 2, WIDTH / 2 - 8);
    printf("R: Restart, Q: Quit");

    fflush(stdout);
}

// ============ Main ============
int main(void) {
    srand(time(NULL));
    enable_raw_mode();

    Game* game = game_init();
    draw_game(game);

    while (1) {
        // Handle input
        Direction new_dir = read_direction(game->dir);
        if (new_dir == (Direction)-1) break;  // Q quit
        game->dir = new_dir;

        if (!game->game_over) {
            // Update game
            game_update(game);
            draw_game(game);

            if (game->game_over) {
                draw_game_over(game);
            }
        } else {
            // In game over state, R to restart
            int ch = getchar();
            if (ch == 'r' || ch == 'R') {
                game_free(game);
                game = game_init();
                draw_game(game);
            } else if (ch == 'q' || ch == 'Q') {
                break;
            }
        }

        usleep(game->speed);
    }

    game_free(game);

    MOVE(HEIGHT + 4, 1);
    printf("Exiting game.\n");

    return 0;
}

Compile and Run

gcc -o snake snake_game.c
./snake

Step 5: Feature Extensions

Add Wall Wrap Mode

// Add to configuration
#define WALL_WRAP true  // true to wrap to opposite side

// Modify wall collision in game_update function
bool game_update(Game* g) {
    Point next = g->head->pos;
    switch (g->dir) {
        case UP:    next.y--; break;
        case DOWN:  next.y++; break;
        case LEFT:  next.x--; break;
        case RIGHT: next.x++; break;
    }

#if WALL_WRAP
    // Wall wrap: appear on opposite side
    if (next.x <= 0) next.x = WIDTH - 2;
    else if (next.x >= WIDTH - 1) next.x = 1;

    if (next.y <= 0) next.y = HEIGHT - 2;
    else if (next.y >= HEIGHT - 1) next.y = 1;
#else
    // Wall collision: game over
    if (next.x <= 0 || next.x >= WIDTH - 1 ||
        next.y <= 0 || next.y >= HEIGHT - 1) {
        g->game_over = true;
        return false;
    }
#endif

    // ... rest of code
}

Add Obstacles

// Obstacle structure
#define MAX_OBSTACLES 10

typedef struct {
    Point obstacles[MAX_OBSTACLES];
    int count;
} Obstacles;

// Spawn obstacles
void spawn_obstacles(Game* g, Obstacles* obs, int count) {
    obs->count = 0;

    for (int i = 0; i < count && obs->count < MAX_OBSTACLES; i++) {
        Point p;
        do {
            p.x = 2 + rand() % (WIDTH - 4);
            p.y = 2 + rand() % (HEIGHT - 4);
        } while (snake_at(g->head, p.x, p.y) ||
                 (p.x == g->food.x && p.y == g->food.y));

        obs->obstacles[obs->count++] = p;
    }
}

// Check obstacle collision
bool hit_obstacle(Obstacles* obs, int x, int y) {
    for (int i = 0; i < obs->count; i++) {
        if (obs->obstacles[i].x == x && obs->obstacles[i].y == y) {
            return true;
        }
    }
    return false;
}

// Draw obstacles
void draw_obstacles(Obstacles* obs) {
    for (int i = 0; i < obs->count; i++) {
        MOVE(obs->obstacles[i].y + 1, obs->obstacles[i].x + 1);
        printf("\033[35m█\033[0m");  // Magenta color
    }
}

Level System

typedef struct {
    int level;
    int food_to_next;  // Food needed for next level
    int food_eaten;
} LevelSystem;

void level_init(LevelSystem* ls) {
    ls->level = 1;
    ls->food_to_next = 5;
    ls->food_eaten = 0;
}

bool level_eat_food(LevelSystem* ls) {
    ls->food_eaten++;

    if (ls->food_eaten >= ls->food_to_next) {
        ls->level++;
        ls->food_eaten = 0;
        ls->food_to_next += 2;  // More food needed each level
        return true;  // Level up!
    }
    return false;
}

// Calculate speed for level
int get_speed_for_level(int level) {
    int base_speed = 150000;
    int speed = base_speed - (level - 1) * 15000;
    return (speed < 50000) ? 50000 : speed;
}

Score Saving (High Score)

#include <stdio.h>

#define SCORE_FILE "snake_highscore.dat"

int load_highscore(void) {
    FILE* f = fopen(SCORE_FILE, "r");
    if (!f) return 0;

    int score;
    if (fscanf(f, "%d", &score) != 1) {
        score = 0;
    }
    fclose(f);
    return score;
}

void save_highscore(int score) {
    int current_high = load_highscore();
    if (score > current_high) {
        FILE* f = fopen(SCORE_FILE, "w");
        if (f) {
            fprintf(f, "%d", score);
            fclose(f);
        }
    }
}

// Call on game end
void game_end(Game* g) {
    int highscore = load_highscore();

    if (g->score > highscore) {
        MOVE(HEIGHT / 2 + 3, WIDTH / 2 - 8);
        printf("\033[33m★ New Record! ★\033[0m");
        save_highscore(g->score);
    } else {
        MOVE(HEIGHT / 2 + 3, WIDTH / 2 - 8);
        printf("High Score: %d", highscore);
    }
}

Step 6: ncurses Version (Optional)

Using the ncurses library enables cleaner code.

Install ncurses

# macOS
brew install ncurses

# Ubuntu/Debian
sudo apt install libncurses5-dev

# Fedora
sudo dnf install ncurses-devel

ncurses Version Basic Structure

// snake_ncurses.c
#include <ncurses.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

#define WIDTH 40
#define HEIGHT 20

// Direction
enum { UP, DOWN, LEFT, RIGHT };

// Node
typedef struct Node {
    int x, y;
    struct Node* next;
} Node;

// Global state
Node* head = NULL;
int dir = RIGHT;
int food_x, food_y;
int score = 0;
bool game_over = false;

void spawn_food(void) {
    do {
        food_x = 1 + rand() % (WIDTH - 2);
        food_y = 1 + rand() % (HEIGHT - 2);
    } while (/* check snake position */ 0);
}

void init_game(void) {
    // Initialize ncurses
    initscr();
    cbreak();
    noecho();
    nodelay(stdscr, TRUE);  // non-blocking input
    keypad(stdscr, TRUE);   // enable arrow keys
    curs_set(0);            // hide cursor

    // Initialize colors
    if (has_colors()) {
        start_color();
        init_pair(1, COLOR_GREEN, COLOR_BLACK);  // snake
        init_pair(2, COLOR_RED, COLOR_BLACK);    // food
        init_pair(3, COLOR_CYAN, COLOR_BLACK);   // wall
    }

    // Initialize snake
    for (int i = 0; i < 3; i++) {
        Node* n = malloc(sizeof(Node));
        n->x = WIDTH / 2 - i;
        n->y = HEIGHT / 2;
        n->next = head;
        head = n;
    }

    srand(time(NULL));
    spawn_food();
}

void draw(void) {
    clear();

    // Wall
    attron(COLOR_PAIR(3));
    for (int i = 0; i < WIDTH; i++) {
        mvaddch(0, i, ACS_HLINE);
        mvaddch(HEIGHT - 1, i, ACS_HLINE);
    }
    for (int i = 0; i < HEIGHT; i++) {
        mvaddch(i, 0, ACS_VLINE);
        mvaddch(i, WIDTH - 1, ACS_VLINE);
    }
    mvaddch(0, 0, ACS_ULCORNER);
    mvaddch(0, WIDTH - 1, ACS_URCORNER);
    mvaddch(HEIGHT - 1, 0, ACS_LLCORNER);
    mvaddch(HEIGHT - 1, WIDTH - 1, ACS_LRCORNER);
    attroff(COLOR_PAIR(3));

    // Food
    attron(COLOR_PAIR(2));
    mvaddch(food_y, food_x, 'O');
    attroff(COLOR_PAIR(2));

    // Snake
    attron(COLOR_PAIR(1));
    for (Node* n = head; n; n = n->next) {
        mvaddch(n->y, n->x, n == head ? '@' : '#');
    }
    attroff(COLOR_PAIR(1));

    // Score
    mvprintw(HEIGHT + 1, 0, "Score: %d", score);

    refresh();
}

void input(void) {
    int ch = getch();
    switch (ch) {
        case KEY_UP:    if (dir != DOWN) dir = UP; break;
        case KEY_DOWN:  if (dir != UP) dir = DOWN; break;
        case KEY_LEFT:  if (dir != RIGHT) dir = LEFT; break;
        case KEY_RIGHT: if (dir != LEFT) dir = RIGHT; break;
        case 'q': game_over = true; break;
    }
}

void update(void) {
    // Next position
    int nx = head->x, ny = head->y;
    switch (dir) {
        case UP:    ny--; break;
        case DOWN:  ny++; break;
        case LEFT:  nx--; break;
        case RIGHT: nx++; break;
    }

    // Wall collision
    if (nx <= 0 || nx >= WIDTH - 1 || ny <= 0 || ny >= HEIGHT - 1) {
        game_over = true;
        return;
    }

    // New head
    Node* new_head = malloc(sizeof(Node));
    new_head->x = nx;
    new_head->y = ny;
    new_head->next = head;
    head = new_head;

    // Check food
    if (nx == food_x && ny == food_y) {
        score += 10;
        spawn_food();
    } else {
        // Remove tail
        Node* curr = head;
        while (curr->next && curr->next->next) curr = curr->next;
        free(curr->next);
        curr->next = NULL;
    }
}

void cleanup(void) {
    while (head) {
        Node* next = head->next;
        free(head);
        head = next;
    }
    endwin();
}

int main(void) {
    init_game();

    while (!game_over) {
        input();
        update();
        draw();
        usleep(100000);
    }

    // Game over message
    mvprintw(HEIGHT / 2, WIDTH / 2 - 5, "GAME OVER!");
    mvprintw(HEIGHT / 2 + 1, WIDTH / 2 - 6, "Score: %d", score);
    refresh();
    nodelay(stdscr, FALSE);
    getch();

    cleanup();
    return 0;
}

Compile

# macOS
gcc -o snake_ncurses snake_ncurses.c -lncurses

# Linux
gcc -o snake_ncurses snake_ncurses.c -lncurses

Practice Problems

Exercise 1: Pause Feature

Implement pause functionality when P key is pressed.

Exercise 2: Special Items

Add special items that appear occasionally: - Golden apple: 30 points - Speed down: Temporarily reduces speed - Invisibility: Temporarily allows passing through self

Exercise 3: Two Player Mode

Implement 2-player mode with WASD and arrow keys for each player.

Exercise 4: AI Snake

Add an AI snake that automatically finds food. - Hint: Use BFS or simple heuristics


Key Concepts Summary

Concept Description
ANSI Escape Codes Terminal screen control (cursor, colors)
termios Terminal I/O configuration
Raw mode Immediate input without buffering
Game loop Input → Update → Render cycle
Frame rate Speed control with usleep
ncurses Terminal UI library

Next Steps

After completing the snake game, move on to the next project: - Project 11: Mini Shell - Simple command shell implementation

to navigate between lessons