Creating a Snake Game in C

CCBeginner
Practice Now

Introduction

In this project, you will learn how to create a simple snake game in C using the ncurses library. This classic game involves controlling a snake to eat food, grow longer, while avoiding collisions with walls and itself. The functionality of the game is broken down into several key components: initialization, game loop, snake movement, collision detection, and so on. By the end of this project, you will have a basic snake game that can be run on a terminal.

👀 Preview

Snake Game

🎯 Tasks

In this project, you will learn:

  • How to implement the game loop for updating the snake's position and handling user input.
  • How to create functions to initialize the game, draw the game window, and display game over messages.
  • How to implement collision detection to check for collisions with walls, the snake's own body, and food.
  • How to develop features such as increasing the snake's length when it eats food.

🏆 Achievements

After completing this project, you will be able to:

  • Use the ncurses library in C to create a terminal-based game.
  • Implement game logic, including updating game state and handling user input.
  • Create and manipulate data structures to represent game objects, such as the snake and food.
  • Implement collision detection to provide game rules and determine when the game should end.

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL c(("`C`")) -.-> c/BasicsGroup(["`Basics`"]) c(("`C`")) -.-> c/ControlFlowGroup(["`Control Flow`"]) c(("`C`")) -.-> c/CompoundTypesGroup(["`Compound Types`"]) c(("`C`")) -.-> c/PointersandMemoryGroup(["`Pointers and Memory`"]) c(("`C`")) -.-> c/FunctionsGroup(["`Functions`"]) c/BasicsGroup -.-> c/comments("`Comments`") c/BasicsGroup -.-> c/variables("`Variables`") c/BasicsGroup -.-> c/data_types("`Data Types`") c/BasicsGroup -.-> c/operators("`Operators`") c/ControlFlowGroup -.-> c/if_else("`If...Else`") c/ControlFlowGroup -.-> c/switch("`Switch`") c/ControlFlowGroup -.-> c/while_loop("`While Loop`") c/ControlFlowGroup -.-> c/for_loop("`For Loop`") c/ControlFlowGroup -.-> c/break_continue("`Break/Continue`") c/CompoundTypesGroup -.-> c/strings("`Strings`") c/PointersandMemoryGroup -.-> c/memory_address("`Memory Address`") c/PointersandMemoryGroup -.-> c/pointers("`Pointers`") c/CompoundTypesGroup -.-> c/structures("`Structures`") c/CompoundTypesGroup -.-> c/enums("`Enums`") c/FunctionsGroup -.-> c/function_parameters("`Function Parameters`") c/FunctionsGroup -.-> c/function_declaration("`Function Declaration`") subgraph Lab Skills c/comments -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/variables -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/data_types -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/operators -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/if_else -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/switch -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/while_loop -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/for_loop -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/break_continue -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/strings -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/memory_address -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/pointers -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/structures -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/enums -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/function_parameters -.-> lab-298831{{"`Creating a Snake Game in C`"}} c/function_declaration -.-> lab-298831{{"`Creating a Snake Game in C`"}} end

Basic Knowledge

In the era of widespread use of teletype machines, teletype machines acted as output terminals connected to central computers via cables. Users had to send a series of specific control commands to the terminal program in order to control the output on the terminal screen. For example, changing the cursor position on the screen, clearing the content of a certain area on the screen, scrolling the screen, switching display modes, underlining text, changing the appearance, color, brightness, and so on of characters. These controls are implemented through a string called an escape sequence. The escape sequences are called so because these continuous bytes start with a 0x1B character, which is the escape character (the character entered by pressing the ESC key). Even now, we can simulate the output effect of teletype terminals from that era by inputting escape sequences into terminal emulation programs. If you want to display a piece of text on the terminal (or terminal emulation program) with colored background, you can input the following escape sequence into your command prompt:

echo "^[[0;31;40mIn Color"

