Tetriminos

The pieces in Tetris are called Tetriminos.

There are 7 different Tetriminos, which are named after the letter that they look like. These are Z, S, J, T, L, I and O.

Each Tetrimino automatically moves towards the bottom of the grid. When it touches the bottom or another Tetrimino, it is locked into that position.

While the Tetrimino is falling, you are allowed to move it left, right and rotate it. The Tetriminocan be moved as far as the most left column, at which point it can move no further. The Tetriminocan be moved as far as the most right column, at which point it can move no further. When rotating a Tetrimino, it rotates on its axis.

The way that I will tackle each shape and its rotation is by drawing them in a square, made from squares: I takes up the most space as it is 4 squares in length. Therefore I will represent I and its rotation like this:

l tetrimino tetris html html5 css javascript canvas

Create a 2D array for each type of Tetrimino

As discussed, we will store the values of each Tetrimino and its rotations in a 2D array. However, technically JavaScript does not support multidimensional arrays! However, JavaScript is flexible enough to be able to emulate one by placing arrays in arrays!

const I = [
	[
		[0, 0, 0, 0],
		[1, 1, 1, 1],
		[0, 0, 0, 0],
		[0, 0, 0, 0],
	],
	[
		[0, 0, 1, 0],
		[0, 0, 1, 0],
		[0, 0, 1, 0],
		[0, 0, 1, 0],
	],
	[
		[0, 0, 0, 0],
		[0, 0, 0, 0],
		[1, 1, 1, 1],
		[0, 0, 0, 0],
	],
	[
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
		[0, 1, 0, 0],
	]
];

const J = [
	[
		[1, 0, 0],
		[1, 1, 1],
		[0, 0, 0]
	],
	[
		[0, 1, 1],
		[0, 1, 0],
		[0, 1, 0]
	],
	[
		[0, 0, 0],
		[1, 1, 1],
		[0, 0, 1]
	],
	[
		[0, 1, 0],
		[0, 1, 0],
		[1, 1, 0]
	]
];

const L = [
	[
		[0, 0, 1],
		[1, 1, 1],
		[0, 0, 0]
	],
	[
		[0, 1, 0],
		[0, 1, 0],
		[0, 1, 1]
	],
	[
		[0, 0, 0],
		[1, 1, 1],
		[1, 0, 0]
	],
	[
		[1, 1, 0],
		[0, 1, 0],
		[0, 1, 0]
	]
];

const O = [
	[
		
		[1, 1],
		[1, 1],
		
	]
];

const S = [
	[
		[0, 1, 1],
		[1, 1, 0],
		[0, 0, 0]
	],
	[
		[0, 1, 0],
		[0, 1, 1],
		[0, 0, 1]
	],
	[
		[0, 0, 0],
		[0, 1, 1],
		[1, 1, 0]
	],
	[
		[1, 0, 0],
		[1, 1, 0],
		[0, 1, 0]
	]
];

const T = [
	[
		[0, 1, 0],
		[1, 1, 1],
		[0, 0, 0]
	],
	[
		[0, 1, 0],
		[0, 1, 1],
		[0, 1, 0]
	],
	[
		[0, 0, 0],
		[1, 1, 1],
		[0, 1, 0]
	],
	[
		[0, 1, 0],
		[1, 1, 0],
		[0, 1, 0]
	]
];

const Z = [
	[
		[1, 1, 0],
		[0, 1, 1],
		[0, 0, 0]
	],
	[
		[0, 0, 1],
		[0, 1, 1],
		[0, 1, 0]
	],
	[
		[0, 0, 0],
		[1, 1, 0],
		[0, 1, 1]
	],
	[
		[0, 1, 0],
		[1, 1, 0],
		[1, 0, 0]
	]
];

Write HTML

<html>
<head>
    <title>Tetris</title>
</head>
<body>
    <div>
        Score <div id="score">0</div>
    </div>
<canvas id="tetris" width="200" height="400"></canvas>
</body>
</html>

Write CSS

