Before you go, check out these stories!

0
Hackernoon logoMaking the Snake Game in your Browser: Practicum by Yandex by@evgeniy-lebedev

Making the Snake Game in your Browser: Practicum by Yandex

Author profile picture

@evgeniy-lebedevEvgeniy Lebedev

Chief Marketing Officer Practicum by Yandex, edtech expert, practicum.yandex.com

Want to make your own classic arcade game?

I’ve been watching YouTube videos about teaching AI to play Snake. Needless to say, it’s hard. Meanwhile, the game Snake has been exactly the right level of challenge for humans since 1976! Philosophically speaking, why teach machines to play games when we can play those games ourselves?

And thus, I decided to make Snake and play it.

“But then again,” I realized, “Why make Snake from the ground up when I can base the game on someone else’s code?”

I browsed the web for a good template, and I found one. Now, I’ll walk you through the code to make Snake, explaining it the best way I can.

This project contains the following parts:

  1. The HTML page
  2. The canvas for the game
  3. The main loop
  4. Keyboard controls
  5. Launch

1. The HTML page

The page is simple: it’s just a blank HTML template with a title and an empty body:

<!DOCTYPE html>
<html>
<head>
  <title>The Snake</title>
  <style></style>
</head>
<body>
  <!-- Here be the game -->
</body>
</html>

I put this into an

.html
file and save it on my hard drive. Everything will run inside that html file inside my browser.

I’d love the page to be black, have no margins, and have the game sit right in the middle of the screen. So, I go into

<style>...</style>
and add the following:

html, body {
  height: 100%;
  margin: 0;
}
body {
  background: black;
  display: flex;
  align-items: center;
  justify-content: center;
}
/*Canvas is explained below*/
canvas {
  border: 1px solid white;
}

2. The canvas for the game

There’s a problem with normal, vanilla HTML: it’s a markup language for text, not graphics. It’s great for laying out text, word by word, in blocks, headings, and tables. You can use HTML to tell the browser which text you want on the page, and the browser will take care of positioning that text, rendering it, etc.

(We take it for granted that as we browse the web, we can resize text, zoom in and out of a page, have text flow from one line to another, and more. But it actually takes a lot of clever programming for it to work; browsers are great at that.)

Once we venture outside text, that’s where the problems with HTML start:

  • It’s not that easy to place an object at a specific point on a page
  • Moving objects across the page isn’t straightforward
  • Once the page starts resizing, goes to a mobile device, or changes resolution, everything goes to hell.

In short, when you’re using HTML, you can’t just say, “Please draw a black square at coordinates (X, Y).” You have to say something like, “Okay, let’s pretend this virtual block is a square, give it a black background, position it absolutely in a relatively-positioned area with 100% height and width, and add positioning properties relative to the top and left to make this block sit in a certain place.” Oh, and it won’t work in half of your browsers anyway.

A recent addition to HTML is canvas—an element designed for drawing with JavaScript. It’s an empty block of pixels that you can manipulate directly with JavaScript. Using JavaScript and canvas, you can actually say, ”Draw me a square at coordinates (X, Y),” and you’ll get that square, plain and simple.

So, let’s initialize a blank canvas:

<canvas id="game" width="400" height="400"></canvas>

To control the canvas, we’ll need to add our JavaScript code somewhere below. We’ll put that script in between the

<script>...</script>
tags.

The first thing we’ll need to do is to include the canvas inside our script so that we can work with the canvas. We accomplish this with two lines of code:

// We basically link the canvas in our document to a variable in JavaScript…sort of. We’ll call that variable in order to work with canvas.
var canvas = document.getElementById('game');
// Next, within the canvas, we want to create a two-dimensional space. So, we call the canvas and tell it to be 2D:
var context = canvas.getContext('2d');

From now on, in order to draw stuff inside our canvas, we’ll call context and use the 2D drawing methods that context can perform.

At this point, we also need to do some housekeeping; we need to set up the variables for our game:

// the size of a single unit in our grid. For now, the snake will run in blocks of 16 pixels. If we want a slimmer snake, we can set this number lower. 
var grid = 16;
// The variable ‘count’ will contain a number that controls how fast the snake runs.
var count = 0;
// Our snake will be an object. This object will contain the snake’s speed, the coordinates of the head, and the array that corresponds to the snake’s body. 
var snake = {
  // Initial coordinates
  x: 160,
  y: 160,
  // Initial speed. When the game starts, the snake will move horizontally, so its speed on the X axis will be equal to grid (which is 16). The snake’s Y speed will be zero because our snake won’t be moving up and down. 
  dx: grid,
  dy: 0,
  // ‘Cells’ will be an array containing all parts of the snake’s body (its tail?). At this point, the array is going to be empty. 
  cells: [],
  // Now, let’s make the snake’s body start off with 4 cells. 
  maxCells: 4
};
// Here be food: 
var apple = {
  // The first food starts off at these coordinates: 
  x: 320,
  y: 320
};

Finally, let’s make a function that can generate a random coordinate. This function will help our game position the next food:

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

3. The main loop

The main loop is what happens at every frame of the game. This loop needs to:

  • Clear the game screen
  • Calculate the snake’s position
  • If the snake hits the edge of the canvas, warp the snake to the other side of the canvas
  • Advance the snake (virtually and in memory)
  • Place the food
  • Draw the snake’s head and body
  • Check whether the snake hit itself

Now, let’s do this:

function loop() {
  // The next function makes the game skip every 3 frames out of 4, effectively lowering the game speed to 15 frames per second. We need to do this to make the game playable.
  requestAnimationFrame(loop);
  if (++count < 4) {
    return;
  }
  count = 0;
  // Next up: clearing the canvas. 
  context.clearRect(0,0,canvas.width,canvas.height);
  // Now, let’s move the snake.
  snake.x += snake.dx;
  snake.y += snake.dy;
  // Warping snake if it hits edge of canvas on the X axis
  if (snake.x < 0) {
    snake.x = canvas.width - grid;
  }
  else if (snake.x >= canvas.width) {
    snake.x = 0;
  }
  // Warping snake if it hits the edge on the Y axis
  if (snake.y < 0) {
    snake.y = canvas.height - grid;
  }
  else if (snake.y >= canvas.height) {
    snake.y = 0;
  }
  // Advancing the snake’s head
  snake.cells.unshift({x: snake.x, y: snake.y});
  // And removing the tail end of the snake’s body
  if (snake.cells.length > snake.maxCells) {
    snake.cells.pop();
  }
  // Planting the food
  context.fillStyle = 'red';
  context.fillRect(apple.x, apple.y, grid-1, grid-1);
  // Setting the style of fills for the snake’s body
  context.fillStyle = 'green';
  // Filling each unit of the snake’s body
  snake.cells.forEach(function(cell, index) {
    // To make the game look retro, adding some black borders to the body cells
    context.fillRect(cell.x, cell.y, grid-1, grid-1);  
    // If the snake reaches the apple…
    if (cell.x === apple.x && cell.y === apple.y) {
      // then increase the snake’s length,
      snake.maxCells++;
      // and plant new food.
      apple.x = getRandomInt(0, 25) * grid;
      apple.y = getRandomInt(0, 25) * grid;
    }
    // Checking to see if the snake hit itself
    for (var i = index + 1; i < snake.cells.length; i++) {
      // If it did, then start over
      if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {
        // Resetting the game
        snake.x = 160;
        snake.y = 160;
        snake.cells = [];
        snake.maxCells = 4;
        snake.dx = grid;
        snake.dy = 0;
        // Planting food at random
        apple.x = getRandomInt(0, 25) * grid;
        apple.y = getRandomInt(0, 25) * grid;
      }
    }
  });
}

4. Keyboard controls

Since we want to use the keyboard to control the snake, we need our game to listen to the key presses. Once there is a key pressed, the game needs to see which key was pressed, and react accordingly.

