Snake

This sketch implements the classic Snake game where players control a growing snake using arrow keys to collect food and increase their score. The game ends if the snake hits walls or itself, the food changes color with each bite, and the game accelerates as the score increases until reaching a winning score of 52.

🧪 Try This!

Experiment with the code by making these changes:

  1. Make the game twice as fast — Lowering the speed variable causes move() to be called more frequently, making the snake zip across the screen
  2. Make the snake and food smaller — Changing the size argument of square() shrinks all game objects so more snake can fit on the board
  3. Make the game end at a higher score — Changing the win condition from 52 means players must eat more food to see the victory message
  4. Make the snake start longer — Adding more coordinate pairs to the sp array means the player starts with a longer snake and faces more self-collision risk
  5. Change the snake head color — The head is drawn with a slightly different shade of red; change this to make it stand out more
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch recreates the timeless Snake game, a perfect project for learning how to build interactive games in p5.js. The player steers a pixel snake using arrow keys to eat randomly-placed food pellets. Each pellet eaten grows the snake by one segment and increases the score; collision with walls or the snake's own body ends the game. The mechanics teach essential game-building techniques: the draw loop for continuous animation, keyboard input handling, collision detection, and dynamic difficulty scaling through increasing game speed.

The code is organized around three key ideas: a data structure called `sp` that stores the snake's body segments as coordinates, a `move()` function that updates the snake's position based on input, and a `draw()` function that renders the game state sixty times per second. By reading it, you will learn how arrays hold game objects, how timing with `millis()` controls game speed independent of frame rate, and how simple collision checks create engaging gameplay.

⚙️ How It Works

  1. When the sketch loads, setup() creates a 400×400 canvas and sets rectangle drawing mode to CENTER. The snake begins as a four-segment horizontal line at coordinates [250,200], [225,200], [200,200], and [175,200].
  2. On every frame, draw() clears the background and randomly generates a food color if one hasn't been set yet.
  3. The code checks if the snake's head occupies the same position as a food pellet, and if so, repositions the food to a new random location from the predefined grid.
  4. If enough time has passed (controlled by the `speed` variable and `millis()` timing), move() is called to shift the snake in the direction of the last pressed arrow key.
  5. The move() function loops through the snake array starting from the tail, copying each segment's position to the segment in front of it, then moves the head in the input direction by shifting it 25 pixels.
  6. Collision checks trigger game over if the head exits the canvas boundaries or collides with the snake's own body.
  7. When the head eats food, foodEaten() extends the snake by adding a new segment, the score increases, food color randomizes, and game speed accelerates up to a minimum threshold, then all segments and the food are drawn to the screen.

🎓 Concepts You'll Learn

Game loops and state managementCollision detectionArray data structures for game objectsKeyboard input and event handlingTiming with millis() for frame-rate-independent movementDynamic difficulty scaling

📝 Code Breakdown

setup()

setup() runs exactly once when the sketch starts, making it the place to initialize your canvas and any starting configuration.

function setup() {
  createCanvas(400, 400);
  rectMode(CENTER);
}
Line-by-line explanation (2 lines)
createCanvas(400, 400);
Creates a 400×400 pixel canvas where the game board will be drawn
rectMode(CENTER);
Tells p5.js to draw all rectangles and squares from their center point instead of the top-left corner, making collision math easier

draw()

draw() runs 60 times per second and is where all rendering happens. It checks game conditions, updates the display, and calls move() when enough time has passed. The timing check with millis() is key—it lets you control game speed independently of the frame rate.

🔬 This loop draws every body segment except the head. What happens if you change the loop to start at i = 0 instead of i = 1, so it draws the head twice?

  for (i = 1; i < sp.length; i++) {
    square(sp[i][0], sp[i][1], 25);
  }

🔬 The third argument (25) is the head's size. What happens if you change it to 50 so the head is twice as big as the body?

  square(sp[0][0],sp[0][1],25)
