Лучший способ отпугнуть монстров на Хэллоуин — это не только свечку в тыкве зажечь, но и страшную игру написать.
Чтобы вы, при желании, смогли это сделать вместе со мной, не заморачиваясь настройками и установкой, выберем следующий учебный стек:
Python
Модуль p5py (p5.js, но только для Python)
- 
Online-IDE в браузере
(при желании, можно стандартно, скажем, в VS Code)
 

В чистом поле
Нарисуем красную/оранжевую тыкву, пока просто в стиле Майнкрафт в виде квадрата.
from p5py import *
run()
size(400, 300)
background(0, 0, 100)
# PumpKeen Game (like a Commander Keen)
x = width / 2
y = height - 30
size = 20
# Персонаж
fill(255, 0, 0)
rect((x, y), size, size)
Чтобы открыть в online-IDE, нажмите здесь.

Сделаем классно
Но давайте, что ль, сразу в ООП писать? Персонаж у нас уже есть. Создадим для него класс, экземпляр и всё такое:
from p5py import *
run()
# PumpKeen Game (Helloween + Commander Keen)
size(400, 300)
# Персонаж
class Player:
    def __init__(self):
        self.x = width / 2
        self.y = height - 30
        self.size = 20
    def display(self):
        fill(255, 0, 0)
        rect((self.x, self.y), self.size, self.size)
player = Player()
def draw():
    background(0, 0, 100)
    player.display()
Вообще, для одного персонажа можно и без классов обойтись. Но так код более структурный получается.
Главное — не стоять на месте
У нас, конечно, не Doom, а PumpKeen, но… Добавим движение. Чтобы стрелочками вправо-влево можно было перемещаться.
from p5py import *
run()
# PumpKeen Game (Helloween + Commander Keen)
size(400, 300)
# Персонаж
class Player:
    def __init__(self):
        self.x = width / 2
        self.y = height - 30
        self.speed = 5
        self.size = 20
    def move(self):
        if keyCode == LEFT_ARROW:
            self.x -= self.speed
        elif keyCode == RIGHT_ARROW:
            self.x += self.speed
    def display(self):
        fill(255, 0, 0)
        rect((self.x, self.y), self.size, self.size)
player = Player()
def draw():
    background(0, 0, 100)
    player.display()
def key_pressed():
    player.move()
Откуда взялись константы LEFT_ARROW и RIGHT_ARROW? Константы BACKSPACE, DELETE, ENTER, RETURN, TAB, ESCAPE, SHIFT, CONTROL, OPTION, ALT, UP_ARROW, DOWN_ARROW, LEFT_ARROW и RIGHT_ARROW — это просто удобные сокращения кодов клавиш. Их можно найти на сайте вроде keycode.info.
Ну вот и почти готов платформер. Герой есть, платформа есть.
Вот только сейчас клаву приходится долбить, чтобы он двигался. Тыц-тыц-тыц и сдвинулся на 15 пикселей всего. Не круто. Исправим на следующем шаге.
Беги, Пампкин, беги...
Перенесём движение персонажа в главный игровой цикл, чтобы при нажатии кнопки игрок продолжал двигаться:
def draw():
    background(0, 0, 100)
    player.move()
    player.display()p
Теперь если нажать стрелку влево или вправо, он начинает бежать без остановки.
Эти прыжки посложнее, чем я думал
Прыжки!
Какой же платформер без прыжков? Введём переменную для отслеживания его состояния:
self.is_jumping = False
self.jump_speed = 10
self.velocity_y = 0
Гравитация!
Добавим гравитацию, чтобы персонаж мог вернуться на землю после прыжка:
self.gravity = 0.5
Добавим метод «прыжок» Пампкину:
Суть простая — если не прыгал, то теперь прыгает:
def jump(self):
    if not self.is_jumping:
        self.is_jumping = True
        self.velocity_y = self.jump_speed
Границы!
Заодно добавим проверку, чтобы персонаж не выходил за пределы экрана:
self.x = constrain(self.x, 0, width - self.size)
constrain()— функция из p5.js, она же и в p5py. Проверка на границы – частая задача в играх и анимации
Соберём всё вместе и расширим метод move():
def move(self):
        if keyCode == LEFT_ARROW:
            self.x -= self.speed
        elif keyCode == RIGHT_ARROW:
            self.x += self.speed
        if self.is_jumping:
            self.y -= self.velocity_y
            self.velocity_y -= self.gravity
            # Если персонаж снова на земле
            if self.y >= height - 30:
                self.y = height - 30
                self.is_jumping = False
                self.velocity_y = 0
        # Ограничение по ширине
        self.x = constrain(self.x, 0, width - self.size)
