The pieces in Tetris are called Tetriminos.
There are 7 different Tetriminos, which are named after the letter that they look like. These are Z, S, J, T, L, I and O.
Each Tetrimino automatically moves towards the bottom of the grid. When it touches the bottom or another Tetrimino, it is locked into that position.
While the Tetrimino is falling, you are allowed to move it left, right and rotate it. The Tetriminocan be moved as far as the most left column, at which point it can move no further. The Tetriminocan be moved as far as the most right column, at which point it can move no further. When rotating a Tetrimino, it rotates on its axis.
The way that I will tackle each shape and its rotation is by drawing them in a square, made from squares: I takes up the most space as it is 4 squares in length. Therefore I will represent I and its rotation like this:
Create a 2D array for each type of Tetrimino
As discussed, we will store the values of each Tetrimino and its rotations in a 2D array. However, technically JavaScript does not support multidimensional arrays! However, JavaScript is flexible enough to be able to emulate one by placing arrays in arrays!
const I = [
[
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
[0, 0, 0, 0],
],
[
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
[0, 0, 1, 0],
],
[
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 1, 1, 1],
[0, 0, 0, 0],
],
[
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
[0, 1, 0, 0],
]
];
const J = [
[
[1, 0, 0],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 1],
[0, 1, 0],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 1],
[0, 0, 1]
],
[
[0, 1, 0],
[0, 1, 0],
[1, 1, 0]
]
];
const L = [
[
[0, 0, 1],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 0],
[0, 1, 1]
],
[
[0, 0, 0],
[1, 1, 1],
[1, 0, 0]
],
[
[1, 1, 0],
[0, 1, 0],
[0, 1, 0]
]
];
const O = [
[
[1, 1],
[1, 1],
]
];
const S = [
[
[0, 1, 1],
[1, 1, 0],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 1],
[0, 0, 1]
],
[
[0, 0, 0],
[0, 1, 1],
[1, 1, 0]
],
[
[1, 0, 0],
[1, 1, 0],
[0, 1, 0]
]
];
const T = [
[
[0, 1, 0],
[1, 1, 1],
[0, 0, 0]
],
[
[0, 1, 0],
[0, 1, 1],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 1],
[0, 1, 0]
],
[
[0, 1, 0],
[1, 1, 0],
[0, 1, 0]
]
];
const Z = [
[
[1, 1, 0],
[0, 1, 1],
[0, 0, 0]
],
[
[0, 0, 1],
[0, 1, 1],
[0, 1, 0]
],
[
[0, 0, 0],
[1, 1, 0],
[0, 1, 1]
],
[
[0, 1, 0],
[1, 1, 0],
[1, 0, 0]
]
];
Write HTML
<html>
<head>
<title>Tetris</title>
</head>
<body>
<div>
Score <div id="score">0</div>
</div>
<canvas id="tetris" width="200" height="400"></canvas>
</body>
</html>
Write CSS
<style>
#score{
display: inline-block;
}
div{
font-size: 25px;
font-weight: bold;
font-family: monospace;
text-align: center;
}
canvas{
display: block;
margin:0 auto;
}
</style>
Canvas
const cvs = document.getElementById("tetris");
const ctx = cvs.getContext("2d");
const scoreElement = document.getElementById("score");
Rows, columns and squares
const ROW = 20;
const COL = COLUMN = 10;
const SQ = squareSize = 20;
const VACANT = "WHITE"; // color of an empty square
Draw a square
// draw a square
function drawSquare(x,y,color){
ctx.fillStyle = color;
ctx.fillRect(x*SQ,y*SQ,SQ,SQ);
ctx.strokeStyle = "BLACK";
ctx.strokeRect(x*SQ,y*SQ,SQ,SQ);
}
Create the board
// create the board
let board = [];
for( r = 0; r <ROW; r++){
board[r] = [];
for(c = 0; c < COL; c++){
board[r][c] = VACANT;
}
}
Draw the board
// draw the board
function drawBoard(){
for( r = 0; r <ROW; r++){
for(c = 0; c < COL; c++){
drawSquare(c,r,board[r][c]);
}
}
}
drawBoard();
Give the Tetriminos colour
The official Tetrimino colours are: O = yellow, I = light blue, T = purple, L = orange, J = dark blue, S = green and Z = red. I wish that they gave specific values (eg HEX). I thought that the CSS versions of these colours looked pleasing, except for the light blue. I felt that it was too light and didn’t stand out as well as the other colours. Light blue is the equivalent to #ADD8E6. I used this a starting point to find a more suitable light blue. I settled with #86c5da. I then created a 2D array to store the name of each Tetrimino and its colour.
const PIECES = [
[Z,"red"],
[S,"green"],
[T,"purple"],
[O,"yellow"],
[L,"orange"],
[I,"#86c5da"],
[J,"dark blue"]
];
Generate the next random piece
To generate a random piece, we need to generate a random number. We can generate a random number by using a JavaScript method, Math.random().
Math.random();
On its own Math.random is not enough, as it will output a floating-point number. We can use another JavaScript method, Math.floor(), to make sure that our random number is an integer.
Math.floor(Math.random());
We need our random number to be between 1 and 7 (inclusive), as there are 7 different Tetriminos. We can use the .length() property to make sure that this happens.
function randomPiece(){
Math.floor(Math.random() * PIECES.length);
}
// generate the next random piece
function randomPiece(){
let r = randomN = Math.floor(Math.random() * PIECES.length);
return new Piece(PIECES[r][0],PIECES[r][1]);
}
let p = randomPiece();
Object piece
function Piece(tetromino, color){
this.tetromino = tetromino;
this.color = color;
this.tetrominoN = 0; // start from the first pattern
this.activeTetromino = this.tetromino[this.tetrominoN];
// control the pieces
this.x = 3;
this.y = -2;
}
Fill function
Piece.prototype.fill = function(color){
for( r = 0; r < this.activeTetromino.length; r++){
for(c = 0; c < this.activeTetromino.length; c++){
// we draw only occupied squares
if (this.activeTetromino[r][c]){
drawSquare(this.x + c,this.y + r, color);
}
}
}
}
Draw a piece on the board
Piece.prototype.draw = function(){
this.fill(this.color);
}
Undraw piece
Piece.prototype.unDraw = function(){
this.fill(VACANT);
}
Move piece down
Piece.prototype.moveDown = function(){
if(!this.collision(0,1,this.activeTetromino)){
this.unDraw();
this.y++;
this.draw();
} else {
// we lock the piece and generate a new one
this.lock();
p = randomPiece();
}
}
Move piece to the right
Piece.prototype.moveRight = function(){
if (!this.collision(1,0,this.activeTetromino)){
this.unDraw();
this.x++;
this.draw();
}
}
Move piece to the left
Piece.prototype.moveLeft = function(){
if (!this.collision(-1,0,this.activeTetromino)){
this.unDraw();
this.x--;
this.draw();
}
}
Rotate a piece
Piece.prototype.rotate = function(){
let nextPattern = this.tetromino[(this.tetrominoN + 1)%this.tetromino.length];
let kick = 0;
if (this.collision(0,0,nextPattern)){
if (this.x > COL/2){
// it's the right wall
kick = -1; // we need to move the piece to the left
} else {
// it's the left wall
kick = 1; // we need to move the piece to the right
}
}
if (!this.collision(kick,0,nextPattern)){
this.unDraw();
this.x += kick;
this.tetrominoN = (this.tetrominoN + 1)%this.tetromino.length; // (0+1)%4 => 1
this.activeTetromino = this.tetromino[this.tetrominoN];
this.draw();
}
}
let score = 0;
Piece.prototype.lock = function(){
for(r = 0; r < this.activeTetromino.length; r++){
for(c = 0; c < this.activeTetromino.length; c++){
// we skip the vacant squares
if (!this.activeTetromino[r][c]){
continue;
}
// pieces to lock on top = game over
if (this.y + r < 0){
alert("Game Over");
// stop request animation frame
gameOver = true;
break;
}
// we lock the piece
board[this.y+r][this.x+c] = this.color;
}
}
// remove full rows
for(r = 0; r < ROW; r++){
let isRowFull = true;
for( c = 0; c < COL; c++){
isRowFull = isRowFull && (board[r][c] != VACANT);
}
if (isRowFull){
// if the row is full
// we move down all the rows above it
for ( y = r; y > 1; y--){
for( c = 0; c < COL; c++){
board[y][c] = board[y-1][c];
}
}
// the top row board[0][..] has no row above it
for ( c = 0; c < COL; c++){
board[0][c] = VACANT;
}
// increment the score
score += 10;
}
}
// update the board
drawBoard();
// update the score
scoreElement.innerHTML = score;
}
// collision fucntion
Piece.prototype.collision = function(x,y,piece){
for( r = 0; r < piece.length; r++){
for(c = 0; c < piece.length; c++){
// if the square is empty, we skip it
if(!piece[r][c]){
continue;
}
// coordinates of the piece after movement
let newX = this.x + c + x;
let newY = this.y + r + y;
// conditions
if(newX < 0 || newX >= COL || newY >= ROW){
return true;
}
// skip newY < 0; board[-1] will crush our game
if(newY < 0){
continue;
}
// check if there is a locked piece alrady in place
if( board[newY][newX] != VACANT){
return true;
}
}
}
return false;
}
Controlling Tetriminos
To control Tetriminos I have used an event listener. There is a JavaScript event listener method, addEventListener() that I have used to fire when one of the arrow keys have been pressed. I have chosen the left arrow for left, right arrow for right, down arrow for down and hard down and up for rotation.
I added two parameters to the addEventsListener method, keydown and CONTROL. Keydown tells the event listener that we want to respond to when a key is pressed down. CONTROL is a function that will set out what should happen when the arrow keys are pressed.
Why keydown?
As well as keydown, there is also a keyup and keypress. If you don’t know why I chose keydown, read my article on JavaScript event listeners.
document.addEventListener("keydown", CONTROL);
Next, I wrote a function (CONTROL) that when called runs an if statement to determine what to do if one of the arrow keys are pressed.
function CONTROL(event){
if (event.keyCode == 37){
p.moveLeft();
dropStart = Date.now();
} else if (event.keyCode == 38){
p.rotate();
dropStart = Date.now();
} else if (event.keyCode == 39){
p.moveRight();
dropStart = Date.now();
} else if (event.keyCode == 40){
p.moveDown();
}
}
let dropStart = Date.now();
let gameOver = false;
function drop(){
let now = Date.now();
let delta = now - dropStart;
if(delta > 1000){
p.moveDown();
dropStart = Date.now();
}
if( !gameOver){
requestAnimationFrame(drop);
}
}
drop();
Piece.prototype.rotate = function(){
let nextPattern = this.tetromino[(this.tetrominoN + 1)%this.tetromino.length];
let kick = 0;
if(this.collision(0,0,nextPattern)){
if(this.x > COL/2){
// it's the right wall
kick = -1; // we need to move the piece to the left
}else{
// it's the left wall
kick = 1; // we need to move the piece to the right
}
}
if(!this.collision(kick,0,nextPattern)){
this.unDraw();
this.x += kick;
this.tetrominoN = (this.tetrominoN + 1)%this.tetromino.length; // (0+1)%4 => 1
this.activeTetromino = this.tetromino[this.tetrominoN];
this.draw();
}
}
let score = 0;
Piece.prototype.lock = function(){
for( r = 0; r < this.activeTetromino.length; r++){
for(c = 0; c < this.activeTetromino.length; c++){
// we skip the vacant squares
if( !this.activeTetromino[r][c]){
continue;
}
// pieces to lock on top = game over
if(this.y + r < 0){
alert("Game Over");
// stop request animation frame
gameOver = true;
break;
}
// we lock the piece
board[this.y+r][this.x+c] = this.color;
}
}
// remove full rows
for(r = 0; r < ROW; r++){
let isRowFull = true;
for( c = 0; c < COL; c++){
isRowFull = isRowFull && (board[r][c] != VACANT);
}
if(isRowFull){
// if the row is full
// we move down all the rows above it
for( y = r; y > 1; y--){
for( c = 0; c < COL; c++){
board[y][c] = board[y-1][c];
}
}
// the top row board[0][..] has no row above it
for( c = 0; c < COL; c++){
board[0][c] = VACANT;
}
// increment the score
score += 10;
}
}
// update the board
drawBoard();
// update the score
scoreElement.innerHTML = score;
}
// collision fucntion
Piece.prototype.collision = function(x,y,piece){
for( r = 0; r < piece.length; r++){
for(c = 0; c < piece.length; c++){
// if the square is empty, we skip it
if(!piece[r][c]){
continue;
}
// coordinates of the piece after movement
let newX = this.x + c + x;
let newY = this.y + r + y;
// conditions
if(newX < 0 || newX >= COL || newY >= ROW){
return true;
}
// skip newY < 0; board[-1] will crush our game
if(newY < 0){
continue;
}
// check if there is a locked piece alrady in place
if( board[newY][newX] != VACANT){
return true;
}
}
}
return false;
}
// CONTROL the piece
document.addEventListener("keydown",CONTROL);
function CONTROL(event){
if(event.keyCode == 37){
p.moveLeft();
dropStart = Date.now();
}else if(event.keyCode == 38){
p.rotate();
dropStart = Date.now();
}else if(event.keyCode == 39){
p.moveRight();
dropStart = Date.now();
}else if(event.keyCode == 40){
p.moveDown();
}
}
// drop the piece every 1sec
let dropStart = Date.now();
let gameOver = false;
function drop(){
let now = Date.now();
let delta = now - dropStart;
if (delta > 1000) {
p.moveDown();
dropStart = Date.now();
}
if (!gameOver) {
requestAnimationFrame(drop);
}
}
drop();