Цель этого материала – показать, как использовать библиотеку графического интерфейса Tkinter, входящую в стандартный комплект поставки Python. Игра, которую я выбрал, не является революционной, но она достаточно интересна, чтобы вы могли немного поиграть в нее после того, как закончите работу над программой, которую мы будем разрабатывать.
Эта игра называется Flipping Bits.
Если вы программист, то я предполагаю, что вы уже разрабатывали ее хотя бы раз в своей карьере, но если нет, то это ваш шанс восполнить этот пробел.
Из квадратной сетки (в двух измерениях), состоящей из 0 и 1, сгенерированных случайным образом, необходимо получить целевую сетку, выполняя операции двух типов:
Щелчок мышью на столбце меняет местами все 0 и 1 в этом столбце. Щелчок по строке меняет местами все 0 и 1 в ней. Эта операция называется переворачиванием строки.
Вы решите эту головоломку, когда получите целевую сетку.
Конкретно, вот исходная сетка размером 3×3:
0 1 0
1 0 0
1 1 1
Вот сетка, которую вы хотите получить:
1 0 1
0 1 1
0 0 1
Допустим, если щелкнуть на столбце 1 исходной сетки, то получится следующая конфигурация:
1 1 0
0 0 0
0 1 1
И так далее.
Моделирование программы
Наша программа будет состоять из двух классов.
Первый – класс FlippingBitsGame
, который будет моделировать саму игру с методами для переворачивания столбца и строки, а также для генерации новой сетки.
Второй класс будет называться FlippingBitsGUI
. Он будет моделировать графическую часть игры, управляя графической частью, а также взаимодействием с пользователем. В частности, речь идет о нажатии на столбец и строку, а также о сообщении игроку о его выигрыше.
Для материализации числа 0
мы будем использовать желтый цвет. Синий цвет будет зарезервирован для цифры 1
.
При отображении квадратов сетки в левом верхнем углу следует поместить маленький квадратик с целевым цветом для данного квадрата. Это позволит игроку узнать цель, которую необходимо достичь для решения игры Flipping Bits.
Создание класса FlippingBitsGame
В конструкторе класса FlippingBitsGame
в качестве члена данных мы определяем level
. Это соответствует размеру нашей квадратной сетки. Затем мы определяем два двумерных массива для хранения соответственно сетки, которая будет достигнута, и текущей сетки.
Определяем булево значение solved
, которое будет указывать, решена сетка или нет. Наконец, мы вызываем метод newgame
, который будет определен позже. Он позволит создать новую игру.
Затем мы определяем метод flipcol
и метод fliprow
.
Метод flipcol
инвертирует все значения ячеек столбца в решаемой сетке. Метод fliprow
проделает ту же работу со значениями ячеек в строке сетки.
При вызове этих методов значения 0
превращаются в 1
, а значения 1
– в 0
.
Таким образом, мы получаем следующий код для этих двух методов:
# method to flip a column def flipcol(self, r): for i in range(len(self.board[r])): self.board[r][i] ^= 1 # 0 -> 1, 1 -> 0 # method to flip a row def fliprow(self, c): for row in self.board: row[c] ^= 1
Оператор ^=
используется для выполнения побитовой операции XOR (исключающее ИЛИ) между левым и правым значение, и затем присваивания результата левому значению. В нашем случае если у нас будет 0
, то результат XOR будет 1
, а в случае 1
, будет равен 0
Для того чтобы создать разную сетку между целевой и той, с которой будет играть игрок, определим метод shuffle
, который будет случайным образом выполнять различные операции переворачивания строк и столбцов.
В результате получается следующий код:
def shuffle(self): for _ in range(self.level * self.level): if random.random() > 0.5: self.flipcol(random.randint(0, self.level - 1)) else: self.fliprow(random.randint(0, self.level - 1))
Затем можно определить метод newgame
. Новая игра может быть создана только в том случае, если сетка помечена как решенная. Для этого мы проверяем значение члена данных solved.
Затем выполним итерации для создания целевой сетки, отличной от текущей. Для этого необходимо вызвать метод shuffle
и скопировать текущую сетку в целевую.
Если текущая сетка еще не решена, то работа завершается. В противном случае итерация продолжается. Жалко будет предлагать игроку уже решенную сетку…
Осталось завершить код этого класса FlippingBitsGame
методом issolved
, который будет проверять, соответствует ли текущая сетка сетке, до которой нужно добраться.
Таким образом, мы получаем следующий код класса FlippingBitsGame
:
class FlippingBitsGame: def __init__(self, level): self.level = level # Level = size of the square self.target = [[0] * level for _ in range(level)] # the board to obtain when you play self.board = [[0] * level for _ in range(level)] # the current board played by the user self.solved = True self.newgame() # new game method to define later # method to flip a column def flipcol(self, r): for i in range(len(self.board[r])): self.board[r][i] ^= 1 # 0 -> 1, 1 -> 0 # method to flip a row def fliprow(self, c): for row in self.board: row[c] ^= 1 # method to shuffle the board def shuffle(self): for _ in range(self.level * self.level): if random.random() > 0.5: self.flipcol(random.randint(0, self.level - 1)) else: self.fliprow(random.randint(0, self.level - 1)) # new game def newgame(self): if self.solved: # generate a new game while True: self.shuffle() self.target = deepcopy(self.board) # we make a deep copy of board into the target self.shuffle() # then we shuffle the board if self.issolved() == False: break self.solved = False # is solved method. We check if board == target def issolved(self): for i in range(self.level): for j in range(self.level): if self.board[i][j] != self.target[i][j]: return False self.solved = True return True
Создание класса FlippingBitsGUI
С классом FlippingBitsGame
покончено, пора переходить к классу FlippingBitsGUI
, который будет отображать пользователю состояние решаемой сетки, а также управлять взаимодействием.
Когда я говорю о взаимодействии, я имею в виду нажатие кнопки мыши для переворачивания столбца или строки, чтобы решить сетку и достичь целевой сетки, которая будет отображаться в виде маленьких квадратиков в левом верхнем углу.
В конструкторе класса FlippingBitsGUI
мы создаем экземпляр игры, используя класс FlippingBitsGame
.
В итоге мы получим корневой элемент GUI, который позволит нам создать Canvas
, на котором будет отображаться игра. В завершение мы вызываем метод drawboard
, который отображает сетку на экране.
Внутри этого метода drawboard
мы сначала сбрасываем Canvas
с помощью вызова его метода delete.
Мы вычисляем размер квадрата в зависимости от количества ячеек, которые необходимо отобразить для этого квадрата. Пользуясь случаем, мы также вычисляем размер маленького квадрата, который будет отображаться в левом верхнем углу.
Затем необходимо выполнить итерацию по каждой ячейке таблицы, позволяющей хранить сетку. Для каждой ячейки мы нарисуем прямоугольник, заполненный желтым цветом, если значение ячейки равно 0
, и синим – в противном случае.
В конце метода мы проверяем, была ли игра решена пользователем, вызывая метод issolved
объекта FlippingBitsGame
. Если игра решена, то мы выводим на экран сообщение о том, что пользователю достаточно щелкнуть мышью, чтобы сгенерировать новую игру.
Таким образом, мы получаем следующий код для метода drawboard
:
def drawboard(self): # we clean the canvas self.canvas.delete("all") squaresize = 500 / self.game.level targetsize = squaresize / 10 for i in range(self.game.level): x = 100 + i * squaresize for j in range(self.game.level): y = 100 + j * squaresize value = self.game.board[i][j] self.canvas.create_rectangle(x, y, x + squaresize, y + squaresize, fill = ("blue", "yellow")[value == 0]) # draw the target on the top left target = self.game.target[i][j] self.canvas.create_rectangle(x + 10, y + 10, x + 10 + targetsize, y + 10 + targetsize, fill = ("blue", "yellow")[target == 0]) if self.game.issolved(): self.canvas.create_text(200, 750, text = "Solved. Click to start a new game", font = ("Helvetica", "20", "bold"))
Нам еще предстоит управлять щелчком пользователя для перелистывания столбцов. В этом нет ничего сложного, но мы должны быть внимательны, чтобы преобразовать щелчок в номер столбца или строки.
Очевидно, что прежде чем делать что-либо еще, мы проверим, не решена ли уже сетка. Если да, то этот щелчок означает, что необходимо сгенерировать новый набор.
Затем мы получаем координаты щелчка пользователя. Проверяем, находятся ли эти координаты за пределами сетки. Если щелчок находится в правой или левой части сетки, то мы получаем строку, которую нужно перевернуть. Затем вызываем метод fliprow
экземпляра FlippingBitsGame
.
Если щелчок находится в верхней или нижней части сетки, то мы получаем столбец, который нужно перевернуть. Затем мы вызываем метод flipcol
для экземпляра FlippingBitsGame
.
После выполнения этих операций необходимо вызвать метод drawboard
, чтобы снова нарисовать графический интерфейс.
Это дает нам следующий код для метода onclick
:
def onclick(self, event): if self.game.solved: self.game.newgame() self.drawboard() return # we get coordinates of the user's click idx = int(event.x) idy = int(event.y) # check if you must flip a column or a row if (idx > 0 and idx < 100 and idy > 100 and idy < 600) or (idx > 600 and idx < 700 and idy > 100 and idy < 600): # we need to flip a row squaresize = 500 / self.game.level row = int((idy - 100) / squaresize) self.game.fliprow(row) elif (idy > 0 and idy < 100 and idx > 100 and idx < 600) or (idy > 600 and idy < 800 and idx > 100 and idx < 600): # we need to flip a col squaresize = 500 / self.game.level col = int((idx - 100) / squaresize) self.game.flipcol(col) self.drawboard()
Сборка классов
Наконец, нам нужно объединить наши классы, чтобы запустить игру. Мы создаем корневое окно с помощью вызова метода Tk
объекта tk
из библиотеки Tkinter. Затем мы инициируем объект FlippingBitsGUI
с уровнем, установленным на 5
. Мы привязываем левую кнопку мыши к методу onclick
объекта FlippingBitsGUI
. Запускаем приложение, вызывая метод mainloop
на корневом окне Tkinter
Таким образом, мы получаем следующий полный код нашей игры:
import tkinter as tk import random from copy import deepcopy class FlippingBitsGame: def __init__(self, level): self.level = level # Level = size of the square self.target = [[0] * level for _ in range(level)] # the board to obtain when you play self.board = [[0] * level for _ in range(level)] # the current board played by the user self.solved = True self.newgame() # new game method to define later # method to flip a column def flipcol(self, r): for i in range(len(self.board[r])): self.board[r][i] ^= 1 # 0 -> 1, 1 -> 0 # method to flip a row def fliprow(self, c): for row in self.board: row[c] ^= 1 # method to shuffle the board def shuffle(self): for _ in range(self.level * self.level): if random.random() > 0.5: self.flipcol(random.randint(0, self.level - 1)) else: self.fliprow(random.randint(0, self.level - 1)) # new game def newgame(self): if self.solved: # generate a new game while True: self.shuffle() self.target = deepcopy(self.board) # we make a deep copy of board into the target self.shuffle() # then we shuffle the board if self.issolved() == False: break self.solved = False # is solved method. We check if board == target def issolved(self): for i in range(self.level): for j in range(self.level): if self.board[i][j] != self.target[i][j]: return False self.solved = True return True # Now we need to write the code for the GUI class FlippingBitsGUI: def __init__(self, root, level): self.game = FlippingBitsGame(level) self.root = root self.root.title("Flipping Bits Game - SSaurel") self.root.geometry("700x800") self.canvas = tk.Canvas(self.root, width = 700, height = 800) self.canvas.pack() self.drawboard() # method to define letting us to draw the board for the user def drawboard(self): # we clean the canvas self.canvas.delete("all") squaresize = 500 / self.game.level targetsize = squaresize / 10 for i in range(self.game.level): x = 100 + i * squaresize for j in range(self.game.level): y = 100 + j * squaresize value = self.game.board[i][j] self.canvas.create_rectangle(x, y, x + squaresize, y + squaresize, fill = ("blue", "yellow")[value == 0]) # draw the target on the top left target = self.game.target[i][j] self.canvas.create_rectangle(x + 10, y + 10, x + 10 + targetsize, y + 10 + targetsize, fill = ("blue", "yellow")[target == 0]) if self.game.issolved(): self.canvas.create_text(200, 750, text = "Solved. Click to start a new game", font = ("Helvetica", "20", "bold")) def onclick(self, event): if self.game.solved: self.game.newgame() self.drawboard() return # we get coordinates of the user's click idx = int(event.x) idy = int(event.y) # check if you must flip a column or a row if (idx > 0 and idx < 100 and idy > 100 and idy < 600) or (idx > 600 and idx < 700 and idy > 100 and idy < 600): # we need to flip a row squaresize = 500 / self.game.level row = int((idy - 100) / squaresize) self.game.fliprow(row) elif (idy > 0 and idy < 100 and idx > 100 and idx < 600) or (idy > 600 and idy < 800 and idx > 100 and idx < 600): # we need to flip a col squaresize = 500 / self.game.level col = int((idx - 100) / squaresize) self.game.flipcol(col) self.drawboard() root = tk.Tk() gui = FlippingBitsGUI(root, 6) # we will use a 5x5 board gui.canvas.bind("<Button-1>", gui.onclick) root.mainloop() # Time to put our game in action :P # Watch the tutorial on YouTube => https://www.youtube.com/watch?v=mPZOLy9wui0
Наша игра Flipping Bits в действии
Наступила самая приятная часть урока – мы можем запустить нашу игру Flipping Bits Game в действие.
Вот первый дисплей, который нас ожидает:
Остается только найти решение. После нескольких щелчков мышью, в большей или меньшей степени в соответствии с вашим чувством логического мышления 😉, вы получите следующую решенную решетку:
Перевод статьи «Learning to Make a GUI in Python With Tkinter by Creating a Flipping Bits Game».