Простейшая змейка на Python менее, чем в 100 строчек кода

На самом деле строчек кода с логикой игры будет гораздо меньше, добрую половину скрипта занимает подготовка игрового поля, рисование новорождённой змеи, назначение клавиш управления и ещё парочка мелочей. Публикация может быть полезна для таких же начинающих, как и я сам. И да, в коде могут быть ошибки, которые я в настоящий момент не вижу в силу своего небольшого опыта программирования, заканчивающегося на прочтении Марка Лутца и пока ещё недописанном телеграм боте, но змейка работает исправно и делает всё, что было задумано. Писать буду с использованием модуля turtle. Погнали.

import time
import turtle
from random import randrange

BREAK_FLAG = False

Импортируем необходимые модули и задаём глобальную переменную. Модуль time понадобится для установки паузы в основном бесконечном цикле программы, turtle будет отвечать за графику в игре, из модуля random возьмём один метод randrange для генерации координат еды для нашей змеи. С помощью переменной BREAK_FLAG будем останавливать игру при укусе змейки самой себя, подробнее об этом позже.

# draw a window for the game
screen = turtle.Screen()
screen.title('Snake with turtle module')
screen.bgcolor('orange')
screen.setup(650, 650)
screen.tracer(0)

Следующим шагом создаём окно игры, назначаем название, задаём цвет фона и размеры окна. Строка screen.tracer(0) отключает анимацию, рисовать кадры будем сами в основном цикле программы. Если этого не сделать вновь созданный сегмент змейки и новый элемент еды, после поедания добычи, будет появляться в центре поля и только потом перемещаться в заданные координаты и весь этот процесс мы будем видеть. Это особенность библиотеки turtle, каждый новый объект всегда появляется в центре координат.

# draw a game field border
border = turtle.Turtle()
border.hideturtle()
border.penup()
border.goto(-311, 311)
border.pendown()
border.goto(311, 311)
border.goto(311, -311)
border.goto(-311, -311)
border.goto(-311, 311)

Для наглядности нарисуем границы игрового поля. Создаём объект черепашки border = turtle.Turtle(), делаем его невидимым border.hideturtle(), так как от него нам понадобится только линия, которую он чертит при перемещении. И опуская и поднимая перо border.penup(), border.pendown() перемещаем нашу черепашку по строго заданным координатам. На выходе получаем чёрный квадрат, границы которого нельзя будет пересекать нашей змее.

# draw a snake of three segments and
# paint the head of the snake in black
snake = []
for i in range(3):
    snake_segment = turtle.Turtle()
    snake_segment.shape('square')
    snake_segment.penup()
    if i > 0:
        snake_segment.color('gray')
    snake.append(snake_segment)

Создадим змейку. Наша вновь рождённая змея будет состоять из трёх сегментов каждый из которых будет являться новым экземпляром класса Turtle. Другими словами змея будет состоять из множества черепашек. Надеюсь контекст слова «черепашка» в этой публикации понятен всем без объяснений. Хранить змейку целиком будем в виде списка в переменной snake. Создаём змею в цикле for, который прогоняем три раза. Создаём новый сегмент snake_segment = turtle.Turtle(), задаём форму snake_segment.shape('square') и поднимаем перо snake_segment.penup() так, как нам не надо, чтобы змейка оставляла после себя след. Условие if необходимо для окраски сегментов в серый цвет. Красятся все кроме первого, голова остаётся чёрной. В конце каждой итерации добавляем сегмент в список хранящий всю змею целиком snake.append(snake_segment).

# draw a food for the snake
food = turtle.Turtle()
food.shape('circle')
food.penup()
food.goto(randrange(-300, 300, 20), randrange(-300, 300, 20))

В этом блоке кода создаём объект еды, задаём ему круглую форму, генерируем координаты и перемещаем еду на определённое для неё место.

# snake control
screen.onkeypress(lambda: snake[0].setheading(90), 'Up')
screen.onkeypress(lambda: snake[0].setheading(270), 'Down')
screen.onkeypress(lambda: snake[0].setheading(180), 'Left')
screen.onkeypress(lambda: snake[0].setheading(0), 'Right')
screen.listen()

Управлять змейкой будем кнопками навигации со стрелками. За привязку клавиш отвечает метод screen.onkeypress(). Методу необходимо передать в качестве аргументов функцию и имя кнопки, которая будет вызывать функцию. Метод setheading() задаёт направление движения объекта черепашки. Использовать его будем в таком виде snake[0].setheading(90), то есть голову змейки повернуть на 90 градусов относительно направления по умолчанию. Для нас это значит вверх. Оборачиваем метод в лямбда выражение, это отложит его вызов до момента нажатия на клавишу. Имя кнопки передаём в виде строки 'Up' повторяем процедуру для остальных направлений. Начинаем слушать события с клавиатуры screen.listen()

