Изучаем Tkinter на примере игры Flipping Bits

Цель этого материала – показать, как использовать библиотеку графического интерфейса 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».

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *