How to build Snake using only JavaScript, HTML & CSS: Think like a Developer

How to build Snake using only JavaScript, HTML & CSS: Think like a Developer

How to build Snake using only JavaScript, HTML & CSS: Think like a Developer

Welcome on board. Today we will embark on an exciting adventure, where we will be making our very own snake game. You’ll learn how to work through a problem by breaking it down into smaller simpler steps. By the end of this journey, you will have learned some new things, and you’ll feel confident to explore more on your own.

Getting Started

Let’s begin by creating a file “snake.html” that will contain all our code.

Since this is an HTML file, the first thing we need is the <!DOCTYPE>declaration . In snake.html type the following:

<!DOCTYPE html>
<html>
  <head>
   <title>Snake Game</title>
  </head>
  
  <body>
    Welcome to Snake!
  </body>
</html>

Great, now go ahead and open snake.html in your preferred browser. You should be able to see Welcome to Snake!

We are off to a good start

Creating the Canvas
To be able to create our game, we have to make use of HTML <canvas> . This is what is used to draw graphics using JavaScript.

Replace the welcome message in snake.html with the following:

<canvas id="gameCanvas" width="300" height="300"><canvas>

The id is what identifies the canvas and should always be specified. We will use it to access the canvas later. The width and height are the dimensions of the canvas, and should also be specified. In this case, 300 x 300 pixels.

Your snake.html file should now look like this.

<!DOCTYPE html>
<html>
  <head>
   <title>Snake Game</title>
  </head>
  <body>
    <canvas id="gameCanvas" width="300" height="300"></canvas>
  </body>
</html>

If you refresh your browser page where you previously opened snake.htmlyou will now see a blank page. This is because, by default, the canvas is empty and has no background. Lets fix that.

Give the canvas a background colour and a border

To make our canvas visible, we can give it a border by writing some JavaScript code. To do that, we need to insert <script></script> tags after the </canvas>, where all our JavaScript code will go.

If you put the <script> tag before the <canvas> your code won’t work, as the HTML will not be loaded.

We can now write some JavaScript code, between the enclosing<script></script> tags. Update your code as below.

<!DOCTYPE html>
<html>
  <head>
   <title>Snake Game</title>
  </head>
  <body>
    <canvas id="gameCanvas" width="300" height="300"></canvas>
    
    <script>
      /** CONSTANTS **/
      const CANVAS_BORDER_COLOUR = 'black';
      const CANVAS_BACKGROUND_COLOUR = "white";
      
      // Get the canvas element
      var gameCanvas = document.getElementById("gameCanvas");
      // Return a two dimensional drawing context
      var ctx = gameCanvas.getContext("2d");
      //  Select the colour to fill the canvas
      ctx.fillStyle = CANVAS_BACKGROUND_COLOUR;
      //  Select the colour for the border of the canvas
      ctx.strokestyle = CANVAS_BORDER_COLOUR;
      // Draw a "filled" rectangle to cover the entire canvas
      ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
      // Draw a "border" around the entire canvas
      ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
    </script>
  </body>
</html>

First we get the canvas element using the id (gameCanvas) we specified earlier. We then get the canvas “2d” context, which means we will be drawing into 2D space.

Finally we draw a 300 x 300 white rectangle with a black border. This covers the entire canvas, starting from the top left corner (0, 0).

If you reload snake.html in your browser, you should see a white box with a black border! Good job, we have a canvas that we can use to create our snake game!  On to the next challenge!

Representing our snake

For our snake game to work, we need to know the location of the snake on the canvas. To do that, we can represent the snake as an array of coordinates. Thus, to create a horizontal snake in the middle of the canvas (150, 150) we can write the following:

let snake = [
  {x: 150, y: 150},
  {x: 140, y: 150},
  {x: 130, y: 150},
  {x: 120, y: 150},
  {x: 110, y: 150},
];

Notice that the y coordinate for all parts is always 150. The x coordinate of each part is -10px (to the left) of the previous part. The first pair of coordinates in the array {x: 150, y: 150} represents the head at the very right of the snake.

This will become clearer when we draw the snake in the next section.

Creating and drawing our snake

To display the snake on the canvas, we can write a function to draw a rectangle for each pair of coordinates.

function drawSnakePart(snakePart) {
  ctx.fillStyle = 'lightgreen';
  ctx.strokestyle = 'darkgreen';
ctx.fillRect(snakePart.x, snakePart.y, 10, 10);
  ctx.strokeRect(snakePart.x, snakePart.y, 10, 10);
}

Next we can create another function that prints the parts on the canvas.

function drawSnake() {
  snake.forEach(drawSnakePart);
}

Our snake.html file should now look like this:

