Tetris game in Java [with source code]

Table of Content

  • What is Tetris?
  • MVC Approach
  • Tetris.java
  • Form.java
  • Controller.java
  • Images
  • Potential

What is Tetris?

Tetris is a classic video game that has been enjoyed by millions of people around the world since its inception in 1984. The game involves manipulating falling blocks to create complete lines, which then disappear, allowing the player to score points. The game's simple yet addictive gameplay has made it a favorite among gamers of all ages.

The player's objective is to manipulate the falling shapes(tetraminoes) in the playfield to create complete horizontal lines and player earns points and that row clears up.If the playfield gets filled up to top , the game ends.

Rules

  • Players can only move the shape left and right.
  • Players can rotate the shape if required.
  • Players can also speed up the downwards movement.
  • Typically there are 7 types of shapes, and players need to manage to fill up the rows.

image--2-

But what makes Tetris so interesting is its universal appeal. It's a game that can be enjoyed by people of all ages and skill levels, from casual gamers to hardcore enthusiasts. Its simple gameplay mechanics make it easy to pick up and play, but its increasing difficulty and strategic depth keep players coming back for more.

Tetris has also had a significant impact on popular culture. Its catchy music has been remixed and covered countless times, and its blocky graphics have inspired everything from fashion to architecture. It's a game that has transcended its humble origins to become a cultural phenomenon.

So today we will try to implement a basic tetris game in Java.
We will try to keep the approach minimal and understandable and we will see different logics that will be used in the game. We will be using JavaFX for basic GUI and it will also involve OOPS concepts.

MVC Approach

MVC (Model-View-Controller) is a software design pattern that separates an application into three interconnected components: the model, the view, and the controller. This approach is widely used in software development to create applications that are easy to maintain, test, and modify.

The model represents the data and business logic of the application. It is responsible for managing the data and providing methods to access and manipulate it. The view is responsible for displaying the data to the user. It is a user interface component that presents the data in a way that is easy to understand and interact with. The controller acts as an intermediary between the model and the view. It receives input from the user and updates the model accordingly. It also updates the view to reflect any changes made to the model.

The MVC approach has several benefits. It promotes separation of concerns, making it easier to maintain and modify the application. It also makes it easier to test the application since each component can be tested independently. Additionally, it allows for greater flexibility in the user interface since the view can be changed without affecting the underlying data or business logic.

Frame-1

So we will have mainly 3 classes for a basic tetris.

Controller.java

Form.java

Tetris.java

Before getting started, we need to see Tetris not from the perspective of a gamer but from the perspective of a programmer or developer.

Requirements

  1. Java Development Kit (JDK): Ensure that you have the latest version of JDK installed on your system to compile and run Java programs.

  2. JavaFX: Make sure you have the JavaFX library included in your project. You can use either the standalone JavaFX SDK or the JavaFX modules bundled with JDK 11 and later versions.

  3. IDE (Integrated Development Environment): Use an IDE of your choice, such as Eclipse, IntelliJ IDEA, or NetBeans, to write, debug, and run your Java code. Set up the IDE to work with JavaFX, including configuring the build path and module settings.

  4. User Interface (UI): Confirm that your JavaFX UI is properly designed and implemented. This includes creating the necessary UI components, such as the playfield grid, tetromino shapes, score display, and game over screen. Ensure that the UI is responsive and provides a smooth user experience.

  5. Event Handling: Implement event handlers to capture user input, such as keyboard events for controlling the tetromino movement and rotation. Make sure the event handling is robust and handles different scenarios, such as simultaneous key presses or rapid key presses.

Strategy

A tetris can be seen as a large matrix or a 2 dimensional array and that's where we will start. Every shape in Tetris is made up of 4 blocks which we will make and manipulate their movements in order to create different shapes and rotating them.
We set their X & Y coordinates according to the keystrokes events received.
We also make checks to make sure the blocks stay within our bounds only.

Untitled-Diagram.drawio--1-

Tetris.java

This will be our main class which will work as view in our tetris.
We will initialize few basic things such as dimensions of the window, the size of a single move of the shape and the size of a block and other things like Pane, Scene required for the GUI.

We will start by initializing the entire array by zero.
On one side, the game will be running and on the other side, score and statistics can be there.

for(int[] a: MESH){         //initializing entire array with 0
            Arrays.fill(a,0);
        }

The first part of the code creates a Line object that represents the divider between the game area and the score area. The Line object is created with two points, one at the maximum x-coordinate of the game area and the other at the maximum y-coordinate of the game area.

The second part of the code creates a Text object that displays the text "Score : " on the game screen. The setY() method is used to set the y-coordinate of the text, and the setX() method is used to set the x-coordinate of the text to the right of the game area.

The third part of the code creates another Text object that displays the text "Lines : " on the game screen. The setX() and setY() methods are used to set the coordinates of the text.

The last part of the code adds the score text, line counter, and divider to a Group object called "group". The addAll() method is used to add all the objects to the group at once.
We will also set the scene, and the stage's title and will invoke its show() method to display the GUI.

We creates a variable "a" and assigns it the value of the next shape to be used in the game from the Form class which we will define later.

As we invoke the constructor of the Form class, it initializes 4 rectangles as every shape in tetris is made up of 4 blocks so we can rearrange them for different shapes.Same can be done for their rotations.

1463399260039-upload-e95fc777-c31e-4891-8351-7c3079144151-3309478779

Then we set up event listeners so that when a key is pressed, the shape "a" will move in response to that key. So it sets the current object on the game board to be shape "a". Then a new shape is generated and process is repeated with every iteration.

Now we need a timer according to which shapes will fall.We create a new Timer object called "fall" and a new TimerTask object called "task". The TimerTask object has a run() method that is overridden to execute certain actions. These actions include checking if certain objects have a height of 0, incrementing a variable called "top" if they do, displaying a "Game over" message if "top" reaches 2, exiting the program if "top" reaches 15, moving an object down, updating the score and lines displayed on the screen. The task is scheduled to run every 300 milliseconds using the Timer object.

Timer

Timer fall = new Timer();
    TimerTask task = new TimerTask() {
        @Override
        public void run() {
            Platform.runLater(() -> {
                if(object.a.getHeight() == 0  object.b.getHeight() == 0  object.c.getHeight() == 0 && object.d.getHeight()==0){
                    top++;
                }
                else
                    top = 0;
                if(top == 2){
                    //game over
                    Text over = new Text("Game over");
                    over.setFill(Color.RED);
                    over.setStyle("-fx-font: 70 arial;");
                    over.setY(250);
                    over.setX(10);
                    group.getChildren().add(over);
                    game = false;
                }
                if(top == 15 ){
                    System.exit(0);
                }

                if(game){
                    MoveDown(object);
                    scoretext.setText("Score : "+ score);
                    level.setText("Lines : "+ linesNo);
                }
            });
        }
    };
    fall.schedule(task,0,300);
    }

Then we create a switch case that takes the event code of the keystroke that will be pressed.

Shape Rotations

