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

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

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

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

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

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

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

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

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

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


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

import random # Импорт модуля для работы со случайными числами



def Generator_Map(mines, first_move):
    """Метод создания карты"""
    # Задаем размер игрового поля (классический сапёр - 9x9)
    size = 9
    map_dict = {}  # Используем словарь вместо списка

    mines_count = mines
    mines = set()  # убрать дубликаты

    # Генерация случайных мин, исключая первую клетку игрока
    first_x, first_y = first_move
    while len(mines) < mines_count:
        x, y = random.randint(0, size - 1), random.randint(0, size - 1)
        if (x, y) != (first_x, first_y):  # Исключаем первую клетку игрока
            mines.add((x, y))
            map_dict[(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 (x, y) not in map_dict:  # Если это не мина
                count = 0
                for dx, dy in directions:
                    nx, ny = x + dx, y + dy
                    if (nx, ny) in map_dict and map_dict[(nx, ny)] == 'M':
                        count += 1
                if count > 0:
                    map_dict[(x, y)] = str(count)
                else:
                    map_dict[(x, y)] = ' '

    # Преобразуем словарь обратно в список
    map_mines = [[' '] + list(range(1, size + 1))]
    for i in range(size):
        row = [i + 1]
        for j in range(size):
            row.append(map_dict.get((i, j), ' '))
        map_mines.append(row)

    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():
    """Получение координат с проверкой"""
    MIN = 1
    MAX = 9
    while True:
        try:
            coords = input("Введите координаты (формат Y-X): ").strip().split('-')
            if len(coords) != 2:
                raise ValueError("Неправильный формат ввода")
            x, y = map(int, coords)
            if not (MIN <= x <= MAX) or not (MIN <= y <= MAX):
                print(f"Координаты должны быть от {MIN} до {MAX}!")
                continue
            return x - 1, y - 1  # Переводим в индексы массива (0-8)

        except ValueError as e:
            print("Ошибка! Используйте формат 'Y-X' (например, 3-5)")

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

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

def main():
    dictionary_for_x_y = {'y': [], 'x': []}
    bomb_count = get_bomb_count()

    # Получаем первый ход до генерации карты
    print("Сделайте первый ход:")
    first_x, first_y = get_coordinates()

    # Генерируем карту с минами, исключая первую клетку игрока
    hidden_map = Generator_Map(bomb_count, (first_x, first_y))

    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])

    # Обрабатываем первый ход
    reveal_cells(hidden_map, player_map, display_map, first_x, first_y, 9)
    dictionary_for_x_y['y'].append(first_x + 1)
    dictionary_for_x_y['x'].append(first_y + 1)
    print(first_x + 1, first_y + 1, dictionary_for_x_y)

    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. Если найдёте баги — сообщите, исправлю

