Создаём игру крестики-нолики на Kivy

Kivy — кросcплатформенный графический фреймворк на Python, направленный на создание новейших пользовательских интерфейсов даже для приложений, работающих с сенсорными экранами. Приложения, написанные на Kivy, могут работать не только на таких традиционных платформах как Linux, OS X и Windows, но также на Android, iOS и Rapberry Pi.

Это означает, что в разработке можно использовать различные библиотеки, как Requests, SQLAlchemy или даже NumPy. Допускается даже доступ к нативным мобильным API посредством дочерних проектов Kivy. Еще одна отличительная черта Cython — оптимизированный конвейерный обработчик OpenGL. При его помощи можно легко добиться сложных GPU эффектов, не прибегая к сложным конструкциям в коде.

По сути Kivy это набор модулей Python/Cython, которые можно легко установить при помощи pip. Но существует ряд зависимостей. Вам понадобится Pygame, он используется в качестве рендера, Cython для компиляции скоростных составляющих графических элементов и GStreamer для работы с мультимедиа. Как правило, все это легко установить при помощи стандартного менеджера пакетов или pip.

После установки всех зависимостей, можно установить и сам Kivy. Текущая версия — 1.8.0, которая работает как с Python2, так и с Python3. Код, использованный в этой статье должен успешно работать под версиями Python 2.7 и 3.3.

pip install kivy

Если у вас возникли проблемы с установкой через pip, то всегда можно воспользоваться easy_install. Существует несколько специальных репозиториев и готовых сборок для популярных дистрибутивов OS. На официальном сайте Kivy вы найдете более подробную информацию по теме.

Приложение Kivy всегда начинается с инициализации и запуска класса App. Именно он отвечает за создание окон, интрефейса с OS, а также предоставляет входную точку при создании элементов интерфейса пользователя. Начнем с простейшего приложения Kivy:

from kivy.app import App
class TicTacToeApp(App):
    pass
if __name__ == "__main__":
    TicTacToeApp().run()

Уже это приложение можно запустить, вы увидите пустое черное окно. Впечатляет, не так ли? Мы можем строить GUI при помощи виджетов Kivy. Каждый из них представляет собой простой графический элемент со своим функционалом от стандартного — кнопки, лейблы, текстовые поля ввода, или вспомогательные элементы для позиционирования дочерних, до более абстрактных — диалоги выбора файлов, камера или видео проигрыватель. Большинство основных виджетов Kivy легко сочетаются друг с другом, тем самым образуя новые варианты GUI, то есть, нет необходимости изобретать велосипед. В этой статье я приведу несколько примеров подобного использования виджетов.

Так как «Hello, world!» уже стало практически неизбежным примером, давайте поскорее его рассмотрим и перейдем к более интересным примерам. Воспользуемся обычным лейблом для отображения текста:

from kivy.uix.label import Label

Используем лейбл в качестве корневого виджета. У каждого приложения должен быть корневой виджет, он играет роль верхушки дерева элементов и автоматически принимает размер окна. Позднее мы рассмотрим как строить полноценные GUI при помощи большего количества виджетов, но на данном этапе нам достаточно, при помощи нового метода, вернуть корневой виджет:

class TicTacToeApp(App):
    def build(self):
        return Label(text="Hello World!",
            font_size=100,
            color=(0, 1, 0, 1))

Метод build исполняется при запуске приложения, и виджет, который он возвращает, автоматически занимает корневое положение. В нашем случае это лейбл, которому мы задали несколько параметров — text, font_size и color. Для каждого виджета существует свой набор параметров, контролирующих его поведение. Все их можно изменять динамически, но мы установим их лишь один раз при инициализации.

Обратите внимание, что эти свойства являются частью Kivy, а не Python. Они доступны как стандартные атрибуты класса, при этом они предоставляют дополнительный функционал через систему событий Kivy. Позднее мы рассмотрим примеры создания свойств работающих совместно с событийной системой Kivy.

Так вот, предыдущего примера вполне достаточно для отображения текста. В чем вы можете убедиться, запустив наше приложение. Если у вас возникли трудности с параметрами, которые мы задавали, поэкспериментируйте с ними.

Собственный виджет: крестики-нолики

Конечно, Kivy не имеет в своем составе виджета для игры в крестики-нолики, поэтому нам придется создать свой. Мы создадим новый класс виджета и опишем в нем необходимое нам поведение:

from kivy.uix.gridlayout import GridLayout
class TicTacToeGrid(GridLayout):
    pass

Очевидно, что пока этот класс не содержит в себе никакого функционала, за исключением того, что он унаследовал от GridLayout, которому мы укажем требуемое количество столбцов, а он уже, в свою очередь, разместит все дочерние элементы в пределах созданной сетки. Нам понадобится три столбца и девять дочерних элементов.

Настало время рассмотреть язык Kivy (kv) — специальный язык для описания дерева виджетов Kivy. Он одновременно очень прост и позволяет избежать большого объема монотонного кода Python. Python предоставляет нам возможность динамически манипулировать GUI элементами, но иногда нам надо просто описать базовую структуру GUI. В принципе, kv можно легко заменить python кодом, но в дальнейших примерах мы используем kv, как и большинство программистов.

Kivy изначально содержит в себе все необходимое для работы с kv. Проще всего создать новый файл с таким же именем как и App класс и описывать весь GUI в нем. То есть создадим файл tictactoe.kv и напишем в нем следующее:

<TicTacToeGrid>:
    cols: 3 # Number of columns

В этом весь синтаксис kv, для каждого виджета мы создаем свое правило, описывающее его поведение, настройки и дочерние виджеты. В примере демонстрируется правило для виджета tictactoe, которое указывает, что каждый объект класса TicTacToeGrid уже при создании получает значение 3 для свойства cols.

Мы еще вернемся к kv, но сейчас давайте создадим несколько кнопок для начала игры.

from kivy.uix.button import Button
from kivy.properties import ListProperty
class GridEntry(Button):
    coords = ListProperty([0, 0])

Мы наследуем свойства и методы виджета Button для взаимодействия с мышью или касаниями тач скрина и запуска соответствующих событий. То есть мы можем выполнять собственные функции, когда пользователь нажал на кнопку, и можем устанавливать свойство text кнопки в X или О. Мы также создали новое свойство для нашего виджета — coords — координаты, они очень нам помогут в дальнейшем. Создание новых свойств практически идентично созданию нового атрибута класса в Python — достаточно добавить self.coords = [0, 0] в GridEntry.__init__.

Также как и для TicTacToeGrid создадим ряд правил для нового виджета:

<GridEntry>:
    font_size: self.height

Как и раньше, первое правило описывает начальные параметры при инициализации виджета, на этот раз мы указываем размер шрифта для лейбла кнопки. Основное удобство использования языка kv заключается в том, что он автоматически определяет, что мы ссылаемся на собственный размер кнопки, и теперь если мы изменим размер кнопки, то и размер шрифта изменится соответственно.

Теперь заполним TicTacToeGrid виджетами GridEntry. Мы столкнемся с новыми элементами Kivy: при создании виджетов GridEntry мы сразу задаем им координаты, передав соответствующие параметры. Этот функционал полностью поддерживается Kivy.

class TicTacToeGrid(GridLayout):
    def __init__(self, *args, **kwargs):
        super(TicTacToeGrid, self).__init__(*args, **kwargs)
        for row in range(3):
            for column in range(3):
                grid_entry = GridEntry(
                    coords=(row, column))
                grid_entry.bind(on_release=self.button_pressed)
                self.add_widget(grid_entry)
    def button_pressed(self, button):
        print ('{} button clicked!'.format(button.coords))

Мы использовали метод bind для привязки обработчика button_pressed к событию on_release виджета GridEntry, так метод button_pressed будет выполнен каждый раз после того, как пользователь отпустил кнопку. Мы также могли использовать событие on_press, то есть событие при первом нажатии на кнопку. Таким образом, можно привязать свой обработчик к любому событию Kivy.

Мы добавили каждый виджет GridEntry при помощи метода add_widget. То есть каждый является дочерним элементом TicTacToeGrid и будет расположен в сетке в соответствии с заданными ранее нами параметрами.

Теперь нам осталось только вернуть этот виджет в качестве корневого и мы сможем посмотреть на наше приложение.

def build(self):
    return TicTacToeGrid()

Запустив главное Python приложение вы увидите только что созданный интерфейс игры. Все отлично, надпись текста была заменена на сетку из девяти кнопок, на каждую из которых можно нажать (она автоматически изменит цвет) и отпустить (в консоли вы увидите текст из обработчика). Конечно, можно и дальше изменять внешний вид кнопок, но пока мы оставим их как есть.

Так, а кто победил?

Нам понадобится следить за состоянием поля, чтобы определить победителя. Для этого изменим несколько свойств Kivy:

from kivy.properties import (ListProperty, NumericProperty)
class TicTacToeGrid(GridLayout):
    status = ListProperty([0, 0, 0, 0, 0, 0, 0, 0, 0])
    current_player = NumericProperty(1)

Таким образом, мы добавили способ отслеживания состояния поля и очередность хода (1 — О, -1 — Х). Разместив эти числа в списке мы сможем определить победителя, сумма столбца, строки или диагонали должна быть равна +-3. Теперь мы сможем обновить игровое поле после хода.

def button_pressed(self, button):
    # Create player symbol and colour lookups
    player = {1: 'O', -1: 'X'}
    colours = {1: (1, 0, 0, 1), -1: (0, 1, 0, 1)} # (r, g, b, a)
    row, column = button.coords # The pressed button is automatically
                              # passed as an argument
    # Convert 2D grid coordinates to 1D status index
    status_index = 3*row + column
    already_played = self.status[status_index]
    # If nobody has played here yet, make a new move
    if not already_played:
        self.status[status_index] = self.current_player
        button.text = {1: 'O', -1: 'X'}[self.current_player]
        button.background_color = colours[self.current_player]
        self.current_player *= -1 # Switch current player

