Tetris Turns 35

On June 6, 2019 Tetris turned 35 years old. To celebrate, I wrote my own version and posted it in my GitHub repo. No libraries. No frameworks. Just JavaScript. Those learning JavaScript might find a few lessons here!

Here’s a list of my best web development tutorials.

Tetris In The Dark I developed to celebrate 35 years of Tetris!
The new Tetris logo was recently announced on June 6, 2019, the game’s 35th anniversary of the game. This image was borrowed from the Twin Galaxies article, a neat gaming site, check it out. The Tetris logo is copyright The Tetris Company, LLC with its headquarters Honolulu, HI.
Tetris In The Dark.
First attempt at making Tetris.
let color = { background: "#5c2a3b",     // background
wall: "#d83c66", // walls
solid: "#49b5ab", // solid tetromino
tetromino: "#e97539" }; // falling tetromino

Complete Tetris Source Code

This tutorial is based on existing Tetris code I wrote a week ago. You can fork the Complete Tetris source code from my GutHub profile. *I won’t be listing entire source code in this tutorial to avoid redundancy. But all important functions will be listed here.

Well

10 by 20 is the classic size of the Tetris well. But it can be any size. In this demo we also have walls that happen to be part of the well array. So even though the well is 10 squares in width, with the walls it’s actually 12:

12 x 17
50 x 36
let width = 50;                 // well width
let height = 36; // well height
let square_size = 16; // square size in pixels
let well = new Array(height);   // array holding the entire well
// Reset entire well to all 0's
for (let y = 0; y < width; y++)
well[y] = new Array( height ).fill(0);

// Mark bottom
for (let x = 0; x < width; x++)
well[x][height - 1] = 2;

// Mark walls
for (let y = 0; y < height; y++) {
well[0][y] = 1;
well[width - 1][y] = 1;
}
// Generate well on the screen by creating HTML elements dynamically
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { // Create a DIV element dynamically
let square = document.createElement("div");
// Create an intuitive easy to access ID, eg: "square_x5y13"
square.setAttribute("id", "square_x" + x + "y" + y);
// Set some CSS properties for the square
square.style.position = "absolute";
square.style.left = x * square_size + "px";
square.style.top = y * square_size + "px";
square.style.width = square_size + "px";
square.style.height = square_size + "px";
square.style.zIndex = 0;
let block_type = well[x][y];

// Empty space color:
if (block_type == 0) square.style.background = "#082b7f";
// Wall color:
if (block_type == 1) square.style.background = "#841550";
// Bottom wall color:
if (block_type == 2) square.style.background = "#c20c98";
// Add created square to the <BODY> dynamically
document.body.appendChild( square );
}
}

Tetrominos

The 7 classic patterns consisting of 4 squares are called tetrominos.

The classic set of seven tetrominos.
The stick is an odd-ball case as it’s the only tetromino that requires a 4x4 grid.
Technically, you can use a 2-dimensional array to represent a 9x9 tetromino. But it can also be represented by a 1-dimensional array (pictured here.) This way it’s a bit easier rasterizing it on the well grid.
One way of defining a tetromino using a 1-dimensional array.
let A = [0,0,1,
0,0,1,
0,1,1];

let B = [1,0,0,
1,0,0,
1,1,0];

let C = [0,0,0,
0,1,0,
1,1,1];

let D = [0,0,0,
0,1,1,
1,1,0];

let E = [0,0,0,
1,1,0,
0,1,1];

let F = [1,1,0,
1,1,0,
0,0,0];

let G = [0,0,0,
1,1,1,
0,0,0];
let tetrominos = [A,B,C,D,E,F,G];
let current = [0,0,0, 0,0,0, 0,0,0];
let next = [0,0,0, 0,0,0, 0,0,0];
// Generate a random tetromino and return it as 3x3 array
function make_random() {
// 1.) Select random tetromino from tetrominos array by index
let index = Math.floor((Math.random() * tetrominos.length));
// 2.) Copy it into current array (avoid reference assignment)
return [...tetrominos[ index ]];
}
current = make_random();
next = make_random();

Keyboard Controls

Here is the keyboard controls source code:

// Keyboard input
document.addEventListener("keydown", (e) => {

let key_code = e.keyCode;
// Erase the teetromino
erase();

// Left
if (key_code == 37) {
if (will_collide(dir.LEFT)) {
reset();
} else position.x -= 1
}

// Right
if (key_code == 39) {
if (will_collide(dir.RIGHT)) {
reset();
} else position.x += 1
}

// Down
if (key_code == 40) {
if (will_collide(dir.DOWN)) {
reset();
} else position.y += 1
}

if (key_code == 38) { position.y -= 1 }

// Rotate
if (key_code == 90) { rotate_left() }
if (key_code == 88) { rotate_right(); }
// Draw the current tetromino
draw();
});

Falling Animation

The game loop consists of erase, fall and draw functions.

// Game-loop Animation
setInterval(() => {
// Erase the current tetromino block from the well
erase();
// Progress the tetromino by 1 square down
fall();
// Draw the tetromino at its new fallen position
draw();

}, 15);

Collision Detection

There are two types of collisions in Tetris. With walls and with fallen bricks.

Avoid writing collision detection in “real time.” You need to figure out if the current block will collide at a future time IF it is moved in the direction it is moving on the next frame, not on the current frame. And if there is a future collision, prevent any further movement and “paste” the brick into the well as a solid block (the latter is not shown on this animation, it will be explained in one of the following sections.)
// Left arrow key is pressed
if (key_code == 37) {
// Will tetromino collide with walls or if it is moved left?
if (will_collide( dir.LEFT ))
reset();
else
// Tetromino will not collide, move to that position
position.x -= 1;
}

reset()

The reset function is more of a helper function. It calls paste(), clear_row(), make_random(), update_next() and erases the fog of darkness.

function reset() {
paste(); // paste current tetromino onto the well
clear_row(); // clear rows if any
current = [...next]; // swap current and next tetromino
next = make_random(); // generate next tetromino
update_next(); // Update "next" box

// reset current position to top and middle of the well
position.x = parseInt(width/2) - 1;
position.y = -3;

reset_fog(); // clear the fog of darkness
}

paste()

// "paste" current block onto the well
function paste() {
let index = 0; // Prevent pasting blocks that fall outside of the well:
if (position.x >= 0 && position.x <= width - 1) {
if (position.y >= -3 && position.y <= height - 1) {
// Iterate over the 3x3 block of tetromino:
for (let y = position.y; y < position.y + 3; y++) {
for (let x = position.x; x < position.x + 3;
x++, index++) {
// If tetromino is solid at that square
if (current[index] == 1) {
let id = "square_x" + x + "y" + y;
let sq = document.getElementById(id);
if (sq) {
well[x][y] = 3;
sq.style.backgroundColor = color.solid;
}
}
}
}
}
}
}

erase()

It’s the same as paste() only it sets the currently falling tetromino to all 0’s, effectively erasing it from the well array (before animating it to next position.)

function erase() {    let index = 0;    if (position.x >= 0 && position.x <= width - 1) {
if (position.y >= -3 && position.y <= height - 1) {
for (let y = position.y; y < position.y + 3; y++) {
for (let x = position.x; x < position.x + 3;
x++, index++) {
if (current[index] == 1) {
let id = "square_x" + x + "y" + y;
let square = document.getElementById(id);
if (square) {
if (true) { // well[x] && well[y]
well[x][y] = 0;
if (x == 0 || x == width - 1 ) { }
else {
square.style.backgroundColor
= color.background;
}
}
}
}
}
}
}
}
}

Pasting The Fallen Block Into The Well

Once a block is considered “fallen” it is physically pasted into the well array.

Once a brick collides with walls or other bricks, it gets “pasted” into the well and marked as solid.

Row Breaking Algorithm

This is the most complex piece of code when it comes to Tetris. This algorithm will check if 1) there are any rows to clear 2) rebuild the well again without the complete rows to cancel them out and create block collapsing illusion.

