Глава 19 · Правила игры

Еда и свободная клетка

Новая голова плюс старое тело строит движение за один проход — и та же идея множества свободных клеток решает, куда честно поставить еду.

Элегантная альтернатива циклу по сегментам

Вместо того чтобы двигать каждый существующий сегмент по отдельности (раздел 19.17), можно построить новый список целиком — новая голова спереди, старое тело позади, без последнего элемента (если змейка не растёт):

2
1
H
snake до
2
1
H
[новая_голова] + snake[:-1]
move_snake.py
def move_snake(snake, new_head, *, grow):
    if grow:
        return [new_head, *snake]
    return [new_head, *snake[:-1]]

Хвост при этом естественным образом «отваливается» — snake[:-1] отбрасывает последний элемент. Никакого цикла, никакого риска перепутать порядок обновления: результат строится за один проход, а не мутирует существующие позиции одну за другой.

Еда — случайная свободная клетка

В разделе 19.3 клетка для яблока выбиралась без проверки — при достаточно длинной змейке яблоко иногда оказывалось бы прямо внутри тела. Честная версия выбирает только среди по-настоящему свободных клеток:

choose_food.py
def choose_food(snake, rng, *, half=280, step=20):
    occupied = set(snake)
    free = tuple(
        cell for cell in all_cells(half=half, step=step) if cell not in occupied
    )
    if not free:
        return None
    return rng.choice(free)
rng как параметр — та же идея пригодится в разделе 19.30
choose_food() принимает готовый random.Random, а не читает глобальный random модуль напрямую. В реальной игре передают random.Random() — настоящую случайность; в тестах — random.Random(seed) с фиксированным зерном, чтобы результат был предсказуем и его можно было проверить assert-ом.

Инвариант: еда — только из свободных клеток

Контракт choose_food() можно записать формулой: food ∈ FREE_CELLS, где FREE_CELLS = ALL_CELLS − SNAKE_CELLS. Это должно оставаться верным ВСЕГДА, включая крайний случай, до которого редко доходят на практике, но который обязан быть предусмотрен: змейка выросла настолько, что заняла буквально каждую клетку поля. Тогда FREE_CELLS пусто, и честного ответа, где поставить еду, не существует.

ALL_CELLS
все клетки поля
вычитание
минус SNAKE_CELLS
клетки, занятые змейкой
=
FREE_CELLS
food ∈ FREE_CELLS
Если FREE_CELLS пусто — свободной клетки для еды физически не существует.
Нельзя тайком подставить клетку змейки вместо None
Соблазнительное, но неверное решение — вернуть, например, snake[0], когда свободных клеток не осталось: тогда food == snake[0], и инвариант «еда никогда не внутри змейки» нарушается прямо в самом же коде, который эту еду выбирает. Честный ответ — None: свободной клетки для еды в самом деле нет, и вызывающий код (раздел 19.32) обязан явно это обработать, а не притворяться, что еда по-прежнему существует.

В SnakeApp.game_tick() это выглядит так: если змейка съела еду и choose_food() вернула None, игра переходит в терминальное состояние GameStatus.WON — поле заполнено целиком, дальше двигаться некуда, и это победа, а не ошибка. state.food в этом состоянии тоже становится None, и отрисовка (раздел 19.29) обязана явно проверить это перед тем, как рисовать еду на экране.

Реальное окно: игровое поле целиком заполнено телом змейки, сверху табло со счётом, по центру надпись ПОБЕДА и итоговый счёт
Реальное окно после того, как змейка честно заняла каждую легальную клетку поля — GameStatus.WON, а не еда внутри тела.
Практика: еда только на свободной клетке
Автоматическая проверка — функции move_snake() и choose_food(): еда никогда не внутри змейки
Открыть практику →