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.

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:

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 fileimport 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:

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+num,this.current.location+i]:null)        const checks = lows.map(low=>low!==null?[low+1,low]:null)        return !checks.find(check=>(check && (check>=21 || this.board[check][check])));    }canMoveLeft() {        const lefts = this.current.piece.leftmost().map((num,i)=>num!==null?[this.current.location+i,this.current.location+num]:null);        const checks = lefts.map(left=>left!==null?[left,left-1]:null);        return !checks.find(check=>(check && (check<0 || this.board[check][check])));    }canMoveRight() {        const rights = this.current.piece.rightmost().map((num,i)=>num!==null?[this.current.location+i,this.current.location+num]:null);        const checks = rights.map(right=>right!==null?[right,right+1]:null);        return !checks.find(check=>(check && (check>=10 || this.board[check][check])));    }moveDown() {        if (this.canMoveDown()) this.placePiece([this.current.location+1, this.current.location]);        else this.checkBoard();    }moveLeft() {        if (this.canMoveLeft()) this.placePiece([this.current.location, this.current.location-1]);    }moveRight() {        if (this.canMoveRight()) this.placePiece([this.current.location, this.current.location+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 < 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;`