Let's Make a Snake Game

December 12, 2011

Before we continue, i'll assume that you're using a modern web browser that supports HTML5, such as Chrome, Firefox, Opera, Safari or Internet Explorer 9/10. If you're not, i strongly recommend you to download any of them, otherwise you won't be able to try the examples.

I'm pretty sure we all know (and have played) this famous game about a snake that gets longer and longer as it eats small dots that show up randomnly on a map. It's fun and engaging so it makes it a fun little project to learn how to do game development using HTML5 and JavaScript.

So let's get started, shall we?

First of all, like many 2D games, believe or not, it can be accomplished by using a two-dimensional matrix. In case you don't know what a matrix is, you can look at it as a "grid" where you have several rows and columns as seen in the image below.

Empty Grid

As we can see, our matrix has 5 rows and 5 columns. When a "dot" (let's call it "food") shows up, it will obviously fill one of the positions like this:

Food on the grid

Also, the snake will fill other positions of the matrix, namely, one position for each part of its body. Later on, we'll talk about the snake occuppying "4 slots"; By slots we'll mean invidual positions on the matrix as seen below (the orange crosses represent the snake).

Food + Snake on the Grid

Every time that the snake "head" tries to get into the same "slot" as a piece of food, its body will get longer and the movement speed will increase a bit as well.

Now that we understand how a snake game works, let's start developing it. We'll start off by creating an HTML file called snake.html and filling it with the following content:

<!DOCTYPE html>
<html>
    <head>
        <title>Snake Game</title>
        <script src="snake.js" type="text/javascript"></script>
    </head>
    <body>

    </body>
</html>

This empty page calls a JavaScript file called snake.js, so let's create that file as well on the same path as snake.html

The first thing that we're going to take care of is detecting when the page has finished loading. We can do that easily by listening the onload event of the window object.

window.onload = function()
{
    // The code goes here...
};

After the browser has finished loading the page (triggering the onload event) we're going to dynamically insert an HTML5 Canvas object to the body of the page.

The HTML5 Canvas is an extremely fast and flexible drawable region on the screen that can be controlled with pixel-level accuracy using JavaScript.

In our case, we'll be using it to display our graphics.

In order to get access to the 2D drawing functions of the canvas object (Canvas also supports WebGL, which we'll cover later on some time in the future) we'll also need to call what it's called the "2D Context".

To create a canvas object dynamically we'll use the createElement() method of the document object in the following way:

var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');

By default, the canvas object will have a height of 300 pixels of height by 300 pixels of width. Our game will be using a 20x20 matrix where each slot has a size of 10 pixels of height by 10 pixels of width. This means that our canvas will need to have a size of 200 pixels of height by 200 pixels of width, plus 20 more pixels of height to accomodate the current score and level. It'd be also nice to add 4 more pixels of width and height to add a nice border around the map. Sounds good? Allrighty then, let's do it.

var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');

canvas.width = 204;
canvas.height = 224;

So far so good... but if we reload the page we'll still get a blank page. Part of the problem is that, while we have created the canvas object it only exists in the "memory of the computer" but we still haven't "attached" it to the body of the page. Don't worry though, that's pretty easy to solve.

First, we need to get a reference to the "body" object of our page. We can do that by using the getElementsByTagName() method of the document object in the following way:

var body = document.getElementsByTagName('body')[0];

You'll notice that i have used getElementsByTagName('body')[0], the reason of this is because getElementsByTagName() doesn't return a single element but an array (a list). Even though there's a single "body" element on our page we still need to refer to it as part of a list with more than one element.

Finally, we'll need to use the appendChild() method to append our canvas object to the body, like this:

body.appendChild(canvas);

Our entire code (so far) then, will look like this:

window.onload = function()
{
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');

    canvas.width = 204;
    canvas.height = 224;

    var body = document.getElementsByTagName('body')[0];
    body.appendChild(canvas);
};

Excellent! However, if we reload the snake.html file in our browser we'll still see a blank page... and the reason is that we haven't drawn anything on the canvas yet!
A couple of paragraphs ago we said that we were going to add 4 more pixels of height and width to draw a nice little border around the canvas.
If you pay attention to the code we've been using, we're storing a reference of the 2D Context on a variable called ctx, which gives us access to the 2D drawing methods. One of such methods is strokeRect().

context.strokeRect(X1, Y1, X2, Y2) allows us to draw outlined rectangles. The first parameter, X1 defines the starting point on the X axis, the second parameter Y1 defines the start point on the Y axis. The third, X2, defines the end coordinate on the X axis and the fourth, Y2 defines the end coordinate on the Y axis. For example, context.strokeRect(0, 0, 30, 30) means that it will draw an outlined rectangle starting on the pixels 0, 0 (top left) and will end on the pixels 30, 30. Pretty simple, right?
Additionally, we can use the context.strokeStyle property to define the outline color, and context.lineWidth to define how thick we want the outline to be.

So, if we want to draw a black 2-pixel border around our canvas we'll only need to use the following code:

window.onload = function()
{
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d');

    canvas.width = 204;
    canvas.height = 224;

    var body = document.getElementsByTagName('body')[0];
    body.appendChild(canvas);

    ctx.lineWidth = 2; // Our border will have a thickness of 2 pixels
    ctx.strokeStyle = 'black'; // The border will also be black

    // The border is drawn on the outside of the rectangle, so we'll
    // need to move it a bit to the right and up. Also, we'll need
    // to leave a 20 pixels space on the top to draw the interface.
    ctx.strokeRect(2, 20, canvas.width - 4, canvas.height - 24);
};

Now that we have a nice little border drawn around our canvas, we'll add two variables, score and level to indicate the current score and level.
Of course, we'll also need to display the level and the score on the top of the canvas. Let's do that then, and add the declarations below the ctx variable, like this:

var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
score = 0,
level = 0;

Also, after we draw the border we'll need to display both values. We can do that by using the fillText() method of the context object. fillText() accepts 3 parameters:

  • A string containing the text that we want to display
  • The start position on the X axis
  • The start position on the Y axis

In order to change the text "style" we'll need to change the context.font property. It works similarly to a CSS rule, so we can easily do the following:

context.font = '14px sans-serif';
context.fillText('Hello World!', 10, 20);

So far, i've hidden from you the fact that the HTML5 Canvas object works in immediate mode -- This means that once you draw something on the screen you'll loose all the references to it. It also means that in order to change the position of something on the canvas, you'll need to erase that bit and draw it again on the new coordinates. While it's an expensive operation, it's not as incovenient as it sounds.

However, this forces us to rethink our current code a bit more: If we "clear" a part of the canvas, that will also clear the border and the text as well as anything else that we draw on the canvas, which is why we're going to declare a new function, called drawMain() that will take care of drawing things such as the border, and displaying the current score and level.

Our code then, will look like this -- you'll notice that i also added the bit that displays both the score and the level using fillText():

window.onload = function()
{
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d'),
        score = 0,
        level = 0;

    canvas.width = 204;
    canvas.height = 224;

    var body = document.getElementsByTagName('body')[0];
    body.appendChild(canvas);

    function drawMain() 
    {
        ctx.lineWidth = 2; // Our border will have a thickness of 2 pixels
        ctx.strokeStyle = 'black'; // The border will also be black

        // The border is drawn on the outside of the rectangle, so we'll
        // need to move it a bit to the right and up. Also, we'll need
        // to leave a 20 pixels space on the top to draw the interface.
        ctx.strokeRect(2, 20, canvas.width - 4, canvas.height - 24);

        ctx.font = '12px sans-serif';
        ctx.fillText('Score: ' + score + ' - Level: ' + level, 2, 12);
    }
};

If we now reload the page, you'll notice that we'll get a blank page again, and the reason why we're getting a blank page is that we're not calling the drawMain() function! So we're now going to add one additional function called drawGame() that will not only take care of calling drawMain(), but also of drawing the snake and the food.

window.onload = function()
{
    var canvas = document.createElement('canvas'),
        ctx = canvas.getContext('2d'),
        score = 0,
        level = 0;

    canvas.width = 204;
    canvas.height = 224;

    var body = document.getElementsByTagName('body')[0];
    body.appendChild(canvas);

    drawGame();

    function drawGame() 
    {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawMain();
    }

    function drawMain() 
    {
        ctx.lineWidth = 2; // Our border will have a thickness of 2 pixels
        ctx.strokeStyle = 'black'; // The border will also be black

        // The border is drawn on the outside of the rectangle, so we'll
        // need to move it a bit to the right and up. Also, we'll need
        // to leave a 20 pixels space on the top to draw the interface.
        ctx.strokeRect(2, 20, canvas.width - 4, canvas.height - 24);

        ctx.font = '12px sans-serif';
        ctx.fillText('Score: ' + score + ' - Level: ' + level, 2, 12);
    }
};

You'll notice that inside drawGame() we're calling the clearRect() method of the context object -- Its purpose is pretty simple; It clears a section of the canvas and works similarly to strokeRect().

Like we discussed before, the whole concept of the snake game is built around a simple two-dimensional matrix, which in our case has 20 rows and 20 columns. In order to build that matrix we're going to use the following code:

var map = new Array(20);
for (var i = 0; i < map.length; i++) {
    map[i] = new Array(20);
}

In order to place "food" on a random position of the matrix, we can use the following code:

function generateFood(map)
{
    // Generate a random position for the rows and the columns.
    var rndX = Math.round(Math.random() * 19),
        rndY = Math.round(Math.random() * 19);

    // We also need to watch so as to not place the food
    // on the a same matrix position occupied by a part of the
    // snake's body.
    while (map[rndX][rndY] === 2) {
        rndX = Math.round(Math.random() * 19);
        rndY = Math.round(Math.random() * 19);
    }

    map[rndX][rndY] = 1;
    return map;
}

We also need to take care of defining some gameplay constrains. In a "snake game", the snake can only go up, down, left or right, but can't travel diagonally.

To solve this problem, we're going to define a variable called direction:

  • 0 will mean that the snake is going right,
  • 1 that it's going left,
  • 2 that it's going down and
  • 3 that it's going up

Pretty simple, right? Let's recap for a minute. So far we are:

  • Detecting when the page has finished loading
  • We're creating and attaching an HTML5 Canvas object to the body of the page
  • Then we're defining three functions:
    • One that takes care of cleaning the canvas and calling all the other drawing functions
    • One that draws the borders and the current level and score
    • And finally, one that generates "food" on a random position of the map.

Next up we're going to add the matrix initilization code and we're going to pass it as an argument to the generateFood() function. You'll notice that we're also adding the direction variable:

window.onload = function()
{
    var canvas = document.createElement('canvas'),
    ctx = canvas.getContext('2d'),
    score = 0,
    level = 0,
    direction = 0;

    // Initialize the matrix.
    var map = new Array(20);
    for (var i = 0; i < map.length; i++) {
        map[i] = new Array(20);
    }

    canvas.width = 204;
    canvas.height = 224;

    var body = document.getElementsByTagName('body')[0];
    body.appendChild(canvas);

    // Add the food
    map = generateFood(map);
    drawGame();

    function drawGame() 
    {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        drawMain();
    }

    function drawMain() 
    {
        ctx.lineWidth = 2; // Our border will have a thickness of 2 pixels
        ctx.strokeStyle = 'black'; // The border will also be black

        // The border is drawn on the outside of the rectangle, so we'll
        // need to move it a bit to the right and up. Also, we'll need
        // to leave a 20 pixels space on the top to draw the interface.
        ctx.strokeRect(2, 20, canvas.width - 4, canvas.height - 24);

        ctx.font = '12px sans-serif';
        ctx.fillText('Score: ' + score + ' - Level: ' + level, 2, 12);
    }

    function generateFood(map)
    {
        // Generate a random position for the rows and the columns.
        var rndX = Math.round(Math.random() * 19),
            rndY = Math.round(Math.random() * 19);
        
        // We also need to watch so as to not place the food
        // on the a same matrix position occupied by a part of the
        // snake's body.
        while (map[rndX][rndY] === 2) {
            rndX = Math.round(Math.random() * 19);
            rndY = Math.round(Math.random() * 19);
        }
        
        map[rndX][rndY] = 1;

        return map;
    }
};

I think it's about time to take care of introducing our dear friend, Mrs. Snake, to our little game. By default, the snake will only have 3 body "pieces" (or matrix slots). If you pay attention to the code we've been using so far, you'll notice that our default direction is 0, which means that the snake will be moving to the right. This means that the "extra" slots will have to be on the left of the "head" slot. For each slot, we'll need to know the row and the column where they are located as we know that every time that the snake advances one position each piece of the body takes the place of the previous slot, and to solve that, we're going to use an array called snake, like this:

var snake = new Array(3);

Of course, just like with the "food", we'll need to generate an initial position for the head slot, and to do this, we're going to create a function called generateSnake() that also takes a matrix as a parameter.

function generateSnake(map)
{
    // Generate a random position for the row and the column of the head.
    var rndX = Math.round(Math.random() * 19),
        rndY = Math.round(Math.random() * 19);

    // Let's make sure that we're not out of bounds as we also need to 
    // make space to accomodate the other two body pieces
    while ((rndX - snake.length) < 0) {
        rndX = Math.round(Math.random() * 19);
    }
	
    for (var i = 0; i < snake.length; i++) {
        snake[i] = { x: rndX - i, y: rndY };
        map[rndX - i][rndY] = 2;
    }

    return map;
}

Now that our matrix is filled with food slots as well as with snake slots, it's time to display them on the canvas. Back when we began the article we said that our matrix was going to have 20 rows of width and 20 columns of height and that we were going to make each "slot" 10 pixels tall by 10 pixels wide. This basically means that if that we need to present something on row 3 and column 5, that'd mean that we'd need to display a 10 x 10 square starting on the 30th pixel of the X axis, and the 50th pixel of the Y axis, or rather, multiply both the row and the column by 10.

As we can see, in order to draw both the food as well as the snake, we'll need to cycle the matrix:

  • If we find a 1, it means that we'll need to draw the food (which we'll display as a black square)
  • If we find a 2, it means that it belongs to a piece of the snake's body (which we'll be displaying as orange squares)

So, let's modify our drawGame() function to accomodate these changes. Let's also remember that we'll need to leave vertical offset of 20 pixels, which we're using to display the score and the level:

function drawGame() 
{
    // Clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Draw the border as well as the score
    drawMain();

    // Start cycling the matrix
    for (var x = 0; x < map.length; x++) {
        for (var y = 0; y < map[0].length; y++) {
            if (map[x][y] === 1) {
                ctx.fillStyle = 'black';
                ctx.fillRect(x * 10, y * 10 + 20, 10, 10);
            } else if (map[x][y] === 2) {
                ctx.fillStyle = 'orange';
                ctx.fillRect(x * 10, y * 10 + 20, 10, 10);			
            }
        }
    }
}

Just in case you got lost for some reason, your snake.js file should be filled with the following contents:

window.onload = function()
{
    var canvas = document.createElement('canvas'),
    ctx = canvas.getContext('2d'),
    score = 0,
    level = 0,
    direction = 0,
    snake = new Array(3);

    // Initialize the matrix.
    var map = new Array(20);
    for (var i = 0; i < map.length; i++) {
        map[i] = new Array(20);
    }

    canvas.width = 204;
    canvas.height = 224;

    var body = document.getElementsByTagName('body')[0];
    body.appendChild(canvas);

    // Add the snake
    map = generateSnake(map);

    // Add the food
    map = generateFood(map);

    drawGame();

    function drawGame() 
    {
        // Clear the canvas
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // Draw the border as well as the score
        drawMain();

        // Start cycling the matrix
        for (var x = 0; x < map.length; x++) {
            for (var y = 0; y < map[0].length; y++) {
                if (map[x][y] === 1) {
                    ctx.fillStyle = 'black';
                    ctx.fillRect(x * 10, y * 10 + 20, 10, 10);
                } else if (map[x][y] === 2) {
                    ctx.fillStyle = 'orange';
                    ctx.fillRect(x * 10, y * 10 + 20, 10, 10);          
                }
            }
        }
    }


    function drawMain() 
    {
        ctx.lineWidth = 2; // Our border will have a thickness of 2 pixels
        ctx.strokeStyle = 'black'; // The border will also be black

        // The border is drawn on the outside of the rectangle, so we'll
        // need to move it a bit to the right and up. Also, we'll need
        // to leave a 20 pixels space on the top to draw the interface.
        ctx.strokeRect(2, 20, canvas.width - 4, canvas.height - 24);

        ctx.font = '12px sans-serif';
        ctx.fillText('Score: ' + score + ' - Level: ' + level, 2, 12);
    }

    function generateFood(map)
    {
        // Generate a random position for the rows and the columns.
        var rndX = Math.round(Math.random() * 19),
            rndY = Math.round(Math.random() * 19);
        
        // We also need to watch so as to not place the food
        // on the a same matrix position occupied by a part of the
        // snake's body.
        while (map[rndX][rndY] === 2) {
            rndX = Math.round(Math.random() * 19);
            rndY = Math.round(Math.random() * 19);
        }
        
        map[rndX][rndY] = 1;

        return map;
    }

    function generateSnake(map)
    {
        // Generate a random position for the row and the column of the head.
        var rndX = Math.round(Math.random() * 19),
            rndY = Math.round(Math.random() * 19);

        // Let's make sure that we're not out of bounds as we also need to make space to accomodate the
        // other two body pieces
        while ((rndX - snake.length) < 0) {
            rndX = Math.round(Math.random() * 19);
        }
        
        for (var i = 0; i < snake.length; i++) {
            snake[i] = { x: rndX - i, y: rndY };
            map[rndX - i][rndY] = 2;
        }

        return map;
    }
};

