January 23, 2020

Крестики-нолики на Python за 15 минут

Kivy - это открытая Python библиотека для быстрой разработки приложений. У нее много преимуществ, например, кросс-платформенность, открытость а также использование GPU для ускорения отрисовки графических интерфейсов.

Информацию об API вы можете найти тут.

Для начала работы нам потребуется библиотека kivy. Установить ее можно с помощью команды (Windows):

pip install kivy

Либо с помощью команды (Linux, Python 3):

pip3 install kivy

Импортируем необходимые нам элементы:

from kivy.app import App

from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout

from kivy.uix.button import Button
from kivy.config import Config

С помощью Config и функции set зададим параметры нашего окна:

Config.set("graphics", "resizable", "0")
Config.set("graphics", "width", "300")
Config.set("graphics", "height", "300")

Мы установили размер окна 300х300 и указали что окно нельзя перерастягивать.

Нам нужно, чтобы при каждом новом ходе крестик сменялся ноликом и наоборот. Для этого создадим класс нашего приложения, и назовем его MainApp. Оно должно наследоваться от класса kivy – App. Внутри класса создадим переменную self.switch, и определим ее в функции __init__:

class MainApp(App):
    def __init__(self):
        self.switch = True
        super().__init__()

Строчка с super() нужна для того, чтобы инициализировать родительский класс App.

Внутри класса мы также объявим 3 функции: tic_tac_toe, restart и build:

def tic_tac_toe(self):
    pass

def restart(self):
    pass

def build(self):
    pass

Поговорим подробнее о каждой из них.

  • tic_tac_toe: функция, которая будет исполнять всю логику нашего приложения, с английского tic tac как раз и означает крестики-нолики
  • restart: функция, которую мы будем вызывать для того, чтобы перезапустить игру, по сути сбросив все значения кнопок и переменных до начального состояния
  • build: важная функция, она определена в самом App, но ее поведение мы должны "перегрузить"

Начнем как раз с функции build.

def build(self):
    self.title = "Крестики-нолики"

    root = BoxLayout(orientation="vertical", padding=5)

    grid = GridLayout(cols=3)

self.title это название нашего окна, далее создадим объект BoxLayout, он будет являться своеобразным контейнером для всех наших кнопок. Также создадим переменную GridLayout, и укажем число колонок = 3. На рисунке ниже ее область будет показана красным цветом. После того, как отрисуем область сетки, добавим внизу кнопочку Restart (показана синим цветом).

self.buttons = []

После этого, в цикле мы добавим в лист 9 объектов класса Button. Каждый такой объект имеет цвет, размер шрифта и другие переменные. При нажатии на такую кнопку вызывается метод tic_tac_toe, о котором мы поговорим чуть позднее. В конце цикла не забываем добавить полученные в ходе тела цикла кнопки в наш лист self.buttons, кроме того, добавляем новый виджет в grid:

for _ in range(9):
    button = Button(
        color = [0,0,0,1],
        font_size = 24,
        disabled = False,
        on_press = self.tic_tac_toe
    )
    self.buttons.append(button)
    grid.add_widget(button)

Также добавим виджет для кнопки Restart:

root.add_widget(
    Button(
        text = "Restart",
        size_hint = [1,.1],
        on_press = self.restart
    )
)

Весь код функции build целиком:

def build(self):
    self.title = "Крестики-нолики"

    root = BoxLayout(orientation="vertical", padding=5)

    grid = GridLayout(cols=3)
    self.buttons = []
    for _ in range(9):
        button = Button(
            color = [0,0,0,1],
            font_size = 24,
            disabled = False,
            on_press = self.tic_tac_toe
        )
        self.buttons.append(button)
        grid.add_widget(button)

    root.add_widget(grid)

    root.add_widget(
        Button(
            text = "Restart",
            size_hint = [1,.1],
            on_press = self.restart
        )
    )

    return root

Поскольку мы добавили кнопку Restart, давайте поговорим об ее функционале. Добавим в функцию restart следующий код:

def restart(self, arg):
    self.switch = True

    for button in self.buttons:
        button.color = [0,0,0,1]
        button.text = ""
        button.disabled = False

Переменная switch = True показывает, что ход снова возвращается к крестикам, как и было указано в начале игры. Далее мы проходимся в цикле по всем кнопкам, возвращая им цвет по умолчанию и стирая текст. Модификатор disabled показывает, что теперь вы снова можете нажать на кнопку.

Но вернемся к функции tic_tac_toe. Для начала укажем снова глобальную переменную switch. Как вы помните, она переключается в обратное положение каждый раз после нажатия кнопки, чем символизирует переключение хода на другого игрока. В этой функции аргумент arg будет как раз являться нашим конкретным button.

При нажатии на кнопку, согласно с логикой приложения, мы должны выключить возможность нажать на нее второй раз. Для этого необходимо указать arg.disabled = True.

def tic_tac_toe(self, arg):
    arg.disabled = True

Теперь проверим в каком положении стоит наш self.switch, и в зависимости от этого, определим переменную arg.text крестиком либо ноликом:

def tic_tac_toe(self, arg):
    arg.disabled = True
    arg.text = 'X' if self.switch else 'O'

Фактически, выражение 'X' if self.switch else 'O' означает 'X' в случае если self.switch = True и 'O' в случае если self.switch = False.

После этого, разумеется, мы должны переключить ход:

self.switch = not self.switch

Далее начнется самое сложное. Нам необходимо задать все возможные координаты кнопок, которые дадут выигрыш:

coordinate = (
    (0,1,2),(3,4,5),(6,7,8), # по оси X
    (0,3,6),(1,4,7),(2,5,8), # по оси Y
    (0,4,8),(2,4,6),         # по диагонали D
)

Также зададим и возможные вектора самих нажатых кнопок (в них будет храниться буквы 'X' либо 'O'). Легче всего это сделать, написав короткую лямбда-функцию, которая будет возвращать список букв для каждого набора координат:

vector = lambda item: [self.buttons[x].text for x in item]

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

Определим переменную win = False, а также цвет кнопок в результате выигрыша:

win = False
color = [0,1,0,1] # Зеленый

Теперь напишем цикл, который будет определять победную комбинацию нажатых кнопок. В переменной coordinate есть 8 комбинаций с победными сочетаниями кнопок. Пройдемся по ним в цикле:

for item in coordinate:
    if vector(item).count('X') == 3\\
    or vector(item).count('O') == 3:
        win = True
        for i in item:
            self.buttons[i].color = color
            break

Если в таком векторе функция count посчитает 3 раза буквы 'X' либо 'O', значит игрок победил, в таком случае окрашиваем победную комбинацию кнопок в зеленый цвет.

Также необходимо залочить все кнопки в случае победы:

if win:
    for button in self.buttons:
        button.disabled = True

Внимательный читатель мог заметить, что переменная win в принципе и не нужна, а последний цикл можно переместить в цикл после перекрашивания цвета кнопок в зеленый. Полный код функции tic_tac_toe после рефакторинга:

def tic_tac_toe(self, arg):
    arg.disabled = True
    arg.text = 'X' if self.switch else 'O'
    self.switch = not self.switch

    coordinate = (
            (0,1,2),(3,4,5),(6,7,8), # X
            (0,3,6),(1,4,7),(2,5,8), # Y
            (0,4,8),(2,4,6),         # D
    )

    vector = lambda item: [self.buttons[x].text for x in item]

    color = [0, 1, 0, 1]

    for item in coordinate:
        if vector(item).count('X') == 3 \\
        or vector(item).count('O') == 3:
            for i in item:
                self.buttons[i].color = color
            for button in self.buttons:
                button.disabled = True
            break

Весь код целиком — https://pastebin.com/8iutyKKw


Материал подготовлен образовательной организацией Python Academy.