diff --git a/minesweeper/minesweeper.py b/minesweeper/minesweeper.py index ab213fc..7faf9ab 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): @@ -60,6 +63,7 @@ def reset(self): self.is_revealed = False self.is_flagged = False + self.is_end = False self.update() @@ -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 @@ -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() @@ -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)) @@ -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): @@ -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 = [] @@ -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 @@ -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 @@ -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_()