from PyQt5.QtGui import * from PyQt5.QtWidgets import * from PyQt5.QtCore import * import random import time IMG_BOMB = QImage("./images/bug.png") IMG_FLAG = QImage("./images/flag.png") IMG_START = QImage("./images/rocket.png") IMG_CLOCK = QImage("./images/clock-select.png") NUM_COLORS = { 1: QColor('#f44336'), 2: QColor('#9C27B0'), 3: QColor('#3F51B5'), 4: QColor('#03A9F4'), 5: QColor('#00BCD4'), 6: QColor('#4CAF50'), 7: QColor('#E91E63'), 8: QColor('#FF9800') } LEVELS = [ (8, 10), (16, 40), (24, 99) ] STATUS_READY = 0 STATUS_PLAYING = 1 STATUS_FAILED = 2 STATUS_SUCCESS = 3 STATUS_ICONS = { STATUS_READY: "./images/plus.png", STATUS_PLAYING: "./images/smiley.png", STATUS_FAILED: "./images/cross.png", STATUS_SUCCESS: "./images/smiley-lol.png", } class Pos(QWidget): expandable = pyqtSignal(int, int) clicked = pyqtSignal() ohno = pyqtSignal() def __init__(self, x, y, *args, **kwargs): super(Pos, self).__init__(*args, **kwargs) self.setFixedSize(QSize(20, 20)) self.x = x self.y = y 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() def paintEvent(self, event): p = QPainter(self) p.setRenderHint(QPainter.Antialiasing) r = event.rect() if self.is_revealed: color = self.palette().color(QPalette.Background) outer, inner = color, color else: outer, inner = Qt.gray, Qt.lightGray p.fillRect(r, QBrush(inner)) pen = QPen(outer) pen.setWidth(1) p.setPen(pen) p.drawRect(r) if self.is_revealed: if self.is_start: p.drawPixmap(r, QPixmap(IMG_START)) elif self.is_mine: p.drawPixmap(r, QPixmap(IMG_BOMB)) elif self.adjacent_n > 0: pen = QPen(NUM_COLORS[self.adjacent_n]) p.setPen(pen) f = p.font() f.setBold(True) p.setFont(f) p.drawText(r, Qt.AlignHCenter | Qt.AlignVCenter, str(self.adjacent_n)) elif self.is_flagged: p.drawPixmap(r, QPixmap(IMG_FLAG)) def flag(self): self.is_flagged = True self.update() self.clicked.emit() def reveal(self): self.is_revealed = True self.update() def click(self): if not self.is_revealed: self.reveal() if self.adjacent_n == 0: self.expandable.emit(self.x, self.y) self.clicked.emit() def mouseReleaseEvent(self, e): if (e.button() == Qt.RightButton and not self.is_revealed): self.flag() elif (e.button() == Qt.LeftButton): self.click() 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] w = QWidget() hb = QHBoxLayout() self.mines = QLabel() self.mines.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.clock = QLabel() self.clock.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) f = self.mines.font() f.setPointSize(24) f.setWeight(75) self.mines.setFont(f) self.clock.setFont(f) self._timer = QTimer() 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)) self.button.setIcon(QIcon("./images/smiley.png")) self.button.setFlat(True) self.button.pressed.connect(self.button_pressed) l = QLabel() l.setPixmap(QPixmap.fromImage(IMG_BOMB)) l.setAlignment(Qt.AlignRight | Qt.AlignVCenter) hb.addWidget(l) hb.addWidget(self.mines) hb.addWidget(self.button) hb.addWidget(self.clock) l = QLabel() l.setPixmap(QPixmap.fromImage(IMG_CLOCK)) l.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) hb.addWidget(l) vb = QVBoxLayout() vb.addLayout(hb) self.grid = QGridLayout() self.grid.setSpacing(5) vb.addLayout(self.grid) w.setLayout(vb) self.setCentralWidget(w) self.init_map() self.update_status(STATUS_READY) self.reset_map() self.update_status(STATUS_READY) self.show() 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.expandable.connect(self.expand_reveal) w.ohno.connect(self.game_over) def reset_map(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() # Add mines to the 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)) def get_adjacency_n(x, y): positions = 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 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 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()) return positions def button_pressed(self): if self.status == STATUS_PLAYING: self.update_status(STATUS_FAILED) self.reveal_map() elif self.status == STATUS_FAILED: 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() def trigger_start(self, *args): if self.status != STATUS_PLAYING: # First click. self.update_status(STATUS_PLAYING) # Start timer. self._timer_start_nsecs = int(time.time()) def update_status(self, status): self.status = status self.button.setIcon(QIcon(STATUS_ICONS[self.status])) def update_timer(self): if self.status == STATUS_PLAYING: n_secs = int(time.time()) - self._timer_start_nsecs self.clock.setText("%03d" % n_secs) def game_over(self): self.reveal_map() self.update_status(STATUS_FAILED) if __name__ == '__main__': app = QApplication([]) window = MainWindow() app.exec_()