If you had a Nokia 3310, you know what we’re talking about. Snake is not just a game; it’s the father of mobile video games. And today, we are going to program it from scratch.
Unlike other projects where the challenge is the electrical assembly, here the challenge is the software. How do we make a row of lights “move” like a snake? How do we remember where each part of its body is? How do we detect if it has bitten its tail?
We are not going to use an LCD screen, but an 8x8 LED Matrix retro style.
This project is ideal for understanding the management of Arrays and the difference between Game Logic (what happens in memory) and Rendering (what shows on the lights).
Anatomy of a digital snake
The beginner’s mistake is to think of the screen as a grid of pixels grid[8][8] and try to move the lit points. That is a programming nightmare.
The correct way is to abstract. Our snake is not lights, it’s a list of coordinates. We will use two Arrays:
bodyX[]: Stores the X coordinates of each segment.bodyY[]: Stores the Y coordinates of each segment.
Index 0 will always be the Head.
Index 1 will be the neck.
The last index will be the Tail.
Movement algorithm
To move the snake, we don’t move all the points at once. We apply the “drag” logic:
We start with the tail. The tail moves to where the previous segment was.
We repeat until we reach the neck.
Finally, we move the Head one cell in the direction indicated by the Joystick.
Components
- [1x|blue] Arduino Uno | Or Nano
- [1x|green] LED Matrix 8x8 Module with MAX7219 | Retro display {integrated driver}
- [1x|orange] Analog Joystick (KY-023) | Direction control
- [Various|gray] Dupont Wires | Connections
Using the MAX7219 chip saves us a lot of work. If you try to control 64 LEDs directly with Arduino, you’ll run out of pins and patience. This chip does it all with just 3 wires.
Connection Diagram
We have two peripherals: the input (Joystick) and the output (Display).
LED Matrix 8x8
The matrix with MAX7219 uses three control signals plus power.
[ { “from”: “Matrix VCC”, “to”: “Arduino 5V”, “color”: “red” }, { “from”: “Matrix GND”, “to”: “Arduino GND”, “color”: “black” }, { “from”: “Matrix DIN”, “to”: “Pin 12”, “color”: “green”, “note”: “Data for LedControl” }, { “from”: “Matrix CS”, “to”: “Pin 10”, “color”: “blue”, “note”: “Chip Select” }, { “from”: “Matrix CLK”, “to”: “Pin 11”, “color”: “purple”, “note”: “Clock for LedControl” } ]
Analog Joystick
The joystick delivers two analog signals, one for each axis. We won’t use the built-in button in this version.
[ { “from”: “Joystick VCC”, “to”: “Arduino 5V”, “color”: “red” }, { “from”: “Joystick GND”, “to”: “Arduino GND”, “color”: “black” }, { “from”: “Joystick VRx”, “to”: “Pin A0”, “color”: “orange” }, { “from”: “Joystick VRy”, “to”: “Pin A1”, “color”: “orange” } ]
The Code
We will need the LedControl.h library to talk to the MAX7219 chip. You can install it from the library manager.
The code is a bit longer than usual because it includes the game logic. We have divided it into functions for readability.
Global variables and configuration
We define the arrays with a maximum size (e.g. 64 segments, which would be a full screen).
#include <LedControl.h>
// Matrix pins: DIN=12, CLK=11, CS=10
LedControl lc = LedControl(12, 11, 10, 1);
// Joystick pins
const int pinJoyX = A0;
const int pinJoyY = A1;
// --- GAME VARIABLES ---
const int MAX_LENGTH = 64;
int bodyX[MAX_LENGTH];
int bodyY[MAX_LENGTH];
int snakeLength = 3; // We start with size 3
int direction = 1; // 0:Up, 1:Right, 2:Down, 3:Left
// Food
int foodX, foodY;
int previousTailX, previousTailY;
// Timing (Game Speed)
unsigned long prevTime = 0;
int speed = 300; // Milliseconds per move (lower = faster)
void setup() {
// Initialize Matrix
lc.shutdown(0, false); // Wake up
lc.setIntensity(0, 4); // Brightness (0-15)
lc.clearDisplay(0);
// Initial position (center)
bodyX[0]=4; bodyY[0]=4; // Head
bodyX[1]=3; bodyY[1]=4;
bodyX[2]=2; bodyY[2]=4;
randomSeed(analogRead(A5)); // Random seed
generateFood();
}
Main loop
In video games, the loop usually has three phases: Read Inputs -> Update Logic -> Draw (Render).
void loop() {
// 1. READ INPUTS (Joystick)
readJoystick();
// 2. UPDATE LOGIC (Only every 'speed' ms)
if (millis() - prevTime >= speed) {
prevTime = millis();
moveSnake();
checkCollisions();
// 3. DRAW
drawGame();
}
}
Reading the joystick
Here’s an important trick: Avoid suicide. If the snake is going RIGHT, we cannot allow the user to press LEFT, because it would instantly collide with its own neck.
void readJoystick() {
int x = analogRead(pinJoyX);
int y = analogRead(pinJoyY);
// Thresholds to detect direction
// 0:Up, 1:Right, 2:Down, 3:Left
// Note: Orientation depends on how you place the joystick.
// Adjust these 'ifs' according to your setup.
if (y < 200 && direction != 2) direction = 0; // Up (unless going down)
if (y > 800 && direction != 0) direction = 2; // Down
if (x > 800 && direction != 3) direction = 1; // Right
if (x < 200 && direction != 1) direction = 3; // Left
}
Movement logic
Here we apply the array shifting algorithm.
void moveSnake() {
// Save the tail in case the snake eats during this move
previousTailX = bodyX[snakeLength - 1];
previousTailY = bodyY[snakeLength - 1];
// Each segment takes the position of the one in front of it
for (int i = snakeLength - 1; i > 0; i--) {
bodyX[i] = bodyX[i - 1];
bodyY[i] = bodyY[i - 1];
}
// Move the head
if (direction == 0) bodyY[0]--;
if (direction == 1) bodyX[0]++;
if (direction == 2) bodyY[0]++;
if (direction == 3) bodyX[0]--;
}
Collisions and food
We check if we have died or if we have eaten.
void checkCollisions() {
// 1. WALL collision
if (bodyX[0] < 0 || bodyX[0] > 7 || bodyY[0] < 0 || bodyY[0] > 7) {
gameOver();
return;
}
// 2. SELF collision
for (int i = 1; i < snakeLength; i++) {
if (bodyX[0] == bodyX[i] && bodyY[0] == bodyY[i]) {
gameOver();
return;
}
}
// 3. EAT
if (bodyX[0] == foodX && bodyY[0] == foodY) {
if (snakeLength < MAX_LENGTH) {
bodyX[snakeLength] = previousTailX;
bodyY[snakeLength] = previousTailY;
snakeLength++;
}
// Increase speed (difficulty)
if (speed > 100) speed -= 10;
if (snakeLength < MAX_LENGTH) generateFood();
}
}
void generateFood() {
bool occupied;
do {
foodX = random(0, 8);
foodY = random(0, 8);
occupied = false;
for (int i = 0; i < snakeLength; i++) {
if (bodyX[i] == foodX && bodyY[i] == foodY) {
occupied = true;
break;
}
}
} while (occupied);
}
void gameOver() {
// Screen flash
for(int i=0; i<3; i++){
lc.clearDisplay(0);
delay(200);
drawGame();
delay(200);
}
// Reset game
snakeLength = 3;
bodyX[0]=4; bodyY[0]=4;
bodyX[1]=3; bodyY[1]=4;
bodyX[2]=2; bodyY[2]=4;
direction = 1;
speed = 300;
generateFood();
}
Rendering
Finally, we dump the memory state to the LEDs.
void drawGame() {
lc.clearDisplay(0);
// Draw Food
lc.setLed(0, foodY, foodX, true); // (addr, row, col, state)
// Draw Snake
for (int i = 0; i < snakeLength; i++) {
lc.setLed(0, bodyY[i], bodyX[i], true);
}
}
Note that lc.setLed usually takes (row, column). Depending on how you soldered your matrix, your X and Y might be swapped. If the snake moves weirdly (up is left), simply swap the parameters in this function.
Possible improvements
You can add sound when picking up food, show a victory animation when occupying all 64 squares, or connect two matrices to create a 16x8 board.