
В первой части мы разобрали, как создать консольную версию «Сапёра» на 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. Если найдёте баги — сообщите, исправлю
Комментарии (7)
urvanov
15.08.2025 19:57Создание игр, кстати, хороший путь к изучению языка. Сам так же всегда делал. Ободряю.
urvanov
А в следующей части добавим лутбоксы, колесо фортуны, коллекционируемые предметы, секреты, скины на мины, скины на флажки, скины на цифры, скины на игровую доску, дерево прокачки, способности, квесты, записки с сюжетом, интервью с разработчиками, дейлики, награды за непрерывный вход, премиум подписки и баттлпасы.
Laborant_Code Автор
ахахахах хорошая идея, еще нужно, чтобы донат решал все и можно выпускать игру в стим