Site icon TechVidvan

Java Tetris Game – Thrilling Blocks & Endless Joy!

A well-known game’s Java adaptation is the Tetris game project. It demonstrates how to develop a graphical user interface using the AWT and Swing libraries. Players must arrange falling blocks to form entire lines in order to win the game. The project presents key ideas in Java GUI programming and game creation.

About Java Tetris Game

Prerequisites For Tetris Game Using Java

Download Java Tetris Game Project

Please download the source code of the Java Tetris Game Project from the following link: Java Tetris Game Project Code.

Steps to Create Tetris Game in Java:

1. Initial SetUp:

This step involves the initialization of the game and setting up the necessary libraries required for the respective development. The imported packages are ‘java.awt’ and ‘javax.swing’ for the GUI implementation.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

2. Creating the Game Board:

This step involves the definition of the main class ‘Tetris’ that extends ‘JPanel’ to create a game panel. The basic elements, like the game board and the GUI components, like- buttons and labels, are set up inside this main class.

public Tetris() {
    initBoard();
}

private void initBoard() {
    setFocusable(true);
    addKeyListener(new TAdapter());
    setBackground(Color.BLACK);
    curPiece = new Shape();
    timer = new Timer(INITIAL_DELAY, new GameCycle());
    board = new Tetrominoe[BOARD_WIDTH][BOARD_HEIGHT];
    clearBoard();

    // Designing the Game Over Label
    gameOverLabel = new JLabel("Game Over");
    gameOverLabel.setForeground(Color.RED);
    gameOverLabel.setFont(new Font("SansSerif", Font.BOLD, 36));
    gameOverLabel.setBounds(50, 230, 200, 50);
    gameOverLabel.setVisible(false);
    add(gameOverLabel);

    // Designing the START Button
    startButton = new JButton("Start");
    startButton.setBackground(Color.ORANGE);
    startButton.setOpaque(true);
    startButton.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            if (isStarted) {
                restart();
            } else {
                start();
            }
        }
    });
    setLayout(null);
    startButton.setBounds(0, 560, 300, 30);
    add(startButton);
}

@Override
public Dimension getPreferredSize() {
    return new Dimension(BOARD_WIDTH * SQUARE_SIZE, BOARD_HEIGHT * SQUARE_SIZE + 40);
}

// Helper methods for square dimensions
private int squareWidth() {
    return getWidth() / BOARD_WIDTH;
}

private int squareHeight() {
    return getHeight() / BOARD_HEIGHT;
}

// Get the Tetrominoe shape at a specific position on the board
private Tetrominoe shapeAt(int x, int y) {
    return board[x][y];
}

3. Implementing the Tetriminoe Shapes:

This step basically creates the class ‘Shapes’ to represent the Tetriminoe shapes.

import java.util.Random;

public class Shape {
    private Tetrominoe pieceShape;     // Represents the type of Tetrominoe shape of the piece.
    private int[][] coordinates;       // Stores the x and y coordinates of each block in the piece.

    public Shape() {
        coordinates = new int[4][2];   // Initialize the coordinates array with 4 rows and 2 columns.
        setShape(Tetrominoe.NoShape);  // Set the initial shape to NoShape.
    }

