Tetris is one of the first games that many of us played as youth. It's fun and challenging and a 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 that will reset after a game over state has been achieved. Let's get started.

Building Tetris In JavaScript Part 1

Step 0. Define a few variables

You can never have too many variables. A motto to code by. This isn't 1950, so memory is more than plentiful. Sometimes a single variable declaration can change the shape of your entire codebase. Here are all the variables that were defined 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;

Step 1. Make the game board

First off, let's make a game board for our game. Based on old specs and images, the number of horizontal running boxes is 10 and I'll be making the height negligible on this one. The board itself will be comprised of div elements created dynamically in JavaScript.

HTML

    <div class="tetris-board"></div>

JavaScript

 var board = document.getElementsByClassName('tetris-board')[0];
    board.innerHTML = '';
    var counter = 0;
    for (var y = 0; y < height; y++)
    {
        var row = document.createElement('div');
        row.className = 'row';
        row.dataset.row = y;
        
        for (var x = 0; x < width; x++)
        {
            var 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 contains several dataset properties that will be used later to determine current state and the index of said block.

Step 2. Create shapes

A Tetris block is essentially a series of 0s and 1s. You can think of it as something like the following.

[0, 0]
[1, 0]
[2, 0]
[0, 1]
[1, 1]
[2, 1]

So if we treat the board as a series of X, Y coordinates, then we'll know exactly how to draw our shapes based its own coordinates. Continuing with this logic, we can create


function createShapes()
{
    var other = [[1, 0], [0,1], [1,1],[2,1]]; // 
    var line = [[0, 0], [0, 1], [0, 2], [0, 3]]; // line
    var square = [[0, 0], [0, 1], [1, 0], [1, 1]];
    var l = [[2,0], [0, 1], [1, 1], [2,1]];
    shapes.push(square);
    shapes.push(line);
    shapes.push(other);
    shapes.push(l);
}

We run this function once on page load, which will populate an array of arrays. We'll be able to pull from this array a random index when we render it onto the game board.

Step 3. Draw a shape onto the board

Next up, now that we have our essentials out of the way, 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 our shapes array, a random color, selected from our colors array,


function createShape()
{
    var randomShape = Math.floor(Math.random() * shapes.length);
    var randomColor = Math.floor(Math.random() * colors.length);
    var center = Math.floor(width / 2);
    var shape = shapes[randomShape];
    var 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.

Building Tetris In JavaScript Part 1

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.

  1. Calculate the board coordinates based on offset and shape selected
  2. For each block in the given shape, set the state of the board block to "occupied"
  3. Pretty much it

That's pretty much it for actually drawing the shape onto the board. Tetris is game of 1's and 0's if you think about it. Each move we're toggling on and off grid blocks until two 1's collide.


function drawShape()
{
    // draw the current shape onto board
    var shape = currentShape.shape;
    var 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(var i = 0; i < shape.length; i++)
    {
        var x = shape[i][0] + location[0];    //  x + offset
        var y = shape[i][1] + location[1];    // y + offset
        var 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);
}

function clearCurrent()
{
    // reset all blocks
    var shape = currentShape.shape;
    var location = currentShape.location;
    
    for(var i = 0; i < shape.length; i++)
    {
        var x = shape[i][0] + location[0];
        var y = shape[i][1] + location[1];
        var block = document.querySelector('[data-x="' + x + '"][data-y="' + y + '"]');
        block.classList.remove('filled');
        block.style.backgroundColor="";
    }
}

The first thing we do is to clear any previous blocks that were filled. In this way, drawShape() will be called each time we have a block movement. Next up, based on the direction that our shape is moving, we will update the offset accordingly to any of the 3 allowed directions. And we'll redraw the shape right after that.

Next up, let's handle the keyboard events in order to move our shape around.

Step 5. Keyboard movement

Moving our shape around is relatively simple thanks to the offset logic. We specify which direction we expect the shape to go, and we update the offset accordingly. The drawShape() function will handle rendering the shape.


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();
}

function start()
{
    createBoard();
    createShapes();
    createShape();
    drawShape();
    document.onkeydown = checkKey;
}

Let's see it up and running.

Beautiful. You should be able to move a random shape around the board at this point. That's it for this part 1 of the Tetris tutorial. It hopefully gives an idea of how the game can be transcribed into a series of binary manipulations on an 'X' x 'Y' board.

In the next part, we're going to go over collisions, both with the surrounding walls and with other shapes. Happy coding!

Check out part 2 right over here!

Comments