От консоли к GUI: Как написать игру «Сапёр» на Python с нуля версия GUI (часть вторая)

от автора

игра «Сапёра»

игра «Сапёра»

В первой части мы разобрали, как создать консольную версию «Сапёра» на 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

Архитектура игры:

«Сапёр» состоит из нескольких ключевых компонентов:

  1. Главное окно (класс MinesweeperGUI) — основа интерфейса

  2. Игровое поле — сетка 9×9 кнопок

  3. Логика игры — генерация мин, обработка ходов

  4. Интерфейс — меню, таймер, кнопки

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. Генерация мин после первого хода

Чтобы игра не заканчивалась сразу, мины генерируем только после первого клика, исключая саму клетку и соседей:

Ключевые методы:

  1. place_mines(first_row, first_col) — размещает мины, избегая зоны 3×3 вокруг первого клика

  2. count_adjacent_mines(row, col) — считает мины вокруг указанной клетки

  3. 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. Завершение игры

При поражении или победе показываем все мины и выводим сообщение:

  1. start_new_game() — сбрасывает состояние и начинает заново

  2. 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *