Коротко: «Змійку» на 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 років.