Last week, I did a very simple implementation of Tetris that caused Xs and Os to appear on the screen, with basic logic to handle rotations, checking the validity of moves (e.g. whether a piece can go down or be moved left or right) completion of rows, updating score and determining when a game is over. Now, we’re going to give this program a makeover. The underlying logic will be the same, but the presentation will be in… React!

Creating a React App

There are several ways to launch a React App, but the easiest path is to run the following lines of code:

npx create-react-app my-app
cd my-app
npm start

pieceObj

Each piece will have two basic visual attributes:

  • Shape (represented as an array of arrays- e.g. 2x2 square, 4x1 rod)
const pieceObj = {
1: {'color': 'orange',
'shape': [[0,0,0,0],[0,0,0,0],[0,0,0,0],[1,1,1,1]]
},
2: {'color': 'dark-blue',
'shape': [[0,0,0],[1,1,1],[1,0,0]]
},
3: {'color': 'purple',
'shape': [[0,0,0],[1,1,1],[0,0,1]]
},
4: {'color': 'yellow',
'shape': [[0,0,0],[1,1,1],[0,1,0]]
},
5: {'color': 'light-blue',
'shape': [[0,0,0],[1,1,0],[0,1,1]]
},
6: {'color': 'green',
'shape': [[0,0,0],[0,1,1],[1,1,0]]
},
7: {'color': 'red',
'shape': [[1,1],[1,1]]
}
};

Piece

Next, we move to our Piece class, which will be initialized with a randomly selected piece object from pieceObj and then given several additional functions for rotating, calculating borders and rendering:

import pieceDict from './PieceDict';
import React from 'react';
class Piece extends React.Component {
constructor(n=Math.ceil(7 * Math.random())) {
this.piece = pieceDict[n];
}
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;
}
renderPiece() {
console.log(this.piece.map(row=>row.map(col=>col?"X":" ").join('')).join('\n'))
}
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;
})
}
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;
})
}
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;
})
}
}export default Piece;

Tetris

import Piece from './Piece';
import React from './React';
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;

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