The final project of my second course in object oriented programming (2014) had a very simple, open-ended prompt: Create a non-trivial game written in the Java language that made use of OOP principles. Because of my love for the game, I decided to create a Tetris clone.
Background
|
Tetris was first released in 1984 by Russian game designer Alexey Pajitnov. Game pieces called "tetriminos", consisting of four blocks in various configurations, would drop down from the top of the screen. The player would control the left and right movement, and orientation, before it reached the bottom, and then clear lines by lining up the blocks horizontally with no gaps. Modern variants of tetris sometimes allow the player to preview a queue of tetriminos instead of just one, allow the user to immediately drop a tetrimino to the bottom (usually using the spacebar), and/or hold a piece off to the side until it is needed, simply referred to as "holding". When the hold button is pressed, the tetrimino currently being held (if there is one) and the tetrimino currently dropping switch places. |
About this Game
This Java version of Tetris implements all three previously mentioned modern features, with the player able to drop a piece by pressing the spacebar or hold a piece by pressing the shift key. The righthand panel previews the next four tetrimios that will drop and gives line count and scoring totals. Watch the following video for a short demonstration of gameplay.
The code
It was programmed using only native Java libraries, including swing, and all graphics were custom designed. A Grid class controls the individual blocks on the game screen, containing an array of Tetromino objects and responding to input requests from the main class. Each tetromino object extends to its own class, which defines its behavior, such as rotation.
package tetris;
import java.util.*;
import javax.swing.*;
public class Grid {
final int previewSize = 4;
int[][] gridMatrix;
int width, height, score, linesCleared, linesTilNext = 10, level = 1;
Tetromino tetromino, holdTetromino;
Tetromino[] nextTetrominos = new Tetromino[previewSize];
boolean holdEnabled;
public Grid(int w, int h) {
gridMatrix = new int[w][h];
width = w;
height = h;
for (int i = 0; i < previewSize; i++)
nextTetrominos[i] = getRandomTetromino();
nextTetromino();
}
public void set(int x, int y, int val) {
gridMatrix[x][y] = val;
}
public int get(int x, int y) {
return gridMatrix[x][y];
}
public void setTetromino(Tetromino tetro) {
tetromino = tetro;
}
public void moveTetromino(Direction d) throws ArrayIndexOutOfBoundsException {
int blockx, blocky, movex, movey;
boolean goodToMove = true;
for (int j = 0; j < 2; j++) { //3 iterations, j=0 checks adjacent spaces; if clear, j=1 clears them and j=2 updates
for (int i = 0; i < 4; i++) {
blockx = tetromino.getX(i);
blocky = tetromino.getY(i);
movex = blockx;
movey = blocky;
switch (d) {
case DOWN:
if (i == 0 && tetromino.getFarthest(Direction.DOWN) == height - 1) {
clearLines();
return;
} //ground collision
movey = blocky + 1;
break;
case LEFT:
if (tetromino.getFarthest(Direction.LEFT) == 0) goodToMove = false; //left collision
movex = blockx - 1;
break;
case RIGHT:
if (tetromino.getFarthest(Direction.RIGHT) == width - 1) goodToMove = false; //right collision
movex = blockx + 1;
break;
case SPIN:
movex = tetromino.getNextSpin(i).getX();
movey = tetromino.getNextSpin(i).getY();
}
switch (j) {
case 0:
if (get(movex, movey) > 0 && !tetromino.contains(new Coord(movex, movey))) { //there is a block in the way
if (d == Direction.DOWN) {
if (tetromino.getFarthest(Direction.DOWN) < 5)
gameOver();
else
clearLines();
return;
} else
goodToMove = false;
}
break;
case 1:
if (i == 0) clearTetromino(); //clear the current block
tetromino.setBlock(i, movex, movey); //set new block
set(movex, movey, tetromino.getNumber());
if (i == 3 && d == Direction.SPIN) tetromino.spinCounter++;
}
}
if (!goodToMove) return;
}
}
public void hardDrop() {
do {
moveTetromino(Direction.DOWN);
} while (tetromino.getFarthest(Direction.DOWN) > 3);
}
public void gameOver() {
clearAll();
score = 0;
linesCleared = 0;
linesTilNext = 10;
level = 1;
holdTetromino = null;
Tetris.paused = true;
Tetris.clip.stop();
JOptionPane.showMessageDialog(null, new JComponent[] {
new JLabel("Your score: " + score + "\n Click ok to begin a new game.")
}, "Game Over!", JOptionPane.PLAIN_MESSAGE);
}
private void nextTetromino() {
tetromino = nextTetrominos[0];
for (int i = 1; i < previewSize; i++)
nextTetrominos[i - 1] = nextTetrominos[i];
nextTetrominos[previewSize - 1] = getRandomTetromino();
holdEnabled = true; //new tetromino, so reenable the ability to hold
}
private Tetromino getRandomTetromino() {
Random rand = new Random();
int randomNumber = rand.nextInt(7);
Tetromino[] tetrominoList = {
new IBlock(),
new JBlock(),
new LBlock(),
new OBlock(),
new SBlock(),
new TBlock(),
new ZBlock()
};
return tetrominoList[randomNumber];
}
public void hold() {
Tetromino temp;
clearTetromino();
if (holdTetromino == null) {
holdTetromino = tetromino;
nextTetromino();
} else {
temp = tetromino;
tetromino = holdTetromino;
holdTetromino = temp;
}
tetromino.reconstruct();
tetromino.spinCounter = 0;
holdEnabled = false;
}
private void clearAll() {
for (int i = 2; i < height; i++) //draw the grid
for (int j = 0; j < width; j++)
set(j, i, 0);
}
private void clearTetromino() {
for (int i = 0; i < 4; i++)
set(tetromino.getX(i), tetromino.getY(i), 0);
}
private void clearLines() {
int x = 0, linesThisTime = 0, y = tetromino.getFarthest(Direction.DOWN);
for (int lines = 0; lines < 4; lines++) {
if (lineAt(y)) {
linesCleared++;
linesThisTime++;
for (int i = y; i > 0; i--) //shift down
for (int j = 0; j < width; j++)
set(j, i, get(j, i - 1));
} else y--;
}
score += linesThisTime * linesThisTime * level * 100;
linesTilNext -= linesThisTime;
if (linesTilNext < 1) {
level++;
linesTilNext += level * 10;
}
nextTetromino();
}
private boolean lineAt(int y) {
boolean r = true;
for (int x = 0; x < width; x++)
if (get(x, y) == 0) {
r = false;
break;
}
return r;
}
}