JavaScript Game Tutorial

How to Make a Tetris-Style Block Game in JavaScript

A falling block puzzle looks complex, but it is built from a handful of small ideas: a grid, seven tiny matrices, one collision function and a rule for clearing full rows. This tutorial builds each one from the Supagames Blocks game.

IntermediateJavaScriptHTML5 Canvas18 min read

Production context

This guide studies Blocks, a published Supagames game. Repository source: games/lvl01/05-blocks.html.

Play a few rounds of Blocks first and watch how pieces rotate near the walls and how cleared rows collapse. The snippets below come from that game, including the wall kicks and the 7-bag randomizer we added while writing this guide.

The game draws on a 300 by 600 canvas: 10 columns and 20 rows of 30 pixel cells, plus a small canvas that previews the next piece.

1. Store the Playfield as a Grid

The playfield is a two-dimensional array with 20 rows and 10 columns. An empty cell holds 0, and a locked block stores its colour, so drawing the board is a single loop and collision checks are array lookups.

The falling piece is not part of the board while it moves. It lives in its own object with a shape, a position and a colour, and it is written into the board only when it locks.

const COLS = 10, ROWS = 20, BS = 30; // BS = block size in pixels
let board = Array.from({ length: ROWS }, () => Array(COLS).fill(0));
let piece = { shape: [], x: 0, y: 0, color: "" };
let score = 0, lines = 0, level = 1, gameOver = false;

2. Describe the Seven Pieces as Small Matrices

Each piece is a matrix of 1s and 0s: the I is one row of four, the O is a 2x2 square, and the T, L, J, S and Z pieces fit in 2x3 boxes. Matching the index of PIECES and COLORS gives every piece a stable colour.

A new piece spawns horizontally centred at the top. If it collides immediately, the stack has reached the top and the game is over.

const PIECES = [
  [[1, 1, 1, 1]],                 // I
  [[1, 1], [1, 1]],               // O
  [[0, 1, 0], [1, 1, 1]],         // T
  [[1, 0, 0], [1, 1, 1]],         // J
  [[0, 0, 1], [1, 1, 1]],         // L
  [[1, 1, 0], [0, 1, 1]],         // Z
  [[0, 1, 1], [1, 1, 0]],         // S
];
const COLORS = ["#ff6b6b", "#ffd93d", "#6bcb77", "#4d96ff", "#ff9ff3", "#a29bfe", "#fd79a8"];

function spawnPiece() {
  piece = { shape: nextPiece.shape.map((r) => [...r]),
            x: Math.floor((COLS - nextPiece.shape[0].length) / 2), y: 0, color: nextPiece.color };
  nextPiece = randomPiece();
  if (collision(piece.shape, piece.x, piece.y)) gameOver = true;
}

3. Write One Collision Function

Every rule in the game asks the same question: would this shape at this position overlap a wall, the floor or a locked block? Moving, rotating, soft dropping and spawning all call the same function with a candidate position.

Cells above the visible board, with a negative row, are allowed so pieces can rotate while partly above the top edge.

function collision(shape, ox, oy) {
  for (let r = 0; r < shape.length; r++) {
    for (let c = 0; c < shape[r].length; c++) {
      if (!shape[r][c]) continue;
      const x = ox + c, y = oy + r;
      if (x < 0 || x >= COLS || y >= ROWS) return true; // walls and floor
      if (y >= 0 && board[y][x]) return true;             // locked blocks
    }
  }
  return false;
}

function moveHoriz(dx) {
  if (!collision(piece.shape, piece.x + dx, piece.y)) { piece.x += dx; draw(); }
}

4. Rotate With a Transpose and Add Wall Kicks

Rotating a matrix 90 degrees clockwise is a transpose followed by reversing each row. In JavaScript that is one map over the first row's indexes.

Without help, a piece touching a wall simply refuses to rotate, which feels broken. A wall kick tries the rotated shape at a few sideways offsets and keeps the first one that fits. Offsets up to three columns let the long I piece rotate even when it is flat against the wall.

function rotate() {
  const rot = piece.shape[0].map((_, i) => piece.shape.map((row) => row[i]).reverse());
  for (const kick of [0, -1, 1, -2, 2, -3, 3]) {
    if (!collision(rot, piece.x + kick, piece.y)) {
      piece.shape = rot;
      piece.x += kick;
      draw();
      return;
    }
  }
}

5. Lock Pieces, Clear Lines and Speed Up

When a piece cannot move down any further, merge writes its colour into the board. Then every full row is removed with splice and an empty row is inserted at the top with unshift. The r++ after a clear checks the same index again, because the rows above have just moved down into it.

