Уровни и процедурная генерация

Бесконечно рисовать уровни руками невозможно. Процедурная генерация создаёт платформы, врагов и монетки по правилам — и каждый запуск становится новым.
Суть: уровень можно описать данными (сеткой или списком объектов) и строить его кодом. Процедурная генерация с случайностью даёт бесконечные непохожие уровни из простых правил.

Маленькую игру можно собрать, расставив платформы руками. Но как только хочется десять уровней или бесконечный раннер — рисовать каждый вручную нереально. Решение — описывать уровень данными и строить его кодом. Это отделяет «что в уровне» от «как он рисуется», и открывает дорогу к процедурной генерации.

Самый простой способ — текстовая карта. Уровень это список строк, где каждый символ что-то значит: # — стена, . — пусто, C — монетка, E — враг. Код пробегает по сетке и для каждого символа создаёт нужный спрайт в нужной клетке. Менять уровень теперь так же легко, как редактировать текст.

Следующий шаг — генерировать карту автоматически. С модулем random мы по правилам расставляем платформы и врагов: «платформа каждые 3-6 клеток», «на платформе с шансом 30% монетка». Каждый запуск даёт новый уровень. Это и есть процедурная генерация — то, что делает Minecraft и роглайки бесконечно реиграбельными, и снова это чистая логика без графики.

Как работает под капотом

Карта-сетка превращается в объекты: индекс символа даёт координату клетки, символ — тип спрайта. Координата = индекс × размер клетки:

   текстовая карта        ->   объекты в мире

   "..C..."   ряд 0            монетка в клетке (2,0)
   "..###."   ряд 1            3 стены в ряду 1
   "E....."   ряд 2            враг в клетке (0,2)

   x_пикс = столбец * TILE
   y_пикс = ряд     * TILE

Построение уровня из карты на pygame (читаем):

TILE = 40
level = ["..C...", "..###.", "E....."]

for row, line in enumerate(level):
    for col, ch in enumerate(line):
        x, y = col * TILE, row * TILE
        if ch == "#": walls.add(Wall(x, y))
        elif ch == "C": coins.add(Coin(x, y))
        elif ch == "E": enemies.add(Enemy(x, y))

И парсинг карты, и процедурную генерацию можно полностью проверить без графики. Сгенерируем простой уровень случайно и распарсим его в объекты. Попробуй сам:

import random
random.seed(42)   # чтобы результат был воспроизводимым

def generate_level(width):
    row = []
    for col in range(width):
        r = random.random()
        if r < 0.15:   row.append("#")   # стена
        elif r < 0.30: row.append("C")   # монетка
        elif r < 0.38: row.append("E")   # враг
        else:          row.append(".")   # пусто
    return "".join(row)

TILE = 40
line = generate_level(12)
print("карта:", line)
for col, ch in enumerate(line):
    if ch != ".":
        kind = {"#":"стена","C":"монетка","E":"враг"}[ch]
        print(f"  {kind} в позиции x={col*TILE}")

Сиды и честная случайность

Слово «процедурная» пугает, но за ним стоит простая идея: вместо готового уровня мы храним рецепт и зерно (seed) — число, задающее последовательность случайных значений. Один и тот же seed всегда даёт один и тот же уровень. Это даёт сразу два бонуса: при отладке баг воспроизводится, а игроки могут делиться «кодом уровня» (тем самым seed), как делают в Minecraft. Случайность, которой можно управлять, — мощный инструмент, а не хаос.

Но чистый рандом часто рождает несправедливые или скучные уровни: стена прямо на старте, непроходимая яма, монотонная пустота. Поэтому к генерации добавляют правила и проверки: гарантировать стартовую безопасную зону, не ставить две ямы подряд, держать долю стен в разумных пределах, а после генерации прогнать проверку проходимости. Получается «управляемая случайность» — каждый уровень новый, но всегда играбельный. Этот баланс между разнообразием и честностью — главное искусство процедурной генерации, и осваивается оно экспериментами с правилами, которые ты запускал выше.

Частые ошибки

  • Жёстко прописывать координаты каждого объекта — невозможно поддерживать. Описывай уровень данными.
  • Генерировать «непроходимые» уровни — слишком много стен или враг на старте. Добавляй правила-ограничения.
  • Не фиксировать seed при отладке — баг исчезает при следующем запуске, не воспроизвести.

Best practices

  • Описывай уровни данными (карты, списки) отдельно от кода отрисовки.
  • При генерации задавай разумные правила и проверяй проходимость.
  • Фиксируй random.seed при отладке, отпускай в релизе для разнообразия.

Итог: уровень — это данные, превращаемые в объекты кодом. Добавь случайность с правилами — и получишь бесконечные непохожие уровни.

Проверьте себя
1. Как обычно вычисляют пиксельную координату клетки из текстовой карты?
Aслучайно
Bкоордината = индекс_клетки * размер_тайла
Cкоордината = индекс / 2
Dкоордината всегда 0
2. Зачем фиксировать random.seed при отладке генерации уровней?
AЧтобы уровни были красивее
BЧтобы результат был воспроизводимым и баг можно было повторить
CЧтобы ускорить игру
DЧтобы отключить врагов