Языки программирования

Библиотека Tkinter: Разработка игры «Змейка»

В этой статье мы напишем игру змейка на Python. Это одна из самых распространенных аркад в мире. В основе ее реализации будет три класса:
  • Класс сегмента (одного блока змейки).
  • Класс змеи (по сути собрание сегментов).
  • Класс подсчета очков.
В процессе написания змейки у нас будут следующие этапы:
  1. Создание окна приложения;
  2. Объявление вспомогательных переменных;
  3. Создание игрового поля;
  4. Создание классов сегмента, змеи и очков;
  5. Создание разных дополнительных функций;
  6. Создание функций для начала новой игры и выхода из игры.

Создание окна приложения

Первым делом нужно разработать каркас для нашего приложения — главное окно. Листинг:
from tkinter import *

# Настройка главного окна
root = Tk()
root.title("Змейка")

# запускаем окно
root.mainloop()

Объявление вспомогательных переменных

Добавляем в нашу программу вспомогательные переменные. Их нужно добавить сразу после оператора импорта tkinter:
# Ширина экрана
WIDTH = 800

# Высота экрана
HEIGHT = 600

# Размер сегмента змеи
SEG_SIZE = 20

# Переменная, отвечающая за состояние игры
IN_GAME = True
Это глобальные переменные, которые мы будем использовать в процессе игры.

Создание игрового поля

Игровое поле мы реализуем с помощью Canvas. Создадим холст нужного нам размера и зальем его черным цветом. Листинг:
# Создаем экземпляр класса Canvas
c = Canvas(root, width=WIDTH, height=HEIGHT, bg="#000000")
c.grid()

# Захватываем фокус для отлавливания нажатий клавиш
c.focus_set()

Создание основных классов

Класс сегмента змеи

Первый класс — это сегмент змейки. Визуально сегмент змейки будет представлен обычным прямоугольником, созданным при помощи метода create_rectangle класса Canvas модуля tkinter. Листинг:
class Segment(object):
    """ Сегмент змейки """
    def __init__(self, x, y):
        self.instance = c.create_rectangle(x, y,
                                           x + SEG_SIZE, y + SEG_SIZE,
                                           fill="green")

Класс змейки

Змейка у нас будет набором сегментов. У нее будут методы движения, изменения направления и добавления сегмента. Листинг:
class Snake(object):
    """ Класс змейки """
    def __init__(self, segments):
        self.segments = segments

        # Варианты движения
        self.mapping = {"Down": (0, 1), "Right": (1, 0),
                        "Up": (0, -1), "Left": (-1, 0)}

        # инициируем направление движения
        self.vector = self.mapping["Right"]

    def move(self):
        """ Двигаем змейку в заданном направлении """
        for index in range(len(self.segments) - 1):
            segment = self.segments[index].instance
            x1, y1, x2, y2 = c.coords(self.segments[index + 1].instance)
            c.coords(segment, x1, y1, x2, y2)

        x1, y1, x2, y2 = c.coords(self.segments[-2].instance)
        c.coords(self.segments[-1].instance,
                 x1 + self.vector[0] * SEG_SIZE, y1 + self.vector[1] * SEG_SIZE,
                 x2 + self.vector[0] * SEG_SIZE, y2 + self.vector[1] * SEG_SIZE)

    def add_segment(self):
        """ Добавляем сегмент змейки """
        score.increment()
        last_seg = c.coords(self.segments[0].instance)
        x = last_seg[2] - SEG_SIZE
        y = last_seg[3] - SEG_SIZE
        self.segments.insert(0, Segment(x, y))

    def change_direction(self, event):
        """ Изменение направления движения змейки """
        if event.keysym in self.mapping:
            self.vector = self.mapping[event.keysym]

    # Функция обновления змейки при старте новой игры
    def reset_snake(self):
        for segment in self.segments:
            c.delete(segment.instance)
Конструктор (метод __init__) задает список доступных направлений движения змейки и задает направление по умолчанию — вправо.

Метод move() двигает змейку в заданном направлении. Метод change_direction() изменяет направление движения змейки.