    public void setShape(Tetrominoe shape) {
        int[][][] coordsTable = new int[][][] {
                {{0, 0}, {0, 0}, {0, 0}, {0, 0}},             // NoShape
                {{0, -1}, {0, 0}, {-1, 0}, {-1, 1}},         // ZShape
                {{0, -1}, {0, 0}, {1, 0}, {1, 1}},           // SShape
                {{0, -1}, {0, 0}, {0, 1}, {0, 2}},           // LineShape
                {{-1, 0}, {0, 0}, {1, 0}, {0, 1}},           // TShape
                {{0, 0}, {1, 0}, {0, 1}, {1, 1}},             // SquareShape
                {{-1, -1}, {0, -1}, {0, 0}, {0, 1}},         // LShape
                {{1, -1}, {0, -1}, {0, 0}, {0, 1}}           // MirroredLShape
        };

        // Copy the coordinates from the lookup table for the given shape.
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 2; ++j) {
                coordinates[i][j] = coordsTable[shape.ordinal()][i][j];
            }
        }
        pieceShape = shape;   // Set the piece shape.
    }

    private void setX(int index, int x) {
        coordinates[index][0] = x;   // Set the x-coordinate of the block at the given index.
    }

    private void setY(int index, int y) {
        coordinates[index][1] = y;   // Set the y-coordinate of the block at the given index.
    }

    public int x(int index) {
        return coordinates[index][0];   // Get the x-coordinate of the block at the given index.
    }

    public int y(int index) {
        return coordinates[index][1];   // Get the y-coordinate of the block at the given index.
    }

    public Tetrominoe getShape() {
        return pieceShape;   // Get the shape of the piece.
    }

    public void setRandomShape() {
        Random rand = new Random();
        int x = Math.abs(rand.nextInt()) % 7 + 1;
        Tetrominoe[] values = Tetrominoe.values();
        setShape(values[x]);   // Set the shape of the piece to a random shape.
    }

    public int minY() {
        int minY = coordinates[0][1];
        for (int i = 0; i < 4; i++) {
            minY = Math.min(minY, coordinates[i][1]);   // Find the minimum y-coordinate of the piece.
        }
        return minY;
    }

    public Shape rotateLeft() {
        if (pieceShape == Tetrominoe.SquareShape) {
            return this;   // Square shape does not change when rotated, so return the current instance.
        }
        Shape rotatedShape = new Shape();   // Create a new Shape instance for the rotated shape.
        rotatedShape.pieceShape = pieceShape;   // Set the shape of the rotated piece.

        // Perform rotation calculations for each block.
        for (int i = 0; i < 4; ++i) {
            rotatedShape.setX(i, y(i));
            rotatedShape.setY(i, -x(i));
        }

        return rotatedShape;   // Return the rotated shape.
    }

    public Shape rotateRight() {
        if (pieceShape == Tetrominoe.SquareShape) {
            return this;   // Square shape does not change when rotated, so return the current instance.
        }
        Shape rotatedShape = new Shape();   // Create a new Shape instance for the rotated shape.
        rotatedShape.pieceShape = pieceShape;   // Set the shape of the rotated piece.

        // Perform rotation calculations for each block.
        for (int i = 0; i < 4; ++i) {
            rotatedShape.setX(i, -y(i));
            rotatedShape.setY(i, x(i));
        }

        return rotatedShape;   // Return the rotated shape.
    }
}

Enum named ‘Tetriminoe’ has also been created to provide color to the shapes.

import java.awt.*;

public enum Tetrominoe {
    NoShape, ZShape, SShape, LineShape, TShape, SquareShape, LShape, MirroredLShape;

    // Define the color associated with each shape
    private final Color[] shapeColors = {
            Color.BLACK,           // NoShape (Empty)
            new Color(204, 102, 102),  // ZShape
            new Color(102, 204, 102),  // SShape
            new Color(102, 102, 204),  // LineShape
            new Color(204, 204, 102),  // TShape
            new Color(204, 102, 204),  // SquareShape
            new Color(102, 204, 204),  // LShape
            new Color(218, 170, 0)     // MirroredLShape
    };

    // Get the color associated with the shape
    public Color getColor() {
        return shapeColors[this.ordinal()];
    }
}

4. Handling User Input:

This step basically creates an event listener using the ‘TAdapter’ class. Handling of the key presses for movement of the shapes and pause functionality of the game is managed under this step.

// Handle keyboard input
private class TAdapter extends KeyAdapter {
    @Override
    public void keyPressed(KeyEvent e) {
        if (!isStarted || curPiece.getShape() == Tetrominoe.NoShape) {
            return;
        }
        int keycode = e.getKeyCode();
        if (keycode == 'p' || keycode == 'P') {
            pause();
        }
        if (isPaused) {
            return;
        }
        switch (keycode) {
            case KeyEvent.VK_LEFT:
                tryMove(curPiece, curX - 1, curY);
                break;
            case KeyEvent.VK_RIGHT:
                tryMove(curPiece, curX + 1, curY);
                break;
            case KeyEvent.VK_DOWN:
                tryMove(curPiece, curX, curY - 1); // Move the piece one line down
                break;
            case KeyEvent.VK_UP:
                tryMove(curPiece.rotateLeft(), curX, curY);
                break;
            case KeyEvent.VK_SPACE:
                dropShape();
                break;
        }
    }
}

// Game cycle timer
private class GameCycle implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        gameCycle();
    }
}

5. Game Logic and State Updation:

The game starts from this step. Here we have three basic methods to control the game state: ‘start’, ‘restart’ and ‘pause’.

