Создание интерактивных игр в терминале на Python

Курс научит вас создавать увлекательные текстовые игры и интерактивные CLI-приложения на Python. Вы освоите работу с терминалом, создание игрового цикла и использование современных библиотек для стилизации вывода.

1. Основы терминальных приложений и модуль cmd

Основы терминальных приложений и модуль cmd

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

Терминальные приложения работают по принципу непрерывного диалога между пользователем и системой. Этот процесс называется REPL.

> REPL (Read-Eval-Print Loop) — это интерактивная среда, которая читает ввод пользователя, вычисляет (обрабатывает) его, печатает результат и возвращается в состояние ожидания новой команды.

Текстовые игры и терминальные приложения невероятно эффективны. Терминальные приложения потребляют в среднем на 80 процентов меньше оперативной памяти, чем их графические аналоги. Если базовая игра с графическим интерфейсом требует минимум 50 мегабайт памяти, то текстовый квест обойдется всего в 10 мегабайт, что делает их отличным выбором для прототипирования.

Анатомия интерактивного цикла

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

Этот подход работает для простых скриптов, но при создании полноценной игры с десятками команд код быстро превратится в нечитаемую стену из if и elif. Кроме того, разработчику придется самостоятельно писать логику для обработки аргументов (например, "идти на север"), создания меню помощи и обработки ошибок ввода.

В играх часто возникает необходимость вычислять дистанцию между сущностями. Для этого применяется теорема Пифагора: , где — искомое расстояние, — разница координат по оси X, а — разница координат по оси Y. Если игрок находится в точке (0, 0), а враг в точке (3, 4), то расстояние составит 5 единиц. Обработка таких команд вручную через input() усложняет архитектуру.

Встроенный модуль cmd

Для решения проблемы масштабируемости в стандартной библиотеке Python существует модуль cmd. Он предоставляет готовую объектно-ориентированную структуру для создания строчно-ориентированных интерпретаторов.

