Arduboy Snake The Game

Learn how to program the classic Snake game to the portable Arduboy powered by Arduino hardware. The Arduboy is small, the same size as a credit card and contains the same AVR microcontroller as the popular Arduino board. The Arduboy is designed to look like a classic Gameboy by Nintendo copying the placement of the D-pad, buttons and the screen. It's less powerful than the original hardware but comes with a familiar development environment and tonnes of resources and online documentation.

The logic of the Snake Game?

The Snake game starts with the snake head and food for the snake randomly positioned within the screen. The player uses the left and right buttons on the D-pad to turn the snake head while it moves forwards at a constant pace. The goal is to collect the food which results in the snake growing a body which follows the head. Game over occurs when the player runs into the snake head into the snake body. This game will also feature the warping ability where the snake will be warped to the opposite side of the screen when collision occurs with the side of the screen. Keep playing to achieve the best high score.

Coding the Snake Game

Familiar with coding already? Jump straight to the code repository.
https://github.com/devvid/arduboy-snake

// Snake game for Arduino / Arduboy hardware
// Made by David Cedar / Supereasyelectronics / Devvid 2018
// Repo https://github.com/devvid/arduboy-snake

#include <Arduboy2.h>
#include <EEPROM.h>

// 128x64 / 4 = 32x16
#define WORLD_WIDTH  16
#define WORLD_HEIGHT 8
#define SCALE_FACTOR 8

// make an instance of arduboy used for many functions
Arduboy2 arduboy;

// Game state
enum state {
    TITLE,
    GAME,
    WIN,
    GAMEOVER,
    RESET
};
enum state GAMESTATE = RESET;
// Position & Button enum
enum pos {X = 0, Y = 1};
enum button {A = 0, B = 1, UP = 2, LEFT = 3, DOWN = 4, RIGHT = 5 };

// Game variables
byte foodPos[2] = {0};                                             // The position of the food. Currently we are only showing one piece
boolean pressed [6] = {false, false, false, false , false, false}; // Check if button A & B is pressed already
int delayTime = 300;                                               // The delay the game will use to control movement
unsigned long previousTime;                                        // Used with the control of the deplay
int timeDecay = 50;                                                // Use will use an exponential decay to control play speed. Speed up the game over time.
byte scoreHuman = 0;                                                    // Keep track of the player score, this is saved into EEPROM when gameover.
int snakeHumanDirection = 3;                                       // The direction of the snake. 0 top, 1 left, 2 down, 3 right
byte snakeHuman[WORLD_WIDTH*WORLD_HEIGHT][2] = {0};                // The snake body is a 2d array, rows consisting of the body parts and col the x & y cords [The long body][y & x]



// Manage vector like function using 2d array instead.
void push_front_array(int, int, byte [WORLD_WIDTH*WORLD_HEIGHT][2]);
void push_back_array(byte [WORLD_WIDTH*WORLD_HEIGHT][2]);
void push_back_array(int, int, byte [WORLD_WIDTH*WORLD_HEIGHT][2]);
byte get_array_end(byte [WORLD_WIDTH*WORLD_HEIGHT][2]);
void pop_array_element(int , int , byte [WORLD_WIDTH*WORLD_HEIGHT][2]);
void clear_array(byte [WORLD_WIDTH*WORLD_HEIGHT][2]);

// Time function: We still want to listen to our button presses in real time.
// else we wan to delay our game soo the player ... steps 
void timeCritial(){
    // Delay the system so we create the stepping effect while still listeniig to controls
    previousTime = millis();
    while((millis() - previousTime) < exp(-scoreHuman/timeDecay)*delayTime){
        // Player control D Pad
        if(arduboy.pressed(RIGHT_BUTTON) and !pressed[RIGHT]){
            snakeHumanDirection++;
            if(snakeHumanDirection == 4) snakeHumanDirection = 0;
            pressed[RIGHT] = true;
        }
        if(arduboy.pressed(LEFT_BUTTON) and !pressed[LEFT]){
            snakeHumanDirection--;
            if(snakeHumanDirection == -1) snakeHumanDirection = 3;
            pressed[LEFT] = true;
        }

        //Check if the button is being held down
        if(arduboy.notPressed(UP_BUTTON)) pressed[UP] = false;
        if(arduboy.notPressed(LEFT_BUTTON)) pressed[LEFT] = false;
        if(arduboy.notPressed(DOWN_BUTTON)) pressed[DOWN] = false;
        if(arduboy.notPressed(RIGHT_BUTTON)) pressed[RIGHT] = false;
    }    
}

