<img alt="javascript snake" data- data-src="https://kirelos.com/wp-content/uploads/2023/10/echo/javascript-snake.jpg/w=800" data- decoding="async" height="420" src="data:image/svg xml,” width=”800″>

In this article, I will explain how to make a Snake game using HTML, CSS, and JavaScript.

We will not use additional libraries; the game will run in a browser. Creating this game is a fun exercise that helps you stretch and exercise your problem-solving muscles.

Project Outline

<img alt="game" data- data-src="https://kirelos.com/wp-content/uploads/2023/10/echo/ben-neale-zpxKdH_xNSI-unsplash.jpg/w=800" data- decoding="async" src="data:image/svg xml,” width=”800″>

Snake is a simple game where you guide the movements of a snake towards food while dodging obstacles. When the snake reaches the food, it eats it and grows longer. As the game progresses, the snake becomes increasingly long.

The snake is not supposed to run into walls or itself. Therefore, as the game progresses, the snake gets longer and becomes increasingly harder to play.

The goal of this JavaScript Snake Tutorial is to build the game below:

The code for the game is available on my GitHub. A live version is hosted on GitHub Pages.

Prerequisites

We will build this project using HTML, CSS, and JavaScript. We are only going to be writing basic HTML and CSS. Our primary focus is on JavaScript. Therefore, you should already understand it to follow along with this JavaScript Snake Tutorial. If not, I highly recommend you check out our article on the best places to learn JavaScript.

You will also need a code editor to write your code in. In addition to that, you will need a browser, which you probably have if you’re reading this.

Setting up the Project

To begin, let’s set up the project files. In an empty folder, create an index.html file and add the following markup.



  
    
    
    
    Snake
  
  
    

Game Over

The markup above creates a basic ‘Game Over’ screen. We will toggle this screen’s visibility using JavaScript. It also defines a canvas element on which we will draw the maze, the snake, and the food. The markup also links the stylesheet and the JavaScript code.

Next, create a styles.css file for the styling. Add the following styles to it.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    font-family: 'Courier New', Courier, monospace;
}

body {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    background-color: #00FFFF;
}

#game-over-screen {
    background-color: #FF00FF;
    width: 500px;
    height: 200px;
    border: 5px solid black;
    position: absolute;
    align-items: center;
    justify-content: center;
    display: none;
}

In the ‘*’ ruleset, we target all elements and reset spacing. We also set the font family for every element and set the sizing of elements to a more predictable sizing method called border-box. For the body, we set its height to the full height of the viewport and aligned all items to the center. We also gave it a blue background color.

Lastly, we styled the ‘Game Over’ screen to give it a height and width of 200 and 500 pixels, respectively. We also gave it a magenta background color and a black border. We set its position to absolute so that it is outside the normal document flow and aligned to the center of the screen. Then, we centered its content. We set its display to none, so it is hidden by default.

Next, create a snake.js file, which we will write over the next few sections.

Creating Global Variables

The next step in this JavaScript Snake tutorial is to define some global variables we will use. In the snake.js file, add the following variable definitions at the top:

// Creating references to HTML elements
let gameOverScreen = document.getElementById("game-over-screen");
let canvas = document.getElementById("canvas");

// Creating context which will be used to draw on canvas
let ctx = canvas.getContext("2d");

These variables store references to the ‘Game Over’ screen and the canvas elements. Next, we created a context, which will be used to draw on the canvas.

Next, add these variable definitions beneath the first set.

// Maze definitions
let gridSize = 400;
let unitLength = 10;

The first one defines the size of the grid in pixels. The second defines a unit length in the game. This unit length is going to be used in several places. For example, we will use it to define how thick the maze walls are, how thick the snake is, the height and width of the food, and the increments in which the snake moves.

Next, add the following gameplay variables. These variables are used to keep track of the state of the game.

// Game play variables
let snake = [];
let foodPosition = { x: 0, y: 0 };
let direction = "right";
let collided = false;

The snake variable keeps track of the positions currently occupied by the snake. The snake comprises units, and each unit occupies a position on the canvas. The position each unit occupies is stored in the snake array. The position will have x and y values as its coordinates. The first element in the array represents the tail, while the last represents the head.

