Сегодня мы напишем одну из самых распространенных игр — сапер. Для графического интерфейса будем использовать библиотеку 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() # Запускаем программу
Получим примерно такую картинку
Теперь добавим возможность отслеживать мины и нажатые клеточки, а также функционал для обработки клика по клеткам:
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)
Если вы все сделали правильно, то сможете кликать по клеткам:
Тут зеленая клетка обозначает чистое поле, красная — мину, а желтая флажок для обозначения мины.
После этого наш план такой: при клике по клетке, если на этом месте нету мины мы должны высчитать все соседение клетки. Далее проверяем есть ли среди соседних клеток мины. Если мины есть — написать на клетке по которой мы кликнули количество мин в соседних клетках. Если мин в соседних клетках нету — прогнать тот же процесс по ним.
Для начала следует объяснить, что у каждой клеточки есть свой уникальный идентификатор. В коде мы получаем доступ к нему через переменную библиотеки tkinter CURRENT. Если нарисовать идентификаторы на клетках, то получим вот такую картину.
Теперь напишем функцию для получения соседних идентификаторов клетки. У нас может быть восемь уникальных ситуаций, когда количество соседних клеток не равно восьми. Посмотрите на нижнее изображение (поле GRID_SIZE увеличено c 8 до 9 для наглядности)
У угловых клеток (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
Сапёр – это такая одиночная игра, суть которой заключается в том, чтобы исследовать территорию вокруг вашей ракеты на луне и избегать контакта с пришельцами. В разных версиях игры разные сценарии.
Это простая альтернативная версия классического Сапёра, где вам приходилось переворачивать плитки для поиска спрятанных мин. Наша версия использует пользовательские объекты 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 вызывает эти функции для настройки.
Процесс заключается в следующем:
- Убрать все мины (и обновить данные) с поля;
- Добавить новые мины на поле;
- Подсчитать количество мин, смежных с каждой позицией;
- Добавить стартовый маркер (ракету) и запустить начальную проверку;
- Сбросить таймер.
Как это выглядит в коде:
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_size—1), random.randint(0, self.b_size—1) 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) |
Позиции плиток
Игра является структурной так что индивидуальные позиции плиток содержат собственную информацию состояния. Это значит, что плитки 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() |
Конец игры
Состояния конца игры обнаруживаются во время процесса раскрытия, следующего за нажатием на плитку. После этого возможны два развития:
- Это мина, игра заканчивается;
- Это не мина, декрементируем 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_() |
Больше идей!
Если вы хотите расширить игру сапёр, есть несколько идей:
- Позвольте игроку выбрать свой первый ход. Попробуйте включить расчет позиций мин после первого нажатия игрока, после чего генерируйте позиции, пока не промахнетесь;
- Добавьте усилители, такие как сканер для разведки определенной части стола в автоматическом режиме;
- Дайте возможность спрятанным минам двигаться по полю между ходами. Храните список доступных нераскрытых позиций и позвольте минам двигаться по ним. Вам нужно будет пересчитывать смежности после каждого нажатия.
Являюсь администратором нескольких порталов по обучению языков программирования 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.
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:
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.
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
Видеоурок по созданию игры «Сапер»: