Как написать сапера на питоне

Сегодня мы напишем одну из самых распространенных игр — сапер. Для графического интерфейса будем использовать библиотеку tkinter.

Для начала зададим глобальные переменные и создадим окно игры:

from tkinter import *
import random

GRID_SIZE = 8 # Ширина и высота игрового поля
SQUARE_SIZE = 50 # Размер одной клетки на поле
MINES_NUM = 10 # Количество мин на поле

root = Tk() # Основное окно программы
root.title("Pythonicway Minesweep")
c = Canvas(root, width=GRID_SIZE * SQUARE_SIZE, height=GRID_SIZE * SQUARE_SIZE) # Задаем область на которой будем рисовать
c.pack()

# Следующий код отрисует решетку из клеточек серого цвета на игровом поле
for i in range(GRID_SIZE):
    for j in range(GRID_SIZE):
        c.create_rectangle(i * SQUARE_SIZE, j * SQUARE_SIZE,
                           i * SQUARE_SIZE + SQUARE_SIZE,
                           j * SQUARE_SIZE + SQUARE_SIZE, fill='gray')

root.mainloop() # Запускаем программу

Получим примерно такую картинку

 python minesweep

Теперь добавим возможность отслеживать мины и нажатые клеточки, а также функционал для обработки клика по клеткам:

mines = set(random.sample(range(1, GRID_SIZE**2+1), MINES_NUM))  # Генерируем мины в случайных позициях
clicked = set()  # Создаем сет для клеточек, по которым мы кликнули

# Функция реагирования на клик
def click(event):
    ids = c.find_withtag(CURRENT)[0]  # Определяем по какой клетке кликнули
    if ids in mines:
        c.itemconfig(CURRENT, fill="red") # Если кликнули по клетке с миной - красим ее в красный цвет
    elif ids not in clicked:
        c.itemconfig(CURRENT, fill="green") # Иначе красим в зеленый
    c.update()

# Функция для обозначения мин
def mark_mine(event):
    ids = c.find_withtag(CURRENT)[0]
    # Если мы не кликали по клетке - красим ее в желтый цвет, иначе - в серый
    if ids not in clicked:
        clicked.add(ids)
        x1, y1, x2, y2 = c.coords(ids)
        c.itemconfig(CURRENT, fill="yellow")
    else:
        clicked.remove(ids)
        c.itemconfig(CURRENT, fill="gray")

 Осталось привязать обработчики событий для созданных функций. Поместите следующий код после строчки с.pack() :

c.bind("<Button-1>", click)
c.bind("<Button-3>", mark_mine)

 Если вы все сделали правильно, то сможете кликать по клеткам:

python сапер обозначение мин

Тут зеленая клетка обозначает чистое поле, красная — мину, а желтая флажок для обозначения мины.
После этого наш план такой: при клике по клетке, если на этом месте нету мины мы должны высчитать все соседение клетки. Далее проверяем есть ли среди соседних клеток мины. Если мины есть — написать на клетке по которой мы кликнули количество мин в соседних клетках. Если мин в соседних клетках нету — прогнать тот же процесс по ним.

Для начала следует объяснить, что у каждой клеточки есть свой уникальный идентификатор. В коде мы получаем доступ к нему через переменную библиотеки tkinter CURRENT. Если нарисовать идентификаторы на клетках, то получим вот такую картину.

minesweep python

Теперь напишем функцию для получения соседних идентификаторов клетки. У нас может быть восемь уникальных ситуаций, когда количество соседних клеток не равно восьми. Посмотрите на нижнее изображение (поле GRID_SIZE увеличено c 8 до 9 для наглядности)

python minesweep, unique situations

У угловых клеток (1, 9, 73, 81) только по 3 соседа. У клеток из крайних рядов (например, 5, 37, 45, 77) по 5 соседей. Во всех остальных ситуациях (например, 41) соседних клеток 8. Напишем функцию, реализующую данный функционал:

def generate_neighbors(square):
    """ Возвращает клетки соседствующие с square """
    # Левая верхняя клетка
    if square == 1:
        data = {GRID_SIZE + 1, 2, GRID_SIZE + 2}
    # Правая нижняя 
    elif square == GRID_SIZE ** 2:
        data = {square - GRID_SIZE, square - 1, square - GRID_SIZE - 1}
    # Левая нижняя
    elif square == GRID_SIZE:
        data = {GRID_SIZE - 1, GRID_SIZE * 2, GRID_SIZE * 2 - 1}
    # Верхняя правая
    elif square == GRID_SIZE ** 2 - GRID_SIZE + 1:
        data = {square + 1, square - GRID_SIZE, square - GRID_SIZE + 1}
    # Клетка в левом ряду
    elif square < GRID_SIZE:
        data = {square + 1, square - 1, square + GRID_SIZE,
                square + GRID_SIZE - 1, square + GRID_SIZE + 1}
    # Клетка в правом ряду
    elif square > GRID_SIZE ** 2 - GRID_SIZE:
        data = {square + 1, square - 1, square - GRID_SIZE,
                square - GRID_SIZE - 1, square - GRID_SIZE + 1}
    # Клетка в нижнем ряду
    elif square % GRID_SIZE == 0:
        data = {square + GRID_SIZE, square - GRID_SIZE, square - 1,
                square + GRID_SIZE - 1, square - GRID_SIZE - 1}
    # Клетка в верхнем ряду
    elif square % GRID_SIZE == 1:
        data = {square + GRID_SIZE, square - GRID_SIZE, square + 1,
                square + GRID_SIZE + 1, square - GRID_SIZE + 1}
    # Любая другая клетка
    else:
        data = {square - 1, square + 1, square - GRID_SIZE, square + GRID_SIZE,
                square - GRID_SIZE - 1, square - GRID_SIZE + 1,
                square + GRID_SIZE + 1, square + GRID_SIZE - 1}
    return data

Теперь создадим функцию подсчета мин в соседних клетках. Это достаточно просто сделать используя метод intersection типа данных сет.

def check_mines(neighbors):
        # Возвращаем длинну пересечения мин и соседних клеток
	return len(mines.intersection(neighbors))

И, наконец, рекурсивная функция которая свяжет все это вместе:

def clearance(ids):
      # Добавляем клетку по которой кликнули в список
      clicked.add(ids)
      # Получаем список соседних клеток
      neighbors = generate_neighbors(ids)
      # Определяем количество мин в соседних клетках
      around = check_mines(neighbors)
      # Если мины вокруг клетки есть
      if around:
        # Определяем координаты клетки
        x1, y1, x2, y2 = c.coords(ids)
        # Окрашиваем клетку в зеленый
        c.itemconfig(ids, fill="green")
        # Пишем на клетке количество мин вокруг
        c.create_text(x1 + SQUARE_SIZE / 2,
                    y1 + SQUARE_SIZE / 2,
                    text=str(around), font="Arial {}".format(int(SQUARE_SIZE / 2)), fill='yellow')
      # Если мин вокруг нету
      else:
        # Проходимся по всем соседним клеткам, по которым мы еще не кликнули
        for item in set(neighbors).difference(clicked):
          # красим клекту  зеленый
          c.itemconfig(item, fill="green")
          # Рекурсивно вызываем нашу функцию для данной клетки
          clearance(item)

На этом, казалось бы, все, игра работает. Однако, на самом деле, у нас есть одна серьезная проблема. Попробуйте увеличить размер игрового поля и уменьшить количество мин, например, GRID_SIZE = 50, MINES_NUM = 2. Кликнув по клетке, вы, скорее всего, получите ошибку RecursionError: maximum recursion depth exceeded while calling a Python object. Дело в том, что для избежания перегрузки стека в питоне установлен лимит на максимальное количество вызовов рекурсии.
Решить эту проблему можно несколькими способами. Самый простой, однако неэффективный — это просто увеличить максимальную глубину рекурсии. Для этого нужно добавить следующие строки в начало файла:

import sys

sys.setrecursionlimit(5000) # По умолчанию лимит на глубину рекурсии 1000, однако, это зависит от платформы.

Конечно, простое увеличение лимита рекурсии лишь отодвигает возникновение ошибки, но не решает проблему как таковую. Попробуйте определить функцию clearance таким образом, чтобы убрать рекурсию вообще. Я не публикую решение этой задачи тут, оно будет добавлено на Github. Постарайтесь решить эту задачу самостоятельно. На этом все, приятной игры.

Полный код игры сапер на Python на GitHub

