I grew up in a video-game-deprived household. I received my first console until I was nearly 16 years old: a joint Christmas gift to my sister and me of… a Nintendo Wii. *sobs* Hey, what can I say? You can keep your fancy graphics and story lines; Wii Golf is as good as it gets! As a result of my minimal exposure to video games, I’ve never spent much time on them, and the few games that have caught my attention are simple, lo-fi affairs: Wii Golf, Wii Tennis, Pokemon (Generations 1–3), 2048… and lately, Tetris! This Soviet classic, developed by Alexey Pajitnov in 1984, has been surpassed in plot, graphics over the past 36 years but still lives on. I have frittered away shameful amounts of free time at FreeTetris.org over the past month or so. My recent forays into football and baseball simulations made me think: how would I go about implementing this simple little game? What sort of unexpected complexities and edge cases does it contain? After a few days tinkering around, my answer is: many!

Basic Rules

Here are the basic rules of Tetris:

  • Board consists of 18 rows and 10 columns

Object-Oriented Implementation

The game consists of several main classes:

  • Piece: A piece can fit into a 2x2, 3x3 or 4x4 grid. It can be rotated 90 clockwise.
Image for post
Image for post
The seven pieces of Tetris, in their default positions

One of our main goals will be to program the falling of tetris pieces and to regulate their movement. Pieces will continue to fall until hitting another piece or the ground, at which point it will freeze in place, and a new piece will begin to fall from the top. We also need to make sure that pieces cannot be moved horizontally off the screen.

  • Game: A game contains several main objects
  1. Board: The aforementioned 18x10 grid, each of whose spots will either be occupied by a piece or not. We’ll implement it as an 18x10 array with each member being 0 (empty) or 1 (taken).

Piece

Here is our code for the Piece class.

First, we create a dictionary that holds all of the possible piece shapes. These pieces will be randomly generated and inserted into a game board:

const pieceDict = {
1: [[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,1,1,1]],
2: [[1,1,1],[1,0,0],[0,0,0]],
3: [[1,1,1],[0,0,1],[0,0,0]],
4: [[1,1,1],[0,1,0],[0,0,0]],
5: [[1,1,0],[0,1,1],[0,0,0]],
6: [[0,1,1],[1,1,0],[0,0,0]],
7: [[1,1],[1,1]]
};

Now, we’ll write some code to print out each piece; ideally, we should see seven pieces that correspond to the above photo:

Image for post
Image for post

Hooray! It ain’t pretty, but we can see all seven of the desired shapes.

Now, let’s code out the Piece class itself:

// importing dictionary from separate file
import pieceDict from './PieceDict';
class Piece {
// Randomly generate piece from dictionary
constructor(n=Math.ceil(7 * Math.random())) {
this.piece = pieceDict[n];
}
// Mathematical operations to rotate pieces to the right; similar to a matrix transformation
rotate() {
const copy = JSON.parse(JSON.stringify(this.piece));
const n = this.piece.length;
for (let i=0;i<n;i++) {
for (let j=0;j<n;j++) {
copy[i][n-1-j] = this.piece[j][i];
}
}
this.piece = copy;
}
// using above code snippet to display piece on the screen
renderPiece() {
console.log(this.piece.map(row=>row.map(col=>col?"X":" ").join('')).join('\n'))
}
// Calculates the bottom boundary for each piece; this will be used in the Tetris class, to determine when a piece will contact
bottommost() {
const n = this.piece.length;
return [...Array(n).keys()].map(i=> {
for (let j=n-1;j>=0;j--) {
if (this.piece[j][i]) {
return j;
}
}
return null;
})
}
// Calculates the right boundary for each piece; this will be used in the Tetris class, to determine whether a piece can be moved to the right
leftmost() {
const n = this.piece.length;
return [...Array(n).keys()].map(i=> {
for (let j=0;j<n;j++) {
if (this.piece[i][j]) {
return j;
}
}
return null;
})
}
// Calculates the right boundary for each piece; this will be used in the Tetris class, to determine whether a piece can be moved to the right
rightmost() {
const n = this.piece.length;
return [...Array(n).keys()].map(i=> {
for (let j=n-1;j>=0;j--) {
if (this.piece[i][j]) {
return j;
}
}
return null;
})
}

Let’s test out our rotate() function on one of the pieces; it should rotate 90 degrees clockwise each time:

Image for post
Image for post

Hooray again! Our piece is rotating 90 degrees clockwise each time, eventually returning to its original position.

Now, let’s move on to the Tetris class.

Tetris

import Piece from './Piece';class Tetris {
constructor() {
this.board = [...Array(21).keys()].map(i=>[...Array(10).keys()].map(j=>0));
this.current = {};
this.gameOn = true;
this.score = 0;
this.level = 1;
this.rowsCompleted = 0;
this.getPiece();
}
placePiece(coords=this.current.location) {
const [currX, currY] = this.current.location;
const [x, y] = coords;
const shape = this.current.piece.piece;
for (let i=0; i<this.current.len; i++) {
for (let j=0; j<this.current.len; j++) {
if (shape[i][j]===1) this.board[currX+i][currY+j]=0;
}
}
for (let i=0; i<this.current.len; i++) {
for (let j=0; j<this.current.len; j++) {
if (this.board[x+i]!==undefined && this.board[x+i][y+j]!==undefined && this.board[x+i][y+j]===0) this.board[x+i][y+j] = shape[i][j];
}
}
this.current.location = [x,y];
this.renderBoard();
}
getPiece() {
this.board = this.board.map(row=>row.map(col=>col?2:0));
const piece = new Piece();
const n = piece.piece.length;
this.current = {
piece: piece,
location: [4-n, 3],
len: n
}
this.placePiece();
}
canMoveDown() {
const lows = this.current.piece.bottommost().map((num,i)=>num!==null?[this.current.location[0]+num,this.current.location[1]+i]:null)
const checks = lows.map(low=>low!==null?[low[0]+1,low[1]]:null)
return !checks.find(check=>(check && (check[0]>=21 || this.board[check[0]][check[1]])));
}
canMoveLeft() {
const lefts = this.current.piece.leftmost().map((num,i)=>num!==null?[this.current.location[0]+i,this.current.location[1]+num]:null);
const checks = lefts.map(left=>left!==null?[left[0],left[1]-1]:null);
return !checks.find(check=>(check && (check[1]<0 || this.board[check[0]][check[1]])));
}
canMoveRight() {
const rights = this.current.piece.rightmost().map((num,i)=>num!==null?[this.current.location[0]+i,this.current.location[1]+num]:null);
const checks = rights.map(right=>right!==null?[right[0],right[1]+1]:null);
return !checks.find(check=>(check && (check[1]>=10 || this.board[check[0]][check[1]])));
}
moveDown() {
if (this.canMoveDown()) this.placePiece([this.current.location[0]+1, this.current.location[1]]);
else this.checkBoard();
}
moveLeft() {
if (this.canMoveLeft()) this.placePiece([this.current.location[0], this.current.location[1]-1]);
}
moveRight() {
if (this.canMoveRight()) this.placePiece([this.current.location[0], this.current.location[1]+1]);
}
rotate() {
const [x, y] = this.current.location;
for (let i=0; i<this.current.len; i++) {
for (let j=0; j<this.current.len; j++) {
if (this.current.piece.piece[i][j]) this.board[x+i][y+j]=0;
}
}
this.current.piece.rotate();
this.placePiece();
}
renderBoard() {
console.log(`SCORE: ${this.score}\nLEVEL: ${this.level}\nROWS COMPLETED: ${this.rowsCompleted}`);
console.log(this.board.slice(0,21).map(
row=>row.map(col=> {
if (col===2) return "X";
else if (col===1) return "O"
else return " ";
}).join('')).join('\n')
);
}
checkBoard() {
if (this.current.location[0] < 3 && !this.canMoveDown()) this.gameOn = false;
else {
const completedRows = [...Array(this.board.length).keys()].filter(i=>this.board[i].find(col=>!col)===undefined);
completedRows.forEach(i=>{
this.board.splice(i,1);
this.board.unshift([...Array(10).keys()].map(i=>0));
this.score += 40 * this.level;
this.rowsCompleted += 1;
if (this.rowsCompleted % 10 === 0) this.level += 1;
})
this.getPiece();
}
}
playGame() {
setInterval();
}
}
export default Tetris;

Written by

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