void placeFood(){
    // Set the Food to a random spot on the map
    foodPos[X] = 0;
    foodPos[Y] = 0;
    while(foodPos[X] == 0 and foodPos[Y] == 0 ){
        foodPos[X] = random(1,WORLD_WIDTH-1);
        foodPos[Y] = random(1,WORLD_HEIGHT-1);
        for(int i = 0; i < WORLD_HEIGHT*WORLD_HEIGHT; i++){
            if(foodPos[X] == snakeHuman[i][X] && foodPos[Y] == snakeHuman[i][Y]){
                foodPos[X] = 0;
                foodPos[Y] = 0;
            }
        }
    }
}

// This function runs once in your game.
// use it for anything that needs to be set only once in your game.
void setup() {
    // initiate arduboy instance
    arduboy.begin();

    //Seed the random number generator
	arduboy.initRandomSeed();

    // here we set the framerate to 15, we do not need to run at
    // default 60 and it saves us battery life
    arduboy.setFrameRate(60);
    arduboy.clear();
}

// our main game loop, this runs once every cycle/frame.
// this is where our game logic goes.
void loop() {
    // pause render until it's time for the next frame
    if (!(arduboy.nextFrame()))
    return;

    // first we clear our screen to black and start from top left corner
    arduboy.clear();
    arduboy.setCursor(0, 0);

    
    // Game Logic
    switch(GAMESTATE){
        case RESET:
            // Initilise Stack
            for (byte y = 0; y < WORLD_HEIGHT*WORLD_WIDTH; y++)
                for ( byte x = 0; x < 2; x++)
                    snakeHuman[y][x] = 255;

            // Set player
            snakeHuman[0][X] = random(1,WORLD_WIDTH-1);
            snakeHuman[0][Y] = random(1,WORLD_HEIGHT-1); 

            // Set the Food to a random spot on the map
            placeFood();

            // Reset score
            scoreHuman = 0;

            // Proced to the title screen.
            GAMESTATE = TITLE;
            break;

        case TITLE:
            arduboy.setCursor(0, 0);
            arduboy.setTextSize(2);
			      arduboy.print("Snake V2\n");
            arduboy.setTextSize(1);
            arduboy.print("Controls: \nLeft & Right pad\n");
            arduboy.print("Press A to play\n\n");
            //arduboy.setCursor(0, 45);
            arduboy.print("David Cedar 2018\n");
            arduboy.print("www.devvid.com/snake");
            
            if(arduboy.pressed(A_BUTTON) and !pressed[A]) {
      				pressed[A] = true;
      				GAMESTATE = GAME;
      			}
            break;

        case GAME:

            // Slow down game: This makes the whole game freeze, not so good as we lose input.
            //delay(exp(-score/50)*delayTime);
    
            // Set Direction. & Slow the game using tight loop whiles responding to button clicks
            timeCritial();

            // Body movement logic
            switch (snakeHumanDirection){
                case 0: // UP
                    if(snakeHuman[0][Y] - 1 < 0) snakeHuman[0][Y] = 8; 
                    push_front_array(snakeHuman[0][X], snakeHuman[0][Y] - 1, snakeHuman );
                    break;
                case 1: // RIGHT
                    if(snakeHuman[0][X] + 1 > 15) snakeHuman[0][X] = -1; 
                    push_front_array(snakeHuman[0][X] + 1, snakeHuman[0][Y], snakeHuman );
                    break;
                case 2: // DOWN
                    if(snakeHuman[0][Y] + 1 > 7) snakeHuman[0][Y] = -1; 
                    push_front_array(snakeHuman[0][X], snakeHuman[0][Y] + 1, snakeHuman );
                    break;
                case 3: // LEFT
                    if(snakeHuman[0][X] - 1 < 0) snakeHuman[0][X] = 16;
                    push_front_array(snakeHuman[0][X] - 1, snakeHuman[0][Y], snakeHuman );
                    break;
            }

            // Chop off tail
            pop_back_array(snakeHuman);

            // Collision check with Body
            for(byte i = 1; i < WORLD_HEIGHT*WORLD_WIDTH; i++){
                if (snakeHuman[0][X] == snakeHuman[i][X] && snakeHuman[0][Y] == snakeHuman[i][Y])
                    GAMESTATE = GAMEOVER;
            }

            // Collision detection snakeHuman v Food
            if(snakeHuman[0][X] == foodPos[X] && snakeHuman[0][Y] == foodPos[Y]) {
                placeFood();
                scoreHuman++;
                push_back_array(snakeHuman);
            }

            // Drawing! inc player, head, tail, fruit
            arduboy.setCursor(0, 0);
            // Draw fruit
            arduboy.drawCircle((foodPos[X] * SCALE_FACTOR) + SCALE_FACTOR/2, (foodPos[Y] * SCALE_FACTOR) + SCALE_FACTOR/2, SCALE_FACTOR/2, WHITE);
            // Draw snakeHuman
            for (byte i = 0; i < get_array_end(snakeHuman); i++)
                arduboy.fillRect(snakeHuman[i][X] * SCALE_FACTOR, snakeHuman[i][Y] * SCALE_FACTOR, SCALE_FACTOR, SCALE_FACTOR, WHITE);
            break;

        case WIN:
            // Do you ever win?
            break;

        case GAMEOVER:
            // Read the EEPROM for save data.
            byte savedScore = EEPROM.read(0);
            if ( savedScore == 255) savedScore = 0;

            arduboy.setCursor(0, 0);
            arduboy.print("GAME OVER!");
            arduboy.print("\n\n");
            arduboy.print("Score: ");
            arduboy.print(scoreHuman);
            arduboy.print("\nHigh Score: ");
            arduboy.print(savedScore);
            arduboy.print("\n\n");
            arduboy.print("Press A to Play Again");
            
            // Save to EEPROM if new high score.
            if (scoreHuman > savedScore) EEPROM.update(0, scoreHuman);
            
            if(arduboy.pressed(A_BUTTON) and !pressed[A]) {
                pressed[A] = true;
                GAMESTATE = RESET;
            }
            break;    
    }

    // Reset the game if both A and B are pressed.
    if(arduboy.pressed(A_BUTTON) && arduboy.pressed(B_BUTTON) && !pressed[A] && !pressed[B]) GAMESTATE = RESET;
    
    //Check if the button is being held down
    if(arduboy.notPressed(A_BUTTON)) pressed[A] = false;
    if(arduboy.notPressed(B_BUTTON)) pressed[B] = false;
    if(GAMESTATE != GAME){
        // Dont both when in GAME as timeCritial() takes care of D Pad
        if(arduboy.notPressed(UP_BUTTON)) pressed[UP] = false;
        if(arduboy.notPressed(LEFT_BUTTON)) pressed[LEFT] = false;
        if(arduboy.notPressed(DOWN_BUTTON)) pressed[DOWN] = false;
        if(arduboy.notPressed(RIGHT_BUTTON)) pressed[RIGHT] = false;
        }
    // then we finaly we tell the arduboy to display what we just wrote to the display
    arduboy.display();
}

