Skip to content
180 changes: 124 additions & 56 deletions minesweeper/minesweeper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import random
import time
import sys

IMG_BOMB = QImage("./images/bug.png")
IMG_FLAG = QImage("./images/flag.png")
Expand Down Expand Up @@ -42,7 +43,9 @@

class Pos(QWidget):
expandable = pyqtSignal(int, int)
expandable_safe = pyqtSignal(int, int)
clicked = pyqtSignal()
flagged = pyqtSignal(bool)
ohno = pyqtSignal()

def __init__(self, x, y, *args, **kwargs):
Expand All @@ -60,6 +63,7 @@ def reset(self):

self.is_revealed = False
self.is_flagged = False
self.is_end = False

self.update()

Expand All @@ -72,6 +76,8 @@ def paintEvent(self, event):
if self.is_revealed:
color = self.palette().color(QPalette.Background)
outer, inner = color, color
if self.is_end or (self.is_flagged and not self.is_mine):
inner = NUM_COLORS[1]
else:
outer, inner = Qt.gray, Qt.lightGray

Expand Down Expand Up @@ -99,41 +105,54 @@ def paintEvent(self, event):
elif self.is_flagged:
p.drawPixmap(r, QPixmap(IMG_FLAG))

def flag(self):
self.is_flagged = True
def toggle_flag(self):
self.is_flagged = not self.is_flagged
self.update()
self.flagged.emit(self.is_flagged)

self.clicked.emit()

def reveal(self):
def reveal_self(self):
self.is_revealed = True
self.update()

def click(self):
def reveal(self):
if not self.is_revealed:
self.reveal()
self.reveal_self()
if self.adjacent_n == 0:
self.expandable.emit(self.x, self.y)

self.clicked.emit()
if self.is_mine:
self.is_end = True
self.ohno.emit()

def mouseReleaseEvent(self, e):
def click(self):
if not self.is_revealed and not self.is_flagged:
self.reveal()

if (e.button() == Qt.RightButton and not self.is_revealed):
self.flag()
def mouseReleaseEvent(self, e):
self.clicked.emit()
if e.button() == Qt.RightButton:
if not self.is_revealed:
self.toggle_flag()
else:
self.expandable_safe.emit(self.x, self.y)

elif (e.button() == Qt.LeftButton):
elif e.button() == Qt.LeftButton:
self.click()
self.clicked.emit()

if self.is_mine:
self.ohno.emit()


class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)

self.b_size, self.n_mines = LEVELS[1]

app = QApplication.instance()
app_args = app.arguments()

self.level = int(app_args[1]) if len(app_args) == 2 and app_args[1].isnumeric() else 1
if self.level < 0 or self.level > len(LEVELS):
raise ValueError('level out of bounds')
self.b_size, self.n_mines = LEVELS[self.level]

w = QWidget()
hb = QHBoxLayout()
Expand All @@ -154,9 +173,6 @@ def __init__(self, *args, **kwargs):
self._timer.timeout.connect(self.update_timer)
self._timer.start(1000) # 1 second timer

self.mines.setText("%03d" % self.n_mines)
self.clock.setText("000")

self.button = QPushButton()
self.button.setFixedSize(QSize(32, 32))
self.button.setIconSize(QSize(32, 32))
Expand Down Expand Up @@ -195,6 +211,7 @@ def __init__(self, *args, **kwargs):
self.reset_map()
self.update_status(STATUS_READY)

self.setWindowTitle("Moonsweeper")
self.show()

def init_map(self):
Expand All @@ -206,14 +223,18 @@ def init_map(self):
# Connect signal to handle expansion.
w.clicked.connect(self.trigger_start)
w.expandable.connect(self.expand_reveal)
w.expandable_safe.connect(self.expand_reveal_if_looks_safe)
w.flagged.connect(self.flag_toggled)
w.ohno.connect(self.game_over)

def reset_map(self):
self.n_mines = LEVELS[self.level][1]
self.mines.setText("%03d" % self.n_mines)
self.clock.setText("000")

# 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()
for _, _, w in self.get_all():
w.reset()

# Add mines to the positions
positions = []
Expand All @@ -225,38 +246,38 @@ def reset_map(self):
positions.append((x, y))

def get_adjacency_n(x, y):
positions = self.get_surrounding(x, y)
positions = [w for _, _, w in self.get_surrounding(x, y)]
n_mines = sum(1 if w.is_mine else 0 for w in positions)

return n_mines

# Add adjacencies to the positions
for x, y, w in self.get_all():
w.adjacent_n = get_adjacency_n(x, y)

