[Из песочницы] Создание сапера при помощи модуля Tkinter

День добрый. Почти каждый начинающий программист стремится к созданию своей первой игры. Спустя пол года ленивого кропотливого обучения я решился написать сапера. Языком написания был выбран Python, модулем для добавления интерфейса tkinter, потому как уже имелся опыт работы с ним. Этот пост будет полезен скорее начинающим кодерам, но если вы итак все знаете, можете написать свои советы по улучшению кода в комменты.

Приступим. Первым делом нужно было определиться, что будет собой представлять клетка. Самое выгодное решение — создать класс поля:
class pole(object): #создаем Класс поля, наследуемся от Object
    def __init__(self,master,row, column): #Инициализация поля. master - окно Tk().
        self.button = Button(master, text = '   ') #Создаем для нашего поля атрибут 'button'
        self.mine = False #Переменная наличия мины в поле
        self.value = 0 #Кол-во мин вокруг
        self.viewed = False #Открыто/закрыто поле
        self.flag = 0 #0 - флага нет, 1 - флаг стоит, 2 - стоит "?"
        self.around = [] #Массив, содержащий координаты соседних клеток
        self.clr = 'black' #Цвет текста
        self.bg = None #Цвет фона
        self.row = row #Строка
        self.column = column #Столбец

Теперь надо создать интерфейс для настройки игры:
settings = Tk() #Создаем окно
settings.title('Настройки') #Пишем название окна
settings.geometry('200x150') #Задаем размер
mineText = Text(settings, width = 5, height = 1) #Создаем поля для ввода текста и пояснения
mineLabe = Label(settings, height = 1, text = 'Бомбы:')
highText = Text(settings, width = 5, height = 1)
highLabe = Label(settings, height = 1, text = 'Ширина:')
lenghtText = Text(settings, width = 5, height = 1)
lenghtLabe = Label(settings, height = 1, text = 'Высота:')
mineBut = Button(settings, text = 'Начать:', command = bombcounter) #Создаем кнопку
mineBut.place(x = 70, y = 90)  #Размещаем это все
mineText.place(x = 75, y = 5)
mineLabe.place(x = 5, y = 5)
highText.place(x = 75, y = 30)
highLabe.place(x = 5, y = 30)
lenghtText.place(x = 75, y = 55)
lenghtLabe.place(x = 5, y = 55)
settings.mainloop() 

В итоге получаем вот такое вот окно:
image

Теперь нужно прописать функцию bombcounter
def bombcounter(): 
    global bombs 
    if mineText.get('1.0', END) == '\n': #Проверяем наличие текста
        bombs = 10 #Если текста нет, то по стандарту кол-во бомб - 10
    else:
        bombs = int(mineText.get('1.0', END)) #Если текст есть, то это и будет кол-во бомб
    if highText.get('1.0', END) == '\n':
        high = 9
    else:
        high = int(highText.get('1.0', END))
    if lenghtText.get('1.0', END) == '\n':
        lenght = 9
    else:
        lenght = int(lenghtText.get('1.0', END))
    game(high,lenght) #Начинаем игру, передавая кол-во полей

Теперь приступаем к основной части, написанию функции игры:
def game(high,lenght): #получаем значения
    root = Tk() 
    root.title('Сапер') 
    global buttons
    global mines
    global flags
    flags = [] #Массив, содержащий в себе места, где стоят флажки
    mines = [] #Массив, содержащий в себе места, где лежат мины
    buttons = [[pole(root,j,i) for i in range(high)] for j in range(lenght)] #Двумерный массив, в котором лежат поля
    for i in range(len(buttons)): #Цикл по строкам
        for j in range(len(buttons[i])): #Цикл по элементам строки
            buttons[i][j].button.grid(column = j, row = i, ipadx = 7, ipady = 1) #Размещаем все в одной сетке при помощи grid
            buttons[i][j].button.bind('', buttons[i][j].view) #Биндим открывание клетки
            buttons[i][j].button.bind('', buttons[i][j].setFlag) #Установка флажка
            buttons[i][j].setAround() #Функция заполнения массива self.around
    buttons[0][0].button.bind('', cheat) #создаем комбинацию клавиш для быстрого решения
    root.resizable(False,False) #запрещаем изменения размера
    root.mainloop()

У нас появилось целых три функции, которые нужно написать. Начнем с .setAround ():
    def setAround(self):
        if self.row == 0:
            self.around.append([self.row+1,self.column])
            if self.column == 0:
                self.around.append([self.row,self.column+1])
                self.around.append([self.row+1,self.column+1])
            elif self.column == len(buttons[self.row])-1:
                self.around.append([self.row,self.column-1])
                self.around.append([self.row+1,self.column-1])
            else:
                self.around.append([self.row,self.column-1])
                self.around.append([self.row,self.column+1])
                self.around.append([self.row+1,self.column+1])
                self.around.append([self.row+1,self.column-1])
        elif self.row == len(buttons)-1:
            self.around.append([self.row-1,self.column])
            if self.column == 0:
                self.around.append([self.row,self.column+1])
                self.around.append([self.row-1,self.column+1])
            elif self.column == len(buttons[self.row])-1:
                self.around.append([self.row,self.column-1])
                self.around.append([self.row-1,self.column-1])
            else:
                self.around.append([self.row,self.column-1])
                self.around.append([self.row,self.column+1])
                self.around.append([self.row-1,self.column+1])
                self.around.append([self.row-1,self.column-1])
        else:
            self.around.append([self.row-1,self.column])
            self.around.append([self.row+1,self.column])
            if self.column == 0:
                self.around.append([self.row,self.column+1])
                self.around.append([self.row+1,self.column+1])
                self.around.append([self.row-1,self.column+1])
            elif self.column == len(buttons[self.row])-1:
                self.around.append([self.row,self.column-1])
                self.around.append([self.row+1,self.column-1])
                self.around.append([self.row-1,self.column-1])
            else:
                self.around.append([self.row,self.column-1])
                self.around.append([self.row,self.column+1])
                self.around.append([self.row+1,self.column+1])
                self.around.append([self.row+1,self.column-1])
                self.around.append([self.row-1,self.column+1])
                self.around.append([self.row-1,self.column-1])

Все, что здесь происходит, это заполнение массива self.around. Мы рассматриваем различные случаи и на выходе получаем верный ответ. Если есть варианты, как сделать это проще, я приму их во внимание.

Пишем view ()

    def view(self,event):
        if mines == []: #При первом нажатии
            seter(0,self.around,self.row,self.column) #Устанавливаем мины
        if self.value == 0: #Устанавливаем цвета. Можно написать и для 6,7 и 8, но у меня закончилась фантазия
            self.clr = 'yellow'
            self.value = None
            self.bg = 'lightgrey'
        elif self.value == 1:
            self.clr = 'green'
        elif self.value == 2:
            self.clr = 'blue'
        elif self.value == 3:
            self.clr = 'red'
        elif self.value == 4:
            self.clr = 'purple'
        
        if self.mine and not self.viewed and not self.flag: #Если в клетке есть мина, она еще не открыта и на ней нет флага
            self.button.configure(text = 'B', bg = 'red') #Показываем пользователю, что тут есть мина
            self.viewed = True #Говорим, что клетка раскрыта
            for q in mines:
                buttons[q[0]][q[1]].view('') #Я сейчас буду вскрывать ВСЕ мины
            lose() #Вызываем окно проигрыша
        
        elif not self.viewed and not self.flag: #Если мины нет, клетка не открыта и флаг не стоит
            self.button.configure(text = self.value, fg = self.clr, bg = self.bg) #выводим в текст поля значение
            self.viewed = True
            if self.value == None: #Если вокруг нет мин
                for k in self.around:
                    buttons[k[0]][k[1]].view('') #Открываем все поля вокруг 

