Snake Game

This program was my attempt recreate the Snake game) on the RaspberryPi Pico using Prof. Hunter Adam's VGA Library for the RaspberryPi Pico. The game is controlled using pushbuttons attached to 4 different GPIO pins(one for each direction the snake can move in. The resources for the project include the C SDK User Guide, the RP2040 Datasheet and Prof. Hunter's website.


The complete code

/*
 * Parth Sarthi Sharma (pss242@cornell.edu)
 * Code based on examples from Raspberry Pi Foundation.
 * This code is an implementation of the famous snake game.
 * It uses 4 GPIO pins to control the snake which has to eat the food
 * in order to grow in length.
 */
#include <stdio.h> //The standard C library
#include <math.h> //The standard math library
#include "pico/stdlib.h" //Standard library for Pico
#include "hardware/pio.h" //The hardware PIO library
#include "hardware/dma.h" //The hardware DMA library
#include "pico/time.h" //The pico time library
#include "hardware/gpio.h" //The hardware GPIO library
#include "vga_graphics.h" //The graphics library

#define HEIGHT 480 //Height of the VGA screen
#define WIDTH 640 //Width of the VGA screen

#define BOARD_HEIGHT (HEIGHT / 4) //The height if the board
#define BOARD_WIDTH (WIDTH / 4) //The width of the board

//The directions
#define UP 0 //The UP state
#define DOWN 1 //The DOWN state
#define LEFT 2 //The LEFT state
#define RIGHT 3 //The RIGHT state

//The GPIO pins
#define UP_PIN 3 //The UP GPIO pin
#define DOWN_PIN 2 //The DOWN GPIO pin
#define LEFT_PIN 5 //The LEFT GPIO pin
#define RIGHT_PIN 4 //The RIGHT GPIO pin

//Structure to hold the node (each point) of the snake.
//The snake is constructed using a linked list type data structure.
typedef struct Node{ //Node structure
    short x, y; //The x coordinate and the y coordinate of the node
    struct Node *next; //The next node of the snake
}Node;

Node *head = NULL; //The head of the snake

short food[2]; //The array to hold x-coordinate and the y-coordinate of the food
volatile char direction = RIGHT; //The current direction of the snake
char alive = 1; //The flag that determines if the snake is alive
volatile char reset = 1; //The reset flag

inline int randomRange(int min, int max){ //Function to generate a random number between min and max
    return (rand() % (max - min)) + min;
}

void swap(short *x, short *y){ //Function to swap two shorts
    if(*x != *y){
        *x = *x ^ *y;
        *y = *x ^ *y;
        *x = *x ^ *y;
    }
}

void genFood(){ //The function to generate the food
genAgain:
    food[0] = randomRange(0, BOARD_WIDTH); //Get a random x coordinate
    food[1] = randomRange(0, BOARD_HEIGHT); //Get a random y coordinate
    Node *current = head; //Create a new current node
    current = current->next; //Make the current node point to the next node
    while(current != NULL){ //Iterate to the end of the list
        if(food[0] == current->x && food[1] == current->y){ //If at any point the food lies on the body of the snake
            goto genAgain; //Generate new coordinates for the food
        }
        current = current->next; //Else point to the next node
    }
    fillRect(food[0] << 2, food[1] << 2, 4, 4, YELLOW); //Draw a yellow rectangle for food
}

void addNode(short x, short y){ //Function to add a new node for the snake
    if(head == NULL){ //If the linked list is empty
        head = (Node *)malloc(sizeof(Node)); //Allocate memory for the head
        head->x = x; //Set x-coordinate for the head
        head->y = y; //Set y-coordinate for the head
        head->next = NULL; //Make the next of head as NULL
        fillRect(x << 2, y << 2, 4, 4, RED); //Draw a red rectangle for head
    }
    else{ //Else
        Node *current = head; //Create a new pointer pointing to the head
        while(current->next != NULL){ //Iterate to the last element of the list
            current = current->next;
        }
        current->next = (Node *)malloc(sizeof(Node)); //Allocate new memory for the next node
        current->next->x = x; //Set x-coordinate for the new node
        current->next->y = y; //Set y-coordinate for the new node
        current->next->next = NULL; //Set the next node as NULL
        fillRect(x << 2, y << 2, 4, 4, GREEN); //Draw a green rectangle for the list
    }
}

void move(short x, short y){ //Function to move the snake such that the new coordinates of the head are x, y
    fillRect(x << 2, y << 2, 4, 4, RED); //Draw the new head
    Node *current = head; //Create a new pointer pointing to the head
    while(current != NULL){ //For all the elements of the snake's body
        fillRect(current->x << 2, current->y << 2, 4, 4, GREEN); //Draw the new snake body
        //Swap the coordinates and the current node coordinates
        swap(&(current->x), &x);
        swap(&(current->y), &y);
        current = current->next; //Go over to the next node
        if(current != NULL){ //If the current node is not null
            if(current->x == head->x && current->y == head->y){ //If the snake eats itself
                alive = 0; //Kill it
                fillRect(head->x << 2, head->y << 2, 4, 4, RED); //Draw the head one last time
                setTextColor(WHITE); //Set text colour to white
                setTextSize(4); //Set the text size as 4
                setCursor(100, 200); //Set the cursor
                writeString("Death awaits us all"); //Print the message
                break;
            }
        }
    }
    fillRect(x << 2, y << 2, 4, 4, BLACK); //Clear the oldest element
}

void eatAndMove(short x, short y){ //Function to eat the food and still move by increasing the length of the snake
    fillRect(x << 2, y << 2, 4, 4, RED); //Draw the new head
    Node *current = head; //Create a new pointer pointing to the head
    while(current->next != NULL){ //For all the elements until the second last element of the snake's body
        //Store the old values of x and y coordinates in the temporary placeholders
        fillRect(current->x << 2, current->y << 2, 4, 4, GREEN); //Draw the new snake body
        //Swap the coordinates and the current node coordinates
        swap(&(current->x), &x);
        swap(&(current->y), &y);
        current = current->next; //Go over to the next node
    }
    current->next = (Node *)malloc(sizeof(Node)); //Allocate new memory for the next node
    //Update the new coordinates of the body
    fillRect(current->x << 2, current->y << 2, 4, 4, GREEN); //Draw the new snake body
    //Swap the coordinates and the current node coordinates
    swap(&(current->x), &x);
    swap(&(current->y), &y);
    current = current->next;  //Go over to the next node
    //Update the last element of the body
    current->x = x;
    current->y = y;
    current->next = NULL; //Make the last element point to NULL
}

void moveUp(){ //The function to move UP
    if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
        if(head->y == 0){ //If the head is on the top edge of the screen
            eatAndMove(head->x, BOARD_HEIGHT - 1); //Eat the food and move to the bottom edge of the screen
        }
        else{ //Else
            eatAndMove(head->x, head->y - 1); //Eat the food and move UP
        }
        genFood(); //Generate new food
    }
    else{ //Else
        if(head->y == 0){ //If the head is on the top edge of the screen
            move(head->x, BOARD_HEIGHT - 1); //Move to the bottom edge of the screen
        }
        else{ //Else
            move(head->x, head->y - 1); //Move UP
        }
    }
}

void moveDown(){ //The function to move DOWN
    if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
        if(head->y == BOARD_HEIGHT - 1){ //If the head is on the bottom edge of the screen
            eatAndMove(head->x, 0); //Eat the food and move to the top edge of the screen
        }
        else{ //Else
            eatAndMove(head->x, head->y + 1); //Eat the food and move DOWN
        }
        genFood(); //Generate new food
    }
    else{ //Else
        if(head->y == BOARD_HEIGHT - 1){ //If the head is on the bottom edge of the screen
            move(head->x, 0); //Move to the top edge of the screen
        }
        else{ //Else
            move(head->x, head->y + 1); //Move DOWN
        }
    }
}

void moveLeft(){ //The function to move LEFT
    if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
        if(head->x == 0){ //If the head is on the left edge of the screen
            eatAndMove(BOARD_WIDTH - 1, head->y); //Eat the food and move to the right edge of the screen
        }
        else{ //Else
            eatAndMove(head->x - 1, head->y); //Eat the food and move LEFT
        }
        genFood(); //Generate new food
    }
    else{ //Else
        if(head->x == 0){ //If the head is on the left edge of the screen
            move(BOARD_WIDTH - 1, head->y); //Move to the right edge of the screen
        }
        else{ //Else
            move(head->x - 1, head->y); //Move LEFT
        }
    }
}

void moveRight(){ //The function to move RIGHT
    if(head->x == food[0] && head->y == food[1]){ //If the head is on the food
        if(head->x == BOARD_WIDTH - 1){ //If the head is on the right edge of the screen
            eatAndMove(0, head->y); //Eat the food and move to the left edge of the screen
        }
        else{ //Else
            eatAndMove(head->x + 1, head->y); //Eat the food and move RIGHT
        }
        genFood(); //Generate new food
    }
    else{ //Else
        if(head->x == BOARD_WIDTH - 1){ //If the head is on the right edge of the screen
            move(0, head->y); //Move to the left edge of the screen
        }
        else{ //Else
            move(head->x + 1, head->y); //Move RIGHT
        }
    }
}

void changeDir(uint gpio, uint32_t events) { //The change direction interrupt handler
    if(alive){ //If the snake is alive
        switch(gpio){ //Switch based on the GPIO
            case UP_PIN: direction = (direction == DOWN ? DOWN : UP); //If the command is to go UP and the snake isn't going DOWN, go UP
                         break;
            case DOWN_PIN: direction = (direction == UP ? UP : DOWN); //If the command is to go DOWN and the snake isn't going UP, go DOWN
                           break;
            case LEFT_PIN: direction = (direction == RIGHT ? RIGHT : LEFT); //If the command is to go LEFT and the snake isn't going RIGHT, go LEFT
                           break;
            case RIGHT_PIN: direction = (direction == LEFT ? LEFT : RIGHT); //If the command is to go RIGHT and the snake isn't going LEFT, go RIGHT
                            break;
            default: break;
        }
    }
    else{ //If the snake is dead
        reset = 1; //Set the reset flag
    }
}

