React Tetris

Jack Overby
3 min readDec 7, 2020

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

Per React’s documentation, you need to have both Node Package Manager (npm) version ≥5.6 and Node (node) version ≥ 8.10. React contains a large number of files, so the download can take 10 or more minutes. Be patient… it’s worth it! And you can delete the extraneous files later.

Recall that our program last week contained two classes (Piece and Tetris), as well as a dictionary containing the seven different pieces that can appear (and be rotated) during a game of Tetris. We’re going to upgrade the simple vanilla JavaScript classes into React class components, and we’re going to flesh out the pieceObj object to contain some visual elements, such as color.

pieceObj

Each piece will have two basic visual attributes:

  • Color (ranging from red to purple)
  • 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;

--

--