Ну и наконец самое интересное. Логика игры будет обрабатываться в бесконечном цикле. Для наглядности положу его полностью здесь под
спойлером
while True:

    # creating a new segment of the snake
    # and redraw a food for the snake
    if snake[0].distance(food) < 10:
        food.goto(randrange(-300, 300, 20), randrange(-300, 300, 20))
        snake_segment = turtle.Turtle()
        snake_segment.shape('square')
        snake_segment.color('gray')
        snake_segment.penup()
        snake.append(snake_segment)

    # snake body movement
    for i in range(len(snake)-1, 0, -1):
        x = snake[i-1].xcor()
        y = snake[i-1].ycor()
        snake[i].goto(x, y)

    # snake head movement
    snake[0].forward(20)

    screen.update()

    # snake collision with border
    x_cor = snake[0].xcor()
    y_cor = snake[0].ycor()
    if x_cor > 300 or x_cor < -300:
        screen.bgcolor('red')
        break
    if y_cor > 300 or y_cor < -300:
        screen.bgcolor('red')
        break

    # snake collision with itself
    for i in snake[1:]:
        i = i.position()
        if snake[0].distance(i) < 10:
            BREAK_FLAG = True
    if BREAK_FLAG:
        screen.bgcolor('red')
        break
    time.sleep(0.2)



    # creating a new segment of the snake
    # and redraw a food for the snake
    if snake[0].distance(food) < 10:
        food.goto(randrange(-300, 300, 20), randrange(-300, 300, 20))
        snake_segment = turtle.Turtle()
        snake_segment.shape('square')
        snake_segment.color('gray')
        snake_segment.penup()
        snake.append(snake_segment)

А теперь по порядку. Если расстояние от головы змеи до еды меньше 10 if snake[0].distance(food) < 10:, то есть змейка съела добычу, генерируем и размещаем новую добычу, создаём новый сегмент змейки и помещаем его в список snake.

    # snake body movement
    for i in range(len(snake)-1, 0, -1):
        x = snake[i-1].xcor()
        y = snake[i-1].ycor()
        snake[i].goto(x, y)

Этот блок кода отвечает за перемещение тела змеи. Перебираем в цикле for сегменты змеи начиная с хвоста. Получаем координаты x и y предпоследнего сегмента методами xcor() и ycor() перемещаем на их место последний сегмент snake[i].goto(x, y) и так двигаемся до самой головы.

    # snake head movement
    snake[0].forward(20)

Саму же голову двигаем отдельно на 20 пикселей вперёд на каждой итерации бесконечного цикла while. Таким образом мы управляем только первым сегментом змейки, остальные просто повторяют его движение перебираясь в цикле for.

По правде говоря, с передвижением тела змеи у меня возникли некоторые проблемы. Текущую реализацию цикла for i in range(len(snake)-1, 0, -1) я подсмотрел на гитхабе. Но мне показалось не логичным начинать движение с хвоста. Попытки переписать цикл в обратном направлении(то есть начинать перемещение с головы) успехом не увенчались. Поэтому пользуясь случаем обращаюсь к опытным читателям показать возможную реализацию цикла в другом направлении. Если конечно в этом есть смысл, может мне просто кажется текущее решение не логичным.

screen.update()

Двигаемся дальше. screen.update() обновляет кадр, то есть по сути отвечает за анимацию, которую мы отключили в самом начале.

# snake collision with border
    x_cor = snake[0].xcor()
    y_cor = snake[0].ycor()
    if x_cor > 300 or x_cor < -300:
        screen.bgcolor('red')
        break
    if y_cor > 300 or y_cor < -300:
        screen.bgcolor('red')
        break

Эти два условия проверяют расстояние от головы змеи до граничных координат. Если голова Выходит за предельные значение окрашиваем экран в красный цвет и прерываем работу главного цикла, Game Over короче.

    # snake collision with itself
    for i in snake[1:]:
        i = i.position()
        if snake[0].distance(i) < 10:
            BREAK_FLAG = True
    if BREAK_FLAG:
        screen.bgcolor('red')
        break

В этом блоке кода реализовано поведение игры при укусе змейки самой себя. В цикле for перебираем все сегменты змеи кроме головы. Сравниваем расстояние от головы до текущего сегмента и если оно меньше 10 if snake[0].distance(i) < 10:, что равно укусу, передаём значение True глобальной переменной BREAK_FLAG. Далее проверяем на истинность BREAK_FLAG и если оно True красим экран в красный и останавливаем игру. Если False переходим к следующей строке.

time.sleep(0.2)

Метод time.sleep(0.2) останавливает цикл на 20 мс, значением аргумента метода можно управлять скоростью игры.

screen.mainloop()
И последняя строка завершает код. Метод mainloop() должен всегда завершать программу написанную с использованием модуля turtle.

На этом всё, буду рад любой критике. Надеюсь было интересно и возможно для кого-то даже полезно. Исходный код змейки можно найти в моём github аккаунте. Программа написана в ОС Ubuntu 18.04, на Windows машине графика выглядит расплывчато и слишком крупно, но змейка вполне рабочая.
Tags:
python

You can't comment this post because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author's username will be hidden by an alias.