[Из песочницы] Создание сапера при помощи модуля Tkinter
День добрый. Почти каждый начинающий программист стремится к созданию своей первой игры. Спустя пол года ленивого кропотливого обучения я решился написать сапера. Языком написания был выбран Python, модулем для добавления интерфейса tkinter, потому как уже имелся опыт работы с ним. Этот пост будет полезен скорее начинающим кодерам, но если вы итак все знаете, можете написать свои советы по улучшению кода в комменты.
Приступим. Первым делом нужно было определиться, что будет собой представлять клетка. Самое выгодное решение — создать класс поля:
Теперь надо создать интерфейс для настройки игры:
В итоге получаем вот такое вот окно:
Теперь нужно прописать функцию bombcounter
Теперь приступаем к основной части, написанию функции игры:
У нас появилось целых три функции, которые нужно написать. Начнем с .setAround ():
Все, что здесь происходит, это заполнение массива self.around. Мы рассматриваем различные случаи и на выходе получаем верный ответ. Если есть варианты, как сделать это проще, я приму их во внимание.
Итак. Сейчас у нас написаны функции: открытия клетки, заполнения массива around, начала игры и получения значения насчет размера игрового поля и кол-ва мин. Но до сих пор нет функции для установки мин. Исправляемся:
И вторая важная для нас функция: setValue ()
На этом заканчивается основная часть. Игра может работать прямо сейчас, но без установки флажка и определения победы/проигрыша. Тут все просто. Установка флажка:
Функции lose () и winer () просты и не требуют объяснений. Если будет нужно, напишу в комменты.
Свои вопросы, предложения и критику пишите в комменты, постараюсь ответить, обсудить и подумать.
Приступим. Первым делом нужно было определиться, что будет собой представлять клетка. Самое выгодное решение — создать класс поля:
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()
В итоге получаем вот такое вот окно:
Теперь нужно прописать функцию 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 () просты и не требуют объяснений. Если будет нужно, напишу в комменты.
Финальный вид:
Свои вопросы, предложения и критику пишите в комменты, постараюсь ответить, обсудить и подумать.
Комментарии (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. Таскать их неудобно из функции в функцию. Плюс это константы по сути для каждого запуска.
Morphostain12 апреля 2017 в 20:53
0↑
↓
я не говорю, что использовать globals нельзя, лишь то, что нужно понимать когда это оправданно, а когда нет, ну и самособой принимая риски использования.можете вынести в отдельный модуль (сделав простенький аналог settings в django) или же передавать как параметр в функции (можно даже объектом), главное чтобы это было явно указано.