Introduction

In Part 1, we set up the project and rendered the Tetris grid using TypeScript and the Canvas API. Now, it's time to bring the game to life by adding Tetrominoes, handling basic movement, and implementing collision detection.

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

  • Defined Tetromino shapes and colors
  • Created a Tetromino class with spawn and rotation logic
  • Handled player movement and collision checks
  • Implemented basic keyboard controls

Let’s get started!


Updated Project Structure

We’ll add a few new files to organize the logic:

tetris-ts/
├── src/
│   ├── tetromino.ts       # Tetromino shape logic and rotation
│   ├── player.ts          # Player position, movement, and collision
│   └── main.ts            # Entry point

Step 1: Define Tetromino Types and Colors

Create src/tetromino.ts:

export type Matrix = number[][];

export enum TetrominoType {
  T = 'T',
  O = 'O',
  L = 'L',
  J = 'J',
  I = 'I',
  S = 'S',
  Z = 'Z',
}

export const COLORS: Record<number, string> = {
  0: '#000000',
  1: '#FF0D72',
  2: '#0DC2FF',
  3: '#0DFF72',
  4: '#F538FF',
  5: '#FF8E0D',
  6: '#FFE138',
  7: '#3877FF',
};

export const TETROMINOES: Record<TetrominoType, Matrix> = {
  T: [
    [0, 1, 0],
    [1, 1, 1],
    [0, 0, 0],
  ],
  O: [
    [2, 2],
    [2, 2],
  ],
  L: [
    [0, 0, 3],
    [3, 3, 3],
    [0, 0, 0],
  ],
  J: [
    [4, 0, 0],
    [4, 4, 4],
    [0, 0, 0],
  ],
  I: [
    [0, 5, 0, 0],
    [0, 5, 0, 0],
    [0, 5, 0, 0],
    [0, 5, 0, 0],
  ],
  S: [
    [0, 6, 6],
    [6, 6, 0],
    [0, 0, 0],
  ],
  Z: [
    [7, 7, 0],
    [0, 7, 7],
    [0, 0, 0],
  ],
};

export function rotate(matrix: Matrix, dir: number): Matrix {
  // Transpose
  for (let y = 0; y < matrix.length; y+=1) {
    for (let x = 0; x < y; x+=1) {
      [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
    }
  }
  
  // Reverse
  return dir > 0 ? matrix.map(row => row.reverse()) : matrix.reverse();
}

Step 2: Player Logic (Movement and Collision)

Create src/player.ts:

import { TETROMINOES, TetrominoType, Matrix, rotate } from './tetromino';
import { COLS, ROWS } from './grid';

export class Player {
  matrix: Matrix = [[]];
  pos = { x: 0, y: 0 };
  arena: Matrix;

  constructor(arena: Matrix) {
    this.arena = arena;
    this.reset();
  }

  reset(): void {
    const types = Object.values(TetrominoType);
    const type = types[Math.floor(Math.random() * types.length)];
    this.matrix = TETROMINOES[type];
    this.pos.y = 0;
    this.pos.x = Math.floor((COLS - this.matrix[0].length) / 2);

    if (this.collide()) {
      this.arena.forEach(row => row.fill(0));
    }
  }

  move(dir: number): void {
    this.pos.x += dir;
    if (this.collide()) {
      this.pos.x -= dir;
    }
  }

  drop(): void {
    this.pos.y++;

    if (this.collide()) {
      this.pos.y--;
      this.merge();
      this.reset();
    }
  }

  rotate(dir: number): void {
    const original = this.matrix.map(row => [...row]);
    
    rotate(this.matrix, dir);

    const offset = 1;
    let newX = this.pos.x;
    
    while (this.collide()) {
      newX += offset;
      
      if (newX > this.matrix[0].length) {
        this.matrix = original;
        return;
      }
      
      this.pos.x = newX;
    }
  }

  collide(): boolean {
    const m = this.matrix;
    const o = this.pos;
    
    for (let y = 0; y < m.length; y+=1) {
      for (let x = 0; x < m[y].length; x+=1) {
        if (
          m[y][x] !== 0 &&
          (this.arena[y + o.y] && this.arena[y + o.y][x + o.x]) !== 0
        ) {
          return true;
        }
      }
    }

    return false;
  }

  merge(): void {
    this.matrix.forEach((row, y) => {
      row.forEach((value, x) => {
        if (value !== 0) {
          this.arena[y + this.pos.y][x + this.pos.x] = value;
        }
      });
    });
  }
}

Step 3: Update main.ts

Modify main.ts to use the Player class and add controls:

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

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

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

  draw();

  requestAnimationFrame(update);
}

// Keyboard controls
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();

Summary and Next Steps

In this tutorial, you:

  • Defined Tetromino types and rotation logic
  • Created a Player class to manage the current piece
  • Handled movement, rotation, and collision detection
  • Enabled keyboard controls for gameplay

On our Part 3, we’ll complete the core gameplay by adding:

  • Line clearing
  • Scoring
  • Game loop polish
  • Game over detection

We’re nearly there — let’s wrap up this game in the next part!


In case you missed Part 1

(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.

Read for Part 3?

(Part 3/Final) Building Tetris in TypeScript: Completing the Game (Scoring, Line Clearing, and Game Loop)
Wrap up your Tetris game by adding scoring, line-clearing logic, and game over detection. This tutorial ties everything together with a polished game loop and a simple UI to track player progress.