В первой части мы разобрали, как создать консольную версию «Сапёра» на Python. Теперь пришло время сделать полноценное графическое приложение с помощью Tkinter. (Tkinter поможет нам создать оконную игру, которая позволит пользователям взаимодействовать с вашей программой)
Почему именно GUI-версия?
После текстового интерфейса в консоли, логично перейти к визуальному оформлению. Вот что нас ждёт:
1 — Кнопки вместо ввода координат
2 — Визуальные флажки и открытие клеток
3 — Таймер
4 — Полноценный игровой процесс, как в классической версии
Если вы только начинаете работать с GUI в Python — этот проект идеально подойдёт для практики.(буду стараться подробно описать комментариями в коде, если не понятно, то напишите в коммментариях, чтобы обновил статью и сделал её более подробной)
1. Подготовка: что нам понадобится?
Для работы потребуются только стандартные библиотеки Python, и поэтому перед тем, как приступить к написанию, убедитесь, что у вас установлен Python (версия 3.6 или выше), если нет, то вы можете скачать её с официального сайта python.org.
import random import tkinter as tk from tkinter import messagebox, simpledialog from time import time
Архитектура игры:
«Сапёр» состоит из нескольких ключевых компонентов:
-
Главное окно (класс MinesweeperGUI) — основа интерфейса
-
Игровое поле — сетка 9×9 кнопок
-
Логика игры — генерация мин, обработка ходов
-
Интерфейс — меню, таймер, кнопки
2. Создаём основу игры
Создаём класс MinesweeperGUI и настраиваем главное окно:
2.1. Инициализация окна
Метод init выполняет начальную настройку:
-
root— главное окно приложения, которое передается в конструктор. -
Задаем параметры игры, которые можно будет настраивать или изменять.
class MinesweeperGUI: def __init__(self, root): self.root = root self.root.title("Сапёр") # названия окна для игры сапер # Настройки игры self.grid_size = 9 # Стандартный размер поля 9x9 self.max_bombs = 80 # Максимальное допустимое количество мин self.min_bombs = 1 # Минимальное количество мин self.default_bombs = 10 # Количество мин по умолчанию self.bomb_count = self.default_bombs # Текущее количество мин # Состояние игры self.flags = set() # Множество координат клеток с флагами self.correct_flags = set() # Множество правильных флагов self.first_click = True # Флаг первого хода (для мин так, чтобы не было ошибок, как в первой статье) self.game_started = False # Флаг запущенной игры (для таймера) self.start_time = 0 # Время начала игры # Элементы интерфейса (таймер) self.timer_label = None self.buttons = [] # Инициализация интерфейса self.create_menu() self.create_game_interface() self.init_game()
2.2. Добавляем меню
Метод create_menu() добавляет стандартное меню с пунктами:
-
Новая игра (вызывает
start_new_game()) -
Настройки (вызывает
change_settings())
def create_menu(self): """Создает меню игры""" menubar = tk.Menu(self.root) game_menu = tk.Menu(menubar, tearoff=0) game_menu.add_command(label="Новая игра", command=self.start_new_game) game_menu.add_command(label="Настройки", command=self.change_settings) game_menu.add_separator() # Разделитель между пунктами game_menu.add_command(label="Выход", command=self.root.quit) menubar.add_cascade(label="Игра", menu=game_menu) self.root.config(menu=menubar) def create_game_interface(self): """Создает игровой интерфейс (таймер и поле)""" # Создаем таймер self.timer_label = tk.Label( self.root, text="Время: 0 сек", font=('Calibri', 12) ) self.timer_label.grid(row=0, column=0, columnspan=self.grid_size, sticky="ew") # Создаем кнопки для клеток self.create_buttons() def create_buttons(self): """Создает кнопки игрового поля""" # Создаем новые кнопки self.buttons = [] # Очищаем массив кнопок for row in range(self.grid_size): button_row = [] # Создаем ряд кнопок for col in range(self.grid_size): btn = tk.Button( self.root, text=' ', # Пустой текст по умолчанию width=3, height=1, # Размер кнопки font=('Calibri', 12, 'bold'), command=lambda r=row, c=col: self.on_left_click(r, c) ) btn.bind('<Button-3>', lambda event, r=row, c=col: self.on_right_click(r, c)) btn.grid(row=row + 1, column=col, sticky='nsew') # +1 чтобы пропустить строку с таймером button_row.append(btn) self.buttons.append(button_row)
3. Игровая логика
3.1. Генерация мин после первого хода
Чтобы игра не заканчивалась сразу, мины генерируем только после первого клика, исключая саму клетку и соседей:
Ключевые методы:
-
place_mines(first_row, first_col)— размещает мины, избегая зоны 3×3 вокруг первого клика -
count_adjacent_mines(row, col)— считает мины вокруг указанной клетки -
create_buttons()— создаёт сетку интерактивных кнопок
def place_mines(self, first_row, first_col): """Размещает мины на поле, избегая первой клетки и соседей""" # Создаем безопасную зону 3x3 вокруг первого клика safe_zone = set() for r in range(first_row - 1, first_row + 2): for c in range(first_col - 1, first_col + 2): # Проверяем, что координаты находятся в пределах поля if 0 <= r < self.grid_size and 0 <= c < self.grid_size: safe_zone.add((r, c)) # Генерируем мины, избегая безопасной зоны self.mines = set() # Множество для хранения координат мин while len(self.mines) < self.bomb_count: r, c = random.randint(0, self.grid_size - 1), random.randint(0, self.grid_size - 1) # randint вручную задаёте диапазон, и из него в консоль выводится случайное целое число. if (r, c) not in safe_zone: self.mines.add((r, c)) self.hidden_map[r][c] = 'M' # Заполняем поле числами (количество мин вокруг каждой клетки) for r in range(self.grid_size): for c in range(self.grid_size): if self.hidden_map[r][c] != 'M': count = self.count_adjacent_mines(r, c) if count > 0: self.hidden_map[r][c] = str(count) def count_adjacent_mines(self, row, col): """Считает количество мин вокруг указанной клетки""" count = 0 # Проверяем все соседние клетки (3x3 область) for r in range(row - 1, row + 2): for c in range(col - 1, col + 2): if (0 <= r < self.grid_size and 0 <= c < self.grid_size and self.hidden_map[r][c] == 'M'): count += 1 return count
3.2. Обработка кликов
Ключевые методы:
-
on_left_click(row, col)— обрабатывает открытие клетки. -
on_right_click(row, col)— обработка флагов. -
reveal_cells(row, col)— рекурсивно открывает соседние пустые клетки
def on_left_click(self, row, col): """Обрабатывает левый клик мыши (открытие клетки)""" if (row, col) in self.flags: # Не открываем помеченные флажками клетки return # Если это первый ход - генерируем мины if self.first_click: self.place_mines(row, col) self.start_game_timer() self.first_click = False # Если наступили на мину - конец игры if self.hidden_map[row][col] == 'M': self.game_over(False) return # Открываем клетку/область self.reveal_cells(row, col) self.update_buttons() if self.check_win(): self.game_over(True) def on_right_click(self, row, col): """Обрабатывает правый клик мыши (установка/снятие флага)""" if self.player_map[row][col] != '-': # Не ставим флаги на открытые клетки return # Если флаг уже стоит - убираем его if (row, col) in self.flags: self.flags.remove((row, col)) if (row, col) in self.correct_flags: self.correct_flags.remove((row, col)) else: # Ставим новый флаг self.flags.add((row, col)) if self.hidden_map[row][col] == 'M': self.correct_flags.add((row, col)) self.update_buttons() # Проверяем победу (все мины правильно помечены) if (len(self.correct_flags) == self.bomb_count and len(self.flags) == self.bomb_count): self.game_over(True) def reveal_cells(self, row, col): """Рекурсивно открывает клетки""" # Проверяем, что координаты в пределах поля и клетка не открыта if not (0 <= row < self.grid_size and 0 <= col < self.grid_size) or self.player_map[row][col] != '-': return self.player_map[row][col] = self.hidden_map[row][col] # Если клетка пустая, открываем соседей if self.hidden_map[row][col] == ' ': for r in range(row - 1, row + 2): for c in range(col - 1, col + 2): if r != row or c != col: # Не проверяем саму клетку self.reveal_cells(r, c)
4. Визуальные улучшения
4.1. Добавляем таймер
Чтобы игрок видел, сколько времени он играет: Методы Таймер (start_game_timer(), update_timer() — Запускается при первом ходе)
def start_game_timer(self): """Запускает таймер игры""" self.game_started = True self.start_time = time() self.update_timer() def update_timer(self): """Обновляет отображение таймера""" if self.game_started: elapsed = int(time() - self.start_time) # прошедшее время self.timer_label.config(text=f"Время: {elapsed} сек") self.root.after(1000, self.update_timer) else: self.timer_label.config(text="Время: 0 сек")
4.2. Подсветка чисел
Как в оригинальной игре, цифры будут разного цвета:
def update_buttons(self): """Обновляет внешний вид кнопок в соответствии с состоянием игры""" for row in range(self.grid_size): for col in range(self.grid_size): if (row, col) in self.flags: self.buttons[row][col].config(text='🚩', fg='red', bg='SystemButtonFace') elif self.player_map[row][col] == '-': self.buttons[row][col].config(text=' ', bg='SystemButtonFace') else: cell = self.player_map[row][col] self.buttons[row][col].config(text=cell, bg='light gray') # Разные цвета для цифр if cell.isdigit(): num = int(cell) colors = ['', 'blue', 'green', 'red', 'dark blue', 'brown', 'teal', 'black', 'gray'] self.buttons[row][col].config(fg=colors[num])
5. Завершение игры
При поражении или победе показываем все мины и выводим сообщение:
-
start_new_game()— сбрасывает состояние и начинает заново -
check_win()— проверяет условия победы
def check_win(self): """Проверяет условия победы""" # Все безопасные клетки должны быть открыты for row in range(self.grid_size): for col in range(self.grid_size): if self.hidden_map[row][col] != 'M' and self.player_map[row][col] == '-': return False return True def game_over(self, won): """Обрабатывает завершение игры""" self.game_started = False # Показываем все мины for row in range(self.grid_size): for col in range(self.grid_size): if self.hidden_map[row][col] == 'M': self.buttons[row][col].config( text='💣', bg='light green' if won else 'orange' ) # Отключаем все кнопки for row in range(self.grid_size): for col in range(self.grid_size): self.buttons[row][col].config(state='disabled') # Показываем сообщение if won: elapsed = int(time() - self.start_time) messagebox.showinfo("Победа!", f"Поздравляем! Вы выиграли за {elapsed} секунд!") else: messagebox.showinfo("Поражение", "Вы наступили на мину!") def start_new_game(self): """Начинает новую игру""" self.init_game()
6.Настройки игры и запус:
change_settings() — позволяет изменить количество мин
def change_settings(self): """Изменяет настройки игры (количество мин)""" # simpledialog - это модуль, который предоставляет удобные функции для создания стандартных диалоговых окон bombs = simpledialog.askinteger( "Количество мин", f"Введите количество мин ({self.min_bombs}-{self.max_bombs}):", parent=self.root, minvalue=self.min_bombs, maxvalue=self.max_bombs, initialvalue=self.bomb_count ) if bombs is not None: # Если пользователь не нажал "Отмена" self.bomb_count = bombs self.start_new_game()
# запуск игры if __name__ == "__main__": root = tk.Tk() game = MinesweeperGUI(root) root.mainloop()
Итог: Что мы получили?
6. Что можно улучшить?
Наш «Сапёр» уже полностью играбелен, но есть куда расти:
☆ Добавить уровни сложности (размер поля и количество мин)
☆ Реализовать таблицу рекордов по времени
☆ Сделать анимацию взрыва при поражении или победе
☆ Добавить звуковые эффекты
→ Полный код доступен на GitHub
→ Первая часть (консольная версия) — здесь
Пишите идеи в комментарии! Ваш вариант кода может попасть в обновлённую версию статьи, как было с первой.
P.S. Если найдёте баги — сообщите, исправлю
ссылка на оригинал статьи https://habr.com/ru/articles/937688/
Добавить комментарий