dan

dan

groovin'

Achieving our goal

Eating the apple is the only goal in snake. Each apple the player eats, the larger the snake grows, and the more points he scores. Each time the snake eats the apple, the apple is respawned in another location on the grid. In order to spawn our apple in random locations, let's write a function to generate a random tile.

Creating chaos

JavaScript has a built-in Math library with plenty of functions we can use out of the box. Math.random is the first function we will use. It generates a random floating-point (decimal) number that is greater than or equal to 0 and less than 1. We want to generate numbers that corrospond to tile coordinates, which will be whole numbers between 0 and 9 for our 10x10 grid. Math.random() * max will create x where 0 <= x < max. Or, using interval notation, [0,max).

Since we need whole numbers, we must round the result of Math.random() * max. If we use Math.round, it will be less likely for us to get a number at the bottom and top of the range. For example, to roll a 0, Math.random() * max would have to return a number lower than .5. To roll a 1, it would have to return a number between .5 and 1.5, which is twice the probability of rolling a 0. To give the entire range the same probability of being chosen, we will use Math.floor. It will round any number less than 1 down to 0. The only catch with using Math.floor is that we need to provide a max of 10 to achieve a range of 0-9.

function randomTile(max) {
	// Generate a random number in the range [0, max)
	let randomFloat = Math.random() * max;
 
	// Convert float (decimal) to integer
	return Math.floor(randomFloat);
}
You can put this function at the bottom of game.js

Creating the Apple

Since the apple needs to respawn every time it gets eaten by the snake, let's write a function to create the apple. The function should be called immediately, so that upon loading the game, the apple will be waiting for the player.

let apple;
 
createApple();
 
function createApple() {
 
  // Choose a random tile for the Apple
  apple = {
  	x: randomTile(tilesX),
  	y: randomTile(tilesY)
  }
}

Looks good! But with our current code, we could throw an apple directly on top of the snake. To avoid that, let's check if the randomly generated apple is on top of the snake, and if it is, we will try to randomly generate the apple again. We will use a do...while loop to repeat the generation logic if there is a collision. Since there is only one apple, once we detect a collision, we can stop looping through the elements in the player array.

function createApple() {
  let collision;
 
  do {
    collision = false;
 
    // Choose a random tile for the Apple
    apple = {
      x: randomTile(tilesX),
      y: randomTile(tilesY)
    }
 
    // Check to see if the apple was generated on a tile the player occupies
    for(let i = 0; i < player.length; ++i) {
      if(player[i].x == apple.x && player[i].y == apple.y) {
    	  collision = true;
    	  break;
      }
    }
  } while(collision);
}

Rendering the apple

It's time to draw our objective on the screen! But first, let's break out our tile rendering logic into a function called drawTile. This will make it easier to draw the apple.

function drawTile(x, y) {
  // Calculate the x, y, w, h for each tile
  x = x * tileWidth + borderWidth;
  y = y * tileHeight + borderWidth;
  let w = tileWidth - borderWidth * 2;
  let h = tileHeight - borderWidth * 2;
 
  // Draw each tile
  ctx.fillRect(x, y, w, h);
}
You can put this function at the bottom of the file.

Let's use this new function to draw the background, player, and the apple. Your render function should now look like this:

function render() {
	// Draw background
	ctx.fillStyle = "black";
	ctx.fillRect(0, 0, width, height);
 
	// Draw grid
	for(var i = 0; i < tilesX; ++i) {
		for(var j = 0; j < tilesY; ++j) {
 
			// Set the default tile color
			ctx.fillStyle = "blue";
 
			// Detect if the player is in this tile
			for(var k = 0; k < player.length; ++k) {
				if(player[k].x == i && player[k].y == j) {
					ctx.fillStyle = "green";
					break;
				}
			}
			
			drawTile(i,j);
		}
	}
 
	// Draw apple
	ctx.fillStyle = "red";
	drawTile(apple.x, apple.y);
}

Great! Now you should see an apple randomly drawn on the screen. But we aren't finished with this apple, yet! The snake will run right through the apple without eating it. To fix this, we need to implement the update logic for our apple.

Eating the apple

Start by defining a new varible to store whether or not the apple has been eaten. You can put this near the top of the file with the other variables.

let eaten = false;

At the bottom of the update function, we need to write some logic to check whether the player's new head position collides with the apple. If there is a collision, we will update the eaten variable, and respawn the apple.

// Check if the snake hit the apple
if(head.x == apple.x && head.y == apple.y) {
  eaten = true;
  createApple();
}

Question

Now that the apple has been eaten, how can we increase the size of the snake by 1 tile?

Solution

Remember that we are removing the tail of the snake each time the snake moves? Well, if the apple was eaten, let's not remove the tail of the snake, but allow the head of the snake to continue moving forward!

// Remove the last element in the array
if(!eaten)
  player.pop();
else
  eaten = false;

To make sure you're all caught up, here is the entire update function.

function update() {
  if(lastKey == undefined || gameOver)
    return;
 
  // Remove the last element in the array
  if(!eaten)
    player.pop();
  else
    eaten = false;
 
  // This is a confusing line of code. We are copying the head of the snake at player[0]
  // into an empty object {} and returning it to the variable `head`.
  // If we use `head = player[0]`, we would be referencing `player[0]` instead of copying it.
  let head = Object.assign({}, player[0]);
 
  if(lastKey == "ArrowLeft")
    --head.x;
  else if(lastKey == "ArrowRight")
    ++head.x;
  else if(lastKey == "ArrowUp")
    --head.y;
  else if(lastKey == "ArrowDown")
    ++head.y;
 
  // Add the head of the snake to the beginning of the array
  player.unshift(head);
 
  for(let i = 0; i < player.length; ++i) {
    // Check if the snake has hit itself
    if(i > 0 && head.x == player[i].x && head.y == player[i].y)
      gameOver = true;
    
    // Check if the snake is out of bounds
    if(player[i].x == tilesX)
      player[i].x = 0;
    else if(player[i].x == -1)
      player[i].x = tilesX -1;
    
    if(player[i].y == tilesY) 
      player[i].y = 0;
    else if(player[i].y == -1)
      player[i].y = tilesY -1;
  }
 
  // Check if the snake hit the apple
  if(head.x == apple.x && head.y == apple.y) {
    eaten = true;
    createApple();
  }
}

Great work!

Now our snake can achieve his only goal in life - eating the apple. Our game has all the basic mechanics of Snake! In the next chapter, we learn how to complete the game loop to allow players to play our game over and over again.