<!DOCTYPE html>
<html>
  <head>
   <title>Snake Game</title>
  </head>
  <body>
    <canvas id="gameCanvas" width="300" height="300"></canvas>
    
    <script>
      /** CONSTANTS **/
      const CANVAS_BORDER_COLOUR = 'black';
      const CANVAS_BACKGROUND_COLOUR = "white";
      const SNAKE_COLOUR = 'lightgreen';
      const SNAKE_BORDER_COLOUR = 'darkgreen';
      
      let snake = [
        {x: 150, y: 150},
        {x: 140, y: 150},
        {x: 130, y: 150},
        {x: 120, y: 150},
        {x: 110, y: 150}
      ]
      
      // Get the canvas element
      var gameCanvas = document.getElementById("gameCanvas");
      // Return a two dimensional drawing context
      var ctx = gameCanvas.getContext("2d");
      //  Select the colour to fill the canvas
      ctx.fillStyle = CANVAS_BACKGROUND_COLOUR;
      //  Select the colour for the border of the canvas
      ctx.strokestyle = CANVAS_BORDER_COLOUR;
      // Draw a "filled" rectangle to cover the entire canvas
      ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
      // Draw a "border" around the entire canvas
      ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
      drawSnake();
      
      /**
       * Draws the snake on the canvas
       */
      function drawSnake() {
        // loop through the snake parts drawing each part on the canvas
        snake.forEach(drawSnakePart)
      }
      /**
       * Draws a part of the snake on the canvas
       * @param { object } snakePart - The coordinates where the part should be drawn
       */
      function drawSnakePart(snakePart) {
        // Set the colour of the snake part
        ctx.fillStyle = SNAKE_COLOUR;
        // Set the border colour of the snake part
        ctx.strokestyle = SNAKE_BORDER_COLOUR;
        // Draw a "filled" rectangle to represent the snake part at the coordinates
        // the part is located
        ctx.fillRect(snakePart.x, snakePart.y, 10, 10);
        // Draw a border around the snake part
        ctx.strokeRect(snakePart.x, snakePart.y, 10, 10);
      }
    </script>
  </body>
</html>

If you refresh your browser page now you will see a green snake in the middle of the canvas. Awesome! 

Enabling the snake to move horizontally

Next we want to give the snake the ability to move. But how do we do that?

Well, to make the snake move one step (10px) to the right, we can increase the x-coordinate of every part of the snake by 10px (dx = +10px). To make the snake move to the left, we can decrease the x-coordinate of every part of the snake by 10px (dx = -10).

dx is the horizontal velocity of the snake.

Creating a snake that has moved 10px to the right should then look like this

Create a function called advanceSnake that we will use to update the snake.

function advanceSnake() {
  const head = {x: snake[0].x + dx, y: snake[0].y};
snake.unshift(head);
snake.pop();
}

First we create a new head for the snake. We then add the new head to the beginning of snake using unshift and remove the last element of snake using pop. This way all the other snake parts shift into place as shown above.

Boom, you are getting the hang of this.

Enabling the snake to move vertically

To move our snake up and down, we can’t alter all y-coordinates by 10px. That would shift the whole snake up and down.

Instead we can alter the y-coordinate of the head. Decreasing it by 10px to move the snake down, and increasing it by 10px to move the snake up. This will make the snake move correctly.

Also Read:- How to Create Multiplication Table Using JavaScript

Luckily, because of the way we wrote the advanceSnake function, this is very easy to do. Inside advanceSnake, update the head to also increase the y-coordinate of the head by dy.

const head = {x: snake[0].x + dx, y: snake[0].y + dy};

To test how our advanceSnake function works, we can temporarily call it before the drawSnake function.

// Move on step to the right
advanceSnake()
// Change vertical velocity to 0
dx = 0;
// Change horizontal velocity to 10
dy = -10;
// Move one step up
advanceSnake();
// Draw snake on the canvas
drawSnake();

This is how our snake.html file looks so far.

<!DOCTYPE html>
<html>
  <head>
   <title>Snake Game</title>
  </head>
  <body>
    <canvas id="gameCanvas" width="300" height="300"></canvas>
    
    <script>
      /** CONSTANTS **/
      const CANVAS_BORDER_COLOUR = 'black';
      const CANVAS_BACKGROUND_COLOUR = "white";
      const SNAKE_COLOUR = 'lightgreen';
      const SNAKE_BORDER_COLOUR = 'darkgreen';
      
      let snake = [
        {x: 150, y: 150},
        {x: 140, y: 150},
        {x: 130, y: 150},
        {x: 120, y: 150},
        {x: 110, y: 150}
      ]
      
      // Horizontal velocity
      let dx = 10;
      // Vertical velocity
      let dy = 0;
      
      // Get the canvas element
      var gameCanvas = document.getElementById("gameCanvas");
      // Return a two dimensional drawing context
      var ctx = gameCanvas.getContext("2d");
      //  Select the colour to fill the canvas
      ctx.fillStyle = CANVAS_BACKGROUND_COLOUR;
      //  Select the colour for the border of the canvas
      ctx.strokestyle = CANVAS_BORDER_COLOUR;
      // Draw a "filled" rectangle to cover the entire canvas
      ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
      // Draw a "border" around the entire canvas
      ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
      
      // Move on step to the right
      advanceSnake()
      // Change vertical velocity to 0
      dx = 0;
      // Change horizontal velocity to 10
      dy = -10;
      // Move one step up
      advanceSnake();
      // Draw snake on the canvas
      drawSnake();
      
      /**
       * Advances the snake by changing the x-coordinates of its parts
       * according to the horizontal velocity and the y-coordinates of its parts
       * according to the vertical veolocity
       */
      function advanceSnake() {
        const head = {x: snake[0].x + dx, y: snake[0].y + dy};
        snake.unshift(head);
        snake.pop();
      }
      
      /**
       * Draws the snake on the canvas
       */
      function drawSnake() {
        // loop through the snake parts drawing each part on the canvas
        snake.forEach(drawSnakePart)
      }
      /**
       * Draws a part of the snake on the canvas
       * @param { object } snakePart - The coordinates where the part should be drawn
       */
      function drawSnakePart(snakePart) {
        // Set the colour of the snake part
        ctx.fillStyle = SNAKE_COLOUR;
        // Set the border colour of the snake part
        ctx.strokestyle = SNAKE_BORDER_COLOUR;
        // Draw a "filled" rectangle to represent the snake part at the coordinates
        // the part is located
        ctx.fillRect(snakePart.x, snakePart.y, 10, 10);
        // Draw a border around the snake part
        ctx.strokeRect(snakePart.x, snakePart.y, 10, 10);
      }
    </script>
  </body>
</html>

Refreshing the browser, we can see that our snake has moved. Success!

Refactoring our code

Before we move on, let’s do some refactoring and move the code that draws the canvas inside a function. This will help us in the next section.

“Code refactoring is the process of restructuring existing computer code, without changing its external behaviour.”

function clearCanvas() {
  ctx.fillStyle = "white";
  ctx.strokeStyle = "black";
ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
  ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
}

We are making great strides! 

Making our snake move automatically

Okay, now that we have successfully refactored our code, we can make our snake move automatically.

Earlier, to test that our advanceSnake function worked, we called it twice. Once to make the snake move to the right, and once to make the snake move up.

Thus if we wanted to make the snake move five steps to the right we would call advanceSnake() five times in a row.

clearCanvas();
advanceSnake();
advanceSnake();
advanceSnake();
advanceSnake();
advanceSnake();
drawSnake();

But, calling it five times in a row as shown above, will make the snake jump 50px forward.

Instead we want to make the snake appear to be moving forward step by step.

To do that, we can add a slight delay between each call, using setTimeout. We also need to make sure to call drawSnake every time we call advanceSnake. If we don’t, we won’t be able to see the intermediate steps that show the snake moving.

Notice how we also call clearCanvas() inside each setTimeout. This is to remove all the previous positions of the snake that would leave a trail behind.

Also Read:- How to Create Multiplication Table Using JavaScript

Although, there is a problem with the above code. There is nothing here to tell the program that it has to wait for setTimeout before it moves to the next setTimeout. This means that the snake will still jump 50px forward but after a slight delay.

To fix that, we have to wrap our code inside functions, calling one function at a time.

stepOne();
    
function stepOne() {
  setTimeout(function onTick() {
    clearCanvas();
    advanceSnake();
    drawSnake();
   // Call the second function
   stepTwo();
  }, 100)
}
function stepTwo() {
  setTimeout(function onTick() {
    clearCanvas();
    advanceSnake();
    drawSnake();
    // Call the third function
    stepThree();
  }, 100)
}

How do we make our snake keep moving? Instead of creating an infinite number of functions that call each other, we can instead create one function main and call it over and over again.

function main() {
  setTimeout(function onTick() {
    clearCanvas();
    advanceSnake();
    drawSnake();
// Call main again
    main();
  }, 100)
}

Voilà! We now have a snake that will keep moving to the right. Although, once it reaches the end of the canvas, it continues its infinite journey into the unknown. We will fix that in due time, patience young padawan.

Changing the snake’s direction

Our next task is to change the snake’s direction when one of the arrow keys is pressed. Add the following code after the drawSnakePartfunction.

function changeDirection(event) {
  const LEFT_KEY = 37;
  const RIGHT_KEY = 39;
  const UP_KEY = 38;
  const DOWN_KEY = 40;
const keyPressed = event.keyCode;
  const goingUp = dy === -10;
  const goingDown = dy === 10;
  const goingRight = dx === 10;
  const goingLeft = dx === -10;
if (keyPressed === LEFT_KEY && !goingRight) {
    dx = -10;
    dy = 0;
  }
if (keyPressed === UP_KEY && !goingDown) {
    dx = 0;
    dy = -10;
  }
if (keyPressed === RIGHT_KEY && !goingLeft) {
    dx = 10;
    dy = 0;
  }
if (keyPressed === DOWN_KEY && !goingDown) {
    dx = 0;
    dy = 10;
  }
}

There is nothing tricky going on here. We check if the key pressed matches one of the arrow keys. If it does, we change the vertical and horizontal velocity as described earlier.

Notice that we also check if the snake is moving in the opposite direction of the new intended direction. This is to prevent our snake from reversing, for example when you press the right arrow key when the snake is moving to the left.

To connect changeDirection to our game, we can use addEventListener on the document to ‘listen’ for when a key is pressed. Then we can call changeDirection with the keydown event. Add the following code after the main function.

document.addEventListener("keydown", changeDirection)

You should now be able to change the snake’s direction using the four arrow keys. Great work, you are on fire!

Next lets see how we can generate food and grow our snake.

Generating food for the snake

For our snake food, we have to generate a random set of coordinates. We can use a helper function randomTen to produce two numbers. One for the x-coordinate and one for the y-coordinate.

We also have to make sure that the food is not located where the snake currently is. If it is, we have to generate a new food location.

function randomTen(min, max) {
  return Math.round((Math.random() * (max-min) + min) / 10) * 10;
}
function createFood() {
  foodX = randomTen(0, gameCanvas.width - 10);
  foodY = randomTen(0, gameCanvas.height - 10);
snake.forEach(function isFoodOnSnake(part) {
    const foodIsOnSnake = part.x == foodX && part.y == foodY
    if (foodIsOnSnake)
      createFood();
  });
}

We then have to create a function to draw the food on the canvas.

function drawFood() {
 ctx.fillStyle = 'red';
 ctx.strokestyle = 'darkred';
 ctx.fillRect(foodX, foodY, 10, 10);
 ctx.strokeRect(foodX, foodY, 10, 10);
}

Finally we can call createFood before calling main. Don’t forget to also update main to use the drawFood function.

function main() {
  setTimeout(function onTick() {
    clearCanvas();
    drawFood()
    advanceSnake();
    drawSnake();
main();
  }, 100)
}

Growing the snake

Growing our snake is simple. We can update our advanceSnake function to check if the head of the snake is touching the food. If it is we can skip removing the last part of the snake and create a new food location.

function advanceSnake() {
  const head = {x: snake[0].x + dx, y: snake[0].y};
snake.unshift(head);
const didEatFood = snake[0].x === foodX && snake[0].y === foodY;
  if (didEatFood) {
    createFood();
  } else {
    snake.pop();
  }
}

Keeping track of the score

To make the game more enjoyable for the player, we can also add a score that increases when the snake eats food.

Create a new variable score and set it to 0 after the snake declaration.

let score = 0;

Next add a new div with an id “score” before the canvas. We can use this to display the score.

<div id="score">0</div>
<canvas id="gameCanvas" width="300" height="300"></canvas>

Finally update advanceSnake to increase and display the score when the snake eats the food.

function advanceSnake() {
  ...
if (didEatFood) {
    score += 10;
    document.getElementById('score').innerHTML = score;
createFood();
  } else {
    ...
  }
}

Pheew, that was quite a lot, but we have come a long way

End the game

There is one final piece left, and that is to end the game. To do that we can create a function didGameEnd that returns true when the game has ended or false otherwise.

function didGameEnd() {
  for (let i = 4; i < snake.length; i++) {
    const didCollide = snake[i].x === snake[0].x &&
      snake[i].y === snake[0].y
if (didCollide) return true
  }
const hitLeftWall = snake[0].x < 0;
  const hitRightWall = snake[0].x > gameCanvas.width - 10;
  const hitToptWall = snake[0].y < 0;
  const hitBottomWall = snake[0].y > gameCanvas.height - 10;
return hitLeftWall || 
         hitRightWall || 
         hitToptWall ||
         hitBottomWall
}

First we check if the snake’s head touches another part of the snake and return true if it does.

Notice that we start our loop from index 4. There are two reasons for that. The first is that didCollide would immediately evaluate to true if the index was 0, so the game would end. The second is that, it is impossible for the first three parts to touch each other.

Next we check if the snake hit any of the canvas walls and return true if it did, otherwise we return false.

Now we can return early in our main function if didEndGame returns true, thus ending the game.

function main() {
  if (didGameEnd()) return;
...
}

Our snake.html should now look like this:

<!DOCTYPE html>
<html>
  <head>
   <title>Snake Game</title>
  </head>
  <body>
    <div id="score">0</div>
    <canvas id="gameCanvas" width="300" height="300"></canvas>

    <script>
      /** CONSTANTS **/
      const CANVAS_BORDER_COLOUR = 'black';
      const CANVAS_BACKGROUND_COLOUR = "white";
      const SNAKE_COLOUR = 'lightgreen';
      const SNAKE_BORDER_COLOUR = 'darkgreen';
      const FOOD_COLOUR = 'red';
      const FOOD_BORDER_COLOUR = 'darkred';
      let snake = [
        {x: 150, y: 150},
        {x: 140, y: 150},
        {x: 130, y: 150},
        {x: 120, y: 150},
        {x: 110, y: 150}
      ]
      // The user's score
      let score = 0;
      // Horizontal velocity
      let dx = 10;
      // Vertical velocity
      let dy = 0;
      // Get the canvas element
      var gameCanvas = document.getElementById("gameCanvas");
      // Return a two dimensional drawing context
      var ctx = gameCanvas.getContext("2d");
      //  Select the colour to fill the canvas
      ctx.fillStyle = CANVAS_BACKGROUND_COLOUR;
      //  Select the colour for the border of the canvas
      ctx.strokestyle = CANVAS_BORDER_COLOUR;
      // Draw a "filled" rectangle to cover the entire canvas
      ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
      // Draw a "border" around the entire canvas
      ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
      // Start game
      main();
      // Create the first food location
      createFood();
      // Call changeDirection whenever a key is pressed
      document.addEventListener("keydown", changeDirection);
      function main() {
        if (didGameEnd()) return;
        setTimeout(function onTick() {
          clearCanvas();
          drawFood();
          advanceSnake();
          drawSnake();
          // Call main again
          main();
        }, 100)
      }
      /**
       * Change the background colour of the canvas to CANVAS_BACKGROUND_COLOUR and
       * draw a border around it
       */
      function clearCanvas() {
        //  Select the colour to fill the drawing
        ctx.fillStyle = CANVAS_BACKGROUND_COLOUR;
        //  Select the colour for the border of the canvas
        ctx.strokestyle = CANVAS_BORDER_COLOUR;
        // Draw a "filled" rectangle to cover the entire canvas
        ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
        // Draw a "border" around the entire canvas
        ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
      }
      /**
       * Returns true if the head of the snake touched another part of the game
       * or any of the walls
       */
      function didGameEnd() {
        for (let i = 4; i < snake.length; i++) {
          const didCollide = snake[i].x === snake[0].x && snake[i].y === snake[0].y
          if (didCollide) return true
        }
        const hitLeftWall = snake[0].x < 0;
        const hitRightWall = snake[0].x > gameCanvas.width - 10;
        const hitToptWall = snake[0].y < 0;
        const hitBottomWall = snake[0].y > gameCanvas.height - 10;
        return hitLeftWall || hitRightWall || hitToptWall || hitBottomWall
      }
      /**
       * Draw the food on the canvas
       */
      function drawFood() {
        ctx.fillStyle = FOOD_COLOUR;
        ctx.strokestyle = FOOD_BORDER_COLOUR;
        ctx.fillRect(foodX, foodY, 10, 10);
        ctx.strokeRect(foodX, foodY, 10, 10);
      }
      /**
       * Advances the snake by changing the x-coordinates of its parts
       * according to the horizontal velocity and the y-coordinates of its parts
       * according to the vertical veolocity
       */
      function advanceSnake() {
        // Create the new Snake's head
        const head = {x: snake[0].x + dx, y: snake[0].y + dy};
        // Add the new head to the beginning of snake body
        snake.unshift(head);
        const didEatFood = snake[0].x === foodX && snake[0].y === foodY;
        if (didEatFood) {
          // Increase score
          score += 10;
          // Display score on screen
          document.getElementById('score').innerHTML = score;
          // Generate new food location
          createFood();
        } else {
          // Remove the last part of snake body
          snake.pop();
        }
      }
      /**
       * Generates a random number that is a multiple of 10 given a minumum
       * and a maximum number
       * @param { number } min - The minimum number the random number can be
       * @param { number } max - The maximum number the random number can be
       */
      function randomTen(min, max) {
        return Math.round((Math.random() * (max-min) + min) / 10) * 10;
      }
      /**
       * Creates random set of coordinates for the snake food.
       */
      function createFood() {
        // Generate a random number the food x-coordinate
        foodX = randomTen(0, gameCanvas.width - 10);
        // Generate a random number for the food y-coordinate
        foodY = randomTen(0, gameCanvas.height - 10);
        // if the new food location is where the snake currently is, generate a new food location
        snake.forEach(function isOnSnake(part) {
          if (part.x == foodX && part.y == foodY) createFood();
        });
      }
      /**
       * Draws the snake on the canvas
       */
      function drawSnake() {
        // loop through the snake parts drawing each part on the canvas
        snake.forEach(drawSnakePart)
      }
      /**
       * Draws a part of the snake on the canvas
       * @param { object } snakePart - The coordinates where the part should be drawn
       */
      function drawSnakePart(snakePart) {
        // Set the colour of the snake part
        ctx.fillStyle = SNAKE_COLOUR;
        // Set the border colour of the snake part
        ctx.strokestyle = SNAKE_BORDER_COLOUR;
        // Draw a "filled" rectangle to represent the snake part at the coordinates
        // the part is located
        ctx.fillRect(snakePart.x, snakePart.y, 10, 10);
        // Draw a border around the snake part
        ctx.strokeRect(snakePart.x, snakePart.y, 10, 10);
      }
      /**
       * Changes the vertical and horizontal velocity of the snake according to the
       * key that was pressed.
       * The direction cannot be switched to the opposite direction, to prevent the snake
       * from reversing
       * For example if the the direction is 'right' it cannot become 'left'
       * @param { object } event - The keydown event
       */
      function changeDirection(event) {
        const LEFT_KEY = 37;
        const RIGHT_KEY = 39;
        const UP_KEY = 38;
        const DOWN_KEY = 40;
        const keyPressed = event.keyCode;
        const goingUp = dy === -10;
        const goingDown = dy === 10;
        const goingRight = dx === 10;
        const goingLeft = dx === -10;
        if (keyPressed === LEFT_KEY && !goingRight) {
          dx = -10;
          dy = 0;
        }
        if (keyPressed === UP_KEY && !goingDown) {
          dx = 0;
          dy = -10;
        }
        if (keyPressed === RIGHT_KEY && !goingLeft) {
          dx = 10;
          dy = 0;
        }
        if (keyPressed === DOWN_KEY && !goingUp) {
          dx = 0;
          dy = 10;
        }
      }
    </script>
  </body>