За добавление нового сегмента отвечает метод add_segment(). Метод reset_snake() отвечает за обновления змейки при старте новой игры. Внимательно читайте все комментарии к коду.

Добавим в код функцию для создания сегментов и змейки:
# Создаем сегменты и змейку
def create_snake():
    segments = [Segment(SEG_SIZE, SEG_SIZE),
                Segment(SEG_SIZE * 2, SEG_SIZE),
                Segment(SEG_SIZE * 3, SEG_SIZE)]
    return Snake(segments)

Класс подсчета очков

Последний класс отвечает за счет очков при съедании змейкой сегмента:
# Подсчет очков
class Score(object):

    # функция отображения очков на экране
    def __init__(self):
        self.score = 0
        self.x = 55
        self.y = 15
        c.create_text(self.x, self.y, text="Счёт: {}".format(self.score), font="Arial 20",
                      fill="Yellow", tag="score", state='hidden')

    # функция счета очков и вывод на экран
    def increment(self):
        c.delete("score")
        self.score += 1
        c.create_text(self.x, self.y, text="Счёт: {}".format(self.score), font="Arial 20",
                      fill="Yellow", tag="score")

    # функция сброса очков при начале новой игры
    def reset(self):
        c.delete("score")
        self.score = 0
Конструктор (метод __init__) отображает очки на экране при поедании змейкой сегментов. Метод increment() ведет счет очков. Метод reset() обновляет очки при старте новой игры.

Для работы нашего класса необходимо добавить строчку в основной экран нашего приложения:
# Считаем очки
score = Score()

Создание разных дополнительных функций

Нам понадобятся две вспомогательные функции. Первая — create_block(), которая создает пищу для змейки. В нашем случае это будет яблоко — кружок зеленого цвета. Чтобы эта функция работала необходимо импортировать модуль random:
import random
Листинг функции:
# Вспомогательная функция
def create_block():
    """ Создаем еду для змейки """
    global BLOCK
    posx = SEG_SIZE * random.randint(1, (WIDTH - SEG_SIZE) / SEG_SIZE)
    posy = SEG_SIZE * random.randint(1, (HEIGHT - SEG_SIZE) / SEG_SIZE)
    BLOCK = c.create_oval(posx, posy,
                          posx + SEG_SIZE, posy + SEG_SIZE,
                          fill="green")
Следующая вспомогательная функция main() будет управлять игровым процессом — именно она будет управлять тем, куда пойдет змейка, обрабатывает столкновения с границами экрана, отвечает за поедание яблок и увеличение змейки. Листинг:
def main():
    """ Моделируем игровой процесс """
    global IN_GAME
    if IN_GAME:
        s.move()

        # Определяем координаты головы
        head_coords = c.coords(s.segments[-1].instance)
        x1, y1, x2, y2 = head_coords

        # столкновения с краями игрового поля
        if x2 > WIDTH or x1 < 0 or y1 < 0 or y2 > HEIGHT:
            IN_GAME = False

        # Поедание яблока
        elif head_coords == c.coords(BLOCK):
            s.add_segment()
            c.delete(BLOCK)
            create_block()

        # Поедание змейки
        else:
            for index in range(len(s.segments) - 1):
                if head_coords == c.coords(s.segments[index].instance):
                    IN_GAME = False

        # скорость змейки
        root.after(100, main)
    # Не IN_GAME -> останавливаем игру и выводим сообщения
    else:
        set_state(restart_text, 'normal')
        set_state(game_over_text, 'normal')
        set_state(close_but, 'normal')

Создание функций для начала новой игры и выхода из игры

При проигрыше в нашей игре необходимо, чтобы можно было начать новую игру без перезагрузки приложения и выхода из игры. Разработаем данные функции: Листинг:
# функция для вывода сообщения
def set_state(item, state):
    c.itemconfigure(item, state=state)
    c.itemconfigure(BLOCK, state='hidden')

# Функция для нажатия кнопки (новая игра)
def clicked(event):
    global IN_GAME
    s.reset_snake()
    IN_GAME = True
    c.delete(BLOCK)
    score.reset()
    c.itemconfigure(restart_text, state='hidden')
    c.itemconfigure(game_over_text, state='hidden')
    c.itemconfigure(close_but, state='hidden')
    start_game()