<style>
        #score{
            display: inline-block;
        }
        div{
            font-size: 25px;
            font-weight: bold;
            font-family: monospace;
            text-align: center;
        }
        canvas{
            display: block;
            margin:0 auto;
        }
    </style>

Canvas

const cvs = document.getElementById("tetris");
const ctx = cvs.getContext("2d");
const scoreElement = document.getElementById("score");

Rows, columns and squares

const ROW = 20;
const COL = COLUMN = 10;
const SQ = squareSize = 20;
const VACANT = "WHITE"; // color of an empty square

Draw a square

// draw a square
function drawSquare(x,y,color){
    ctx.fillStyle = color;
    ctx.fillRect(x*SQ,y*SQ,SQ,SQ);

    ctx.strokeStyle = "BLACK";
    ctx.strokeRect(x*SQ,y*SQ,SQ,SQ);
}

Create the board

// create the board

let board = [];
for( r = 0; r <ROW; r++){
    board[r] = [];
    for(c = 0; c < COL; c++){
        board[r][c] = VACANT;
    }
}

Draw the board

// draw the board
function drawBoard(){
    for( r = 0; r <ROW; r++){
        for(c = 0; c < COL; c++){
            drawSquare(c,r,board[r][c]);
        }
    }
}

drawBoard();

Give the Tetriminos colour

The official Tetrimino colours are: O = yellow, I = light blue, T = purple, L = orange, J = dark blue, S = green and Z = red. I wish that they gave specific values (eg HEX). I thought that the CSS versions of these colours looked pleasing, except for the light blue. I felt that it was too light and didn’t stand out as well as the other colours. Light blue is the equivalent to #ADD8E6. I used this a starting point to find a more suitable light blue. I settled with #86c5da. I then created a 2D array to store the name of each Tetrimino and its colour.

const PIECES = [
    [Z,"red"],
    [S,"green"],
    [T,"purple"],
    [O,"yellow"],
    [L,"orange"],
    [I,"#86c5da"],
    [J,"dark blue"]
];

Generate the next random piece

To generate a random piece, we need to generate a random number. We can generate a random number by using a JavaScript method, Math.random().

Math.random();

On its own Math.random is not enough, as it will output a floating-point number. We can use another JavaScript method, Math.floor(), to make sure that our random number is an integer.

Math.floor(Math.random());

We need our random number to be between 1 and 7 (inclusive), as there are 7 different Tetriminos. We can use the .length() property to make sure that this happens.

function randomPiece(){
    Math.floor(Math.random() * PIECES.length);
}
// generate the next random piece

function randomPiece(){
    let r = randomN = Math.floor(Math.random() * PIECES.length);
    return new Piece(PIECES[r][0],PIECES[r][1]);
}

let p = randomPiece();

Object piece

function Piece(tetromino, color){
    this.tetromino = tetromino;
    this.color = color;
    
    this.tetrominoN = 0; // start from the first pattern
    this.activeTetromino = this.tetromino[this.tetrominoN];
    
    // control the pieces
    this.x = 3;
    this.y = -2;
}

Fill function

Piece.prototype.fill = function(color){
    for( r = 0; r < this.activeTetromino.length; r++){
        for(c = 0; c < this.activeTetromino.length; c++){

            // we draw only occupied squares
            if (this.activeTetromino[r][c]){
                drawSquare(this.x + c,this.y + r, color);
            }
        }
    }
}

Draw a piece on the board

Piece.prototype.draw = function(){
    this.fill(this.color);
}

Undraw piece

Piece.prototype.unDraw = function(){
    this.fill(VACANT);
}

Move piece down

Piece.prototype.moveDown = function(){
    if(!this.collision(0,1,this.activeTetromino)){
        this.unDraw();
        this.y++;
        this.draw();
    } else {
        // we lock the piece and generate a new one
        this.lock();
        p = randomPiece();
    }
    
}

Move piece to the right