Итак. Сейчас у нас написаны функции: открытия клетки, заполнения массива around, начала игры и получения значения насчет размера игрового поля и кол-ва мин. Но до сих пор нет функции для установки мин. Исправляемся:
def seter(q, around,row,column): #Получаем массив полей вокруг и координаты нажатого поля
    if q == bombs: #Если кол-во установленных бомб = кол-ву заявленных
        for i in range(len(buttons)): #Шагаем по строкам
            for j in range(len(buttons[i])): #Шагаем по полям в строке i
                for k in buttons[i][j].viewAround(): #Шагаем по полям вокруг выбранного поля j
                    if buttons[k[0]][k[1]].viewMine(): #Если в одном из полей k мина
                        buttons[i][j].setValue(buttons[i][j].viewValue()+1) #То увеличиваем значение поля j
        return
    a = choice(buttons) #Выбираем рандомную строку
    b = choice(a) #Рандомное поле
    if [buttons.index(a),a.index(b)] not in mines and [buttons.index(a),a.index(b)] not in around and [buttons.index(a),a.index(b)] != [row,column]: #Проверяем, что выбранное поле не выбиралось до этого и, что не является полем на которую мы нажали (или окружающим ее полем)
        b.setMine() #Ставим мину
        mines.append([buttons.index(a),a.index(b)]) #Добавляем ее в массив 
        seter(q+1,around,row,column) #Вызываем установщик, сказав, что одна мина уже есть
    else:
        seter(q,around,row,column) #Вызываем установщик еще раз

И вторая важная для нас функция: setValue ()
    def setValue(self,value):
        self.value = value

На этом заканчивается основная часть. Игра может работать прямо сейчас, но без установки флажка и определения победы/проигрыша. Тут все просто. Установка флажка:
    def setFlag(self,event):
        if self.flag == 0 and not self.viewed: #Если поле не открыто и флага нет
            self.flag = 1 #Ставим флаг
            self.button.configure(text = 'F', bg = 'yellow') #Выводим флаг
            flags.append([self.row,self.column]) #Добавляем в массив флагов
        elif self.flag == 1: #Если флаг стоим
            self.flag = 2 #Ставим значение '?'
            self.button.configure(text = '?', bg = 'blue') #Выводим его
            flags.pop(flags.index([self.row,self.column])) #Удаляем флаг из массива флагов
        elif self.flag == 2: #Если вопрос
            self.flag = 0 #Устанавливаем на отсутствие флага
            self.button.configure(text = '   ', bg = 'white') #Выводим пустоту
        if sorted(mines) == sorted(flags) and mines != []: #если массив флагов идентичен массиву мин
            winer() #Сообщаем о победе

Функции lose () и winer () просты и не требуют объяснений. Если будет нужно, напишу в комменты.

Финальный вид:

image

Свои вопросы, предложения и критику пишите в комменты, постараюсь ответить, обсудить и подумать.

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

  • 12 апреля 2017 в 20:06

    0

    не используйте globals
    не используйте magic numbers, используйте константы
    разделяйте отображение и внутреннее состояние игры
    setAround дичь какая-то
    ну и публикуйте полные исходники (хотя бы на github)
    • 12 апреля 2017 в 20:38

      0

      Чем заменять global?
      Что значит разделять отображение и внутренне состояние игры?
      setAround себя оправдывает, без него приходилось повторять один и тот же момент кода 3–4 раза.
      • 12 апреля 2017 в 20:48

        +1

        если по-другому спроектировать работу программы, то использовать global не потребуется

        опубликуйте полные исходники, например на https://gist.github.com/, так будет проще комментировать происходящее, если конечно вам этот комментарий нужен :)

    • 12 апреля 2017 в 20:46

      0

      Globals совсем не вариант использовать? Я к ним пришел для передачи аргументов запуска программы функциям. Просто одной нужно знать args.silent, другой args.thresh0. Таскать их неудобно из функции в функцию. Плюс это константы по сути для каждого запуска.
      Morphostain
      • 12 апреля 2017 в 20:53

        0

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

        можете вынести в отдельный модуль (сделав простенький аналог settings в django) или же передавать как параметр в функции (можно даже объектом), главное чтобы это было явно указано.

© Habrahabr.ru