Building a Tetris-like Web Game


Currently me and my team are working on a jelly like soft body tetris-like game. There's actually even more than just softbody sim in this game, but for now it's a secret. MEanwhile I've decided to build couple of prototypes to understand how tetris and tetris like game work, and as I'm doing it in a way that it should be avaialble for my distributed team all the prototypes are done using web technologies.

Introduction

In this post, we’ll walk through building a basic version of the classic game Tetris using JavaScript and HTML5 Canvas. We’ll cover the key steps, starting with setting up the project and ending with implementing the basic game loop, movement of pieces, and score tracking. The original Tetris was created by Alexey Pajitnov in 1984, and it has since become one of the most iconic games in the world. Our goal here is to recreate its core mechanics and take some creative liberties along the way.


Step 1: Project Setup — HTML, CSS, and Basic Structure

First, we need to set up the project with three core files:

  • HTML for the structure.
  • CSS for the styling.
  • JavaScript for the game logic.

HTML

We’ll start by creating a simple HTML page with a canvas for the game and a sidebar to display score, level, and the next piece:

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tetris</title>
    <link rel="stylesheet" href="style.css">
    <div class="tetris-container">
        <canvas id="gameCanvas" width="300" height="600"></canvas>
        <div class="sidebar">
            <h2>Next Piece:</h2>
            <canvas id="nextPieceCanvas" width="120" height="120"></canvas>
            <div>
                <h2>Score: <span id="score">0</span></h2>
                <h2>Level: <span id="level">1</span></h2>
                <button id="pauseButton">Pause</button>
                <button id="resetButton">Reset</button>
            </div>
        </div>
    </div>
    <script src="script.js"></script>

CSS

For the basic styling, we can use the following CSS:

body {
    font-family: Arial, sans-serif;
    background-color: #000;
    color: #fff;
    text-align: center;
}
.tetris-container {
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 50px;
}
canvas {
    border: 1px solid #fff;
}
.sidebar {
    margin-left: 20px;
    text-align: left;
}
button {
    margin-top: 10px;
    padding: 10px;
    background-color: #333;
    color: white;
    border: none;
    cursor: pointer;
}

Now we have a basic page structure with two canvases — one for the game and the other to show the next piece.

Step 2: Basic Game Loop and Drawing Pieces

The core idea of Tetris is that pieces (Tetrominos) fall from the top of the screen and the player can control them. We need a game loop that updates the game at regular intervals.

In the JavaScript file, we start by adding the basic code to draw pieces on the canvas:

const canvas = document.getElementById("gameCanvas");
const context = canvas.getContext("2d");
const COLS = 10;
const ROWS = 20;
const BLOCK_SIZE = 30;
function drawBlock(x, y, color) {
    context.fillStyle = color;
    context.fillRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
    context.strokeRect(x * BLOCK_SIZE, y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE);
}
// Example of drawing a block:
drawBlock(1, 1, '#00f0f0');

This allows us to draw individual blocks on the canvas, each representing a part of a Tetromino.

Step 3: Adding Movement and Rotation Logic

Now that we can draw pieces, it’s time to add movement logic. Pieces should move left, right, and down, and players should be able to rotate them.

We can handle player input through the keyboard by listening for arrow key presses:

document.addEventListener("keydown", handleKeyPress);
function handleKeyPress(event) {
    switch (event.key) {
        case 'ArrowLeft':
            // Move piece left
            break;
        case 'ArrowRight':
            // Move piece right
            break;
        case 'ArrowDown':
            // Move piece down faster
            break;
        case 'ArrowUp':
            // Rotate piece
            break;
    }
}

This gives the player control over the movement and rotation of pieces as they fall.

Step 4: Collision Detection and Locking Pieces

Next, we need to handle what happens when a piece hits the bottom of the board or another piece. When this happens, the piece should "lock" in place, and a new piece should spawn at the top.

function moveDown() {
    currentPiece.y++;
    if (!pieceFits(currentPiece)) {
        currentPiece.y--; // Move piece back
        lockPiece(); // Lock the piece in place
        currentPiece = nextPiece;
        nextPiece = randomPiece();
    }
}

This ensures that the pieces are properly placed and that the game continues with a new piece once the current one locks.

Step 5: Scoring and Level System

To add some fun, we’ll implement a score system. Players earn points when they clear lines, and as they score more points, the level increases, speeding up the game.

let score = 0;
let level = 1;
function clearLines() {
    let linesCleared = 0;
    for (let y = 0; y < ROWS; y++) {
        if (board[y].every(cell => cell)) {
            board.splice(y, 1);
            board.unshift(new Array(COLS).fill(0));
            linesCleared++;
        }
    }
    if (linesCleared > 0) {
        score += linesCleared * 100;
        if (score >= level * 1000) {
            level++;
            dropSpeed = Math.max(100, dropSpeed - 100); // Increase speed
        }
    }
}

Much like in Tetris 99, the increase in speed as levels go up adds to the challenge, and players need to react quickly.

Step 6: Pause and Reset

Finally, let’s add the ability to pause and reset the game using buttons. This is useful for when players need a break or want to start over.

let isPaused = false;
pauseButton.addEventListener("click", () => {
    isPaused = !isPaused;
    pauseButton.textContent = isPaused ? "Resume" : "Pause";
});
resetButton.addEventListener("click", () => {
    board = [];
    createBoard();
    score = 0;
    level = 1;
    gameOver = false;
    currentPiece = randomPiece();
    nextPiece = randomPiece();
    gameLoop();
});

End of Tetrision

The falling blocks, each with distinct colors, can be likened to vibrant anime hairstyles — from cyan-blue hair (like Ayanami) to fiery red (a nod to Aska). You might even say the falling pieces resemble characters embarking on an endless adventure, much like a scene from an Isekai anime where every choice matters.

In future posts, we’ll dive deeper into adding more advanced functionality, including soft-body physics, to bring even more life to the game.