As the snake moves, we will push elements to the end of the array. This will move the head forward. We will also remove the first element or tail from the array so that the length stays the same.

The food position variable stores the current location of food using x and y coordinates. The direction variable stores the direction the snake is moving, while the collided variable is a boolean variable flagged to true when a collision has been detected.

Declaring Functions

The entire game is broken down into functions, making it easier to write and manage. In this section, we will declare those functions and their purposes. The following sections will define the functions and discuss their algorithms.

function setUp() {}
function doesSnakeOccupyPosition(x, y) {}
function checkForCollision() {}
function generateFood() {}
function move() {}
function turn(newDirection) {}
function onKeyDown(e) {}
function gameLoop() {}

Briefly, the setUp function sets the game up. The checkForCollision function checks if the snake has collided with a wall or itself. The doesSnakeOccupyPosition function takes a position, defined by x and y coordinates, and checks if any part of the snake’s body is in that position. This will be useful when looking for a free position to add food to.

The move function moves the snake in whatever direction it points, while the turn function changes that direction. Next, the onKeyDown function will listen for key presses that are used to change direction. The gameLoop function will move the snake and check for collisions.

Defining the Functions

In this section, we will define the functions we declared earlier. We will also discuss how each function works. There will be a brief description of the function before the code and comments to explain line by line where necessary.

setUp function

The setup function will do 3 things:

  1. Draw the maze borders on the canvas.
  2. Set up the snake by adding its positions to the snake variable and drawing it to the canvas.
  3. Generate the initial food position.

Therefore, the code for that will look like this:

  // Drawing borders on canvas
  // The canvas will be the size of the grid plus thickness of the two side border
  canvasSideLength = gridSize   unitLength * 2;

  // We draw a black square that covers the entire canvas
  ctx.fillRect(0, 0, canvasSideLength, canvasSideLength);

  // We erase the center of the black to create the game space
  // This leaves a black outline for the that represents the border
  ctx.clearRect(unitLength, unitLength, gridSize, gridSize);

  // Next, we will store the initial positions of the snake's head and tail
  // The initial length of the snake will be 60px or 6 units

  // The head of the snake will be 30 px or 3 units ahead of the midpoint
  const headPosition = Math.floor(gridSize / 2)   30;

  // The tail of the snake will be 30 px or 3 units behind the midpoint
  const tailPosition = Math.floor(gridSize / 2) - 30;

  // Loop from tail to head in unitLength increments
  for (let i = tailPosition; i <= headPosition; i  = unitLength) {

    // Store the position of the snake's body and drawing on the canvas
    snake.push({ x: i, y: Math.floor(gridSize / 2) });

    // Draw a rectangle at that position of unitLength * unitLength
    ctx.fillRect(x, y, unitLength, unitLength);
  }

  // Generate food
  generateFood();

doesSnakeOccupyPosition

This function takes in x and y coordinates as a position. It then checks such a position exists in the snake’s body. It uses the JavaScript array find method to find a position with matching coordinates.

function doesSnakeOccupyPosition(x, y) {
  return !!snake.find((position) => {
    return position.x == x && y == foodPosition.y;
  });
}

checkForCollision

This function checks if the snake has collided with anything and sets the collided variable to true. We will start by checking for collisions against the left and right walls, the top and bottom walls, and then against the snake itself.

To check for collisions against the left and right walls, we check if the x coordinate of the snake’s head is greater than the gridSize or less than 0. To check for collisions against the top and bottom walls, we will perform the same check but with y-coordinates.

Next, we will check for collisions against the snake itself; we will check if any other part of its body occupies the position currently occupied by the head. Combining all this, the body for the checkForCllision function should look like this:

 function checkForCollision() {
  const headPosition = snake.slice(-1)[0];
  // Check for collisions against left and right walls
  if (headPosition.x = gridSize - 1) {
    collided = true;
  }

  // Check for collisions against top and bottom walls
  if (headPosition.y = gridSize - 1) {
    collided = true;
  }

  // Check for collisions against the snake itself
  const body = snake.slice(0, -2);
  if (
    body.find(
      (position) => position.x == headPosition.x && position.y == headPosition.y
    )
  ) {
    collided = true;
  }
}

