Creating a Game Like Minesweeper in Flutter
Explore Flutter’s capability to create game UI and logic by learning to create a game like classic Minesweeper. By Samarth Agarwal.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Creating a Game Like Minesweeper in Flutter
30 mins
- Getting Started
- Introducing the Minesweeper Flutter Game
- Playing Minesweeper
- Understanding the Moore Neighborhood
- Understanding the CellModel Class and Properties
- Laying Out Widgets
- Building the Grid
- Using Rows and Columns
- Adding Game Logic
- Generating Mines
- Generating Cell Values
- Adding User Interactions
- Adding onTap to Uncover Cells
- Adding longPress to Flag Cells
- Checking if the Game Is Over
- Checking if the Player Won
- Finishing Up
- Handling Edge Cases
- Adding ProgressBar
- Adding Rules
- Where to Go From Here?
Adding User Interactions
In this section, you'll write code that allows the player to tap a cell to uncover it and long-press a cell to flag or unflag it. When the player taps a cell, it's uncovered, but all the non-mine cells in the cell's Moore neighborhood are also uncovered recursively. Finally, you'll also check if the player has uncovered all the non-mine cells or has uncovered a mine, resulting in Game Over.
Adding onTap to Uncover Cells
You already have GestureDetector
in place, which you return from buildButton
. Earlier, you left onTap
blank, but now it's time to wire it up to a method.
Add the following to GestureDetector
:
...
onTap: () {
onTap(cell);
},
...
In the code above, you simply pass cell
to onTap
, which uncovers the tapped cell. Next, you need to implement onTap
:
void onTap(CellModel cell) async {
// 1
if (cell.isMine) {
// 2
unrevealRecursively(cell);
setState(() {});
// TODO: Show game over dialog
return;
} else {
// 3
unrevealRecursively(cell);
setState(() {});
// TODO: Check if player won
}
}
In the code above, here's what's happening:
- Check if the tapped cell is a mine.
- If it is a mine, uncover cells recursively using
unrevealRecursively
and return. Later, you'll display a Game Over dialog here that also allows the player to restart. - If the tapped cell isn't a mine, uncover cells recursively using
unrevealRecursively
. Later, you'll also check to see if the player has discovered all the non-mine cells and, if they have, display a Congratulations dialog.
The most important piece of code is still missing: unrevealRecursively
. Time to implement that:
void unrevealRecursively(CellModel cell) {
if (cell.x > size || cell.y > size || cell.x < 0 || cell.y < 0 || cell.isRevealed) {
return;
}
cell.isRevealed = true;
totalCellsRevealed++;
if (cell.value == 0) {
int xStart = (cell.x - 1) < 0 ? 0 : (cell.x - 1);
int xEnd = (cell.x + 1) > (size - 1) ? (size - 1) : (cell.x + 1);
int yStart = (cell.y - 1) < 0 ? 0 : (cell.y - 1);
int yEnd = (cell.y + 1) > (size - 1) ? (size - 1) : (cell.y + 1);
for (int i = xStart; i <= xEnd; ++i) {
for (int j = yStart; j <= yEnd; ++j) {
if (!cells[i][j].isMine && !cells[i][j].isRevealed
&& cells[i][j].value == 0) {
unrevealRecursively(cells[i][j]);
}
}
}
} else {
return;
}
}
The method above is a recursive one. It uncovers not only the current cell but also all the cells in its Moore neighborhood. It continues until it uncovers all non-mine cells in overlapping Moore neighborhoods, meaning all consecutive non-mine cells are uncovered at once. That's how it works in the classic Minesweeper game.
Save everything and perform a hot-restart of the app. Try tapping some cells in the game. You'll uncover a cell as you tap it. Also, notice how cells are uncovered recursively:
Adding longPress to Flag Cells
Now that you can uncover cells by tapping them, you just need to add one more gesture: long-press. In the same GestureDetector
, you also have an onLongPress
event that's currently wired to a blank function. It's now time to wire it up to an actual function that flags/unflags a cell:
...
onLongPress: () {
markFlagged(cell);
},
...
Next, you need to implement markFlagged
. Here's how it should look:
void markFlagged(CellModel cell) {
cell.isFlagged = !cell.isFlagged;
setState(() {});
}
The code above is very straightforward. It just toggles the Boolean isFlagged
for the cell.
Save everything, hot-restart the app, and try long-pressing a cell — now you can flag it. If you repeat the action, the cell is unflagged:
Checking if the Game Is Over
Next, you need to check if the game is over. This is as simple as detecting if the cell the player tapped is a mine. Add this code inside onTap
:
void onTap(CellModel cell) async {
if(cell.isMine) {
unrevealRecursively(cell);
setState(() {});
// Add this
bool response = await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text("Game Over"),
content: Text("You stepped on a mine. Be careful next time."),
actions: [
MaterialButton(
color: Colors.deepPurple,
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text("Restart"),
),
],
),
);
if (response) {
restart();
}
return;
}
...
The code above displays the Game Over dialog to the player with a button that says Restart. When the player taps Restart, restart
is invoked and the game starts over. The next step is to implement restart
:
void restart() {
setState(() {
generateGrid();
});
}
It's so simple! restart
invokes generateGrid
, which regenerates the grid and therefore, restarts the game. setState
rebuilds the UI.
Save everything. Restart the app and play:
As soon as you tap a mine, you'll see the Game Over dialog. Tap Restart, and the game starts over.
Checking if the Player Won
Checking if the player has won is also very simple. You know the total number of cells, mines and uncovered cells. With this information, you can easily determine whether the user has uncovered all the non-mine cells.
Add the following code to onTap
:
void onTap(CellModel cell) async {
if(cell.isMine) {
...
} else {
// Add this
unrevealRecursively(cell);
setState(() {});
if (checkIfPlayerWon()) {
bool response = await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text("Congratulations"),
content: Text("You discovered all the tiles without stepping on any mines. Well done."),
actions: [
MaterialButton(
color: Colors.deepPurple,
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text("Next Level"),
),
],
),
);
if (response) {
size++;
restart();
}
} else {
setState(() {});
}
}
}
In the code snippet above, you use checkIfPlayerWon
to determine whether the player has won. If they have, then checkIfPlayerWon
returns true
. Else, it returns false
. If checkIfPlayerWon
returns true
, you display a Congratulations dialog with a Next Level button. Tapping this button increases the size of the grid by one and restarts the game.
Next, you need to implement checkIfPlayerWon
:
void checkIfPlayerWon() {
if (totalCellsRevealed + totalMines == size * size) {
return true;
} else {
return false;
}
}
In the code snippet above, you're calculating the sum of uncovered cells plus the number of mine cells and seeing if it equals the total number of cells.
Save everything and restart the app. At this point, you can perform a full run-through of the game:
After you finish the game successfully, you can move on to the next level, which has a bigger grid. Awesome, right? :]