Skip to content

Minesweeper - complete features #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
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_()