Table of contents
- Features Required:
- Design Patterns Involved:
- Multiple Algorithms Involved:
- Diagrams
- Code (Java)
- Explanation of Design Choices:
- What if Tetris shapes were completely unpredictable and more complex than just the usual L or T?
- What if you had to run this on an embedded system like a Raspberry Pi with limited resources?
- What if this system were online? Are there specific changes or optimizations you'd need to make compared to offline systems?
Features Required:
Board creation and management.
Falling tetrominoes (Tetris blocks).
Rotation and movement of tetrominoes.
Line completion and scoring.
Game over condition.
Rendering the board.
Handling player inputs (move left, right, down, rotate).
Design Patterns Involved:
Factory Pattern:
- Why: The factory pattern is used for creating various types of Tetrominoes (L, T, I, O, Z shapes) dynamically without exposing the instantiation logic. This makes the system extensible to add more shapes.
Strategy Pattern:
- Why: Used to handle different rotation algorithms for tetrominoes based on their shape. Some shapes rotate in 90-degree increments, others have more complex rotations. Strategy pattern enables encapsulation of these different behaviors.
Observer Pattern:
- Why: The game needs to update the board and game status (like score, level) whenever certain actions occur (like clearing a line, block moving). The observer pattern allows decoupled classes like the
Board
to notify theScoreManager
and other game components about events like line completion.
- Why: The game needs to update the board and game status (like score, level) whenever certain actions occur (like clearing a line, block moving). The observer pattern allows decoupled classes like the
Singleton Pattern:
- Why: Used for
GameBoard
to ensure only one instance of the game board exists throughout the application.
- Why: Used for
Command Pattern:
- Why: Handling player inputs (move left, move right, rotate) in a way that commands can be decoupled from the actual logic that processes them.
Template Method Pattern:
- Why: Implemented in the base
Tetromino
class to provide a standard skeleton for operations like falling, rotating, and moving. Subclasses will override specific methods for specialized behavior (like rotation specifics for different shapes).
- Why: Implemented in the base
Multiple Algorithms Involved:
Tetromino Rotation:
- Using matrix rotation algorithms for 2D arrays representing the tetrominoes.
Line Completion:
- When a row is filled, all blocks in that row are cleared, and rows above move down. This can be efficiently handled using row traversal and shifting.
Collision Detection:
- Detects if a block can move or rotate by checking its future position against existing blocks and the boundaries of the board.
Diagrams
Code (Java)
// Interface for Tetromino Rotation Strategies
interface IRotationStrategy {
void rotate(Tetromino tetromino, GameBoard board);
}
// Strategy for Standard Rotation (90-degree)
class StandardRotationStrategy implements IRotationStrategy {
@Override
public void rotate(Tetromino tetromino, GameBoard board) {
// Algorithm to rotate a tetromino 90 degrees clockwise
// Rotate 2D array representation of tetromino
int[][] shape = tetromino.getShape();
int N = shape.length;
int[][] rotated = new int[N][N];
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
rotated[j][N - i - 1] = shape[i][j];
}
}
if (!board.isCollision(rotated, tetromino.getX(), tetromino.getY())) {
tetromino.setShape(rotated);
}
}
}
// Abstract class for Tetromino
abstract class Tetromino {
protected int[][] shape;
protected int x, y; // position on the board
protected IRotationStrategy rotationStrategy;
public Tetromino(IRotationStrategy strategy) {
this.rotationStrategy = strategy;
}
public void rotate(GameBoard board) {
rotationStrategy.rotate(this, board);
}
public void moveLeft(GameBoard board) {
if (!board.isCollision(shape, x - 1, y)) {
x--;
}
}
public void moveRight(GameBoard board) {
if (!board.isCollision(shape, x + 1, y)) {
x++;
}
}
public void moveDown(GameBoard board) {
if (!board.isCollision(shape, x, y + 1)) {
y++;
} else {
board.placeTetromino(this);
}
}
public int[][] getShape() {
return shape;
}
public void setShape(int[][] shape) {
this.shape = shape;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
}
// L-shaped Tetromino
class LTetromino extends Tetromino {
public LTetromino() {
super(new StandardRotationStrategy());
this.shape = new int[][] {
{1, 0},
{1, 0},
{1, 1}
};
this.x = 3;
this.y = 0;
}
}
// Factory to create Tetrominoes
class TetrominoFactory {
public static Tetromino createTetromino(char type) {
switch (type) {
case 'L': return new LTetromino();
case 'T': return new TTetromino();
case 'I': return new ITetromino();
// Add more tetromino types
default: throw new IllegalArgumentException("Unknown Tetromino type");
}
}
}
// Singleton for GameBoard
class GameBoard {
private static final int WIDTH = 10;
private static final int HEIGHT = 20;
private static GameBoard instance = null;
private int[][] board;
private int score;
private GameBoard() {
this.board = new int[HEIGHT][WIDTH];
}
public static GameBoard getInstance() {
if (instance == null) {
instance = new GameBoard();
}
return instance;
}
public boolean isCollision(int[][] shape, int x, int y) {
// Check for collisions with walls, floor, or other blocks
for (int i = 0; i < shape.length; i++) {
for (int j = 0; j < shape[i].length; j++) {
if (shape[i][j] != 0) {
if (y + i >= HEIGHT || x + j < 0 || x + j >= WIDTH || board[y + i][x + j] != 0) {
return true;
}
}
}
}
return false;
}
public void placeTetromino(Tetromino tetromino) {
int[][] shape = tetromino.getShape();
int x = tetromino.getX();
int y = tetromino.getY();
for (int i = 0; i < shape.length; i++) {
for (int j = 0; j < shape[i].length; j++) {
if (shape[i][j] != 0) {
board[y + i][x + j] = shape[i][j];
}
}
}
checkLineCompletion();
}
private void checkLineCompletion() {
for (int i = 0; i < HEIGHT; i++) {
boolean isFullLine = true;
for (int j = 0; j < WIDTH; j++) {
if (board[i][j] == 0) {
isFullLine = false;
break;
}
}
if (isFullLine) {
clearLine(i);
score += 100;
}
}
}
private void clearLine(int line) {
for (int i = line; i > 0; i--) {
System.arraycopy(board[i - 1], 0, board[i], 0, WIDTH);
}
// Clear top line
for (int j = 0; j < WIDTH; j++) {
board[0][j] = 0;
}
}
public void renderBoard() {
// Render the board to console
for (int i = 0; i < HEIGHT; i++) {
for (int j = 0; j < WIDTH; j++) {
System.out.print(board[i][j] == 0 ? "." : "#");
}
System.out.println();
}
System.out.println("Score: " + score);
}
}
// Game class
class TetrisGame {
private GameBoard board;
private Tetromino currentTetromino;
public TetrisGame() {
board = GameBoard.getInstance();
currentTetromino = TetrominoFactory.createTetromino('L'); // Start with an 'L' Tetromino
}
public void startGame() {
while (true) {
board.renderBoard();
// Handle user inputs for moving the Tetromino (omitted for brevity)
}
}
}
// Command Interface for handling user inputs
interface ICommand {
void execute();
}
class MoveLeftCommand implements ICommand {
private Tetromino tetromino;
private GameBoard board;
public MoveLeftCommand(Tetromino tetromino, GameBoard board) {
this.tetromino = tetromino;
this.board = board;
}
@Override
public void execute() {
tetromino.moveLeft(board);
}
}
class MoveRightCommand implements ICommand {
private Tetromino tetromino;
private GameBoard board;
public MoveRightCommand(Tetromino tetromino, GameBoard board) {
this.tetromino = tetromino;
this.board = board;
}
@Override
public void execute() {
tetromino.moveRight(board);
}
}
// Main Class
public class Main {
public static void main(String[] args) {
TetrisGame game = new TetrisGame();
game.startGame();
}
}
Explanation of Design Choices:
Factory Pattern:
TetrominoFactory
is used to instantiate different tetromino shapes (LTetromino
,TTetromino
, etc.) dynamically, allowing for easy future expansion.Strategy Pattern: Rotation logic for tetrominoes is abstracted into the
IRotationStrategy
interface, allowing each shape to define its rotation algorithm (in this case, a standard 90-degree clockwise rotation).Observer Pattern: Not implemented in this snippet for simplicity but would be used for score updates, notifying when a line is cleared.
Singleton Pattern: Ensures that only one
GameBoard
instance is used throughout the game, avoiding inconsistencies in game state.Command Pattern:
ICommand
interface is used to encapsulate user input (e.g., moving tetrominoes left, right, down) into separate command classes. This allows decoupling user actions from their implementation.
Time and Space Complexity:
Tetromino Movement: Each movement (left, right, down) requires checking for collisions with the board, which takes O(n) time, where n is the number of cells in the tetromino.
Rotation: The rotation of tetrominoes involves a matrix transformation, which is O(n^2) for n x n matrices (typically small for Tetris).
Line Completion Check: Checking and clearing lines requires O(w) time per row, where w is the width of the board.
Garbage Collection and Optimization:
- Tetrominoes are created frequently, so care must be taken to minimize object churn to avoid frequent garbage collection pauses. Reusing tetromino objects or using object pools can help reduce garbage collection overhead.
PlayGround/IDE : https://ide.lldcoding.com/tetris-game
What if Tetris shapes were completely unpredictable and more complex than just the usual L or T?
Imagine designing systems that handle such randomness! Learn how in our course and take your low-level design skills to the next level.
What if you had to run this on an embedded system like a Raspberry Pi with limited resources?
Learn how to optimize low-level design for constrained environments in our course and master the skills to tackle real-world challenges!
What if this system were online? Are there specific changes or optimizations you'd need to make compared to offline systems?
Learn how to adapt your design for scalable, reliable online environments in our course!"