Игра выросла: в ней есть меню, пауза, звук и рекорд в файле. Всё работает, но main.py разбух, а числа вроде 960 и 360 разбросаны по нескольким файлам. В этом уроке мы наведём порядок: соберём все настройки в одном месте и научимся проверять логику игры одной командой, не открывая окно.
Как выглядит беспорядок
Представьте кухню, где соль лежит в трёх разных шкафах: на одной полке, у плиты и в коробке с чаем. Пока вы один и помните, где что, жить можно. Но стоит позвать помощника или вернуться через месяц, и вы начинаете открывать все дверцы подряд. В коде бывает то же самое.
Первый признак — длинный файл. Когда в main.py набралось двести пятьдесят строк и в нём живут цикл, три экрана, загрузка рекорда и десяток констант, нужную строку приходится искать глазами. Второй признак — числа без имени. Программисты называют их магическими числами: читаешь код и не понимаешь, откуда взялась цифра и что случится, если её поменять.
ship_x = 500
meteor_top = 700
if ship_x > 960 - 24:
ship_x = 960 - 24
if meteor_top > 640 + 40:
print("метеор улетел за экран")
Что здесь значат 960, 640 и 24? Для вас сегодня это ширина окна, высота и половина корабля. Через неделю это просто числа. А если вы решите сделать окно шире, придётся найти каждое место, где написано 960. Пропустите одно — корабль упрётся в невидимую стену, и вы долго будете искать причину.
Правило одного места
У каждого числа, которое вы захотите менять, должно быть имя, и записано оно должно быть ровно один раз. Остальной код пользуется именем, а не числом.
Файл настроек: один щиток для всей игры
В квартире все автоматы собраны на одном щитке: хотите выключить свет в кладовке — идёте к щитку, а не лазите по стенам. Сделаем такой щиток для игры. Это отдельный файл settings.py, в котором лежат только имена и значения: размер окна, скорость корабля, очки, времена, цвета.
Официально такой файл — обычный модуль Python, а значения в нём — константы. Константа — это переменная, которую программа не меняет после запуска. Python не запрещает её менять, поэтому есть договорённость: имена констант пишут ЗАГЛАВНЫМИ_БУКВАМИ. Увидели SHIP_SPEED — значит, это настройка, а не рабочая переменная.
Подключаются настройки так же, как вы подключали logic.py: командой from ... import.
from settings import WIDTH, HEIGHT, SHIP_SPEED
ship_x = WIDTH // 2
max_x = WIDTH - 24
print(ship_x, max_x, SHIP_SPEED)
Разбор по строкам
from settings import WIDTH, HEIGHT, SHIP_SPEED— из модуляsettings(файлsettings.py) берём три имени. Теперь в этом файле можно писатьWIDTHтак, словно мы объявили его здесь.ship_x = WIDTH // 2— корабль стоит посередине окна; число 960 в этой строке нигде не написано.max_x = WIDTH - 24— правая граница; когда вsettings.pyизменится ширина, граница сдвинется сама.print(ship_x, max_x, SHIP_SPEED)— для окна 960 и скорости 360 напечатает480 936 360.
Что выносить, а что нет
Выносите числа, которые вы захотите покрутить, чтобы игра стала легче, быстрее или красивее: размеры, скорости, очки, длительности, цвета. Не выносите то, что вычисляется по ходу игры (score, lives, положение корабля) и то, что без поворота игры не имеет смысла менять (число 2 в формуле x // 2).
Есть и приятный побочный эффект. Хотите игру сложнее? Открываете один файл и меняете START_LIVES с трёх на один. Хотите друга порадовать — ставите пять жизней. Ни в какой другой файл заходить не нужно.
Карта проекта
Теперь у каждого файла своя работа. Запомните эту карту: при любом изменении вы будете знать, в какой файл идти.
| Файл | Что в нём живёт |
|---|---|
main.py | игровой цикл, экраны меню, паузы и «Игра окончена», загрузка и сохранение рекорда |
sprites.py | классы Player, Meteor, Crystal, Laser, Explosion и загрузка картинок и звуков |
logic.py | чистые функции и классы без pygame: движение, столкновения, таймеры, состояния, очки |
settings.py | только константы: размеры, скорости, очки, времена, цвета |
check_logic.py | проверки assert для logic.py |
make_assets.py | создаёт картинки и звуки в assets/; запускается один раз |
assets/ | готовые .png и .wav |
record.json | рекорд; появляется после первой игры |
Заметьте, кто про кого знает. main.py знает про все файлы, sprites.py — про logic.py и settings.py, logic.py — только про settings.py, а settings.py не знает ни о ком. Так и должно быть: фундамент не зависит от того, что на нём построено. Импорты идут строго снизу вверх, и по кругу ходить нечему.
Проверки логики: assert
Перед каждым полётом пилот идёт по контрольному списку: двигатель, закрылки, топливо. Он не «чувствует, что вроде всё нормально», а отмечает пункт за пунктом, и один неотмеченный пункт отменяет взлёт. Так же можно проверять функции: записать, что должно получиться, и пусть компьютер сверит.
В Python для этого есть слово assert (по-русски — «утверждение»). Вы пишете условие, которое обязано быть верным. Если оно верно, ничего не происходит. Если нет, программа останавливается с ошибкой AssertionError и показывает строку, где утверждение не сбылось.
from logic import clamp
assert clamp(5, 0, 3) == 3
assert clamp(-1, 0, 3) == 0, "ниже границы должен быть low"
print("Все проверки пройдены")
Разбор по строкам
from logic import clamp— берём изlogic.pyфункцию, которую хотим проверить.assert clamp(5, 0, 3) == 3— утверждаем: число 5, зажатое между 0 и 3, станет 3. Условие верное, поэтому строка молча проходит.assert clamp(-1, 0, 3) == 0, "..."— после запятой можно дописать сообщение: оно появится рядом с ошибкой, если проверка провалится.print("Все проверки пройдены")— эта строка выполнится только если ни одно утверждение выше не упало.
Если в clamp забыть нижнюю границу, вы увидите примерно такое (стрелки ^^^ показывают, какое выражение дало неверный ответ):
Traceback (most recent call last):
File "check_logic.py", line 4, in <module>
assert clamp(-1, 0, 3) == 0, "ниже границы должен быть low"
^^^^^^^^^^^^^^^^^^^^
AssertionError: ниже границы должен быть low
Теперь вы сразу знаете, какая именно проверка упала и что от неё ожидалось. Хорошая привычка: по одной-три проверки на функцию — обычный случай, граница и «странный» ввод (ноль, отрицательное число, пустое значение).
Ловушка: assert можно выключить
Если запустить программу как python -O файл.py, все assert будут пропущены. Для наших проверок это не страшно, мы запускаем их обычной командой. Но запомните: assert — инструмент проверки кода, а не защита от неправильного ввода игрока.
Почему всё это стало возможно
Помните главный приём курса: логика отдельно от отрисовки? Именно из-за него check_logic.py запускается за долю секунды: logic.py не импортирует pygame, а значит, не нужны ни окно, ни звуковая карта, ни картинки. Вы поменяли формулу уровней — запустили проверки — через секунду знаете, не сломали ли что-то. Без разделения пришлось бы запускать игру и руками играть до нужного места. И это те же самые проверки, которыми проверяют ваши решения задачи курса: функция получает числа и возвращает результат, а правильность сверяется сравнением.
Практика: наводим порядок в «Комете»
Работаем в папке kometa/, виртуальное окружение активно. Порядок важен: сначала создаём settings.py, потом по одному подключаем к нему остальные файлы. После каждого файла игра должна по-прежнему запускаться.
Шаг 1. Файл settings.py
Создайте рядом с main.py новый файл settings.py. Мы собрали в него все числа и цвета, которые до сих пор жили в main.py, sprites.py и в начале logic.py (формулы уровней), и добавили несколько чисел, которые раньше были разбросаны прямо по коду: STAR_COUNT, FONT_SIZE, BIG_FONT_SIZE, SHADE_ALPHA, PLAYER_START_X, PLAYER_START_Y и METEOR_SPIN.
"""Все настройки игры «Комета». Здесь только константы, импортов нет."""
# Окно
WIDTH = 960
HEIGHT = 640
FPS = 60
TITLE = "Комета"
# Цвета
BG_COLOR = (12, 14, 32)
WHITE = (255, 255, 255)
GRAY = (150, 150, 170)
RED = (255, 90, 120)
TITLE_COLOR = (90, 200, 255)
SELECT_COLOR = (255, 220, 80)
BOX_COLOR = (80, 255, 120)
CIRCLE_COLOR = (255, 220, 80)
# Оформление
FONT_SIZE = 32
BIG_FONT_SIZE = 96
SHADE_ALPHA = 150
STAR_COUNT = 120
STAR_SPEED = 40
MENU_ITEMS = ["Играть", "Выход"]
# Корабль
PLAYER_SPEED = 360
PLAYER_RADIUS = 16
PLAYER_START_X = WIDTH // 2
PLAYER_START_Y = HEIGHT - 80
SHIP_HALF = 24
SHIP_MIN_Y = HEIGHT // 2
SHIP_MAX_Y = HEIGHT - 40
SHIP_BOUNDS = (SHIP_HALF, SHIP_MIN_Y, WIDTH - SHIP_HALF, SHIP_MAX_Y)
START_LIVES = 3
INVULNERABLE_TIME = 1.5
BLINK_PERIOD = 0.1
# Выстрелы
SHOT_COOLDOWN = 0.25
LASER_SPEED = 700
# Метеоры и кристаллы
METEOR_MIN_SIZE = 20
METEOR_MAX_SIZE = 48
METEOR_SPIN = 90
SPEED_SPREAD = 40
CRYSTAL_RADIUS = 14
CRYSTAL_SPEED = 90
CRYSTAL_INTERVAL = 2.5
OFFSCREEN_MARGIN = 40
# Очки и уровни
METEOR_SCORE = 10
CRYSTAL_SCORE = 25
SCORE_PER_LEVEL = 200
MAX_LEVEL = 10
LEVEL_BANNER_TIME = 1.5
BASE_SPEED = 180
SPEED_STEP = 30
BASE_INTERVAL = 0.9
INTERVAL_STEP = 0.07
MIN_INTERVAL = 0.25
# Взрывы и звук
EXPLOSION_COUNT = 6
EXPLOSION_FRAME_TIME = 0.06
MASTER_VOLUME = 0.8
EFFECT_LEVELS = {"shot": 0.5, "boom": 0.9, "pickup": 0.7}
MUSIC_LEVEL = 0.4
Разбор по строкам
PLAYER_START_X = WIDTH // 2— в файле настроек одна константа может пользоваться другой: ширина изменится — стартовая точка корабля сдвинется сама.SHIP_BOUNDS = (SHIP_HALF, SHIP_MIN_Y, ...)— рамки корабля вычисляются из размеров окна.SHIP_HALF,SHIP_MIN_YиSHIP_MAX_Yнужны только для этой строки, поэтому в других файлах их импортировать не придётся.EFFECT_LEVELS = {...}— словарь тоже можно держать в настройках: уровни громкости выстрела, взрыва и кристалла лежат рядом сMASTER_VOLUME.# Окно, # Цвета, ...— комментарии-заголовки делят файл на группы, чтобы нужное число находилось глазами.
Два пути остаются на месте
ASSETS в sprites.py и RECORD_PATH в main.py мы в settings.py не переносим. Они считаются от расположения файла (Path(__file__).parent) и скоро потребуют отдельного разговора, когда мы будем собирать игру для друзей.
Шаг 2. logic.py берёт формулы уровней из настроек
Всё в начале logic.py от первой строки до первой функции (ship_points) замените на такой блок: он подключает json, math и нужные настройки.
# Логика игры: только чистый Python, без pygame.
import json
import math
from settings import (
BASE_INTERVAL,
BASE_SPEED,
INTERVAL_STEP,
MAX_LEVEL,
MIN_INTERVAL,
SCORE_PER_LEVEL,
SPEED_STEP,
)
Затем найдите перед функцией level_for_score семь строк с константами и удалите их: SCORE_PER_LEVEL, MAX_LEVEL, BASE_SPEED, SPEED_STEP, BASE_INTERVAL, INTERVAL_STEP, MIN_INTERVAL. Функции остаются как были: имена те же, но теперь они приходят из settings.py. Pygame в logic.py по-прежнему нет, главный приём курса не нарушен.
Шаг 3. sprites.py
Строку from logic import ... и все константы под ней (до EXPLOSION_FRAME_TIME = 0.06 включительно) замените на этот блок. Строка ASSETS стояла среди них, поэтому она уже включена в блок и остаётся на своём месте (в уроке 7.3 мы научим её находить папку внутри сборки):
from logic import direction_from_keys, frame_index, step_ship
from settings import (
CRYSTAL_RADIUS,
CRYSTAL_SPEED,
EXPLOSION_FRAME_TIME,
LASER_SPEED,
METEOR_MAX_SIZE,
METEOR_MIN_SIZE,
METEOR_SPIN,
OFFSCREEN_MARGIN,
PLAYER_RADIUS,
PLAYER_SPEED,
SPEED_SPREAD,
)
ASSETS = Path(__file__).parent / "assets"
В классе Meteor, в методе __init__, замените строку со скоростью вращения (остальное в методе не трогайте):
class Meteor(pygame.sprite.Sprite):
def __init__(self, width, height, speed):
# ...остальной код без изменений...
self.spin = random.randint(-METEOR_SPIN, METEOR_SPIN)
Параметры width и height в Meteor(width, height, speed) и Crystal(width, height) мы оставляем. Размер окна — не собственная настройка спрайта, а сведения, которые ему сообщает хозяин (main.py). Тогда спрайт не привязан к конкретному окну, и его легко проверить с любым размером.
Шаг 4. main.py
Здесь три правки. Первая: замените весь блок импортов в начале файла на этот.
import json
import random
from pathlib import Path
import pygame
from logic import (
Cooldown,
SpawnTimer,
best_score,
blink_visible,
effect_volume,
handle_key,
level_for_score,
menu_move,
meteor_speed,
next_state,
parse_record,
spawn_interval,
wrap,
)
from settings import (
BG_COLOR,
BIG_FONT_SIZE,
BLINK_PERIOD,
BOX_COLOR,
CIRCLE_COLOR,
CRYSTAL_INTERVAL,
CRYSTAL_SCORE,
EFFECT_LEVELS,
EXPLOSION_COUNT,
FONT_SIZE,
FPS,
GRAY,
HEIGHT,
INVULNERABLE_TIME,
LEVEL_BANNER_TIME,
MASTER_VOLUME,
MENU_ITEMS,
METEOR_SCORE,
MUSIC_LEVEL,
PLAYER_START_X,
PLAYER_START_Y,
RED,
SELECT_COLOR,
SHADE_ALPHA,
SHIP_BOUNDS,
SHOT_COOLDOWN,
START_LIVES,
STAR_COUNT,
STAR_SPEED,
TITLE,
TITLE_COLOR,
WHITE,
WIDTH,
)
from sprites import ASSETS, Crystal, Explosion, Laser, Meteor, Player, load_frames, load_sound
Вторая: удалите все константы, от строки WIDTH = 960 до строки MENU_ITEMS = ["Играть", "Выход"] включительно. Остаться должна только RECORD_PATH = ... и функции чтения и записи рекорда под ней. Третья: в нескольких местах числа заменяются именами.
| Было | Стало |
|---|---|
font = pygame.font.Font(None, 32) | font = pygame.font.Font(None, FONT_SIZE) |
font_big = pygame.font.Font(None, 96) | font_big = pygame.font.Font(None, BIG_FONT_SIZE) |
shade.set_alpha(150) | shade.set_alpha(SHADE_ALPHA) |
for _ in range(120): | for _ in range(STAR_COUNT): |
Player(WIDTH // 2, HEIGHT - 80, SHIP_BOUNDS) (два места: до цикла и в new_game) | Player(PLAYER_START_X, PLAYER_START_Y, SHIP_BOUNDS) |
Совет: поиск по файлу
Нажмите Ctrl+F (на macOS Cmd+F) и найдите 960, 640, 32), 96), 150, 120 и 80,. Редактор покажет все места сразу; те, что остались, заменяйте по таблице.
Шаг 5. make_assets.py
Скрипт, который делает картинки и звуки, тоже хранил своё число кадров взрыва. В начале файла замените строку from logic import ship_points двумя строками и удалите строку EXPLOSION_COUNT = 6:
from logic import ship_points
from settings import EXPLOSION_COUNT
Теперь число кадров взрыва записано один раз, и скрипт, и игра берут его из одного места.
Шаг 6. check_logic.py
Создайте файл check_logic.py рядом с остальными. В нём проверки для каждой функции и каждого класса из logic.py: обычный случай, граница и странный ввод. Старайтесь ставить проверки по группам и подписывать их комментариями, чтобы файл читался как оглавление логики.
"""Проверки logic.py. Запуск: python check_logic.py"""
from logic import (
Cooldown,
Mover,
SpawnTimer,
best_score,
blink_visible,
circles_overlap,
clamp,
direction_from_keys,
effect_volume,
frame_index,
handle_key,
level_for_score,
menu_move,
meteor_speed,
move,
next_state,
parse_record,
rects_overlap,
ship_points,
spawn_interval,
step_ship,
wrap,
)
# Движение и границы
assert ship_points(100, 100, 10) == [(100, 90), (93.0, 106.0), (107.0, 106.0)]
assert move(10, 20, 100, -50, 0.5) == (60.0, -5.0)
assert direction_from_keys(0, 0, 0, 0) == (0.0, 0.0)
assert direction_from_keys(0, 1, 0, 0) == (1.0, 0.0)
assert round(direction_from_keys(0, 1, 0, 1)[0], 3) == 0.707, "диагональ должна быть нормирована"
assert clamp(5, 0, 3) == 3
assert clamp(-1, 0, 3) == 0
assert clamp(2, 0, 3) == 2
assert wrap(11, 0, 10) == 1
assert wrap(-1, 0, 10) == 9
assert step_ship(100, 100, 1, 0, 100, 0.5, (0, 0, 200, 200)) == (150.0, 100.0)
assert step_ship(190, 100, 1, 0, 100, 0.5, (0, 0, 200, 200)) == (200, 100.0), "корабль не вылетает за рамки"
mover = Mover(0, 0, 10, 20)
mover.update(0.5)
assert mover.position() == (5.0, 10.0)
# Таймеры и столкновения
timer = SpawnTimer(1.0)
assert timer.update(0.4) == 0
assert timer.update(0.7) == 1
assert timer.update(2.5) == 2, "остаток времени не теряется"
cooldown = Cooldown(0.25)
assert cooldown.ready()
cooldown.start()
assert not cooldown.ready()
cooldown.update(0.3)
assert cooldown.ready()
assert rects_overlap((0, 0, 10, 10), (5, 5, 10, 10))
assert not rects_overlap((0, 0, 10, 10), (10, 0, 10, 10)), "касание не считается пересечением"
assert circles_overlap(0, 0, 5, 8, 0, 5)
assert not circles_overlap(0, 0, 5, 10, 0, 5)
# Мигание, анимация, звук
assert blink_visible(0, 0.1)
assert blink_visible(0.05, 0.1)
assert not blink_visible(0.15, 0.1)
assert frame_index(0.13, 0.06, 6, False) == 2
assert frame_index(0.5, 0.06, 6, False) is None
assert frame_index(0.5, 0.06, 6, True) == 2
assert effect_volume(0.8, 0.5, False) == 0.4
assert effect_volume(0.8, 0.5, True) == 0.0
assert effect_volume(2, 1, False) == 1.0
# Состояния и уровни
assert next_state("menu", "start") == "play"
assert next_state("play", "die") == "gameover"
assert next_state("play", "start") == "play", "неизвестное действие состояние не меняет"
assert menu_move(0, -1, 2) == 1
assert menu_move(1, 1, 2) == 0
assert level_for_score(0) == 1
assert level_for_score(199) == 1
assert level_for_score(200) == 2
assert level_for_score(99999) == 10
assert meteor_speed(1) == 180
assert meteor_speed(3) == 240
assert spawn_interval(1) == 0.9
assert spawn_interval(10) == 0.27
assert handle_key("menu", "return") == "start"
assert handle_key("play", "escape") == "pause"
assert handle_key("gameover", "m") == "mute"
assert handle_key("play", "x") is None
# Рекорд
assert parse_record('{"record": 1234}') == 1234
assert parse_record("") == 0
assert parse_record('{"record": -5}') == 0
assert parse_record('{"record": true}') == 0
assert parse_record("[1, 2]") == 0
assert best_score(100, 50) == 100
assert best_score(100, 150) == 150
print("Все проверки пройдены")
Разбор по строкам
from logic import (...)— явный список всего, что проверяем. Если что-то забыли, Python сразу сообщит об этом ошибкой импорта.assert step_ship(190, 100, 1, 0, 100, 0.5, (0, 0, 200, 200)) == (200, 100.0)— корабль у правого края хочет уехать за границу, но функция зажимает его на 200. Сообщение рядом объяснит, что проверялось.assert round(direction_from_keys(0, 1, 0, 1)[0], 3) == 0.707— числа с плавающей точкой мы не сравниваем напрямую, а округляем: так же, как в задачах курса.assert not rects_overlap(..., ...)— утверждать можно и отрицание:notпревращает верное «нет пересечения» в верное условие.assert frame_index(0.5, 0.06, 6, False) is None— результатNoneпроверяют черезis None, а не через== None.print("Все проверки пройдены")— если ни одинassertне упал, мы доходим до этой строки.
Шаг 7. Запуск и проверка
Сначала проверьте логику. Игру запускать для этого не нужно:
python check_logic.py
Все проверки пройдены
Теперь проведём короткий эксперимент и убедимся, что проверки действительно ловят поломку. Откройте settings.py, временно поменяйте BASE_SPEED = 180 на BASE_SPEED = 200 и запустите проверки ещё раз:
Traceback (most recent call last):
File ".../kometa/check_logic.py", line 84, in <module>
assert meteor_speed(1) == 180
^^^^^^^^^^^^^^^^^^^^
AssertionError
Проверка нашла изменение за долю секунды и показала нужную строку. Верните BASE_SPEED = 180. Если вы поменяли число специально, поправьте и ожидаемое значение в check_logic.py: проверки — это договорённость о том, как должна работать игра.
Теперь запустите саму игру, она должна вести себя как раньше:
python main.py
И последнее: откройте settings.py, поставьте START_LIVES = 1 и запустите игру ещё раз. Первый же метеор теперь заканчивает партию. Одна цифра в одном файле, и сложность изменилась. Верните тройку.
Проверьте себя
python check_logic.pyпечатает «Все проверки пройдены», а после временной правкиBASE_SPEEDпадает сAssertionErrorна нужной строке.- В
main.pyне осталось строк видаWIDTH = 960: поиск по файлу не находит ни одной константы изsettings.py. - Игра запускается и играется как раньше: меню, пауза, рекорд, звук работают.
- Размер окна можно поменять в одном месте: попробуйте поставить
WIDTH = 1100, и корабль, метеоры и звёзды подстроятся. Верните960. - В
settings.pyнет ни одной строкиimport.
Частые ошибки
ImportError: cannot import name ... from partially initialized module
Что видно: игра не запускается, в сообщении слова partially initialized module (наполовину загруженный модуль) и имя вашего файла. Почему: получился круг. main.py импортирует settings.py, а в settings.py кто-то написал from main import .... Python начинает загружать main.py, переходит в settings.py, а оттуда просится обратно в ещё не загруженный main.py. Как исправить: убрать лишний импорт. Файл настроек не импортирует из игры вообще ничего, а нужное значение записывается в нём заново или вычисляется из других констант.
from settings import * вместо явного списка
Что видно: игра работает, но непонятно, откуда взялось имя WIDTH. Почему плохо: звёздочка тащит в файл вообще всё, что есть в settings.py. Читателю (и вам через месяц) приходится гадать, где объявлено имя, редактор не может подсказать опечатку, а если в двух модулях окажутся одинаковые имена, один молча затрёт другой. Как исправить: перечислять нужные имена явно, как мы сделали в main.py: тогда любое имя видно в начале файла.
assert со скобками вокруг условия и сообщения
Что видно: assert (clamp(5, 0, 3) == 4, "ошибка") никогда не падает, а Python печатает предупреждение SyntaxWarning: assertion is always true, perhaps remove parentheses?. Почему: скобки с запятой внутри создают кортеж из двух элементов, а непустой кортеж всегда считается истинным. Как исправить: убрать скобки — assert clamp(5, 0, 3) == 4, "ошибка".
Поменяли число в settings.py, а игра не изменилась
Что видно: вы правите START_LIVES, а жизней по-прежнему три. Почему: в main.py осталась старая строка START_LIVES = 3. Она стоит ниже импорта и затирает значение из настроек. Как исправить: найти поиском по файлу и удалить лишнее определение. Правило одного места работает, только когда место действительно одно.
ModuleNotFoundError: No module named 'settings'
Что видно: ошибка на первой строке импорта. Почему: файл назван иначе (Settings.py, settings.txt) или лежит не рядом с main.py, либо вы запускаете команду из другой папки. Как исправить: проверить имя (только строчные буквы) и расположение, перейти в папку kometa/ и запустить игру оттуда.
Что важно запомнить
- Длинный файл и числа без имени (магические числа) — признаки беспорядка; чинятся выносом в модули и константы.
settings.pyхранит только константы с ЗАГЛАВНЫМИ именами; каждое число записано ровно один раз.- Подключайте настройки явным списком:
from settings import WIDTH, HEIGHT. settings.pyничего не импортирует, аlogic.pyберёт толькоsettings.py, поэтому циклов в импортах нет.assert условие, "сообщение"молчит, если условие верно, и падает сAssertionError, если нет.- Проверки логики запускаются без окна и pygame — это награда за то, что логика живёт отдельно от отрисовки.
Игра устроена как следует, а проверки на страже логики. Остался последний шаг: у ваших друзей Python и pygame, скорее всего, не стоят. В следующем уроке соберём «Комету» в один файл, который запускается двойным щелчком.