One classic genre for casual games is named, appropriately, “Snake.” The basic idea of these sorts of games is that you control a snake, which gets ever longer. The challenge being that in order to score points, you move the snake across “apples” (is this some veiled Garden of Eden reference?) that increase the snake’s length. The snake continually moves in whatever direction it is facing, and if the head touches any part of the body, you lose.
This sort of game is perfect for a grid; each segment of the snake’s body can be represented by a grid cell. In this version, the snake
object consists of an array of points, represented by Cartesian coordinates. The snake can then be moved one square at a time by inserting a new set of coordinates as the first element of the array, and popping the last set of coordinates off the end of the array.
The initial game setup is fairly basic. Download the Waffle project template and make these changes at the start of the game.js
file:
- const rows = 10;
- const columns = 10;
+ const rows = 50;
+ const columns = 50;
Waffle.init(rows, columns);
+ const snake = [
+ { x: 25, y: 25 }, // this is the "head" of the snake
+ { x: 24, y: 25 },
+ { x: 23, y: 25 },
+ { x: 22, y: 25 }
+ ];
+
+ const state = Waffle.state;
+
+ // set the correct grid cells with the snake's body
+ for (const {x, y} of snake) {
+ state[x][y] = 'snake';
+ }
+
+ // update the game state, which draws the snake
+ Waffle.state = state;
Waffle.onKeyDown(({ key }) => {
Add the following style rules to main.css
, in order to represent the game background and the snake.
div {
border: 1px solid black;
border-top: 0;
border-left: 0;
+ background-color: black;
}
- .highlight {
+ .snake {
- background-color: violet;
+ background-color: limegreen;
}
}
Load index.html
in a browser and you should see a 50x50 grid, with a green “snake” in the center. Jawesome!

Now that the basics are in place, next step is to get the snake a-movin’. Since the snake is supposed to continually move forward (even without player input), we’ll need some sort of function that is called every few milliseconds that runs code to update the snake’s position. One way to regularly have a function called in JavaScript is using the global setInterval(callback, milliseconds)
function, which runs callback
every milliseconds
ms. setInterval
isn’t precise; the duration of time between callbacks running can vary based on various circumstances, but it’s fine for our purposes.
After the code which initializes the snake, add this line, which sets up the update loop to run every 30ms.
// set the correct grid cells with the snake's body
for (const {x, y} of snake) {
state[x][y] = 'snake';
}
Waffle.state = state;
+
+ // store the ID so we can turn off the update loop later if we need to
+ const intervalId = setInterval(update, 30);
Now that we’ve referenced the update
function, we actually have to write it. Add the following function to the end game.js
.
const update = () => {
// copy the snake's head to make a new one
const snakeHead = { ...snake[0] };
// for now, it will always move to the right
snakeHead.x += 1;
// this check will "wrap" the snake around the screen
if (snakeHead.x >= Waffle.columns) {
snakeHead.x = 0;
}
const state = Waffle.state;
// clear the current snake position
for (const {x, y} of snake) {
state[x][y] = '';
}
// add the new head to the beginning of the `snake` array
snake.unshift(snakeHead);
// remove the last tail segment
snake.pop();
// re-draw the snake
for (const {x, y} of snake) {
state[x][y] = 'snake';
}
// update the game state
Waffle.state = state;
};
// store the ID so we can turn off the update loop later if we need to
const intervalId = setInterval(update, 30);
Save the game source file and reload the page; you should see the snake continually moving from left to right.

Nice! We’ve gone from static to dynamic — and the next step is to add interactivity. For simplicity’s sake, we’ll focus on using a keyboard for user input; touch controls will be left as an exercise for the reader. The game.js
script already has an example of how waffle handles keyboard input. If you open the developer console and mash the keyboards while viewing index.html
, you should see a bunch of messages scroll by indicating which key was pressed.
Now we need to add logic that actually changes the direction the snek is moving. We already have a snake
variable which stores location, so it makes sense to make a new variable to store direction.
const snake = [
{ x: 25, y: 25 }, // this is the "head" of the snake
{ x: 24, y: 25 },
{ x: 23, y: 25 },
{ x: 22, y: 25 }
];
+ const direction = { x: 1, y: 0 };
We can now use that new variable in the update
function when determining which way the snake moves.
// copy the snake's head to make a new one
const snakeHead = { ...snake[0] };
- // for now, it will always move to the right
- snakeHead.x += 1;
+ snakeHead.x += direction.x;
+ snakeHead.y += direction.y;
// this check will "wrap" the snake around the screen
if (snakeHead.x >= Waffle.columns) {
snakeHead.x = 0;
}
If you save these changes and reload the page, it won’t look like anything has changed: the snake will continue to move to the right and wrap around the screen. But the difference is that the direction the snake moves isn’t hard-coded anymore. We can now add some checks in onKeyDown
to update the direction, based on classic WASD input.
Waffle.onKeyDown(({ key }) => {
- console.log(`pressed ${key}`);
+ if (key === 'w') {
+ direction.x = 0;
+ direction.y = -1;
+ } else if (key === 's') {
+ direction.x = 0;
+ direction.y = 1;
+ } else if (key === 'a') {
+ direction.x = -1;
+ direction.y = 0;
+ } else if (key === 'd') {
+ direction.x = 1;
+ direction.y = 0;
+ }
});
Reload the page, and now you can use the w
, a
, s
and d
keys to change the direction the snake moves.

One thing you quickly notice is that if the snake moves off the right-hand side of the grid, it appears on the left, wrapping around. That doesn’t happen on any of the other grid edges, because we started off by making the snake move only to the right, and therefore only checked if it moved off the right-hand side. Let’s add similar “wrapping” functionality to the other sides.
// this check will "wrap" the snake around the screen
if (snakeHead.x >= Waffle.columns) {
snakeHead.x = 0;
+ } else if (snakeHead.x < 0) {
+ snakeHead.x = Waffle.columns - 1;
+ } else if (snakeHead.y >= Waffle.rows) {
+ snakeHead.y = 0;
+ } else if (snakeHead.y < 0) {
+ snakeHead.y = Waffle.rows - 1;
+ }
The next thing we’ll add are the “apples” that the snake can eat. Currently, the values the grid can contain are either 'snake'
or ''
(an empty string). We’ll use the string 'apple'
to represent (what else?) an apple. Add the CSS rule:
div {
border: 1px solid black;
border-top: 0;
border-left: 0;
background-color: black;
}
.snake {
background-color: limegreen;
}
+ .apple {
+ background-color: red;
+ }
}
Now we can write the code that randomly places apples in empty spaces around the grid.
const state = Waffle.state;
// set the correct grid cells with the snake's body
for (const {x, y} of snake) {
state[x][y] = 'snake';
}
+
+ // create 10 apples
+ for (let i = 0; i < 10; i += 1) {
+ let point = Waffle.randomPoint;
+ // if random point is not empty, loop until an empty one is found
+ while (!Waffle.isEmpty(state[point.x][point.y])) {
+ point = Waffle.randomPoint;
+ }
+
+ state[point.x][point.y] = 'apple';
+ }
// update the game state, which draws the snake
Waffle.state = state;
Reload, and you’ll see apples randomly dotted around.

Theoretically the rules of this game stipulate that “eating” an apple makes the snake longer. Who comes up with these things? This is surprisingly easy to do. In our update
function, the way the snake moves is that we clone the head (the new head and old head temporarily overlap), advance the new head one space in the direction the snake is moving, then push the new head on top of the old one and delete the last tail segment. This creates the illusion that the snake is moving, when really it’s being slowly re-created and deleted on each update, Ship of Theseus-style.
To make the snake longer if it runs over an apple, we insert a step between creating the new head and moving it. Before moving the new head, check to see if that grid cell contains an apple — if it does, that means we can keep the last tail segment instead of deleting it. This makes the snake grow by one segment.
// push the new head on to the front of the `snake` array,
snake.unshift(snakeHead);
+ // this is tricky -- remove the last tail part if the next space is empty
+ // otherwise we can assume the snake hit an apple, and keep the tail
+ if (Waffle.isEmpty(state[snakeHead.x][snakeHead.y])) {
// remove the last tail segment
snake.pop();
+ }
// re-draw the snake
for (const {x, y} of snake) {
state[x][y] = 'snake';
}
The last rule we’ll program in is that the snake can’t touch its own body — no ouroboros-ing! We’ll accomplish this by checking if the new head overlaps the body. If it does, then display a message and stop the update loop. Otherwise, continue as normal.
const state = Waffle.state;
+
+ if (state[snakeHead.x][snakeHead.y] === 'snake') {
+ Waffle.alert('game over, man!');
+ clearInterval(intervalId);
+ }
// clear the current snake position
for (const {x, y} of snake) {
state[x][y] = '';
}
// push the new head on to the front of the `snake` array,
snake.unshift(snakeHead);
There it is! A (very) basic “snake” game. If you haven’t had enough yet, there are a few more features you can add.
Extra Credit
- Create a new apple every time the snake eats one
- Keep score — add a point for each apple eaten
- Make the snake move faster after eating an apple