Weekend hack: NxN Snake

I started a side hobby of building little electronics projects about a year ago.  While I did graduate with a dual electrical engineering degree and computer science honours degree (wow, was it really so long ago in 2002?!), I never kept up the EE knowledge because I focused on software development as my passion instead.  I forgot how much fun it is to build a physical circuit as opposed to the abstract software applications I wire together daily.  The most fun is marrying the two — making something on the web change something in the “real” world.  I literally giggled when I first got a lamp to turn on and off by clicking a button on a web page.  I know it’s a stupidly simple little thing (and my husband rightly teased me for finally putting that EE degree to good use), but the whole IOT concept is fascinating, and I like that I can do both.

One of my favorite things to play with is a 16×16 NeoPixel matrix (I stumbled upon a cheap knock off from Amazon, but here’s the Adafruit alternative:  Flexible 16×16 NeoPixel RGB Matrix).  The resolution is high enough that you can create simple little games, but it only requires 2 pins on an Arduino to control.  The Adafruit libraries also make it super easy to code.

And the colors are stunning.

Since it’s been a cold snowy weekend here in Saskatoon, I decided to hook up the matrix and write the classic Snake.  First, the wiring (shown in the fritzing diagram below).  Some things I learned while putting this together:

  • 16×16 fritzing parts are hard to find, so I just used an 8×8 in the diagram.
  • Use a breadboard power supply to power the matrix because the Arduino isn’t powerful enough to handle the current required by the matrix.  I used the 5V side of my configurable power supply.
  • Joystick callibration can be a bugger.  It took a lot of trial and error to settle on the right numbers to detect a player’s movement accurately.
  • Use math whenever possible to reduce memory requirements for the microcontroller.  I initially used an Uno and mapped my game’s board array to the NeoPixel matrix numbers by using a mapping array, but this took up so much memory that it wouldn’t load on the Uno.  I switched to a Mega fix this limitation before realizing I could remove the memory requirement entirely by using a function to calculate it instead.
snake-fritzing
Snake with an Arduino Mega, NeoPixel matrix, breadboard power supply, and a joystick

There are a bunch of C++ Snake clones out there I could have copied, but for fun I wrote my own:

snake_with_rbg_matrix-NxN.ino:

#include <Adafruit_NeoPixel.h>

#include <Adafruit_GFX.h>
#include <gfxfont.h>

#define PIN 10
#define UP 1
#define LEFT 2
#define RIGHT 3
#define DOWN 4
const int JOYSTICK_XPIN = A0;
const int JOYSTICK_YPIN = A1;

#define ROWS 16
#define COLS 16

Adafruit_NeoPixel matrix = Adafruit_NeoPixel(256, PIN, NEO_GRB + NEO_KHZ800);
uint32_t RED = matrix.Color(255, 0, 0);
uint32_t GREEN = matrix.Color(0, 255, 0);
uint32_t BLUE = matrix.Color(0, 0, 255);
uint32_t CLEAR = matrix.Color(0, 0, 0); 
uint32_t PURPLE = matrix.Color(255, 0, 255); 
uint32_t YELLOW = matrix.Color(255, 255, 0);

struct point {
  int x;
  int y;
};

point player[ROWS * COLS];

int playerDirection;
int playerLength;
point playerHead;

point apple;

int board[ROWS][COLS];

unsigned long lastClockTick;
int gameRate;
int numApplesEaten = 0;

void setup() { 
  matrix.begin();  
  matrix.setBrightness(15);
  matrix.show();
  randomSeed(analogRead(0));

  defineBoard();  
  startGame();    
}

void defineBoard() {  
  // draw the outer board
  for(int i = 0; i < COLS; i++) {
    board[0][i] = 1;    
    board[ROWS - 1][i] = 1;
  }  
  generateRandomBoard();
}

void generateRandomBoard() {
  // clear the existing board first
  for(int i = 1; i < ROWS - 1; i++) {
    for(int j = 0; j < COLS; j++) {
      if(j == 0 || j == (COLS - 1)) {
        board[i][j] = 1;         
      } else {
        board[i][j] = 0;
      }
    }
  }
  // 20 random pixels set, but make sure that it won't cause the player to lose (diagonal pixels)
  for(int i = 0; i < 20; i++) {
    bool found = false;
    int x = random(1, ROWS - 1);
    int y = random(1, COLS - 1);
    while(!found) {          
      if(!boardContainsCoordinates(x, y)
         && !boardContainsCoordinates(x - 1, y - 1)         
         && !boardContainsCoordinates(x + 1, y - 1)
         && !boardContainsCoordinates(x - 1, y + 1) 
         && !boardContainsCoordinates(x + 1, y + 1)){
         found = true;
      } else {
        x = random(1, ROWS - 1);
        y = random(1, COLS - 1);
      }
    }
    board[x][y] = 1;
  }
}

void startGame() {
  matrix.clear();
  resetGameVariables();
  drawBoard();  
  drawPlayer();  
  drawApple();
  matrix.show();  
}

void resetGameVariables() {  
  generateRandomBoard();
  bool found = false;  
  // generate a random spot for the user to start, and a random direction
  while(!found) {
    // start the player in a random spot
    playerHead.x = random(1, ROWS - 1);
    playerHead.y = random(1, COLS - 1);
    int startDirection = random(0, 2);

    if(startDirection == 0) {
      if(playerHead.y < COLS / 2) {
        playerDirection = RIGHT;
      } else {
        playerDirection = LEFT;
      }
    } else {
      if(playerHead.x < ROWS / 2) {
        playerDirection = DOWN;
      } else {
        playerDirection = UP;
      }  
    }

    if(playerHas5Moves()) {
      found = true;
    }
  }

  generateApple();

  playerLength = 1;
  player[0].x = playerHead.x;
  player[0].y = playerHead.y;

  lastClockTick = millis();
  gameRate = 300;  
  numApplesEaten = 0;
}
// make sure with the random start that the player has a few moves to react
bool playerHas5Moves() {
  for(int i = 0; i < 5; i++) {
    switch(playerDirection) {
      case RIGHT:
        if(board[playerHead.x][playerHead.y + i] == 1) {
          return false;
        }
        break;
      case LEFT:
        if(board[playerHead.x][playerHead.y - i] == 1) {
          return false;
        }
        break;
      case UP:
        if(board[playerHead.x - i][playerHead.y] == 1) {
          return false;
        }
        break;
      case DOWN:
        if(board[playerHead.x + i][playerHead.y] == 1) {
          return false;
        }
        break;
    }
  }
  return true;
}
void generateApple() {
  bool found = false;
  // make sure the apple doesn't end up on a board coordinate or on top of the player
  while(!found) {    
    apple.x = random(1, ROWS - 1);
    apple.y = random(1, COLS - 1);
    if(!playerContainsCoordinates(apple.x, apple.y)
        && !boardContainsCoordinates(apple.x, apple.y)) {       
      found = true;
    }
  }  
}
bool playerContainsCoordinates(int x, int y) {  
  for(int i = 0; i < playerLength; i++) {
    if(player[i].x == x && player[i].y == y) {      
      return true;
    }
  }
  return false;
}
bool boardContainsCoordinates(int x, int y) {
  return board[x][y] == 1;
}

void drawPlayer() {  
  for(int i = 0; i < playerLength; i++) {        
    matrix.setPixelColor(convertToMatrixPoint(player[i].x, player[i].y), BLUE);
  }  
}
void drawBoard() {
  for(int i = 0; i < ROWS; i++) {
    for(int j = 0; j < COLS; j++) {
      if(board[i][j] == 1) {        
        matrix.setPixelColor(convertToMatrixPoint(i, j), GREEN);     
      } else {
        matrix.setPixelColor(convertToMatrixPoint(i, j), CLEAR);
      }
    }
  }  
  // identify the bottom left pixel by painting it blue
  matrix.setPixelColor(convertToMatrixPoint(0, 0), BLUE);   
  // identify the matrix start pixels and direction
  matrix.setPixelColor(0, PURPLE);
  matrix.setPixelColor(1, YELLOW);
}
void drawApple() {
  matrix.setPixelColor(convertToMatrixPoint(apple.x, apple.y), RED);
}

void loop() {  
  float x = analogRead(JOYSTICK_XPIN);
  float y = analogRead(JOYSTICK_YPIN);

  float deltax = abs(510 - x);
  float deltay = abs(505 - y);

  // detect if the player's movement is more likely x or y direction
  if(deltax > deltay) {
    if(x < 480) {
      playerDirection = DOWN;
    } else if(x > 540) {
      playerDirection = UP;
    }
  } else {
    if(y < 475) {
      playerDirection = RIGHT;
    } else if(y > 535) {
      playerDirection = LEFT;
    }
  }  

  if(millis() - lastClockTick > gameRate) {
    advancePlayer();    
    detectCollision();   
    detectAppleEaten();
    updateBoard();
    lastClockTick = millis();
  }
}

