From a2fae40cd8f83b1f68bcea8cf1e33d5abc5abcac Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Wed, 26 Sep 2018 15:22:02 +0300 Subject: [PATCH 1/9] Minesweeper: allow flags to be toggled --- minesweeper/minesweeper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index ab213fc..5fb15da 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -99,8 +99,8 @@ 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.clicked.emit() @@ -120,7 +120,7 @@ def click(self): def mouseReleaseEvent(self, e): if (e.button() == Qt.RightButton and not self.is_revealed): - self.flag() + self.toggle_flag() elif (e.button() == Qt.LeftButton): self.click() From 97b30a5cae65cb2a28aa277c567f966ca7fb5a80 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Wed, 26 Sep 2018 17:29:18 +0300 Subject: [PATCH 2/9] Minesweeper: correctly recognize when the player has won * also allow right clicking on a revealed cell/number whose adjacent number of flags match the number to reveal more cells quickly * set the window title * allow the user to specify the level (size) on the command line --- minesweeper/minesweeper.py | 114 ++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index 5fb15da..667ace1 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -4,6 +4,7 @@ import random import time +import sys IMG_BOMB = QImage("./images/bug.png") IMG_FLAG = QImage("./images/flag.png") @@ -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): @@ -102,38 +105,50 @@ def paintEvent(self, event): 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.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.toggle_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() @@ -154,9 +169,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)) @@ -195,6 +207,7 @@ def __init__(self, *args, **kwargs): self.reset_map() self.update_status(STATUS_READY) + self.setWindowTitle("Minesweeper") self.show() def init_map(self): @@ -206,14 +219,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 = [] @@ -231,10 +248,8 @@ def get_adjacency_n(x, y): return n_mines # Add adjacencies to the 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.adjacent_n = get_adjacency_n(x, y) + for x, y, w in self.get_all(): + w.adjacent_n = get_adjacency_n(x, y) # Place starting marker while True: @@ -242,7 +257,6 @@ def get_adjacency_n(x, y): 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. @@ -251,6 +265,11 @@ def get_adjacency_n(x, y): w.click() break + def get_all(self): + for x in range(0, self.b_size): + for y in range(0, self.b_size): + yield (x, y, self.grid.itemAtPosition(y, x).widget()) + def get_surrounding(self, x, y): positions = [] @@ -265,29 +284,36 @@ 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() + for _, _, w in self.get_all(): + w.reveal_self() + + def expand_reveal(self, x, y, force=False): + for w in self.get_surrounding(x, y): + if (force or not w.is_mine) and not w.is_flagged: 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() + def expand_reveal_if_looks_safe(self, x, y): + 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: + self.expand_reveal(x, y, True) # TODO: reveal all where flags match the adjacents, not just those surrounding this tile and its relatives with 0 adjacents 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 @@ -302,8 +328,20 @@ 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) + # TODO: if the only unrevealed squares are mines, then no need to flag them, the player wins + if __name__ == '__main__': - app = QApplication([]) + app = QApplication(sys.argv) window = MainWindow() app.exec_() From e6713c241f838fe5f8e12ba940c599cd2197d513 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Thu, 27 Sep 2018 09:12:40 +0300 Subject: [PATCH 3/9] Minesweeper: highlight the mine that killed the player --- minesweeper/minesweeper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index 667ace1..4ca1d2f 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -63,6 +63,7 @@ def reset(self): self.is_revealed = False self.is_flagged = False + self.is_end = False self.update() @@ -75,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: + inner = NUM_COLORS[1] else: outer, inner = Qt.gray, Qt.lightGray @@ -118,6 +121,7 @@ def reveal(self): self.expandable.emit(self.x, self.y) if self.is_mine: + self.is_end = True self.ohno.emit() def click(self): From d1a465a0360518feee2ba0be3a228fb07af7dc39 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Thu, 27 Sep 2018 09:45:58 +0300 Subject: [PATCH 4/9] Minesweeper: more useful auto reveal on right click revealed cell --- minesweeper/minesweeper.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index 4ca1d2f..8d601bd 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -246,7 +246,7 @@ 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 @@ -264,7 +264,7 @@ def get_adjacency_n(x, y): w.is_start = True # Reveal all positions around this, if they are not mines either. - for w in self.get_surrounding(x, y): + for _, _, w in self.get_surrounding(x, y): if not w.is_mine: w.click() break @@ -279,7 +279,7 @@ def get_surrounding(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)): - positions.append(self.grid.itemAtPosition(yi, xi).widget()) + positions.append((xi, yi, self.grid.itemAtPosition(yi, xi).widget())) return positions @@ -296,19 +296,32 @@ def reveal_map(self): for _, _, w in self.get_all(): w.reveal_self() - def expand_reveal(self, x, y, force=False): - for w in self.get_surrounding(x, y): + 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: - w.reveal() + yield (xi, yi, w) - def expand_reveal_if_looks_safe(self, x, y): + 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): + 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: - self.expand_reveal(x, y, True) # TODO: reveal all where flags match the adjacents, not just those surrounding this tile and its relatives with 0 adjacents + 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_READY: From 9beebb03c551db0bc50c422d94a4e338fcecdf31 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Thu, 27 Sep 2018 13:04:27 +0300 Subject: [PATCH 5/9] Minesweeper: mark incorrectly flagged mines at game over also mark the game as won if all the non-mine squares have been revealed --- minesweeper/minesweeper.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index 8d601bd..32c64f2 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -76,7 +76,7 @@ def paintEvent(self, event): if self.is_revealed: color = self.palette().color(QPalette.Background) outer, inner = color, color - if self.is_end: + if self.is_end or (self.is_flagged and not self.is_mine): inner = NUM_COLORS[1] else: outer, inner = Qt.gray, Qt.lightGray @@ -294,7 +294,9 @@ def button_pressed(self): def reveal_map(self): for _, _, w in self.get_all(): - w.reveal_self() + # 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): @@ -355,7 +357,20 @@ 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) - # TODO: if the only unrevealed squares are mines, then no need to flag them, the player wins + 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__': From 245aee6de9a5ffdc630db44917ed71f82bdb787a Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Thu, 27 Sep 2018 14:33:48 +0300 Subject: [PATCH 6/9] Minesweeper: only auto reveal from unrevealed cells --- minesweeper/minesweeper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index 32c64f2..d38a450 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -300,7 +300,7 @@ def reveal_map(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: + 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): From 1ab4be1a05d616f3e95814a7355b5a5e49dd9fcb Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 28 Sep 2018 17:14:25 +0300 Subject: [PATCH 7/9] Minesweeper: correct name in window title to Moonsweeper --- minesweeper/minesweeper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index d38a450..ebe6e65 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -211,7 +211,7 @@ def __init__(self, *args, **kwargs): self.reset_map() self.update_status(STATUS_READY) - self.setWindowTitle("Minesweeper") + self.setWindowTitle("Moonsweeper") self.show() def init_map(self): From cadd6b83b8a0645392b605036c7dbbd7bd3801fe Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sun, 30 Sep 2018 08:41:12 +0300 Subject: [PATCH 8/9] Minesweeper: don't start on a tile with adjacent mines as the rocket otherwise covers the number --- minesweeper/minesweeper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index ebe6e65..d37cb28 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -260,7 +260,7 @@ def get_adjacency_n(x, y): 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: + if (x, y) not in positions and not w.adjacent_n: w.is_start = True # Reveal all positions around this, if they are not mines either. From 4d20d8ff431f4483a35ff6aa299f752855e26bd7 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Mon, 1 Oct 2018 09:30:09 +0300 Subject: [PATCH 9/9] Minesweeper: make start tile logic more reliable --- minesweeper/minesweeper.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index d37cb28..7faf9ab 100644 --- a/minesweeper/minesweeper.py +++ b/minesweeper/minesweeper.py @@ -255,19 +255,17 @@ def get_adjacency_n(x, y): for x, y, w in self.get_all(): 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 and not w.adjacent_n: - 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 + # 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):