Here, ^ and [ are the so-called escape characters. (Note: in this case, ^[ is one character. It is not entered by typing the ^ and [ characters sequentially. To print this character, you must first press Ctrl+V and then press the ESC key.) After executing the above command, you should see the background of In Color changed to red. From then on, all displayed text will be outputted with this effect. If you want to terminate this effect and return to the original format, you can use the following command:

echo "^[[0;37;40m"

Now do you know the purpose of these characters (escape sequences)? (Try changing the parameters between the semicolons and see what results you get.) Maybe it will be different from what you imagine? It may be because the terminal environment is different, which depends on the different terminals or operating systems. (You can't make a monochrome terminal display colored characters, right?) In order to avoid such compatibility issues and achieve consistent output on different terminals, the designers of UNIX invented a mechanism called termcap. termcap is actually a file that is released together with the escape sequences. This file lists all the escape sequences that the current terminal can execute correctly, ensuring that the execution result of the input escape sequences conforms to the specifications in this file. However, in the years following the invention of this mechanism, another mechanism called terminfo gradually replaced termcap. Since then, users no longer need to consult the complex escape sequence specifications in termcap when programming, and only need to access the terminfo database to control the screen output.

Assuming that all applications access the terminfo database to control output (such as sending control characters, etc.) under the situation of using terminfo, soon these code calls will make the entire program difficult to control and manage. The emergence of these problems led to the birth of CURSES. The name CURSES comes from a pun called cursor optimization.

The CURSES library provides users with a flexible and efficient API (application programming interface) by encapsulating the underlying control codes (escape sequences) of the terminal, which allows users to control the cursor, create windows, change foreground and background colors, and handle mouse operations. This enables users to bypass those annoying low-level mechanisms when writing applications on character terminals.

NCURSES is a clone of CURSES from System V Release 4.0 (SVr4). It is a freely configurable library that is fully compatible with older versions of CURSES. In short, it is a library that allows applications to directly control the display of the terminal screen. When the CURSES library is mentioned later, it also refers to the NCURSES library.

NCURSES not only encapsulates the underlying terminal functions, but also provides a fairly stable working framework to generate beautiful interfaces. It includes functions for creating windows. And its sister libraries, Menu, Panel, and Form, are extensions of the CURSES base library. These libraries are generally distributed together with CURSES. We can build an application that contains multiple windows, menus, panels, and forms. The windows can be managed independently, such as scrolling or hiding them. Menus allow users to create command options for easy execution of commands. Forms allow users to create windows for simple data input and display. Panels are extensions of NCURSES window management functions, and can overlay or stack windows.

Define Constants

First, open the terminal and execute the following command to install the ncurses library:

sudo apt-get update
sudo apt-get install libncurses5-dev

Navigate to the ~/project directory and create the project file snake.c:

cd ~/project
touch snake.c

Next, we need to write the C code. The first step is to include the header files:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <ncurses.h>

Before writing the main() function, let's complete some basic tasks. Since we are using a terminal character interface, ASCII characters are essential. Therefore, we need to define some constants:

#define TBool            int
#define True             1
#define False            0
#define SHAPE_FOOD       '@'  // Food
#define SHAPE_SNAKE      '#'  // Snake body
#define GAMEWIN_YLEN     15
#define GAMEWIN_XLEN     60
#define LOGWIN_YLEN      7
#define LOGWIN_XLEN      (GAMEWIN_XLEN)
#define LOGBUF_NUM       (LOGWIN_YLEN-2)
#define LOGBUF_LEN       (GAMEWIN_XLEN-2)
#define MAXLEVEL         12

#define GetSnakeTail(s)  ((s)->head->front)

WINDOW *logwin; // Declare a log window
#define INITRUNLOG()     logwin = newlogw() // Create a log window by calling the custom function newlogw()
#define RUNLOG(str)      runlog(logwin, str) // Run the log window to display game prompts
#define DESTROYRUNLOG()  delwin(logwin)

int g_level; // Player level, a global variable

We have also added some struct and enum definitions:

enum TDirection {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
};

struct TFood {
    int y;
    int x;
};

struct TSnakeNode {
    int y;
    int x;
    struct TSnakeNode *front;
};

struct TSnake {
    int    length;
    struct TSnakeNode *head;
    enum   TDirection  dir;
};

Now we will declare the functions that we are going to create:

int refreshgamew(WINDOW *win, struct TSnake *psnake);
void movesnake(struct TSnake *psnake);
int checksnake(struct TFood *pfood, struct TSnake *psnake);
void snakegrowup(struct TFood *pfood, struct TSnake *psnake);
void gameover(WINDOW *win, char *str);
struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x);
WINDOW* newgamew();
struct TSnake* initsnake();
void destroysnake(struct TSnake *psnake);
void drawsnakew(WINDOW *win, struct TSnake *psnake);
void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake);
TBool checkfood(struct TFood *pfood, struct TSnake *psnake);
WINDOW* newlogw();
void runlog(WINDOW *win, char *str);
void cleanline(WINDOW *win, int y, int x);

main Function

int main()
{
    initscr();  /* Initialize, enter ncurses mode */
    raw();      /* Disable line buffering, see results immediately */
    noecho();   /* Do not display control characters on the terminal, such as Ctrl+C */
    keypad(stdscr, TRUE);   /* Allow user to use keyboard in terminal */
    curs_set(0);    /* Set cursor visibility, 0 is invisible, 1 is visible, 2 is completely visible */
    refresh();      /* Write the contents of the virtual screen to the display and refresh */

    g_level = 1;

    INITRUNLOG();

    RUNLOG("  Press 'q' or 'Q' to quit.");
    RUNLOG("  Press 'w/s/a/d' or 'W/S/A/D' to move the snake.");
    RUNLOG("Info:");

    WINDOW *gwin = newgamew(); /* Create game window, implemented by a custom function called newgamew */
    struct TSnake *psnake = initsnake();
    drawsnakew(gwin, psnake);

    while (refreshgamew(gwin, psnake) >= 0)
        ;

    /* getch() is different from getchar() */
    getch();

    destroysnake(psnake);
    delwin(gwin);    /* Clear the game window and free the memory and information of the window data structure */
    DESTROYRUNLOG(); /* Clear the information display window */
    endwin();        /* Exit ncurses mode */

    return 0;
}

In keypad(stdscr, TRUE), stdscr refers to a virtual window where all our operations are first written, and then the contents of stdscr are displayed on the screen using the refresh function.

When we use printw, the data is actually printed on a virtual window called stdscr, which is not directly output to the screen. The purpose of the printw() function is to continuously write some display markers and related data structures on the virtual display and write these data into the buffer of stdscr. So, in order to display the data in this buffer, we must use the refresh() function to tell the curses system to output the contents of the buffer to the screen. This mechanism allows the programmer to continuously write data on the virtual screen and make it look like it's done all at once when refresh() is called. Because the refresh() function only checks the parts of the window and data that have changed, this flexible design provides an efficient feedback mechanism.

Window Mechanism

The window mechanism is a core concept of CURSES. As you have seen from the previous examples, all functions operate by default on a "window" (stdscr). Even if you are designing the simplest graphical user interface (GUI), you still need to use windows. One of the main reasons for using windows is that you can divide the screen into different parts and operate within them simultaneously. This can improve efficiency. Another reason is that you should always strive for a better and more manageable design in your program. If you are designing a large and complex user interface, pre-designing these parts will improve your efficiency.

WINDOW* newlogw()
{
    /* Parameters are: height, width, starting position (y,x) of the window */
    WINDOW *win = newwin(LOGWIN_YLEN, LOGWIN_XLEN, GAMEWIN_YLEN + 2, 3);

    /* Parameters are: known window pointer, 0 and 0 are the default row and column starting positions of the characters */
    box(win, 0, 0);

    mvwprintw(win, 0, 2, " LOG ");
    wrefresh(win); // Refresh the specified window

    return win;
}

In the WINDOW* newlogw() function, you can see that the creation of a window starts with newwin(). Although we have created a window, it is not visible yet, which is similar to a <div> element in HTML. If you don't add styles to a <div> element, you won't see anything on the webpage. So we need to use the box function to add borders to the known window.

mvwprintw prints the specified content at the specified coordinates (y,x) within the specified window.

Display Game Information

RUNLOG is a macro that calls the custom function runlog.

void runlog(WINDOW *win, char *str)
{
    static char logbuf[LOGBUF_NUM][LOGBUF_LEN] = {0};
    static int  index = 0;

    strcpy(logbuf[index], str);

    int i = 0;

    /* #define LOGBUF_NUM  (LOGWIN_YLEN-2); LOGBUF_NUM=5 */
    for (; i < LOGBUF_NUM; ++i) {

        /* Custom function, cleanline */
        cleanline(win, i+1, 1);

        /* Print string on each line */
        mvwprintw(win, i+1, 1, logbuf[(index+i) % LOGBUF_NUM]);
        wrefresh(win);
    }

    index = (index + LOGBUF_NUM - 1) % LOGBUF_NUM;
}

runlog takes a known window and the information to be displayed as parameters. The str will be stored in a two-dimensional array (logbuf) using the strcpy function. Then, the custom function cleanline is called to clear the line that will be printed next. The mvwprintw function is used to print the information.

/* Clear the coordinates (x,y) of the window win */
void cleanline(WINDOW *win, int y, int x)
{
    char EMPTYLINE[LOGBUF_LEN] = {0}; // LOGBUF_LEN=57

    /* Set positions 0-56 of the array to empty characters */
    memset(EMPTYLINE, ' ', LOGBUF_LEN-1);

    /* Move the cursor to position (y,x) in the window win and print the string EMPTYLINE */
    mvwprintw(win, y, x, EMPTYLINE);

    /* Display content on the specified window */
    wrefresh(win);
}

Initializing the Snake Queue

Now, you will define the data structure of the snake and create functions to initialize it and draw it on the game window.

struct TSnake* initsnake()
{
    struct TSnake *psnake = (struct TSnake *)malloc(sizeof(struct TSnake));

    psnake->dir    = DIR_LEFT;
    psnake->length = 4; // Initialize the length of the snake to 4

    newsnakenode (
        &newsnakenode (
            &newsnakenode (
                &newsnakenode( &psnake->head, 4, 50 )->front, 4, 53
            )->front, 4, 52
        )->front, 4, 51
    )->front = psnake->head;

    return psnake;
}

struct TSnakeNode *newsnakenode(struct TSnakeNode **ppsnode, int y, int x)
{
    *ppsnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));
    (*ppsnode)->y = y;
    (*ppsnode)->x = x;
    (*ppsnode)->front = NULL;

    return *ppsnode;
}

