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

от автора

Всем привет! Если вы когда-нибудь задумывались, как написать свою игру «Сапёр» с нуля, то эта статья именно для вас. Мы разберёмся в том, как создать простую текстовую версию этой классической игры на языке Python. Здесь не потребуется никаких особых знаний — просто следуйте пошаговым инструкциям, и вы самостоятельно создадите свою уникальную версию «Сапёра».

Что такое «Сапёр» и почему именно эта игра?

Пример игры "Сапёр"

Пример игры «Сапёр»

Игра «Сапёр» — это популярная логическая игра, в которой игрок открывает клетки на поле, пытаясь избежать мин и угадывая, где они расположены. Считаю, что это идеальный проект для изучения Python:

  • Простые правила

  • Отличный способ освоить рекурсию и работу с GUI (ps в будущем)

  • Наглядный результат, который можно улучшать бесконечно

Подготовка к разработке

Перед тем как приступить к кодированию, убедитесь, что у вас установлен Python (версия 3.6 или выше). Вы можете скачать её с официального сайта python.org.

Шаг 1: Генерация карты

Первым делом нам нужно создать карту, заполненную пустыми клетками и минами. Для этого мы напишем функцию Generator_Map, которая будет генерировать игровое поле заданного размера и случайным образом размещать на нём мины. (буду стараться подробно описать комментариями в коде, если не понятно, то напишите в коммментариях, чтобы обновил статью и сделал её более подробной)

import random # Импорт модуля для работы со случайными числами    def Generator_Map(mines):     """Метод создания карты"""     # Задаем размер игрового поля (классический сапёр - 9x9)     size = 9     # Создаем пустое игровое поле, заполненное пробелами     # Пробел означает пустую клетку без мин и чисел     map = [[' ' for _ in range(size)] for _ in range(size)]     mines_count = mines      mines = set()  # убрать дубликаты     # Генерация случайных мин     while len(mines) < mines_count:         # Генерируем случайные координаты в пределах поля         x, y = random.randint(0, size - 1), random.randint(0, size - 1)  # возвращает случайное число N, где a ≤ N ≤ b         mines.add((x, y))         # Помечаем клетку с миной         map[x][y] = 'M'      # Заполняем числами (количество мин вокруг) - список смещений для проверки 8 соседних клеток:     directions = [(-1, -1), (-1, 0), (-1, 1),# Верхние соседи                   (0, -1), (0, 1),           # Боковые соседи                   (1, -1), (1, 0), (1, 1)]   # Нижние соседи     # Проходим по всем клеткам поля для заполнения чисел     for x in range(size):         for y in range(size):             if map[x][y] != 'M':                 count = 0 # Счетчик мин вокруг                 for dx, dy in directions:                     nx, ny = x + dx, y + dy                     if 0 <= nx < size and 0 <= ny < size:                         if map[nx][ny] == 'M':                             count += 1                 if count > 0:                     map[x][y] = str(count)      map_mines = [[' '] + list(range(1, size + 1))]     for i in range(size):         map_mines.append([i + 1] + map[i])     # Возвращаем готовое игровое поле     return map_mines

Шаг 2: Отображение карты

Теперь, когда у нас есть карта, необходимо научиться её отображать. Для этого мы напишем функцию print_map, которая будет выводить текущее состояние игры.

def print_map(map, flags=None):     """Метод прорисовки игрового поля в консоли"""     if flags is None:         flags = set()     # Перебираем строки игрового поля с их индексами     for i, row in enumerate(map):         printed_row = []         for j, cell in enumerate(row):            # Проверяем, стоит ли флаг на текущей позиции (с учетом смещения)             if (i - 1, j - 1) in flags and cell == 'M':                 printed_row.append('F')                 # Если флаг стоит на мине - показываем 'F' (правильная пометка)             elif (i - 1, j - 1) in flags:                # Если флаг стоит не на мине - показываем '?' (ошибка игрока)                 printed_row.append('?')             else:               # Для всех остальных случаев выводим содержимое ячейки как есть                 printed_row.append(str(cell))         # Собираем строку из элементов, разделяя их пробелами и выводим         print(' '.join(printed_row))

Шаг 3: Обработка пользовательского ввода

Для того чтобы игрок мог взаимодействовать с игрой, нам нужно реализовать функции get_bomb_count, которые будут запрашивать у него количество мин.

def get_bomb_count():     """Метод запроса у пользователя количества мин"""     while True:         count_of_bomb = int(input("Введите одно число - количество мин от 1-80 шт.\n"))         if count_of_bomb < 1 or count_of_bomb > 80:             continue         return count_of_bomb         break

Шаг 4: Логика открытия клеток

Сердце игры — функция reveal_cells(), которая реализует рекурсивное открытие пустых областей. Вот как это работает:

def reveal_cells(map, player_map, display_map, x, y, size):     """Рекурсивно открывает все соседние пустые клетки"""     # Проверяем, что координаты в пределах поля и клетка еще не открыта     if not (0 <= x < size and 0 <= y < size) or player_map[x][y] != '-':         return      # Открываем текущую клетку     player_map[x][y] = map[x + 1][y + 1]  # +1 из-за заголовков в map     display_map[x + 1][y + 1] = player_map[x][y]  # Обновляем отображаемую карту      # Если клетка пустая (не число и не мина), открываем соседей     if player_map[x][y] == ' ':         directions = [(-1, -1), (-1, 0), (-1, 1),                       (0, -1), (0, 1),                       (1, -1), (1, 0), (1, 1)]          for dx, dy in directions:             reveal_cells(map, player_map, display_map, x + dx, y + dy, size)

Шаг 5: Обработка флагов

Игрок может помечать предполагаемые мины через функцию get_action():

def get_action():     """Метод запроса действия: открыть или пометить"""     while True:         action = input("Выберите действие (d - открыть, f - пометить флагом): ").lower()         if action in ('d', 'f'):             return action         print("Используйте 'd' или 'f'!")

Шаг 6: Проверка ввода

Проверка правильности формата и контроль диапазона чисел (1-9)

def get_coordinates():     """Получение координат с проверкой"""     while True:         try:           # Запрашиваем ввод и разбиваем по разделителю '-'             coords = input("Введите координаты (формат Y-X): ").split('-')             if len(coords) != 2:                 raise ValueError             x, y = map(int, coords)# Преобразуем в числа             # Проверяем диапазон координат (1-9)             if abs(x - 5.5) > 4.5 or abs(y - 5.5) > 4.5:                 print("Координаты должны быть от 1 до 9!")                 continue             return x - 1, y - 1  # Переводим в индексы массива         except ValueError:             print("Ошибка! Используйте формат 'Y-X' (например, 3-5)")

Шаг 7: Запуск игры и последняя часть кода

Главный цикл в функции main() управляет игровым процессом:

def main():     # Инициализация структур данных     dictionary_for_x_y = {'y': [], 'x': []}  # История ходов     bomb_count = get_bomb_count()             # Запрос количества мин     hidden_map = Generator_Map(bomb_count)    # Генерация поля с минами     player_map = [['-' for _ in range(9)] for _ in range(9)]  # Видимое поле     flags = set()               # Множество флагов     correct_flags = set()       # Правильно поставленные флаги      # Создание отображаемой карты с номерами строк/столбцов     display_map = []     display_map.append([' '] + list(range(1, 10)))     for i in range(9):         display_map.append([i + 1] + player_map[i])      # Главный игровой цикл     while True:         print("\nТекущая карта:")         print_map(display_map, flags)                  # Получение действия и координат         action = get_action()         x, y = get_coordinates()          # Проверка на уже открытую клетку         if player_map[x][y] != '-':             print("Эта клетка уже открыта!")             continue          # Логика установки/снятия флага         if action == 'f':             if (x, y) in flags:                 flags.remove((x, y))                 if hidden_map[x + 1][y + 1] == 'M':                     correct_flags.remove((x, y))             else:                 flags.add((x, y))                 if hidden_map[x + 1][y + 1] == 'M':                     correct_flags.add((x, y))              # Проверка победы по флагам             if len(correct_flags) == bomb_count and len(flags) == bomb_count:                 print("\nПоздравляем! Вы правильно отметили все мины!")                 print("Игровое поле:")                 print_map(hidden_map)                 break             continue          # Логика открытия клетки         if (x, y) in flags:             print("Сначала уберите флаг с этой клетки!")             continue          if hidden_map[x + 1][y + 1] == 'M':             print("\nBOOM! Вы наступили на мину!")             print("Игровое поле:")             print_map(hidden_map)             break          # Рекурсивное открытие клеток         reveal_cells(hidden_map, player_map, display_map, x, y, 9)          # Обновление истории ходов         dictionary_for_x_y['y'].append(x + 1)         dictionary_for_x_y['x'].append(y + 1)         print(x + 1, y + 1, dictionary_for_x_y)          # Проверка победы по открытым клеткам         if all(player_map[i][j] != '-' or hidden_map[i + 1][j + 1] == 'M'                for i in range(9) for j in range(9)):             print("\nПоздравляем! Вы открыли все безопасные клетки!")             print("Игровое поле:")             print_map(hidden_map)             break  main() # запуск главной функции          

Итог: Что мы получили?

Пример консольного вывода

Пример консольного вывода
  1. Полноценная консольная версия «Сапёра»

  2. Чистый код с разделением логики и отображения

  3. Гибкую систему, которую можно расширять:

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

    • Реализовать уровни сложности

    • Перевести на GUI (Tkinter/PyQt)

Полный код доступен в репозитории GitHub.

Пишите идеи в комментарии! Ваш вариант кода может попасть в обновлённую версию статьи.

P.S. Если найдёте баги — сообщите, исправлю


ссылка на оригинал статьи https://habr.com/ru/articles/936038/


Комментарии

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

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