function draw() {
  background(220);
  noStroke();
  if (r == null && g == null && b == null){
    r = round(random(255))
    g = round(random(255))
    b = round(random(255))
  }
  for (i = sp.length-1; i >= 0; i--){
    if (food[0] == sp[i][0] && food[1] == sp[i][1]){
      food=[random(foodRandom), random(foodRandom)]
    }
  }
  fill(255, 0, 0);
  if (millis() - timeInterval > speed){
    timeInterval = millis()
    move()
  }
  //print(milliseconds)
  for (i = 1; i < sp.length; i++) {
    square(sp[i][0], sp[i][1], 25);
  }
  fill(227, 7, 7)
  square(sp[0][0],sp[0][1],25)
  fill(r, g, b);
  square(food[0], food[1], 25);
  push();
  strokeWeight(25);
  stroke(250);
  line(0, 0, 0, height);
  line(0, 0, width, 0);
  line(width, 0, width, height);
  line(0, height, width, height);
  pop();
  push();
  fill(0, 0, 0);
  textAlign(LEFT, TOP);
  textSize(15);
  text("Score: " + score, 15, 15);
  pop();
  if (isdead) {
    push();
    fill(0, 0, 0);
    textAlign(CENTER, CENTER);
    textSize(50);
    text("Game Over", 200, 200);
    pop();
  } 
  if (score >= 52){
    push()
    fill(0, 0, 0);
    textAlign(CENTER, CENTER);
    textSize(50);
    text("You Won", 200, 200);
    pop()
  }
}
Line-by-line explanation (24 lines)

🔧 Subcomponents:

conditional Food Color Initialization if (r == null && g == null && b == null){

Generates random RGB values once for the food's color at the start of the game

for-loop Food-Snake Overlap Prevention for (i = sp.length-1; i >= 0; i--){

Ensures food never spawns on top of the snake's body by checking every segment

conditional Game Speed Timer if (millis() - timeInterval > speed){

Limits move() calls based on elapsed milliseconds, creating consistent game speed independent of frame rate

for-loop Draw Snake Body for (i = 1; i < sp.length; i++) {

Draws all snake body segments except the head in bright red

conditional Game Over Message if (isdead) {

Displays a centered 'Game Over' message when the snake collides with walls or itself

conditional Win Condition Display if (score >= 52){

Displays a centered 'You Won' message when the player reaches the winning score

background(220);
Fills the entire canvas with light gray, clearing the previous frame so the snake doesn't leave a trail
noStroke();
Disables outline strokes for shapes drawn after this line, so the squares appear solid
if (r == null && g == null && b == null){
Checks if the food color has never been set (all RGB values are null)
r = round(random(255))
Generates a random integer between 0 and 255 for the red component of the food's color
for (i = sp.length-1; i >= 0; i--){
Loops through every segment of the snake array, starting from the tail and working backward
if (food[0] == sp[i][0] && food[1] == sp[i][1]){
Checks if the food's x and y coordinates match any snake segment's position
food=[random(foodRandom), random(foodRandom)]
If the food overlaps with the snake, moves it to a new random position from the predefined grid
if (millis() - timeInterval > speed){
Checks if enough milliseconds have passed since the last move; if so, it's time to move the snake
timeInterval = millis()
Updates the timer to the current millisecond value, resetting the countdown until the next move
for (i = 1; i < sp.length; i++) {
Loops through all snake segments except the head (starting at index 1) to draw them
square(sp[i][0], sp[i][1], 25);
Draws a 25×25 pixel square at each body segment's position in bright red (from the previous fill command)
fill(227, 7, 7)
Changes the fill color to a darker red for the next shape drawn
square(sp[0][0],sp[0][1],25)
Draws the snake's head as a 25×25 pixel square at index 0 in the darker red color
fill(r, g, b);
Sets the fill color to the randomly generated food color before drawing the food
square(food[0], food[1], 25);
Draws the food as a 25×25 pixel square at the food's current position
push();
Saves the current drawing state (stroke, fill, and other settings) so changes don't affect later drawing
stroke(250);
Sets the stroke (outline) color to light gray for the border lines
line(0, 0, 0, height);
Draws the left border line from top-left to bottom-left
pop();
Restores the saved drawing state so the border's stroke settings don't affect subsequent shapes
text("Score: " + score, 15, 15);
Draws the current score in the top-left corner by concatenating the string 'Score: ' with the numeric score value
if (isdead) {
Checks if the snake has collided with a wall or itself
text("Game Over", 200, 200);
If the snake is dead, displays 'Game Over' centered on the canvas
if (score >= 52){
Checks if the player has eaten enough food to reach the winning score
text("You Won", 200, 200);
If the score is 52 or higher, displays 'You Won' centered on the canvas

move()

move() is the heart of the game logic. It handles four directional movements by the same pattern: loop through the body backwards copying positions, then move the head. It also checks all three collision types (walls, food, self) and handles the food-eating logic including snake growth and difficulty scaling.

🔬 These lines end the game if the head exits the horizontal canvas boundaries. What happens if you change >= 400 to >= 350, so the playable area becomes smaller?

  if (sp[0][0] >= 400) {
    isdead = true;
  } else if (sp[0][0] <= 0) {
    isdead = true;
  }
function move(){
  if(isdead == false){
  if (lastKeyCode == RIGHT_ARROW){
      for (i = sp.length - 1; i > 0; i--) {
    sp[i][0] = sp[i - 1][0];
    sp[i][1] = sp[i - 1][1];
  }
      sp[0][0] = sp[0][0]+25
    } 
    if (lastKeyCode == LEFT_ARROW){
            for (i = sp.length - 1; i > 0; i--) {
    sp[i][0] = sp[i - 1][0];
    sp[i][1] = sp[i - 1][1];
  }
      sp[0][0] = sp[0][0]-25
    }
    if (lastKeyCode == DOWN_ARROW){
      for (i = sp.length - 1; i > 0; i--) {
    sp[i][0] = sp[i - 1][0];
    sp[i][1] = sp[i - 1][1];
  }
      sp[0][1] = sp[0][1]+25
    }
    if (lastKeyCode == UP_ARROW){
      for (i = sp.length - 1; i > 0; i--) {
    sp[i][0] = sp[i - 1][0];
    sp[i][1] = sp[i - 1][1];
  }
      sp[0][1] = sp[0][1]-25
    }
  }
  if (sp[0][0] >= 400) {
    isdead = true;
  } else if (sp[0][0] <= 0) {
    isdead = true;
  } else if (sp[0][1] >= 400) {
    isdead = true;
  } else if (sp[0][1] <= 0) {
    isdead = true;
  }
  if (sp[0][0] == food[0] && sp[0][1] == food[1]) {
    foodEaten();
    food=[random(foodRandom), random(foodRandom)];
    score += 1;
    r = round(random(255))
    g = round(random(255))
    b = round(random(255))
    if (speed > 75){
    speed -= 5
    }
  }
  for (i = sp.length - 1; i > 0; i--) {
    if (sp[0][0] == sp[i][0] && sp[0][1] == sp[i][1]) {
      isdead = true;
    } 
  }
}
Line-by-line explanation (17 lines)

🔧 Subcomponents:

conditional Move Right if (lastKeyCode == RIGHT_ARROW){

If the player pressed right arrow, shift all body segments forward and move the head 25 pixels right

conditional Move Left if (lastKeyCode == LEFT_ARROW){

If the player pressed left arrow, shift all body segments forward and move the head 25 pixels left

conditional Move Down if (lastKeyCode == DOWN_ARROW){

If the player pressed down arrow, shift all body segments forward and move the head 25 pixels down

conditional Move Up if (lastKeyCode == UP_ARROW){

If the player pressed up arrow, shift all body segments forward and move the head 25 pixels up

conditional Wall Collision Detection if (sp[0][0] >= 400) {

Checks if the head has exited any of the four canvas boundaries and triggers game over

conditional Food Collision Handler if (sp[0][0] == food[0] && sp[0][1] == food[1]) {

Detects when the head reaches the food, extends the snake, and increases difficulty

for-loop Self Collision Detection for (i = sp.length - 1; i > 0; i--) {

Loops through the snake's body segments to check if the head collides with any of them

if(isdead == false){
Only executes movement if the snake is still alive, preventing movement after game over
if (lastKeyCode == RIGHT_ARROW){
Checks if the last key pressed was the right arrow
for (i = sp.length - 1; i > 0; i--) {
Loops backward through the snake from tail (last segment) to the segment right behind the head
sp[i][0] = sp[i - 1][0];
Copies the x-coordinate of the segment in front of the current segment to the current segment
sp[i][1] = sp[i - 1][1];
Copies the y-coordinate of the segment in front to the current segment, shifting the entire body forward
sp[0][0] = sp[0][0]+25
Moves the head 25 pixels to the right by adding 25 to its x-coordinate
if (sp[0][0] >= 400) {
Checks if the head's x-coordinate is at or beyond the right canvas boundary (400 pixels)
isdead = true;
Sets the game over flag to true when the head hits a wall
if (sp[0][0] == food[0] && sp[0][1] == food[1]) {
Checks if the head's coordinates exactly match the food's coordinates
foodEaten();
Calls the function that adds a new segment to the snake's body
food=[random(foodRandom), random(foodRandom)];
Moves the food to a new random position from the predefined grid array
score += 1;
Increases the player's score by 1 for eating the food
r = round(random(255))
Generates a new random red value for the next food color
if (speed > 75){
Checks if the current speed allows further acceleration (hasn't reached the minimum speed threshold)
speed -= 5
Decreases the speed by 5 milliseconds, making the snake move faster each time food is eaten
for (i = sp.length - 1; i > 0; i--) {
Loops through all snake body segments except the head to check for self-collision
if (sp[0][0] == sp[i][0] && sp[0][1] == sp[i][1]) {
Checks if the head's coordinates match any body segment's coordinates

foodEaten()

foodEaten() grows the snake by adding a new segment positioned behind the current tail in the direction the snake was moving. This is why the snake never looks disjointed when it eats food—the new segment appears as a natural extension of the body.

function foodEaten() {
  var dx = sp[sp.length - 2][0] - sp[sp.length - 1][0];
  var dy = sp[sp.length - 2][1] - sp[sp.length - 1][1];
  sp.push([sp[sp.length - 1][0] - dx, sp[sp.length - 1][1] - dy]);
  return;
}
Line-by-line explanation (4 lines)
var dx = sp[sp.length - 2][0] - sp[sp.length - 1][0];
Calculates the horizontal difference between the second-to-last segment and the last segment (the tail's direction)
var dy = sp[sp.length - 2][1] - sp[sp.length - 1][1];
Calculates the vertical difference between the second-to-last segment and the last segment
sp.push([sp[sp.length - 1][0] - dx, sp[sp.length - 1][1] - dy]);
Adds a new segment to the end of the snake array positioned in the same direction the tail was traveling, maintaining body alignment
return;
Exits the function (though this return statement is not strictly necessary)

keyPressed()

keyPressed() is called every time the player presses a key. It stores the last arrow key pressed in lastKeyCode, which move() reads later. The opposite-direction checks prevent instant death by blocking 180-degree turns.

🔬 These lines prevent the snake from turning into itself. What happens if you remove the `&& lastKeyCode != LEFT_ARROW` check, so the snake CAN turn 180 degrees?

  if (keyCode == RIGHT_ARROW && lastKeyCode != LEFT_ARROW) {
    lastKeyCode = RIGHT_ARROW
  } else if (keyCode == LEFT_ARROW && lastKeyCode != RIGHT_ARROW) {
function keyPressed() {
  if (keyCode == RIGHT_ARROW && lastKeyCode != LEFT_ARROW) {
    lastKeyCode = RIGHT_ARROW
  } else if (keyCode == LEFT_ARROW && lastKeyCode != RIGHT_ARROW) {
    lastKeyCode = LEFT_ARROW
  } else if (keyCode == DOWN_ARROW && lastKeyCode != UP_ARROW) {
    lastKeyCode = DOWN_ARROW
  } else if (keyCode == UP_ARROW && lastKeyCode != DOWN_ARROW) {
    lastKeyCode = UP_ARROW
  }
}
Line-by-line explanation (5 lines)

🔧 Subcomponents:

conditional Prevent Reverse Direction if (keyCode == RIGHT_ARROW && lastKeyCode != LEFT_ARROW) {

Prevents the snake from turning 180 degrees into itself by checking that the new direction is not opposite to the current one

if (keyCode == RIGHT_ARROW && lastKeyCode != LEFT_ARROW) {
Checks if the right arrow was pressed AND the snake is not currently moving left (preventing a 180-degree turn)
lastKeyCode = RIGHT_ARROW
Stores the right arrow as the last pressed key, so move() will use this on the next game tick
} else if (keyCode == LEFT_ARROW && lastKeyCode != RIGHT_ARROW) {
Otherwise, checks if left arrow was pressed and the snake is not moving right
} else if (keyCode == DOWN_ARROW && lastKeyCode != UP_ARROW) {
Otherwise, checks if down arrow was pressed and the snake is not moving up
} else if (keyCode == UP_ARROW && lastKeyCode != DOWN_ARROW) {
Otherwise, checks if up arrow was pressed and the snake is not moving down

📦 Key Variables

sp array

Stores the snake's body as an array of coordinate pairs; sp[0] is the head, and each element is [x, y]

var sp = [[250, 200], [225, 200], [200, 200], [175, 200]];
food array

Stores the current food location as a two-element array [x, y]

var food = [325, 200];
foodRandom array

A predefined list of valid grid coordinates (in increments of 25) where food can spawn

var foodRandom = [25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 275, 300, 325, 350, 375];
score number

Tracks how many food pellets the player has eaten; increases by 1 each time food is eaten

var score = 0;
isdead boolean

Tracks whether the snake has collided with a wall or itself; true stops movement and displays game over

var isdead = false;
speed number

Controls game speed in milliseconds; higher values make the snake slower, decreases as score increases up to a minimum of 75ms

var speed = 150;
timeInterval number

Stores the last time millis() was checked to create consistent movement timing independent of frame rate

var timeInterval = 0;
r number

Stores the red component of the food's RGB color (0-255)

var r = null;
g number

Stores the green component of the food's RGB color (0-255)

var g = null;
b number

Stores the blue component of the food's RGB color (0-255)

var b = null;
lastKeyCode number

Stores the key code of the last arrow key pressed, used by move() to determine the snake's direction

var lastKeyCode;

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

STYLE move() function

The four direction conditionals repeat the same body-shifting loop four times, creating redundant code

💡 Extract the body-shifting logic into a helper function that's called once per move, then only change the head position based on direction. This would reduce lines and improve maintainability.

BUG keyPressed()

The lastKeyCode variable is never initialized with a starting direction, so the snake won't move until a key is pressed

💡 Initialize `var lastKeyCode = RIGHT_ARROW;` at the top of the sketch so the snake begins moving right immediately

PERFORMANCE draw() - food overlap check

The food overlap check loops through the entire snake every frame, even when the food is already placed safely

💡 Move this check inside the foodEaten() and food-spawn logic, so it only runs when food is newly placed rather than constantly re-checking

FEATURE Game mechanics

The game allows the snake to move through walls without wrapping to the other side (no toroidal topology)

💡 Add a modulo operation to make the snake wrap around: `if (sp[0][0] > 400) sp[0][0] = 0;` creates a continuous play space

STYLE Variable declarations

Color variables r, g, b are initialized as null and checked with == null throughout the code

💡 Initialize them as regular variables (e.g., `var r = 0;`) and use a separate boolean flag like `var foodColorSet = false;` for clearer intent

🔄 Code Flow

Code flow showing setup, draw, move, foodeaten, keypressed

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> timingcheck[timing-check] timingcheck --> move[move] move --> wallcollision[wall-collision] wallcollision -->|Collision Detected| gameover[game-over-display] wallcollision -->|No Collision| foodcollision[food-collision] foodcollision -->|Food Eaten| foodeaten[foodeaten] foodcollision -->|No Food| selfcollision[self-collision] selfcollision -->|Collision Detected| gameover selfcollision -->|No Collision| rightmove[right-move] selfcollision -->|No Collision| leftmove[left-move] selfcollision -->|No Collision| downmove[down-move] selfcollision -->|No Collision| upmove[up-move] rightmove --> move leftmove --> move downmove --> move upmove --> move draw --> bodyrender[body-render] bodyrender --> draw draw --> wincheck[win-display] wincheck -->|Win Condition Met| win wincheck -->|No Win| draw click setup href "#fn-setup" click draw href "#fn-draw" click move href "#fn-move" click foodeaten href "#fn-foodeaten" click keypressed href "#fn-keypressed" click timingcheck href "#sub-timing-check" click bodyrender href "#sub-body-render" click gameover href "#sub-game-over-display" click wincheck href "#sub-win-display" click rightmove href "#sub-right-move" click leftmove href "#sub-left-move" click downmove href "#sub-down-move" click upmove href "#sub-up-move" click wallcollision href "#sub-wall-collision" click foodcollision href "#sub-food-collision" click selfcollision href "#sub-self-collision" click oppositecheck href "#sub-opposite-check"

❓ Frequently Asked Questions

What visual elements does the Snake sketch create?

The Snake sketch visually represents a snake made of squares that moves around the canvas, along with randomly generated food items represented as colorful squares.

How can users interact with the Snake game?

Users can control the movement of the snake using the arrow keys on their keyboard to guide it towards the food and avoid collisions.

What creative coding concepts are demonstrated in this p5.js sketch?

This sketch demonstrates concepts such as game mechanics, collision detection, and dynamic color generation, showcasing how to create an interactive game environment.

Preview

Snake - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Snake - Code flow showing setup, draw, move, foodeaten, keypressed
Code Flow Diagram