Теперь после запуска приложения вы наглядно увидите, что именно мы сделали. После каждого нажатия на кнопку на ней появляется О или Х, а цвет фона будет изменятся, показывая чей ход следующий. Так же нажать на кнопку можно только один раз, так как при помощи массива мы отслеживаем состояние поля.

Теперь уже можно играть в крестики-нолики, но один важный элемент все же пока отсутствует — громадное окно во весь экран, оповещающее что мы выиграли! Но сначала необходимо добавить немного кода для определения окончания игры.

Здесь мы воспользуемся еще одним удобным элементов Kivy. Каждый раз при изменении свойства Kivy запускается метод on_propertyname и срабатывает соответствующее событие. Таким образом, мы запросто можем создать метод, который будет отрабатывать при каждом изменении свойства кнопки, как на Python, так и на kv. В нашем случае мы будем проверять список состояний каждый раз при его обновлении и выполнять определенный метод, если игра закончилась.

# ...
def on_status(self, instance, new_value):
    status = new_value
    # Sum each row, column and diagonal.
    # Could be shorter, but let's be extra
    # clear what’s going on
    sums = [sum(status[0:3]), # rows
           sum(status[3:6]), sum(status[6:9]), sum(status[0::3]), # columns
           sum(status[1::3]), sum(status[2::3]), sum(status[::4]), # diagonals
           sum(status[2:-2:2])]
    # Sums can only be +-3 if one player
    # filled the whole line
if 3 in sums:
    print("Os win!")
elif -3 in sums:
    print("Xs win!")
elif 0 not in self.status: # Grid full
    print("Draw!")

Таким образом, мы определяем победу или ничью, но результат мы сообщаем только в консоль. Будет логично каждый раз сбрасывать состояние игрового поля для начала новой игры и показывать счет.

# Note the *args parameter! It's important later when we make a binding
# to reset, which automatically passes an argument that we don't care about
def reset(self, *args):
    self.status = [0 for _ in range(9)]
    # self.children is a list containing all child widgets
    for child in self.children:
        child.text = ''
        child.background_color = (1, 1, 1, 1)
self.current_player = 1

Теперь мы можем изменить метод on_status, чтобы сбросить состоянии игры в начальное и показать победителя при помощи виджета ModalView.

from kivy.uix.modalview import ModalView

Это виджет всплывающего окна, который отображается поверх всех остальных виджетов. Он автоматически закрывается при нажатии на него.

# ...
winner = None
if -3 in sums:
    winner = 'Xs win!'
elif 3 in sums:
    winner = 'Os win!'
elif 0 not in self.status:
    winner = 'Draw...nobody wins!'
if winner:
    popup = ModalView(size_hint=(0.75, 0.5))
    victory_label = Label(text=winner, font_size=50)
    popup.add_widget(victory_label)
    popup.bind(on_dismiss=self.reset)
    popup.open()

По сути ничего нового здесь нет, мы добавили текстовый лейбл к ModalView и при помощи методов виджета ModalView показали само окно. Также мы воспользовались событием on_dismiss, которое срабатывает при закрытии окна. Наконец, мы установили общее для всех виджетов свойство size_hint, которое в нашем случае задает размер модального окна относительно основного окна. Пока открыто модальное окно, вы можете изменять размер главного окна и вы увидите, что модальное окно будет также изменяться в размерах. В данном случае мы воспользовались привязкой свойства hint_size, а обработку полностью взял на себя Kivy.

Вот и все, игра готова. Теперь мы можем не только играть в крестики-нолики, но наша программа также выдает сообщение при окончании игры и сбрасывает состояние игры.

Пора поэкспериментировать

Мы рассмотрели основы работы с Kivy, но, я надеюсь, что вы увидели принцип построения приложений на Kivy. Мы строим приложения из отдельных виджетов, которые взаимодействуют друг с другом через изменение параметров или срабатывании событий. Мы также коротко коснулись языка kv и привели пример использования автоматического связывания свойств с событиями.

В архиве{:target=»blank»} вы найдете полную копию программы для самопроверки. Мы также добавили дополнительный виджет, Interface, чтобы продемонстрировать как полностью построить структуру игры на языке kv, то есть мы показали еще один способ добавлять дочерние элементы. Проверить его вы сможете убрав комментарии со строки return Interface() в TicTacToeGrid.build. Мы не изменили ничего глобального, а всего лишь показали более углубленный пример использования языка kv, где мы использовали привязку свойств, чтобы автоматически обновлять текст на кнопках и изменять размеры поля, таким образом, чтобы оно всегда оставалось пропорциональным. Вы можете поэкспериментировать со свойствами, чтобы окончательно разобраться: за что отвечает каждое из них или изменить типы виджетов и посмотреть как будут реагировать на них другие виджеты.