// Start the game
private void start() {
    if (isPaused) {
        return;
    }
    isStarted = true;
    startButton.setVisible(false);
    isFallingFinished = false;
    numLinesRemoved = 0;
    clearBoard();
    newPiece();
    timer.start();
    startButton.setEnabled(false);
    gameOverLabel.setVisible(false); // Reset the game over label
    requestFocus();
}

// Restart the game
private void restart() {
    isStarted = false;
    isFallingFinished = false;
    isPaused = false;
    numLinesRemoved = 0;
    clearBoard();
    startButton.setEnabled(true);
    gameOverLabel.setVisible(false); // Reset the game over label
    repaint();
}

// Pause or resume the game
private void pause() {
    if (!isStarted) {
        return;
    }
    isPaused = !isPaused;
    if (isPaused) {
        timer.stop();
    } else {
        timer.start();
    }
    repaint();
}

This step also handles all the implementation and updation of the game like, implementing a timer cycle (‘gameCycle’), moving shapes (‘oneLineDown’, ‘dropShape’, ‘pieceDropped’, ‘tryMove’), line clearings ( ‘removeFullLines’ ), creating new shapes (‘newPiece’ ) and managing gameover condition (‘update’).

// Move the current piece one line down
private void oneLineDown() {
    if (!tryMove(curPiece, curX, curY - 1)) {
        pieceDropped();
    }
}

// Drop the current piece to the bottom
private void dropShape() {
    int newY = curY;
    while (newY > 0) {
        if (!tryMove(curPiece, curX, newY - 1)) {
            break;
        }
        newY--;
    }
    pieceDropped();
}

// Place the dropped piece on the board
private void pieceDropped() {
    for (int i = 0; i < 4; i++) {
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        board[x][y] = curPiece.getShape();
    }
    removeFullLines();
    if (!isFallingFinished) {
        newPiece();
    }
}

// Remove full lines from the board and update the score
private void removeFullLines() {
    int numFullLines = 0;
    for (int i = BOARD_HEIGHT - 1; i >= 0; i--) {
        boolean lineIsFull = true;
        for (int j = 0; j < BOARD_WIDTH; j++) {
            if (shapeAt(j, i) == Tetrominoe.NoShape) {
                lineIsFull = false;
                break;
            }
        }
        if (lineIsFull) {
            numFullLines++;
            for (int k = i; k < BOARD_HEIGHT - 1; k++) {
                for (int j = 0; j < BOARD_WIDTH; j++) {
                    board[j][k] = shapeAt(j, k + 1);
                }
            }
        }
    }
    if (numFullLines > 0) {
        numLinesRemoved += numFullLines;
        isFallingFinished = true;
        curPiece.setShape(Tetrominoe.NoShape);
        repaint();
    }
}

// Generate a new random piece
private void newPiece() {
    curPiece.setRandomShape();
    curX = BOARD_WIDTH / 2 - 1;
    curY = BOARD_HEIGHT - 1 + curPiece.minY();

    if (!tryMove(curPiece, curX, curY)) {
        curPiece.setShape(Tetrominoe.NoShape);
        timer.stop();
        isStarted = false;
        startButton.setEnabled(true);
        gameOverLabel.setVisible(true);
        startButton.setVisible(true);
    }
}

// Try to move the current piece to a new position
private boolean tryMove(Shape newPiece, int newX, int newY) {
    for (int i = 0; i < 4; i++) {
        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);
        if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT || shapeAt(x, y) != Tetrominoe.NoShape) {
            return false;
        }
    }
    curPiece = newPiece;
    curX = newX;
    curY = newY;
    repaint();
    return true;
}
// Update the game state
private void gameCycle() {
    update();
    repaint();
}


// Update the game state
private void update() {
    if (isFallingFinished) {
        isFallingFinished = false;
        newPiece();
    } else {
        oneLineDown();
    }
}

6. Drawing the Game Components:

This step involves the drawing and display of the game board and its components like grid lines, occupied squares, score and lines cleared display, etc. The doDrawing method manages colors and rendering, using the drawSquare method to draw each square with its associated color. It sets rendering hints for smooth graphics and creates a grid structure with vertical and horizontal lines. The method iterates over the game board, retrieves the shape at each position, and renders colored squares for occupied positions. The current falling piece is drawn using the curX and curY variables. The drawPause method displays a pause message when needed. Overall, the doDrawing method ensures visually appealing game components.