Piece.prototype.moveRight = function(){
    if (!this.collision(1,0,this.activeTetromino)){
        this.unDraw();
        this.x++;
        this.draw();
    }
}

Move piece to the left

Piece.prototype.moveLeft = function(){
    if (!this.collision(-1,0,this.activeTetromino)){
        this.unDraw();
        this.x--;
        this.draw();
    }
}

Rotate a piece

Piece.prototype.rotate = function(){
    let nextPattern = this.tetromino[(this.tetrominoN + 1)%this.tetromino.length];
    let kick = 0;
    
    if (this.collision(0,0,nextPattern)){
        if (this.x > COL/2){
            // it's the right wall
            kick = -1; // we need to move the piece to the left
        } else {
            // it's the left wall
            kick = 1; // we need to move the piece to the right
        }
    }
    
    if (!this.collision(kick,0,nextPattern)){
        this.unDraw();
        this.x += kick;
        this.tetrominoN = (this.tetrominoN + 1)%this.tetromino.length; // (0+1)%4 => 1
        this.activeTetromino = this.tetromino[this.tetrominoN];
        this.draw();
    }
}

let score = 0;

Piece.prototype.lock = function(){
    for(r = 0; r < this.activeTetromino.length; r++){
        for(c = 0; c < this.activeTetromino.length; c++){
            // we skip the vacant squares
            if (!this.activeTetromino[r][c]){
                continue;
            }

            // pieces to lock on top = game over
            if (this.y + r < 0){
                alert("Game Over");
                // stop request animation frame
                gameOver = true;
                break;
            }
            // we lock the piece

            board[this.y+r][this.x+c] = this.color;
        }
    }

    // remove full rows
    for(r = 0; r < ROW; r++){
        let isRowFull = true;

        for( c = 0; c < COL; c++){
            isRowFull = isRowFull && (board[r][c] != VACANT);
        }
        if (isRowFull){
            // if the row is full

            // we move down all the rows above it
            for ( y = r; y > 1; y--){
                for( c = 0; c < COL; c++){
                    board[y][c] = board[y-1][c];
                }
            }
            // the top row board[0][..] has no row above it
            for ( c = 0; c < COL; c++){
                board[0][c] = VACANT;
            }
            // increment the score
            score += 10;
        }
    }
    // update the board
    drawBoard();
    
    // update the score
    scoreElement.innerHTML = score;
}

// collision fucntion

Piece.prototype.collision = function(x,y,piece){
    for( r = 0; r < piece.length; r++){
        for(c = 0; c < piece.length; c++){
            // if the square is empty, we skip it
            if(!piece[r][c]){
                continue;
            }
            // coordinates of the piece after movement
            let newX = this.x + c + x;
            let newY = this.y + r + y;
            
            // conditions
            if(newX < 0 || newX >= COL || newY >= ROW){
                return true;
            }
            // skip newY < 0; board[-1] will crush our game
            if(newY < 0){
                continue;
            }
            // check if there is a locked piece alrady in place
            if( board[newY][newX] != VACANT){
                return true;
            }
        }
    }
    return false;
}

Controlling Tetriminos

To control Tetriminos I have used an event listener. There is a JavaScript event listener method, addEventListener() that I have used to fire when one of the arrow keys have been pressed. I have chosen the left arrow for left, right arrow for right, down arrow for down and hard down and up for rotation.

I added two parameters to the addEventsListener method, keydown and CONTROL. Keydown tells the event listener that we want to respond to when a key is pressed down. CONTROL is a function that will set out what should happen when the arrow keys are pressed.

Why keydown?

As well as keydown, there is also a keyup and keypress. If you don’t know why I chose keydown, read my article on JavaScript event listeners.
document.addEventListener("keydown", CONTROL);

Next, I wrote a function (CONTROL) that when called runs an if statement to determine what to do if one of the arrow keys are pressed.

