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?ā
ā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:
- The HTML page
- The canvas for the game
- The main loop
- Keyboard controls
- 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.