// Draw the game components on the panel
private void doDrawing(Graphics g) {
    Graphics2D g2d = (Graphics2D) g;
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    Dimension size = getSize();
    g2d.setColor(Color.DARK_GRAY);
    int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight();

    // Draw the vertical lines of the board
    for (int i = 0; i < BOARD_HEIGHT; i++) {
        g2d.drawLine(0, boardTop + i * squareHeight(), BOARD_WIDTH * squareWidth(), boardTop + i * squareHeight());
    }

    // Draw the horizontal lines of the board
    for (int j = 0; j < BOARD_WIDTH; j++) {
        g2d.drawLine(j * squareWidth(), boardTop, j * squareWidth(), boardTop + BOARD_HEIGHT * squareHeight());
    }

    // Draw the occupied squares on the board
    for (int i = 0; i < BOARD_HEIGHT; i++) {
        for (int j = 0; j < BOARD_WIDTH; j++) {
            Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);
            if (shape != Tetrominoe.NoShape) {
                drawSquare(g2d, j * squareWidth(),
                        boardTop + i * squareHeight(), shape);
            }
        }
    }

    // Draw the current falling piece
    if (curPiece.getShape() != Tetrominoe.NoShape) {
        for (int i = 0; i < 4; i++) {
            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            drawSquare(g2d, x * squareWidth(),
                    boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
                    curPiece.getShape());
        }
    }

    // Draw the pause message if the game is paused
    if (isPaused) {
        drawPause(g2d);
    }
}

// Draw a square with a specified color
private void drawSquare(Graphics2D g2d, int x, int y, Tetrominoe shape) {
    Color[] colors = {
            new Color(0, 0, 0), new Color(204, 102, 102),
            new Color(102, 204, 102), new Color(102, 102, 204),
            new Color(204, 204, 102), new Color(204, 102, 204),
            new Color(102, 204, 204), new Color(218, 170, 0)
    };
    Color color = colors[shape.ordinal()];
    g2d.setColor(color);
    g2d.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);
    g2d.setColor(color.brighter());
    g2d.drawLine(x, y + squareHeight() - 1, x, y);
    g2d.drawLine(x, y, x + squareWidth() - 1, y);
    g2d.setColor(color.darker());
    g2d.drawLine(x + 1, y + squareHeight() - 1,
            x + squareWidth() - 1, y + squareHeight() - 1);
    g2d.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
            x + squareWidth() - 1, y + 1);
}




// Clear the board by setting all positions to NoShape
private void clearBoard() {
    for (int i = 0; i < BOARD_WIDTH; i++) {
        for (int j = 0; j < BOARD_HEIGHT; j++) {
            board[i][j] = Tetrominoe.NoShape;
        }
    }
}

// Draw the pause message on the panel
private void drawPause(Graphics2D g2d) {
    String pauseMsg = "Paused";
    g2d.setColor(Color.YELLOW);
    g2d.setFont(new Font("SansSerif", Font.BOLD, 24));
    FontMetrics fm = getFontMetrics(g2d.getFont());
    int msgWidth = fm.stringWidth(pauseMsg);
    int msgHeight = fm.getHeight();
    int x = (getWidth() - msgWidth) / 2;
    int y = (getHeight() - msgHeight) / 2;
    g2d.drawString(pauseMsg, x, y);
}

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);

    // Draw the lines cleared and score information
    Font font = new Font("SansSerif", Font.BOLD, 14);
    FontMetrics fontMetrics = g.getFontMetrics(font);
    String linesClearedText = "Lines Cleared: " + numLinesRemoved;
    String scoreText = "Score: " + (numLinesRemoved * 100);
    int textWidth = fontMetrics.stringWidth(linesClearedText);
    g.setColor(Color.GREEN);
    g.setFont(font);
    g.drawString(linesClearedText, 5, 15);
    g.drawString(scoreText, 180, 15);

    // Draw the game components
    doDrawing(g);
}

7. The main() method:

The main method is the entry point of the program. It initiates the frame and its border layout by invoking through SwingUtilites.invokeLater().

// Entry point of the program
public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
        JFrame frame = new JFrame("TechVidvan's Tetris");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.add(new Tetris(), BorderLayout.CENTER);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    });
}

8. Test the Tetris game:

Run the respective code to test the game.

1. Basic GUI

2. Score Updation

3. Pause State

4. GameOver State

Conclusion:

In conclusion, using the AWT and Swing APIs, this Tetris project illustrated how to create a graphical user interface. To win the game, players must arrange falling blocks into complete lines. The project has also demonstrated important concepts in Java GUI programming and game development, including user input, object-oriented programming, and event handling. Anyone who is interested in learning more about these subjects should check out this initiative.

Exit mobile version