#!/usr/bin/env python __author__ = «Dmitriy Krasota aka g0t0wasd» # Minesweeper in Python using Tkinter. # More at http://pythonicway.com/python-games/python-arcade/31-python-minesweep from tkinter import * import random GRID_SIZE = 20 # Размер поля SQUARE_SIZE = 20 # Размер клетки MINES_NUM = 40 # Количество мин на поле mines = set(random.sample(range(1, GRID_SIZE**2+1), MINES_NUM)) # Устанавливаем случайным образом мины на поле clicked = set() # Сет, хранящий все клетки, по которым мы кликнули def check_mines(neighbors): «»» Функция, возвращающая количество мин вокруг neighbors «»» return len(mines.intersection(neighbors)) def generate_neighbors(square): «»» Возвращает клетки соседствующие с square «»» # Левая верхняя клетка if square == 1: data = {GRID_SIZE + 1, 2, GRID_SIZE + 2} # Правая нижняя elif square == GRID_SIZE ** 2: data = {square GRID_SIZE, square 1, square GRID_SIZE 1} # Левая нижняя elif square == GRID_SIZE: data = {GRID_SIZE 1, GRID_SIZE * 2, GRID_SIZE * 2 1} # Верхняя правая elif square == GRID_SIZE ** 2 GRID_SIZE + 1: data = {square + 1, square GRID_SIZE, square GRID_SIZE + 1} # Клетка в левом ряду elif square < GRID_SIZE: data = {square + 1, square 1, square + GRID_SIZE, square + GRID_SIZE 1, square + GRID_SIZE + 1} # Клетка в правом ряду elif square > GRID_SIZE ** 2 GRID_SIZE: data = {square + 1, square 1, square GRID_SIZE, square GRID_SIZE 1, square GRID_SIZE + 1} # Клетка в нижнем ряду elif square % GRID_SIZE == 0: data = {square + GRID_SIZE, square GRID_SIZE, square 1, square + GRID_SIZE 1, square GRID_SIZE 1} # Клетка в верхнем ряду elif square % GRID_SIZE == 1: data = {square + GRID_SIZE, square GRID_SIZE, square + 1, square + GRID_SIZE + 1, square GRID_SIZE + 1} # Любая другая клетка else: data = {square 1, square + 1, square GRID_SIZE, square + GRID_SIZE, square GRID_SIZE 1, square GRID_SIZE + 1, square + GRID_SIZE + 1, square + GRID_SIZE 1} return data def clearance(ids): «»» Итеративная (эффективная) функция очистки поля «»» clicked.add(ids) # добавляем нажатую клетку в сет нажатых ids_neigh = generate_neighbors(ids) # Получаем все соседние клетки around = check_mines(ids_neigh) # высчитываем количество мин вокруг нажатой клетки c.itemconfig(ids, fill=«green») # окрашиваем клетку в зеленый # Если вокруг мин нету if around == 0: # Создаем список соседних клеток neigh_list = list(ids_neigh) # Пока в списке соседей есть клетки while len(neigh_list) > 0: # Получаем клетку item = neigh_list.pop() # Окрашиваем ее в зеленый цвет c.itemconfig(item, fill=«green») # Получаем соседение клетки данной клетки item_neigh = generate_neighbors(item) # Получаем количество мин в соседних клетках item_around = check_mines(item_neigh) # Если в соседних клетках есть мины if item_around > 0: # Делаем эту проверку, чтобы писать по нескольку раз на той же клетке if item not in clicked: # Получаем координаты этой клетки x1, y1, x2, y2 = c.coords(item) # Пишем на клетке количество мин вокруг c.create_text(x1 + SQUARE_SIZE / 2, y1 + SQUARE_SIZE / 2, text=str(item_around), font=«Arial {}».format(int(SQUARE_SIZE / 2)), fill=‘yellow’) # Если в соседних клетках мин нету else: # Добавляем соседние клетки данной клетки в общий список neigh_list.extend(set(item_neigh).difference(clicked)) # Убираем повторяющиеся элементы из общего списка neigh_list = list(set(neigh_list)) # Добавляем клетку в нажатые clicked.add(item) # Если мины вокруг есть else: # Высчитываем координаты клетки x1, y1, x2, y2 = c.coords(ids) # Пишем количество мин вокруг c.create_text(x1 + SQUARE_SIZE / 2, y1 + SQUARE_SIZE / 2, text=str(around), font=«Arial {}».format(int(SQUARE_SIZE / 2)), fill=‘yellow’) def rec_clearance(ids): «»» Рекурсивная (неэффективная) функция очистки поля «»» clicked.add(ids) neighbors = generate_neighbors(ids) around = check_mines(neighbors) if around: x1, y1, x2, y2 = c.coords(ids) c.itemconfig(ids, fill=«green») c.create_text(x1 + SQUARE_SIZE / 2, y1 + SQUARE_SIZE / 2, text=str(around), font=«Arial {}».format(int(SQUARE_SIZE / 2)), fill=‘yellow’) else: for item in set(neighbors).difference(clicked): c.itemconfig(item, fill=«green») rec_clearance(item) def click(event): ids = c.find_withtag(CURRENT)[0] if ids in mines: c.itemconfig(CURRENT, fill=«red») elif ids not in clicked: clearance(ids) c.itemconfig(CURRENT, fill=«green») c.update() def mark_mine(event): ids = c.find_withtag(CURRENT)[0] if ids not in clicked: clicked.add(ids) x1, y1, x2, y2 = c.coords(ids) c.itemconfig(CURRENT, fill=«yellow») else: clicked.remove(ids) c.itemconfig(CURRENT, fill=«gray») root = Tk() root.title(«Pythonicway Minesweep») c = Canvas(root, width=GRID_SIZE * SQUARE_SIZE, height=GRID_SIZE * SQUARE_SIZE) c.bind(«<Button-1>», click) c.bind(«<Button-3>», mark_mine) c.pack() for i in range(GRID_SIZE): for j in range(GRID_SIZE): c.create_rectangle(i * SQUARE_SIZE, j * SQUARE_SIZE, i * SQUARE_SIZE + SQUARE_SIZE, j * SQUARE_SIZE + SQUARE_SIZE, fill=‘gray’) root.mainloop()

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

Создаем игру «Сапёр» на PyQt5

Это простая альтернативная версия классического Сапёра, где вам приходилось переворачивать плитки для поиска спрятанных мин. Наша версия использует пользовательские объекты QWidget для плиток, которые индивидуально сохраняют свое состояние в качестве мин, статус и смежное количество мин. В данной версии мины заменены на инопланетян, но здесь вы уже можете придумывать что угодно.

Есть вопросы по Python?

На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!

Telegram Чат & Канал

Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!

Паблик VK

Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!

В различных вариациях Сапёра первый ход — полностью безопасный. Если вы попадаете на мину в свой первый ход, она сменит свое расположение. На этом моменте мы немного смухлюем, отдавая первый ход игроку предупредив, что мин на нём не будет. Это дает нам возможность не беспокоиться о том, что первый ход будет неудачным и требовать пересчета смежности.

Скачать исходный код

Полный исходный код игры сапёр доступен в пятнадцатиминутном репозитории на Github. Вы можете скачать или клонировать его для получения рабочей копии, после это вам нужно установить все необходимое при помощи:

pip3 install r requirements.txt

После этого, вы можете запустить игру при помощи:

Читайте дальше, что бы узнать , как работает код

Игровое поле

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

Уровень

Размеры

Количество мин

Легко

8 x 8

10

Средне

16 x 16

40

Тяжело

24 x 24

99

Мы сохраняем эти значения как постоянные УРОВНИ, определенные в верхней части файла. Так как игровое поле имеет квадратную форму, мы сохраняем значение только один раз (8, 16 или 24).

LEVELS = [

    («Easy», 8, 10),

    («Medium», 16, 40),

    («Hard», 24, 99)

]

Игровая сетка может быть представлена несколькими способами, включая, например, двухмерный «список списков», который представляет различные состояния игровых позиций (мины, раскрытые мины, помеченные мины).

Однако, этот вариант использует объектно-ориентированный подход. Отдельные квадраты на карте содержат релевантные данные о своем нынешнем состоянии и отвечают за прорисовку. В Qt мы можем сделать это просто, создав дочерний класс QWidget и использовав простую функцию рисования.

Поскольку наши объекты плиток являются дочерними классами QWidget, мы можем расположить их как любой другой виджет. Мы сделаем это, настроив QGridLayout.

self.grid = QGridLayout()

self.grid.setSpacing(5)

self.grid.setSizeConstraint(QLayout.SetFixedSize)

Мы можем обыграть создание позиции наших плиточных виджетов и добавить их в нашу сетку. Изначальная настройка уровня считывается из LEVELS и присваивает количество переменных в окне.

def set_level(self, level):

    self.level_name, self.b_size, self.n_mines = LEVELS[level]

    self.setWindowTitle(«Moonsweeper — %s» % (self.level_name))

    self.mines.setText(«%03d» % self.n_mines)

    self.clear_map()

    self.init_map()

    self.reset_map()

Рассмотрим функции настройки!

Класс Pos представляет плитку и содержит всю необходимую информацию о своей позиции на карте, включая, например, является ли она миной, открытой миной или отмеченной миной, а также количество мин в непосредственной близости.

Каждый объект Pos также имеет три пользовательских сигнала: на него можно кликнуть, раскрыть и расширить, что мы и подключаем к пользовательским слотам. Наконец, мы можем вызвать resize для настройки размера окна в соответствии с новым содержимым. Это нужно в тех случаях, когда окно сжимается, в остальных случаях Qt увеличивает его размер автоматически.

def init_map(self):

    # Добавляем позиции на карте.

    for x in range(0, self.b_size):

        for y in range(0, self.b_size):

            w = Pos(x,y)

            self.grid.addWidget(w, y, x)

            # Подключаем сигнал для обработки расширения.

            w.clicked.connect(self.trigger_start)

            w.revealed.connect(self.on_reveal)

            w.expandable.connect(self.expand_reveal)

    # Размещаем resize в очереди событий, передав контроль Qt заранее.

    QTimer.singleShot(0, lambda: self.resize(1,1)) # <1>

Таймер singleShot нужен для того, чтобы убедиться в том, что resize запускается после того, как мы вернулись к циклу событий и Qt уведомлен о новом содержимом.

Теперь у нас есть сетка позиционных объектов плиток, и мы можем приступить к созданию начальных условий игрового поля. Это делится на несколько функций. Мы назовем их _reset (низкое подчеркивание является условным обозначением частной функции, не предназначенной для внешнего использования). Главная функция reset_map вызывает эти функции для настройки.
Процесс заключается в следующем:

  1. Убрать все мины (и обновить данные) с поля;
  2. Добавить новые мины на поле;
  3. Подсчитать количество мин, смежных с каждой позицией;
  4. Добавить стартовый маркер (ракету) и запустить начальную проверку;
  5. Сбросить таймер.