</html>

You now have a functioning snake game that you can play and share with your friends. But before celebrating lets look at one final problem. This will be the last one, I promise.

Sneaky bugs

If you play the game enough times, you might notice that sometimes the game ends unexpectedly. This is a very good example on how bugs can sneak into our programs and cause trouble.

When a bug occurs, the best way to solve it is to first have a reliable way of reproducing it. That is, come up with the precise steps that lead to the unexpected behaviour. Then you need to understand why they cause the unexpected behaviour and then come up with a solution.

Reproducing the bug

In our case, the steps taken to reproduce the bug are as follows:

-> The snake is moving to the left

-> The player presses the down arrow key

-> The player immediately presses the right arrow key (before 100ms have lapsed)

-> The game ends

Making sense of the bug

Let’s break down what happens step by step.

Snake is moving to the left

  • Horizontal velocity, dx is equal to -10
  • main function is called
  • advanceSnake is called which advances the snake -10px to the left.

Also Read:- How to Create Multiplication Table Using JavaScript

The player presses the down arrow key

  • changeDirection is called
  • keyPressed === DOWN_KEY && dy !goingUp evaluates to true
  • dx changes to 0
  • dy changes to +10

Player immediately presses the right arrow (before 100ms have lapsed)

  • changeDirection is called
  • keyPressed === RIGHT_KEY && !goingLeft evaluates to true
  • dx changes to +10
  • dy changes to 0

The game ends

  • main function is called after 100ms have lapsed.
  • advanceSnake is called which advances the snake 10px to the right.
  • const didCollide = snake[i].x === snake[0].x && snake[i].y === snake[0].y evaluates to true
  • didGameEnd returns true
  • main function returns early
  • The game ends

Fixing the bug

After studying what happened, we learn that the game ended because the snake reversed.

That is because when the player pressed the down arrow, dx was set to 0. Thus keyPressed === RIGHT_KEY && !goingLeft evaluated to true, and dx changed to 10.

It is important to note that the change in direction occurred before100ms had lapsed. If 100ms lapsed, then the snake would have first taken a step down and would not have reversed.

To fix our bug, we have to make sure that we can only change direction after main and advanceSnake have been called. We can create a variable changingDirection. This will be set to true when changeDirection is called, and to false when advanceSnake is called.

Inside our changeDirection function, we can return early if changingDirection is true.

