tetris

Technical Walkthrough

How the Level‑Based Tetris Was Engineered

How the Level‑Based Tetris Was Built — Technical Walkthrough

Technical Walkthrough How the Level‑Based Tetris Was Engineered

This page documents the internal architecture of a self‑contained HTML5 Tetris implementation. Every design decision — from grid geometry to the game loop — is explained so you can understand exactly which code does what and why it was written that way. The entire game lives inside a single .html file: no CDN, no external JavaScript, no framework. Just HTML, CSS, and vanilla JS.

1. Why a Self‑Contained Single File?

Blogger and most lightweight CMS platforms strip or block external <script src> tags and CDN dependencies for security reasons. To guarantee the game works everywhere, all logic is inlined. The trade‑off is a larger file, but the payoff is total portability: paste the code into any HTML widget and it runs.

  • No CDN Zero external requests → faster load, no CORS issues.
  • No Build Step Plain ES5‑compatible JavaScript wrapped in an IIFE.
  • Responsive Canvas scales via CSS while keeping a fixed logical resolution.

2. The Coordinate System & Canvas

The board is a 10×20 grid. Each cell is 30px in logical space, giving a fixed canvas resolution of 300 × 600. This never changes — the browser scales it visually with CSS to fit any screen width.

const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
const CANVAS_W = COLS * BLOCK_SIZE; // 300
const CANVAS_H = ROWS * BLOCK_SIZE; // 600

Keeping the logical resolution constant means all collision math uses clean integers. The resizeCanvas() function only touches canvas.style.width/height, never the width/height attributes — so the drawing buffer is untouched and no blurry scaling occurs.

3. Board Representation

The playfield is a 2D array of ROWS rows, each containing COLS cells. A cell is either null (empty) or a color string like #00f0f0. Using color strings directly means we never need a separate lookup table when drawing.

function initBoard() {
  board = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
}

4. Tetromino Definitions

Each of the seven classic tetrominoes is stored as a matrix of 0/1 values plus a hex color. The I piece uses a 4×4 matrix so that rotation works uniformly across all pieces without special cases.

const SHAPES = [
  { matrix: [[0,0,0,0],[1,1,1,1],[0,0,0,0],[0,0,0,0]], color: '#00f0f0' },
  { matrix: [[1,1],[1,1]], color: '#f0f000' },
  // ... T, S, Z, J, L
];

5. Collision Detection

This is the heart of the game. collision(matrix, offsetX, offsetY) walks every filled cell of a piece and checks three things: left/right walls, the floor, and existing locked blocks on the board. It returns true the moment any overlap is found.

function collision(matrix, offsetX, offsetY) {
  for (let r = 0; r < matrix.length; r++) {
    for (let c = 0; c < matrix[r].length; c++) {
      if (matrix[r][c] !== 0) {
        const bx = offsetX + c, by = offsetY + r;
        if (bx < 0 || bx >= COLS || by >= ROWS || by < 0) return true;
        if (board[by][bx] !== null) return true;
      }
    }
  }
  return false;
}

Every movement, rotation, and drop calls this function first. If it returns false, the move is safe and applied. This single guard prevents all impossible states.

6. Rotation with Wall Kicks

Rotating a matrix 90° clockwise is done with a transpose‑and‑reverse one‑liner. But near a wall, a plain rotation would collide. To fix this, we try five horizontal offsets (0, -1, +1, -2, +2) and accept the first one that does not collide. This is a lightweight version of the “SRS wall kick” system used in modern Tetris.

function rotatePiece() {
  const rotated = currentPiece.matrix[0].map((_, i) =>
    currentPiece.matrix.map(row => row[i]).reverse()
  );
  for (const dx of [0, -1, 1, -2, 2]) {
    if (!collision(rotated, currentPiece.x + dx, currentPiece.y)) {
      currentPiece.matrix = rotated;
      currentPiece.x += dx;
      return;
    }
  }
}

7. Line Clearing & Scoring

When a piece locks, we scan rows from bottom to top. Any fully filled row is removed with splice, and a fresh empty row is unshifted to the top. Score follows the classic Tetris table, multiplied by the current level.

Lines ClearedBase Score
1 (Single)100
2 (Double)300
3 (Triple)500
4 (Tetris)800
const LINE_SCORES = [0, 100, 300, 500, 800];
score += LINE_SCORES[Math.min(linesCleared, 4)] * level;

8. Level Progression

Every 10 cleared lines advances the level. The drop interval shrinks geometrically, so the game accelerates smoothly instead of jumping. Level is capped at 10 to keep the game playable on touch devices.

const LINES_PER_LEVEL = 10;
const MAX_LEVEL = 10;
const BASE_DROP_INTERVAL = 800;  // ms at level 1
const SPEED_FACTOR = 0.82;        // per level

const dropInterval = BASE_DROP_INTERVAL * Math.pow(SPEED_FACTOR, level - 1);

At level 1 the piece falls every 800 ms. At level 10 it falls every 800 × 0.82⁹ ≈ 135 ms — roughly six times faster.

9. The Game Loop

The loop uses requestAnimationFrame and a delta‑time accumulator. Every frame we add the elapsed milliseconds to dropCounter. When it exceeds dropInterval, the piece moves down one cell and the counter resets.

function gameLoop(time) {
  const delta = time - lastTime;
  lastTime = time;
  dropCounter += delta;
  if (dropCounter > dropInterval) {
    // move down or lock
    dropCounter = 0;
  }
  draw();
  requestAnimationFrame(gameLoop);
}
Why not setInterval? setInterval drifts and cannot be paused cleanly. Delta‑time with requestAnimationFrame is frame‑rate independent, pauses perfectly, and lets us redraw the canvas every frame for smooth visuals.

10. Rendering Strategy

Every frame we clear the canvas and redraw three layers: the grid lines, the locked blocks on the board, and the currently falling piece. Each block is drawn with a base fill, a subtle inner highlight, and a dark border — three cheap operations that give a polished “beveled” look without images or gradients.

function drawBlock(x, y, color, size) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, size, size);
  ctx.fillStyle = 'rgba(255,255,255,0.15)';
  ctx.fillRect(x + 2, y + 2, size - 4, size - 4);
  ctx.strokeStyle = 'rgba(0,0,0,0.5)';
  ctx.strokeRect(x + 1, y + 1, size - 2, size - 2);
}

11. Input Handling

Two input paths are supported: physical keyboard and on‑screen buttons. The keyboard handler prevents default browser scrolling on arrow keys and space, which is critical — otherwise pressing “down” would scroll the page instead of moving the piece.

document.addEventListener('keydown', (e) => {
  if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) {
    e.preventDefault();
  }
  switch (e.key) {
    case 'ArrowLeft':  movePiece(-1); break;
    case 'ArrowRight': movePiece(1);  break;
    case 'ArrowDown':  softDrop();     break;
    case 'ArrowUp':    rotatePiece();  break;
    case ' ':          hardDrop();     break;
  }
});

Touch buttons use a touchstart listener with preventDefault() to eliminate the 300 ms tap delay and prevent double‑tap zoom on mobile.

12. Responsive Design

The wrapper is capped at 480px and the canvas scales proportionally inside it. This ensures the game is playable on phones, tablets, and desktops without ever cropping the board or requiring horizontal scrolling.

function resizeCanvas() {
  const containerWidth = wrapper.clientWidth - 24;
  const scale = Math.min(1, containerWidth / CANVAS_W);
  canvas.style.width  = (CANVAS_W * scale) + 'px';
  canvas.style.height = (CANVAS_H * scale) + 'px';
}

The scale factor is capped at 1 so on large screens the canvas stays at its native 300×600 instead of becoming blurry from upscaling.

13. Summary of Design Principles

  1. Separation of concerns: State (board, piece, score) is decoupled from rendering and input.
  2. Single source of truth: collision() is the only authority on whether a move is legal.
  3. Frame‑rate independence: Delta‑time accumulation makes the game feel identical on 60 Hz and 120 Hz displays.
  4. Progressive difficulty: Geometric speed scaling produces a smooth curve rather than a stair‑step.
  5. Zero dependencies: The whole game ships in one file, works offline, and drops into any HTML page.
Level‑Based Tetris — Technical Documentation · Single‑File HTML5 · Vanilla JavaScript