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 3 of 4 of this article. Click here to view the first page.

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:

  1. Check if the tapped cell is a mine.
  2. 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.
  3. 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:

Uncovering the cells on tap

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:

Flagging/Unflagging the cells on long-press

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:

Gameplay ending with Game Over message and Restart option

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:

Gameplay ending with Congratulations and Next Level option

After you finish the game successfully, you can move on to the next level, which has a bigger grid. Awesome, right? :]