Чтобы превратить обычный скрипт в мощное терминальное приложение, нужно выполнить несколько шагов:

  • Импортировать модуль cmd.
  • Создать собственный класс, который наследуется от базового класса cmd.Cmd.
  • Определить методы, названия которых начинаются с префикса do_.
  • Вызвать метод cmdloop() для запуска приложения.
  • Любой метод, начинающийся с do_, автоматически становится доступной командой в вашем терминале. Если вы назовете метод do_attack, пользователь сможет ввести слово attack, и программа выполнит соответствующий код.

    | Характеристика | Цикл while + input() | Модуль cmd.Cmd | |---|---|---| | Масштабируемость | Низкая (много if/elif) | Высокая (каждая команда — отдельный метод) | | Справка (Help) | Нужно писать вручную | Генерируется автоматически | | Автодополнение | Отсутствует | Поддерживается (клавиша Tab) | | Обработка аргументов | Ручной парсинг строки | Аргументы передаются в метод автоматически |

    Создание игрового лобби

    Рассмотрим, как модуль cmd применяется на практике при создании текстовой ролевой игры. Нам нужно создать лобби, где игрок может осмотреться, проверить инвентарь или выйти из игры.

    В классе Cmd есть два важных атрибута, которые позволяют кастомизировать внешний вид терминала: * intro — текст, который выводится один раз при запуске программы (приветствие). * prompt — строка приглашения ко вводу, которая отображается перед курсором.

    Обратите внимание на метод do_quit. Он возвращает логическое значение True. В архитектуре модуля cmd возвращение истины из метода команды сигнализирует циклу cmdloop() о том, что работу интерпретатора необходимо завершить. Если метод не возвращает ничего (или возвращает False), цикл продолжает запрашивать новые команды.

    В играх важна математика характеристик. Урон = Базовая атака * Множитель оружия. При базовой атаке 15 и множителе стального меча 1.5, итоговый урон составит 22.5 единиц. Всю эту логику можно инкапсулировать внутри конкретного метода do_attack, не засоряя глобальную область видимости.

    Обработка аргументов и встроенная справка

    Каждый метод do_ принимает параметр arg. Это строка, содержащая всё, что пользователь ввел после названия команды.

    > Если пользователь вводит команду take sword, система вызывает метод do_take(self, arg), где переменная arg будет содержать строку "sword".

    Это позволяет создавать гибкие команды. Разработчику остается лишь разбить строку arg на части (например, с помощью метода split()), если команда требует нескольких параметров.

    Еще одна мощная функция модуля — автоматическая генерация документации. Если в запущенном приложении ввести команду help, система выведет список всех доступных команд (всех методов с префиксом do_). Если ввести help look, терминал напечатает строку документации (docstring), которую мы указали внутри метода do_look. Это избавляет от необходимости писать отдельные громоздкие функции для обучения игрока управлению.

    Использование объектно-ориентированного подхода позволяет легко сохранять состояние игры. Атрибуты класса (например, self.hp или self.inventory) будут доступны в любом методе команды, обеспечивая связность данных на протяжении всей игровой сессии. Если переменная здоровья , где — текущее здоровье игрока, можно принудительно вызвать завершение цикла, имитируя конец игры.

    2. Игровой цикл и обработка пользовательского ввода

    Игровой цикл и обработка пользовательского ввода

    В предыдущих материалах мы изучили модуль cmd, который идеально подходит для создания пошаговых текстовых квестов. Программа ждет, пока пользователь введет команду и нажмет Enter. Но что делать, если вы хотите создать динамичную игру, где враги двигаются, таймеры тикают, а мир живет своей жизнью независимо от того, нажимает ли игрок на кнопки? Для этого пошаговая архитектура не годится. Нам нужен механизм, который непрерывно двигает игру вперед.

    > Игровой цикл (Game Loop) — это центральный шаблон проектирования любой динамической программы, представляющий собой бесконечный цикл. Он непрерывно обрабатывает пользовательский ввод, обновляет состояние виртуального мира и отрисовывает результат на экране.

    Этот паттерн является сердцем любого движка реального времени. Понимание того, как построить его в терминальной среде, дает фундаментальную базу, необходимую для плавного перехода к графическим библиотекам.

    Три кита игрового цикла

    Каждая итерация (или кадр) игрового цикла состоит из трех строго последовательных этапов. Если нарушить этот порядок, игра начнет вести себя непредсказуемо, а управление станет отзывчивым через раз.

  • Обработка ввода (Process Input): Чтение нажатий клавиш, движений мыши или команд с геймпада.
  • Обновление состояния (Update): Изменение координат объектов, расчет столкновений, уменьшение здоровья, работа искусственного интеллекта.
  • Отрисовка (Render): Вывод обновленной картинки или перерисовка текста в консоли.
  • В стандартном терминале функция input() блокирует выполнение программы. Игра замирает до нажатия клавиши ввода. Чтобы создать терминальную игру в реальном времени, разработчики используют методы неблокирующего ввода (например, чтение буфера клавиатуры напрямую), но логическая структура остается идентичной графическим движкам.

    Независимость от частоты кадров

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

    Чтобы решить эту проблему, разработчики вводят концепцию Delta Time (dt) — это количество времени, прошедшее с момента отрисовки предыдущего кадра.

    Для расчета новой позиции объекта независимо от частоты кадров используется формула равномерного движения: , где — пройденное расстояние, — скорость объекта, а — затраченное время (в нашем случае это Delta Time).

    Представим, что космический корабль имеет скорость 50 пикселей в секунду. На быстром компьютере один кадр занял 0.01 секунды. Корабль сдвинется на 50 × 0.01 = 0.5 пикселя. На медленном компьютере кадр занял 0.1 секунды. Корабль сдвинется на 50 × 0.1 = 5 пикселей. В обоих случаях ровно через одну секунду реального времени корабль преодолеет ровно 50 пикселей, обеспечивая одинаковый игровой опыт на любом железе.

    Коллизии и границы мира

    На этапе обновления состояния (Update) происходит не только перемещение, но и проверка правил игры. Самое частое правило — ограничение перемещения. Персонаж не должен выходить за пределы терминала или экрана.

    Математически это выражается через проверку координат после их изменения: , где — новая координата по оси X, — старая координата, — скорость, а — дельта времени.

    Если ширина игрового поля составляет 80 символов, а после расчетов становится равным 82, программа должна принудительно установить координату X равной 80. Это простейший пример обработки коллизий (столкновений) с границей экрана, который не позволяет сущности покинуть разрешенную зону.

    От терминала к архитектуре Pygame

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

    | Этап цикла | Терминальный подход | Подход Pygame | |---|---|---| | Инициализация | Настройка консоли, скрытие курсора | pygame.init(), создание окна | | Сбор событий | Чтение буфера клавиатуры (модуль keyboard) | pygame.event.get() | | Отрисовка | Очистка консоли (cls / clear), печать строк | screen.fill(), screen.blit(), pygame.display.update() | | Контроль времени | time.sleep() | pygame.time.Clock().tick(FPS) |

    В Pygame система ввода построена вокруг очереди событий. Когда пользователь нажимает клавишу, операционная система посылает сигнал, который библиотека сохраняет в специальный список. На этапе "Обработка ввода" ваш код проходится по этому списку.

    Обратите внимание на блок for event in pygame.event.get():. Это цикл событий (Event Loop), вложенный внутрь основного игрового цикла. Он гарантирует, что если пользователь нажмет несколько клавиш между кадрами (например, прыжок и стрельбу одновременно), ни одно действие не будет потеряно.

    Если игра работает со скоростью 60 кадров в секунду, один кадр занимает примерно 16.6 миллисекунд. Если игрок нажмет пробел на 5-й миллисекунде и Ctrl на 12-й миллисекунде, оба эти события попадут в очередь и будут обработаны в самом начале следующей итерации цикла.

    Освоив непрерывный цикл и обновление состояний в текстовой среде, вы фактически изучаете ядро профессиональной разработки игр. Единственное различие между терминальной игрой и графическим блокбастером заключается в сложности математики на этапе обновления и технологиях рендеринга на этапе отрисовки.