Creating Minesweeper in Flutter

Making a Minesweeper clone in Flutter (without a game engine)

Minesweeper is one of the classics when it comes to games. The origins of minesweeper came in the 1980s and is most popular for being a part of the Windows suite of games. Today we’ll recreate minesweeper in Flutter without using any game engine like Flame. First off we’ll cover what the game is and the things we need to do to recreate it.

If you already know how minesweeper works and are good in Flutter, find the Github link at the end of the article.

What is Minesweeper?

Minesweeper contains a board filled with, you guessed it, mines. The objective is to “unlock” or “open” (I don’t know the terminology, bear with me) the squares without bombs. Each square has eight squares around it. The number on the square is simply the number of bombs around that square.

Our objective is to open all squares except the mines. If you click on a mine, you lose. The flags are to mark bombs.

Also, if you click on a square with no bombs around it, it opens up all connected squares which don’t have a bomb. (“Connected” does not mean adjacent. It is connected as long as it is connected on one of four sides with any number of no-bomb squares in the middle)

Let’s go ahead and see the elements we need to recreate this game.

Getting Started

Our final aim in this tutorial is to create something like this:

Let’s see the things we need to do in the programming side:

  1. The grid of squares
  2. Generate bombs on squares
  3. Calculate number of bombs around each square
  4. Storing the opened squares and flagged squares
  5. Expanding connected no-bomb squares when a no-bomb square is hit
  6. Handling a win condition
  7. Displaying the board on the screen (Widgets!)

Let’s code

We’ll go in the order we discussed.

Making the grid of squares

First we make a list of squares. The square class is pretty simple to make:

class BoardSquare {

bool hasBomb;
int bombsAround;

BoardSquare({this.hasBomb = false, this.bombsAround = 0});

}

It has a simple boolean which stores if the square has a bomb and an integer which stores the number of bombs around the square. We initialise both to false and 0 respectively.

Now we set a simple row and column count.

int rowCount = 18;
int columnCount = 10;

And then the board itself. This is just a 2D list.

List<List<BoardSquare>> board;

Note that we need to initialise all the squares of the board. We do it as:

board = List.generate(rowCount, (i) {
return List.generate(columnCount, (j) {
return BoardSquare();
});
});

This assigns a square to each index in the 2D list and as of yet, there are no bombs on the board.

Generate bombs on squares

This is a pretty simple task, we just generate bombs randomly. In my case, I’m generating a number from 0 to 15 for each square, and if the number is lesser than 3, the square gets a bomb.

int bombProbability = 3;
int maxProbability = 15;
Random random = new Random();
// Generate randomly for each square
for
(int i = 0; i < rowCount; i++) {
for (int j = 0; j < columnCount; j++) {
int randomNumber = random.nextInt(maxProbability);
if (randomNumber < bombProbability) {
board[i][j].hasBomb = true;

//bombCount is a variable
//to store the number of bombs on the board
bombCount++;
}
}
}

Calculate number of bombs around each square

We simply visit each square and check the squares around it and increment the square’s bombsAround variable.

for (int i = 0; i < rowCount; i++) {
for (int j = 0; j < columnCount; j++) {
if (i > 0 && j > 0) {
if (board[i - 1][j - 1].hasBomb) {
board[i][j].bombsAround++;
}
}

if (i > 0) {
if (board[i - 1][j].hasBomb) {
board[i][j].bombsAround++;
}
}

if (i > 0 && j < columnCount - 1) {
if (board[i - 1][j + 1].hasBomb) {
board[i][j].bombsAround++;
}
}

if (j > 0) {
if (board[i][j - 1].hasBomb) {
board[i][j].bombsAround++;
}
}

if (j < columnCount - 1) {
if (board[i][j + 1].hasBomb) {
board[i][j].bombsAround++;
}
}

if (i < rowCount - 1 && j > 0) {
if (board[i + 1][j - 1].hasBomb) {
board[i][j].bombsAround++;
}
}

if (i < rowCount - 1) {
if (board[i + 1][j].hasBomb) {
board[i][j].bombsAround++;
}
}

if (i < rowCount - 1 && j < columnCount - 1) {
if (board[i + 1][j + 1].hasBomb) {
board[i][j].bombsAround++;
}
}
}
}

Storing the opened squares and flagged squares