Поправим key_pressed(), теперь будем в нём отслеживать только прыжок:
def key_pressed():
    if key == ' ':
        player.jump()  # Нажмите пробел для прыжка
Довольно резво теперь бегает и прыгает. 

Проверьте: код
Но, похоже, ему там скучно. Обещали праздник, Halloween, а посадили в пустую коробку. Сейчас поправим...
Декоративные платформы
Добавим платформы, по которым Пампкин сможет прыгать. Когда-нибудь. Но это не точно.
class Platform:
    def __init__(self, x, y, width):
        self.x = x
        self.y = y
        self.width = width
    def display(self):
        fill(0, 255, 0)
        rect((self.x, self.y), self.width, 10)
platforms = [Platform(0, height - 50, 100), Platform(200, height - 100, 100), Platform(400, height - 150, 100)]
def draw():
    background(0, 0, 100)
    player.move()
    player.display()
    for platform in platforms:
        platform.display()
Пока платформы только отображаются, а запрыгнуть на них никак нельзя. Декорация — она и в Хэллоуин декорация.
Сбор всякой нечисти
Можно добавить возможность сбора предметов (например, монеток) для набора очков.
class Collectible:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.size = 15
    def display(self):
        fill(255, 165, 0)
        ellipse((self.x, self.y), self.size, self.size)
collectibles = [Collectible(rand(0, width), rand(0, height - 100)) for _ in range(5)]
def draw():
    background(0, 0, 100)
    player.move()
    player.display()
    for platform in platforms:
        platform.display()
    for collectible in collectibles:
        collectible.display()
Монетки, как вы заметили, у нас пока просто кружочки. «Концептъ», так сказать.
Фармим
Можно добавить систему очков. Они будут увеличиваться при сборе предметов.
score = 0
def check_collectibles():
    global score
    for collectible in collectibles:
        if (player.x < collectible.x + collectible.size and
            player.x + player.size > collectible.x and
            player.y < collectible.y + collectible.size and
            player.y + player.size > collectible.y):
            collectibles.remove(collectible)
            score += 1
Не забудем вызывать
check_collectibles()в функции draw.
def draw():
    background(0, 0, 100)
    player.move()
    player.display()
    for platform in platforms:
        platform.display()
    for collectible in collectibles:
        collectible.display()
    check_collectibles()
    fill(255)
    text(f"Очки: {score}", (10, 20))  # Показать очки на экране
Можно порезвиться

Наш Пампкин теперь бегает и прыгает, собирает монетки и получает за это очки. А ещё он очень пытается запрыгнуть на платформу, но это ему пока не удаётся...
Подправим сеттинг
Заодно подправим цветовую гамму. Пусть будет более хэллоуински.
Заменим квадрат на тыкву:
class Player:
    def display(self):
        # fill(255, 140, 0)  # Оранжевый цвет для тыквы
        # rect((self.x, self.y), self.size, self.size)
        text_size(self.size)
        text_align(LEFT, TOP)
        text("?", (self.x, self.y))
А кружочки на алмазы:
class Collectible:
    def display(self):
        # fill(255, 165, 0)
        # ellipse((self.x, self.y), self.size, self.size)
        text_size(self.size)
        text_align(CENTER, CENTER)
        text("?", (self.x, self.y))

Финальный штрих. Превратим декоративные платформы в настоящие
Запрыгиваем на платформу. Всё что нужно сделать, это добавить проверку столкновения с платформами:
on_ground = False
for platform in platforms:
    if (self.x + self.size > platform.x and
        self.x < platform.x + platform.width and
        self.y + self.size >= platform.y and
        self.y + self.size <= platform.y + 10):  # Учитываем высоту платформы
        self.y = platform.y - self.size  # Поставить персонажа на платформу
        self.velocity_y = 0
        on_ground = True
        self.is_jumping = False
if not on_ground and self.y < height - 30 and not self.is_jumping:  # Если не на платформе и не на земле и не в прыжке
        self.y -= self.velocity_y
        self.velocity_y -= self.gravity
Разберём их чуть подробней
on_ground = False: эта переменная используется для отслеживания того, находится ли персонаж на платформе (то есть на земле). Изначально предполагается, что персонаж не находится на земле.
Цикл for platform in platforms: проходит по всем платформам, которые были созданы в игре.
Условия столкновения, проверяют, пересекается ли область персонажа с платформой::
if (self.x + self.size > platform.x and
    self.x < platform.x + platform.width and
    self.y + self.size >= platform.y and
    self.y + self.size <= platform.y + 10):
