Snake game in C language based on Ncurse graphics library under Linux environment

ncurse

ncurse can provide powerful key response. If you use scanf or getchar to obtain key input, you have to press Enter every time the input is completed. The speed will be very slow, so you must use ncurse. At the same time, it should be noted that ncurse is being abandoned. Even the C++ graphics library QT, which has a much stronger experience than ncurse, is slowly withdrawing from the stage of history and being replaced by an increasingly popular and cheap Android system.

But why do we still use ncurse? Although ncurse is an old thing, it still has certain advantages over the functions that come with the C language. actions.

The following is the most basic framework for using curse: (don’t forget the header files)

At the same time, when compiling a c file with curse, it should be expressed as: gcc xxx.c -lcurses

At the same time, in order to enable curse to recognize the up, down, left, and right keys, a sentence must be added to the main: keypad(stdscr,1); and specify a variable type of int or longer bytes to carry getch(), because in the curses.h source file, up, down, left, and right are recognized as 4-bit integers, so if you use a char with only 1 byte length to undertake, an error will be displayed.

After the above modification, a fixed integer appears after inputting up, down, left, and right:

In order to make the displayed results easier to understand, add a switch case to the code to identify:

#include <curses.h>


int main()
{
initscr();
int key;
keypad(stdscr,1);
\t
while (1) {
key = getch();
switch(key){
case KEY_DOWN:
printw("Down\\
");
break;
case KEY_UP:
                printw("Up\\
");
break;
case KEY_LEFT:
                printw("Left\\
");
break;
case KEY_RIGHT:
                 printw("Right\\
");
break;
}
}

endwin();
return 0;
}

The result at this time is much better than before:

Map planning of Snake game

Map size setting: 20×20

Vertical boundary of the map: ” |

Horizontal boundary of the map: ”

Snake’s body ” [ ]

Eat snake food” ##

Realization of greedy snake body

For the setting of the snake body, define a linked list.

At the same time, in the drawing function of the previous map, before drawing ” “, add a judgment condition, scan to see if the point should print the body of the snake or continue to print blank.

At the same time, a function needs to be written to realize the dynamic addition of linked list snake nodes.

The head of the snake visually corresponds to the tail of the linked list, and the tail of the snake visually corresponds to the head of the linked list; therefore, the movement of the snake is based on the input values of the up, down, left, and right keys to determine where the new node should be placed at the end of the linked list Up, down, left, and right, at the same time, delete the head of the linked list, making the new head of the linked list the second element of the original linked list.

After the snake eats food, its body becomes longer. It is only necessary to judge whether the head of the snake (the tail of the linked list) overlaps with the food before each movement of the snake. If it overlaps, the snake will continue to move forward, but the head of the linked list will not be deleted< /strong> (that is, it will not reduce the length of the snake’s tail, which is equivalent to adding a section of body after eating food).

After the snake eats a lot of food, in addition to judging whether the snake head touches the boundary and dies, it is also necessary to judge whether the snake head (tail of the linked list) has touched the body before each snake moves. The method is to start from the head of the linked list (snake tail) Traversing backwards to see if there is an element in the linked list that is not at the end of the linked list at the same position as the end of the linked list. Both of these death methods will trigger a restart of the game.

The snake can move up, down, left, and right, but cannot move left and right or up and down all the time, so the macro definition defines that the up and down of the snake is the opposite number with the same absolute value, and the left and right are the same. After receiving the keyboard signal, do one step first Determine whether the absolute value of the direction is the same as the current direction, and ignore the command if they are the same.

Food realization

Similar to the printing of the snake’s body, in the drawing function of the previous map, before drawing ” “, Add a judgment condition, scan to see if the point should print food or snake body or continue to print blank .

The appearance position of the food needs to be random. If you want to get a random integer in the range [0, x], one way is to use the rand() function and the remainder % symbol. In the game, you want to get [0 ,20] Integers in the interval, food coordinates can be expressed as rand()

Multi-threaded operation

In the actual code running, there will be such a situation: requires a function to continuously refresh the interface, and another function needs to continuously receive the keyboard’s up, down, left, and right signals, and these two functions have while(1), so Under normal circumstances, if any function is called in front, the latter function cannot be run. The solution is to use the Linux thread concept and define two functions as two threads, so that they can run almost simultaneously. For threads, you need to pay attention to the following points:

Add thread library: #include

Threads are defined in the main function: pthread_t th1; and pthread_t th2;

Create threads in the main function: pthread_create( & amp;th1, NULL, function name 1, NULL); and pthread_create( & amp;th2, NULL, function name 2, NULL);

For both functions that need to be threads, the function type needs to be void *

In the final compilation, in addition to adding -lncurses, also add -lpthread

Final code implementation and full comments:

#include <curses.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h> //Add related libraries

#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2 //Macro defines up and down, left and right, the absolute value of up and down is the same, and the absolute value of left and right is the same


struct Snake
{
        int hang;
        int lie;
        struct Snake *next; //Snake structure linked list
};

struct Snake *head = NULL;
struct Snake *tail = NULL;
int dir;

struct Snake food; //Define the global variable "snake head", "snake tail" pointer, "direction" and "snake food", because these variables need to be called in multiple functions

void initFood() //Initialize food so that food is randomly generated in the map
{
int x = rand() ;
int y = rand() ;
food. hang = x;
food.lie = y;
}

void addNode() //Function to add snake body node
{
    struct Snake *new = (struct Snake *)malloc(sizeof(struct Snake)); //For a new pointer variable, if it is not assigned a value at the beginning, it should first open up space for it to prevent it from becoming a wild pointer Segmentation fault occurred

new->next = NULL; //Because it is a linked list, the structure also contains pointer members, so in order for the next pointer in the structure not to become a wild pointer, it is also set to NULL first
switch(dir){ //Judging where the added snake body node is located at the snake head (end of the linked list) according to the up, down, left, and right obtained after receiving
case UP:
new -> hang = tail -> hang - 1;
 new -> lie = tail -> lie;
break;
case DOWN:
            new -> hang = tail -> hang + 1;
            new -> lie = tail -> lie;
            break;
case LEFT:
            new -> hang = tail -> hang;
            new -> lie = tail -> lie - 1;
            break;
case RIGHT:
            new -> hang = tail -> hang;
            new -> lie = tail -> lie + 1;
            break;
}

        tail->next = new; // After setting the row and column values of the new node, make the original tail point to new
        tail = new; //let new be the new tail
}

void deleNode() //delete snake body node function
{
struct Snake *p;
p = head;
head = head->next; //To delete a node, only this sentence is enough, but in order to prevent the memory from being continuously occupied, the address space corresponding to the original head needs to be freed
free(p);
}

void initSnake() //function to initialize the snake
{
struct Snake *p;

dir = RIGHT; //Initialization direction is right

while(head!=NULL){ //Traverse whether there are unused pointers (snake nodes), if there are, free them to prevent wild pointers
p = head;
head = head->next;
free(p);
}

initFood(); //Initialize food

head = (struct Snake *)malloc(sizeof(struct Snake)); //For a new pointer variable, if it has not been assigned a value at the beginning, it should first open up space for it to prevent it from becoming a wild pointer and sending a segment error
head -> hang = 2;
head -> lie = 2;
head -> next = NULL; //Set the position of the snake during initialization and set the corresponding next pointer to NULL to prevent wild pointers

tail = head; //The snake has only one section at this time, so tail is equal to head, and tail must be assigned a value at this time, because the subsequent function will call tail and modify it, if tail is a null pointer, an error will be reported
addNode();
addNode();
addNode(); //Add three nodes in a row, since the initialization direction is RIGHT, therefore, the initialized snake is a 4-section body in the horizontal direction to the right
}


int hasSnakeNode(int i, int j) //Function to determine whether it is a snake body node according to the input row and column values
{
struct Snake *p;
p = head;
while(p != NULL){
if(p->hang == i & amp; & amp; p->lie == j){
            return 1;
       }
p = p->next; //traverse the whole body of the snake
}

return 0;
}

int hasFood(int i, int j) //Function to determine whether it is food according to the input row and column values
{
if(food.hang == i & amp; & amp; food.lie == j){
return 1;
}else{
return 0;
}
}

int ifSnakeDie() //Function to determine whether the snake is dead
{
struct Snake *p;
p = head;

if(tail->hang < 0 || tail->lie == 0 || tail->hang == 20 || tail->lie ==20){ //If the snake touches the boundary, it dies
return 1;
}

while(p->next != NULL){ //If the snake head (tail of the linked list) touches the snake body (any non-tail node in the linked list), it will die
if(p->hang == tail->hang & amp; & amp; p->lie == tail->lie){
return 1;
}
p=p->next;
}
return 0;
}

void moveSnake() //function to control snake movement
{
addNode(); //First add a node at the end of the linked list (snake head) according to the previously encapsulated logic (according to keyboard input)
if(hasFood(tail->hang,tail->lie)){ //Judge whether the snake head (tail of the linked list) has eaten food
initFood(); //Reinitialize the food when you eat food, without deleting the node, that is, the snake adds a body
}else{
deleNode(); //Delete the node if it is not eaten, keep the original length
}

if(ifSnakeDie()){ // After moving, determine whether the snake has triggered any death conditions
initSnake(); //Initialize the snake when it is triggered, and continue if it is not triggered
}
}



void initNcurse() //function to initialize Ncurse
{
initscr();
keypad(stdscr,1);

noecho(); //The ncurse library has its own functions to prevent garbled characters, but this sentence cannot completely eliminate the garbled characters, which should be a common problem of ncurse
}

void gamemap() //Function for drawing game images
{
int hang;
int lie;

move(0,0); //The ncurse library has its own function. Every time the function is called, the cursor is moved to the top, so that the image can be continuously covered by calling the function continuously to achieve the effect of animation

for(hang = 0; hang < 20; hang ++ ){
if(hang == 0){ //drawing of the upper boundary
for(lie = 0; lie<20; lie ++ ){
printw("--");
}
printw("\\
");
}
if(hang>=0 & amp; & amp; hang <=19){ //The drawing of the middle part of the game
for(lie = 0; lie<=20; lie ++ ){
if(lie == 0 || lie == 20){ //drawing the left and right borders
printw("|");
}else if (hasSnakeNode(hang,lie)){ //Judge whether the scanned point belongs to the body of the snake
printw("[]");
}else if(hasFood(hang,lie)){ //Judge whether the scanned point belongs to food
printw("##");
}else{ //If it is neither snake body nor food, print blank as the basic background of the game
printw(" ");
}
}
printw("\\
");
}
if(hang == 19){ //drawing of the lower boundary
for(lie = 0; lie<20; lie ++ ){
                                printw("--");
                        }
                        printw("\\
");
printw("--by mjm\\
");
}

}
}

void *refreshJiemian() //The first function as a thread, the function is to constantly refresh the interface
{
        while(1){
            moveSnake(); //Continuously make the snake take the next step according to the input
            gamemap(); //Continuously cover and draw the interface of the game
refresh(); //ncurse library comes with functions to refresh the interface
usleep(50000); //Control the refresh time, and control the speed of snake movement in disguise
        }
}

void turn(int direction) //determine the function of "providing the current direction of the snake to the code"
{
if(abs(dir) != abs(direction)){ // only change the current direction if the absolute value of the received direction is different from the absolute value of the current direction
dir = direction;
}
}

void *changDir() //The second function as a thread, the function is to continuously accept keyboard up, down, left, and right inputs
{
int key = 0;
        while(1){
                key = getch(); //Use int variable to receive keyboard input
                switch(key){
                        case KEY_DOWN: //If the input is down
                                turn(DOWN); //Then hand over to the turn function to decide whether to provide this direction to the code
                                break;
                        case KEY_UP: //If the input is up
                                turn(UP); //It is handed over to the turn function to decide whether to provide this direction to the code
                                break;
                        case KEY_LEFT: //If the input is left
                                turn(LEFT); //Then hand over to the turn function to decide whether to provide this direction to the code
                                break;
                        case KEY_RIGHT: //If the input is right
                                turn(RIGHT); // then hand over to the turn function to decide whether to provide this direction to the code
                                break;
                }
        }
}

int main() //main function
{
pthread_t th1;//Define two threads
pthread_t th2;

initNcurse(); //Initialize Ncurse

initSnake(); //Initialize the snake
gamemap(); //draw interface

pthread_create( & amp;th1,NULL,refreshJiemian,NULL); //Create two concurrently running threads
pthread_create( &th2,NULL,changDir,NULL);

while(1); //Prevent the main function from exiting
getch();
endwin();
return 0;
}

The final effect