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
}
}
}
- Initially when
highlightNeighboringButtons
gets called, we try to find the selected tile from theregularButtonsHolder
array by given sequence number. - Second we also check whether tile has already been visited or not. This condition is very important to avoid infinite recursion
- Next, we also increment
totalNumberOfTilesRevealed
. This number is important to keep track of whether user has won the game or not - We set the
isVisited
flag associated withmineButton
to true and update the background color of tile to indicate that it has already been visited - 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 gettingsequenceOfSurroundingTiles
for giveninert
tile and callhighlightNeighboringButtons
on each of them - 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
- 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()
}
}
- 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 - 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 - 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!