int main(){
    short i; //An interator
    stdio_init_all();//Initialize all of the present standard stdio types that are linked into the binary
    initVGA(); //Initialize the VGA screen and functions

    srand(1); //Random seed

    gpio_set_irq_enabled_with_callback(UP_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
    gpio_set_irq_enabled_with_callback(DOWN_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
    gpio_set_irq_enabled_with_callback(LEFT_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO
    gpio_set_irq_enabled_with_callback(RIGHT_PIN, GPIO_IRQ_EDGE_FALL, true, &changeDir); //Attach the callback to the specified GPIO

    while(1){ //While eternity
        if(reset){ //If the reset flag has been set
            fillRect(0, 0, WIDTH, HEIGHT, BLACK); //Clear the screen
            alive = 1; //Resurrect the snake

            //Clear the linked list
            Node *tmp; //Create a temporary node
            while(head != NULL){ //While head exists
                tmp = head; //Make the temp as head
                head = head->next; //Head is the next of itself
                free(tmp); //Free the temp
            }
            //Create the snake
            for(i = 0; i < 8; i++){
                addNode((BOARD_WIDTH / 2) - i, BOARD_HEIGHT / 2);
            }
            direction = RIGHT; //Set default direction to be RIGHT
            genFood(); //Generate food
            reset = 0; //Clear the reset flag
        }
        while(alive){ //While the snake is alive
            unsigned long begin_time = (unsigned long)(get_absolute_time() / 1000); //Save the start time of the loop
            switch(direction){ //Switch based on the direction the snake is moving in
                case UP: moveUp(); //If the direction is UP, move UP
                            break;
                case DOWN: moveDown(); //If the direction is DOWN, move DOWN
                            break;
                case LEFT: moveLeft(); //If the direction is LEFT, move LEFT
                            break;
                case RIGHT: moveRight(); //If the direction is RIGHT, move RIGHT
                            break;
                default: break;
            }
            sleep_ms(33 - ((unsigned long)(get_absolute_time() / 1000) - begin_time)); //Sleep for the amount of time to achieve 30fps
        }
    }
}


Code Organization

The code has a VGA library which has been explained really nicely by Prof. Adams and the main file. Most of the code is self explanatory, however, the main components of the game are explained in the following subsections.

The body of the snake

First and foremost, the snake is implemented in the form of a linked list. According to Wikipedia, a linked list is a linear collection of data elements whose order is not given by their physical placement in memory. Instead, each element points to the next. It is a data structure consisting of a collection of nodes which together represent a sequence. In its most basic form, each node contains: data, and a reference (in other words, a link) to the next node in the sequence. Each node in our snake linked list holds the x-coordinate of the node, the y-coordinate of the node and the pointer to the next node. Therefore, moving the snake is nothing more than shifting the data of the nodes of the linked list.

typedef struct Node{
    short x, y;
    struct Node *next;
}Node;


The generation of food

The function to generate food is pretty simple. First, I randomly generate x and y coordinates for the food. Then, I iterate through the linked list. If the food coordinates coincide with the snake's body, I regenerate the food coordinates.

void genFood(){
genAgain:
    food[0] = randomRange(0, BOARD_WIDTH);
    food[1] = randomRange(0, BOARD_HEIGHT);
    Node *current = head;
    current = current->next;
    while(current != NULL){
        if(food[0] == current->x && food[1] == current->y){
            goto genAgain;
        }
        current = current->next;
    }
    fillRect(food[0] << 2, food[1] << 2, 4, 4, YELLOW);
}


Moving the snake

Two functions which are responsible for motion are move() and eatAndMove(). These functions are responsible for the normal motion and the motion of the snake respectively. They're quite similar, except for the fact that eatAndMove() increases the length of the snake by adding a new element at the end of the linked list. Both these functions work by shifting the data in the linked list.

void move(short x, short y){
    fillRect(x << 2, y << 2, 4, 4, RED);
    Node *current = head;
    while(current != NULL){
        fillRect(current->x << 2, current->y << 2, 4, 4, GREEN);
        swap(&(current->x), &x);
        swap(&(current->y), &y);
        current = current->next;
        if(current != NULL){
            if(current->x == head->x && current->y == head->y){
                alive = 0;
                fillRect(head->x << 2, head->y << 2, 4, 4, RED);
                setTextColor(WHITE);
                setTextSize(4);
                setCursor(100, 200);
                writeString("Death awaits us all");
                break;
            }
        }
    }
    fillRect(x << 2, y << 2, 4, 4, BLACK);
}

void eatAndMove(short x, short y){
    fillRect(x << 2, y << 2, 4, 4, RED);
    Node *current = head;
    while(current->next != NULL){
        fillRect(current->x << 2, current->y << 2, 4, 4, GREEN);
        swap(&(current->x), &x);
        swap(&(current->y), &y);
        current = current->next;
    }
    current->next = (Node *)malloc(sizeof(Node));
    fillRect(current->x << 2, current->y << 2, 4, 4, GREEN);
    swap(&(current->x), &x);
    swap(&(current->y), &y);
    current = current->next;
    current->x = x;
    current->y = y;
    current->next = NULL;
}


The output

Snake game on RaspberryPi Pico

CMakeLists.txt

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(Snake-project)

pico_sdk_init()

add_executable(Snake)

pico_enable_stdio_usb(Snake 1)
pico_enable_stdio_uart(Snake 1)

pico_generate_pio_header(Snake ${CMAKE_CURRENT_LIST_DIR}/hsync.pio)
pico_generate_pio_header(Snake ${CMAKE_CURRENT_LIST_DIR}/vsync.pio)
pico_generate_pio_header(Snake ${CMAKE_CURRENT_LIST_DIR}/rgb.pio)

target_sources(Snake PRIVATE Snake.c vga_graphics.c)

target_link_libraries(Snake PRIVATE pico_stdlib hardware_pio hardware_dma hardware_adc hardware_irq pico_time pico_multicore)

pico_add_extra_outputs(Snake)