# Place starting marker - we don't want to start on a mine
# or adjacent to a mine because the start marker will hide the adjacency number.
no_adjacent = [(x, y, w) for x, y, w in self.get_all() if not w.adjacent_n and not w.is_mine]
idx = random.randint(0, len(no_adjacent) - 1)
x, y, w = no_adjacent[idx]
w.is_start = True

# 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()

def get_all(self):
for x in range(0, self.b_size):
for y in range(0, self.b_size):
w = self.grid.itemAtPosition(y, x).widget()
w.adjacent_n = get_adjacency_n(x, y)

# 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 (x, y) not in positions:
w = self.grid.itemAtPosition(y, x).widget()
w.is_start = True

# 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
yield (x, y, self.grid.itemAtPosition(y, x).widget())

def get_surrounding(self, x, y):
positions = []

for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
positions.append(self.grid.itemAtPosition(yi, xi).widget())
positions.append((xi, yi, self.grid.itemAtPosition(yi, xi).widget()))

return positions

Expand All @@ -265,29 +286,51 @@ def button_pressed(self):
self.update_status(STATUS_FAILED)
self.reveal_map()

elif self.status == STATUS_FAILED:
elif self.status in (STATUS_FAILED, STATUS_SUCCESS):
self.update_status(STATUS_READY)
self.reset_map()

def reveal_map(self):
for x in range(0, self.b_size):
for y in range(0, self.b_size):
w = self.grid.itemAtPosition(y, x).widget()
w.reveal()

def expand_reveal(self, x, y):
for xi in range(max(0, x - 1), min(x + 2, self.b_size)):
for yi in range(max(0, y - 1), min(y + 2, self.b_size)):
w = self.grid.itemAtPosition(yi, xi).widget()
if not w.is_mine:
w.click()
for _, _, w in self.get_all():
# don't reveal correct flags
if not (w.is_flagged and w.is_mine):
w.reveal_self()

def get_revealable_around(self, x, y, force=False):
for xi, yi, w in self.get_surrounding(x, y):
if (force or not w.is_mine) and not w.is_flagged and not w.is_revealed:
yield (xi, yi, w)

def expand_reveal(self, x, y, force=False):
for _, _, w in self.get_revealable_around(x, y, force):
w.reveal()

def determine_revealable_around_looks_safe(self, x, y, existing):
flagged_count = 0
for _, _, w in self.get_surrounding(x, y):
if w.is_flagged:
flagged_count += 1
w = self.grid.itemAtPosition(y, x).widget()
if flagged_count == w.adjacent_n:
for xi, yi, w in self.get_revealable_around(x, y, True):
if (xi, yi) not in ((xq, yq) for xq, yq, _ in existing):
existing.append((xi, yi, w))
self.determine_revealable_around_looks_safe(xi, yi, existing)

def expand_reveal_if_looks_safe(self, x, y):
reveal = []
self.determine_revealable_around_looks_safe(x, y, reveal)
for _, _, w in reveal:
w.reveal()

def trigger_start(self, *args):
if self.status != STATUS_PLAYING:
if self.status == STATUS_READY:
# First click.
self.update_status(STATUS_PLAYING)
# Start timer.
self._timer_start_nsecs = int(time.time())
elif self.status == STATUS_PLAYING:
self.check_win_condition()

def update_status(self, status):
self.status = status
Expand All @@ -302,8 +345,33 @@ def game_over(self):
self.reveal_map()
self.update_status(STATUS_FAILED)

def flag_toggled(self, flagged):
adjustment = -1 if flagged else 1
self.n_mines += adjustment
self.mines.setText("%03d" % self.n_mines)
#self.check_win_condition()

def check_win_condition(self):
if self.n_mines == 0:
if all(w.is_revealed or w.is_flagged for _, _, w in self.get_all()):
self.update_status(STATUS_SUCCESS)
else:
# if the only unrevealed squares are mines
unrevealed = []
for _, _, w in self.get_all():
if not w.is_revealed and not w.is_flagged:
unrevealed.append(w)
if len(unrevealed) > self.n_mines or not w.is_mine:
return
if len(unrevealed) == self.n_mines:
# check that all the existing flags are correct, then no need to flag the unrevealed squares manually, the player wins
if all(w.is_flagged == w.is_mine or w in unrevealed for _, _, w in self.get_all()):
for w in unrevealed:
w.toggle_flag()
self.update_status(STATUS_SUCCESS)


if __name__ == '__main__':
app = QApplication([])
app = QApplication(sys.argv)
window = MainWindow()
app.exec_()