// Check if a row needs to be cleared
function clear_row() {

// Placeholder for new rows
let placeholder = [];

// How many rows cleared?
let rows_cleared = 0;

// Scan the well one row at a time and capture any
// non-filled rows in placeholder
// (except the last row)

for (let y = 0; y < height - 1; y++) {
let start = y * width;
let scanned = scan_row(y);
let total = scanned[0];
let row_data = scanned[1];
// Skip all horizontal rows that are completely filled
if (total != width) {
// Memorize only uncleared rows
let len = placeholder.length;
placeholder[len] = row_data;
} else {
start_highlight(y);
rows_cleared++;
}
}

// If at least one row was cleared, update the well
if (rows_cleared > 0) {
// Clear the well, except last row (well's bottom)
for (let y = 0; y < height - 1; y++) {
// Clear all except walls ([0] and [width - 1])
for (let x = 1; x < width - 1; x++) {
well[x][y] = 0;
// Paint empty square
let square =
document.getElementById("square_x" + x + "y" + y);
if (square)
square.style.backgroundColor = color.background;
}
}

// Paste captured placeholder rows onto the well
// but from bottom up

let r = height - 2;
for (let i = placeholder.length - 1; i > 0; i--) {
let row = placeholder[i];
for (let x = 0; x < width; x++) {
if (row[x] != 0) {
well[x][r] = 3;
if (x != 0 && x != width - 1) {
let square =
document.getElementById("square_x"+x+"y"+r);
if (square)
square.style.backgroundColor = color.solid;
}
}
}
r--;
}
}
}

Tetris In The Dark (Included in source code!)

Strategy video games have something called for of war. It covers an unexplored area of terrain with blackness.

Adding Light

To create a light spot, first I simply created a secondary grid sharing the same dimensions as the well and used it as an overlay. By default all DIV squares on that grid were set to black color and opacity of 1.

// light position
let light = { x: 0, y: 0 };
// lightspot data
let light_mask = [
0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,4,4,0,0,0,0,0,0,
0,0,0,2,3,4,5,5,4,3,2,0,0,0,
0,0,2,3,4,5,6,6,5,4,3,2,0,0,
0,2,3,4,5,6,7,7,6,5,4,3,2,0,
0,3,4,5,6,7,8,8,7,6,5,4,3,0,
0,4,5,6,7,8,9,9,8,7,6,5,4,0,
0,4,5,6,7,8,9,9,8,7,6,5,4,0,
0,3,4,5,6,7,8,8,7,6,5,4,3,0,
0,2,3,4,5,6,7,7,6,5,4,3,2,0,
0,0,2,3,4,5,6,6,5,4,3,2,0,0,
0,0,0,2,3,4,5,5,4,3,2,0,0,0,
0,0,0,0,0,0,4,4,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,
];
function draw_light() {
let index = 0;
for (let y = 0; y < 14; y++) {
for (let x = 0; x < 14; x++, index++) {
let posx = x + position.x;
let posy = y + position.y;
let id = "fog_x" + (posx - 6) + "y" + (posy - 6);
let square = document.getElementById(id);
if (square) {
let type = light_mask[index];
if (type == 9) square.style.opacity = '0';
if (type == 8) square.style.opacity = '0.1';
if (type == 7) square.style.opacity = '0.2';
if (type == 6) square.style.opacity = '0.3';
if (type == 5) square.style.opacity = '0.4';
if (type == 4) square.style.opacity = '0.5';
if (type == 3) square.style.opacity = '0.7';
if (type == 2) square.style.opacity = '0.8';
if (type == 1) square.style.opacity = '0.9';
if (type == 0) square.style.opacity = '1.0';
}
}
}
}

Breaking Row Animation

The row-breaking animation is done separately from everything else. It’s just a list of long horizontal DIV elements at every height level of the well.

Final Results

When working with the same subject for a long period of time you tend to get a bit bored and an impulse to innovate awakens.

Tetris In The Dark

Issues. Every webdev has them. Published author of CSS Visual Dictionary https://amzn.to/2JMWQP3 few others…

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store