The purpose of these two functions is to initialize the data structure of the snake in a snake game, including setting the initial direction and length of the snake, as well as creating a snake with an initial body node. The function newsnakenode is used to create individual nodes, and then these nodes are connected together through nested function calls to form the initial snake.

Display the Snake on the Game Window

void drawsnakew(WINDOW *win, struct TSnake *psnake)
{
    static int taily = 0;
    static int tailx = 0;
    if (taily != 0 && tailx != 0) {
        mvwaddch(win, taily, tailx, ' ');
    }

    /* #define GetSnakeTail(s)  ((s)->head->front) */
    struct TSnakeNode *psnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i) {
        mvwaddch(win, psnode->y, psnode->x, SHAPE_SNAKE);
        psnode = psnode->front;
    }

    taily = GetSnakeTail(psnake)->y;
    tailx = GetSnakeTail(psnake)->x;

    wrefresh(win);
}

To display each node's (y,x) coordinates, we can use the macro definition GetSnakeTail to retrieve the coordinates. The remaining step is to use a loop to display them using mvwaddch. mvwaddch is used to move the cursor to the specified position (taily,tailx) in the specified window (win) and then output a character.

To actually display it on the screen, we need to use wrefresh(win).

Write the Core of the Game

Take a look at the semicolon after while (refreshgamew(gwin, psnake) >= 0);. This establishes a loop to handle the snake's movement and length change, as well as boundary detection and other issues.

The while loop calls the refreshgamew function:

int refreshgamew(WINDOW *win, struct TSnake *psnake)
{
    static TBool ffood = False;
    struct TFood pfood;
    /* When starting the game or when food is eaten, ffood=False, and drawfoodw is executed to redraw the food */
    if (!ffood) {
        drawfoodw(win, &pfood, psnake);
        ffood = True;
    }
    int key = -1;

    fd_set set;
    FD_ZERO(&set);
    FD_SET(0, &set);

    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec= (6 - (int)(g_level/3)) * 100*1000;

    // Next, we will use the select kernel API, which will listen for a series of keyboard and mouse inputs. We use `key=getch()` and `switch` to determine the input type. The second `switch` is used to display the level and the current movement speed of the snake.
    if (select(1, &set, NULL, NULL, &timeout) < 0)
        return -1;

    if (FD_ISSET(0, &set)) {
        while ((key = getch()) == -1);

        switch (key) {
        case 'w':
        case 'W':
            (psnake->dir == DIR_DOWN) ? : (psnake->dir = DIR_UP);
            break;

        case 's':
        case 'S':
            (psnake->dir == DIR_UP) ? : (psnake->dir = DIR_DOWN);
            break;

        case 'a':
        case 'A':
            (psnake->dir == DIR_RIGHT) ? : (psnake->dir = DIR_LEFT);
            break;

        case 'd':
        case 'D':
            (psnake->dir == DIR_LEFT) ? : (psnake->dir = DIR_RIGHT);
            break;

        case 'q':
        case 'Q':
            RUNLOG("Quit Game!");
            gameover(win, "Quit Game!");
            return -1;

        default:
            break;
        }
    }
    movesnake(psnake);
    drawsnakew(win, psnake);
    switch (checksnake(&pfood, psnake)) {
    case 0:
        break;

    // Food has been eaten, set ffood to 0 to redraw the food.
    case 1:
        ffood = False;
        if (++g_level > MAXLEVEL) {
            RUNLOG("Win!!!");
            gameover(win, "Win!!!");
            return -1;
        }
        mvwprintw(win, GAMEWIN_YLEN-1, 2, " Level: %d ", g_level);
        mvwprintw(win, GAMEWIN_YLEN-1, 30, " Speed: %d ", (int)(g_level/3));
        wrefresh(win);
        RUNLOG("Level UP!");
        snakegrowup(&pfood, psnake);
        break;

    default:
        RUNLOG("Game over!");
        gameover(win, "Game over!");
        return -1;
    }

    return 1;
}

