Tetris Rules
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
- Pieces (7 different available shapes) fall vertically down from the sky (i.e. the top of the screen)
- They continue to fall until landing on top of another piece or hitting the bottom of the screen
- When an entire row is filled by pieces, it disappears, and the player receives points
- Every 10 rows, the level increases: pieces fall faster, and each completed row yields more points
- Pieces can be rotated clockwise; thus, each piece can, in effect, take 4 different shapes
- The game ends when the board is filled and no new pieces can descend onto the screen
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
- 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).
- Score: A player’s score starts at 0 and increases with each completed row. The number of points earned per completed row increases; therefore, score is a 2nd-degree polynomial function of the number of rows completed.
- Rows completed & level: These increment upwards with each finished row.
- Current piece: At any given moment, only one pieces is falling & can be moved. Once a piece “stops”, it freezes permanently in place, and another piece is randomly generated at the top of the board.
- Game over or not: The game essentially exists as a while loop: while game is not over, it continues. Once the board is filled, game over switches to true (or game on becomes false), and the game ends.
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 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:
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;