void advancePlayer() {
  if(playerDirection == LEFT) {
    playerHead.y -= 1;
  } else if(playerDirection == RIGHT) {    
    playerHead.y += 1;    
  } else if(playerDirection == UP) {    
    playerHead.x -= 1;    
  } else if(playerDirection == DOWN) {        
    playerHead.x += 1;
  }
  // see if this point already exists in the player's matrix
  for(int i = 0; i < playerLength; i++) {
    if(player[i].x == playerHead.x && player[i].y == playerHead.y) {
      gameOver();
    }
  }
  for(int i = playerLength - 1; i > 0; i--) {
    player[i] = player[i - 1];
  }
  player[0].x = playerHead.x;
  player[0].y = playerHead.y;
}

void detectCollision() {
  if(board[playerHead.x][playerHead.y] == 1) {
    gameOver();
  }
}
void detectAppleEaten() {
  if(playerHead.x == apple.x && playerHead.y == apple.y) {
    numApplesEaten++;
    playerLength += 1;
    player[playerLength - 1].x = playerHead.x;
    player[playerLength - 1].y = playerHead.y;
    if(numApplesEaten % 5 == 0 && gameRate > 100) {
      gameRate -= 20;
    }
    generateApple();
  }
}

void updateBoard() {  
  drawBoard();
  drawPlayer();
  drawApple();
  matrix.show();
}
void gameOver() {
  matrix.clear();
  for(int i = 0; i < ROWS; i++) {
    for(int j = 0; j < COLS; j++) {
      matrix.setPixelColor(convertToMatrixPoint(i, j), GREEN);      
    }    
  }
  matrix.show();
  delay(3000);
  startGame();
}

int convertToMatrixPoint(int i, int j) {
  if(i % 2 == 0) {
    return (COLS * i) + (COLS - 1) - j;
  } else {
    return (COLS * i) + j;
  }
}

The method convertToMatrixPoint is where I map the game board (which was a two dimensional matrix) to the NeoPixel matrix library (which identifies individual pixels with a single number).  To make it fun, the matrix locations go in a long continuous strand from bottom to top, so the formula is different for even and odd rows.  Thank you Mr. Long for teaching mapping functions in grade 9!

When I first wrote the code for the joystick, I just used an if/elseif statement instead of checking if x (left/right) or y (up/down) was the larger movement.  That meant that if the user had any left/right movement that the game would detect that movement first, even if the up/down movement was what the user actually indicated.  That meant a lot of crashing when I was certain I moved the joystick in the right direction.  The check for the larger movement solved that problem — I like how it handles now much better.

I uploaded the project to GitHub:  https://github.com/collene/arduino-snake-nxn and the video to YouTube:

 

Not bad for a weekend project!  Apparently this month’s Hackerbox (which I have yet to receive, damn international shipping) also has an LED matrix with an ESP32 board.  I might have to take Snake here to a cloud version…

Collene Hansen

I'm a software engineer interested in too many things to list here! This blog includes my thoughts about various subjects -- technology, programming, career, life, electronics, books, and whatever else I feel like writing about.

9 comments

  • Nice Work!! trying to do the same. But my Matrix will not light up. I have the same komponents. I wired all like u. Have u any Idea?

    Like

    • Are you also using a breadboard power supply, or are you trying to power the matrix off of the Arduino? I didn’t have much luck with running it off the Arduino, so now I always use a breadboard power supply. When you turn the breadboard power supply on, do a few of the pixels on the matrix appear to blink? Have you used the matrix in other projects?

      Like

  • Yeah i used the matrix in another project too. In other projects it works fine. But with that code no pixel will light up…

    Like

    • Do you have the Adafruit libraries installed (though I’m assuming the code actually compiles and uploads to the Arduino since you’ve used the matrix in other projects). Have you tried adding a few Serial.println statements and seeing where the program stops executing?

      Like

    • I tried to make the code flexible, so you should just be able to adjust lines 14 and 15 to be:

      #define ROWS 8
      #define COLS 8

      “Should” is the key word there because I haven’t tested it 🙂 You might need to adjust some of the other methods. If you do find a bug when you change the rows and cols, let me know how you fixed it!

      Like

  • Hi Collene
    I’m a italian student, I have done your project , it’s all ok but I’m using a power directly in the arduino module with a ac/ad adapter output 9v 1a
    sometimes happen that dosn’t work properly, could be a problem by the power?
    thanks in advance

    Like

    • Hi Luciano, Yeah it’s probably a power problem. That’s why I used a separate breadboard power supply and didn’t power it right off the arduino. If you have a lot of the neopixels turned on (or a lot of white/brighter pixels), it can draw a lot of current. Try using a separate power supply for the matrix. Or increasing the adapter to 2A. You can read a good explanation on the Adafruit product page: https://www.adafruit.com/product/1487

      Like

Submit a comment