// ######################################
// Manage vector like effects
// ######################################
void push_front_array(int x, int y, byte tailStack[WORLD_WIDTH*WORLD_HEIGHT][2]){
	// Push
	int stopByte = get_array_end(tailStack);
	if (stopByte == 255) return;
    if (stopByte > 0 && stopByte < 255) {
        // Move elements down one, ready to push to top
        for(int i = stopByte - 1; i > -1; i--){
            tailStack[i + 1][X] = tailStack[i][X];
            tailStack[i + 1][Y] = tailStack[i][Y];
        }
    }  
    tailStack[0][X] = x;
    tailStack[0][Y] = y;
}
void push_back_array(byte tailStack[WORLD_WIDTH*WORLD_HEIGHT][2]){
    int stopByte = get_array_end(tailStack);
    tailStack[stopByte][X] = tailStack[stopByte - 1][X];
    tailStack[stopByte][Y] = tailStack[stopByte - 1][Y];
}
void push_back_array(int x, int y, byte tailStack[WORLD_WIDTH*WORLD_HEIGHT][2]){
    int stopByte = get_array_end(tailStack);
    tailStack[stopByte][X] = x;
    tailStack[stopByte][Y] = y;
}
// Returns the actual end byte, in other words not the final real data the user wants. kinda like '\0'
byte get_array_end(byte tailStack[WORLD_WIDTH*WORLD_HEIGHT][2]){
	for (int i = 0; i < WORLD_WIDTH*WORLD_HEIGHT; i++){
		if (tailStack[i][X] == 255) return i;
	}
	return 255;
}
void pop_back_array(byte tailStack[WORLD_WIDTH*WORLD_HEIGHT][2]){
    int stopByte = get_array_end(tailStack);
    tailStack[stopByte - 1][X] = 255;
    tailStack[stopByte - 1][Y] = 255;
}
void clear_array(byte tailStack[WORLD_WIDTH*WORLD_HEIGHT][2]){
	for (int i = 0; i < WORLD_WIDTH*WORLD_HEIGHT; i++){
		tailStack[i][X] = 255;
        tailStack[i][Y] = 255;
    }
}