Как это выглядит в коде:

def reset_map(self):

    self._reset_position_data()

    self._reset_add_mines()

    self._reset_calculate_adjacency()

    self._reset_add_starting_marker()

    self.update_timer()

Мы детально рассмотрим разделение шагов от 1 до 5 ниже, с кодом для каждого шага.

Первый шаг – это сброс данных для каждой позиции на карте. Мы перебираем каждую позицию на доске, вызываем .reset() в виджете для каждой точки. Код для функции .reset() определен в нашем классе Pos. Мы детальнее рассмотрим этот момент позже. На данный момент достаточно знать, что он чистит мины, флажки и настраивает позицию в изначальную, т.е., плитки не раскрыты.

def _reset_position_data(self):

    # Очистка всех позиций мин.

    for x in range(0, self.b_size):

        for y in range(0, self.b_size):

            w = self.grid.itemAtPosition(y, x).widget()

            w.reset()

Теперь все позиции пустые, и мы можем начать процесс добавления мин на карту. Максимальное количество мин n_mines определяется настройками уровня, упомянутых раньше.

def _reset_add_mines(self):

    # Добавляем позиции мин.

    positions = []

    while len(positions) < self.n_mines:

        x, y = random.randint(0, self.b_size1), random.randint(0, self.b_size1)

        if (x ,y) not in positions:

            w = self.grid.itemAtPosition(y,x).widget()

            w.is_mine = True

            positions.append((x, y))

    # Подсчет итоговых позиций.

    self.end_game_n = (self.b_size * self.b_size) (self.n_mines + 1)

    return positions

С минами на позиции, мы теперь можем подсчитать «смежное» количество для каждой позиции – просто берем количество мин в непосредственной близости, используя сетку 3х3 вокруг данной точки. Функция get_surrounding легко возвращает результаты этих позиций вокруг заданного расположения х и у. Мы подсчитаем их количество, где мина это is_mine == True и сохраняем.

Такой способ предварительного подсчета смежных чисел помогает упростить логику обнаружения мин в будущем.

def _reset_calculate_adjacency(self):

    def get_adjacency_n(x, y):

        positions = self.get_surrounding(x, y)

        return sum(1 for w in positions if w.is_mine)

    # Добавляем смежности на позициях.

    for x in range(0, self.b_size):

        for y in range(0, self.b_size):

            w = self.grid.itemAtPosition(y, x).widget()

            w.adjacent_n = get_adjacency_n(x, y)

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

def _reset_add_starting_marker(self):

    # Размещение стартового маркера.

    # Устанавливаем начальный статус (нужно для функции .click)

    self.update_status(STATUS_READY)

    while True:

        x, y = random.randint(0, self.b_size 1), random.randint(0, self.b_size 1)

        w = self.grid.itemAtPosition(y, x).widget()

        # Нам не нужно начинать на мине.

        if not w.is_mine:

            w.is_start = True

            w.is_revealed = True

            w.update()

            # Раскрываем все позиции вокруг данной, если они также не являются минами

            for w in self.get_surrounding(x, y):

                if not w.is_mine:

                    w.click()

            break

    # Обновляем статус до следующих начальных кликов.

    self.update_status(STATUS_READY)

Создаем игру «Сапёр» на PyQt5

Позиции плиток

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

Так как класс Pos относительно сложный, мы разобьем его разбор на несколько частей и обсудим поочередно. Начальный блок настройки __init__ достаточно простой, принимает позицию х и у и сохраняет её в объект. Позиции Pos никогда не меняются после создания.

Для завершения настройки вызывается функция функции .reset() , которая сбрасывает все атрибуты объектов до значения по умолчанию, т.е. нулевые значения. Это значит, что мина не будет на стартовой позиции, с нее снимается значение мина, она не раскрыта и не отмечена флагом. Мы также сбрасываем смежный подсчет.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

class Pos(QWidget):

expandable = pyqtSignal(int,int)

revealed = pyqtSignal(object)

clicked = pyqtSignal()

def __init__(self, x, y, *args, **kwargs):

    super(Pos, self).__init__(*args, **kwargs)

    self.setFixedSize(QSize(20, 20))

    self.x = x

    self.y = y

    self.reset()

def reset(self):

    self.is_start = False

    self.is_mine = False

    self.adjacent_n = 0

    self.is_revealed = False

    self.is_flagged = False

    self.update()

Игровой процесс сосредоточен вокруг взаимодействия мыши с плитками на игровом поле, так что обнаружение и реакция на нажатия мыши являются приоритетными. В PyQt мы ловим нажатия мыши благодаря mouseReleaseEvent. Чтобы сделать это для нашего виджета Pos, мы определяем обработчик класса. Он получит QMouseEvent с информацией о том, что случилось. В данном случае мы заинтересованы только в том, происходили ли нажатия левой или правой кнопки мыши.

При нажатии левой кнопки мыши мы проверяем, отмечена ли плитка флажком, или уже открыта. Если это так, мы игнорируем нажатие – делая отмеченные флажком плитки «безопасными» и не давая возможности случайного нажатия. Если плитка не отмечена флажком, мы просто инициируем метод .click() (см. далее).

Для нажатия правой кнопки мыши на плитку, которая не является раскрытой, мы вызываем наш метод .toggle_flag() для включения и выключения флага.

def mouseReleaseEvent(self, e):

    if (e.button() == Qt.RightButton and not self.is_revealed):

        self.toggle_flag()

    elif (e.button() == Qt.LeftButton):

        # Блокировка нажатий на мины, отмеченные флажком.

        if not self.is_flagged and not self.is_revealed:

            self.click()

Методы, вызываемые обработчиком mouseReleaseEvent указаны ниже.

Обработчик .toggle_flag просто настраивает .is_flagged таким образом, чтобы он стал инверсией самого себя (True становится False, False становится True) с эффектом включения и выключения. Обратите внимание на то, что нам нужно вызывать .update() для того, чтобы перерисовка изменила свое состояние. Мы также выдаем наш пользовательский сигнал .clicked, который используется для запуска таймера, так как размещение флага должно также считаться как начало, а не просто для раскрытия квадрата.

Метод .click() обрабатывает нажатие левой кнопкой мыши, и в свою очередь, приводит к раскрытию квадрата. Если количество смежных мин в нашем Pos является нулем, мы запускаем сигнал .expandable для начала процесса автоматического расширения исследованного региона (см. далее). Наконец, мы снова выдаем .clicked в качестве сигнала о начале игры.

Наконец, метод .reveal() проверяет, является ли плитка раскрытой. Если нет, то .is_revealed указывается как True. Мы снова вызываем .update() для вызова перерисовки виджета.

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

def toggle_flag(self):

    self.is_flagged = not self.is_flagged

    self.update()

    self.clicked.emit()

def click(self):

    self.reveal()

    if self.adjacent_n == 0:

        self.expandable.emit(self.x, self.y)

    self.clicked.emit()

def reveal(self, emit=True):

    if not self.is_revealed:

        self.is_revealed = True

        self.update()

        if emit:

            self.revealed.emit(self)

Наконец, мы определяем пользовательский метод paintEvent для нашего виджета Pos чтобы обработать изображение состояния нынешней позиции. Чтобы использовать пользовательское рисование в виджете, мы используем QPainter и event.rect(), которые предоставляют границы, в которых мы можем рисовать. В данном случае, во внешней границе виджета Pos.

Раскрытые плитки рисуются по-другому, в зависимости от того, является ли конкретная плитка стартовой позицией, миной или пустым пространством. Первые две представлены в виде иконок ракеты и бомбы соответственно. Их рисуют в плитке QRect, используя .drawPixmap. Обратите внимание на то, что нам нужно конвертировать содержимое QImage в pixmaps, путем передачи через QPixmap.

Вы можете подумать:

Почему просто не хранить их в качестве объектов QPixmap, так как это то, что мы используем?

К сожалению, вы не можете создавать объекты QPixmap до того, как запустится QApplication.

Для пустых позиций (которые не являются ни ракетой, ни бомбой) мы можем указать количество смежностей, если это количество больше нуля. Для рисовки текста в нашем QPainter, мы используем .drawText(), который передается в QRect, флажки выравнивания и количество для рисовки в виде строки. Мы определили стандартный цвет для каждого числа (хранится в NUM_COLORS) для использования.

Для плиток, которые еще не выявлены, мы рисуем плитку путем заполнения прямоугольника светлым серым и добавляем темно-серые границы толщиной в 1 пиксель. Если .is_flagged настроен, мы также рисуем иконку флага поверх плитки используя drawPixmap и плитку QRect.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

def paintEvent(self, event):

    p = QPainter(self)

    p.setRenderHint(QPainter.Antialiasing)

    r = event.rect()

    if self.is_revealed:

        if self.is_start:

            p.drawPixmap(r, QPixmap(IMG_START))

        elif self.is_mine:

            p.drawPixmap(r, QPixmap(IMG_BOMB))

        elif self.adjacent_n > 0:

            pen = QPen(NUM_COLORS[self.adjacent_n])

            p.setPen(pen)

            f = p.font()

            f.setBold(True)

            p.setFont(f)

            p.drawText(r, Qt.AlignHCenter | Qt.AlignVCenter, str(self.adjacent_n))

    else:

        p.fillRect(r, QBrush(Qt.lightGray))

        pen = QPen(Qt.gray)

        pen.setWidth(1)

        p.setPen(pen)

        p.drawRect(r)

        if self.is_flagged:

            p.drawPixmap(r, QPixmap(IMG_FLAG))

Механика

