A while ago, I wrote a minesweeper AI. I intended to publish a writeup, but due to university and life and exams, I never got around to writing it. But having just finished my Fall term, I have some time to write a decent overview of what I did.
Short 30 second video of the AI in action here:
How to Play Minesweeper
If you’re an experienced minesweeper player, you can probably skip this section. Otherwise, I’ll just give a quick overview of some basic strategies that we can use to solve an easy minesweeper game.
We start with a 10×10 Beginner’s grid, and click on a square in the middle:
We can quickly identify some of the mines. When the number 1 has exactly one empty square around it, then we know there’s a mine there.
Let’s go ahead and mark the mines:
Now the next strategy: if a 1 has a mine around it, then we know that all the other squares around the 1 cannot be mines.
So let’s go ahead and click on the squares that we know are not mines:
Keep doing this. In this case, it turns out that these two simple strategies are enough to solve the Beginner’s grid:
Roadmap to an AI
All this seems easy enough. Here’s what we’ll need to do:
- Read the board. If we use a screenshot function, we can get a bitmap of all the pixels on the board. We just need to ‘read’ the numbers on the screen. Luckily for us, the numbers tend to have different colors: 1 is blue, 2 is green, 3 is red, and so on.
- Compute. Run the calculations, figure out where the mines are. Enough said.
- Click the board. This step is easy. In Java, we can use the Robot class in the standard library to send mouse clicks to the screen.
Reading the Field
There’s not a whole lot to this step, so I’m going to skim over it quickly.
At the beginning of the run, while we have a completely empty grid, we invoke a calibration routine – which takes a screenshot and looks for something that looks like a Minesweeper grid. Using heuristics, it determines the location of the grid, the size of a grid square, the dimensions of the board, and things like that.
Now that we know where the squares are, if we want to read a square, we crop a small section of the screenshot and pass it to a detection routine, which looks at a few pixels and figures out what’s in the square.
A few complications came up in the detection routine:
- The color for the number 1 is very close to the color of an unopened square: both are a dark-blue color. To separate them apart, I compared the ‘variance’ of the patch from the average color for the patch.
- The color for 3 is identical to that for 7. Here, I used a simple edge-detection heuristic.
The trivially straightforward algorithm is actually good enough to solve the beginner and intermediate versions of the game a good percent of the time. Occasionally, if we’re lucky, it even manages to solve an advanced grid!
When humans play minesweeper, we compete for the fastest possible time to solve a grid of minesweeper. So it doesn’t matter if we lose 20 games for every game we win: only the wins count.
This is clearly a silly metric when we’re a robot that can click as fast as we want to. Instead, we’ll challenge ourselves with a more interesting metric:
Win as many games as possible.
Consider the following scenario:
Using the straightforward method, we seem to be stuck.
Up until now, whenever we mark a square as having a mine or safe, we’ve only had to look at a single 3×3 chunk at a time. This strategy fails us here: the trick is to employ a multisquare algorithm – look at multiple different squares at once.
From the lower 2, we know that one of the two circled squares has a mine, while the other doesn’t. We just don’t know which one has the mine:
Although this doesn’t tell us anything right now, we can combine this information with the next 2: we can deduce that the two yellowed squares are empty:
Let’s click them to be sure.
And voilà. They’re empty. The rest of the puzzle can be solved easily, after we’ve made the deduction that those two squares were empty.
The Tank Solver Algorithm
It’s difficult to make the computer think deductively like we just did. But there is a way to achieve the same results, without deductive thinking.
The idea for the Tank algorithm is to enumerate all possible configurations of mines for a position, and see what’s in common between these configurations.
In the example, there are two possible configurations:
You can check for yourself that no other configuration could work here. We’ve deduced that the one square with a cross must contain a mine, and the three squares shaded white below must not contain a mine:
This works even better than human deduction!
We always try to apply the simple algorithm first, and only if that gets us stuck, then we bring in the Tank algorithm.
To implement the Tank algorithm, we first make a list of border tiles: all the tiles we aren’t sure about but have some partial information.
Now we have a list of border tiles. If we’re considering every possible configuration, there are of them. With backtracking, this number is cut down enough for this algorithm to be practical, but we can make one important optimization.
The optimization is segregating the border tiles into several disjoint regions:
If you look carefully, whatever happens in the green area has no effect on what happens in the pink area – we can effectively consider them separately.
How much of a speedup do we get? In this case, the green region has 10 tiles, the pink has 7. Taken together, we need to search through combinations. With segregation, we only have : about a 100x speedup.
Practically, the optimization brought the algorithm from stopping for several seconds (sometimes minutes) to think, to giving the solution instantly.
Probability: Making the Best Guess
Are we done now? Can our AI dutifully solve any minesweeper grid we throw at it, with 100% accuracy?
One of the two squares has a mine. It could be in either, with equal probability. No matter how cleverly we program our AI, we can’t do better than a 50-50 guess. Sorry.
The Tank solver fails here, no surprise. Under exactly what circumstances does the Tank algorithm fail?
If it failed, it means that for every border tile, there exists some configuration that this tile has a mine, and some configuration that this tile is empty. Otherwise the Tank solver would have ‘solved’ this particular tile.
In other words, if it failed, we are forced to guess. But before we put in a random guess, we can do some more analysis, just to make sure that we’re making the best guess we could make.
Try this. What do we do here:
From the 3 in the middle, we know that three of them are mines, as marked. But marking mines doesn’t give us any new information about the grid: in order to gain information, we have to uncover some square. Out of the 13 possible squares to uncover, it’s not at all clear which one is the best.
The Tank solver finds 11 possible configurations. Here they are:
Each of these 11 configurations should be equally likely to be the actual position – so we can assign each square a probability that it contains a mine, by counting how many (of the 11) configurations does it contain a mine:
Our best guess would be to click on any of the squares marked ‘2’: in all these cases, we stand an 82% chance of being correct!
Two Endgame Tactics
Up until now, we haven’t utilized this guy:
The mine counter. Normally, this information isn’t of too much use for us, but in many endgame cases it saves us from guessing.
Here, we would have a 50-50 guess, where two possibilities are equally likely.
But what if the mine counter reads 1? The 2-mine configuration is eliminated, leaving just one possibility left. We can safely open the three tiles on the perimeter.
Now on to our final tactic.
So far we have assumed that we only have information on a tile if there’s a number next to it. For the most part, that’s true. If you pick a tile in some distant unexplored corner, who knows if there’s a mine there?
Exceptions can arise in the endgame:
The mine counter reads 2. Each of the two circled regions gives us a 50-50 chance – and the Tank algorithm stops here.
Of course, the middle square is safe!
To modify the algorithm to solve these cases, when there aren’t that many tiles left, do the recursion on all the remaining tiles, not just the border tiles.
The two tricks here have the shared property that they rely on the mine counter. Reading the mine counter, however, is a non-trivial task that I won’t attempt; instead, the program is coded in with the total number of mines in the grid, and keeps track of the mines left internally.
Conclusion, Results, and Source Code
At this point, I’m convinced that there isn’t much more we could do to improve the win rate. The algorithm uses every last piece of information available, and only fails when it’s provably certain that guessing is needed.
How well does it work? We’ll use the success rate for the advanced grid as a benchmark.
- The naïve algorithm could not solve it, unless we get very lucky.
- Tank Solver with probabilistic guessing solves it about 20% of the time.
- Adding the two endgame tricks bumps it up to a 50% success rate.
I’m done for now; the source code for the project is available on Github if anyone is inclined to look at it / tinker with it:
Further discussion on Reddit and in the comments below!