Гра на Python Теніс (Пінг-понг) — це ваш рецепт створення власної аркадної гри, де гравці керують ракетками, щоб відбивати м’ячик по черзі.
Ви навчитеся програмувати рух об’єктів, рахувати очки й робити гру динамічною та цікавою.
І найкрутіше — ви зможете налаштувати гру під себе: змінювати кольори, швидкість і навіть рівні складності, щоб грати ще веселіше!

Практика. Гра на Python Теніс
Створення ігрового поля
1: Почнемо з бази — налаштуємо ігрове поле. Вам потрібно створити головне вікно, область для відображення анімації та додати основні елементи гри.
Створімо заготовку для гри разом з глобальними змінними. Як бачите, у нас буде блок глобальних змінних, блок функцій і блок основної логіки. Розташовуйте код відповідно до цієї схеми — це допоможе створити правильну структуру й уникнути помилок в майбутньому. Створіть вказані змінні.
from tkinter import *
### глобальні змінні
# налаштування вікна
Створіть змінні ширини WIDTH у 900 пікселів та висоти HEIGHT у 300 пікселів.
# налаштування ракеток
Створіть змінні PAD_W та PAD_H для ширини та висоти ракетки відповідно. Встановіть ширину ракетки у 10 пікселів, а висоту у 100 пікселів.
# налаштування мʼячика
Додайте змінну BALL_RADIUS для радіуса м’яча та встановіть її значення у 30 пікселів.
### Функції для роботи з ігровими об'єктами
### Основна програмаЗверніть увагу на глобальні змінні на початку коду. Вони значно спрощують роботу. Наприклад, якщо ширина вікна 900 пікселів, набагато зручніше створити змінну WIDTH, присвоїти їй значення 900 і використовувати її в коді, ніж щоразу вручну писати 900.
Ви особливо оціните цей підхід, коли захочете щось змінити — наприклад, ширину вікна. Достатньо буде змінити лише одне значення, і вся гра автоматично підлаштується 🙂
2: Створімо базове вікно гри та додамо всі основні елементи.


Пояснення коду :
🔹 Створюємо вікно
root = Tk()
root.title(“Pythonic Pong”) – назва вікна зверху
🔹 Поле для гри
c = Canvas(root, width=WIDTH, height=HEIGHT, background=”#003300″) — створюємо “полотно” (Canvas), де буде гра. Вказуємо розмір і колір фону (темно-зелений)
c.pack() – Показуємо це полотно у вікні
🔹 Малюємо лінії поля.
c.create_line(PAD_W, 0, PAD_W, HEIGHT, fill=”white”) малюємо ліву вертикальну лінію (відступаємо від краю поля на ширину ракетки, щоб вона помістилася)
c.create_line(WIDTH-PAD_W, 0, WIDTH-PAD_W, HEIGHT, fill=”white”) малюємо праву лінію (відступаємо від краю поля на ширину ракетки)
c.create_line(WIDTH/2, 0, WIDTH/2, HEIGHT, fill=”white”) малюємо центральну лінію (як у тенісі)
🟨 Створення ліній :
create_line(x1, y1, x2, y2, width=…, fill=…)
- (x1, y1) — точка початку лінії
- (x2, y2) — точка кінця лінії
- width — товщина
- fill — колір
🔹 Створення м’яча : овал, який вписаний в прямокутник. Координати прямокутника вказані за допомогою двох точок : лівого верхнього і правого нижнього кута.
BALL = c.create_oval(…)
create_oval(x1, y1, x2, y2, fill=…)
(x1, y1)— верхній лівий кут уявного прямокутника(x2, y2)— нижній правий кутfill— колір фігури
BALL = c.create_oval(WIDTH/2-BALL_RADIUS/2,
HEIGHT/2-BALL_RADIUS/2,
WIDTH/2+BALL_RADIUS/2,
HEIGHT/2+BALL_RADIUS/2, fill=”white”)
🔹 Ракетки.
LEFT_PAD = c.create_line(PAD_W/2, 0, PAD_W/2, PAD_H, width=PAD_W, fill=”yellow”) малюємо ліву вертикальну лінію (ліву ракетку)
RIGHT_PAD = c.create_line(WIDTH-PAD_W/2, 0, WIDTH-PAD_W/2, PAD_H, width=PAD_W, fill=”yellow”) малюємо праву лінію (праву ракетку)
🟨 Створення ракеток : Ракетка — це товста лінія шириною width=PAD_W, як дуже витягнутий прямокутник.
LEFT_PAD = c.create_line(PAD_W/2, 0, PAD_W/2, PAD_H, width=PAD_W, fill=”yellow”)
Перша точка: PAD_W/2 → це X (по горизонталі, половинка ширини ракетки = центр лінії), а 0 → це Y (верх екрана)
Друга точка: PAD_W/2 (той самий X!) y = PAD_H (вниз на висоту ракетки)
Аналогічно, друга ракетка. Тільки в кінці поля, тому розраховуємо від ширини поля WIDTH-PAD_W.
🔹 Запуск гри: root.mainloop() йде за допомогою циклу. Функція mainloop() для динамічної гри, вона створить нескінченний цикл і слухатиме дії клавіатури чи мишки.
Перший запуск
У результаті у вас має вийти приблизно таке ігрове поле:

Це наш “скелет” гри. Далі додамо рух і зробимо її по-справжньому інтерактивною 😉
Змушуємо м’яч рухатися
Додаймо цій грі трохи драйву! Створимо рух мʼячика. Для цього нам знадобиться нова функція move_ball, у якій ми опишемо логіку руху м’яча. Саме тут м’яч буде “оживати” і переміщатися по полю.
Також створімо функцію main. Вона буде:
🥎 викликати move_ball
🥎 запускати саму себе знову через root.after|
Таким чином, утворюється цикл, який постійно оновлює стан гри. Це і є основа будь-якої простої анімації в tkinter.
3: Додайте глобальні змінні швидкості м’яча:
рух по горизонталі: BALL_X_CHANGE зі значенням 20
рух по вертикалі BALL_Y_CHANGE зі значенням 0
4: Створіть функцію def move_ball() без параметрів. Функція має викликати команду :
c.move() — рух об’єкта з області анімації Canvas (ігрового поля). Ця функція потребує параметрів, тому передайте в неї :
BALL — це саме той об’єкт, який рухаємо (м’яч)
BALL_X_CHANGE — на скільки пікселів рухаємо по горизонталі
BALL_Y_CHANGE — на скільки пікселів рухаємо по вертикалі
5: Також створіть функцію main, яка буде робити дві команди:
🥎 викликати функцію move_ball() без параметрів
🥎 викликати функцію main кожні 30 мілісекунд : root.after(30, main). Це явище в програмування називається рекурсія
6: Додайте виклик функції main() в основну програму. (Перед рядком root.mainloop()). Це запустить рух м’яча із запуском гри
Якщо ви все зробили правильно, після запуску м’яч буде рухатися вправо. Запустіть програму
Ви можете змінювати швидкість і напрямок руху по горизонталі, змінюючи значення змінної BALL_X_CHANGE.
Рекурсія в Python
Рекурсія в Python — це коли функція викликає сама себе, щоб розв’язати задачу по частинах. Уявіть це як “розбирати задачу на менші копії самої себе” 🧩. У рекурсії завжди є 2 частини:
Рекурсивний крок — коли вона викликає сама себе
Базовий випадок — коли функція зупиняється

Як це виглядає в Python на практиці:
Приклад: порахувати факторіал числа (наприклад, 5! = 5×4×3×2×1)
def factorial(n):
# базовий випадок (коли зупинка)
if n == 1:
return 1
# виклик самої себе
return n * factorial(n - 1) 🔹 Як це працює
factorial(5)
→ 5 * factorial(4)
→ 5 * 4 * factorial(3)
→ 5 * 4 * 3 * factorial(2)
→ 5 * 4 * 3 * 2 * factorial(1)
→ 5 * 4 * 3 * 2 * 1 = 120
Не хвилюйтеся, якщо слово “рекурсія” звучить складно — на початку вона може трохи дивувати, але її розуміння дає ключ до розв’язання складніших задач у програмуванні 🙂
Керуємо ракетками
Тепер додамо керування ракетками, щоб гра стала інтерактивною.
Логіка тут дуже проста:
🏓у кожної ракетки є швидкість
🏓 спочатку вона дорівнює 0 — ракетка стоїть на місці
🏓коли користувач натискає клавішу — швидкість змінюється
🏓 ракетка починає рухатися вгору або вниз
🏓коли клавішу відпускають — швидкість знову стає 0
Тобто, фактично ви керуєте не позицією ракетки напряму, а її швидкістю. Цей підхід, використовується в багатьох іграх 🙂
Далі ми реалізуємо це в коді та прив’яжемо рух до клавіш клавіатури
7: Задайте глобальні змінні швидкості ракеток:
🏓PAD_SPEED – швидкість руху ракеток, значення 20
🏓LEFT_PAD_SPEED – швидкість лівої ракетки, значення 0
🏓RIGHT_PAD_SPEED – швидкість правої ракетки, значення 0
8: Створіть функцію руху ракеток:

🏓 c.move(pad, 0, PADS[pad]) – рух об’єкта
pad – об’єкт, який рухаємо (одна з ракеток)
по X = 0 (вліво/вправо не рухається)
по Y = швидкість (вгору/вниз)
Тобто: ракетка рухається вверх або вниз
🏓 Перевірка: чи ракетка не вилізла вгору? if c.coords(pad)[1] < 0:
[1] — це верхня точка ракетки. Якщо вона < 0 → ракетка вилізла за верх
c.move(pad, 0, -c.coords(pad)[1]) – повертаємо її назад на поле
🏓 Перевірка: не вилізла вниз? elif c.coords(pad)[3] > HEIGHT:
[3] — це нижня точка. Якщо > HEIGHT → вилізла вниз
Тоді виправляємо: c.move(pad, 0, HEIGHT – c.coords(pad)[3]) – підтягуємо її назад в поле
9: Оновлюємо функцію main, додамо туди ще постійний виклик руху ракеток:
def main():
move_ball()
move_pads()
root.after(30, main)10: Створімо функцію обробки натискання клавіш. Допишіть код нижче, керуючись поясненнями:
movement_handler(event) – це функція, яка спрацьовує, коли гравець натискає кнопку на клавіатурі
event — це “повідомлення” від клавіатури. В ньому є: event.keysym – назва клавіші, наприклад “w” чи “Up” (стрілка вгору)
🏓 Клавіші керування лівою ракеткою : “w” – рух вгору і “s” – рух вниз
🏓 Клавіші керування правою ракеткою : “Up” (стрілка вгору) – рух вгору і “Down”(стрілка вгору) – рух вниз
🔹 Логіка кнопок
if event.keysym == “w”: – якщо натиснули w
LEFT_PAD_SPEED = -PAD_SPEED → Ліва ракетка їде вгору
elif event.keysym == “s”: – якщо натиснули s
LEFT_PAD_SPEED = PAD_SPEED → Ліва ракетка їде вниз
# обробка натискання клавіш
def movement_handler(event):
global LEFT_PAD_SPEED, RIGHT_PAD_SPEED
if event.keysym == "w":
LEFT_PAD_SPEED = -PAD_SPEED
elif event.keysym == "s":
LEFT_PAD_SPEED = PAD_SPEED
elif натуснули_стрілку_вгору:
🏓🏓🏓🏓🏓= 🏓🏓🏓🏓
elif натуснули_стрілку_вниз:
🏓🏓🏓🏓🏓= 🏓🏓🏓🏓Зупинка ракеток
11: Створімо функцію stop_pad з параметром event, яка буде зупиняти рух ракеток, якщо клавішу відпустили:
🏓Вкажіть глобальні змінні LEFT_PAD_SPEED, RIGHT_PAD_SPEED у функції
🏓Якщо event.keysym in (“w”, “s”), то LEFT_PAD_SPEED = 0. Тобто, якщо відпустили одну з клавіш w чи s, то швидкість лівої ракетки стає 0
🏓Аналогічно, якщо відпустили одну зі стрілочок “Up” чи “Down”, то швидкість правої ракетки стає 0.
12: Тепер час змінити основну програму і слухати події натискання клавіш. Додайте цей код перед викликом main():
# встановлюємо фокус на Canvas (щоб ловити натискання клавіш)
c.focus_set()
# прив’язуємо натискання клавіш
c.bind("<KeyPress>", movement_handler)
# прив’язуємо відпускання клавіш
c.bind("<KeyRelease>", stop_pad)13: Запустіть і перевірте, що ви можете керувати обома ракетками 🎮. М’ячик поки не має відбиватися, це ми додамо згодом
Відскок м’яча від стінок і ракеток
Тепер прокачаємо м’ячик, щоб він вмів відбиватися від перешкод:
🥎 при зіткненні зі стінкою або ракеткою змінюємо напрямок руху м’яча
🥎 при ударі об ракетку:
🥎🥎 трохи збільшується швидкість
🥎🥎 вертикальний рух змінюється випадковим чином
14: Додайте бібліотеку import random для обробки випадкових подій. Також додайте кілька нових глобальних змінних:
BALL_SPEED_UP – наскільки збільшується швидкість після удару, значення 1.05
BALL_MAX_SPEED – максимальна швидкість м’яча, значення 40
BALL_X_SPEED – початкова швидкість по горизонталі, значення 20
BALL_Y_SPEED – початкова швидкість по вертикалі, значення 20
right_line_distance – відстань до правої межі поля, значення WIDTH – PAD_W
14: Створімо функцію відскоку м’яча bounce.

Є 2 ситуації
🥎 if action == “strike”: 1: М’яч вдарився об ракетку
🥎 else: 2. М’яч вдарився об стінку
🥎 1. Удар об ракетку
BALL_Y_SPEED = random.randrange(-10, 10) м’яч отримує випадковий рух вверх/вниз
Тобто: кожен удар – це трохи змінює напрямок його руху
Зміна напрямку по X: if abs(BALL_X_SPEED) < BALL_MAX_SPEED:
BALL_X_SPEED *= -BALL_SPEED_UP
Що тут відбувається:
* -1 – змінює напрямок (вліво ↔ вправо)
BALL_SPEED_UP → трохи прискорює
Тобто, м’яч відскакує і стає швидшим
Якщо м’ячик вже дуже швидкий: else:
BALL_X_SPEED = -BALL_X_SPEED
то просто змінює напрямок (без прискорення)
🥎 2. Удар об стінку
BALL_Y_SPEED = -BALL_Y_SPEED міняємо напрямок по вертикалі
летів вниз → полетить вверх
летів вверх → полетить вниз
Програмуємо основну логіку м’ячика
15: Знайдіть функцію move_ball і розширте її логіку (поточний рядок можна буде замінити на новий код):
# функція руху м’яча
def move_ball():
# координати м’яча
ball_left, ball_top, ball_right, ball_bot = c.coords(BALL)
ball_center = (ball_top + ball_bot) / 2
# перевірка руху по горизонталі
if ball_right + BALL_X_SPEED < right_line_distance and \
ball_left + BALL_X_SPEED > PAD_W:
c.move(BALL, BALL_X_SPEED, BALL_Y_SPEED)
# торкання межі
elif ball_right == right_line_distance or ball_left == PAD_W:
if ball_right > WIDTH / 2:
# права ракетка
if c.coords(RIGHT_PAD)[1] < ball_center < c.coords(RIGHT_PAD)[3]:
bounce("strike")
else:
# промах (додамо пізніше)
pass
else:
# ліва ракетка
if c.coords(LEFT_PAD)[1] < ball_center < c.coords(LEFT_PAD)[3]:
bounce("strike")
else:
pass
else:
# якщо м’яч вилітає за межі — повертаємо його
if ball_right > WIDTH / 2:
c.move(BALL, right_line_distance - ball_right, BALL_Y_SPEED)
else:
c.move(BALL, -ball_left + PAD_W, BALL_Y_SPEED)
# відскок від верхньої/нижньої межі
if ball_top + BALL_Y_SPEED < 0 or ball_bot + BALL_Y_SPEED > HEIGHT:
bounce("ricochet")
🧠 Загальна логіка м’яча:
🥎 летить
🥎 перевіряє — не вдарився?
🥎 якщо вдарився → змінює напрямок
🥎 1. Беремо координати ball_left, ball_top, ball_right, ball_bot = c.coords(BALL)
Це межі м’яча: ліва, верхня, права, нижня
ball_center = (ball_top + ball_bot) / 2 центр по вертикалі (щоб перевірити попадання в ракетку)
🥎 2. Якщо нічого не заважає — просто летимо
if ball_right + BALL_X_SPEED < right_line_distance and \ ball_left + BALL_X_SPEED > PAD_W: (якщо не вийшов справа чи зліва), тоді:
c.move(BALL, BALL_X_SPEED, BALL_Y_SPEED) просто рухаємо м’яч (летимо)
🥎 3. Торкання ракетки
elif ball_right == right_line_distance or ball_left == PAD_W: якщо м’яч дійшов до краю (де ракетки)
Якщо справа if ball_right > WIDTH / 2: значить це права сторона
Перевірка попадання
if c.coords(RIGHT_PAD)[1] < ball_center < c.coords(RIGHT_PAD)[3]: центр м’яча в межах ракетки?
✔ так → bounce(“strike”)
❌ ні → pass (пізніше тут буде “гол”)
Якщо зліва — те саме тільки для лівої ракетки
🥎 4. Якщо м’яч виліз за межі else:, то трохи “поправляємо” його назад у поле:
Якщо справа ball_right > WIDTH / 2, то c.move(BALL, right_line_distance – ball_right, BALL_Y_SPEED)
повертаємо назад
Якщо зліва, то теж повертаємо c.move(BALL, -ball_left + PAD_W, BALL_Y_SPEED)
🥎 5. Відскок від верху/низу
if ball_top + BALL_Y_SPEED < 0 or ball_bot + BALL_Y_SPEED > HEIGHT: м’яч торкнувся верху або низу
то bounce(“ricochet”) відскакує
Тепер м’яч відбивається від стінок і ракеток. Якщо гравець не встиг відбити — м’яч “зависає” біля краю. Далі це виправимо, додавши рахунок і перезапуск м’яча.
Підрахунок очок і респаун м’яча
16: Додамо рахунок для обох гравців. Створіть глобальні змінні:
PLAYER_1_SCORE = 0
PLAYER_2_SCORE = 0
17: Тепер відобразимо рахунок на екрані (можна додати в місце, де малюються лінії):
p_1_text = c.create_text(WIDTH - WIDTH/6, PAD_H/4,
text=PLAYER_1_SCORE,
font="Arial 20",
fill="white")
Аналогічно p_2_text, тільки координати : WIDTH/6, PAD_H/4 і текст PLAYER_2_SCORE.
Запустіть гру, має відображатися початковий рахунок:

Залишилося додати оновлення рахунку й у вас буде повноцінний Pong 🏓
18: Створимо функції для оновлення рахунку та респауну. Для цього створіть ще одну глобальну змінну: INITIAL_SPEED — початкова швидкість м’яча, значення 20.
19: Створіть функцію оновлення рахунку update_score з параметром player – це гравець, якому оновлюємо рахунок (правий чи лівий):
🤾♀️ Визначаємо глобальні змінні PLAYER_1_SCORE, PLAYER_2_SCORE
🤾♀️Якщо передали правого гравця (player == “right”), то збільшуємо рахунок першого PLAYER_1_SCORE на 1. І відображаємо рахунок c.itemconfig(p_1_text, text=PLAYER_1_SCORE)
🤾♀️Інакше, збільшуємо рахунок лівого (другого) грався і відображаємо новий рахунок для нього
20: Створимо функцію респауну (оновлення м’яча). Вона буде ставити м’яч у центр і почне його рухати у бік гравця, який тільки-но пропустив з початковою швидкістю.
def spawn_ball():
global BALL_X_SPEED
# ставимо м’яч у центр
c.coords(BALL,
WIDTH/2 - BALL_RADIUS/2,
HEIGHT/2 - BALL_RADIUS/2,
WIDTH/2 + BALL_RADIUS/2,
HEIGHT/2 + BALL_RADIUS/2)
# задаємо напрямок у сторону гравця, який пропустив
# і повертаємо початкову швидкість
BALL_X_SPEED = -(BALL_X_SPEED * -INITIAL_SPEED) / abs(BALL_X_SPEED)
21: Тепер потрібно замінити pass у функції move_ball на виклик функцій оновлення рахунку і респану.
Замініть ТІЛЬКИ виділений жовтим код. Було:
if c.coords(RIGHT_PAD)[1] < ball_center < c.coords(RIGHT_PAD)[3]:
bounce("strike")
else:
pass
else:
if c.coords(LEFT_PAD)[1] < ball_center < c.coords(LEFT_PAD)[3]:
bounce("strike")
else:
pass
Має стати 👇
if c.coords(RIGHT_PAD)[1] < ball_center < c.coords(RIGHT_PAD)[3]:
bounce("strike")
else:
update_score("left")
spawn_ball()
else:
if c.coords(LEFT_PAD)[1] < ball_center < c.coords(LEFT_PAD)[3]:
bounce("strike")
else:
update_score("right")
spawn_ball()
Що тепер відбувається:
🤾♂️якщо гравець не відбив м’яч — суперник отримує очко
🤾♂️рахунок оновлюється прямо на екрані
🤾♂️м’яч повертається в центр і летить у сторону того, хто програв розіграш
Готово 🎉
Тепер ваш «Пінг-понг» — повноцінна гра, у яку вже можна грати з другом.

Приємної гри! 🏓

Домашня робота
1: Звершіть гру та протестуйте, щоб усе працювало правильно. Перефарбуйте ігрове поле та ракетки у кольори на свій смак.
2: Зробіть так, щоб при відбитті м’ячика ракеткою додавалися не одне, а два очки.
3: Домалюйте вертикальну лінію до поля, як на фото.
4: (Завдання виконується окремо від гри). Роздивіться приклад рекурсії. Зробіть одне з завдань на вибір за допомогою рекурсії:

🪐Знайдіть суму чисел від 1 до n. Приклад: sum(5) = 1 + 2 + 3 + 4 + 5 = 15
🪐Піднесіть число до степеня без використання **. Приклад : pow(2, 3) = 8


Level Up!
Гра на Python Теніс
1: Додайте можливість грати одному гравцю (користувач обирає в консолі).
Підказка: простіше реалізувати, якщо збільшити одну з ракеток до максимальної висоти.
2: Додайте 3 рівні складності. Нехай користувач обирає рівень:
Новачок
Середній
Геймер
І залежно від рівня змінюйте швидкість м’яча і розмір ракеток.
3: Розв’яжіть одне з завдань на ваш вибір за допомогою рекурсії:
🪐Послідовність Фібоначчі — це такі числа, де кожне наступне число є сумою попередніх (крім перших): 0, 1, 1, 2, 3, 5, 8, 13, 21…
Поверніть n-те число Фібоначчі.
🪐Переверніть рядок за допомогою рекурсії. Приклад:"hello" → "olleh"
🪐 Знайдіть суму цифр числа. Приклад: 123 → 1 + 2 + 3 = 6

Не зупиняйтеся у вивченні Python! До зустрічі на наступних уроках!
