Коротко: «Змейку» на Python можно собрать на стандартном модуле turtle, без каких-либо дополнительных библиотек: готовая игра занимает 169 строк вместе с пустыми и комментариями. Собираем её за семь шагов: поле и голова, управление стрелками, движение по таймеру ontimer, яблоко в случайной клетке, хвост из списка квадратиков, счёт на экране, конец игры с рестартом. После каждого шага программа запускается и что-то умеет.
Вам понадобится только Python. Модуль turtle входит в стандартную библиотеку: с установщиком с python.org на Windows и macOS он работает сразу, а в некоторых сборках Linux графическую часть (tkinter) ставят отдельным пакетом, в Ubuntu это python3-tk. Как поставить сам Python, расписано в инструкции для Windows. Если черепашка вам совсем в новинку, сначала поиграйте с рисунками черепашкой: там те же goto, penup и color.
Как змейка устроена внутри
Поле поделено на клетки 20×20 пикселей. Голова — квадратик-черепашка, который раз в 150 миллисекунд прыгает на соседнюю клетку. Стрелки голову не двигают, они только меняют направление. Шаг делает игровой цикл. Хвост — список таких же квадратиков, повторяющих путь головы. Яблоко — красный кружочек. Остальная игра состоит из трёх проверок: голова на яблоке, голова в стене, голова в собственном хвосте.
Шаг 1. Поле и голова
Создаём окно 600×400, рисуем белую рамку и ставим голову в центр. Строка screen.tracer(0) выключает автоматическую анимацию: без неё каждое движение черепашки рисуется отдельно, и игра мигает. Кадр теперь показываем сами, командой screen.update().
import turtle
STEP = 20 # размер клетки: голова прыгает на 20 пикселей
screen = turtle.Screen()
screen.title("Змейка")
screen.bgcolor("black")
screen.setup(600, 400)
screen.tracer(0) # выключаем автоанимацию, кадр показываем сами
# рамка поля
pen = turtle.Turtle()
pen.hideturtle()
pen.color("white")
pen.penup()
pen.goto(-250, -150)
pen.pendown()
for side in [500, 300, 500, 300]:
pen.forward(side)
pen.left(90)
# голова змейки
head = turtle.Turtle()
head.shape("square")
head.color("lime")
head.penup()
head.direction = "stop"
screen.update()
screen.mainloop()
Запустите файл, и вы увидите чёрное окно с рамкой и зелёным квадратиком посередине. head.direction — наша собственная переменная, прикреплённая к черепашке. Python позволяет добавить объекту любой атрибут, и в нём голова помнит, куда ползёт. Пока никуда: "stop".
Шаг 2. Управление стрелками через onkey
Функция move() сдвигает голову на одну клетку в сторону, записанную в head.direction. Четыре функции go_... меняют направление, а screen.onkey() привязывает их к стрелкам. Вставьте блок перед двумя последними строками шага 1:
def move():
if head.direction == "up":
head.sety(head.ycor() + STEP)
elif head.direction == "down":
head.sety(head.ycor() - STEP)
elif head.direction == "left":
head.setx(head.xcor() - STEP)
elif head.direction == "right":
head.setx(head.xcor() + STEP)
def go_up():
head.direction = "up"
move() # временно: один шаг на нажатие
screen.update()
def go_down():
head.direction = "down"
move()
screen.update()
def go_left():
head.direction = "left"
move()
screen.update()
def go_right():
head.direction = "right"
move()
screen.update()
screen.listen()
screen.onkey(go_up, "Up")
screen.onkey(go_down, "Down")
screen.onkey(go_left, "Left")
screen.onkey(go_right, "Right")
На этом шаге каждое нажатие делает ровно один шаг, чтобы вы видели, что стрелки работают. Не реагирует на клавиши? Щёлкните мышкой по окну. Нажатия приходят только в окно, которое в фокусе, а screen.listen() включает их приём.
Шаг 3. Движение по таймеру: почему ontimer, а не while True
Настоящая змейка ползёт сама, без ваших нажатий. Уберите из каждой функции go_... строки move() и screen.update(), добавьте вверху DELAY и функцию loop(), а внизу вместо screen.update() вызовите loop(). Три точки в коде обозначают строки, которые не изменились:
DELAY = 150 # пауза между шагами, мс
...
def go_up():
head.direction = "up"
...
def loop():
move()
screen.update()
screen.ontimer(loop, DELAY) # «вызови loop ещё раз через 150 мс»
...
loop()
screen.mainloop()
ontimer(loop, DELAY) означает «вызови loop ещё раз через 150 мс». Функция не крутится бесконечно. Она делает один шаг и заказывает следующий, а между шагами управление у mainloop(): это внутренний цикл turtle, который принимает клавиши и перерисовывает окно.
Во многих примерах в интернете вы найдёте другой вариант:
import time
while True:
move()
screen.update()
time.sleep(0.15)
Голову это тоже двигает, но забирает управление у черепашки. Пока идёт time.sleep(), окно стоит: нажатия ждут в очереди до следующего update(). Забыли update() в цикле — окно зависает совсем. Закрыли окно — цикл пытается рисовать в то, чего уже нет, и в консоли вылетает ошибка. А mainloop() после такого цикла не выполнится никогда. С ontimer ждёт сама черепашка, и скорость игры задаёт одно число.
Голова пока пролетает сквозь рамку и исчезает за краем. Стены появятся на шаге 7.
Шаг 4. Яблоко в случайном месте и столкновение
Яблоко — ещё одна черепашка, только круглая и красная. Функция place_food() бросает её в случайную клетку, а loop() после каждого шага проверяет, стоит ли голова на яблоке:
import random
...
# яблоко
food = turtle.Turtle()
food.shape("circle")
food.color("red")
food.penup()
def place_food():
# только в клетки сетки: координаты кратны STEP
x = random.randint(-12, 12) * STEP
y = random.randint(-7, 7) * STEP
food.goto(x, y)
...
def loop():
move()
if head.distance(food) < 15: # голова на яблоке
place_food()
screen.update()
screen.ontimer(loop, DELAY)
...
place_food()
loop()
screen.mainloop()
Зачем координаты умножаем на STEP? Голова стоит только на числах, кратных 20: 0, 20, 40 и так далее. randint(-12, 12) * 20 даёт от −240 до 240, то есть клетку внутри рамки. Если же бросить яблоко на произвольное число, скажем в точку (35, 5), оно ляжет между клетками. Голова на (20, 0) уже накрывает его краем, но distance() показывает 15,8, и яблоко не съедено. Вы смотрите на голову, наехавшую на яблоко, а счёт стоит. Для игрока это баг.
head.distance(food) считает расстояние между двумя черепашками. Порог 15 меньше клетки, поэтому срабатывает только тогда, когда голова стоит точно на яблоке.
Шаг 5. Хвост: список сегментов
Хвост храним в обычном списке. Если у вас ещё путаются append и range, загляните в справочник о списках и словарях. Каждое яблоко добавляет один квадратик: он рождается за пределами окна, в точке (1000, 1000), и уже на следующем шаге встаёт на своё место. Движение хвоста дописываем в начало move(), перед шагом головы:
segments = [] # квадратики хвоста
...
def place_food():
# ищем клетку, где нет ни головы, ни хвоста
while True:
x = random.randint(-12, 12) * STEP
y = random.randint(-7, 7) * STEP
food.goto(x, y)
free = True
for t in [head] + segments:
if t.distance(food) < 15:
free = False
if free:
return
def add_segment():
s = turtle.Turtle()
s.shape("square")
s.color("green")
s.penup()
s.goto(1000, 1000) # за полем: на своё место встанет следующим шагом
segments.append(s)
...
def move():
# 1. хвост: с конца, каждый квадратик встаёт на место соседа впереди
for i in range(len(segments) - 1, 0, -1):
x = segments[i - 1].xcor()
y = segments[i - 1].ycor()
segments[i].goto(x, y)
# 2. первый квадратик встаёт туда, где была голова
if segments:
segments[0].goto(head.xcor(), head.ycor())
# 3. голова делает шаг
...
В loop(), в блоке с яблоком, перед place_food() добавьте строку add_segment(). Порядок в move() принципиален: сначала хвост, с последнего квадратика к первому, и только потом голова. range(len(segments) - 1, 0, -1) считает назад: для хвоста из четырёх квадратиков это 3, 2, 1. Нулевой квадратик отдельно встаёт туда, где только что стояла голова.
place_food() тоже изменилась: теперь она ищет клетку, свободную от змейки. Мы прогнали игру с подменённым random, который всегда возвращает 0, и старая версия проигрывала сама. Яблоко падало под голову на старте, «съедалось» до первого нажатия, первый квадратик хвоста вставал на место неподвижной головы, а после шага 7 это уже укус и конец игры.
Типичная ошибка: хвост двигают от головы, и сегменты слипаются
Чаще всего хвост пишут «по-человечески», от головы к концу, и вы, скорее всего, тоже так попробуете: for i in range(1, len(segments)). Вот та же логика на обычных координатах, без окна. Её можно запустить где угодно, даже онлайн, без установки Python:
head = (60, 0)
tail = [(40, 0), (20, 0), (0, 0)]
wrong = tail.copy()
for i in range(1, len(wrong)): # с головы к хвосту
wrong[i] = wrong[i - 1]
wrong[0] = head
print("неправильно:", wrong)
right = tail.copy()
for i in range(len(right) - 1, 0, -1): # с хвоста к голове
right[i] = right[i - 1]
right[0] = head
print("правильно: ", right)
Результат запуска:
неправильно: [(60, 0), (40, 0), (40, 0)]
правильно: [(60, 0), (40, 0), (20, 0)]
В первом варианте второй квадратик переехал на место первого, а третий взял уже новую позицию второго. Где второй стоял раньше, никто не запомнил. Хоть из десяти квадратиков, такой хвост занимает две клетки: все, кроме первого, слипаются в одну. Движение с конца даёт каждому квадратику ещё не сдвинутого соседа. Другие ошибки, которые ломают первые программы, собраны в статье о типичных ошибках новичков.
Шаг 6. Счёт на экране
Для надписи заводим отдельную невидимую черепашку hud вверху поля. show_score() стирает старый текст и пишет новый:
# счёт вверху
score = 0
hud = turtle.Turtle()
hud.hideturtle()
hud.penup()
hud.color("white")
hud.goto(0, 160)
...
def show_score():
hud.clear() # без clear() новые цифры ложатся поверх старых
hud.write(f"Счёт: {score}", align="center", font=("Arial", 16, "bold"))
...
def loop():
global score
move()
if head.distance(food) < 15: # голова на яблоке
score += 1
add_segment()
show_score()
place_food()
screen.update()
screen.ontimer(loop, DELAY)
...
show_score()
place_food()
loop()
Счёт меняется внутри loop(), поэтому в начале функции стоит global score. Без него Python решит, что score — локальная переменная, и на первом же яблоке упадёт с UnboundLocalError. Почему так, объясняет справочник о функциях. Вызов show_score() внизу файла нужен, чтобы вы видели счёт с первой секунды. А если убрать hud.clear(), надписи «Счёт: 0», «Счёт: 1», «Счёт: 2» лягут одна на другую.
Шаг 7. Конец игры: стены, укус себя и рестарт
Осталось научить игру проигрывать. Голова в стене, когда модуль x больше 240 или модуль y больше 140: рамка стоит на ±250 и ±150, так что крайние клетки ещё внутри. Укус — когда голова ближе чем на 10 пикселей к любому квадратику хвоста. После проигрыша loop() не заказывает следующего шага, и змейка замирает.
С поворотами есть ловушка. Запретить разворот на 180° кажется легко:
def go_left():
if head.direction != "right": # ловушка: смотрим на direction
head.direction = "left"
Но змейка делает шаг раз в 150 мс, а пальцы быстрее. Вы ползёте вправо и быстро жмёте «вверх» и «влево». Первая стрелка меняет direction на up, вторая видит up и разрешает left, и на следующем шаге голова разворачивается прямо в хвост. В нашем прогоне змейка с тремя квадратиками хвоста проигрывала именно так. Поэтому сравниваем с head.last, направлением, куда голова на самом деле пошла прошлым шагом. Оно обновляется в конце move().
Рестарт на клавишу R прячет квадратики, очищает список, возвращает голову в центр и снова запускает loop(). Проверка if playing: return не даёт нажать R посреди игры: тогда работали бы две цепочки таймеров одновременно, и змейка ползла бы вдвое быстрее. Полный код игры:
import turtle
import random
STEP = 20 # размер клетки: голова прыгает на 20 пикселей
DELAY = 150 # пауза между шагами, мс
screen = turtle.Screen()
screen.title("Змейка")
screen.bgcolor("black")
screen.setup(600, 400)
screen.tracer(0) # выключаем автоанимацию, кадр показываем сами
# рамка поля
pen = turtle.Turtle()
pen.hideturtle()
pen.color("white")
pen.penup()
pen.goto(-250, -150)
pen.pendown()
for side in [500, 300, 500, 300]:
pen.forward(side)
pen.left(90)
# голова змейки
head = turtle.Turtle()
head.shape("square")
head.color("lime")
head.penup()
head.direction = "stop" # куда повернуть на следующем шаге
head.last = "stop" # куда голова на самом деле пошла прошлым шагом
segments = [] # квадратики хвоста
# яблоко
food = turtle.Turtle()
food.shape("circle")
food.color("red")
food.penup()
# счёт вверху
score = 0
best = 0
playing = True
hud = turtle.Turtle()
hud.hideturtle()
hud.penup()
hud.color("white")
hud.goto(0, 160)
def show_score():
hud.clear() # без clear() новые цифры ложатся поверх старых
hud.write(f"Счёт: {score} Рекорд: {best}", align="center", font=("Arial", 16, "bold"))
def place_food():
# ищем клетку, где нет ни головы, ни хвоста
while True:
x = random.randint(-12, 12) * STEP
y = random.randint(-7, 7) * STEP
food.goto(x, y)
free = True
for t in [head] + segments:
if t.distance(food) < 15:
free = False
if free:
return
def add_segment():
s = turtle.Turtle()
s.shape("square")
s.color("green")
s.penup()
s.goto(1000, 1000) # за полем: на своё место встанет следующим шагом
segments.append(s)
def move():
# 1. хвост: с конца, каждый квадратик встаёт на место соседа впереди
for i in range(len(segments) - 1, 0, -1):
x = segments[i - 1].xcor()
y = segments[i - 1].ycor()
segments[i].goto(x, y)
# 2. первый квадратик встаёт туда, где была голова
if segments:
segments[0].goto(head.xcor(), head.ycor())
# 3. голова делает шаг
if head.direction == "up":
head.sety(head.ycor() + STEP)
elif head.direction == "down":
head.sety(head.ycor() - STEP)
elif head.direction == "left":
head.setx(head.xcor() - STEP)
elif head.direction == "right":
head.setx(head.xcor() + STEP)
head.last = head.direction
# развернуться на 180° нельзя: сравниваем с head.last
def go_up():
if head.last != "down":
head.direction = "up"
def go_down():
if head.last != "up":
head.direction = "down"
def go_left():
if head.last != "right":
head.direction = "left"
def go_right():
if head.last != "left":
head.direction = "right"
def game_over():
global playing, best
playing = False
if score > best:
best = score
hud.clear()
hud.write(f"Конец игры! Счёт: {score}. Новая игра: R", align="center", font=("Arial", 16, "bold"))
screen.update()
def restart():
global score, playing
if playing:
return
for s in segments:
s.hideturtle()
segments.clear()
head.goto(0, 0)
head.direction = "stop"
head.last = "stop"
score = 0
playing = True
show_score()
place_food()
loop()
def loop():
global score
if not playing:
return
move()
# врезались в рамку?
if abs(head.xcor()) > 240 or abs(head.ycor()) > 140:
game_over()
return
# укусили себя?
for s in segments:
if head.distance(s) < 10:
game_over()
return
if head.distance(food) < 15: # голова на яблоке
score += 1
add_segment()
show_score()
place_food()
screen.update()
screen.ontimer(loop, DELAY) # «вызови loop ещё раз через 150 мс»
screen.listen()
screen.onkey(go_up, "Up")
screen.onkey(go_down, "Down")
screen.onkey(go_left, "Left")
screen.onkey(go_right, "Right")
screen.onkey(restart, "r")
show_score()
place_food()
loop()
screen.mainloop()
Если у вас не срабатывает R, переключите раскладку на английскую. onkey(restart, "r") ждёт латинскую букву, а стрелки от раскладки не зависят.
Что поменять, когда игра уже работает
Когда ваша змейка ползает и проигрывает как надо, её легко сделать своей:
- Ускорение: вместо постоянного
DELAYзаведите переменнуюdelayи после каждого яблока уменьшайте её,delay = max(60, delay - 5).maxне даёт паузе упасть ниже 60 мс. - Поле без стен: голова, вышедшая справа, появляется слева.
- Второе, фиолетовое яблоко, которое отнимает два очка.
- Змейка для двоих: вторая голова на клавишах w, a, s, d, свой хвост и свой счёт.
На курсе «Python Старт» для 6–10 лет змейку собирают за два занятия: сначала движение и яблоки, потом хвост, рекорд и скорость. Большинство идей из списка выше там дают как задания к урокам.
Чем отличается змейка на Pygame
Pygame — отдельная библиотека, её ставят командой pip install pygame; как работает pip, рассказано в справочнике о библиотеках. Логика змейки та же: список сегментов, движение с хвоста, проверка клеток. Отличается каркас. В Pygame нет mainloop(), который ждёт за вас, поэтому while running: там как раз правильный игровой цикл: на каждом обороте программа сама забирает события из pygame.event.get(), рисует кадр и держит темп через clock.tick(10). Ноль координат — в левом верхнем углу, y растёт вниз, а в turtle ноль в центре и y растёт вверх. Квадратики рисует pygame.draw.rect, столкновения проверяет метод colliderect. Зато с Pygame у вас будут картинки, звук и плавная анимация. Как выглядит первая игра на нём и сколько времени на неё уходит, разобрано в статье о первой игре ребёнка на Pygame. В программе Pygame относится к ступени «Игры и боты» для 10–14 лет.