Vanilla JavaScript Tetris, because everybody should build a Tetris game

Talking about Tetris game, Game development and Javascript.

Today I am showing you a Tetris game written in vanilla JavaScript.

Everybody should be able to build a Tetris game, but, beware that it is not as simple as it seems.

In my fist version, there are three important things missing: wall kicking, Super Rotation System and empty lines above the celing.

Wall kicking is a game mechanic that allows a tetromino (a falling piece) to rotate even when it is partially obstructed by a wall or another block. Instead of the rotation failing, the game attempts to “kick” the piece away from the obstruction by shifting it slightly in one or more directions.

Super Rotation System defines how each tetromino rotates and includes wall kicks to allow smoother movement near walls or obstacles, this way:

All tetrominoes except the O-piece rotate around a central “rotation point.”

Clockwise and counterclockwise rotations use predefined tests for wall kicks.

Wall kicks attempt to shift the piece left, right, or up when the rotation is blocked.

My prototype just rotates arrays.

Empty lines above the ceiling prevent instant game over if a tetromino spawns partially inside a stack of blocks, giving the player a chance to move or rotate it before it locks in place.

A little harder than you imagined, I suppose.

At the moment, here is my prototype in action:

Use ARROW KEYS to move, rotate or drop a piece. You can also play from this page.

And this is the source code, all in one page:

JavaScript
<!DOCTYPE html>
<html lang="it">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tetris</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #222;
            margin: 0;
        }
        canvas {
            background-color: black;
            border: 2px solid white;
        }
    </style>