function CONTROL(event){
    if (event.keyCode == 37){
        p.moveLeft();
        dropStart = Date.now();
    } else if (event.keyCode == 38){
        p.rotate();
        dropStart = Date.now();
    } else if (event.keyCode == 39){
        p.moveRight();
        dropStart = Date.now();
    } else if (event.keyCode == 40){
        p.moveDown();
    }
}
let dropStart = Date.now();
let gameOver = false;
function drop(){
    let now = Date.now();
    let delta = now - dropStart;
    if(delta > 1000){
        p.moveDown();
        dropStart = Date.now();
    }
    if( !gameOver){
        requestAnimationFrame(drop);
    }
}

drop();
Piece.prototype.rotate = function(){
    let nextPattern = this.tetromino[(this.tetrominoN + 1)%this.tetromino.length];
    let kick = 0;
    
    if(this.collision(0,0,nextPattern)){
        if(this.x > COL/2){
            // it's the right wall
            kick = -1; // we need to move the piece to the left
        }else{
            // it's the left wall
            kick = 1; // we need to move the piece to the right
        }
    }
    
    if(!this.collision(kick,0,nextPattern)){
        this.unDraw();
        this.x += kick;
        this.tetrominoN = (this.tetrominoN + 1)%this.tetromino.length; // (0+1)%4 => 1
        this.activeTetromino = this.tetromino[this.tetrominoN];
        this.draw();
    }
}

let score = 0;

Piece.prototype.lock = function(){
    for( r = 0; r < this.activeTetromino.length; r++){
        for(c = 0; c < this.activeTetromino.length; c++){
            // we skip the vacant squares
            if( !this.activeTetromino[r][c]){
                continue;
            }
            // pieces to lock on top = game over
            if(this.y + r < 0){
                alert("Game Over");
                // stop request animation frame
                gameOver = true;
                break;
            }
            // we lock the piece
            board[this.y+r][this.x+c] = this.color;
        }
    }

    // remove full rows
    for(r = 0; r < ROW; r++){
        let isRowFull = true;
        for( c = 0; c < COL; c++){
            isRowFull = isRowFull && (board[r][c] != VACANT);
        }
        if(isRowFull){
            // if the row is full
            // we move down all the rows above it
            for( y = r; y > 1; y--){
                for( c = 0; c < COL; c++){
                    board[y][c] = board[y-1][c];
                }
            }
            // the top row board[0][..] has no row above it
            for( c = 0; c < COL; c++){
                board[0][c] = VACANT;
            }
            // increment the score
            score += 10;
        }
    }

    // update the board
    drawBoard();
    
    // update the score
    scoreElement.innerHTML = score;
}

// collision fucntion

Piece.prototype.collision = function(x,y,piece){
    for( r = 0; r < piece.length; r++){
        for(c = 0; c < piece.length; c++){
            // if the square is empty, we skip it
            if(!piece[r][c]){
                continue;
            }
            // coordinates of the piece after movement
            let newX = this.x + c + x;
            let newY = this.y + r + y;
            
            // conditions
            if(newX < 0 || newX >= COL || newY >= ROW){
                return true;
            }
            // skip newY < 0; board[-1] will crush our game
            if(newY < 0){
                continue;
            }
            // check if there is a locked piece alrady in place
            if( board[newY][newX] != VACANT){
                return true;
            }
        }
    }
    return false;
}

// CONTROL the piece
document.addEventListener("keydown",CONTROL);

function CONTROL(event){
    if(event.keyCode == 37){
        p.moveLeft();
        dropStart = Date.now();
    }else if(event.keyCode == 38){
        p.rotate();
        dropStart = Date.now();
    }else if(event.keyCode == 39){
        p.moveRight();
        dropStart = Date.now();
    }else if(event.keyCode == 40){
        p.moveDown();
    }
}

// drop the piece every 1sec

let dropStart = Date.now();
let gameOver = false;
function drop(){
    let now = Date.now();
    let delta = now - dropStart;
    if (delta > 1000) {
        p.moveDown();
        dropStart = Date.now();
    }
    if (!gameOver) {
        requestAnimationFrame(drop);
    }
}

drop();