Scoring rewards multi-line clears: 100, 300, 500 and 800 points for one to four lines, multiplied by the level. Every ten lines the level rises and the drop timer gets 30 milliseconds faster, down to a floor of 100 milliseconds.

function clearLines() {
  let cleared = 0;
  for (let r = ROWS - 1; r >= 0; r--) {
    if (board[r].every((cell) => cell !== 0)) {
      board.splice(r, 1);
      board.unshift(Array(COLS).fill(0));
      cleared++;
      r++; // re-check this row index, the stack above just fell into it
    }
  }
  if (cleared > 0) {
    score += [0, 100, 300, 500, 800][Math.min(cleared, 4)] * level;
    lines += cleared;
    level = Math.floor(lines / 10) + 1;
    clearInterval(dropInterval);
    dropInterval = setInterval(moveDown, Math.max(100, 500 - (level - 1) * 30));
  }
}

6. Deal Pieces With a 7-Bag Randomizer

Picking a random piece every time can produce long droughts without an I piece, which feels unfair. Modern falling block games use a bag: shuffle all seven pieces, deal them one by one, and refill the bag when it is empty.

With a bag, each piece appears exactly once in every group of seven, and the longest possible wait for any piece is twelve pieces.

let bag = [];

function randomPiece() {
  if (bag.length === 0) {
    bag = PIECES.map((_, i) => i);
    for (let i = bag.length - 1; i > 0; i--) {          // Fisher-Yates shuffle
      const j = Math.floor(Math.random() * (i + 1));
      [bag[i], bag[j]] = [bag[j], bag[i]];
    }
  }
  const idx = bag.pop();
  return { shape: PIECES[idx].map((r) => [...r]), color: COLORS[idx] };
}

7. Add Keyboard and Touch Controls

Arrow keys move, Up rotates, Down soft drops and Space hard drops. Calling preventDefault for those keys is essential, otherwise the page scrolls every time the player rotates a piece.

On touch screens the canvas reads gestures: a short tap rotates, a horizontal swipe moves one column per 30 pixels and a long downward swipe hard drops. touch-action: none keeps the browser from scrolling during play.

document.addEventListener("keydown", (e) => {
  if (["ArrowLeft", "ArrowRight", "ArrowDown", "ArrowUp", " "].includes(e.key)) e.preventDefault();
  if (e.key === "ArrowLeft") moveHoriz(-1);
  else if (e.key === "ArrowRight") moveHoriz(1);
  else if (e.key === "ArrowDown") moveDown();
  else if (e.key === "ArrowUp") rotate();
  else if (e.key === " ") hardDrop();
});

canvas.style.touchAction = "none";
let touchStart = null;
canvas.addEventListener("touchstart", (e) => {
  touchStart = { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
}, { passive: true });
canvas.addEventListener("touchend", (e) => {
  const dx = e.changedTouches[0].clientX - touchStart.x;
  const dy = e.changedTouches[0].clientY - touchStart.y;
  if (Math.abs(dx) < 15 && Math.abs(dy) < 15) return rotate();   // tap
  if (dy > 60 && dy > Math.abs(dx)) return hardDrop();           // swipe down
  for (let i = 0; i < Math.max(1, Math.round(Math.abs(dx) / 30)); i++) moveHoriz(Math.sign(dx));
});

8. Build checklist

  • Keep locked blocks in a board array and the falling piece in its own object.
  • Route every movement and rotation through one collision function.
  • Rotate with transpose plus reverse and try a few wall-kick offsets.
  • Clear full rows with splice and unshift, and re-check the same row index.
  • Use a 7-bag randomizer so piece droughts cannot happen.
  • Prevent arrow-key scrolling and support taps and swipes on phones.

9. FAQ

Who created the original falling block puzzle?

Tetris was created by Alexey Pajitnov in 1984 while he worked at the Soviet Academy of Sciences in Moscow. The name is a trademark, which is why browser versions such as ours use their own names.

How does line clear scoring usually work?

Many modern games award 100, 300, 500 and 800 points for clearing one, two, three or four lines at once, multiplied by the level. Clearing four lines with one I piece is worth far more than four single clears.

What is a 7-bag randomizer?

It deals the seven pieces in shuffled groups of seven, so every piece appears once per group. It removes long droughts and makes planning possible, which is why most modern versions use it.

Why does rotation need wall kicks?

Without them a piece next to a wall or another block cannot rotate even when a small sideways shift would make room. Kicks try those shifts automatically, so rotation feels responsive.

Previous: Memory card game Next: How to make Breakout