// Listen to the keys
document.addEventListener('keydown', function(e) {
  // First, we need to check if the key pressed is in the direction that the snake was already going. In that case, we can ignore the key. 
  // Left key
  if (e.which === 37 && snake.dx === 0) {
    snake.dx = -grid;
    snake.dy = 0;
  }
  // Up key
  else if (e.which === 38 && snake.dy === 0) {
    snake.dy = -grid;
    snake.dx = 0;
  }
  // Right key
  else if (e.which === 39 && snake.dx === 0) {
    snake.dx = grid;
    snake.dy = 0;
  }
  // Down key
  else if (e.which === 40 && snake.dy === 0) {
    snake.dy = grid;
    snake.dx = 0;
  }
});

5. Launch

All we need to do is start the loop that we wrote earlier:

requestAnimationFrame(loop);

Take some time to enjoy what we’ve created here:

What a beautiful retro arcade game.

Here’s the final code:

<!DOCTYPE html>
<html>
<head>
  <title>The Snake</title>
  <style>
    html, body {
      height: 100%;
      margin: 0;
    }
    body {
      background: black;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    canvas {
      border: 1px solid white;
    }
  </style>
</head>
<body>
  <canvas width="400" height="400" id="game"></canvas>
  <script>
    var canvas = document.getElementById('game');
    var context = canvas.getContext('2d');
    var grid = 16;
    var count = 0;
    var snake = {
      x: 160,
      y: 160,
      dx: grid,
      dy: 0,
      cells: [],
      maxCells: 4
    };
    var apple = {
      x: 320,
      y: 320
    };
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}
    function loop() {
      requestAnimationFrame(loop);
      if (++count < 4) {
        return;
      }
      count = 0;
      context.clearRect(0,0,canvas.width,canvas.height);
      snake.x += snake.dx;
      snake.y += snake.dy;
      if (snake.x < 0) {
        snake.x = canvas.width - grid;
      }
      else if (snake.x >= canvas.width) {
        snake.x = 0;
      }
      if (snake.y < 0) {
        snake.y = canvas.height - grid;
      }
      else if (snake.y >= canvas.height) {
        snake.y = 0;
      }
      snake.cells.unshift({x: snake.x, y: snake.y});
      if (snake.cells.length > snake.maxCells) {
        snake.cells.pop();
      }
      context.fillStyle = 'red';
      context.fillRect(apple.x, apple.y, grid-1, grid-1);
      context.fillStyle = 'green';
      snake.cells.forEach(function(cell, index) {
        context.fillRect(cell.x, cell.y, grid-1, grid-1);  
        if (cell.x === apple.x && cell.y === apple.y) {
          snake.maxCells++;
          apple.x = getRandomInt(0, 25) * grid;
          apple.y = getRandomInt(0, 25) * grid;
        }
        for (var i = index + 1; i < snake.cells.length; i++) {
          if (cell.x === snake.cells[i].x && cell.y === snake.cells[i].y) {
            snake.x = 160;
            snake.y = 160;
            snake.cells = [];
            snake.maxCells = 4;
            snake.dx = grid;
            snake.dy = 0;
            apple.x = getRandomInt(0, 25) * grid;
            apple.y = getRandomInt(0, 25) * grid;
          }
        }
      });
    }
    document.addEventListener('keydown', function(e) {
      if (e.which === 37 && snake.dx === 0) {
        snake.dx = -grid;
        snake.dy = 0;
      }
      else if (e.which === 38 && snake.dy === 0) {
        snake.dy = -grid;
        snake.dx = 0;
      }
      else if (e.which === 39 && snake.dx === 0) {
        snake.dx = grid;
        snake.dy = 0;
      }
      else if (e.which === 40 && snake.dy === 0) {
        snake.dy = grid;
        snake.dx = 0;
      }
    });
    requestAnimationFrame(loop);
  </script>
</body>
</html>

Enjoyed this tutorial? Feel free to browse the resources on Practicum. We offer education and mentorship to help you amp up your tech skills and enhance your career.

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.