function changeDirection(event) {
  const LEFT_KEY = 37;
  const RIGHT_KEY = 39;
  const UP_KEY = 38;
  const DOWN_KEY = 40;
if (changingDirection) return;
changingDirection = true;
...
}
function main() {
  setTimeout(function onTick() {
    changingDirection = false;
    
    ...
}, 100)
}

Here is our final version of snake.html

Notice I also added some styles between the <style></style> tags. That is to make the canvas and score appear in the middle of the screen.

<!DOCTYPE html>
<html>
  <head>
    <title>Snake Game</title>
    <link href="https://fonts.googleapis.com/css?family=Antic+Slab" rel="stylesheet">

  </head>

  <body>

    <div id="score">0</div>
    <canvas id="gameCanvas" width="300" height="300"></canvas>

    <style>
      #gameCanvas {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
      }
      #score {
        text-align: center;
        font-size: 140px;
        font-family: 'Antic Slab', serif;
      }
    </style>
  </body>

  <script>
    const GAME_SPEED = 100;
    const CANVAS_BORDER_COLOUR = 'black';
    const CANVAS_BACKGROUND_COLOUR = "white";
    const SNAKE_COLOUR = 'lightgreen';
    const SNAKE_BORDER_COLOUR = 'darkgreen';
    const FOOD_COLOUR = 'red';
    const FOOD_BORDER_COLOUR = 'darkred';
    let snake = [
      {x: 150, y: 150},
      {x: 140, y: 150},
      {x: 130, y: 150},
      {x: 120, y: 150},
      {x: 110, y: 150}
    ]
    // The user's score
    let score = 0;
    // When set to true the snake is changing direction
    let changingDirection = false;
    // Food x-coordinate
    let foodX;
    // Food y-coordinate
    let foodY;
    // Horizontal velocity
    let dx = 10;
    // Vertical velocity
    let dy = 0;
    // Get the canvas element
    const gameCanvas = document.getElementById("gameCanvas");
    // Return a two dimensional drawing context
    const ctx = gameCanvas.getContext("2d");
    // Start game
    main();
    // Create the first food location
    createFood();
    // Call changeDirection whenever a key is pressed
    document.addEventListener("keydown", changeDirection);
    /**
     * Main function of the game
     * called repeatedly to advance the game
     */
    function main() {
      // If the game ended return early to stop game
      if (didGameEnd()) return;
      setTimeout(function onTick() {
        changingDirection = false;
        clearCanvas();
        drawFood();
        advanceSnake();
        drawSnake();
        // Call game again
        main();
      }, GAME_SPEED)
    }
    /**
     * Change the background colour of the canvas to CANVAS_BACKGROUND_COLOUR and
     * draw a border around it
     */
    function clearCanvas() {
      //  Select the colour to fill the drawing
      ctx.fillStyle = CANVAS_BACKGROUND_COLOUR;
      //  Select the colour for the border of the canvas
      ctx.strokestyle = CANVAS_BORDER_COLOUR;
      // Draw a "filled" rectangle to cover the entire canvas
      ctx.fillRect(0, 0, gameCanvas.width, gameCanvas.height);
      // Draw a "border" around the entire canvas
      ctx.strokeRect(0, 0, gameCanvas.width, gameCanvas.height);
    }
    /**
     * Draw the food on the canvas
     */
    function drawFood() {
      ctx.fillStyle = FOOD_COLOUR;
      ctx.strokestyle = FOOD_BORDER_COLOUR;
      ctx.fillRect(foodX, foodY, 10, 10);
      ctx.strokeRect(foodX, foodY, 10, 10);
    }
    /**
     * Advances the snake by changing the x-coordinates of its parts
     * according to the horizontal velocity and the y-coordinates of its parts
     * according to the vertical veolocity
     */
    function advanceSnake() {
      // Create the new Snake's head
      const head = {x: snake[0].x + dx, y: snake[0].y + dy};
      // Add the new head to the beginning of snake body
      snake.unshift(head);
      const didEatFood = snake[0].x === foodX && snake[0].y === foodY;
      if (didEatFood) {
        // Increase score
        score += 10;
        // Display score on screen
        document.getElementById('score').innerHTML = score;
        // Generate new food location
        createFood();
      } else {
        // Remove the last part of snake body
        snake.pop();
      }
    }
    /**
     * Returns true if the head of the snake touched another part of the game
     * or any of the walls
     */
    function didGameEnd() {
      for (let i = 4; i < snake.length; i++) {
        if (snake[i].x === snake[0].x && snake[i].y === snake[0].y) return true
      }
      const hitLeftWall = snake[0].x < 0;
      const hitRightWall = snake[0].x > gameCanvas.width - 10;
      const hitToptWall = snake[0].y < 0;
      const hitBottomWall = snake[0].y > gameCanvas.height - 10;
      return hitLeftWall || hitRightWall || hitToptWall || hitBottomWall
    }
    /**
     * Generates a random number that is a multiple of 10 given a minumum
     * and a maximum number
     * @param { number } min - The minimum number the random number can be
     * @param { number } max - The maximum number the random number can be
     */
    function randomTen(min, max) {
      return Math.round((Math.random() * (max-min) + min) / 10) * 10;
    }
    /**
     * Creates random set of coordinates for the snake food.
     */
    function createFood() {
      // Generate a random number the food x-coordinate
      foodX = randomTen(0, gameCanvas.width - 10);
      // Generate a random number for the food y-coordinate
      foodY = randomTen(0, gameCanvas.height - 10);
      // if the new food location is where the snake currently is, generate a new food location
      snake.forEach(function isFoodOnSnake(part) {
        const foodIsoNsnake = part.x == foodX && part.y == foodY;
        if (foodIsoNsnake) createFood();
      });
    }
    /**
     * Draws the snake on the canvas
     */
    function drawSnake() {
      // loop through the snake parts drawing each part on the canvas
      snake.forEach(drawSnakePart)
    }
    /**
     * Draws a part of the snake on the canvas
     * @param { object } snakePart - The coordinates where the part should be drawn
     */
    function drawSnakePart(snakePart) {
      // Set the colour of the snake part
      ctx.fillStyle = SNAKE_COLOUR;
      // Set the border colour of the snake part
      ctx.strokestyle = SNAKE_BORDER_COLOUR;
      // Draw a "filled" rectangle to represent the snake part at the coordinates
      // the part is located
      ctx.fillRect(snakePart.x, snakePart.y, 10, 10);
      // Draw a border around the snake part
      ctx.strokeRect(snakePart.x, snakePart.y, 10, 10);
    }
    /**
     * Changes the vertical and horizontal velocity of the snake according to the
     * key that was pressed.
     * The direction cannot be switched to the opposite direction, to prevent the snake
     * from reversing
     * For example if the the direction is 'right' it cannot become 'left'
     * @param { object } event - The keydown event
     */
    function changeDirection(event) {
      const LEFT_KEY = 37;
      const RIGHT_KEY = 39;
      const UP_KEY = 38;
      const DOWN_KEY = 40;
      /**
       * Prevent the snake from reversing
       * Example scenario:
       * Snake is moving to the right. User presses down and immediately left
       * and the snake immediately changes direction without taking a step down first
       */
      if (changingDirection) return;
      changingDirection = true;
      
      const keyPressed = event.keyCode;
      const goingUp = dy === -10;
      const goingDown = dy === 10;
      const goingRight = dx === 10;
      const goingLeft = dx === -10;
      if (keyPressed === LEFT_KEY && !goingRight) {
        dx = -10;
        dy = 0;
      }
      
      if (keyPressed === UP_KEY && !goingDown) {
        dx = 0;
        dy = -10;
      }
      
      if (keyPressed === RIGHT_KEY && !goingLeft) {
        dx = 10;
        dy = 0;
      }
      
      if (keyPressed === DOWN_KEY && !goingUp) {
        dx = 0;
        dy = 10;
      }
    }
  </script>
