… in Minesweeper, of course.
One of the basic building blocks of my hands-on software development workshops are Coding Katas. Minesweeper, the classic game that every Microsoft Windows user knows since 1992, is one of the “medium everything” Katas: Medium scope, medium complexity, medium demand. One advantage is that virtually nobody needs any more explanation than “program a Minesweeper”.
The first milestone when programming a Minesweeper game is to lay the mines on the field in a random fashion. And to my continual surprise, this seems to be a hard task for most participants. Not hard in the sense of giving up, but hard in the sense that the solution lacks suitability or even correctness.
So in order to have a reference point I can use in future workshops and to discuss the usual approaches, here is how you lay a minefield in an adequate way.
The task is to fill a grid of tiles or cells with a predefined amount of mines. Let’s say you have a grid of ten rows and ten columns and want to place ten mines randomly.
Spoiler alert: If you want to think about this problem on your own, don’t read any further and develop your solution first!
Task analysis
If you pause for a moment and think about the ingredients that you need for the task, you’ll find three things:
- A die (in the form of a random number generator)
- An amount of mines to place (maybe just represented by a counter variable)
- An amount of tiles (stored in a data structure)
Each solution I’ll present uses one of these things as the primary focus of interest. My language for this effect is that the solution “takes the thing into the hands”, while the other two things “lay on the table”. That’s because if you simulate how the solution works with real objects like paper and dice, you’ll really have one thing in your hands and the others on the table most of the time.
Solution #1: The probability approach
One way to think about the task of placing 10 mines somewhere on 100 tiles is to calculate the probability of a mine being on a tile and just roll the dice for every tile.
Here’s some code that shows how this approach might look like in Java:
private static final int fieldWidth = 10;
private static final int fieldHeigth = 10;
public static Set<Point> placeMinesOn(Set<Point> field) {
Random rng = new Random();
final double probability = 0.1D;
for (int column = 0; column < fieldWidth; column++) {
for (int row = 0; row < fieldHeigth; row++) {
if (rng.nextDouble() < probability) {
field.add(new Point(row, column));
}
}
}
return field;
}
Before we discuss the effects of this code, let’s have a little talk about the data structure that I use for the tiles:
The most common approach to model a two-dimensional grid of tiles in software is to use a two-dimensional array. There is nothing wrong with it, it’s just not the most practical approach. In reality, it is really cumbersome compared to its alternatives (yes, there are several). My approach is to separate the aspect of “two-dimensionalness” from my data structure. That’s why I use a set of points. The set (like a HashSet) is a one-dimensional data structure that more or less can only say “yes, I know this point” or “no, I never heard of this point”. To determine if a certain point (or tile at this coordinate) contains a mine, it just needs to be included in the set. An empty set represents an empty field. If you remove “cleared” mines from the set, its size is the number of remaining mines. With a two-dimensional array, you probably write several loop-in-loop structures, one of them just to count non-cleared mines.
Ok, now back to the solution. It is the approach that holds the die in the hands and uses it for every tile. The problem with it is that our customer didn’t ask for a probability of 10 percent for a mine, he/she asked for 10 mines. And the code above might place 10 mines, or 9, or 11, or even 14. In fact, the code places somewhere between 0 and 100 mines on the field.
The one thing this solution has going for it is the guaranteed runtime. We roll the dice 100 times and that’s it.
So we can categorize this solution as follows:
- Correctness: not guaranteed
- Runtime: guaranteed
If I were the customer, I would reject the solution because it doesn’t produce the outcome I require. A minesweeper contest based on this code would end in a riot.
Solution #2: Sampling with replacement
If you don’t take up the die, but the mines and try to dispense them on the field, you implement our second solution. As long as you have mines on hand, you choose a field at random and place it there. The only exception is that you can’t place a mine above a mine, so you have to check for the presence of a mine first.
Here’s the code for this solution in Java:
public static Set<Point> placeMinesOn(Set<Point> field) {
Random rng = new Random();
int remainingMines = 10;
while (remainingMines > 0) {
Point randomTile = new Point(
rng.nextInt(fieldHeigth),
rng.nextInt(fieldWidth)
);
if (field.contains(randomTile)) {
continue;
}
field.add(randomTile);
remainingMines--;
}
return field;
}
This solution works better than the previous one for the correctness category. There will always be 10 mines on the field once we are finished. The problem is that we can’t guarantee that we are able to place the mines in time before our player gets bored. Taking it to the extreme means that this code might run forever, especially if your random number generator isn’t up to standards.
So, the participants of your minesweeper contest might not protest the arbitrary number of mines on their field, but maybe just because they don’t know yet that they’ll always get 10 mines dispersed.
- Correctness: guaranteed
- Runtime: not guaranteed
This solution will probably work alright in reality. I just don’t see the need to utilize it when there is a clearly superior solution at hands.
Solution #3: Sampling without replacement
So far, we picked up the die and the mines. But what if we pick up the tiles? That’s the third solution. In short, we but all tiles in a bag and pick one by random. We place a mine there and don’t put it back into the bag. After we’ve picked 10 tiles and placed 10 mines, we solved the problem.
Here’s code that implements this solution in Java:
public static Set<Point> placeMinesOn(Set<Point> field) {
List<Point> allTiles = new LinkedList<Point>();
for (int column = 0; column < fieldWidth; column++) {
for (int row = 0; row < fieldHeigth; row++) {
allTiles.add(new Point(row, column));
}
}
Collections.shuffle(allTiles);
int mines = 10;
for (int i = 0; i < mines; i++) {
Point tile = allTiles.remove(0);
field.add(tile);
}
return field;
}
The cool thing about this solution is that it excels in both correctness and runtime. Maybe we use some additional memory for our bag and some runtime for the shuffling, but both can be predefined to an upper limit.
Yet, I rarely see this solution in my workshops. It’s only after I challenge their first solution that people come up with it. I’m biased, of course, because I’ve seen too many approaches (successful and failed) and thought about the problem way longer than usual. But you, my reader, are probably an impartial mind on this topic and can give some thoughts in the comments. I would appreciate it!
So, let’s categorize this approach:
- Correctness: guaranteed
- Runtime: guaranteed
If I were the customer, this would be my anticipation. My minesweeper contest would go unchallenged as long as nobody finds a flaw in the shuffle algorithm.
Summary
It is suprisingly hard to solve simple tasks like “distribute N elements on a X*Y grid” in an adequate way. My approach to deconstruct and analyze these tasks is to visualize myself doing them “in reality”. This is how I come up with the “thing in hands” metaphor that help me to imagine new solutions. I hope this helps you sometimes in the future.
How do you lay a minefield and how do you find your solutions? Write a blog post or leave a comment!
I solved it similar:
public static Board createBoardAndDistributeMinesByRandom(int width, int height, int numberOfMines) {
Board board = new Board(width, height);
List<Coordinates> freeFields = new ArrayList<>(board.getAllFields());
while (board.getNumberOfMines() < numberOfMines) {
int index = (int) (Math.random() * freeFields.size());
Coordinates newMine = freeFields.get(index);
freeFields.remove(index);
board.setMineAt(newMine.getX(), newMine.getY());
}
return board;
}
I like your idea of using Collections.shuffle