Tetris is one of the first games that many of us played growing up. It's fun and challenging and playing a single level can take you from anywhere to a minute to forever if you so play your cards right. So in honor of the game, in this blog post, we'll go over how to build a Tetris clone in JavaScript without using any 3rd party libraries. Just plain old vanilla JavaScript and CSS.
To keep the example as simple as possible, this will be a single level of Tetris game that will reset after a game over state has been achieved.
With that, let's get started. The following is the full "skeleton" view of the game board and the underlying coordinate system that we will be building, just to give you a preemptive look at what is to come.
If it looks like a confusing mess of color and numbers, worry not as it will all be explained down below.
Step 1. Defining variables
Here are all of the global variables that will need to be defined at the top of the script in the making of this game.
var shapes = new Array();
var currentShape;
var height = 15;
var width = 10;
var state = 1; // 1 running - 0 paused - 2 game over
var colors = ['black', 'orange', 'red', 'blue'];
var move = 0;
var occupiedblocks = new Array();
var direction = "";
var points = 0;
Here is a breakdown of each of the variables and what they are designed to do.
- shapes => a collection (Array) of the possible shapes that can be drawn.
- currentShape => the current shape in play (Square, Line, etc)
- height => height of the game board (number of rows)
- width => width of the game board (number of columns)
- state => used to monitor the current state of the gameplay
- colors => available colors to use when rendering shapes
- move => tracks the number of moves the user has made so far
- occupiedBlocks => array containing a list of all current blocks on the board that are filled
- direction => 'up', 'down', 'left', 'right' string used to track where to draw the falling block next
- points => running tally of total points
There will be many more variables used throughout the course of this game, however most will be local to each individual rendering function.
Step 2. Making the game board
First off, let's make the game board where the game will take place. Based on the actual game specs for Tetris, the number of horizontal running boxes is 10 and we can take some liberty with the height. The board itself will be comprised of div elements created dynamically in JavaScript.
HTML
<div class="tetris-board"></div>
JavaScript
function createBoard()
{
let board = document.getElementsByClassName('tetris-board')[0];
board.innerHTML = '';
let counter = 0;
for (let y = 0; y < height; y++)
{
let row = document.createElement('div');
row.className = 'row';
row.dataset.row = y;
for (let x = 0; x < width; x++)
{
let block = document.createElement('div');
block.className = 'block';
block.dataset.x = x;
block.dataset.y = y;
block.dataset.index = counter;
block.dataset.state = 0;
block.innerHTML = "0 : " + counter;
row.appendChild(block);
counter++;
}
board.appendChild(row);
}
}
Each block on the grid will contain several dataset properties that will be used later to determine whether the block is occupied and also the index of said block.
In this particular case, we'll set the state to '0' if the block is empty.
Step 3. Creating the Tetris shapes
A Tetris block is essentially a series of 0s and 1s on a coordinate system. You can think of it as something like the following:
[0, 0]
[1, 0]
[2, 0]
[0, 1]
[1, 1]
[2, 1]
If we treat the board as a series of [X, Y] coordinates, then we'll know exactly how to draw each of the shapes based on their own predefined coordinates. Continuing with this logic, we can create the shapes as follows:
function createShapes()
{
let other = [[1, 0], [0,1], [1,1],[2,1]]; // 't' shape
let line = [[0, 0], [0, 1], [0, 2], [0, 3]]; // line
let square = [[0, 0], [0, 1], [1, 0], [1, 1]]; // 4 x 4 square
let l = [[2,0], [0, 1], [1, 1], [2,1]]; // 'L' shape
shapes.push(square);
shapes.push(line);
shapes.push(other);
shapes.push(l);
}
We run this function once on page load, which will populate the shapes global variable. We'll be able to pull from this array a random index when we render it onto the game board.
Step 4. Draw a shape onto the board
Next up we can begin to draw elements on to the board. Because we can only have one active shape at a time on our board, we can store this in a variable, which I've dubbed currentShape. This variable will get instantiated with a random shape, selected from the shapes array, a random color, selected from the colors array, the location variable which is an array of default [x,y] coordinates, and indexes, which represent all of the blocks that the shape will take up.
function createShape()
{
let randomShape = Math.floor(Math.random() * shapes.length);
let randomColor = Math.floor(Math.random() * colors.length);
let center = Math.floor(width / 2);
let shape = shapes[randomShape];
let location = [center, 0];
currentShape = {
shape: shape,
color: colors[randomColor],
location: location,
indexes: getBlockNumbers(shape, location)
};
}
In order to maintain the shapes spatial positioning on the board, we're going to be keeping track of the top-left corner block on the board as the offset. By default, this will be set with a 'y' coordinate of 0 for the top of the board. We won't have to worry about the indexes property for now as that will be used for collision detection later on in the process. That will be covered in Part 2 of this post, which is linked to down below.
For now add the following placeholder function in lieu of that implementation:
function getBlockNumbers(a, b){
// tba
}
Shape Coordinates + offset = actual position
Given the shape coordinates and offset, we can now draw our shape onto the board. This isn't too difficult and will essentially come down to the following steps:
- Calculate the board coordinates based on offset and shape selected
- For each block in the given shape, set the state of the board block to "occupied", or 1
- That's it!
That's pretty much it for actually drawing the shape onto the board. Tetris is a game of toggling 1's and 0's on a timer tick and checking for certain 'collisions'.
The drawShape() function is as follows:
function drawShape()
{
// draw the current shape onto board
let shape = currentShape.shape;
let location = currentShape.location;
// update status to unoccupied of current block
clearCurrent();
// based on direction of block, set the offset
if (direction=="down")
currentShape.location[1]++;
else if(direction=="left")
currentShape.location[0]--;
else if (direction=="right")
currentShape.location[0]++;
// redraw the shape onto the board
for(let i = 0; i < shape.length; i++)
{
let x = shape[i][0] + location[0]; // x + offset
let y = shape[i][1] + location[1]; // y + offset
let block = document.querySelector('[data-x="' + x + '"][data-y="' + y + '"]');
block.classList.add('filled');
block.style.backgroundColor = currentShape.color;
}
currentShape.indexes = getBlockNumbers(currentShape.shape, currentShape.location);
}
Let's run through a quick breakdown of the function. We start off by keeping a local variable for the currentShape's shape and location. Recall that this variable was initialized above in the createShape() function.
Next up we are making a call to the clearCurrent() function, whose job it is clear the current location of the shape, in order to draw it at its next location.
And clearCurrent() is as follows:
function clearCurrent()
{
// reset all blocks
let shape = currentShape.shape;
let location = currentShape.location;
for(let i = 0; i < shape.length; i++)
{
let x = shape[i][0] + location[0];
let y = shape[i][1] + location[1];
let block = document.querySelector('[data-x="' + x + '"][data-y="' + y + '"]');
block.classList.remove('filled');
block.style.backgroundColor="";
}
}
After clearing the current blocks, we need to update the current shapes coordinates to its new location. This will vary depending on the direction that the block is currently moving in.
// based on direction of block, set the offset
if (direction=="down")
currentShape.location[1]++;
else if(direction=="left")
currentShape.location[0]--;
else if (direction=="right")
currentShape.location[0]++;
The movement really comes down to updating the current shapes X and/or Y coordinates and then redrawing the shape there.
// redraw the shape onto the board
for(let i = 0; i < shape.length; i++)
{
let x = shape[i][0] + location[0]; // x + offset
let y = shape[i][1] + location[1]; // y + offset
let block = document.querySelector('[data-x="' + x + '"][data-y="' + y + '"]');
block.classList.add('filled');
block.style.backgroundColor = currentShape.color;
}
Occupying a space will come down to adding the 'filled' class to the game boards blocks and setting that particular blocks backgroundColor to whatever current color is in play.
Next up, let's handle the keyboard events in order to move our shape around.
Step 5. Keyboard events
Moving the shape around is relatively simple thanks to the offset logic discussed above. We specify which direction we expect the shape to go, and we update the offset accordingly by updating the X or Y coordinates. The drawShape() function will handle rendering the shape after.
function checkKey(e) {
e.preventDefault();
e = e || window.event;
if (e.keyCode == '40') {
// down arrow
direction="down";
}
else if (e.keyCode == '37') {
// left arrow
direction="left";
}
else if (e.keyCode == '39') {
// right arrow
direction="right";
}
drawShape();
}
And lastly, but firstly, we will define the function that will start and initialize the game.
function start()
{
createBoard();
createShapes();
createShape();
drawShape();
document.onkeydown = checkKey;
}
window.addEventListener('load', function(){
start();
});
Let's see it up and running.
You should be able to move a random shape around the board at this point. But the foundation for the rest of the game has been taken care of at this point.
That's it for this part 1 of the Tetris tutorial.
You can download the full source along with the CSS right over here.
Or you can view it running over on Codepen.io.
In the next part, we're going to go over collisions, both with the surrounding walls and with other shapes, and we can start to get into some of the more complex game elements of Tetris. Happy coding!
Check out part 2 right over here!