Making a tetris game

Published on Aug 29, 2023

I started this blog post as a tutorial to write a Tetris game, while I myself figured out how to do this. This tutorial introduces the basic concepts that you need for building the game.
I did not finish writing the tutorial like I intended to -- where you would have your own tetris game by the end. However, this is enough knoweldge to get you going with your first browser based tetris game!

Tetris Unveiled: What is Tetris and Tetrominos?

What is Tetris?

Before we dive into the nitty-gritty of building our Tetris game, let’s take a moment to appreciate the timeless charm of Tetris itself.

Born in the realm of video games, Tetris is all about arranging falling blocks in a way that makes them fit snugly like puzzle pieces.

Clearing lines and watching those blocks disappear is a satisfaction like no other!

What is a Tetromino?

You know what it is, just didn’t know this name.

The falling pieces in the game are called tetrominoes.

These intriguing shapes are formed from four equally-sized blocks and come in a set of seven unique designs.

Now let us look at them:

I or Straight Tetromino

Shape:

▣▣▣▣

Matrix Representation:

[1, 1, 1, 1]

L Tetromino Shape:

▣□
▣□
▣▣

Matrix Representation:

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

J Tetromino

Shape:

□▣
□▣
▣▣

Matrix Representation:

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

O or Square Tetromino

Shape:

▣▣
▣▣

Matrix Representation:

[1, 1],
[1, 1]

S Tetromino

Shape:

□▣▣
▣▣□

Matrix Representation:

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

T Tetromino

Shape:

□▣□
▣▣▣

Matrix Representation:

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

Z Tetromino

▣▣□
□▣▣

Matrix Representation:

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

The Game Board

How to Represent the Board

Our Tetris adventure needs a playground, right? That’s where the game board comes in. Imagine a 16x8 grid where all the magic happens. But how do we represent this board? And how do we draw it on a canvas? Let’s find out!

Look at this code:

let drawBoard = document.getElementById('draw-board');
const rows = 16;
const columns = 8;

const matrix = generateRandomMatrix(rows, columns);

for (let i = 0; i < matrix.length; i++) {
   let row = matrix[i];
   let rowText = '';
   for (let j = 0; j < row.length; j++) {
      rowText += matrix[i][j] === 1 ? '' : '';
   }
   drawBoard.appendChild(document.createTextNode(rowText));
   drawBoard.appendChild(document.createElement('br'));
}
  • The code selects an HTML element with the ID ‘draw-board’.
  • It defines the dimensions of a grid with 16 rows and 8 columns.
  • Using a function called generateRandomMatrix, it creates a matrix filled with random 0s and 1s.
  • It iterates through each row of the matrix and constructs a rowText string.
  • For each element in the row, it appends ‘▣’ if the value is 1, or ‘□’ if the value is 0, and adds a line break after each row to visually represent the matrix within the ‘draw-board’ element.

What do we get when running this?

See the full JS for this demo

Concept of Game Loop and Animation

Time to add some life to our game! We’re going to dive into the world of animation and create a smooth movement for our tetrominos. Brace yourself for the mesmerizing sight of blocks gracefully descending down the canvas.

How to Move a Block on the Board

The secret to animation lies in the game loop – a continuous loop that updates the game state and redraws the canvas. Within this loop, we’ll move our tetrominos step by step, creating the illusion of motion.

Let us look at some sekelton javascript code that would help us implement a game loop for our game:

// Namespace everything under tetris
// We do this by setting up all our functions and constants 
// as properties of a "tetris" object
let tetris = {
   renderWorld: function renderWorld() {
      // Render the game world visually
      // Remember the snippet we used to draw a board before?,
      // You can use the 'world' array to generate a visual representation
   },
   updateWorld: function updateWorld(world){
      // Update the world according to the game's rules
      // and return the modified world
   }

   // The function that runs once per frame, progressing the game
   gameLoop: function gameLoop(world, updateInterval){
    // Update the game world's state
    let updatedWorld = this.updateWorld(world); 
   
   // Draw the current state of the world 
    this.renderWorld(updatedWorld); 

   // Save the current context to a variable 'game'
   game = this;
   // To make a "loop", set the gameLoop function to be
   // called again at the next interval, but with updatedWorld
   // from this frame
    setTimeout(
      function(){
         game.gameLoop(updatedWorld,updateInterval);
      }
      , updateInterval
      )
   }
}

let world = [
    [0, 1, 0],
    [1, 0, 1],
    [0, 1, 0]
];
// Set the frame rate at which we want the 
// game to run.
// With a frameRate of 60, we want the game to 
// progress 60 frames in a second.
const frameRate = 60,


// Calculate the time interval between the frames
const updateInterval = 1000 / this.frameRate,
tetris.gameLoop(world, updateInterval);

An example game loop

Now let’s try making a functioning game loop. Here is a updateWorld function that blinks a block in our game:

function updateWorld(world) {
    // In this game, simply toggle the block at 0,1
    nextVal = world[0][1] == 1 ? 0 : 1
    world[0][1] = nextVal;
    return world;
}

See the full JS for this demo