# Функция для старта игры
def start_game():
    global s
    create_block()
    s = create_snake()

    # Реагируем на нажатие клавиш
    c.bind("<KeyPress>", s.change_direction)
    main()

# выход из игры
def close_win(root):
    exit()
В главном окне необходимо добавить строки:
# Текст результата игры
game_over_text = c.create_text(WIDTH / 2, HEIGHT / 2, text="Ты проиграл!",
                               font='Arial 20', fill='red',
                               state='hidden')
                               
# Текст начала новой игры после проигрыша
restart_text = c.create_text(WIDTH / 2, HEIGHT - HEIGHT / 3,
                             font='Arial 25',
                             fill='green',
                             text="Начать новую игру",
                             state='hidden')

# Текст выхода из программы после проигрыша
close_but = c.create_text(WIDTH / 2, HEIGHT - HEIGHT / 5, font='Arial 25',
                          fill='green',
                          text="Выход из игры",
                          state='hidden')

# Отработка событий при нажимания кнопок
c.tag_bind(restart_text, "<Button-1>", clicked)
c.tag_bind(close_but, "<Button-1>", close_win)

# Запускаем игру
start_game()
На этом наша разработка игры подошла к завершению. Если вы все правильно сделали, тогда у вас должна заработать игра. Подробнее изучите наш код и пишите в комментариях свои предложения по изменению.

Полный исходный код

В данном разделе мы оставим полный исходный код, чтобы вы смогли свериться с вашим вариантом. Листинг:
from tkinter import *
import random

# Создаем глобальные переменные
# Ширина экрана
WIDTH = 800

# Высота экрана
HEIGHT = 600

# Размер сегмента змеи
SEG_SIZE = 20

# Переменная, отвечающая за состояние игры
IN_GAME = True


# Вспомогательная функция
def create_block():
    """ Создаем еду для змейки """
    global BLOCK
    posx = SEG_SIZE * random.randint(1, (WIDTH - SEG_SIZE) / SEG_SIZE)
    posy = SEG_SIZE * random.randint(1, (HEIGHT - SEG_SIZE) / SEG_SIZE)
    BLOCK = c.create_oval(posx, posy,
                          posx + SEG_SIZE, posy + SEG_SIZE,
                          fill="green")

# Подсчет очков
class Score(object):

    # функция отображения очков на экране
    def __init__(self):
        self.score = 0
        self.x = 55
        self.y = 15
        c.create_text(self.x, self.y, text="Счёт: {}".format(self.score), font="Arial 20",
                      fill="Yellow", tag="score", state='hidden')

    # функция счета очков и вывод на экран
    def increment(self):
        c.delete("score")
        self.score += 1
        c.create_text(self.x, self.y, text="Счёт: {}".format(self.score), font="Arial 20",
                      fill="Yellow", tag="score")

    # функция сброса очков при начале новой игры
    def reset(self):
        c.delete("score")
        self.score = 0

# Функция для управления игровым процессом
def main():
    """ Моделируем игровой процесс """
    global IN_GAME
    if IN_GAME:
        s.move()

        # Определяем координаты головы
        head_coords = c.coords(s.segments[-1].instance)
        x1, y1, x2, y2 = head_coords

        # столкновения с краями игрового поля
        if x2 > WIDTH or x1 < 0 or y1 < 0 or y2 > HEIGHT:
            IN_GAME = False

        # Поедание яблока
        elif head_coords == c.coords(BLOCK):
            s.add_segment()
            c.delete(BLOCK)
            create_block()

        # Поедание змейки
        else:
            for index in range(len(s.segments) - 1):
                if head_coords == c.coords(s.segments[index].instance):
                    IN_GAME = False

        # скорость змейки
        root.after(100, main)
    # Не IN_GAME -> останавливаем игру и выводим сообщения
    else:
        set_state(restart_text, 'normal')
        set_state(game_over_text, 'normal')
        set_state(close_but, 'normal')

class Segment(object):
    """ Сегмент змейки """
    def __init__(self, x, y):
        self.instance = c.create_rectangle(x, y,
                                           x + SEG_SIZE, y + SEG_SIZE,
                                           fill="green")
                                           
class Snake(object):
    """ Класс змейки """
    def __init__(self, segments):
        self.segments = segments

        # Варианты движения
        self.mapping = {"Down": (0, 1), "Right": (1, 0),
                        "Up": (0, -1), "Left": (-1, 0)}

        # инициируем направление движения
        self.vector = self.mapping["Right"]

    def move(self):
        """ Двигаем змейку в заданном направлении """
        for index in range(len(self.segments) - 1):
            segment = self.segments[index].instance
            x1, y1, x2, y2 = c.coords(self.segments[index + 1].instance)
            c.coords(segment, x1, y1, x2, y2)

        x1, y1, x2, y2 = c.coords(self.segments[-2].instance)
        c.coords(self.segments[-1].instance,
                 x1 + self.vector[0] * SEG_SIZE, y1 + self.vector[1] * SEG_SIZE,
                 x2 + self.vector[0] * SEG_SIZE, y2 + self.vector[1] * SEG_SIZE)

    def add_segment(self):
        """ Добавляем сегмент змейки """
        score.increment()
        last_seg = c.coords(self.segments[0].instance)
        x = last_seg[2] - SEG_SIZE
        y = last_seg[3] - SEG_SIZE
        self.segments.insert(0, Segment(x, y))

    def change_direction(self, event):
        """ Изменение направления движения змейки """
        if event.keysym in self.mapping:
            self.vector = self.mapping[event.keysym]

    # Функция обновления змейки при старте новой игры
    def reset_snake(self):
        for segment in self.segments:
            c.delete(segment.instance)

# функция для вывода сообщения
def set_state(item, state):
    c.itemconfigure(item, state=state)
    c.itemconfigure(BLOCK, state='hidden')


# Функция для нажатия кнопки (новая игра)
def clicked(event):
    global IN_GAME
    s.reset_snake()
    IN_GAME = True
    c.delete(BLOCK)
    score.reset()
    c.itemconfigure(restart_text, state='hidden')
    c.itemconfigure(game_over_text, state='hidden')
    c.itemconfigure(close_but, state='hidden')
    start_game()

# Функция для старта игры
def start_game():
    global s
    create_block()
    s = create_snake()

    # Реагируем на нажатие клавиш
    c.bind("<KeyPress>", s.change_direction)
    main()


# Создаем сегменты и змейку
def create_snake():
    segments = [Segment(SEG_SIZE, SEG_SIZE),
                Segment(SEG_SIZE * 2, SEG_SIZE),
                Segment(SEG_SIZE * 3, SEG_SIZE)]
    return Snake(segments)
    
# выход из игры
def close_win(root):
    exit()

# Настройка главного окна
root = Tk()
root.title("Змейка")

# Создаем экземпляр класса Canvas
c = Canvas(root, width=WIDTH, height=HEIGHT, bg="#000000")
c.grid()

# Захватываем фокус для отлавливания нажатий клавиш
c.focus_set()

# Текст результата игры
game_over_text = c.create_text(WIDTH / 2, HEIGHT / 2, text="Ты проиграл!",
                               font='Arial 20', fill='red',
                               state='hidden')
                               
# Текст начала новой игры после проигрыша
restart_text = c.create_text(WIDTH / 2, HEIGHT - HEIGHT / 3,
                             font='Arial 25',
                             fill='green',
                             text="Начать новую игру",
                             state='hidden')

# Текст выхода из программы после проигрыша
close_but = c.create_text(WIDTH / 2, HEIGHT - HEIGHT / 5, font='Arial 25',
                          fill='green',
                          text="Выход из игры",
                          state='hidden')

# Отработка событий при нажимания кнопок
c.tag_bind(restart_text, "<Button-1>", clicked)
c.tag_bind(close_but, "<Button-1>", close_win)

# Считаем очки
score = Score()

# Запускаем игру
start_game()

# запускаем окно
root.mainloop()
Интерфейс приложения:
Библиотека Tkinter