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

Read for Part 3?

Discussion