Adding a tetromino to the empty board.

We saw so far that our board can be represented as a two dimensional array or a matrix.

We have also seen we can represent tetrominoes by smaller matrices.

But to represent a tetromino moving in our board, we will need to represent where the tetromino is on our board.

Which means, we now need, x and y - two co-ordinates representing the position of the tetromino on our board.

So let us represent a tetromino as

tetromino = { 
               x: 1,
               y: 2,
               shape: [
                        [0,1,0],
                        [1,1,1]
                      ]
            }

To place the tetromino on the board, we have to look up the x and y on the board, and flip the correct bits to 1 or 0.

Let us try placing the above tetromino on a 16x8 board.

Look at the following function, that takes a matrix called board, and a tetromino object. The function updates the board matrix with the bits in tetromino.

   function placeOnBoard(board, tetromino) {
        boardClone = structuredClone(board);
        for (i = 0; i < tetromino.shape.length; i++) {
            row = tetromino.shape[i];
            for (j = 0; j < row.length; j++) {
                block = row[j];
                let boardX = tetromino.x + j;
                let boardY = tetromino.y + i;
                if (boardY < boardClone.length) {
                    if (boardX < boardClone[boardY].length) {
                        boardClone[boardY][boardX] = block
                    }
                }
            }
        }
        return boardClone;
    }

Let us integrate this newly found tool to our game loop:

See the full JS for this demo

Make them move!

Let’s bring more life to our game. To be closer to a playable Tetris, we should have the following elements in our game:

  1. A random Tetromino should fall onto the board
  2. The Tetromino should keep falling until it hits something
  3. The player should be able to rotate and move the falling Tetromino

Let’s dive into each of these requirements in detail:

How to Make Tetrominoes Fall onto the Board

To make Tetrominoes fall onto the game board, we’ll follow these steps:

  1. Generate Random Tetromino: First, we need to select a random Tetromino from our predefined Tetrominoes. This random Tetromino will become the one that falls onto the board.

  2. Initial Position: We’ll set the initial position of the Tetromino at the top of the game board. This typically means placing it at the top row, centered horizontally.

  3. Falling Mechanism: The Tetromino should start moving downward. This can be done by updating its position vertically at regular intervals. You can use a timer or requestAnimationFrame to control this.

  4. Render Tetromino: As the Tetromino falls, we need to continuously render it on the game board, updating its position based on the falling mechanism.

Here’s the code to make Tetrominoes fall onto the board:

// Generate a random Tetromino
const tetromino = getRandomTetromino();

// Set its initial position at the top center
tetromino.x = Math.floor(boardWidth / 2) - Math.floor(tetromino.shape[0].length / 2);
tetromino.y = 0;

// Define a falling interval (e.g., every 1 second)
const fallingInterval = 1000;

// Start the Tetromino falling loop
const fallTimer = setInterval(() => {
    // Move the Tetromino downward
    tetromino.y++;

    // Check for collisions and handle them
    if (collisionDetected()) {
        // Stop falling and place the Tetromino on the board
        clearInterval(fallTimer);
        placeTetrominoOnBoard();
    } else {
        // Continue rendering the falling Tetromino
        renderGameBoard();
    }
}, fallingInterval);

How to Make Tetrominoes Keep Falling Until They Hit Something

To ensure Tetrominoes keep falling until they hit something, we use a loop that repeatedly moves the Tetromino downward. We also need to implement collision detection to know when the Tetromino has landed. Here’s how:

  1. Falling Loop: Set up a loop that continually moves the Tetromino downward at a regular interval.

  2. Collision Detection: Inside the loop, check for collisions with existing blocks on the game board or the board boundaries. If a collision is detected, stop the loop.

  3. Placement on the Board: If a collision occurs, place the Tetromino on the board at its current position.

This ensures that Tetrominoes keep falling until they hit something, at which point they become part of the game board.

How to Enable Player Control: Rotation and Movement

Allowing players to rotate and move the falling Tetromino adds interactivity to the game. Here’s how to implement this:

  1. Keyboard Event Listeners: Set up event listeners to capture player input via keyboard controls. Common controls include arrow keys for movement and a rotation key (e.g., the “up” arrow) for rotation.

  2. Movement Functions: Create functions that handle the Tetromino’s left, right, and downward movement based on player input. These functions should update the Tetromino’s position accordingly.

  3. Rotation Function: Implement a function that rotates the Tetromino when the player presses the rotation key. Ensure the rotation is valid and doesn’t result in collisions.

By implementing these controls, players can actively influence the placement of Tetrominoes on the game board, making the game more engaging and strategic.

Now that we’ve covered these essential gameplay elements, we’re one step closer to having a fully playable Tetris game.

See the full JS for this demo

To implement the seemingly simple looking game of tetris, we still need to explore how to detect and handle collisions effectively, ensuring that Tetrominoes interact seamlessly with the game board.

If you’ve read so far and the idea is interesting for you, I encourage you to checkout my current version of the game here : /tetris. It is still implemented with the ideas you saw in this page, but constantly updated as I learn more.

Good luck building your own game! Have fun!