Pyqt5 Project 3
Pyqt5 Project 3
NEW!
PyQt5 book Tutorials ▾ Examples ▾ Widgets ▾ ▾ Services ▾ Register S
Examples
Moonsweeper
by Martin Fitzpatrick
Explore the mysterious moon of Q'tee without getting too close to the alien natives!
Moonsweeper is a single-player puzzle video game. The objective of the game is to explore the area around your landed space
rocket, without coming too close to the deadly B'ug aliens. Your trusty tricounter will tell you the number of B'ugs in the
vicinity.
This a simple single-player exploration game modelled on Minesweeper where you must reveal all the tiles without hitting
hidden mines. This implementation uses custom QWidget objects for the tiles, which individually hold their state as mines,
status and the adjacent count of mines. In this version, the mines are replaced with alien bugs (B'ug) but they could just as
easily be anything else.
Installers for Windows, Linux and Mac are available to download above, along with the complete source code.
python3 minesweeper.py
Read on for a walkthrough of how the code works. The code is compatible with PyQt5 or PySide2 (Qt for Python), the only
thing that changes is the imports and signal signature (see later).
PyQt5 PySide2
PYTHON
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
https://www.learnpyqt.com/examples/moonsweeper/ 1/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
from PyQt5.QtCore import *
Playing Field
The playing area for Moonsweeper is a NxN grid, containing a set number of mines. The dimensions and mine counts we'll
used are taken from the default values for the Windows version of Minesweeper. The values used are shown in the table
below:
Easy 8x8 10
Medium 16 x 16 40
Hard 24 x 24 99
We store these values as a constant LEVELS de ned at the top of the le. Since all the playing elds are square we only need to
store the value once (8, 16 or 24).
LEVELS = [
("Easy", 8, 10),
("Medium", 16, 40),
("Hard", 24, 99)
]
The playing grid could be represented in a number of ways, including for example a 2D 'list of lists' representing the different
states of the playing positions (mine, revealed, agged).
However, this implementation uses an object-orientated approach. Individual squares on the map hold all relevant data about
their current state and are also responsible for drawing themselves. In Qt we can do this simply by subclassing from QWidget
and then implementing a custom paint function.
Since our tile objects are subclassing from QWidget we can lay them out like any other widget. We do this, by setting up a
QGridLayout .
self.grid = QGridLayout()
self.grid.setSpacing(5)
self.grid.setSizeConstraint(QLayout.SetFixedSize)
We can set up the playing around by creating our position tile widgets and adding them our grid. The initial setup for the level
reads from LEVELS and assigns a number of variables to the window. The window title and mine counter are updated, and
then the setup of the grid starts.
self.clear_map()
self.init_map()
self.reset_map()
The Pos class represents a tile, and holds all the relevant information for its relevant position in the map — including, for
example, whether it is a mine, revealed, agged and the number of mines in the immediate vicinity.
Each Pos object also has 3 custom signals clicked, revealed and expandable which we connect to custom slosts. Finally, we
call resize to adjust the size of the window to the new contents. This is actually only necessary when the window shrinks — Qt
will grow it automatically.
https://www.learnpyqt.com/examples/moonsweeper/ 2/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
def init_map(self):
# Add positions to the map
for x in range(0, self.b_size):
for y in range(0, self.b_size):
w = Pos(x,y)
self.grid.addWidget(w, y, x)
# Connect signal to handle expansion.
w.clicked.connect(self.trigger_start)
w.revealed.connect(self.on_reveal)
w.expandable.connect(self.expand_reveal)
1. The singleShot timer is required to ensure the resize runs after we've returned to the event loop and Qt is aware of the
new contents.
Now we have our grid of positional tile objects in place, we can begin creating the initial conditions of the playing board. This
is broken down into a number of functions. We name them _reset (the leading underscore is a convention to indicate a
private function, not intended for external use). The main function reset_map calls these functions in turn to set it up.
def reset_map(self):
self._reset_position_data()
self._reset_add_mines()
self._reset_calculate_adjacency()
self._reset_add_starting_marker()
self.update_timer()
The separate steps from 1-5 are described in detail in turn below, with the code for each step.
The rst step is to reset the data for each position on the map. We iterate through every position on the board, calling
.reset() on the widget at each point. The code for the .reset() function is de ned on our custom Pos class, we'll explore in
detail later. For now it's enough to know it clears mines, ags and sets the position back to being unrevealed.
def _reset_position_data(self):
# Clear all mine positions
for x in range(0, self.b_size):
for y in range(0, self.b_size):
w = self.grid.itemAtPosition(y, x).widget()
w.reset()
Now all the positions are blank, we can begin the process of adding mines to the map. The maximum number of mines
n_mines is de ned by the level settings, described earlier.
def _reset_add_mines(self):
# Add mine positions
positions = []
while len(positions) < self.n_mines:
x, y = random.randint(0, self.b_size-1), random.randint(0, self.b_size-1)
if (x ,y) not in positions:
w = self.grid.itemAtPosition(y,x).widget()
w.is_mine = True
positions.append((x, y))
https://www.learnpyqt.com/examples/moonsweeper/ 3/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
With mines in position, we can now calculate the 'adjacency' number for each position — simply the number of mines in the
immediate vicinity, using a 3x3 grid around the given point. The custom function get_surrounding simply returns those
positions around a given x and y location. We count the number of these that is a mine is_mine == True and store.
Pre-calculating the adjacent counts here helps simplify the reveal logic later. But it means we can't allow the user to
choose their initial move — we can explain this away as the "initial exploration around the rocket" and make it sound
completely sensible.
def _reset_calculate_adjacency(self):
A starting marker is used to ensure that the rst move is always valid. This is implemented as a brute force search through the
grid space, effectively trying random positions until we nd a position which is not a mine. Since we don't know how many
attempts this will take, we need to wrap it in an continuous loop.
Once that location is found, we mark it as the start location and then trigger the exploration of all surrounding positions. We
break out of the loop, and reset the ready status.
def _reset_add_starting_marker(self):
# Place starting marker.
while True:
x, y = random.randint(0, self.b_size - 1), random.randint(0, self.b_size - 1)
w = self.grid.itemAtPosition(y, x).widget()
# We don't want to start on a mine.
if not w.is_mine:
w.is_start = True
w.is_revealed = True
w.update()
# Reveal all positions around this, if they are not mines either.
for w in self.get_surrounding(x, y):
if not w.is_mine:
w.click()
break
https://www.learnpyqt.com/examples/moonsweeper/ 4/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
Position Tiles
The game is structure game so that individual tile positions hold their own state information. This means that Pos tiles are
able to handle their own game logic.
Since the Pos class is relatively complex, it is broken down here to a few parts, which are discussed in turn. The initial setup
__init__ block is simple, accepting an x and y position and storing it on the object. Pos positions never change once
created.
To complete setup the .reset() function is called which resets all object attributes back to default, zero values. This ags the
mine as not the start position, not a mine, not revealed and not agged. We also reset the adjacent count.
PyQt5 PySide2
PYTHON
class Pos(QWidget):
expandable = pyqtSignal(int,int)
revealed = pyqtSignal(object)
clicked = pyqtSignal()
self.setFixedSize(QSize(20, 20))
self.x = x
self.y = y
self.reset()
def reset(self):
self.is_start = False
self.is_mine = False
self.adjacent_n = 0
self.is_revealed = False
self.is_flagged = False
self.update()
Gameplay is centered around mouse interactions with the tiles in the play eld, so detecting and reacting to mouse clicks is
central. In Qt we catch mouse clicks by detecting the mouseReleaseEvent . To do this for our custom Pos widget we de ne a
handler on the class. This receives QMouseEvent with the information containing what happened. In this case we are only
interested in whether the mouse release occurred from the left or the right mouse button.
For a left mouse click we check whether the tile is agged or already revealed. If it is either, we ignore the click — making
agged tiles 'safe', unable to be click by accident. If the tile is not agged we simply initiation the .click() method (see later).
For a right mouse click, on tiles which are not revealed, we call our .toggle_flag() method to toggle a ag on and off.
https://www.learnpyqt.com/examples/moonsweeper/ 5/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
The .toggle_flag handler simply sets .is_flagged to the inverse of itself ( True becomes False , False becomes True ) having
the effect of toggling it on and off. Note that we have to call .update() to force a redraw having changed the state. We also
emit our custom .clicked signal, which is used to start the timer — because placing a ag should also count as starting, not
just revealing a square.
The .click() method handles a left mouse click, and in turn triggers the reveal of the square. If the number of adjacent mines
to this Pos is zero, we trigger the .expandable signal to begin the process of auto-expanding the region explored (see later).
Finally, we again emit .clicked to signal the start of the game.
Finally, the .reveal() method checks whether the tile is already revealed, and if not sets .is_revealed to True . Again we call
.update() to trigger a repaint of the widget.
The optional emit of the .revealed signal is used only for the endgame full-map reveal. Because each reveal triggers a further
lookup to nd what tiles are also revealable, revealing the entire map would create a large number of redundant callbacks. By
suppressing the signal here we avoid that.
def toggle_flag(self):
self.is_flagged = not self.is_flagged
self.update()
self.clicked.emit()
def click(self):
self.reveal()
if self.adjacent_n == 0:
self.expandable.emit(self.x, self.y)
self.clicked.emit()
if emit:
self.revealed.emit(self)
Finally, we de ne a custom paintEvent method for our Pos widget to handle the display of the current position state. As
described in [chapter] to perform custom paint over a widget canvas we take a QPainter and the event.rect() which provides
the boundaries in which we are to draw — in this case the outer border of the Pos widget.
Revealed tiles are drawn differently depending on whether the tile is a start position, bomb or empty space. The rst two are
represented by icons of a rocket and bomb respectively. These are drawn into the tile QRect using .drawPixmap . Note we need
to convert the QImage constants to pixmaps, by passing through QPixmap by passing.
You might think "why not just store these as QPixmap objects since that's what we're using? Unfortunately you can't
create QPixmap objects before your QApplication is up and running.
For empty positions (not rockets, not bombs) we optionally show the adjacency number if it is larger than zero. To draw text
onto our QPainter we use .drawText() passing in the QRect , alignment ags and the number to draw as a string. We've
de ned a standard color for each number (stored in NUM_COLORS ) for usability.
https://www.learnpyqt.com/examples/moonsweeper/ 6/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
For tiles that are not revealed we draw a tile, by lling a rectangle with light gray and draw a 1 pixel border of darker grey. If
.is_flagged is set, we also draw a ag icon over the top of the tile using drawPixmap and the tile QRect .
r = event.rect()
if self.is_revealed:
if self.is_start:
p.drawPixmap(r, QPixmap(IMG_START))
elif self.is_mine:
p.drawPixmap(r, QPixmap(IMG_BOMB))
else:
p.fillRect(r, QBrush(Qt.lightGray))
pen = QPen(Qt.gray)
pen.setWidth(1)
p.setPen(pen)
p.drawRect(r)
if self.is_flagged:
p.drawPixmap(r, QPixmap(IMG_FLAG))
Mechanics
We commonly need to get all tiles surrounding a given point, so we have a custom function for that purpose. It simple iterates
across a 3x3 grid around the point, with a check to ensure we do not go out of bounds on the grid edges ( 0 ≥ x ≤
self.b_size ). The returned list contains a Pos widget from each surrounding location.
return positions
The expand_reveal method is triggered in response to a click on a tile with zero adjacent mines. In this case we want to
expand the area around the click to any spaces which also have zero adjacent mines, and also reveal any squares around the
border of that expanded area (which aren't mines).
We start with a list to_expand containing the positions to check on the next iteration, a list to_reveal containing the tile
widgets to reveal, and a ag any_added to determine when to exit the loop. The loop stops the rst time no new widgets are
added to to_reveal .
Inside the loop we reset any_added to False , and empty the to_expand list, keeping a temporary store in l for iterating over.
For each x and y location we get the 8 surrounding widgets. If any of these widgets is not a mine, and is not already in the
to_reveal list we add it. This ensures that the edges of the expanded area are all revealed. If the position has no adjacent
mines, we append the coordinates onto to_expand to be checked on the next iteration.
https://www.learnpyqt.com/examples/moonsweeper/ 7/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
By adding any non-mine tiles to to_reveal , and only expanding tiles that are not already in to_reveal , we ensure that we
won't visit a tile more than once.
while any_added:
any_added = False
to_expand, l = [], to_expand
for x, y in l:
positions = self.get_surrounding(x, y)
for w in positions:
if not w.is_mine and w not in to_reveal:
to_reveal.append(w)
if w.adjacent_n == 0:
to_expand.append((w.x,w.y))
any_added = True
Endgames
Endgame states are detected during the reveal process following a click on a title. There are two possible outcomes —
This continues until self.end_game_n reaches zero, which triggers the win game process by calling either game_over or
game_won . Success/failure is triggered by revealing the map and setting the relevant status, in both cases.
else:
self.end_game_n -= 1 # decrement remaining empty spaces
if self.end_game_n == 0:
self.game_won()
def game_over(self):
self.reveal_map()
self.update_status(STATUS_FAILED)
def game_won(self):
self.reveal_map()
self.update_status(STATUS_SUCCESS)
https://www.learnpyqt.com/examples/moonsweeper/ 8/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
Further ideas
If you want to have a go at expanding Moonsweeper, here are a few ideas —
1. Allow the player to take their own rst turn. Try postponing the calculation of mine positions til after the user rst clicks,
and then generate positions until you get a miss.
2. Add power-ups, e.g. a scanner to reveal a certain area of the board automatically.
3. Let the hidden B'ugs move around between each turn. Keep a list of free-unrevealed positions, and allow the B'ugs to
move into them. You'll need to recalculate the adjacencies after each click.
If you want a little more inspiration, see this PR from Keith Hall which modi es startup to be selectable, among other things!
The full source is available for download below, along with installers for Windows, Mac and Linux.
For information on packaging and distributing PyQt5 applications see this section.
Source Code Ubuntu Installer Windows Installer Mac Disk Image
Enjoyed this?
I've also written a book.
Create Simple GUI Applications is my hands-on guide to making desktop apps with Python. Learn everything you
need to build real apps.
Check it out
Table of contents
Running from source
Playing Field
Position Tiles
Mechanics
Endgames
Further ideas
Share
Twitter
Facebook
https://www.learnpyqt.com/examples/moonsweeper/ 9/10
4/15/2020 Build a minesweeper clone in Python, using PyQt5 — Learn PyQt5 GUI programming hands-on
Facebook
Reddit
Discussion
PyQt5 Installation
PyQt5 Courses
PyQt5 Example Apps
Widget Library
PyQt5 tutorial
Latest articles
Write with me
Contact us
Af liate program
Licensing, Privacy & Legal
Learn PyQt — Copyright ©2019-2020 Martin Fitzpatrick Tutorials CC-BY-NC-SA Public code BSD & MIT
Registered in the Netherlands with the KvK 66435102
https://www.learnpyqt.com/examples/moonsweeper/ 10/10