Now to rotate shapes we will have another switch case, every shape can have 4 rotations so each shape case will contain 4 if conditions for rotations except the shape 'O', as its same with every rotation.

 switch (form.getName()) {
            case "j" -> {
                if (f == 1 && cB(a, 1, -1) && cB(c, -1, -1) && cB(d, -2, -2)) {
                    MoveRight(form.a);
                    MoveDown(form.a);
                    MoveDown(form.c);
                    MoveLeft(form.c);
                    MoveDown(form.d);
                    MoveDown(form.d);
                    MoveLeft(form.d);
                    MoveLeft(form.d);
                    form.changeShape();
                    break;
                }

We will do same for 3 more values of f and this will be repeated for every shape in different cases. Move functions are defined that are defined to set the X & Y coordinates of the blocks a,b,c & d.

Now as the arrays starts to fill up, we also need to clear up the rows if they are completely filled. We check if the rows are completely filled, put them in temporary data structures and clear them again and move the the upper rows to lower ones.

private void RemoveRows(){
        ArrayList<Node> rects = new ArrayList<>();
        ArrayList<Integer> lines = new ArrayList<>();
        ArrayList<Node> newrects = new ArrayList<>();
        int full = 0;
        for(int i = 0; i< MESH[0].length ; i++){
            for (int[] mesh : MESH) {
                if (mesh[i] == 1)
                    full++;
            }
            if(full == MESH.length )
                lines.add(i);
            full = 0;
        }
        if(lines.size() > 0)
                do{
                    for(Node node: Tetris.group.getChildren()){
                        if(node instanceof Rectangle)
                            rects.add(node);
                    }
                    score+=10;
                    linesNo++;
                    for(Node node: rects){
                        Rectangle a = (Rectangle) node;
                        if(a.getY() == lines.get(0)* SIZE){
                            MESH[(int)a.getX()/ SIZE][(int)a.getY()/ SIZE] = 0;
                            Tetris.group.getChildren().remove(node);
                        }
                        else
                            newrects.add(node);
                    }
                    for(Node node:newrects) {
                        Rectangle a = (Rectangle) node;
                        if (a.getY() < lines.get(0) * SIZE) {
                            MESH[(int) a.getX() / SIZE][(int) a.getY() / SIZE] = 0;
                            a.setY(a.getY() + SIZE);
                        }
                    }
                        lines.remove(0);
                        rects.clear();
                        newrects.clear();
                        for(Node node: Tetris.group.getChildren()){
                            if(node instanceof Rectangle)
                                rects.add(node);
                        }
                        for(Node node: rects){
                            Rectangle a = (Rectangle)node;
                                MESH[(int)a.getX()/ SIZE][(int)a.getY()/ SIZE]=1;
                    }rects.clear();
                }while(lines.size() > 0);
    }

Moving Down logic

Now we create a method that moves a shape down in a Tetris game. The method takes in a Form object (which represents the shape) as a parameter.

The first if statement checks if the shape has reached the bottom of the game board (YMAX - SIZE) or if any of its blocks are overlapping with existing blocks on the board. If either of these conditions are true, the method calls moveA(), moveB(), moveC(), and/or moveD() methods to update the positions of each block in the Form object. It also updates the MESH array (which represents the game board) to indicate that these blocks are now part of the board. Then it removes any completed rows from the game board using RemoveRows() method.

If the shape has not reached the bottom of the game board, then it checks if moving down by MOVE pixels will cause any collisions with existing blocks on the board. It does this by checking values in MESH array at positions where each block in Form object would be after moving down by MOVE pixels. If all four blocks can move down without colliding with existing blocks, then their Y-coordinates are updated by adding MOVE pixels.

Finally, if there is no collision and all four blocks have been moved down, it adds a new Form object to be controlled next and sets it as "object". It also adds this new Form object's individual block nodes to a JavaFX Group node called "group" so they can be displayed on screen. Finally, it calls moveOnKeyPress() method to allow user input for controlling this new Form object.

Form.java

In this class we initialize 4 rectangles and set their color according to the name given as a parameter, we also define a method to change shape.This class is used as a model.

public class Form {

     Rectangle a ;
     Rectangle b ;
     Rectangle c ;
     Rectangle d ;
     switch (name) {
            case "j" -> color = Color.BLACK;
            case "l" -> color = Color.BLUE;
            case "o" -> color = Color.GREEN;
            case "s" -> color = Color.RED;
            case "t" -> color = Color.YELLOW;
            case "z" -> color = Color.PINK;
            case "i" -> color = Color.BROWN;    //broken
        }
        public void changeShape() {
        if (form != 4)
            form++;
        else
            form = 1;
    }
   }

Controller.java

Here we define functions which moves the entire shape left and right when keys are pressed.
Before moving the shapes, we also check if we are within the limited bounds of the array or not. Then we set the new X & Y coordinates for each block of the shape.

 public static void moveLeft(Form form) {
        boolean checkA = form.a.getX() - MOVE >= 0;
        if (checkA) {
     int movea = MESH[((int) form.a.getX() / SIZE) - 1][(int) form.a.getY() / SIZE];   
            if (movea == 0 ) {
                form.a.setX(form.a.getX() - MOVE);
            }
        }
    }

Similar thing can be done for other blocks of the shape and another method can be defined to move the shape in right direction.

Now finally we only need to define a method to generate shapes.
To randomize which shape will be generated, we use Math.random() function to generate a number between 0-100 and use that to decide the shape accordingly in if statements.Inside if statements, the X & Y coordinates are set for each block of the shape.

int block = (int) (Math.random() * 100);
if (block < 15) {
            a.setX((double) XMAX /2-SIZE);
            b.setX((double) XMAX /2-SIZE);
            b.setY(SIZE);
            c.setX((double) XMAX /2);
            c.setY(SIZE);
            d.setX((double) XMAX /2+SIZE);
            d.setY(SIZE);
            name = "j";
        } else if (....

Similar thing should be done for all shapes for each block and this method will return an instance of Form class with the rectangles of which we defined coordinates. And now our basic Tetris is ready!

Tetris Gameplay

Potential

While this may not be the most advanced bugless tetris game, but it shows the basic implementation of it in most understandable way nonetheless with few bugs here and there. But users can make their own custom shapes similar to the implementation defined here. High scores can also be stored for comparisons.

Complete Code