generateFood

The generateFood function uses a do-while loop to look for a position to place food not occupied by the snake. Once found, the food position is recorded and drawn on the canvas. The code for the generateFood function should look like this:

function generateFood() {
  let x = 0,
    y = 0;
  do {
    x = Math.floor((Math.random() * gridSize) / 10) * 10;
    y = Math.floor((Math.random() * gridSize) / 10) * 10;
  } while (doesSnakeOccupyPosition(x, y));

  foodPosition = { x, y };
  ctx.fillRect(x, y, unitLength, unitLength);
}

move

The move function starts by creating a copy of the position of the head of the snake. Then, based on the current direction, it increases or decreases the value of the x or y coordinate of the snake. For example, increasing the x coordinate is equivalent to moving to the right.

Once that has been done, we push the new headPosition to the snake array. We also draw the new headPosition to the canvas.

Next, we check if the snake has eaten food in that move. We do this by checking if the headPosition is equal to the foodPosition. If the snake has eaten food, we call the generateFood function.

If the snake hasn’t eaten food, we delete the first element of the snake array. This element represents the tail, and removing it will keep the snake’s length the same while giving the illusion of movement.

function move() {
  // Create a copy of the object representing the position of the head
  const headPosition = Object.assign({}, snake.slice(-1)[0]);

  switch (direction) {
    case "left":
      headPosition.x -= unitLength;
      break;
    case "right":
      headPosition.x  = unitLength;
      break;
    case "up":
      headPosition.y -= unitLength;
      break;
    case "down":
      headPosition.y  = unitLength;
  }

  // Add the new headPosition to the array
  snake.push(headPosition);

  ctx.fillRect(headPosition.x, headPosition.y, unitLength, unitLength);

  // Check if snake is eating
  const isEating =
    foodPosition.x == headPosition.x && foodPosition.y == headPosition.y;

  if (isEating) {
    // Generate new food position
    generateFood();
  } else {
    // Remove the tail if the snake is not eating
    tailPosition = snake.shift();

    // Remove tail from grid
    ctx.clearRect(tailPosition.x, tailPosition.y, unitLength, unitLength);
  }
}

turn

The last major function we will cover is the turn function. This function will take a new direction and change the direction variable to that new direction. However, the snake can only turn in a direction perpendicular to the one they are currently moving in.

Therefore, the snake can only turn left or right if moving upwards or downwards. Conversely, it can only turn up or down if moving left or right. With those constraints in mind, the turn function looks like this:

function turn(newDirection) {
  switch (newDirection) {
    case "left":
    case "right":
      // Only allow turning left or right if they were originally moving up or down
      if (direction == "up" || direction == "down") {
        direction = newDirection;
      }
      break;
    case "up":
    case "down":
      // Only allow turning up or down if they were originally moving left or right
      if (direction == "left" || direction == "right") {
        direction = newDirection;
      }
      break;
  }
}

onKeyDown

The onKeyDown function is an event handler that will call the turn function with the direction corresponding to the arrow key that has been pressed. The function, therefore, looks like this:

function onKeyDown(e) {
  switch (e.key) {
    case "ArrowDown":
      turn("down");
      break;
    case "ArrowUp":
      turn("up");
      break;
    case "ArrowLeft":
      turn("left");
      break;
    case "ArrowRight":
      turn("right");
      break;
  }
}

gameLoop

The gameLoop function will be called regularly to keep the game running. This function will call the move function and the checkForCollision function. It also checks if the collision is true. If so, it stops an interval timer we use to run the game and displays the ‘game over’ screen. The function will look like this:

function gameLoop() {
  move();
  checkForCollision();

  if (collided) {
    clearInterval(timer);
    gameOverScreen.style.display = "flex";
  }
}

Starting the Game

To start the game, add the following lines of code:

setUp();
document.addEventListener("keydown", onKeyDown);
let timer = setInterval(gameLoop, 200);

First, we call the setUp function. Next, we add the ‘keydown’ event listener. Lastly, we use the setInterval function to start the timer.

Conclusion

At this point, your JavaScript file should look like the one on my GitHub. In case something does not work, double-check with the repo. Next, you may want to learn how to create an image slider in JavaScript.