A Beautiful Refactor
For this post I am going to take a break from my regular story about how I rewrote the minesweeper game to Swift. This is more about the beautiful refactor and how I was able to eliminate lot of boilerplate code with simple algorithm to achieve what the old and poor code did.
Goal:
To explain the goal, let me show it from the visual perspective. Let's say we're playing the Minesweeper in 3 X 3 grid which looks like this with each tile numbered going from left to right and then top to bottom.
Given this my goal was to get the sequence of all the neighbors for the given tile. This was to keep highlighting neighboring tiles when user taps on one of them. In the beginning I had considered 9 different cases based on where tile is located.
- 4 corners
- Any tile in 4 border rows (Except the ones at corner)
- Any tile falling inside the grid
This prompted me to write very poor and verbose code to get such neighbors. The original code looked like this,
func neighboringTilesForGivenMineTile(with minesTileSequenceNumber: Int, totalNumberOfTilesInRow: Int) -> [Int] {
var resultantNeightbors: [Int] = []
let topRightCorner = totalNumberOfTilesInRow - 1;
let bottomLeftCorner = totalNumberOfTilesInRow * topRightCorner
if (minesTileSequenceNumber == 0) {
resultantNeightbors = [minesTileSequenceNumber + 1, minesTileSequenceNumber + totalNumberOfTilesInRow, minesTileSequenceNumber + totalNumberOfTilesInRow + 1]
} else if (minesTileSequenceNumber == topRightCorner) {
resultantNeightbors = [minesTileSequenceNumber - 1, minesTileSequenceNumber + totalNumberOfTilesInRow - 1, minesTileSequenceNumber + totalNumberOfTilesInRow]
} else if minesTileSequenceNumber == bottomLeftCorner {
resultantNeightbors = [minesTileSequenceNumber + 1, minesTileSequenceNumber - totalNumberOfTilesInRow + 1, minesTileSequenceNumber - totalNumberOfTilesInRow]
} else if minesTileSequenceNumber == topRightCorner + bottomLeftCorner {
resultantNeightbors = [minesTileSequenceNumber - 1, minesTileSequenceNumber - totalNumberOfTilesInRow - 1, minesTileSequenceNumber - totalNumberOfTilesInRow]
} else if (minesTileSequenceNumber < topRightCorner) {
// Top horizontal Row
resultantNeightbors = [minesTileSequenceNumber - 1, minesTileSequenceNumber + 1, minesTileSequenceNumber + totalNumberOfTilesInRow - 1, minesTileSequenceNumber + totalNumberOfTilesInRow + 1, minesTileSequenceNumber + totalNumberOfTilesInRow]
} else if ((minesTileSequenceNumber + 1) % totalNumberOfTilesInRow == 0) {
// Extreme right vertical row
resultantNeightbors = [minesTileSequenceNumber - 1, minesTileSequenceNumber + totalNumberOfTilesInRow, minesTileSequenceNumber + totalNumberOfTilesInRow - 1, minesTileSequenceNumber - totalNumberOfTilesInRow, minesTileSequenceNumber - totalNumberOfTilesInRow - 1]
} else if (minesTileSequenceNumber % totalNumberOfTilesInRow == 0) {
// Extreme left vertical row
resultantNeightbors = [minesTileSequenceNumber + 1, minesTileSequenceNumber - totalNumberOfTilesInRow, minesTileSequenceNumber - totalNumberOfTilesInRow + 1, minesTileSequenceNumber + totalNumberOfTilesInRow, minesTileSequenceNumber + totalNumberOfTilesInRow + 1]
} else if (minesTileSequenceNumber > bottomLeftCorner) {
// Bottom horizontal row
resultantNeightbors = [minesTileSequenceNumber - 1, minesTileSequenceNumber + 1, minesTileSequenceNumber - totalNumberOfTilesInRow - 1, minesTileSequenceNumber - totalNumberOfTilesInRow + 1, minesTileSequenceNumber - totalNumberOfTilesInRow]
} else {
// Any tile inside grid and not touching any adjacent boundary
resultantNeightbors = [minesTileSequenceNumber - 1, minesTileSequenceNumber + 1, minesTileSequenceNumber + totalNumberOfTilesInRow, minesTileSequenceNumber - totalNumberOfTilesInRow, minesTileSequenceNumber + totalNumberOfTilesInRow - 1, minesTileSequenceNumber + totalNumberOfTilesInRow + 1, minesTileSequenceNumber - totalNumberOfTilesInRow - 1, minesTileSequenceNumber - totalNumberOfTilesInRow + 1]
}
return resultantNeightbors;
}
As you can see I am also passingminesTileSequenceNumber
andtotalNumberOfTilesInRow
which are required parameters to get list of neighbors in any given position
Sure, it does what I want it to do, but it feels like I am assuming so much and hardcoding so many things. It's also difficult to reason about this code just looking at it. Just as you will be in any primitive code, it heavily relies on lot of if-else
conditions.
So I thought why not make it more generic with some algorithm? Result was I ended up writing this code like this which produces exact same result.
func neighboringTilesForGivenMineTile(with minesTileSequenceNumber: Int, totalNumberOfTilesInRow: Int) -> [Int] {
let row = minesTileSequenceNumber / totalNumberOfTilesInRow
let col = minesTileSequenceNumber % totalNumberOfTilesInRow
let resultantNeighbors = [Coordinate(row: row - 1, column: col - 1),
Coordinate(row: row - 1, column: col),
Coordinate(row: row - 1, column: col + 1),
Coordinate(row: row, column: col - 1),
Coordinate(row: row, column: col + 1),
Coordinate(row: row + 1, column: col - 1),
Coordinate(row: row + 1, column: col),
Coordinate(row: row + 1, column: col + 1)
].filter { $0.isCoordinateInGivenRange(low: 0, high: totalNumberOfTilesInRow - 1) }
let result = resultantNeighbors.compactMap { value -> Int in
return value.row * totalNumberOfTilesInRow + value.column
}
return result
}
struct Coordinate {
let row: Int
let column: Int
func isCoordinateInGivenRange(low: Int, high: Int) -> Bool {
return self.row.isInTheInclusiveRange(low: low, high: high) && self.column.isInTheInclusiveRange(low: low, high: high)
}
}
- First off it takes two parameters as input. The tile sequence number which has a range of
[0, pow(numberOfTilesInRow, 2))
- For the given tile sequence, we can easily compute its row and column using sequence number and number of tiles in a row
- One we get coordinates of given tile, we compute the 8 immediate neighbors of a given tile on next line and store them into
resultantNeighbors
- Now, we don't really need all these 8 neighbors. But we need to filter them. Some of them might fall outside the boundary we set for the given box
- This is where the
filter
comes in the picture. Here we iterate over every coordinate and eliminate those where either value falls outside the given range[0, totalNumberOfTilesInRow - 1]
- Next, we have an array of coordinates which are valid immediate neighbors of given tile. But we don't really want coordinates. We want sequence numbers of those neighbors
- Now we take every coordinate from the array and convert that into sequence number corresponding to that tile number - Essentially mapping array of coordinates into array of
int
s and return this value