Как правило, нам нужно получить все плитки вокруг заданной точки, так что у нас есть пользовательская функция для этой задачи. Она просто выполняет итерацию в сетке 3х3 вокруг точки с проверкой, чтобы убедиться в том, что мы не выходим за границы сетки (0 ≥ x ≤ self.b_size). Полученный список содержит виджет Pos со всей окружающей локации.

def get_surrounding(self, x, y):

    positions = []

    for xi in range(max(0, x 1), min(x + 2, self.b_size)):

        for yi in range(max(0, y 1), min(y + 2, self.b_size)):

            if not (xi == x and yi == y):

                positions.append( self.grid.itemAtPosition(yi, xi).widget() )

    return positions

Метод expand_reveal вызывается в ответ на нажатие на плитку, вокруг которой нет мин. В этом случае, нам нужно расширять зону вокруг клика до тех пор, пока количество мин в этой области равно нулю, а также раскрыть все квадраты вокруг границы этой расширенной зоны (которые не являются минами).

Мы начнем со списка to_expand, который содержит позиции для проверки следующей итерации, а также списка to_reveal, который содержит виджеты плиток, которые нужно раскрыть, а также флаг any_added для определения того момента, когда нужно выйти из цикла. Цикл останавливается в тот первый раз, когда ни один виджет не был добавлен в to_reveal.

Внутри цикла мы обнуляем any_added до False и чистим список to_expand, оставив временное хранилище в l для итерации.

Для каждой локации х и у мы получаем 8 окружающих виджетов. Если какой-либо из этих виджетов не является миной и еще не являются в добавленном списке to_reveal. Это дает понять, что все границы разведанной области раскрыты. Если на позиции нет смежных мин, мы добавляем координаты в to_expand для проверки в следующей итерации.

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

def expand_reveal(self, x, y):

    «»»

    Снаружи итерируем с начальной точки, добавив новые локации вочередь.

    Это позволяет нам расширить все за раз, а не за

    несколько колбеков.

    «»»

    to_expand = [(x,y)]

    to_reveal = []

    any_added = True

    while any_added:

        any_added = False

        to_expand, l = [], to_expand

        for x, y in l:

            positions = self.get_surrounding(x, y)

            for w in positions:

                if not w.is_mine and w not in to_reveal:

                    to_reveal.append(w)

                    if w.adjacent_n == 0:

                        to_expand.append((w.x,w.y))

                        any_added = True

    # Итерация и раскрытие всех позиций, которые мы нашли.

    for w in to_reveal:

        w.reveal()

Конец игры

Состояния конца игры обнаруживаются во время процесса раскрытия, следующего за нажатием на плитку. После этого возможны два развития:

  1. Это мина, игра заканчивается;
  2. Это не мина, декрементируем self.end_game_n.

Это будет продолжаться, пока self.end_game_n не достигнет нуля, что приведет к процессу победы, путем вызова либо game_over, либо game_won. Победапоражение вызывается путем раскрытия карты и вывода соответствующего статуса для обоих классов.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

def on_reveal(self, w):

    if w.is_mine:

        self.game_over()

    else:

        self.end_game_n -= 1 # decrement remaining empty spaces

        if self.end_game_n == 0:

            self.game_won()

def game_over(self):

    self.reveal_map()

    self.update_status(STATUS_FAILED)

def game_won(self):

    self.reveal_map()

    self.update_status(STATUS_SUCCESS)

if __name__ == ‘__main__’:

app = QApplication([])

window = MainWindow()

app.exec_()

Создаем игру «Сапёр» на PyQt5

Больше идей!

Если вы хотите расширить игру сапёр, есть несколько идей:

  1. Позвольте игроку выбрать свой первый ход. Попробуйте включить расчет позиций мин после первого нажатия игрока, после чего генерируйте позиции, пока не промахнетесь;
  2. Добавьте усилители, такие как сканер для разведки определенной части стола в автоматическом режиме;
  3. Дайте возможность спрятанным минам двигаться по полю между ходами. Храните список доступных нераскрытых позиций и позвольте минам двигаться по ним. Вам нужно будет пересчитывать смежности после каждого нажатия.

Являюсь администратором нескольких порталов по обучению языков программирования Python, Golang и Kotlin. В составе небольшой команды единомышленников, мы занимаемся популяризацией языков программирования на русскоязычную аудиторию. Большая часть статей была адаптирована нами на русский язык и распространяется бесплатно.

E-mail: vasile.buldumac@ati.utm.md

Образование
Universitatea Tehnică a Moldovei (utm.md)

  • 2014 — 2018 Технический Университет Молдовы, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
  • 2018 — 2020 Технический Университет Молдовы, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»

In this article, we will be going through the steps of creating our own terminal-based Minesweeper using Python Language.

About the game

Minesweeper is a single-player game in which the player has to clear a square grid containing mines and numbers. The player has to prevent himself from landing on a mine with the help of numbers in the neighbouring tiles.


Gameplay Demo

Aftermath of few hours of creating a game of Minesweeper.

Minesweeper Demo

Designing Minesweeper Using Python

Before creating the game logic, we need to design the basic layout of the game. A square grid is rather easy to create using Python by:

# Printing the Minesweeper Layout
def print_mines_layout():
	global mine_values
	global n

	print()
	print("tttMINESWEEPERn")

	st = "   "
	for i in range(n):
		st = st + "     " + str(i + 1)
	print(st)	

	for r in range(n):
		st = "     "
		if r == 0:
			for col in range(n):
				st = st + "______"	
			print(st)

		st = "     "
		for col in range(n):
			st = st + "|     "
		print(st + "|")
		
		st = "  " + str(r + 1) + "  "
		for col in range(n):
			st = st + "|  " + str(mine_values[r][col]) + "  "
		print(st + "|")	

		st = "     "
		for col in range(n):
			st = st + "|_____"
		print(st + '|')

	print()

The grid displayed in each iteration resembles the following figure:

Minesweeper Layout
Minesweeper Layout

The 'M' symbol denotes the presence of a ‘mine’ in that cell. As we can see clearly, any number on the grid denotes the number of mines present in the neighbouring ‘eight’ cells.

The use of variables like, mine_values will be explained further in the tutorial.


Input system

One of the most important parts of any game is sustaining the input method. In our version of Minesweeper, we will be using the row and column numbers for our input technique.

Before starting the game, the script must provide a set of instructions for the player. Our game prints the following.

Minesweeper Instructions
Minesweeper Instructions

The row and column numbers displayed along with the grid are helpful for our input system. As we know, keeping track of mines without any indicator can be difficult. Therefore, Minesweeper has a provision of using ‘flag’ to mark the cells, which we know contains a mine.


Data Storage

For a single game of Minesweeper, we need to keep track of the following information:

  • The size of the grid.
  • The number of mines.
  • The ‘actual’ grid values – At the start of the game, we need a container for storing the real values for the game, unknown to the player. For instance, the location of mines.
  • The ‘apparent’ grid values – After each move, we need to update all the values that must be shown to the player.
  • The flagged positions – The cells which have been flagged.

These values are stored using the following data structures

if __name__ == "__main__":

	# Size of grid
	n = 8
	# Number of mines
	mines_no = 8

	# The actual values of the grid
	numbers = [[0 for y in range(n)] for x in range(n)] 
	# The apparent values of the grid
	mine_values = [[' ' for y in range(n)] for x in range(n)]
	# The positions that have been flagged
	flags = []

There is not much in the game-logic of Minesweeper. All the effort is to be done in setting up the Minesweeper layout.


Setting up the Mines

We need to set up the positions of the mines randomly, so that the player might not predict their positions. This can be done by:

# Function for setting up Mines
def set_mines():

	global numbers
	global mines_no
	global n

	# Track of number of mines already set up
	count = 0
	while count < mines_no:

		# Random number from all possible grid positions 
		val = random.randint(0, n*n-1)

		# Generating row and column from the number
		r = val // n
		col = val % n

		# Place the mine, if it doesn't already have one
		if numbers[r][col] != -1:
			count = count + 1
			numbers[r][col] = -1

In the code, we choose a random number from all possible cells in the grid. We keep doing this until we get the said number of mines.

Note: The actual value for a mine is stored as -1, whereas the values stored for display, denote the mine as 'M'.

Note: The ‘randint’ function can only be used after importing the random library. It is done by writing 'import random' at the start of the program.


Setting up the grid numbers

For each cell in the grid, we have to check all adjacent neighbours whether there is a mine present or not. This is done by:

# Function for setting up the other grid values
def set_values():

	global numbers
	global n

	# Loop for counting each cell value
	for r in range(n):
		for col in range(n):

			# Skip, if it contains a mine
			if numbers[r][col] == -1:
				continue

			# Check up	
			if r > 0 and numbers[r-1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check down	
			if r < n-1  and numbers[r+1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check left
			if col > 0 and numbers[r][col-1] == -1:
				numbers[r][c] = numbers[r][c] + 1
			# Check right
			if col < n-1 and numbers[r][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-left	
			if r > 0 and col > 0 and numbers[r-1][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-right
			if r > 0 and col < n-1 and numbers[r-1][col+1]== -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-left	
			if r < n-1 and col > 0 and numbers[r+1][col-1]== -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-right
			if r < n-1 and col< n-1 and numbers[r+1][col+1]==-1:
				numbers[r][col] = numbers[r][col] + 1

These values are to be hidden from the player, therefore they are stored in numbers variable.


Game Loop

Game Loop is a very crucial part of the game. It is needed to update every move of the player as well as the conclusion of the game.

# Set the mines
set_mines()

# Set the values
set_values()

# Display the instructions
instructions()

# Variable for maintaining Game Loop
over = False
		
# The GAME LOOP	
while not over:
	print_mines_layout()

In each iteration of the loop, the Minesweeper grid must be displayed as well as the player’s move must be handled.


Handle the player input

As we mentioned before, there are two kinds of player input :

# Input from the user
inp = input("Enter row number followed by space and column number = ").split()

Standard input

In a normal kind of move, the row and column number are mentioned. The player’s motive behind this move is to unlock a cell that does not contain a mine.

# Standard Move
if len(inp) == 2:

	# Try block to handle errant input
	try: 
		val = list(map(int, inp))
	except ValueError:
		clear()
		print("Wrong input!")
		instructions()
		continue

Flag input

In a flagging move, three values are sent in by the gamer. The first two values denote cell location, while the last one denotes flagging.

# Flag Input
elif len(inp) == 3:
	if inp[2] != 'F' and inp[2] != 'f':
		clear()
		print("Wrong Input!")
		instructions()
		continue

	# Try block to handle errant input	
	try:
		val = list(map(int, inp[:2]))
	except ValueError:
		clear()
		print("Wrong input!")
		instructions()
		continue

Sanitize the input

After storing the input, we have to do some sanity checks, for the smooth functioning of the game.

# Sanity checks
if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
	clear()
	print("Wrong Input!")
	instructions()
	continue

# Get row and column numbers
r = val[0]-1
col = val[1]-1

On the completion of input process, the row and column numbers are to be extracted and stored in 'r' and 'c'.


Handle the flag input

Managing the flag input is not a big issue. It requires checking for some pre-requisites before flagging the cell for a mine.

The following checks must be made:

  • The cell has already been flagged or not.
  • Whether the cell to be flagged is already displayed to the player.
  • The number of flags does not exceed the number of mines.

After taking care of these issues, the cell is flagged for a mine.

# If cell already been flagged
if [r, col] in flags:
	clear()
	print("Flag already set")
	continue

# If cell already been displayed
if mine_values[r][col] != ' ':
	clear()
	print("Value already known")
	continue

# Check the number for flags 	
if len(flags) < mines_no:
	clear()
	print("Flag set")

	# Adding flag to the list
	flags.append([r, col])
	
	# Set the flag for display
	mine_values[r][col] = 'F'
	continue
else:
	clear()
	print("Flags finished")
	continue	 

Handle the standard input

The standard input involves the overall functioning of the game. There are three different scenarios:

Anchoring on a mine

The game is finished as soon as the player selects a cell having a mine. It can happen out of bad luck or poor judgment.

# If landing on a mine --- GAME OVER	
if numbers[r][col] == -1:
	mine_values[r][col] = 'M'
	show_mines()
	print_mines_layout()
	print("Landed on a mine. GAME OVER!!!!!")
	over = True
	continue

After we land on a cell with mine, we need to display all the mines in the game and alter the variable behind the game loop.

The function 'show_mines()' is responsible for it.

def show_mines():
	global mine_values
	global numbers
	global n

	for r in range(n):
		for col in range(n):
			if numbers[r][col] == -1:
				mine_values[r][col] = 'M'

Visiting a ‘0’-valued cell.

The trickiest part of creating the game is managing this scenario. Whenever a gamer, visits a ‘0’-valued cell, all the neighboring elements must be displayed until a non-zero-valued cell is reached.

# If landing on a cell with 0 mines in neighboring cells
elif numbers[r][n] == 0:
	vis = []
	mine_values[r][n] = '0'
	neighbours(r, col)	

This objective is achieved using Recursion. Recursion is a programming tool in which the function calls itself until the base case is satisfied. The neighbours function is a recursive one, solving our problem.

def neighbours(r, col):
	
	global mine_values
	global numbers
	global vis

	# If the cell already not visited
	if [r,col] not in vis:

		# Mark the cell visited
		vis.append([r,col])

		# If the cell is zero-valued
		if numbers[r][col] == 0:

			# Display it to the user
			mine_values[r][col] = numbers[r][col]

			# Recursive calls for the neighbouring cells
			if r > 0:
				neighbours(r-1, col)
			if r < n-1:
				neighbours(r+1, col)
			if col > 0:
				neighbours(r, col-1)
			if col < n-1:
				neighbours(r, col+1)	
			if r > 0 and col > 0:
				neighbours(r-1, col-1)
			if r > 0 and col < n-1:
				neighbours(r-1, col+1)
			if r < n-1 and col > 0:
				neighbours(r+1, col-1)
			if r < n-1 and col < n-1:
				neighbours(r+1, col+1)	
				
		# If the cell is not zero-valued 			
		if numbers[r][col] != 0:
				mine_values[r][col] = numbers[r][col]

For this particular concept of the game, a new data structure is used, namely, vis. The role of vis to keep track of already visited cells during recursion. Without this information, the recursion will continue perpetually.

After all the cells with zero value and their neighbours are displayed, we can move on to the last scenario.

Choosing a non zero-valued cell

No effort is needed to handle this case, as all we need to do is alter the displaying value.

# If selecting a cell with atleast 1 mine in neighboring cells	
else:	
	mine_values[r][col] = numbers[r][col]

End game

There is a requirement to check for completion of the game, each time a move is made. This is done by:

# Check for game completion	
if(check_over()):
	show_mines()
	print_mines_layout()
	print("Congratulations!!! YOU WIN")
	over = True
	continue

The function check_over(), is responsible for checking the completion of the game.

# Function to check for completion of the game
def check_over():
	global mine_values
	global n
	global mines_no

	# Count of all numbered values
	count = 0

	# Loop for checking each cell in the grid
	for r in range(n):
		for col in range(n):

			# If cell not empty or flagged
			if mine_values[r][col] != ' ' and mine_values[r][col] != 'F':
				count = count + 1
	
	# Count comparison 			
	if count == n * n - mines_no:
		return True
	else:
		return False

We count the number of cells, that are not empty or flagged. When this count is equal to the total cells, except those containing mines, then the game is regarded as over.


Clearing output after each move

The terminal becomes crowded as we keep on printing stuff on it. Therefore, there must be provision for clearing it constantly. This can be done by:

# Function for clearing the terminal
def clear():
	os.system("clear")

Note: There is a need to import the os library, before using this feature. It can be done by 'import os' at the start of the program.


The complete code

Below is the complete code of the Minesweeper game:

# Importing packages
import random
import os

# Printing the Minesweeper Layout
def print_mines_layout():

	global mine_values
	global n

	print()
	print("tttMINESWEEPERn")

	st = "   "
	for i in range(n):
		st = st + "     " + str(i + 1)
	print(st)	

	for r in range(n):
		st = "     "
		if r == 0:
			for col in range(n):
				st = st + "______"	
			print(st)

		st = "     "
		for col in range(n):
			st = st + "|     "
		print(st + "|")
		
		st = "  " + str(r + 1) + "  "
		for col in range(n):
			st = st + "|  " + str(mine_values[r][col]) + "  "
		print(st + "|")	

		st = "     "
		for col in range(n):
			st = st + "|_____"
		print(st + '|')

	print()
 
# Function for setting up Mines
def set_mines():

	global numbers
	global mines_no
	global n

	# Track of number of mines already set up
	count = 0
	while count < mines_no:

		# Random number from all possible grid positions 
		val = random.randint(0, n*n-1)

		# Generating row and column from the number
		r = val // n
		col = val % n

		# Place the mine, if it doesn't already have one
		if numbers[r][col] != -1:
			count = count + 1
			numbers[r][col] = -1

# Function for setting up the other grid values
def set_values():

	global numbers
	global n

	# Loop for counting each cell value
	for r in range(n):
		for col in range(n):

			# Skip, if it contains a mine
			if numbers[r][col] == -1:
				continue

			# Check up	
			if r > 0 and numbers[r-1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check down	
			if r < n-1  and numbers[r+1][col] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check left
			if col > 0 and numbers[r][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check right
			if col < n-1 and numbers[r][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-left	
			if r > 0 and col > 0 and numbers[r-1][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check top-right
			if r > 0 and col < n-1 and numbers[r-1][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-left	
			if r < n-1 and col > 0 and numbers[r+1][col-1] == -1:
				numbers[r][col] = numbers[r][col] + 1
			# Check below-right
			if r < n-1 and col < n-1 and numbers[r+1][col+1] == -1:
				numbers[r][col] = numbers[r][col] + 1

# Recursive function to display all zero-valued neighbours	
def neighbours(r, col):
	
	global mine_values
	global numbers
	global vis

	# If the cell already not visited
	if [r,col] not in vis:

		# Mark the cell visited
		vis.append([r,col])

		# If the cell is zero-valued
		if numbers[r][col] == 0:

			# Display it to the user
			mine_values[r][col] = numbers[r][col]

			# Recursive calls for the neighbouring cells
			if r > 0:
				neighbours(r-1, col)
			if r < n-1:
				neighbours(r+1, col)
			if col > 0:
				neighbours(r, col-1)
			if col < n-1:
				neighbours(r, col+1)	
			if r > 0 and col > 0:
				neighbours(r-1, col-1)
			if r > 0 and col < n-1:
				neighbours(r-1, col+1)
			if r < n-1 and col > 0:
				neighbours(r+1, col-1)
			if r < n-1 and col < n-1:
				neighbours(r+1, col+1)	

		# If the cell is not zero-valued 			
		if numbers[r][col] != 0:
				mine_values[r][col] = numbers[r][col]

# Function for clearing the terminal
def clear():
	os.system("clear")		

# Function to display the instructions
def instructions():
	print("Instructions:")
	print("1. Enter row and column number to select a cell, Example "2 3"")
	print("2. In order to flag a mine, enter F after row and column numbers, Example "2 3 F"")

# Function to check for completion of the game
def check_over():
	global mine_values
	global n
	global mines_no

	# Count of all numbered values
	count = 0

	# Loop for checking each cell in the grid
	for r in range(n):
		for col in range(n):

			# If cell not empty or flagged
			if mine_values[r][col] != ' ' and mine_values[r][col] != 'F':
				count = count + 1
	
	# Count comparison 			
	if count == n * n - mines_no:
		return True
	else:
		return False

# Display all the mine locations					
def show_mines():
	global mine_values
	global numbers
	global n

	for r in range(n):
		for col in range(n):
			if numbers[r][col] == -1:
				mine_values[r][col] = 'M'


if __name__ == "__main__":

	# Size of grid
	n = 8
	# Number of mines
	mines_no = 8

	# The actual values of the grid
	numbers = [[0 for y in range(n)] for x in range(n)] 
	# The apparent values of the grid
	mine_values = [[' ' for y in range(n)] for x in range(n)]
	# The positions that have been flagged
	flags = []

	# Set the mines
	set_mines()

	# Set the values
	set_values()

	# Display the instructions
	instructions()

	# Variable for maintaining Game Loop
	over = False
		
	# The GAME LOOP	
	while not over:
		print_mines_layout()

		# Input from the user
		inp = input("Enter row number followed by space and column number = ").split()
		
		# Standard input
		if len(inp) == 2:

			# Try block to handle errant input
			try: 
				val = list(map(int, inp))
			except ValueError:
				clear()
				print("Wrong input!")
				instructions()
				continue

		# Flag input
		elif len(inp) == 3:
			if inp[2] != 'F' and inp[2] != 'f':
				clear()
				print("Wrong Input!")
				instructions()
				continue

			# Try block to handle errant input	
			try:
				val = list(map(int, inp[:2]))
			except ValueError:
				clear()
				print("Wrong input!")
				instructions()
				continue

			# Sanity checks	
			if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
				clear()
				print("Wrong input!")
				instructions()
				continue

			# Get row and column numbers
			r = val[0]-1
			col = val[1]-1	

			# If cell already been flagged
			if [r, col] in flags:
				clear()
				print("Flag already set")
				continue

			# If cell already been displayed
			if mine_values[r][col] != ' ':
				clear()
				print("Value already known")
				continue

			# Check the number for flags 	
			if len(flags) < mines_no:
				clear()
				print("Flag set")

				# Adding flag to the list
				flags.append([r, col])
				
				# Set the flag for display
				mine_values[r][col] = 'F'
				continue
			else:
				clear()
				print("Flags finished")
				continue	 

		else: 
			clear()
			print("Wrong input!")	
			instructions()
			continue
			

		# Sanity checks
		if val[0] > n or val[0] < 1 or val[1] > n or val[1] < 1:
			clear()
			print("Wrong Input!")
			instructions()
			continue
			
		# Get row and column number
		r = val[0]-1
		col = val[1]-1

		# Unflag the cell if already flagged
		if [r, col] in flags:
			flags.remove([r, col])

		# If landing on a mine --- GAME OVER	
		if numbers[r][col] == -1:
			mine_values[r][col] = 'M'
			show_mines()
			print_mines_layout()
			print("Landed on a mine. GAME OVER!!!!!")
			over = True
			continue

		# If landing on a cell with 0 mines in neighboring cells
		elif numbers[r][col] == 0:
			vis = []
			mine_values[r][col] = '0'
			neighbours(r, col)

		# If selecting a cell with atleast 1 mine in neighboring cells	
		else:	
			mine_values[r][col] = numbers[r][col]

		# Check for game completion	
		if(check_over()):
			show_mines()
			print_mines_layout()
			print("Congratulations!!! YOU WIN")
			over = True
			continue
		clear()	

Conclusion

We hope that this tutorial on creating our own Minesweeper game was understandable as well as fun. For any queries, feel free to comment below. The complete code is also available on my Github account.

Out of boredom I decided to make simple minesweeper in python. I decided to do it using only libraries which are included in standard installation on Windows.

I have overall been coding in Python for a while now, but decided to give my code for review to see whatever my coding skills can be improved.

import tkinter, configparser, random, os, tkinter.messagebox, tkinter.simpledialog

window = tkinter.Tk()

window.title("Minesweeper")

#prepare default values

rows = 10
cols = 10
mines = 10

field = []
buttons = []

colors = ['#FFFFFF', '#0000FF', '#008200', '#FF0000', '#000084', '#840000', '#008284', '#840084', '#000000']

gameover = False
customsizes = []


def createMenu():
    menubar = tkinter.Menu(window)
    menusize = tkinter.Menu(window, tearoff=0)
    menusize.add_command(label="small (10x10 with 10 mines)", command=lambda: setSize(10, 10, 10))
    menusize.add_command(label="medium (20x20 with 40 mines)", command=lambda: setSize(20, 20, 40))
    menusize.add_command(label="big (35x35 with 120 mines)", command=lambda: setSize(35, 35, 120))
    menusize.add_command(label="custom", command=setCustomSize)
    menusize.add_separator()
    for x in range(0, len(customsizes)):
        menusize.add_command(label=str(customsizes[x][0])+"x"+str(customsizes[x][1])+" with "+str(customsizes[x][2])+" mines", command=lambda customsizes=customsizes: setSize(customsizes[x][0], customsizes[x][1], customsizes[x][2]))
    menubar.add_cascade(label="size", menu=menusize)
    menubar.add_command(label="exit", command=lambda: window.destroy())
    window.config(menu=menubar)


def setCustomSize():
    global customsizes
    r = tkinter.simpledialog.askinteger("Custom size", "Enter amount of rows")
    c = tkinter.simpledialog.askinteger("Custom size", "Enter amount of columns")
    m = tkinter.simpledialog.askinteger("Custom size", "Enter amount of mines")
    while m > r*c:
        m = tkinter.simpledialog.askinteger("Custom size", "Maximum mines for this dimension is: " + str(r*c) + "nEnter amount of mines")
    customsizes.insert(0, (r,c,m))
    customsizes = customsizes[0:5]
    setSize(r,c,m)
    createMenu()

def setSize(r,c,m):
    global rows, cols, mines
    rows = r
    cols = c
    mines = m
    saveConfig()
    restartGame()

def saveConfig():
    global rows, cols, mines
    #configuration
    config = configparser.SafeConfigParser()
    config.add_section("game")
    config.set("game", "rows", str(rows))
    config.set("game", "cols", str(cols))
    config.set("game", "mines", str(mines))
    config.add_section("sizes")
    config.set("sizes", "amount", str(min(5,len(customsizes))))
    for x in range(0,min(5,len(customsizes))):
        config.set("sizes", "row"+str(x), str(customsizes[x][0]))
        config.set("sizes", "cols"+str(x), str(customsizes[x][1]))
        config.set("sizes", "mines"+str(x), str(customsizes[x][2]))

    with open("config.ini", "w") as file:
        config.write(file)

def loadConfig():
    global rows, cols, mines, customsizes
    config = configparser.SafeConfigParser()
    config.read("config.ini")
    rows = config.getint("game", "rows")
    cols = config.getint("game", "cols")
    mines = config.getint("game", "mines")
    amountofsizes = config.getint("sizes", "amount")
    for x in range(0, amountofsizes):
        customsizes.append((config.getint("sizes", "row"+str(x)), config.getint("sizes", "cols"+str(x)), config.getint("sizes", "mines"+str(x))))

def prepareGame():
    global rows, cols, mines, field
    field = []
    for x in range(0, rows):
        field.append([])
        for y in range(0, cols):
            #add button and init value for game
            field[x].append(0)
    #generate mines
    for _ in range(0, mines):
        x = random.randint(0, rows-1)
        y = random.randint(0, cols-1)
        #prevent spawning mine on top of each other
        while field[x][y] == -1:
            x = random.randint(0, rows-1)
            y = random.randint(0, cols-1)
        field[x][y] = -1
        if x != 0:
            if y != 0:
                if field[x-1][y-1] != -1:
                    field[x-1][y-1] = int(field[x-1][y-1]) + 1
            if field[x-1][y] != -1:
                field[x-1][y] = int(field[x-1][y]) + 1
            if y != cols-1:
                if field[x-1][y+1] != -1:
                    field[x-1][y+1] = int(field[x-1][y+1]) + 1
        if y != 0:
            if field[x][y-1] != -1:
                field[x][y-1] = int(field[x][y-1]) + 1
        if y != cols-1:
            if field[x][y+1] != -1:
                field[x][y+1] = int(field[x][y+1]) + 1
        if x != rows-1:
            if y != 0:
                if field[x+1][y-1] != -1:
                    field[x+1][y-1] = int(field[x+1][y-1]) + 1
            if field[x+1][y] != -1:
                field[x+1][y] = int(field[x+1][y]) + 1
            if y != cols-1:
                if field[x+1][y+1] != -1:
                    field[x+1][y+1] = int(field[x+1][y+1]) + 1

def prepareWindow():
    global rows, cols, buttons
    tkinter.Button(window, text="Restart", command=restartGame).grid(row=0, column=0, columnspan=cols, sticky=tkinter.N+tkinter.W+tkinter.S+tkinter.E)
    buttons = []
    for x in range(0, rows):
        buttons.append([])
        for y in range(0, cols):
            b = tkinter.Button(window, text=" ", width=2, command=lambda x=x,y=y: clickOn(x,y))
            b.bind("<Button-3>", lambda e, x=x, y=y:onRightClick(x, y))
            b.grid(row=x+1, column=y, sticky=tkinter.N+tkinter.W+tkinter.S+tkinter.E)
            buttons[x].append(b)

def restartGame():
    global gameover
    gameover = False
    #destroy all - prevent memory leak
    for x in window.winfo_children():
        if type(x) != tkinter.Menu:
            x.destroy()
    prepareWindow()
    prepareGame()

def clickOn(x,y):
    global field, buttons, colors, gameover, rows, cols
    if gameover:
        return
    buttons[x][y]["text"] = str(field[x][y])
    if field[x][y] == -1:
        buttons[x][y]["text"] = "*"
        buttons[x][y].config(background='red', disabledforeground='black')
        gameover = True
        tkinter.messagebox.showinfo("Game Over", "You have lost.")
        #now show all other mines
        for _x in range(0, rows):
            for _y in range(cols):
                if field[_x][_y] == -1:
                    buttons[_x][_y]["text"] = "*"
    else:
        buttons[x][y].config(disabledforeground=colors[field[x][y]])
    if field[x][y] == 0:
        buttons[x][y]["text"] = " "
        #now repeat for all buttons nearby which are 0... kek
        autoClickOn(x,y)
    buttons[x][y]['state'] = 'disabled'
    buttons[x][y].config(relief=tkinter.SUNKEN)
    checkWin()

def autoClickOn(x,y):
    global field, buttons, colors, rows, cols
    if buttons[x][y]["state"] == "disabled":
        return
    if field[x][y] != 0:
        buttons[x][y]["text"] = str(field[x][y])
    else:
        buttons[x][y]["text"] = " "
    buttons[x][y].config(disabledforeground=colors[field[x][y]])
    buttons[x][y].config(relief=tkinter.SUNKEN)
    buttons[x][y]['state'] = 'disabled'
    if field[x][y] == 0:
        if x != 0 and y != 0:
            autoClickOn(x-1,y-1)
        if x != 0:
            autoClickOn(x-1,y)
        if x != 0 and y != cols-1:
            autoClickOn(x-1,y+1)
        if y != 0:
            autoClickOn(x,y-1)
        if y != cols-1:
            autoClickOn(x,y+1)
        if x != rows-1 and y != 0:
            autoClickOn(x+1,y-1)
        if x != rows-1:
            autoClickOn(x+1,y)
        if x != rows-1 and y != cols-1:
            autoClickOn(x+1,y+1)

def onRightClick(x,y):
    global buttons
    if gameover:
        return
    if buttons[x][y]["text"] == "?":
        buttons[x][y]["text"] = " "
        buttons[x][y]["state"] = "normal"
    elif buttons[x][y]["text"] == " " and buttons[x][y]["state"] == "normal":
        buttons[x][y]["text"] = "?"
        buttons[x][y]["state"] = "disabled"

def checkWin():
    global buttons, field, rows, cols
    win = True
    for x in range(0, rows):
        for y in range(0, cols):
            if field[x][y] != -1 and buttons[x][y]["state"] == "normal":
                win = False
    if win:
        tkinter.messagebox.showinfo("Gave Over", "You have won.")

if os.path.exists("config.ini"):
    loadConfig()
else:
    saveConfig()

createMenu()

prepareWindow()
prepareGame()
window.mainloop()

This minesweeper creates settings.ini in the same location where from script was run.

Out of boredom I decided to make simple minesweeper in python. I decided to do it using only libraries which are included in standard installation on Windows.

I have overall been coding in Python for a while now, but decided to give my code for review to see whatever my coding skills can be improved.

import tkinter, configparser, random, os, tkinter.messagebox, tkinter.simpledialog

window = tkinter.Tk()

window.title("Minesweeper")

#prepare default values

rows = 10
cols = 10
mines = 10

field = []
buttons = []

colors = ['#FFFFFF', '#0000FF', '#008200', '#FF0000', '#000084', '#840000', '#008284', '#840084', '#000000']

gameover = False
customsizes = []


def createMenu():
    menubar = tkinter.Menu(window)
    menusize = tkinter.Menu(window, tearoff=0)
    menusize.add_command(label="small (10x10 with 10 mines)", command=lambda: setSize(10, 10, 10))
    menusize.add_command(label="medium (20x20 with 40 mines)", command=lambda: setSize(20, 20, 40))
    menusize.add_command(label="big (35x35 with 120 mines)", command=lambda: setSize(35, 35, 120))
    menusize.add_command(label="custom", command=setCustomSize)
    menusize.add_separator()
    for x in range(0, len(customsizes)):
        menusize.add_command(label=str(customsizes[x][0])+"x"+str(customsizes[x][1])+" with "+str(customsizes[x][2])+" mines", command=lambda customsizes=customsizes: setSize(customsizes[x][0], customsizes[x][1], customsizes[x][2]))
    menubar.add_cascade(label="size", menu=menusize)
    menubar.add_command(label="exit", command=lambda: window.destroy())
    window.config(menu=menubar)


def setCustomSize():
    global customsizes
    r = tkinter.simpledialog.askinteger("Custom size", "Enter amount of rows")
    c = tkinter.simpledialog.askinteger("Custom size", "Enter amount of columns")
    m = tkinter.simpledialog.askinteger("Custom size", "Enter amount of mines")
    while m > r*c:
        m = tkinter.simpledialog.askinteger("Custom size", "Maximum mines for this dimension is: " + str(r*c) + "nEnter amount of mines")
    customsizes.insert(0, (r,c,m))
    customsizes = customsizes[0:5]
    setSize(r,c,m)
    createMenu()

def setSize(r,c,m):
    global rows, cols, mines
    rows = r
    cols = c
    mines = m
    saveConfig()
    restartGame()

def saveConfig():
    global rows, cols, mines
    #configuration
    config = configparser.SafeConfigParser()
    config.add_section("game")
    config.set("game", "rows", str(rows))
    config.set("game", "cols", str(cols))
    config.set("game", "mines", str(mines))
    config.add_section("sizes")
    config.set("sizes", "amount", str(min(5,len(customsizes))))
    for x in range(0,min(5,len(customsizes))):
        config.set("sizes", "row"+str(x), str(customsizes[x][0]))
        config.set("sizes", "cols"+str(x), str(customsizes[x][1]))
        config.set("sizes", "mines"+str(x), str(customsizes[x][2]))

    with open("config.ini", "w") as file:
        config.write(file)

def loadConfig():
    global rows, cols, mines, customsizes
    config = configparser.SafeConfigParser()
    config.read("config.ini")
    rows = config.getint("game", "rows")
    cols = config.getint("game", "cols")
    mines = config.getint("game", "mines")
    amountofsizes = config.getint("sizes", "amount")
    for x in range(0, amountofsizes):
        customsizes.append((config.getint("sizes", "row"+str(x)), config.getint("sizes", "cols"+str(x)), config.getint("sizes", "mines"+str(x))))

def prepareGame():
    global rows, cols, mines, field
    field = []
    for x in range(0, rows):
        field.append([])
        for y in range(0, cols):
            #add button and init value for game
            field[x].append(0)
    #generate mines
    for _ in range(0, mines):
        x = random.randint(0, rows-1)
        y = random.randint(0, cols-1)
        #prevent spawning mine on top of each other
        while field[x][y] == -1:
            x = random.randint(0, rows-1)
            y = random.randint(0, cols-1)
        field[x][y] = -1
        if x != 0:
            if y != 0:
                if field[x-1][y-1] != -1:
                    field[x-1][y-1] = int(field[x-1][y-1]) + 1
            if field[x-1][y] != -1:
                field[x-1][y] = int(field[x-1][y]) + 1
            if y != cols-1:
                if field[x-1][y+1] != -1:
                    field[x-1][y+1] = int(field[x-1][y+1]) + 1
        if y != 0:
            if field[x][y-1] != -1:
                field[x][y-1] = int(field[x][y-1]) + 1
        if y != cols-1:
            if field[x][y+1] != -1:
                field[x][y+1] = int(field[x][y+1]) + 1
        if x != rows-1:
            if y != 0:
                if field[x+1][y-1] != -1:
                    field[x+1][y-1] = int(field[x+1][y-1]) + 1
            if field[x+1][y] != -1:
                field[x+1][y] = int(field[x+1][y]) + 1
            if y != cols-1:
                if field[x+1][y+1] != -1:
                    field[x+1][y+1] = int(field[x+1][y+1]) + 1

def prepareWindow():
    global rows, cols, buttons
    tkinter.Button(window, text="Restart", command=restartGame).grid(row=0, column=0, columnspan=cols, sticky=tkinter.N+tkinter.W+tkinter.S+tkinter.E)
    buttons = []
    for x in range(0, rows):
        buttons.append([])
        for y in range(0, cols):
            b = tkinter.Button(window, text=" ", width=2, command=lambda x=x,y=y: clickOn(x,y))
            b.bind("<Button-3>", lambda e, x=x, y=y:onRightClick(x, y))
            b.grid(row=x+1, column=y, sticky=tkinter.N+tkinter.W+tkinter.S+tkinter.E)
            buttons[x].append(b)

def restartGame():
    global gameover
    gameover = False
    #destroy all - prevent memory leak
    for x in window.winfo_children():
        if type(x) != tkinter.Menu:
            x.destroy()
    prepareWindow()
    prepareGame()

def clickOn(x,y):
    global field, buttons, colors, gameover, rows, cols
    if gameover:
        return
    buttons[x][y]["text"] = str(field[x][y])
    if field[x][y] == -1:
        buttons[x][y]["text"] = "*"
        buttons[x][y].config(background='red', disabledforeground='black')
        gameover = True
        tkinter.messagebox.showinfo("Game Over", "You have lost.")
        #now show all other mines
        for _x in range(0, rows):
            for _y in range(cols):
                if field[_x][_y] == -1:
                    buttons[_x][_y]["text"] = "*"
    else:
        buttons[x][y].config(disabledforeground=colors[field[x][y]])
    if field[x][y] == 0:
        buttons[x][y]["text"] = " "
        #now repeat for all buttons nearby which are 0... kek
        autoClickOn(x,y)
    buttons[x][y]['state'] = 'disabled'
    buttons[x][y].config(relief=tkinter.SUNKEN)
    checkWin()

def autoClickOn(x,y):
    global field, buttons, colors, rows, cols
    if buttons[x][y]["state"] == "disabled":
        return
    if field[x][y] != 0:
        buttons[x][y]["text"] = str(field[x][y])
    else:
        buttons[x][y]["text"] = " "
    buttons[x][y].config(disabledforeground=colors[field[x][y]])
    buttons[x][y].config(relief=tkinter.SUNKEN)
    buttons[x][y]['state'] = 'disabled'
    if field[x][y] == 0:
        if x != 0 and y != 0:
            autoClickOn(x-1,y-1)
        if x != 0:
            autoClickOn(x-1,y)
        if x != 0 and y != cols-1:
            autoClickOn(x-1,y+1)
        if y != 0:
            autoClickOn(x,y-1)
        if y != cols-1:
            autoClickOn(x,y+1)
        if x != rows-1 and y != 0:
            autoClickOn(x+1,y-1)
        if x != rows-1:
            autoClickOn(x+1,y)
        if x != rows-1 and y != cols-1:
            autoClickOn(x+1,y+1)

def onRightClick(x,y):
    global buttons
    if gameover:
        return
    if buttons[x][y]["text"] == "?":
        buttons[x][y]["text"] = " "
        buttons[x][y]["state"] = "normal"
    elif buttons[x][y]["text"] == " " and buttons[x][y]["state"] == "normal":
        buttons[x][y]["text"] = "?"
        buttons[x][y]["state"] = "disabled"

def checkWin():
    global buttons, field, rows, cols
    win = True
    for x in range(0, rows):
        for y in range(0, cols):
            if field[x][y] != -1 and buttons[x][y]["state"] == "normal":
                win = False
    if win:
        tkinter.messagebox.showinfo("Gave Over", "You have won.")

if os.path.exists("config.ini"):
    loadConfig()
else:
    saveConfig()

createMenu()

prepareWindow()
prepareGame()
window.mainloop()

This minesweeper creates settings.ini in the same location where from script was run.

Мы покажем, как создать игру «Сапер» внутри популярной вселенной Minecraft с помощью языка программирования Python.

Учиться программировать на Python могут не только взрослые, но и дети. Уже в 10 лет ребенок способен разобраться в логике программирования и научиться писать код. Чтобы сделать обучение проще, можно изучать Python внутри игровой вселенной Minecraft: создавать здания и целые миры с помощью текстового кода. Знакомая детям среда, визуальный аспект и наглядность, элементы геймификации будут мотивировать ребят продолжать обучение. А еще дети сразу начнут ассоциировать программирование с чем-то интересным, поймут на примере, как кодинг помогает автоматизировать уже знакомые вещи и процессы.

В Minecraft можно как просто создавать предметы с помощью кода, так и программировать полноценные мини-игры. К примеру, сделать проект, в котором нужно найти выход из лабиринта. Или запрограммировать бота, который будет преследовать нас по определенному маршруту.

В этом материале школа программирования для детей «Пиксель» расскажет, как написать игру «Сапер» на Python в Майнкрафт.

Как будет устроена игра

Задача игрока в нашем проекте — кликать на блоки земли, за исключением того, под которым находится динамит. Если кликнуть на такой, то игра мгновенно заканчивается. Проиграть можно и в том случае, если игрок упадет с платформы. Нужно не терять концентрацию и постоянно просчитывать, где может оказаться динамит. Давайте приступим к программированию.

Полезные материалы

Для начала необходимо скачать Python и Minecraft на компьютер и настроить их. Мы подготовили видеоинструкции, в которых подробно объясняется, как установить программы.

Также у нас есть полное видео по созданию игры. Если вам проще пройти урок в таком формате, мы добавим его в конце материала.

Начинаем создавать программу: проектирование площадки

Создаем python-файл под названием sapper (в переводе с англ. это означает сапер). Импортируем необходимые для программирования библиотеки и модули: minecraft, block, random и time. Команду для подключения Python к Minecraft кладем в переменную mc. Позиция персонажа будет храниться в переменной pos.

Позицию по каждой оси поместим в отдельные переменные: x, y, z. Проектировать игру мы будем на высоте 100, поэтому в переменную y мы поместили данное число.

Напишем команду, которая перенесет персонажа в указанные координаты. Заранее определим, какие блоки нам нужны. Создадим три переменные для динамита, блока земли и воздуха. Набранные очки будут расположены в переменной scores.

Нам нужен счетчик блоков земли, под которыми нет динамита. Площадка будет состоять из девяти блоков. Значит восемь блоков не будут представлять опасности, и только один под собой будет иметь динамит. Для этой цели создаем переменную countGrasses и поместим в нее число 8.

Площадку будем хранить в переменной grasses. Пока положим в нее пустой список. Создадим функцию place, которая с помощью циклов for будет создавать площадку 3 на 3. В обоих циклах будет один и тот же численный диапазон: от -1 до 2. Данный диапазон будет использован для строительства площадки. Переменная i будет прибавлена к оси x, а k — к z. Циклы будут использовать числа от -1 до 1. Число 2 использовано не будет. У оси y отнимаем единицу, чтобы блоки земли построились под ногами у персонажа. С помощью append все функции поместим в список grasses.

Переменная game будет отвечать за состояние игры. Поместим в нее значение True. Это значит, что включен игровой режим. Выше мы создали функцию place, а теперь ее будем использовать. Не забываем рядом поставить пустые скобки.

Площадка для игры

Запустим цикл while game. Будем останавливать его раз в 0.1 секунду. В переменные xr и zr поместим случайные числа от -1 до 1. К этим числам прибавляем координаты. К первой координате x, а ко второй — z. Функцией setBlock создадим динамит в случайном месте под площадкой.

Площадка с динамитом

Добавляем условия в игру

Переменная searchMode будет отвечать за режим поиска. Поместим туда значение True. Это значит, что режим поиска включен. Нам нужны обновленные координаты персонажа, поместим их в переменную player. Добавим условие: если игрок окажется на высоте ниже ста, то уничтожится игровая зона, выключится режим игры и принудительно завершится режим поиска.

Сообщение после падения с платформы

После условия команду определения клика по блоку поместим в переменную hit. Циклом for будем считывать клики. Позицию клика поместим в переменную pos. Добавим условие: если игрок кликнул по блоку, под которым был динамит, то площадка уничтожается, выключается режим поиска и игры, а также прерывается цикл for с помощью команды break.

Сообщение после того, как мы наткнулись на динамит и игра завершилась

А если игрок кликнул по другому блоку, ему будет прибавлен один балл, блок, на который игрок кликнул, исчезнет, у счетчика блоков без динамита отнимется единица. Далее добавим условие: если все безопасные блоки были использованы, то происходит обновление площадки, счетчик безопасных блоков снова равен восьми, исчезает динамит и выключается режим поиска. После условия введем команду break, чтобы прервать работу цикла for.

На этом разработка игры подошла к концу, теперь можно поиграть в нашу игру.

Если вам понравилось создавать игру в Minecraft с помощью языка программирования Python, попробуйте сделать другие проекты. Ищите уроки на YouTube-канале школы программирования для детей «Пиксель». А также поддержите эту статью, чтобы мы публиковали больше таких обучающих материалов.

Полный код для игры «Сапер» на Python:

import mcpi.minecraft as minecraft
import mcpi.block as block
import random
import time

mc = minecraft.Minecraft.create()
pos = mc.player.getTilePos()

x = pos.x
y = 100
z = pos.z

mc.player.setTilePos(x, y, z)

tnt = block.TNT.id
grass = block.GRASS.id
air = block.AIR.id
scores = 0
countGrasses = 8
grasses = []

def place():
for i in range(-1, 2):
for k in range(-1, 2):
grasses.append(mc.setBlock(x + i, y — 1, z + k, grass))

game = True
place()

while game:
time.sleep(0.1)

xr = random.randint(-1, 1) + x
zr = random.randint(-1, 1) + z
mc.setBlock(xr, y — 2, zr, tnt)

searchMode = True

while searchMode:
player = mc.player.getTilePos()
if player.y < 100:
mc.postToChat(«Мы упали с платформы! Игра окончена!»)
mc.setBlocks(x — 1, y — 2, z — 1, x + 1, y, z + 1, air)
game = False
break
hit = mc.events.pollBlockHits()
for h in hit:
pos = h.pos
if pos.x == xr and pos.y == y — 1 and pos.z == zr:
mc.postToChat(«О нет! Мы наткнулись на мину! Игра окончена!»)
mc.setBlocks(x — 1, y — 2, z — 1, x + 1, y, z + 1, air)
searchMode = False
game = False
break
else:
mc.postToChat(«Отлично! +1 Балл!»)
mc.setBlock(pos.x, pos.y, pos.z, air)
scores += 1
countGrasses -= 1
mc.postToChat(«Очки:» + str(scores))
mc.postToChat(«Блоки земли без динамита:» + str(countGrasses))
if countGrasses == 0:
place()
countGrasses = 8
mc.setBlock(xr, y — 2, zr, air)
searchMode = False
break

Видеоурок по созданию игры «Сапер»:

Понравилась статья? Поделить с друзьями:
  • Как написать сапер на java
  • Как написать свое приложение для андроид на питоне
  • Как написать санузел вместе или раздельно
  • Как написать свое приложение для ios
  • Как написать свое портфолио