self.x + self.size > platform.x: проверяет, что правый край персонажа находится справа от левого края платформы.
self.x < platform.x + platform.width: проверяет, что левый край персонажа находится слева от правого края платформы.
self.y + self.size >= platform.y: проверяет, что нижний край персонажа находится выше верхнего края платформы.
self.y + self.size <= platform.y + 10: проверяет, что нижний край персонажа не проходит ниже низа платформы (учитывается высота платформы, которая равна 10).
self.y = platform.y - self.size: если персонаж сталкивается с платформой, его положение (по вертикали) устанавливается так, чтобы он «стоял» на платформе.
self.velocity_y = 0: сбрасывает вертикальную скорость персонажа, чтобы он не продолжал «падение» после столкновения с платформой.
on_ground = True: теперь мы знаем, что персонаж на земле, поэтому устанавливаем эту переменную в True.
if not on_ground and self.y < height - 30 and not self.is_jumping:: условный оператор проверяет, не находится ли персонаж на платформе (или на земле). Если персонаж не на земле и не прыгает, то его положение будет обновляться для симуляции гравитации.
self.y -= self.velocity_y и self.velocity_y -= self.gravity: обновляет положение персонажа с учётом гравитации. Если персонаж «в воздухе», то его вертикальная скорость уменьшается, что создаёт эффект падения...
Больше платформ, доступнее бонусы
Добавим новые платформы:
platforms = [
    Platform(0, height - 50, 100),
    Platform(200, height - 100, 100),
    Platform(400, height - 150, 100),
    Platform(100, height - 200, 150),
    Platform(500, height - 250, 150),
    Platform(300, height - 250, 120)
]
И чуть ниже сдвинем бонусы:
collectibles = [Collectible(rand(0, width), rand(30, height - 50)) for _ in range(5)]

Фикс джамп
Сейчас если при беге Пампкина и нажать пробел, то он останавливается и прыгает вверх. Давайте сделаем так, чтобы он не останавливался в этом случае, а продолжал бежать и одновременно прыгнул — получится прыжок по диагонали.
Для этого нам нужна не одна переменная, а две. Первая будет отвечать за горизонтальное движение, а вторая – за вертикальное. Добавим self.velocity_x = 0
Стандартная для физики проекция вектора скорости на ось X и Y.
А заодно причешем код в соответствии с принципами ООП. Инкапсулируем поведение в класс Пампкина. Спрячем внутрь класса обращение с его переменными: player.is_moving = False. Теперь у нас красивые функции, обрабатывающие клавиатуру. Почему красивые? Они ничего не знают о внутреннем устройстве класса, а просто дёргают его методы:
def key_pressed():
    if keyCode == SPACE:
        player.start_jump()
    if keyCode == LEFT_ARROW:
        player.start_move_left()
    if keyCode == RIGHT_ARROW:
        player.start_move_right()
def key_released():
    if keyCode == LEFT_ARROW or keyCode == RIGHT_ARROW:
        player.stop_horizontal_movement()
Вот так изменится метод move():
def move(self):
    self.x += self.velocity_x
    if self.is_jumping:
        self.y -= self.velocity_y
        self.velocity_y -= self.gravity
        # Если персонаж снова на земле
        if self.y >= height - 30:
            self.y = height - 30
            self.is_jumping = False
            self.velocity_y = 0
            self.is_moving = False
    # Проверка столкновения с платформами
    on_ground = False
    for platform in platforms:
        if (self.x + self.size > platform.x and
            self.x < platform.x + platform.width and
            self.y + self.size >= platform.y and
            self.y + self.size <= platform.y + 10):  # Учитываем высоту платформы
            self.y = platform.y - self.size  # Поставить персонажа на платформу
            self.velocity_y = 0
            on_ground = True
            self.is_jumping = False
    if not on_ground and self.y < height - 30 and not self.is_jumping:  # Если не на платформе и не на земле
            self.y -= self.velocity_y
            self.velocity_y -= self.gravity
    # Ограничение по ширине
    self.x = constrain(self.x, 0, width - self.size)
В нём мы заменили всё вот это:
    if self.is_moving or self.is_jumping :
        if keyCode == SPACE:
            self.start_jump()  # Нажмите пробел для прыжка
        if keyCode == LEFT_ARROW:
            self.x -= self.speed
        if keyCode == RIGHT_ARROW:
            self.x += self.speed
На одну строчку:
self.x += self.velocity_x
Так как обработку нажатий перенесли в key_pressed() и key_released().
Вы ловите багов? Красивое
Что-то всё слишком гладко, не находите? Так не бывает. А на самом деле у нас два бага:
Если прыгать с самой высокой платформы вниз, то Пампкин так сильно разгоняется, что перелетает платформу на этаж ниже и не останавливается на ней.
- 
Иногда Пампкин оказывается на земле ниже обычного своего уровня. И из-за этого появляется ещё более скрытый баг: в таком случае при нажатии пробела Пампкин выпрыгивает на свой обычный уровень у земли, вместо того чтобы сделать высокий прыжок.
Попробуйте поймать эти ошибки.
 
Второй баг исправляется элементарно. Добавим ограничение по вертикали, как раньше делали по горизонтали:
self.x = constrain(self.x, 0, width - self.size)
self.y = constrain(self.y, 0, height - 30)
А вот первый баг завязан на условие:
if (self.x + self.size > platform.x and
    self.x < platform.x + platform.width and
    self.y + self.size >= platform.y and
    self.y + self.size <= platform.y + 10):
Один из вариантов решения — это смотреть на один ход вперёд. То есть если текущее положение по y выше платформы, а следующее ниже, это значит, что мы летим очень быстро и нужно притормозить.
Сначала просто добавим ещё один if, а потом применим DRY, чтобы не повторяться:
for platform in platforms:
    if (self.x + self.size > platform.x and
        self.x < platform.x + platform.width and
        self.y + self.size >= platform.y and
        self.y + self.size <= platform.y + 10):  # Учитываем высоту платформы
        self.y = platform.y - self.size  # Поставить персонажа на платформу
        self.velocity_y = 0
        on_ground = True
        self.is_jumping = False
    if (self.x + self.size > platform.x and
        self.x < platform.x + platform.width and
        self.y + self.size < platform.y and
        self.y + self.size - self.velocity_y > platform.y + 10):  # Учитываем высоту платформы
        self.y = platform.y - self.size  # Поставить персонажа на платформу
        self.velocity_y = 0
        on_ground = True
        self.is_jumping = False
И сразу улучшим:
if (self.x + self.size > platform.x and
    self.x < platform.x + platform.width and
    (self.y + self.size >= platform.y and
    self.y + self.size <= platform.y + 10 or
    self.y + self.size < platform.y and
    self.y + self.size - self.velocity_y > platform.y + 10)):  # Учитываем высоту платформы
    self.y = platform.y - self.size  # Поставить персонажа на платформу
    self.velocity_y = 0
    on_ground = True
    self.is_jumping = False
Читабельность — вырви глаз.
PEP и 8 спешат на помощь
У нас конвенция по оформлению есть, вот пора к ней и обратиться: PEP 8 indentation. Там три рекомендуемых стиля, я выберу последний, получится так:
if (self.x + self.size > platform.x
        and self.x < platform.x + platform.width
        and (self.y + self.size >= platform.y
            and self.y + self.size <= platform.y + 10
            or self.y + self.size < platform.y
            and self.y + self.size - self.velocity_y > platform.y + 10)):
    self.y = platform.y - self.size
    self.velocity_y = 0
    on_ground = True
    self.is_jumping = False
Нууу, такое... Хотя блок кода лучше видно.
На прикиде
И заодно, раз уж заговорили о стиле, пора избавиться от магического числа 10 (высота платформы). Вынесем его куда-нибудь.Где лучше всего его хранить?
В отдельной константе PLATFORM_HEIGHT = 10? Брр, нет, конечно. Это же характеристика платформы, значит, ей место в классе Platform. Как это правильно сделать? Например, так:
class Platform:
    def __init__(self, x, y, width, height=10):  # Добавляем параметр height со значением по умолчанию
        self.x = x
        self.y = y
        self.width = width
        self.height = height  # Сохраняем высоту как атрибут
И заменим везде десяточки на self.height или platform.height. Заодно получили возможность задать каждой платформе свою высоту:
platforms = [
    Platform(0, height - 50, 100, 10),
    Platform(200, height - 100, 100),
    Platform(400, height - 150, 100, 20),
    Platform(100, height - 200, 150),
    Platform(500, height - 250, 150, 30),
    Platform(300, height - 250, 120)
]
ФИНАЛЬНЫЙ РЕЗУЛЬТАТ

Баги закончились?
А вот и нет. Как только мы сделали платформу толще, становится заметно, что в её середине спавнятся бонусы. И как их забрать? Эту доработку оставлю вам :)
Сможете репродуцировать этот баг? А пофиксить?
Разработка игры никогда не заканчивается. Но мы с вами можем продолжить в следующей части. Монстры, враги, бонусы, конец игры, заставка — много ещё что придумаем. Жду вас на продолжении.
Ну вот и всё
Если вам понравилось экспериментировать и вы заинтересовались развитием или использованием p5py, то подключайтесь к новой группе. 
А ещё можно глянуть:
— прошлую статью на Хабре про то, как мы пишем Игру Жизнь на p5py;
— или про книгу, с которой всё и началось.