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.

4.1 (8) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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

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:

  1. For any given column, you loop over from 0 to size and use buildButton to create a cell.
  2. As the cell is created, it's added to a list of widgets.
  3. 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:

  1. A list of rows of cells is created using buildButtonRow.
  2. You return a column with the list of rows of cells as its children.
  3. 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:

Grid on the game 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:

Grid showing mines and their Moore neighborhoods

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:

Cells with mines and values

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.