How I Built A Tetris Game Using Core Flutter

NO EXTERNAL LIBRARIES USED!

Β·

9 min read

How I Built A Tetris Game Using Core Flutter

I just came up with the idea of building this game, I did some digging, spent some time on it and it was totally worth it.
I managed to build my own game through Flutter, and I am so happy about it, but how did I do this?
Let me take you through the process of what steps did I take to complete this application.
So, only an idea is not enough and the first thought in my mind was how the game will look and then I started my work to build the User interface of the application.
A Tetris game requires multiple blocks, both horizontal and vertical so the perfect widget that came into my mind was the GridView Builder.
I made two variables one for the row and one for the column. like this:


int rowLength = 10;
int colLength = 15;

and I initialize the item count of the gridView to (row length X column length) and in the item builder I returned a simple container like this:

Container(
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(5),
      ),
      margin: const EdgeInsets.all(1),
    );

By doing this I got a complete grid of 150 containers and my UI was set. Now for the game part, I needed an actual board that I can set my state with each time so I'd have to create a two-dimensional list for the row and column like this:

List<List<Tetromino?>> gameBoard =
    List.generate(colLength, (i) => List.generate(rowLength, (j) => null));

For avoiding confusion Tetermino mentioned above is just an enum that I created for initializing shapes.

enum Tetromino { L, J, I, O, S, Z, T }

Now I want to make the pieces fall into my grid. for that, I created a piece class and made a list named position, Like this:

  List<int> position = [];

Now I wanted to have multiple pieces with different kind of shapes fall from the top of the screen and passes through our grid so I made an Initializepiece function and initialized multiple pieces with the index position with the -ve sign because I wanted to have pieces to fall from the top of the screen and not from the first index, So the initialized function looked like this:

void initializePiece() {
    switch (type) {
      case Tetromino.L:
        position = [-26, -16, -6, -5];
        break;

      case Tetromino.I:
        position = [-4, -5, -6, -7];
        break;

      case Tetromino.J:
        position = [-25, -15, -5, -6];
        break;

      case Tetromino.O:
        position = [-15, -16, -5, -6];
        break;

      case Tetromino.S:
        position = [-15, -14, -6, -5];
        break;
      case Tetromino.T:
        position = [-26, -16, -6, -15];
        break;

      case Tetromino.Z:
        position = [-17, -16, -6, -5];
        break;
      default:
    }
  }

I made a total of 7 shapes and initializes each of them with different positions, now I wanted them to fall from the top of my screen and inside my grid and every shape must have a different color so I'd have to do some changes inside the container that I was returning in the GridView builder, and also I have to make some logic to implement unique colors with unique shapes and here is what I did:

For the Colors:

const Map<Tetromino, Color> tetrominoColors = {
  Tetromino.L: Color(
      0xFFFFA500), // Orange Tetromino.JColor.fromARGB(255, 0, 102, 255), // Blue
  Tetromino.I: Color.fromARGB(255, 242, 0, 255), // Pink
  Tetromino.O: Color(0xFFFFFF00), // Yellow
  Tetromino.S: Color(0xFF008000), // Green
  Tetromino.Z: Color(0xFFFF0000), // Red
  Tetromino.T: Color.fromARGB(255, 144, 0, 255), // Purple
};

Here I initialized each shape with a different kind of color with the help of using Maps and now to implement different colors in different shapes into my grid, I did this:

itemBuilder: (context, index) {
                  int col = index % rowLength;
                  int row = (index / rowLength).floor();

                  if (currentpiece.position.contains(index)) {
                    return Pixel(
                      color: currentpiece.color,
                    );
                  } else if (gameBoard[row][col] != null) {
                    final Tetromino? tetrominoType = gameBoard[row][col];
                    return Pixel(color: tetrominoColors[tetrominoType]);
                  } else {
                    return Pixel(
                      color: Colors.grey[900],
                    );
                  }
                }

Don't get confused by what is a Pixel here, it's just a class that contains the code of our simple container.
You know that we create separate classes for clean code ;)
But just for avoiding confusion here is the class that I implemented:


class Pixel extends StatelessWidget {
  var color;
  Pixel({super.key, required this.color});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(5),
      ),
      margin: const EdgeInsets.all(1),
    );
  }
}

Now, the base of the application is set and now we move towards the implementation part like moving the pieces down each time, for that I created a moving function and implemented this logic:

void movingPiece(Directions direction) {
    switch (direction) {
      case Directions.Down:
        for (int i = 0; i < position.length; i++) {
          position[i] += rowLength;
        }

Here Direction is just an enum that I created for Initializing the possible directions a piece will require which are obviously right, left, and down. Here is how I did it:

enum Directions {
  Left,
  Right,
  Down,
}

Now, Just for review, what have we done so far?

First of all, we created our grid. then we created the gameboard to help us get the exact position of each piece. After that we want our piece to fall Into our grid so, for that, we created the logic to implement our piece into our container and generated different colors for different pieces, we then implemented the moving functionality of our pieces with the moving function.
Now of course we want our piece to land right? because without landing our piece will go inside our grid and it will vanish :))
For that, I created this landing function:


  bool checkCollision(Directions directions) {
    for (int i = 0; i < currentpiece.position.length; i++) {
      int col = currentpiece.position[i] % rowLength;
      int row = (currentpiece.position[i] / rowLength).floor();

      if (directions == Directions.Left) {
        col--;
      } else if (directions == Directions.Right) {
        col++;
      } else if (directions == Directions.Down) {
        row++;
      }
      if (col < 0 || col >= rowLength || row >= colLength) {
        return true;
      }
      if (row >= 0 && gameBoard[row][col] != null) {
        return true;
      }
    }
    return false;
  }

  void checkLanding() {
    if (checkCollision(Directions.Down)) {
      for (int i = 0; i < currentpiece.position.length; i++) {
        int row = (currentpiece.position[i] / rowLength).floor();
        int col = currentpiece.position[i] % rowLength;
        if (row >= 0 && col >= 0) {
          gameBoard[row][col] = currentpiece.type;
        }
      }

      createNewPiece();
    }
  }

  void createNewPiece() {
    Random rand = Random();

    Tetromino randomType =
        Tetromino.values[rand.nextInt(Tetromino.values.length)];

    currentpiece = Piece(type: randomType);
    currentpiece.initializePiece();

    if (isGameOver()) {
      gameOver = true;
    }
  }

In our create a new piece function each piece will generate randomly.

Then I created our start game function which initializes the piece from our piece class and will generate the piece and I initialized the start game function into our init state of the app.

void startGame() {
    currentpiece.initializePiece();

    Duration frameRate = const Duration(milliseconds: 500);
    gameLoop(frameRate);
  }

This is the function that I made to generate pieces each time at a particular frame rate:

void gameLoop(Duration frameRate) {
    Timer.periodic(frameRate, (timer) {
      setState(() {
        clearLines();
        checkLanding();

        if (gameOver == true) {
          timer.cancel();
          showGameOverDialog();
        }
        currentpiece.movingPiece(Directions.Down);
      });
    });
  }

Now we have implemented the creation of the piece, the moving of the piece, the landing of the piece, and also a logic to avoid a collision. Everything is set now and we are left with just a couple of things which are:
Moving the piece to the left, and right, and rotating it. and implementing the logic when the game is over and when a user scores a point.
That's it, then we will have our Tetris game ready.

Now let's make a logic for making our piece move left and right:

void moveLeft() {
    if (!checkCollision(Directions.Left)) {
      setState(() {
        currentpiece.movingPiece(Directions.Left);
      });
    }
  }

  void moveRight() {
    if (!checkCollision(Directions.Right)) {
      setState(() {
        currentpiece.movingPiece(Directions.Right);
      });
    }
  }

Isn't this easy? we just made the direction of our moving piece function to left and right ;)
Now we want to rotate our piece by clicking on the rotate button, so for that, I created a function and here it is:


  void rotatePiece() {
    setState(() {
      currentpiece.rotatePiece();
    });
  }

The currentpiece which is the instance of our piece class is calling the method which is rotatePiece and in that method, I have actually set the indexes and the positions a piece will make after we tap on our rotate button and we have a total of 7 peices and for each one of them we have 4 different positions because of rotations so, for keeping this short I'll recommend you to go to the GitHub repository and understand the code from there, also do make sure to give a star if you like it ;)

here is the link: https://github.com/as3hr/Tetris-Game

Now we are only left with the game over part and the increase in the score of the user, here is what I did to increment the score of the user.
First of all, I initialized a current score variable to 0

int currentScore = 0;

then I made the below logic to implement the score functionality in our game:

void clearLines() {
    for (int row = colLength - 1; row >= 0; row--) {
      bool rowIsFull = true;
      for (int col = 0; col < rowLength; col++) {
        if (gameBoard[row][col] == null) {
          rowIsFull = false;
          break;
        }
      }
      if (rowIsFull) {
        for (int r = row; r > 0; r--) {
          gameBoard[r] = List.from(gameBoard[r - 1]);
        }

        gameBoard[0] = List.generate(row, (index) => null);

        currentScore++;
      }
    }
  }

So what's happening here?
The whole crux of this function is that with the help of loops, we are getting to the end of our grid and we are checking if our row is full or not. If our row is full then our list will be decremented by 1 and the score will be incremented by one.

For the game over function, I wanted to end my game when the pieces in my grid reach the top of my screen and touches the maximum height and for that I created this below logic:

bool isGameOver() {
    for (int col = 0; col < rowLength; col++) {
      if (gameBoard[0][col] != null) {
        return true;
      }
    }
    return false;
  }

this will tell me that if there is any kind of piece in our first row then the game will end.

For the reset function of the game, we just have to set everything back to 0, here is what I did:

void resetGame() {
    gameBoard =
        List.generate(colLength, (i) => List.generate(rowLength, (j) => null));
    gameOver = false;
    currentScore = 0;
    createNewPiece();
    startGame();
  }

Congratulations you made it!!πŸ’™πŸ’™
We have now made a Tetris game with pure flutter and dart with no external libraries being used.
Thanks a lot for reading this farπŸ™Œβ€

Check out the Github repo for this game: https://github.com/as3hr/Tetris-Game
Follow me on Github: https://github.com/as3hr

Β