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?
Developing games with Flutter isn’t explored very much. In this tutorial, you’ll build a game like the classic Minesweeper in Flutter. You’ll learn to create the game’s UI and write clean, modular code to implement algorithms that bring the game logic to life.
Here’s a preview of the app you’ll build:
To build this game, you’ll use several Flutter widgets to create the UI and write algorithms to implement the game logic.
While building this app, you’ll learn about:
- Minesweeper’s logic and game theory.
- Implementing the game user interface using Flutter widgets.
- Building a dynamic and random game with a specific difficulty every time.
- Using the
GestureDetector
widget to detect taps and long-presses. - Optimizing the game to avoid edge cases.
Getting Started
Download the starter project by clicking Download Materials at the top or bottom of the tutorial.
Unzip the downloaded file and open it with Android Studio 4.1 or later. You can use Visual Studio Code instead, but if you do, you’ll need to tweak some instructions to follow along.
Click Open an existing Android Studio project and choose the starter folder from your unzipped download.
Run the flutter create command in the starter folder to generate the android and ios folders. Next, download your dependencies by double-clicking pubspec.yaml on the left panel and then clicking pub get at the top of your screen. To avoid problems, delete the test folder, which Flutter generated when you executed the flutter create command.
Finally, build and run to see this:
Here are a few files you’ll see in the starter project’s lib folder:
-
main.dart is the main file that acts as the entry point for the app. It contains
MyApp
, which containsMaterialApp
. This widget usesMyHomePage
as the child. This widget contains the game’s only screen. -
cell.dart contains the model for the cells that you’ll lay out in a grid on the screen. It contains a class called
CellModel
. -
cell_widget.dart contains
CellWidget
, which is a visual representation of a cell on the screen.
Introducing the Minesweeper Flutter Game
Before you get into writing the code to build this Flutter game, it’s important to understand the basics of Minesweeper. It’s a logical single-player game that requires careful assessment of the cells on the board before making the next move. This continues until the player clears the board — or hits a mine. The game originates from the 1960s, and it has been written for many computing platforms in use today. It has many variations and offshoots. Simply do a quick Google search for “Minesweeper”, and you can play a web version of the game right in your browser.
For this project, you’ll need to understand Minesweeper in more detail.
Playing Minesweeper
Minesweeper is a simple — but not to be underestimated — puzzle game that consists of a rectangular board of tiles laid out on the screen. The game’s objective is to clear the board without detonating any hidden “mines”, or bombs, by using help from numbers indicating how many mines are in neighboring cells.
The player selects a cell to uncover it. If the cell has a mine, the game is over. Otherwise, the cell displays the number of mines in the cell’s Moore neighborhood. Using these numbers, the player uncovers nearby cells. This continues until either they hit a mine or uncover all the non-mine cells on the board. Therefore, understanding the numbers in the cells is crucial to completing the game.
While the game is in progress, the player can flag a cell for their reference if they suspect it to be a mine. This has no impact on the game logic but allows the player to keep track of the suspected cells. A flagged cell can be either a mine or a non-mine.
Once the player uncovers all the non-mine cells, the game is complete. In this Minesweeper Flutter version, the game restarts with increased difficulty — the size of the grid increases, and the number of mines also increases.
Understanding the Moore Neighborhood
A Moore neighborhood is a 3×3 grid of cells. It consists of one central cell and all its surrounding cells, including cells with sides adjacent to the central cell and those that touch its corners. The Moore neighborhood is named for Edward F. Moore, a computer scientist known for his work in topics including cellular automata and finite state machines. Here’s a diagram of a cell with its Moore neighborhood, with numbers indicating cell coordinates:
In Minesweeper, when the player selects a non-mine cell, the cell reveals a number. This represents the number of mines in the surrounding eight cells, or the cell’s Moore neighborhood. Using the number, the player can make a calculated guess about the next non-mine cell on the board that they can safely uncover. Keep in mind that no mines are uncovered at any point in the game. Tapping a cell with a mine is the only way to uncover a mine, and that ends the game.
When the player taps a non-mine cell, the game also uncovers all the non-mine cells in its Moore neighborhood. As a result, the non-mine cells in overlapping Moore neighborhoods are also uncovered, and this continues until there are no more non-mine cells in the Moore neighborhood of an uncovered cell. You use recursion to implement this in the code.
In the code, you’ll write unrevealRecursively
, which takes in a CellModel
object and uncovers the cells in the Moore neighborhood recursively.
Understanding the CellModel Class and Properties
Inside cell.dart, you’ll find the definition of the class CellModel
. This is the foundational model class you use to keep track of the cells on the board in the memory. You also use this class to represent the cell visually in combination with CellWidget
.
In the game, you store all the cells in a 2D array. Since you’re using Dart, you’ll create a List
to store all the cells. So, each cell will have a row number and column number. With this in mind, now look at the properties of the CellModel
class:
-
x is an
integer
that stores the cell’s column number. -
y is an
integer
that stores the cell’s row number. -
isMine is a
boolean
that storestrue
if the cell is a mine andfalse
if it isn’t. The default value isfalse
. -
isRevealed is a
boolean
variable that storestrue
if the cell has been uncovered orfalse
if it hasn’t. -
value is an
integer
variable that stores the number of mines in the Moore neighborhood of the cell. The default value is zero. -
isFlagged is a
boolean
variable that storestrue
if the player has flagged the cell orfalse
if not. The default value isfalse
.
Laying Out Widgets
Now that you know all the basics of the game and you’re also aware of CellModel
, it’s time to start writing some code and building the user interface.
Building the Grid
Start by implementing generateGrid
:
void generateGrid() {
cells = [];
totalCellsRevealed = 0;
totalMines = 0;
for (int i = 0; i < size; i++) {
var row = [];
for (int j = 0; j < size; j++) {
final cell = CellModel(i, j);
row.add(cell);
}
cells.add(row);
}
}
The code above simply creates a list of CellModel
objects for each position in the grid. i
is the column number, and j
is the row number. size
is the size of the grid, which defaults to five when the game starts.
At this point, invoke generateGrid
from initState
so the app generates cells randomly as soon it starts:
@override
void initState() {
super.initState();
generateGrid();
}
generateGrid
randomly generates CellModel
objects for all the cells in the game. If the size is five, the grid will have 25 cells. If you save everything and try running the app now, you won't see anything different on-screen. This is because you need to generate CellWidget
objects from these CellModel
objects and lay them out on the screen, which you'll do next.
Using Rows and Columns
The game board is essentially a 2D array of CellWidget
s. These are created based on randomly generated CellModel
objects. Start by implementing generateGrid
, which creates a 2D list of CellModel
objects that you'll use later to create CellWidget
s.
You need to implement a buildButton
that takes in a CellModel
object and returns the corresponding CellWidget
:
Widget buildButton(CellModel cell) {
return GestureDetector(
onLongPress: () {
// TODO
},
onTap: () {
// TODO
},
child: CellWidget(
size: size,
cell: cell,
),
);
}
Import CellWidget
from cell_widget.dart if you get errors regarding undefined symbols.
The code above simply wraps CellWidget
inside GestureDetector
and returns it. You'll use the onTap
and onLongPress
events later to implement user interactions with the cells. For now, you have empty functions bound to those events. CellWidget
requires size and cell properties. The default cell size is calculated according to the screen size, and the corresponding CellModel
is passed to the cell
property.
Next, you'll implement buildButtonRow
and buildButtonColumn
.
Implement buildButtonRow
first:
Row buildButtonRow(int column) {
List<Widget> list = [];
//1
for (int i = 0; i < size; i++) {
//2
list.add(
Expanded(
child: buildButton(cells[i][column]),
),
);
}
//3
return Row(
children: list,
);
}
Here's what's happening in the code snippet above:
- For any given column, you loop over from
0
tosize
and usebuildButton
to create a cell. - As the cell is created, it's added to a list of widgets.
- Finally, a
Row
widget is returned with the cells as its children.
Next, you'll implement buildButtonColumn
:
Column buildButtonColumn() {
List<Widget> rows = [];
//1
for (int i = 0; i < size; i++) {
rows.add(
buildButtonRow(i),
);
}
//2
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: rows,
),
// TODO
],
);
}
In the code snippet above, here's what's happening:
- A list of rows of cells is created using
buildButtonRow
. - You return a column with the list of rows of cells as its children.
- TODO: Leave a placeholder for the rules text and a progress bar in the column, which you'll add later.
Finally, add buildButtonColumn
to the body
of Scaffold
in build
, as shown below:
...
body: Container(
margin: const EdgeInsets.all(1.0),
child: buildButtonColumn(),
),
...
Save everything and restart the app. You'll see a grid of cells on the screen:
Adding Game Logic
Now that you have the basic UI up and running, you need to set up a few things you'll use while implementing the game logic. Start by randomly generating mines and calculating the numerical values for each cell.
Generating Mines
Generating mines is as simple as setting the isMine
property of a cell, an object of CellModel
, to true
. CellWidget
then takes care of the rendering. You do this inside generateGrid
right after generating the cells.
void generateGrid() {
...
// Marking mines
for (int i = 0; i < size; ++i) {
cells[Random().nextInt(size)][Random().nextInt(size)].isMine = true;
}
// Counting mines
for (int i = 0; i < cells.length; ++i) {
for (int j = 0; j < cells[i].length; ++j) {
if (cells[i][j].isMine) totalMines++;
}
}
}
You'll have to add the import for dart:math
package to make the Random
class work.
The code snippet above randomly assigns cells as mines. Since the size is set to five initially, five cells are randomly picked to set as mines. The second for
loop counts the number of mines generated. You may think this is completely irrelevant and unneeded, but that's not true.
Since you're generating mines randomly, it's possible to pick the same cell two or more times and set it as a mine. In such a situation, the totalMines
variable helps keep track of the total number of mines. Also, in this case, totalMines
will be less than size
.
Generating Cell Values
Now that you have mines — cells that have isMine=true
— you can generate the values of cells around the mines. For all other cells that aren't in the Moore neighborhood of any mines, the value always stays 0
— use createInitialNumbersAroundMine
to do this.
First, add the for
loop to invoke createInitialNumbersAroundMine
in generateGrid
:
void generateGrid() {
...
// Updating values of cells in Moore's neighbourhood of mines
for (int i = 0; i < cells.length; ++i) {
for (int j = 0; j < cells[i].length; ++j) {
if (cells[i][j].isMine) {
createInitialNumbersAroundMine(cells[i][j]);
}
}
}
}
In the code snippet above, you call createInitialNumbersAroundMine
for all the mine cells. Then, you pass the cell to the method as an argument.
Next, implement createInitialNumbersAroundMine
. Add the following definition to createInitialNumbersAroundMine
:
void createInitialNumbersAroundMine(CellModel cell) {
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].value++;
}
}
}
}
The code above increases the value of all cells in the input cell's Moore neighborhood by 1
. Remember that the default value is 0
— it's as simple as that. The following illustration will help you understand the function above in more detail:
As you can see in the illustration above, for each red cell representing a mine, the green cells represent its Moore neighborhood. Being in a Moore neighborhood increases the values of these cells by one, irrespective of their previous value. So, if a cell lies in the Moore neighborhood of more than one mine, its value increases by more than one.
At this point, if you want to look at the cell values and mines, you can quickly flip the default value of isRevealed
in cell.dart to true
.
Save the files and hot-restart the app. You'll see something like this:
Note the following points from the image above:
- The mines are randomly placed within the grid.
- Every cell in the Moore neighborhood of a mine has its values increased from zero.
- The final value of a cell is derived from the number of mines it has in its Moore neighborhood.
- The total number of mines should be five because the size is five, but the actual number is four in this case. This is because mines are generated randomly, and it's why you used
totalMines
to keep track of the total number of mines. -
CellWidget
in cell_widget.dart defines the cells' visual representation.
Don't forget to flip the default value of isRevealed
back to false
and save the file.
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? :]
Finishing Up
The game now works perfectly. You can play it, and the difficulty increases with each level. But you still need to put a few pieces into place to really polish your app.
Handling Edge Cases
What if the first cell the player taps is a mine? It wouldn't feel very good to lose immediately, right? You may have noticed this if you've played the game a couple times. While algorithmically, this is OK, fair play suggests avoiding this situation. Here's how to handle it.
When the user makes their first move in a new game and taps a cell that's a mine, you don't uncover the cell or display the Game Over dialog. Instead, you replace the cell they tapped with a non-mine cell. Even better, you regenerate the whole grid until the cell the user taps turns out to be a regular cell. Then you uncover that cell. All this happens so quickly that the user won't even notice.
Add the following code on the very top inside onTap
:
void onTap(CellModel cell) async {
// If the first tapped cell is a mine, regenerate the grid
if (cell.isMine && totalCellsRevealed == 0) {
while (cells[cell.x][cell.y].isMine == true) {
restart();
}
cell = cells[cell.x][cell.y];
}
if (cell.isMine) {
...
In the code above, you check if the first cell the player taps is a mine. If it is, use a while
loop to regenerate the grid using restart
until the player taps a regular cell.
Simple and elegant!
Adding ProgressBar
Next, you'll add a linear progress bar that tells the user their progress in the current game. It isn't required but is a nice touch to have.
Add the following code inside buildButtonColumn
:
return Column(
...
children: [
Column(
children: rows,
),
// Add this
LinearProgressIndicator(
backgroundColor: Colors.white,
value: totalCellsRevealed / (size * size - totalMines),
valueColor: AlwaysStoppedAnimation<Color>(Colors.deepPurple),
),
]
...
In the code above, you add a simple LinearProgressIndicator
widget that calculates the value based on the number of cells uncovered.
Adding Rules
Although Minesweeper is a very simple game, it would be helpful for new players to have the game's rules available. You have some vacant space at the bottom of the screen, so adding rules would be a great way to utilize that. You also have a pre-built method called getRules()
already in the starter project and ready to be used.
Add the following code to the end of the inner Column
widget's list of children inside buildButtonColumn
, right after LinearProgressIndicator
:
return Column(
...
children: [
...
// Add this
Expanded(
child: SingleChildScrollView(
primary: true,
child: getRules(),
),
),
]
...
There you go! Save everything, restart the app and enjoy the game. This is how your final app will look:
Where to Go From Here?
Download the final project using Download Materials at the top or bottom of the tutorial.
You just made a fully functional Minesweeper Flutter game and even added in a few extra niceties — that's awesome! Here are a few more ideas on how you can make the game even better:
- Adjust the difficulty of the game at each level by tweaking the number of mines by increasing the value of
size
. - Optimize the edge-case handling by forgiving not just the first tap on a mine cell, but the first two or even the first five.
- Make the game more fun and engaging by having a cloud-based scoreboard. This will add a sense of competition between players.
- Add music and animation for a more fun and appealing environment.
To learn more about developing games with Flutter, check out the tutorial How to Create a 2D Snake Game in Flutter, and to learn the basics of Flutter, check out all the Flutter tutorials.
We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!