</head>
<body>
    <canvas id="thegame"></canvas>
    <script>
    
        // get canvas
        const canvas = document.getElementById('thegame');
        const context = canvas.getContext('2d');   
        
        // global constants
        const boardRows     = 20;   // board rows
        const boardColumns  = 10;   // board column
        const tileSize      = 16;   // tile size, in pixels 
        const board         = [];   // game board 
        
        let dropInterval  = 500;    // drop interval, in milliseconds
        
        // size and scale canvas according to tile and board size
        canvas.width = boardColumns * tileSize;
        canvas.height = boardRows * tileSize;
        context.scale(tileSize, tileSize);
        
        // array with all tetrominoes
        const tetrominoes = [                       
        
            // I piece
            {                                       
                color : '#00FFFF',                  
                shape: [[1, 1, 1, 1]]
            
            },
            
            // O piece
            {                                       
                color : '#FFFF00',                  
                shape: [[1, 1], [1, 1]]
            
            },
            
            // T piece
            {                                       
                color : '#800080',
                shape: [[0, 1, 0], [1, 1, 1]]       
            
            },
            
            // S piece
            {                                       
                color : '#00A000',
                shape: [[0, 1, 1], [1, 1, 0]]
            
            },
            
            // Z piece
            {
                color : '#A00000',
                shape: [[1, 1, 0], [0, 1, 1]]
            
            },
            
            // J piece
            {
                color : '#0000A0',
                shape : [[1, 0, 0], [1, 1, 1]]
            
            },
            
            // L pieace
            {
                color : '#F0A000',
                shape:[[0, 0, 1], [1, 1, 1]]
            
            }
        ];
        
        // get a random tetromino
        let randomTetromino = getRandomTetromino();
        let currentTetromino = randomTetromino.shape;
        let currentColor = randomTetromino.color;
        let tetrominoPosition = {
            row     : 0,
            column  : 3 
        };
        
        // drop timer
        let dropTimer = 0;
        
        // time since previous frame
        let lastTime = 0; 
        
        // initialize the board
        initializeBoard();
        
        // call update method
        update();
        
        // function to initialize the board, setting all values to zero
        function initializeBoard() {
            for (let i = 0; i < boardRows; i ++) {
                board[i] = [];
                for (let j = 0; j < boardColumns; j ++) {
                    board[i][j] = 0;
                } 
            }
        }
        
        // game loop
        // time: amount of milliseconds passed since script beginning
        function update(time = 0) {
        
            // elapsed time since previous time
            const deltaTime = time - lastTime;
            lastTime = time;
            dropTimer += deltaTime;
            
            // if drop timer is bigger than drop interval, it's time to make the tetromino drop
            if (dropTimer > dropInterval) {      
            
                // reset drop timer
                dropTimer = 0;
                
                // move tetromino down
                tetrominoPosition.row ++;
                
                // is tetromino colloding?
                if (isTetrominoColliding()) {
                
                    // move tetromino up
                    tetrominoPosition.row --;
                    
                    // merge tetromino
                    mergeTetromino();
                    
                    // clear lines if needed
                    clearLines();
                    
                    // place a new tetromino
                    randomTetromino = getRandomTetromino();
                    currentTetromino = randomTetromino.shape;
                    currentColor = randomTetromino.color;
                    tetrominoPosition = { 
                        row     : 0,
                        column  : 3 
                    };
                    
                    // is newborn tetromino already colliding?
                    if (isTetrominoColliding()) {
                    
                        // GAME OVER!! (just clear the board in this case)
                        initializeBoard();
                    }
                }
            }
            
            // draw the board
            drawBoard();
            
            // draw the falling tetromino
            drawTetromino();
            
            // request new frame and call update function (this one)
            requestAnimationFrame(update);
        }

        // function to get a random tetromino
        function getRandomTetromino() {
            return tetrominoes[Math.floor(Math.random() * tetrominoes.length)];
        }

        // function to rotate a tetromino
        // clockwise: rotate clockwise if true
        // it just creates a new array inverting rows with columns
        function rotateTetromino(tetromino, clockwise) {
            const rows = tetromino.length;
            const columns = tetromino[0].length;
            let rotatedTetromino = [];
            if (clockwise) {
                for (let i = 0; i < columns; i ++) {
                    rotatedTetromino[i] = []; 
                    for (let j = rows - 1; j >= 0; j --) {
                        rotatedTetromino[i].push(tetromino[j][i]);
                    }
                }
            } 
            else {
                for (let i = columns - 1; i >= 0; i --) {
                    let newRow = [];                    
                    for (let j = 0; j < rows; j ++) {
                        newRow.push(tetromino[j][i]);
                    }
                    rotatedTetromino.push(newRow);
                }
            }
            return rotatedTetromino;
        }
        
        // functions to merge a tetromino with game board
        function mergeTetromino() {
            for (let i = 0; i < currentTetromino.length; i ++) {
                for (let j = 0; j < currentTetromino[i].length; j ++) {                   
                    if (currentTetromino[i][j] != 0) {
                        board[tetrominoPosition.row + i][tetrominoPosition.column + j] = currentColor;
                    } 
                }
            }  
        }
        
        // function to check if a tetromino is colliding
        function isTetrominoColliding() {
            for (let i = 0; i < currentTetromino.length; i ++) {
                for (let j = 0; j < currentTetromino[i].length; j ++) { 
                    if (currentTetromino[i][j] != 0) {
                        const row = tetrominoPosition.row + i;
                        const column = tetrominoPosition.column + j;
                        if (row < 0 || column < 0 || row >= boardRows || column >= boardColumns || board[row][column] != 0) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        
        // function to clear lines and make above lines fall down
        function clearLines() {
            for (let i = boardRows - 1; i >= 0; i --) {
                let remove = true;
                for (let j = 0; j < boardColumns; j ++) {
                    if (board[i][j] == 0) {
                        remove = false;
                        break;
                    }
                }
                
                // make above lines fall down
                if (remove) {
                    for (let k = i; k >= 0; k --) {
                        for (let j = 0; j < boardColumns; j ++) {
                            if (k == 0) {
                                board[k][j] = 0;
                            }
                            else {
                                board[k][j] = board[k - 1][j];
                            }
                        }
                    }
                    i ++;
                }
            }
        }

        // function to draw the board
        function drawBoard() {
            context.fillStyle = '#000';
            context.fillRect(0, 0, canvas.width, canvas.height);
            for (let i = 0; i < boardRows; i ++) {
                for (let j = 0; j < boardColumns; j ++) { 
                    if (board[i][j] != 0) {
                        context.fillStyle = board[i][j];
                        context.fillRect(j, i, 1, 1);
                    }
                }
            }
        }

        // function to draw the tetromino
        function drawTetromino() {
            context.fillStyle = currentColor;
            for (let i = 0; i < currentTetromino.length; i ++) {
                for (let j = 0; j < currentTetromino[i].length; j ++) {           
                    if (currentTetromino[i][j] != 0) {
                        context.fillRect(tetrominoPosition.column + j, tetrominoPosition.row + i, 1, 1);
                    }   
                }
            }
        }
        
        // keydown listener
        document.addEventListener('keydown', (event) => {
            switch (event.key) {
                case 'ArrowLeft' :
                    tetrominoPosition.column --;
                    if (isTetrominoColliding()) {
                        tetrominoPosition.column ++;
                    }
                    break;
                case 'ArrowRight' :
                    tetrominoPosition.column ++;
                    if (isTetrominoColliding()) {
                        tetrominoPosition.column --;
                    }
                    break;
                case 'ArrowUp' :
                    currentTetromino = rotateTetromino(currentTetromino, false);    
                    if (isTetrominoColliding()) {
                        currentTetromino = rotateTetromino(currentTetromino, true);
                    }
                    break;
                case 'ArrowDown' :
                    dropInterval = 20;
                    break;
            
            }
        })  
        
        // keyup listener
        document.addEventListener('keyup', (event) => {
            if (event.key == 'ArrowDown') {
                dropInterval = 500;
            }
        });
        
        
</script>
</body>
</html>

Will I develop all missing features? Obviously, still using vanilla JS and maybe other languages.

Copy/paste in your projects and have fun.