Minesweeper Clone


I'd wager that most people, of my generation at least, remember Minesweeper. It came pre-installed on every Windows OS that I ever used, and I have fond memories of a friendly Minesweeper rivalry which developed between my family.

I played it a lot and I loved it. So I cloned it.

Two screenshots from the Minesweeper clone.

My goal

At this stage in my journey I was beginning to get a little more comfortable with programming. I had spent a reasonable about of time with Python and had made some small projects including (with the help of a friend) a clone of Snake.

So between a mix of nostalgia and wanting to challenge myself I decided to try and clone Minesweeper. At this point, besides my memories of playing the game, I had no idea how it worked or how I would go about implementing the functionailty and there were certain mechanics in the game which made me especially anxious. But this made it an exciting challenge.


As seen in the screenshot, Minesweeper had a counter on either side of the UI resembling a retro, digital alarm-clock display. One counter keeps track of how many flags the player has placed in the game, and the other is a timer that counts the seconds it takes to complete the current game.

I decided to create this counter programmatically which would take the input of an integer, and display it accordingly.

Stack

For this project I used only Python3; it's standard modules random, threading and time; and it's popular third-party game library Pygame.

Pygame comes with lots of useful built-in functionality. For my sprites I inherited from Pygame's sprite.Sprite class, their most basic Sprite class. This is purely because Pygame has the handy class sprite.Group which is a container class to hold and manage Sprite objects. This is especially useful because I could very easily create groups which would hold different Sprites for different scenarios.

I also used a handful of Pygame's built-in methods. Most notably for things that helped with:

  • Drawing to the screen
  • Collision detection (I know what you're thinking, Minesweeper has no moving elements - I'll come back to this.)
  • Mouse click events and mouse position.

Challenges

Economic Playability

Below is a gif of the clone I built being used.

A screen recording of my clone.

One aspect of the game which I really wanted to implement can be seen in this clip.

  • When an empty square (it contains no mine, nor a number indicating a mine nearby) is clicked it opens up an area of empty squares around it.

This mechanic ensures that the player isn't wasting their time clicking on countless empty squares.

The Solution

At first I was quite unsure how to achieve this. However, I had not long learned about recursion and this made sense to me as a solution for the current scenario:

If the clicked square is empty, check if the square next to it is empty, and then check if the square next to that is empty...

... and so on and so forth.

python
1def left_clicked(self):
2
3 self.clicked = True
4 if self.mine:
5 if not self.flagged:
6 self.end_game()
7
8 elif self.number != 0:
9 if not self.flagged:
10 self.image = image_dict.get(str(self.number))
11 else:
12 if not self.flagged:
13 self.image = image_dict.get("clicked_empty")
14 self.check_if_empty()

The code extract above shows what happens when one of the squares on the Minesweeper board is clicked with the mouses left button:

After setting the self.clicked attribute to True, checking that it is not a mine (and therefore not the end of the game) and also checking that it is not one of the squares with a number (indicating a mine is nearby) the only option remaining is that the square must be empty.

I change the image of the current sprite to correctly resemble the empty square, and then call the self.check_if_empty() method.

python
1def check_if_empty(self):
2 for spr in all_sprites.sprites():
3 if spr.clicked:
4 continue
5 elif self.hitbox.rect.colliderect(spr.hitbox.rect) and not spr.mine and not spr.flagged:
6 spr.left_clicked()

In this method:

  1. I use a for loop to loop through all_sprites, which is a Pygame sprite group.

    • It checks if the sprite in the current iteration has already been clicked: If it has, it is of no concern and so it will move onto the next iteration of the loop.

    • However, if it hasn't been clicked I use the sprite's hitbox to check that the sprite in the current loop iteration is touching the sprite we called the method from; then I check that it also isn't a mine; and I also check that the player hasn't flagged it because - even if it is empty, if the player suspects it may contain a mine we can't overrule that.

With those checks complete I then call the left_clicked() method on the sprite in the for loop - this is the recursion.

The script will carry on recursively until all empty sprites that are indirectly in contact (via empty squares) with the originally clicked square have been opened.

Conclusion

I'm very proud of the what I achieved with this project but as always, there are things I could have done better.

The main lessons I've taken from this project are to:

Abstract and organise my code.

The code may work, but apart from the timer module, and the constants it is all in one script and this definitely makes it difficult to navigate and make sense of, especially to someone unfamiliar with the project.

Write more explicit comments and function docstrings.

Although I did write lots of comments I don't think they were all explicit enough to understand when coming back to the project after a period working on other things.

This is the same problem with the functions. When unfamiliar with the project one can read through the function and figure out what it is doing, but it would be much easier and quicker if I were to have written docstrings plainly explaining it's arguments, why it needs them, and what the function is doing and eventually returning.