</html>

Conclusion

We have reached the end of our journey. I hope you enjoyed learning with me and now feel confident to continue on to your next adventure.

But it doesn’t have to end here. My next article will focus on helping you get started with the very exciting world of open source.

Open source is a great way to learn a lot of new things and get to know amazing people . It is very rewarding but can can be scary at first.

Also Read:- How to Create Multiplication Table Using JavaScript

To get a notification when my next article is out, you can follow me!

It was a pleasure to be on this journey with you.

Author Biography.

Lokesh Gupta
Lokesh Gupta

Overall 3+ years of experience as a Full Stack Developer with a demonstrated history of working in the information technology and services industry. I enjoy solving complex problems within budget and deadlines putting my skills on PHP, MySQL, Python, Codeigniter, Yii2, Laravel, AngularJS, ReactJS, NodeJS to best use. Through Knowledge of UML & visual modeling, application architecture design & business process modeling. Successfully delivered various projects, based on different technologies across the globe.

News & Blogs

6ec3fae84857a1f5dc59eebcc6c0775e.png

Building a carousel component in React using Hooks

One of the problems in web development today is the entangling of different layers. Not only do w...

2cda7519f703462a0eabd1c2e84e0001.jpg

Why shouldn’t you wait to jump onto Angular 2?

AngularJs is a JavaScript framework and is widely adopted framework in building Single Page Web A...

8b77d9abe1f73f94ea9f0f008ff988a1.jpeg

Difference between Sets vs. Arrays in JavaScript

The Set object type was introduced in the 2015 ECMAScript specification and is ready to be used i...