Introduction

On Part 2, we implemented Tetrominoes, added movement logic, and handled collision detection. Now, we will complete our Tetris game by adding line clearing, a scoring system, and the game loop to make the gameplay dynamic. In this final part of our TypeScript Tetris tutorial series, we’ll complete the core game logic by implementing line clearing, scoring, and game over detection.

We’ll also polish the game loop for a smoother experience and set the stage for future enhancements like levels and UI.

By the end of this tutorial, you’ll have:

  • A working scoring system
  • Logic to clear full rows
  • A game over state
  • A responsive game loop using requestAnimationFrame

Let’s wrap up the core game mechanics!


Updated Project Structure

We’ll add one new file:

tetris-ts/
├── src/
│   ├── game.ts            # Game loop and scoring logic

Step 1: Add Line Clearing and Score Logic

Create src/game.ts:

import { Matrix } from './tetromino';
import { Player } from './player';

export class Game {
  private arena: Matrix;
  private player: Player;
  private score: number = 0;
  private scoreElement: HTMLElement;

  constructor(arena: Matrix, player: Player, scoreSelector: string) {
    this.arena = arena;
    this.player = player;
    this.scoreElement = document.querySelector(scoreSelector)!;
    this.updateScore();
  }

  sweepArena(): void {
    outer: for (let y = this.arena.length - 1; y >= 0; y-=1) {
      for (let x = 0; x < this.arena[y].length; x+=1) {
        if (this.arena[y][x] === 0) {
          continue outer;
        }
      }

      const row = this.arena.splice(y, 1)[0].fill(0);
      
      this.arena.unshift(row);
      
      y++;
      
      this.score += 10;
    }

    this.updateScore();
  }

  updateScore(): void {
    this.scoreElement.textContent = `Score: ${this.score}`;
  }

  checkGameOver(): boolean {
    return this.player.collide();
  }
}

Step 2: Add Score UI to index.html

Update index.html to include a score display:

<body>
  <div style="text-align: center; color: white; font-family: monospace; font-size: 18px;">
    <div id="score">Score: 0</div>
  </div>
  <canvas id="tetris"></canvas>
  <script type="module" src="./dist/main.js"></script>
</body>

Step 3: Update main.ts for Full Game Logic

Update main.ts to handle line clearing, score updates, and game over:

import { Grid, COLS, ROWS } from './grid';
import { Player } from './player';
import { COLORS } from './tetromino';
import { Game } from './game';

function createMatrix(w: number, h: number): number[][] {
  const matrix = [];
  
  for (let i = 0; i < h; i+=1) {
    matrix.push(new Array(w).fill(0));
  }

  return matrix;
}

const canvas = document.getElementById('tetris') as HTMLCanvasElement;
const context = canvas.getContext('2d')!;

const grid = new Grid(context);
const arena = createMatrix(COLS, ROWS);
const player = new Player(arena);
const game = new Game(arena, player, '#score');

function drawMatrix(matrix: number[][], offset: { x: number; y: number }) {
  matrix.forEach((row, y) => {
    row.forEach((value, x) => {
      if (value !== 0) {
        context.fillStyle = COLORS[value];
        context.fillRect(x + offset.x, y + offset.y, 1, 1);
      }
    });
  });
}

function draw() {
  grid.clear();

  drawMatrix(arena, { x: 0, y: 0 });
  drawMatrix(player.matrix, player.pos);
}

let dropCounter = 0;
let dropInterval = 1000;
let lastTime = 0;

function update(time = 0) {
  const deltaTime = time - lastTime;
  
  lastTime = time;
  dropCounter += deltaTime;

  if (dropCounter > dropInterval) {
    player.drop();

    if (player.collide()) {
      game.checkGameOver();
    } else {
      game.sweepArena();
    }

    dropCounter = 0;
  }

  draw();

  requestAnimationFrame(update);
}

window.addEventListener('keydown', e => {
  if (e.key === 'ArrowLeft') player.move(-1);
  if (e.key === 'ArrowRight') player.move(1);
  if (e.key === 'ArrowDown' || e.key === 'Enter') player.drop();
  if (e.key === 'q' || event.key === 's') player.rotate(-1);
  if (e.key === 'w' || event.key === ' ') player.rotate(1);
});

update();

Tetris Demo


Summary

On this final part, you:

  • Implemented line clearing and scoring
  • Added a simple UI element to track score
  • Polished the game loop using requestAnimationFrame
  • Handled game over by clearing the arena

Congratulations! You’ve built a complete Tetris game in TypeScript.


Optional Enhancements:

  • Add levels and increase speed as score increases
  • Display next Tetromino preview
  • Add sound effects and music
  • Create a start/reset menu screen
  • Persist high scores using localStorage

Thanks for following along. Happy coding!

In case you missed previous posts

(Part 1) Building Tetris in TypeScript: Setting Up the Game Structure and Rendering the Grid
Learn how to build the foundation of a Tetris game using TypeScript and the Canvas API. In this first tutorial, you’ll set up the project, render a 10x20 game grid, and structure your code for maintainability—all without using any frameworks.
(Part 2) Building Tetris in TypeScript: Implementing Tetrominoes and Game Logic
In this tutorial, we bring Tetris to life by adding Tetrominoes, player movement, rotation, and collision detection using TypeScript classes. You’ll also implement keyboard controls for interactive gameplay.