Недавно на интересный GitHub-репозиторий, содержащий 14 реализаций классических игр на Python. Проекты вроде этого — настоящий клад для анализа. С одной стороны, это отличная песочница для начинающих, чтобы посмотреть, «как оно работает». С другой — это концентрат типичных ошибок, архитектурных компромиссов и «костылей», на разборе которых можно научиться гораздо большему, чем на вылизанных до блеска примерах из учебников.
Поэтому я решил сделать аудит этого репозитория. Мы не будем критиковать автора — создание даже таких, на первый взгляд, простых игр требует времени и усилий. Наша цель — использовать этот код как учебный материал. Мы разберем как удачные решения, так и слабые места, а главное — посмотрим, как можно было бы сделать лучше. Это будет полезно и тем, кто только начинает свой путь в разработке, и тем, кто уже имеет опыт, но хочет наточить свой навык «чтения» и анализа чужого кода.
Общий взгляд на репозиторий
Первое, что бросается в глаза — это разнообразие. Здесь собраны хиты разных эпох и жанров, от текстового «Виселица» до графических Pacman и Tetris. Для реализации использовался целый зоопарк технологий, что само по себе интересно.
Вот краткая сводка по играм и используемым библиотекам:
Игра | Библиотека/Фреймворк | Краткое описание |
---|---|---|
2048 | pygame |
Классическая головоломка со слиянием плиток. |
Checkers (Шашки) | pygame |
Реализация шашек с ООП подходом. |
Dino-Game | pygame |
Клон знаменитой игры из Google Chrome. |
Flappy Bird | pygame |
Тот самый "убийца нервных клеток". |
Hangman (Виселица) | built-in (консоль) |
Классическая игра в угадывание слов. |
Memory game | pygame |
Игра на развитие памяти с поиском парных карточек. |
Minesweeper (Сапер) | tkinter |
Стандартный "Сапер" на нативном GUI-фреймворке. |
Pacman | pygame |
Довольно проработанная версия Pacman. |
Racing game | pygame |
Простая 2D-гонка с уклонением от препятствий. |
Rock Paper Scissors | tkinter |
"Камень, ножницы, бумага" с графическим интерфейсом. |
Snake (Змейка) | turtle |
Классическая "Змейка" на модуле turtle . |
Tetris | pygame , kezmenu |
Тетрис с использованием сторонней библиотеки для меню. |
Whack-a-Mole | pygame |
Игра "Ударь крота". |
Wordle | colorama (консоль) |
Консольная версия популярной игры в слова. |
Общее впечатление — это сборник проектов, написанных, скорее всего, в разное время и, возможно, разными людьми или одним человеком на разных этапах обучения. Код варьируется от простых скриптов в одном файле до попыток разбить логику на несколько модулей. Это делает репозиторий идеальным кандидатом для нашего разбора.
Что хорошо? (Сильные стороны проекта)
Прежде чем переходить к разбору полетов, важно отметить сильные стороны.
1. Широта охвата и наглядность
Репозиторий — отличная демонстрация возможностей Python для создания игр. Новичок может посмотреть и сравнить, как одна и та же задача решается с помощью разных инструментов: pygame
для игр с активной анимацией, tkinter
для более статичного GUI и даже turtle
для простейших вещей.
2. Простота и прямолинейность кода
В большинстве игр логика написана «в лоб», без лишних абстракций и сложных паттернов. Для начинающего разработчика это огромный плюс. Легко отследить основной игровой цикл, понять, как обрабатываются события и как происходит отрисовка. Нет необходимости продираться через слои архитектуры, чтобы понять, как заставить птичку лететь вверх.
3. Функциональность
Несмотря на все потенциальные проблемы с кодом (о них ниже), игры работают. Они выполняют свою главную функцию — в них можно играть. Это важное напоминание о том, что работающий продукт, пусть и не идеальный с технической точки зрения, всегда лучше, чем идеально спроектированный, но так и не законченный.
А теперь перейдем к самой интересной части — анализу того, что можно было бы улучшить.
Зоны роста: Что можно улучшить?
Здесь мы разберем основные "болевые точки", которые повторяются в нескольких проектах. Это те самые грабли, на которые наступают многие начинающие разработчики.
1. Отсутствие единой структуры и управления зависимостями
Это первая и самая очевидная проблема. Чтобы запустить любую из игр, нужно сначала угадать, какие библиотеки ей нужны. В репозитории нет файла requirements.txt
.
[!DANGER] Проблема Пользователь, скачавший репозиторий, не может просто взять и запустить проект. Ему придется вручную ставить
pygame
,colorama
и, возможно, что-то еще, сталкиваясь с ошибкамиModuleNotFoundError
.
Как сделать лучше?
- Создать
requirements.txt
. Файл легко создается командойpip freeze > requirements.txt
в активированном виртуальном окружении, где установлены все зависимости. - Структурировать проект. Каждая игра находится в своей папке, и это хорошо. Но можно пойти дальше: создать общий
main.py
в корне проекта, который бы позволял выбирать и запускать любую игру из списка.
2. Глобальные переменные и «магические числа»
Это классика. Во многих файлах (например, Flappy Bird/game.py
или Dino-Game/dino.py
) ключевые параметры игры заданы как глобальные переменные или просто числа, разбросанные по коду.
Взглянем на фрагмент из Flappy Bird
:
# Flappy Bird/game.py
# Все игровые переменные
window_width = 600
window_height = 499
# ...
elevation = window_height * 0.8
game_images = {}
framepersecond = 32
pipeimage = 'images/pipe.png'
# ...
def flappygame():
# ...
pipeVelX = -4
# bird velocity
bird_velocity_y = -9
bird_Max_Vel_Y = 10
bird_Min_Vel_Y = -8
birdAccY = 1
bird_flap_velocity = -8
bird_flapped = False
# ...
[!WARNING] Проблема
- Магические числа: Что значат
4
,9
,10
? Без глубокого погружения в код понять это невозможно. Через месяц даже сам автор забудет их смысл.- Сложность настройки: Если мы захотим изменить скорость игры или гравитацию, нам придется искать эти значения по всему файлу, рискуя что-то пропустить.
- Глобальное состояние: Глобальные переменные (
window_width
,game_images
) создают неявные зависимости. Функции становятся непредсказуемыми, их сложно тестировать и переиспользовать.
Как сделать лучше?
- Вынести константы: Все "магические числа" должны быть вынесены в константы с говорящими именами в начало файла.
- Использовать классы или dataclass для конфигурации: Создать класс
GameConfig
, который будет хранить все настройки игры (размеры окна, скорости, ускорения). Этот объект конфигурации можно передавать в функции или методы, делая их зависимости явными. - Инкапсулировать логику в класс: Вместо набора глобальных переменных и функций создать класс
FlappyBirdGame
. Все переменные состояния (score
,bird_velocity_y
) станут его атрибутами (self.score
,self.bird_velocity_y
), а функции — методами.
Пример рефакторинга:
# Предлагаемый рефакторинг
class GameConfig:
WINDOW_WIDTH = 600
WINDOW_HEIGHT = 499
FPS = 32
GRAVITY = 1
BIRD_FLAP_VELOCITY = -8
PIPE_VELOCITY_X = -4
# ... и так далее
class FlappyBirdGame:
def __init__(self, config: GameConfig):
self.config = config
self.score = 0
self.bird_velocity_y = 0
self.is_flapped = False
# ... инициализация других состояний
def _handle_input(self, event):
if event.type == KEYDOWN and (event.key == K_SPACE or event.key == K_UP):
if self.vertical > 0:
self.bird_velocity_y = self.config.BIRD_FLAP_VELOCITY
self.is_flapped = True
def _update_state(self):
# ... логика обновления состояния, используя self.config и self. ...
def run(self):
# Основной игровой цикл
while True:
# ...
Такой подход делает код чище, понятнее и гораздо проще для модификации.
3. Смешение логики, состояния и отображения
Еще одна распространенная проблема, особенно в проектах на pygame
. Код, отвечающий за игровую логику (как движется объект), состояние (где он находится) и отображение (как его нарисовать), часто перемешан в одном большом цикле.
Это видно в игре Pacman. Несмотря на то, что код разбит на файлы (player.py
, enemies.py
, game.py
), сама архитектура остается процедурной. Класс Game
в game.py
— это огромный монолит, который управляет всем: событиями, логикой, отрисовкой.
[!WARNING] Проблема
- Высокая связанность (High Coupling): Изменение логики отрисовки может сломать игровую логику, и наоборот.
- Низкая переиспользуемость: Нельзя просто так взять класс
Player
и использовать его в другой игре, потому что он тесно связан сGame
,horizontal_blocks
и другими элементами.- Сложность тестирования: Как протестировать логику движения игрока, не запуская весь графический движок? В текущей реализации — почти никак.
Как сделать лучше?
Следовать принципу разделения ответственности (Separation of Concerns).
- Модель (Model): Классы, которые описывают состояние и логику игры, не зная ничего об отрисовке.
PlayerModel
должен знать свою позицию, скорость, но не то, как его рисовать.GameModel
должен содержать матрицу уровня, положение всех врагов, счет. - Представление (View): Классы, отвечающие только за отрисовку.
PlayerView
получаетPlayerModel
и рисует спрайт в нужной координате.GameView
рисует поле, врагов, счет, основываясь на данных изGameModel
. - Контроллер (Controller): Модуль, который связывает всё вместе. Он обрабатывает ввод от пользователя (клавиатура, мышь), вызывает методы у Модели для изменения ее состояния, а затем говорит Представлению перерисовать экран на основе обновленной Модели.
Этот подход (вариация на тему MVC/MVP) позволяет разрабатывать, тестировать и модифицировать каждую часть системы независимо.
4. Прямые манипуляции с координатами вместо физической модели
В гоночной игре (Racing game/main.py
) или в "Змейке" (Snake/game.py
) движение реализовано прямым изменением координат в игровом цикле.
# Racing game/main.py
class Car:
# ...
def move_left(self):
if self.x > (SCREEN_WIDTH - ROAD_WIDTH) // 2:
self.x -= self.speed
# ...
# В игровом цикле:
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
car.move_left()
[!NOTE] Проблема
Это работает для простых игр, но не масштабируется. Если мы захотим добавить ускорение, инерцию, трение или столкновения между несколькими объектами, такой код превратится в "лапшу" из
if
-ов.
Как сделать лучше?
Даже для простой 2D-игры можно ввести простейшую физическую модель.
- Векторы: Использовать векторы (
pygame.math.Vector2
) для представления позиции, скорости и ускорения. - Обновление на основе времени: Вместо
self.x -= self.speed
использоватьself.position += self.velocity * dt
, гдеdt
— это время, прошедшее с последнего кадра. Это делает движение независимым от FPS (частоты кадров).
Пример рефакторинга:
# Предлагаемый рефакторинг для Car
import pygame
class Car:
def __init__(self):
# ...
self.position = pygame.math.Vector2((SCREEN_WIDTH - CAR_WIDTH) // 2, SCREEN_HEIGHT - CAR_HEIGHT - 10)
self.velocity = pygame.math.Vector2(0, 0)
self.acceleration = pygame.math.Vector2(0, 0)
self.max_speed = 5
self.engine_force = 1.0
self.friction = -0.1 # Сила трения
def update(self, dt):
# Физика: F = ma. Здесь a = F/m. Примем массу за 1.
# Сила трения пропорциональна скорости
friction_force = self.velocity * self.friction
self.acceleration += friction_force
# Обновляем скорость и позицию
self.velocity += self.acceleration * dt
# Ограничиваем максимальную скорость
if self.velocity.length() > self.max_speed:
self.velocity.scale_to_length(self.max_speed)
self.position += self.velocity * dt
# Сбрасываем ускорение от двигателя каждый кадр
self.acceleration = pygame.math.Vector2(0, 0)
def apply_engine_force(self, direction_vector):
self.acceleration += direction_vector * self.engine_force
# В игровом цикле:
dt = clock.tick(FPS) / 1000.0 # Время в секундах
keys = pygame.key.get_pressed()
direction = pygame.math.Vector2(0, 0)
if keys[pygame.K_LEFT]:
direction.x = -1
if keys[pygame.K_RIGHT]:
direction.x = 1
car.apply_engine_force(direction)
car.update(dt)
Да, этот код сложнее, но он закладывает фундамент для гораздо более реалистичного и расширяемого поведения объектов.
5. Непоследовательное применение ООП и архитектурные просчеты
Во многих играх, например, в "Шашках" (Checkers
), автор сделал похвальную попытку применить объектно-ориентированный подход. Есть классы Board
, Tile
, Piece
, и даже наследование Pawn
и King
от Piece
. Это абсолютно правильное направление мысли, которое отделяет этот код от простого процедурного скрипта.
Однако дьявол, как всегда, в деталях. Давайте заглянем в файл Checkers/Piece.py
:
# Checkers/Piece.py
import pygame
class Piece:
# ...
def _move(self, tile):
# ... много логики ...
# Pawn promotion
if self.notation == 'p':
if self.y == 0 or self.y == 7:
from King import King # <---- ВОТ ОНО
tile.occupying_piece = King(
self.x, self.y, self.color, self.board
)
return True
# ...
Импорт модуля King
внутри метода _move
— это классический "код с запашком" (code smell).
[!WARNING] Проблема: Локальный импорт и циклические зависимости
Причина: Почему автор так сделал? Почти наверняка, чтобы избежать циклической зависимости. Смотрите:
Piece.py
должен знать оKing
, чтобы превратить в него пешку. НоKing.py
импортируетPiece
, так как наследуется от него (class King(Piece):
). Если быfrom King import King
стоял вверху файлаPiece.py
, Python выдал бы ошибкуImportError
.Скрытые зависимости: Такой импорт скрывает реальные зависимости модуля. Глядя на начало файла, невозможно понять, что ему на самом деле нужен
King
.Сигнал о проблеме в архитектуре: Сам факт возникновения такой ситуации — это флаг, который кричит: "Что-то не так с вашей архитектурой!". Классы слишком тесно связаны и знают друг о друге больше, чем должны.
Как сделать лучше?
Есть несколько путей решения, от простого к более архитектурно верному.
Forward References: Python позволяет элегантно решать проблему циклических зависимостей для тайп-хинтинга. Хотя здесь хинтинга нет, принцип похож. Мы можем делегировать создание "Короля" классу
Board
, который знает обо всех типах фигур.Делегирование ответственности (Архитектурное решение): Это самый правильный путь. Класс
Piece
не должен отвечать за свою "прокачку". Его дело — двигаться. А вот доска (Board
) — идеальное место для этой логики.
Пример рефакторинга:
# В файле Board.py
# Импортируем все типы фигур в одном месте
from Pawn import Pawn
from King import King
class Board:
# ...
def promote_pawn_if_needed(self, piece, tile):
"""Проверяет, нужно ли превращать пешку в короля, и делает это."""
if isinstance(piece, Pawn) and (tile.y == 0 or tile.y == 7):
# Заменяем объект пешки на объект короля
tile.occupying_piece = King(tile.x, tile.y, piece.color, self)
def handle_click(self, pos):
# ...
# Вместо того, чтобы piece._move() сам себя превращал,
# он просто возвращает True в случае успеха
if self.selected_piece._move(clicked_tile):
# А уже доска решает, нужно ли превращение
self.promote_pawn_if_needed(clicked_tile.occupying_piece, clicked_tile)
# ... остальная логика
В этом случае Piece.py
больше вообще не нужно знать о существовании King.py
. Мы разрываем порочную связь, и каждый класс занимается своим делом. Piece
двигается, Board
управляет правилами и состоянием игры.
6. Управление ресурсами (ассетами) и «хардкод» путей
В большинстве графических игр ресурсы (картинки, звуки) загружаются прямо по месту их использования с жестко заданными путями.
Пример из Dino-Game/dino.py
:
# ...
jump_sound = pygame.mixer.Sound('resources/jump.wav')
die_sound = pygame.mixer.Sound('resources.bak/die.wav')
checkPoint_sound = pygame.mixer.Sound('resources/checkPoint.wav')
def load_image(name, sx=-1, sy=-1, colorkey=None):
fullname = os.path.join('resources', name)
img = pygame.image.load(fullname)
# ...
[!DANGER] Проблема
- Хрупкость: Такой код будет работать, только если вы запускаете скрипт из корня папки
Dino-Game
. Попытка запустить его из родительской директории (python Dino-Game/dino.py
) приведет к ошибкеFileNotFoundError
, потому что он будет искать папкуresources
не там, где нужно.- Отсутствие централизации: Пути к ресурсам разбросаны по всему коду. Если мы решим переименовать папку
resources
вassets
, нам придется править код во многих местах.- Повторная загрузка: Нет никакой гарантии, что один и тот же ресурс не будет загружен в память несколько раз в разных частях программы.
Как сделать лучше?
Использовать абсолютные пути на основе расположения файла: Python позволяет легко получить путь к текущему исполняемому файлу. Это делает загрузку ресурсов независимой от того, откуда был запущен скрипт.
import os # Получаем абсолютный путь к директории, где лежит наш скрипт BASE_DIR = os.path.dirname(os.path.abspath(__file__)) # Строим надежный путь к ресурсу def get_asset_path(filename): return os.path.join(BASE_DIR, 'resources', filename) # Используем jump_sound = pygame.mixer.Sound(get_asset_path('jump.wav'))
Создать менеджер ресурсов (Asset Manager): Это еще один шаг к хорошей архитектуре. Менеджер ресурсов — это класс, который отвечает за загрузку и хранение всех ассетов. Он гарантирует, что каждый ресурс загружается только один раз (ленивая загрузка по требованию) и предоставляет удобный интерфейс для доступа к нему.
Пример простого менеджера ресурсов:
import pygame
import os
class AssetManager:
def __init__(self, base_path=None):
self._images = {}
self._sounds = {}
if base_path is None:
# Автоматически определяем путь к папке 'assets' рядом со скриптом
self.base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'assets')
else:
self.base_path = base_path
def get_image(self, filename, scale=None, colorkey=None):
# Используем filename как ключ, чтобы не загружать повторно
if filename not in self._images:
path = os.path.join(self.base_path, 'images', filename)
try:
image = pygame.image.load(path).convert()
self._images[filename] = image
except pygame.error as e:
print(f"Cannot load image: {path}")
raise SystemExit(e)
# Возвращаем копию, чтобы избежать случайных изменений оригинала
img = self._images[filename].copy()
if scale:
img = pygame.transform.scale(img, scale)
if colorkey:
img.set_colorkey(colorkey)
return img
def get_sound(self, filename):
if filename not in self._sounds:
path = os.path.join(self.base_path, 'sounds', filename)
try:
self._sounds[filename] = pygame.mixer.Sound(path)
except pygame.error as e:
print(f"Cannot load sound: {path}")
raise SystemExit(e)
return self._sounds[filename]
# Использование в игре:
# assets = AssetManager()
# player_img = assets.get_image('player.png', scale=(50, 50))
# jump_sfx = assets.get_sound('jump.wav')
Такой подход не только решает проблему с путями, но и централизует управление ресурсами, делая код чище и профессиональнее.
Понравился материал?
Ваша поддержка — это энергия для новых статей и проектов. Спасибо, что читаете!
Заключение: от работающего кода к хорошему коду
Разбор этого репозитория — прекрасная иллюстрация пути, который проходит каждый разработчик. Сначала мы учимся писать код, который работает. Это само по себе большое достижение. Игры в этом сборнике — прекрасный пример такого кода.
Следующий этап — научиться писать хороший код. Код, который легко читать, модифицировать, тестировать и расширять. И именно здесь анализ чужих ошибок и поиск лучших решений становятся бесценным инструментом обучения.
Ключевые выводы из нашего аудита:
- Конфигурация — отдельно, код — отдельно. Выносите "магические числа" в константы или классы конфигурации.
- Разделяйте ответственности. Старайтесь изолировать игровую логику (модель) от отрисовки (представление).
- Думайте об архитектуре. Даже в маленьком проекте правильная структура (например, разрыв циклических зависимостей) сэкономит вам массу времени в будущем.
- Централизуйте управление ресурсами. Не "хардкодьте" пути. Создайте надежную систему для загрузки ассетов.
Этот репозиторий, со всеми его шероховатостями, — отличный учебный полигон. Попробуйте взять одну из игр и отрефакторить ее, применяя описанные выше принципы. Это будет одно из самых полезных упражнений, которые вы можете сделать для своего профессионального роста.