Вступление

Честно говоря, я давненько хотел написать какую-то игрушку и опыт их написания на данном языке даже был, но все они были консольные :(

Но несколько дней назад мне пришла классная идея: написать ремейк какой-нибудь старой игрушки в минимальное количество строк кода и используя только стандартные библиотеки Python, а именно: Tkinter, Time, Random и Winsound.

Да-да, никакого Pygame’а. Я ещё со школьных уроков Информатики не любил лёгких путей при написании программ на Паскале :)

Для написания я выбрал игрушку Donkey, написанную в далёком 1981 году для IBM PC DOS самим Биллом Гейтсом.

Как-то так)

Как всё писалось?

Для начала я создаю окно для игры. Здесь я устанавливаю иконку и заголовок окна, а также рассчитываю размеры и местоположение, чтобы окно появилось в центре экрана.

Я фиксирую размер окна, чтобы его нельзя было изменить, подготавливая среду для дальнейшей разработки игры:

import tkinter as tk
import time
from random import randint
import winsound

# Create the main application window
window = tk.Tk()

# Setting the window icon
window.iconbitmap('resources\icon.ico')

# Setting the window title
window.title('DonkeyPy 1.0')

# Get the width and height of the screen
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()

# Set the width and height of the window
window_width = 718
window_height = 418

# Calculate coordinates for placing the window in the centre of the screen
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)

# Apply window size and position
window.geometry(f'{window_width}x{window_height}+{x}+{y}')
# Prohibit window resizing
window.resizable(False, False)

Я добавляю фоновое изображение с текстовыми метками. На экране отображается сообщение о том, что для выхода нужно нажать клавишу Esc, а также ведётся счёт для осла, который обновляется с помощью специальной функции.

Кроме того, я загружаю изображение, показывающее, когда осёл побеждает, аналогичное делается и с машиной:

# Set the background of the window
window.image = tk.PhotoImage(file='resources\zf.png')
bg = tk.Label(window, image=window.image)
bg.grid(row=0, column=0)
bg.config(bg='#555555')

# Escape key label
esc_lbl = tk.Label(window, text='Press Esc to exit', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
esc_lbl.place(x=500, y=345)

# Window closing function
def exit(event):
    if event.keysym == 'Escape':
        window.destroy()

window.bind('<KeyPress-Escape>', exit)

# Donkey labels
donkey_lbl = tk.Label(window, text='Donkey', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
donkey_lbl.place(x=26, y=40)

donkey_count = tk.Label(window, text=0, bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
donkey_count.place(x=26, y=90)

donkey_loses = tk.Label(window, text='Donkey loses!', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
donkey_loses.place(x=1000, y=1000)

# Donkey scoring function
def donkey_points_count():
    donkey_count['text'] = int(donkey_count['text']) + 1

donkey_wins = tk.PhotoImage(file='resources\donkey_wins.png')
donkey_wins_label = tk.Label(window)
donkey_wins_label.image = donkey_wins
donkey_wins_label['image'] = donkey_wins_label.image
donkey_wins_label.place(x=1000, y=1000)
donkey_wins_label.config(bg='#555555')

# Car labels
car_lbl = tk.Label(window, text='Driver', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
car_lbl.place(x=500, y=40)

car_count = tk.Label(window, text=0, bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
car_count.place(x=500, y=90)

driver_loses = tk.Label(window, text='Driver loses!', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
driver_loses.place(x=1000, y=1000)

# Driver scoring function
def driver_points_count():
    car_count['text'] = int(car_count['text']) + 1

driver_wins = tk.PhotoImage(file='resources\driver_wins.png')
driver_wins_label = tk.Label(window)
driver_wins_label.image = driver_wins
driver_wins_label['image'] = driver_wins_label.image
driver_wins_label.place(x=1000, y=1000)
driver_wins_label.config(bg='#555555')

Я создаю изображение машины и добавляю его на окно. Затем я определяю функцию, которая перемещает машину при нажатии клавиш Right и Left, и задаю функцию перезапуска игры:

# Uploading car image
car = tk.PhotoImage(file='resources\car.png')
car_label = tk.Label(window)
car_label.image = car
car_label['image'] = car_label.image
car_y = 280
car_label.place(x=250, y=car_y)
car_label.config(bg='#555555')

car_y_initial = 280

# Car move function
def move_car(event):
    if car_y == 100:
        return
    else:
        if event.keysym == 'Right':
            car_label.place(x=380)
        elif event.keysym == 'Left':
            car_label.place(x=250)
        winsound.PlaySound('sounds\move_car.wav', 1)

window.bind('<KeyPress-Right>', move_car)
window.bind('<KeyPress-Left>', move_car)

# Game restart function
def restart_game():
    global car_y, car_y_initial

    car_y = car_y_initial

    car_label.place(x=250)

    donkey_loses.place(x=1000, y=1000)

    driver_wins_label.place(x=1000, y=1000)

    if car_count['text'] == 10:
        car_count['text'] = int(car_count['text']) * 0
        donkey_count['text'] = int(donkey_count['text']) * 0

    change_road()

Этот фрагмент кода загружает изображение осла в программу с помощью библиотеки Tkinter, я размещаю его на экране и устанавливаю некоторые параметры его отображения.

Также в коде есть функции для перезапуска игры (их суммарно 3 штуки) и скрытия метки проигрыша водителя:

# Uploading donkey image
donkey = tk.PhotoImage(file='resources\donkey.png')
donkey_label = tk.Label(window)
donkey_label.image = donkey
donkey_label['image'] = donkey_label.image
donkey_x = 365
donkey_y = -40
donkey_label.place(x=donkey_x, y=donkey_y)
donkey_label.config(bg='#555555')

donkey_y_initial = -1340

# Game restart function in case of a donkey win
def restart_game_2():
    global car_y, car_y_initial, donkey_y, donkey_y_initial

    car_y = car_y_initial
    car_label.place(x=250, y=car_y)

    donkey_y = donkey_y_initial

# Label hiding function
def driver_loses_f():
    driver_loses.place(x=1000, y=1000)

Затем я написал функцию, которая проверяет столкновение между двумя объектами: машиной и ослом.

Функция использует методы winfo_rootx() и winfo_rooty(), чтобы получить координаты объектов на экране. Затем она сравнивает эти координаты, чтобы определить, произошло ли столкновение:

# Function for checking image collision
def check_collision():
    car_x = car_label.winfo_rootx()
    car_y = car_label.winfo_rooty()

    donkey_x = donkey_label.winfo_rootx()
    donkey_y = donkey_label.winfo_rooty()

    # Condition, if the donkey wins
    if car_x >= donkey_x and car_x <= donkey_x + donkey.width() and \
            car_y >= donkey_y and car_y <= donkey_y + donkey.height():
        donkey_points_count()

        winsound.PlaySound('sounds\image_collision.wav', 1)

        if donkey_count['text'] < 10:
            driver_loses.place(x=26, y=140)

            restart_game_2()

            window.after(2500, driver_loses_f)
        else:
            donkey_wins_label.place(x=498, y=225)

            restart_game_3()

            window.after(2500, donkey_wins_f)

Здесь описывается функция, которая приводит осла в движение.

Я проверяю, не выполняется ли уже перемещение, и если нет — устанавливаю флаг is_moving в значение True. Затем я увеличиваю координату Y осла на 50 и проверяю его столкновение с машиной:

is_moving = False

# Donkey move function
def move_donkey():
    global is_moving, donkey_y, car_y

    # If the function is already in progress, exit
    if is_moving:
        return

    # Set the flag that the function is running
    is_moving = True

    donkey_y += 50
    donkey_label.place(y=donkey_y)

    # Checking for collision
    window.after(1000, check_collision)

    # Condition, if the donkey reaches a certain y-coordinate
    if donkey_y == 360:
        donkey_x = 365 if randint(1, 2) == 1 else 230

        donkey_y = -40
        donkey_label.place(x=donkey_x, y=donkey_y)

        car_y -= 20
        car_label.place(y=car_y)

        # Condition, if the driver wins
        if car_y == 100:
            driver_points_count()

            donkey_x = 1000
            donkey_label.place(x=donkey_x)

            if car_count['text'] < 10:
                donkey_loses.place(x=500, y=140)

                window.after(2500, move_donkey)
                window.after(2500, restart_game)
            else:
                driver_wins_label.place(x=498, y=225)

                window.after(2500, move_donkey)
                window.after(2500, restart_game)
        else:
                window.after(110, move_donkey)

    else:
        window.after(110, move_donkey)

    # Reset flag on function termination
    is_moving = False

В этом фрагменте кода я создаю объект, который будет обозначать дорогу:

# Create one Label for the road
road_label = tk.Label(window)

road_label.place(x=308, y=5)

Затем я определяю функцию, которая отвечает за анимацию дорожной разметки. Функция изменяет изображение метки road_label, используя разные файлы изображений, и обновляет его каждые 10 миллисекунд:

# The function responsible for animating road markings
def change_road():
    if car_y == 100:
        return
    else:
        current_time = (int(time.time() * 20) % 3) + 1
        road = tk.PhotoImage(file='resources\doroga_{}.png'.format(current_time))
        road_label.image = road
        road_label['image'] = road_label.image
        road_label.config(bg='#555555')
        window.after(10, change_road)

Итоговый код:

import tkinter as tk
import time
from random import randint
import winsound
window = tk.Tk()
window.iconbitmap('resources\icon.ico')
window.title('DonkeyPy 1.0')
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
window_width = 718
window_height = 418
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
window.geometry(f'{window_width}x{window_height}+{x}+{y}')
window.resizable(False, False)
window.image = tk.PhotoImage(file='resources\zf.png')
bg = tk.Label(window, image=window.image)
bg.grid(row=0, column=0)
bg.config(bg='#555555')
esc_lbl = tk.Label(window, text='Press Esc to exit', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
esc_lbl.place(x=500, y=345)
def exit(event):
    if event.keysym == 'Escape':
        window.destroy()
window.bind('<KeyPress-Escape>', exit)
donkey_lbl = tk.Label(window, text='Donkey', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
donkey_lbl.place(x=26, y=40)
donkey_count = tk.Label(window, text=0, bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
donkey_count.place(x=26, y=90)
donkey_loses = tk.Label(window, text='Donkey loses!', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
donkey_loses.place(x=1000, y=1000)
def donkey_points_count():
    donkey_count['text'] = int(donkey_count['text']) + 1
donkey_wins = tk.PhotoImage(file='resources\donkey_wins.png')
donkey_wins_label = tk.Label(window)
donkey_wins_label.image = donkey_wins
donkey_wins_label['image'] = donkey_wins_label.image
donkey_wins_label.place(x=1000, y=1000)
donkey_wins_label.config(bg='#555555')
car_lbl = tk.Label(window, text='Driver', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
car_lbl.place(x=500, y=40)
car_count = tk.Label(window, text=0, bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
car_count.place(x=500, y=90)
driver_loses = tk.Label(window, text='Driver loses!', bg='#555555', fg='#C0C0C0', font=('Comic Sans MS', 16, 'bold'))
driver_loses.place(x=1000, y=1000)
def driver_points_count():
    car_count['text'] = int(car_count['text']) + 1
driver_wins = tk.PhotoImage(file='resources\driver_wins.png')
driver_wins_label = tk.Label(window)
driver_wins_label.image = driver_wins
driver_wins_label['image'] = driver_wins_label.image
driver_wins_label.place(x=1000, y=1000)
driver_wins_label.config(bg='#555555')
car = tk.PhotoImage(file='resources\car.png')
car_label = tk.Label(window)
car_label.image = car
car_label['image'] = car_label.image
car_y = 280
car_label.place(x=250, y=car_y)
car_label.config(bg='#555555')
car_y_initial = 280
def move_car(event):
    if car_y == 100:
        return
    else:
        if event.keysym == 'Right':
            car_label.place(x=380)
        elif event.keysym == 'Left':
            car_label.place(x=250)
        winsound.PlaySound('sounds\move_car.wav', 1)
window.bind('<KeyPress-Right>', move_car)
window.bind('<KeyPress-Left>', move_car)
def restart_game():
    global car_y, car_y_initial
    car_y = car_y_initial
    car_label.place(x=250)
    donkey_loses.place(x=1000, y=1000)
    driver_wins_label.place(x=1000, y=1000)
    if car_count['text'] == 10:
        car_count['text'] = int(car_count['text']) * 0
        donkey_count['text'] = int(donkey_count['text']) * 0
    change_road()
donkey = tk.PhotoImage(file='resources\donkey.png')
donkey_label = tk.Label(window)
donkey_label.image = donkey
donkey_label['image'] = donkey_label.image
donkey_x = 365
donkey_y = -40
donkey_label.place(x=donkey_x, y=donkey_y)
donkey_label.config(bg='#555555')
donkey_y_initial = -1340
def restart_game_2():
    global car_y, car_y_initial, donkey_y, donkey_y_initial
    car_y = car_y_initial
    car_label.place(x=250, y=car_y)
    donkey_y = donkey_y_initial
def driver_loses_f():
    driver_loses.place(x=1000, y=1000)
def restart_game_3():
    global car_y, car_y_initial, donkey_y, donkey_y_initial
    car_y = car_y_initial
    car_label.place(x=250, y=car_y)
    donkey_y = donkey_y_initial
    if donkey_count['text'] == 10:
        donkey_count['text'] = int(donkey_count['text']) * 0
        car_count['text'] = int(car_count['text']) * 0
def donkey_wins_f():
    donkey_wins_label.place(x=1000, y=1000)
def check_collision():
    car_x = car_label.winfo_rootx()
    car_y = car_label.winfo_rooty()
    donkey_x = donkey_label.winfo_rootx()
    donkey_y = donkey_label.winfo_rooty()
    if car_x >= donkey_x and car_x <= donkey_x + donkey.width() and \
            car_y >= donkey_y and car_y <= donkey_y + donkey.height():
        donkey_points_count()
        winsound.PlaySound('sounds\image_collision.wav', 1)
        if donkey_count['text'] < 10:
            driver_loses.place(x=26, y=140)
            restart_game_2()
            window.after(2500, driver_loses_f)
        else:
            donkey_wins_label.place(x=498, y=225)
            restart_game_3()
            window.after(2500, donkey_wins_f)
is_moving = False
def move_donkey():
    global is_moving, donkey_y, car_y
    if is_moving:
        return
    is_moving = True
    donkey_y += 50
    donkey_label.place(y=donkey_y)
    window.after(1000, check_collision)
    if donkey_y == 360:
        donkey_x = 365 if randint(1, 2) == 1 else 230
        donkey_y = -40
        donkey_label.place(x=donkey_x, y=donkey_y)
        car_y -= 20
        car_label.place(y=car_y)
        if car_y == 100:
            driver_points_count()
            donkey_x = 1000
            donkey_label.place(x=donkey_x)
            if car_count['text'] < 10:
                donkey_loses.place(x=500, y=140)
                window.after(2500, move_donkey)
                window.after(2500, restart_game)
            else:
                driver_wins_label.place(x=498, y=225)
                window.after(2500, move_donkey)
                window.after(2500, restart_game)
        else:
                window.after(110, move_donkey)
    else:
        window.after(110, move_donkey)
    is_moving = False
road_label = tk.Label(window)
road_label.place(x=308, y=5)
def change_road():
    if car_y == 100:
        return
    else:
        current_time = (int(time.time() * 20) % 3) + 1
        road = tk.PhotoImage(file='resources\doroga_{}.png'.format(current_time))
        road_label.image = road
        road_label['image'] = road_label.image
        road_label.config(bg='#555555')
        window.after(10, change_road)
move_donkey()
change_road()
window.mainloop()

Если убрать все пробелы и комментарии, то получается 172 строки.

Скриншот работающей игры:

DonkeyPy 1.0
DonkeyPy 1.0

Заключение

У меня есть идея по развитию проекта на будущее:

  • Создание Android версии игры (надеюсь, руки когда-нибудь до этого дойдут).

Ну а на этом думаю, стоит закончить данную статью. Потыкать исходный код игры или скачать уже собранную версию вы сможете на GitHub.

С вами был Yura_FX. Спасибо, что дочитали данную статью до конца. Не забывайте делиться своим мнением в комментариях :)

Комментарии (4)


  1. Andrey_Solomatin
    25.07.2024 19:26

    Просто не считайте пустые строки, тогда код можно будет отформатировать для чтения.

    И ещё интересный парадокс, если вынести логически близкие части в функции и классы, то кода станет больше, а читать его будет легче.


  1. domix32
    25.07.2024 19:26
    +1

    Сделайте на чём-нибудь комиплируемом в wasm. Тогда оно заведётся в том числе и в андроиде прямо в браузере.


    1. Yura_FX Автор
      25.07.2024 19:26

      Приму это к сведению :)


      1. razoryoutub
        25.07.2024 19:26
        +1

        Также существует pygame wasm