The Movement of the Greedy Snake

The movement of the Greedy Snake is implemented using the function movesnake. We will analyze the case DIR_UP segment, as the rest are similar:

/* The structure TSnake is an inverted linked list with the head and tail connected.
 * Example: [a]<-[b]<-[c]<-[d]    a is the head
 *          |              ^     When the snake moves, only the head points to d,
 *          `--------------'     and the (y,x) of d is modified to the position where the head moves to. */
void movesnake(struct TSnake *psnake)
{
    int hy = psnake->head->y;
    int hx = psnake->head->x;

    psnake->head = GetSnakeTail(psnake);

    switch (psnake->dir) {
    case DIR_UP:
        psnake->head->y = hy - 1;
        psnake->head->x = hx;
        break;

    case DIR_DOWN:
        psnake->head->y = hy + 1;
        psnake->head->x = hx;
        break;

    case DIR_LEFT:
        psnake->head->y = hy;
        psnake->head->x = hx - 1;
        break;

    case DIR_RIGHT:
        psnake->head->y = hy;
        psnake->head->x = hx + 1;
        break;

    default:
        break;
    }
}

Since the coordinate origin is located at the top left corner, with the values of (y,x) increasing downwards and to the right, the coordinate system is flipped 180 degrees along the x-axis. Therefore, in order to make the snake move upward, we subtract 1. Thus, when the player presses the w/W key, the snake will move upwards.

Lengthening the Snake Body

To increase the length of the snake body, you can achieve this by implementing the snakegrowup function, which is relatively straightforward. Simply add a new node to the queue.

void snakegrowup(struct TFood *pfood, struct TSnake *psnake)
{
    // Allocate memory for a new snake node (struct TSnakeNode) and assign the pointer pnode to the new node. This new node will become the new head of the snake.
    struct TSnakeNode *pnode = (struct TSnakeNode *)malloc(sizeof(struct TSnakeNode));

    switch (psnake->dir) {
    case DIR_UP:
        pnode->y = psnake->head->y - 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_DOWN:
        pnode->y = psnake->head->y + 1;
        pnode->x = psnake->head->x;
        break;

    case DIR_LEFT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x - 1;
        break;

    case DIR_RIGHT:
        pnode->y = psnake->head->y;
        pnode->x = psnake->head->x + 1;
        break;

    default:
        break;
    }

    // Set the front pointer of the new node (pnode) to the current snake tail in order to establish a connection between the new head and tail.
    pnode->front = GetSnakeTail(psnake);

    // Set the front pointer of the current snake head to the new node to ensure that the new node becomes the new head.
    psnake->head->front = pnode;
    psnake->head = pnode;

    // Increase the length of the snake to indicate that the snake body has extended by one unit.
    ++psnake->length;
}

The switch statement determines the coordinates of the new head node based on the current movement direction of the snake (psnake->dir). Depending on the different directions, it updates the coordinates of the new node to move one cell up, down, left, or right from the current snake head (psnake->head).

The purpose of this code is to increase the length of the snake body in the game of Snake. It creates a new head node based on the current movement direction of the snake and inserts this new head node at the front of the snake, making the snake's body longer. This is the core logic for increasing the length of the snake's body after it consumes food in the game of Snake.

The Location of Food Production

The drawfoodw function is used to draw food in the game window (win):

void drawfoodw(WINDOW *win, struct TFood *pfood, struct TSnake *psnake)
{
    do {
        pfood->y = random() % (GAMEWIN_YLEN - 2) + 1;
        pfood->x = random() % (GAMEWIN_XLEN - 2) + 1;
    } while (False == checkfood(pfood, psnake));
    checkfood(pfood, psnake);

    mvwaddch(win, pfood->y, pfood->x, SHAPE_FOOD);
    wrefresh(win);
}
  • The random() function is used to generate random coordinate values, which will make the food appear at a random position within the game window.
  • Using a do-while loop, it checks if the generated food position overlaps with any body nodes of the snake. If there is an overlap, it generates a new food position until it finds a position that does not overlap with the snake's body.
  • The mvwaddch function is used to draw the shape of the food at the specified position in the game window.

The checkfood function is used to check that the food does not appear on the snake:

TBool checkfood(struct TFood *pfood, struct TSnake *psnake)
{
    struct TSnakeNode *pnode = GetSnakeTail(psnake);

    int i = 0;
    for (; i < psnake->length; ++i, pnode = pnode->front)
        if (pfood->y == pnode->y && pfood->x == pnode->x)
            return False;

    return True;
}
  • First, it gets the tail node of the snake. Then, it uses a loop to iterate through all the body nodes of the snake and checks if the coordinates of the food match the coordinates of any node.
  • If the coordinates of the food match the coordinates of any snake node, it means that the food's position overlaps with a body node of the snake, and the function returns False.
  • If no overlap is found, the function returns True, indicating that the food position is valid.

Boundary Detection

checksnake is used to check if the snake has collided with the game borders. This includes detection for borders on the top, bottom, left, and right sides of the game. Collisions with the snake's own body are also checked, as both of these situations would result in the game ending.

int checksnake(struct TFood *pfood, struct TSnake *psnake)
{

	/* Check if the coordinates of the snake's head have collided with the game borders on the top, bottom, left, or right */
    if ( psnake->head->y <= 0 || psnake->head->y >= GAMEWIN_YLEN
      || psnake->head->x <= 0 || psnake->head->x >= GAMEWIN_XLEN)
    {
        return -1;
    }

    struct TSnakeNode *pnode = GetSnakeTail(psnake);
    int i = 0;

	/* Check that the snake's head does not collide with any part of its body */
    for (; i < psnake->length - 1; ++i, pnode = pnode->front)
        if (psnake->head->y == pnode->y && psnake->head->x == pnode->x)
            return -1;

  	/* Of course, colliding with food is allowed */
    if (psnake->head->y == pfood->y && psnake->head->x == pfood->x)
        return 1;

    return 0; // No collision occurred
}

Game Over

The gameover function is used to display the game over message on the specified window (win) when the snake game ends:

void gameover(WINDOW *win, char *str)
{
    mvwprintw(win, (int)(GAMEWIN_YLEN/2), (GAMEWIN_XLEN/2 - strlen(str)/2), str);
    mvwprintw(win, (int)(GAMEWIN_YLEN/2 + 1), 20, "Press any key to quit...");
    wrefresh(win);
}
  • Use the mvwprintw function to print the game over message text str in the middle of the window. This ensures that the game over message is displayed centered in the window.
  • Next, print "Press any key to quit..." on the next line in the middle of the window, prompting the player to press any key to exit the game.
  • Finally, use the wrefresh function to refresh the window and ensure that the game over message and exit prompt are correctly drawn on the screen.

To avoid memory leaks, use the destroysnake function to release the memory occupied by the snake game, including the snake nodes and the snake structure itself. This is a common cleanup step when the game ends or restarts.

void destroysnake(struct TSnake *psnake)
{
    struct TSnakeNode *psnode = GetSnakeTail(psnake);
    struct TSnakeNode *ptmp   = NULL;

    int i = 0;
    for (; i < psnake->length; ++i) {
        ptmp   = psnode;
        psnode = psnode->front;
        free(ptmp);
    }

    free(psnake);
    psnake = NULL;
}

Inside the function, it first declares two pointers, psnode and ptmp, for iterating over the snake node linked list.

It enters a loop that iterates the number of times equal to the length (number of nodes) of the snake. In each iteration, it performs the following operations:

  • Sets ptmp to point to the current node psnode in order to free the memory for the current node later.
  • Moves psnode to the next node (pointed to by front).

After the loop ends, all snake nodes have been freed.

Finally, the function frees the memory for the snake structure itself, free(psnake), and sets the pointer psnake pointing to the snake structure to NULL to ensure that the freed memory is no longer used.

Compilation and Execution

The compilation command is slightly different from the usual. It requires adding the -l option to gcc to include the ncurses library:

cd ~/project
gcc -o snake snake.c -l ncurses
./snake
Snake Game

Summary

You have successfully created a simple snake game in C language using the ncurses library. The game includes a game window, snake movement, food generation, and collision detection. By following the above steps, you can run and enjoy the game on the terminal. Have fun playing!

Other C Tutorials you may like