Комментарии (15)


  1. Sergey_Kh
    11.08.2025 14:02

    1. ООП у вас не используется);

    2. Я бы запускал генерацию бомб после получения от игрока координат первого хода, чтобы исключить ситуацию случайного попадания в бомбу на первом ходу;

    3. Как по мне, для хранения игрового поля удобнее использовать словарь, вот так:
      map = {}
      map[(x, y)] = 'M'


    1. Laborant_Code Автор
      11.08.2025 14:02

      Сначала спасибо за комментарий!

      1 - Насчет ООП согласен, поставил тэг, потому что хотел написать сразу две сатьи про игру “Сапёр” (консоль и Gui) и в Gui есть немного от ООП, поэтому и добавил, но согласен, что в этой статье его нет, поэтому уже убрал.

      2 – Звучит логично и мне нравиться эта идея, поэтому постараюсь в скором времени поменять, но до вашего комментария я даже не думал об этом

      3 – Честно, когда писал пришло в голову множество set(), чтобы не было повторов, но сейчас согласен можно и даже лучше использовать словарь. Исправлю!  


  1. IgnatF
    11.08.2025 14:02

    А где тут про GUI? Только ради него открыл статью. В консоле такое сделать дело не хитрое.


    1. Laborant_Code Автор
      11.08.2025 14:02

      Полностью согласен, что консольную игру может написать любой, поэтому в заголовке написано игра с нуля версия консоли, насчет GUI, обещаю в ближайшие дни выпустить вторую часть уже как раз с GUI, и буду благодарен, если там тоже оставите комментарий, спасибо!


  1. makar47
    11.08.2025 14:02

    get_bomb_count()

    Что там происходит? "Ввидите два число" - на каком языке? Почему "два число", если вводится только одно. Что за магические числа 40.5 и 39.5, а логика проверки вообще непонятна.

    if abs(count_of_bomb - 40.5) > 39.5:
                continue


    1. Laborant_Code Автор
      11.08.2025 14:02

      1 -    Насчет ввода извиняюсь, так как при копирование из строчки в строчку не заметил ошибку в тексте, уже исправил, спасибо (вводить нужно одно число)

      2 -  Этот код пытается отсечь крайние значения count_of_bomb.

      Если count_of_bomb меньше 1 (40.5 - 39.5 = 1) или больше 80 (40.5 + 39.5 = 80), то условие срабатывает, и такое значение игнорируется, то есть допустимый диапазон для count_of_bomb — от 1 до 80. Согласен, что проще вот так if count_of_bomb < 1 or count_of_bomb > 80, поэтому в финальном варианте оставлю так.

      Спасибо за комментарий и если есть еще вопросы или жалобы пишите


    1. randomsimplenumber
      11.08.2025 14:02

      if abs(count_of_bomb - 40.5) > 39.5: continue

      не могу представить, что может побудить написать проверку на 80 именно так. Чатик надиктовал?


      1. Laborant_Code Автор
        11.08.2025 14:02

        Если говорить честно, то в тиктоке увидел такой метод проверки решил повторить. А если про чатик говорить, то он наоборот всегда пиши самый простой способ.


        1. randomsimplenumber
          11.08.2025 14:02

          Возможно это был чемпионат на использование пикабушной константы.


  1. voronovnr
    11.08.2025 14:02

    Присутствуют вопросы, касающиеся преимущественно магических цифр и не использования псевдоконстант.
    Гораздо удобнее, когда можно вынести постоянные в коде переменные либо в отдельный файл с последующим подключением его в основной код.
    Магические числа, которые не поясняют зачем они нужны или по какой логике они были сюда подставлены. Методом подбора?

    ...
    # Проверяем диапазон координат (1-9)
    if abs(x - 5.5) > 4.5 or abs(y - 5.5) > 4.5:
        print("Координаты должны быть от 1 до 9!")
        continue
    ...

    Волшебство, не иначе!


    1. Laborant_Code Автор
      11.08.2025 14:02

      Согласен с замечанием. Действительно, использование 'магических чисел' снижает читаемость кода и усложняет его поддержку, но как писал в сообщении выше увидел этот способ и решил повторить. Всегда можно использовать простой метод и что будет правильным с точки 'чистоты кода', например вот такой:

      Min = 1

      MAX = 9

      if not (Min <= x <= Max) or not (Min <= y <= Max):

      print(f"Координаты должны быть от {Min} до {Max}!")

      Ну или что-то похожие


  1. FlyingLemon
    11.08.2025 14:02

    ...
    # Проверяем диапазон координат (1-9)
    if abs(x - 5.5) > 4.5 or abs(y - 5.5) > 4.5:
        print("Координаты должны быть от 1 до 9!")
        continue
    ...

    Тем более такой вариант будет работать не так как ожидается: |1-5.5|=4,5; |9-5.5|=4.5, а 4,5 не > 4,5. Жертвовать читаемостью, чтобы сократить на одно сравнение думаю того вообще не стоит. Также при проверке условий стоит тестировать граничные значения, в данном случае для интервала 1...9 - это 0;2 и 8;10


    1. Laborant_Code Автор
      11.08.2025 14:02

      Согласен, как уже писал выше с этим методом проверки я погорячился, возможно, я его не правильно повторил или не так понял, но на данный момент уже изменил код на более лёгкий и понятный, спасибо за комментарий


  1. S0mbre
    11.08.2025 14:02

    Написано же уже на Хабре:

    https://habr.com/ru/articles/833494/

    В 66 строк человек запихал.


    1. Laborant_Code Автор
      11.08.2025 14:02

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

      Я думаю, что это касается и моей статьи, потому что я не говорю, что написал что-то гениально или чего не видел мир, а наоборот, что это один из простых способов написать игру, чтобы каждый мог повторить сам или просто запустить мой код для себя.

      А так я рад, что в хабр есть люди, которые стремятся улучшить свой код до идеала и минимализма, как человек, который написал игру в 66 строк.