$ sudo teach IT
Модуль 7 · Финал · Урок 7.2

Порядок в коде

Вы вынесете все настройки «Кометы» в один файл и научитесь проверять логику игры простыми утверждениями assert.

Теория + практика~40 минутСредний

Игра выросла: в ней есть меню, пауза, звук и рекорд в файле. Всё работает, но main.py разбух, а числа вроде 960 и 360 разбросаны по нескольким файлам. В этом уроке мы наведём порядок: соберём все настройки в одном месте и научимся проверять логику игры одной командой, не открывая окно.

Как выглядит беспорядок

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

Первый признак — длинный файл. Когда в main.py набралось двести пятьдесят строк и в нём живут цикл, три экрана, загрузка рекорда и десяток констант, нужную строку приходится искать глазами. Второй признак — числа без имени. Программисты называют их магическими числами: читаешь код и не понимаешь, откуда взялась цифра и что случится, если её поменять.

Python · фрагмент
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.

Python · фрагмент
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 и показывает строку, где утверждение не сбылось.

Python · фрагмент
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.

Python · kometa/settings.py
"""Все настройки игры «Комета». Здесь только константы, импортов нет."""

# Окно
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 · kometa/logic.py (начало файла)
# Логика игры: только чистый 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 мы научим её находить папку внутри сборки):

Python · kometa/sprites.py (замените блок после import pygame)
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__, замените строку со скоростью вращения (остальное в методе не трогайте):

Python · kometa/sprites.py (в 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

Здесь три правки. Первая: замените весь блок импортов в начале файла на этот.

Python · kometa/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:

Python · kometa/make_assets.py (импорты)
from logic import ship_points
from settings import EXPLOSION_COUNT

Теперь число кадров взрыва записано один раз, и скрипт, и игра берут его из одного места.

Шаг 6. check_logic.py

Создайте файл check_logic.py рядом с остальными. В нём проверки для каждой функции и каждого класса из logic.py: обычный случай, граница и странный ввод. Старайтесь ставить проверки по группам и подписывать их комментариями, чтобы файл читался как оглавление логики.

Python · kometa/check_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, скорее всего, не стоят. В следующем уроке соберём «Комету» в один файл, который запускается двойным щелчком.

На следующий урок