We simply use two lists to see which squares the user has either opened or flagged. Since we don’t really need the exact row and column of these, we’ll just store the position of the square on the grid.

// "Opened" refers to being clicked already
List<bool> openedSquares;

// A flagged square is a square a user has added a flag on by long pressing
List<bool> flaggedSquares;

And then initialise them

// Initialise list to store which squares have been opened
openedSquares = List.generate(rowCount * columnCount, (i) {
return false;
});

flaggedSquares = List.generate(rowCount * columnCount, (i) {
return false;
});

Expanding connected no-bomb squares when a no-bomb square is hit

As we discussed, when a no-bomb square is hit, it expands the no-bomb squares around it. If the squares around them are no-bomb as well, they also expand the squares around them. And we all know what that means for the implementation.

Recursion

When a square is hit, we’ll expand on four sides of the square and not diagonally.

Let’s write a recursive function to handle a tap on a square.

void _handleTap(int i, int j) {

int position = (i * columnCount) + j;
openedSquares[position] = true;
squaresLeft = squaresLeft - 1;

if (i > 0) {
if (!board[i - 1][j].hasBomb &&
openedSquares[((i - 1) * columnCount) + j] != true) {
if (board[i][j].bombsAround == 0) {
_handleTap(i - 1, j);
}
}
}

if (j > 0) {
if (!board[i][j - 1].hasBomb &&
openedSquares[(i * columnCount) + j - 1] != true) {
if (board[i][j].bombsAround == 0) {
_handleTap(i, j - 1);
}
}
}

if (j < columnCount - 1) {
if (!board[i][j + 1].hasBomb &&
openedSquares[(i * columnCount) + j + 1] != true) {
if (board[i][j].bombsAround == 0) {
_handleTap(i, j + 1);
}
}
}

if (i < rowCount - 1) {
if (!board[i + 1][j].hasBomb &&
openedSquares[((i + 1) * columnCount) + j] != true) {
if (board[i][j].bombsAround == 0) {
_handleTap(i + 1, j);
}
}
}

setState(() {});
}

We simply check all four sides and then if the sides don’t have a bomb and aren’t opened, we go to that square and open it. If the square itself has a non-zero bombsAround value, we don’t continue.

Note that if the first square tapped on has a non-zero bombsAround value, we simply don’t call this function since we don’t need to expand anything.

Handling a win condition

I’ve simple added a variable to track the number of squares left. If the squares left are equal to the bombs on the board (which we also track), then the user wins.

if(squaresLeft <= bombCount) {
_handleWin();
}

Displaying the board on the screen (Making the actual UI)

The UI is rather easy to do. For the grid, we use a GridView.builder :

GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columnCount,
),
itemBuilder: (context, position) {
// Item code here
},
itemCount: rowCount * columnCount,
),

Now, we simply check a few conditions to see what to display here:

  1. If not opened but flagged, display flag
  2. If opened display bombs around or empty
  3. If opened and has bomb, display bomb and end game
itemBuilder: (context, position) {
// Get row and column number of square
int rowNumber = (position / columnCount).floor();
int columnNumber = (position % columnCount);

Image image;

if (openedSquares[position] == false) {
if (flaggedSquares[position] == true) {
image = getImage(ImageType.flagged);
} else {
image = getImage(ImageType.facingDown);
}
} else {
if (board[rowNumber][columnNumber].hasBomb) {
image = getImage(ImageType.bomb);
} else {
image = getImage(
getImageTypeFromNumber(
board[rowNumber][columnNumber].bombsAround),
);
}
}
  return Container(
color: Colors.grey,
child: image,
);
},

Now, we add an InkWell to handle taps to open and long press to flag a square.

The final GridView becomes:

On tap, we check if the square is a bomb. If it is, we declare a loss. Then, if there are bombs around the tapped square, we simply open up the square and nothing else. If there are not, we call our recursive function.

On long press, we simply add a flag to the square.

And we’re done!

The final result of the game is:

For viewing the complete code go to:

That’s it for this article! I hope you enjoyed it, and leave a few claps if you did. Follow me for more Flutter articles and comment for any feedback you might have about this article.
Note: I’ve been working on a Deep Dive series for Flutter widgets for a while now, and if you haven’t seen it before, please give it a read. Here are a few articles from the series.

Feel free to check out my other profiles as well: