Rewriting a Minesweeper - Part 3

Rewriting a Minesweeper - Part 3

Welcome to part 3 of the Minesweeper game. Today we will see logic behind some of the core and bonus features in the game.

Highlighting tiles

Tiles highlighting is triggered when user taps on one of the tiles which is not mine. This triggers a sort of chain reaction to uncover neighboring tiles until it reaches near the border of any mine.

Let's see by the example,

Looking at the image, when I clicked in the bottom left corner of the grid, it uncovered all the tiles until it reached near any mine.

tileButton.tileSelectedClosure = { [weak self] sequence in
    self?.highlightNeighboringButtons(with: sequence)
}

The method is defined as follows,

func highlightNeighboringButtons(with sequenceNumber: Int) {
    guard let mineButton = regularButtonsHolder.first(where: { $0.stateViewModel.sequenceNumber == sequenceNumber }) else { return }

    if !mineButton.stateViewModel.isVisited {
        
        viewModel.totalNumberOfTilesRevealed = viewModel.totalNumberOfTilesRevealed + 1
        mineButton.stateViewModel.isVisited = true            
        mineButton.updateBackgroundColor()

        if mineButton.stateViewModel.numberOfSurroundingMines == 0 {
            viewModel.currentScoreValue = viewModel.currentScoreValue + 1

            let surroundingTilesSequence = mineButton.stateViewModel.sequenceOfSurroundingTiles

            for sequence in surroundingTilesSequence {
                self.highlightNeighboringButtons(with: sequence)
            }
        } else {
            mineButton.setTitle("\(mineButton.stateViewModel.numberOfSurroundingMines)", for: .normal)
            viewModel.currentScoreValue = viewModel.currentScoreValue + (1 * mineButton.stateViewModel.numberOfSurroundingMines)
        }

        topHeaderView.updateScore(score: viewModel.currentScoreValue)

        if self.viewModel.didUserWinCurrentGame() {
            // Show the alert saying user has won the game
        }
    }
}

  1. Initially when highlightNeighboringButtons gets called, we try to find the selected tile from the regularButtonsHolder array by given sequence number.
  2. Second we also check whether tile has already been visited or not. This condition is very important to avoid infinite recursion
  3. Next, we also increment totalNumberOfTilesRevealed. This number is important to keep track of whether user has won the game or not
  4. We set the isVisited flag associated with mineButton to true and update the background color of tile to indicate that it has already been visited
  5. Next we check if this is a inert tile - Which means it does not have any surrounding mines. If this is the case, we can further navigate along the grid until we find the tile with neighboring mine. We achieve this by getting sequenceOfSurroundingTiles for given inert tile and call highlightNeighboringButtons on each of them
  6. On the other hand if tile has at least one neighboring mine, that means we have reached the boundary and it's good time to stop. We do so by updating the tile title with number of mines surrounding it
  7. Lastly, we also check if user has won the game or not. This is achieved by calling didUserWinCurrentGame method
func didUserWinCurrentGame() -> Bool {
    return self.totalNumberOfTilesRevealed == self.totalNumberOfTilesOnScreen()
        - self.totalNumberOfMines
}

Formula is very simple. Since number of total tiles is equal to number of mine tiles  plus number of non-mine tiles, we check if this condition is true and show the alert indicating user has won the game

And to have fun, this is how it looks like in slow motion,

Enabling cheat mode

This is the game, so it cannot be complete until cheat mode has been added. Cheat mode is enabled when user taps on the Reveal button in the header view. This shows all the mines present on the grid

Not quite fair if you're playing the game. But for me it's really useful while debugging the code. This is done with the simple logic,

func toggleMinesDisplayState(isRevealing: Bool) {
    for mineTile in minesButtonsHolder {
        if isRevealing {
            mineTile.stateViewModel.state = .revealed
            mineTile.showImage(with: "mine")
        } else {
            mineTile.stateViewModel.state = .notSelected
            mineTile.hideImage()
        }
        mineTile.updateBackgroundColor()
    }
}
  1. We have a method named toggleMinesDisplayState to toggle the state. Here we pass the parameter to check whether we are revealing or not. This value gets toggled every time this method is called
  2. We already have an array named minesButtonsHolder which stores the tile buttons which are  designated mines. We iterate over them and based on the whether we want to reveal or not we change the appearance
  3. First we update the state of each mine tile, toggle the mine image on the face and then call updateBackgroundColor which updates the background color of tile based on the currently assigned state

Scoring

if tile.stateViewModel.numberOfSurroundingMines == 0 {
    currentScoreValue = currentScoreValue + 1
} else {
    currentScoreValue = currentScoreValue + tile.stateViewModel.numberOfSurroundingMines
}

This algorithm is very simple. If tile hasn't been surrounded by mines, we increment the score by one. But if it has some mines around it, we increment it by the number of surrounding mines.

This can be made slightly more complicated if we want to add difficulty levels to the game. In which case above logic becomes like this,

if tile.stateViewModel.numberOfSurroundingMines == 0 {
    currentScoreValue = currentScoreValue + difficultyLevel * 1
} else {
    currentScoreValue = currentScoreValue + difficultyLevel * tile.stateViewModel.numberOfSurroundingMines
}

This is it for today's code. My next post will be about refactoring the existing codebase, removing redundant portions, and making it ready to get started with writing some good unit tests. Until later folks!