Floating Octothorpe

Writing snake in JavaScript

Snake as a game concept is over 40 years old! The concept was originally introduced by Blockade, however there have since been many clones and variations. Following on in that tradition, this post is going to look at making snake using JavaScript and the Canvas API.

Boiler plate HTML

The first step is to create a simple HTML page similar to the following:

<!DOCTYPE html>
<html>
<head>
  <title>Snake</title>
  <meta charset="utf-8" />
</head>
<body>
  <canvas id="game"></canvas>
  <script>
  </script>
</body>
</html>

Note: the <canvas> element has an id attribute so it can easily be referenced later.

Game variables

For the game to work, it needs to track a few variables. For this example all variables are going to be stored in a game object:

function init(game) {
  game.canvas = document.getElementById("game");
  game.ctx = game.canvas.getContext("2d");
  game.tile_width = 10;
  game.width = 40;
  game.height = 40;
  game.fps = 15;
  game.keys = {};
  game.snake = [
    {"x": 6, "y": 16},
    {"x": 6, "y": 17},
    {"x": 6, "y": 18},
    {"x": 6, "y": 19}
  ];
  game.snake_vx = 0;
  game.snake_vy = -1;

  game.food = {"x": 24, "y": 10};

  game.canvas.width = game.width * game.tile_width;
  game.canvas.height = game.height * game.tile_width;
}

window.onload = function() {
  let game = {};

  init(game);
};

The position of the food and snake tiles are stored as x, y co-ordinates, with (0, 0) as the top left most tile.

Adding a game loop

The next step is to add a game loop, this can be done using the setInterval function:

function render(game) {
}

function logic(game) {
}

window.onload = function() {
  let game = {};

  init(game);
  window.setInterval(function() {
    logic(game);
    render(game);
  }, 1000 / game.fps);
};

The code above will call the logic and the render functions roughly 15 times a second.

Rendering the game

A very simple render function might look something like this:

function render(game) {
  let ctx = game.ctx;

  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, game.canvas.width, game.canvas.height);

  ctx.fillStyle = "green";
  for (let i = 0; i < game.snake.length; i += 1) {
    ctx.fillRect(
      game.snake[i].x * game.tile_width,
      game.snake[i].y * game.tile_width,
      game.tile_width - 1,
      game.tile_width - 1
    );
  }
  ctx.fillStyle = "red";
  ctx.fillRect(
    game.food.x * game.tile_width,
    game.food.y * game.tile_width,
    game.tile_width - 1,
    game.tile_width - 1
  );
}

In the code above three elements are rendered:

  1. First the background is set to black, this needs to be done every time a frame is rendered, both to set the background colour, and to hide any previously rendered frames.

  2. The segments of the snake are then rendered.

  3. Finally the snake food is rendered.

Once the render function is in place the initial state of the canvas should look something like the following:

Canvas element with a single rendered frame.

Listening for key events

Before moving on to the game logic, two functions are added to capture key events:

window.onload = function() {
  let game = {};

  init(game);
  window.onkeyup = function(key_event) {
    game.keys[key_event.keyCode] = false;
  };
  window.onkeydown = function(key_event) {
    game.keys[key_event.keyCode] = true;
  };
  window.setInterval(function() {
    logic(game);
    render(game);
  }, 1000 / game.fps);
};

These functions update the keys object when keys are pressed or released. This will make it easy to decide which direction the snake should move in the game logic.

Adding game logic

Once the key listeners are in place the logic function can be updated to look something like the following:

function logic(game) {

  if (game.keys[37] && game.snake_vx === 0) {
    // Left
    game.snake_vx = -1;
    game.snake_vy = 0;
  } else if (game.keys[38] && game.snake_vy === 0) {
    // Up
    game.snake_vx = 0;
    game.snake_vy = -1;
  } else if (game.keys[39] && game.snake_vx === 0) {
    // Right
    game.snake_vx = 1;
    game.snake_vy = 0;
  } else if (game.keys[40] && game.snake_vy === 0) {
    // Down
    game.snake_vx = 0;
    game.snake_vy = 1;
  }

  // Move head
  let next = {
    "x": (game.snake[0].x + game.snake_vx) % game.width,
    "y": (game.snake[0].y + game.snake_vy) % game.height
  };

  if (next.x < 0) {
    next.x += game.width;
  }
  if (next.y < 0) {
    next.y += game.height;
  }
  for (let i = 0; i < game.snake.length; i += 1) {
    if (game.snake[i].x === next.x && game.snake[i].y === next.y) {
      alert("Game over!\n(score: " + game.snake.length + ")");
      location.reload();
    }
  }
  game.snake.unshift(next);

  // Eat?
  if (game.snake[0].x === game.food.x && game.snake[0].y === game.food.y) {
    game.food.x = Math.floor(Math.random() * game.width);
    game.food.y = Math.floor(Math.random() * game.height);
  } else {
    game.snake.pop();
  }
}

The first section of code updates the snake's vertical and horizontal velocity. This is then used to calculate the new position of the snake's head and make sure the snake doesn't collide with itself. If the head does collide with the snake, a game over dialog is displayed before reloading the page.

Finally the new position of the snakes head is compared to the position of the food. If the positions match, the food is repositioned and the snake effectively grows by one; otherwise the last segment of the snake is removed to keep the snake's length consistent.

Putting it all together

Once everything has been put together you should have a working snake game prototype. At just over 100 lines, the game is obviously very simple, however it could be used as a base for additional features. For example: