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?
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.