And your snake.html file should output the following result (the positions of the objects may vary, remember that they are being generated randomly):

Snake Game Screenshot #1

Right now our game is shaping up just fine, but it's a bit static... don't you think? Right, let's make it move.

Unlike other games, we don't need to constantly refresh the graphics 60 times per second because our graphics will only need to change every 500 milliseconds or so (later on, we'll make it depend on the level). One way that we could do this is by tying the movement to the "refresh" function (in our case, that would be drawGame()), so we're going to change drawGame() so that every time it's called the snake moves in the direction indicated by the direction variable.

Every time that the snake moves, all the pieces or links move to where the last piece used to be, as explained clearly in the graphic displayed below:

Movement of the snake

However, if the head moves to a position of the matrix where food is present, the score should increase (let's say, by 10 points) and a new food position should be automatically generated. Also, we need to take care of detecting if the head of the snake hits another part of its body, or goes out of bounds, in which case the game should end. Meaning that we can loose, also means that we should also add a way to control the flow of the game by using a variable, which we'll be calling active. If active is set to true, the game will run normally, but if it's set to false it won't run at all.

It'd be also nice that when we loose the game a message pops up saying "Game Over" and shows the final score and level. To accomplish that we're going to make a new function called showGameOver(), that will look like this:

function showGameOver()
{
    // Clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    ctx.fillStyle = 'black';
    ctx.font = '16px sans-serif';
    
    ctx.fillText('Game Over!', ((canvas.width / 2) - (ctx.measureText('Game Over!').width / 2)), 50);

    ctx.font = '12px sans-serif';

    ctx.fillText('Your Score Was: ' + score, ((canvas.width / 2) - (ctx.measureText('Your Score Was: ' + score).width / 2)), 70);
    
}

Let's go over that function and figure out how it works.

The first line says:

// Clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);

As we already know, the clearRect() method of the context object can be used to clear an area of the canvas. In this particular case, it clears the coordinates 0, 0, canvas.width, canvas.height which means that it clears the whole canvas.

The next 2 lines are:

ctx.fillStyle = 'black';
ctx.font = '16px sans-serif';

That means that it sets the drawing color to black, and sets a sans-serif font of size 16px. But the next line is...

ctx.fillText('Game Over!', ((canvas.width / 2) - (ctx.measureText('Game Over!').width / 2)), 50);

Okay.. let's see what's going on here... Even though we have used fillText() before, we're now making a weird calculation here. The measureText() method of the context object measures the width of a given text. This basically means that, for example, doing ctx.measureText('Hello') would return an special object called TextMetrics that has a width property. In the case that we want to center an element in the middle of the canvas we can do:

canvas.width / 2 // which would give us a reference to the center of the canvas

And then...

ctx.measureText('Hello').width / 2 // which would give us a reference to the center of the text

Then we only need to do (canvas.width / 2) - (ctx.measureText('Hello').width / 2) to accurately center the text on the canvas, which is exactly what is going on here, and as you can probably tell, a similar thing happens with the next 2 lines.

But we've gone a bit off topic, so let's focus back on the drawGame() function and take into consideration all the things we've discussed so far:

function drawGame() 
{
    // Clear the canvas
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // Traverse all the body pieces of the snake, starting from the last one
    for (var i = snake.length - 1; i >= 0; i--) {

        // We're only going to perform the collision detection using the head
        // so it will be handled differently than the rest
        if (i === 0) {
            switch(direction) {
                case 0: // Right
                    snake[0] = { x: snake[0].x + 1, y: snake[0].y }
                    break;
                case 1: // Left
                    snake[0] = { x: snake[0].x - 1, y: snake[0].y }
                    break;
                case 2: // Up
                    snake[0] = { x: snake[0].x, y: snake[0].y - 1 }
                    break;
                case 3: // Down
                    snake[0] = { x: snake[0].x, y: snake[0].y + 1 }
                    break;
            }

            // Check that it's not out of bounds. If it is show the game over popup
            // and exit the function.
            if (snake[0].x < 0 || 
                snake[0].x >= 20 ||
                snake[0].y < 0 ||
                snake[0].y >= 20) {
                showGameOver();
                return;
            }

            // Detect if we hit food and increase the score if we do,
            // generating a new food position in the process, and also
            // adding a new element to the snake array.
            if (map[snake[0].x][snake[0].y] === 1) {
                score += 10;
                map = generateFood(map);

                // Add a new body piece to the array 
                snake.push({ x: snake[snake.length - 1].x, y: snake[snake.length - 1].y });
                map[snake[snake.length - 1].x][snake[snake.length - 1].y] = 2;

                // If the score is a multiplier of 100 (such as 100, 200, 300, etc.)
                // increase the level, which will make it go faster.
                if ((score % 100) == 0) {
                    level += 1;
                }
            
            // Let's also check that the head is not hitting other part of its body
            // if it does, we also need to end the game.
            } else if (map[snake[0].x][snake[0].y] === 2) {
                showGameOver();
                return;
            }

            map[snake[0].x][snake[0].y] = 2;
        } else {
            // Remember that when they move, the body pieces move to the place
            // where the previous piece used to be. If it's the last piece, it
            // also needs to clear the last position from the matrix
            if (i === (snake.length - 1)) {
                map[snake[i].x][snake[i].y] = null;
            }

            snake[i] = { x: snake[i - 1].x, y: snake[i - 1].y };
            map[snake[i].x][snake[i].y] = 2;
        }
    }

    // Draw the border as well as the score
    drawMain();

    // Start cycling the matrix
    for (var x = 0; x < map.length; x++) {
        for (var y = 0; y < map[0].length; y++) {
            if (map[x][y] === 1) {
                ctx.fillStyle = 'black';
                ctx.fillRect(x * 10, y * 10 + 20, 10, 10);
            } else if (map[x][y] === 2) {
                ctx.fillStyle = 'orange';
                ctx.fillRect(x * 10, y * 10 + 20, 10, 10);          
            }
        }
    }
    
    if (active) {
        // Call the drawGame() function
    }
}

We're also going to modify showGameOver() to indicate that active should be set to false when it is called.

Now, the only thing left to figure out is how to handle the movement speed, remember that the snake should go faster as the level goes up.

One way we could do this is by calling setTimeout() at a regular interval, such as 500 milliseconds, which we'll define in a variable call speed. Then, we could substract the level multiplied by 50. This basically means that:

  • On level 0 the drawGame() function will be called every 500 milliseconds
  • On level 1 the drawGame() function will be called every 450 milliseconds
  • On level 2 the drawGame() function will be called every 400 milliseconds
  • On level 3 the drawGame() function will be called every 350 milliseconds
  • On level 4 the drawGame() function will be called every 300 milliseconds
  • On level 5 the drawGame() function will be called every 250 milliseconds
  • On level 6 the drawGame() function will be called every 200 milliseconds
  • On level 7 the drawGame() function will be called every 150 milliseconds
  • On level 8 the drawGame() function will be called every 100 milliseconds
  • On level 9 the drawGame() function will be called every 50 milliseconds

At that point, the game will be impossible to control and the player will probably loose.

Sounds good? Ok, then. Let's replace the last 3 lines of the drawGame() function with the following code:

if (active) {
    setTimeout(drawGame, speed - (level * 50));
}

Finally, the only thing left for us to add is the control scheme, which will allow us to control the value of the direction variable:

window.addEventListener('keydown', function(e) {
    if (e.keyCode === 38 && direction !== 3) {
        direction = 2; // Up
    } else if (e.keyCode === 40 && direction !== 2) {
        direction = 3; // Down
    } else if (e.keyCode === 37 && direction !== 0) {
        direction = 1; // Left
    } else if (e.keyCode === 39 && direction !== 1) {
        direction = 0; // Right
    }
});

And that's it! We've made ourselves a nice snake game!

Just in case that you want to see the complete code and/or try the example yourself, you can check it out on GitHub. If you have any comments or questions, don't hesitate to ask them below, and thanks for reading!