Гайд Сапер

  • 80
  • 4
Обратите внимание, пользователь заблокирован на форуме. Не рекомендуется проводить сделки.

Сегодня мы напишем одну из самых распространенных игр - сапер. Для графического интерфейса будем использовать библиотеку 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() # Запускаем программу


Получим примерно такую картинку
Посмотреть вложение 2110
Теперь добавим возможность отслеживать мины и нажатые клеточки, а также функционал для обработки клика по клеткам:
Код:
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)

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

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

Для начала следует объяснить, что у каждой клеточки есть свой уникальный идентификатор. В коде мы получаем доступ к нему через переменную библиотеки 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. Постарайтесь решить эту задачу самостоятельно. На этом все, приятной игры.




 
Сверху Снизу