Изучение объектно-ориентированного программирования должно быть увлекательным! Так почему же так много учебных пособий посвящены созданию классов CarBankAccount или Customer?

Во время учебы мне просто хотелось создавать игры. Теперь, став преподавателем, я по-прежнему считаю игры наиболее увлекательным контекстом для обучения. Есть ли еще студенты, которые хотят научиться создавать класс BankAccount? Нет, их нет! Ни одного. Вообще.

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

Игра

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

Классы

В ООП классы действуют как макеты для объектов, которых обычно требуется более одного. В этой игре создадим класс Room (Комната), потому что в страшном замке будет довольно много комнат.

В объектно-ориентированном программировании класс обычно состоит из двух основных частей:

  1. Атрибуты (верхняя половина приведенной выше таблицы): информационные элементы, которые будет хранить каждый объект. Для замка с привидениями это:
  • name и description (идентификация комнаты и ее описание);
  • connections — список целых чисел, указывающих на другие комнаты, связанные с этой;
  • contains_key — простой флаг, отмечающий, находится ли в комнате один из ключей.
  1. Методы (нижняя половина таблицы): действия, которые может выполнять объект, или действия, которые могут быть выполнены с ним. Хорошей практикой является наименование методов с помощью глаголов или глагольных фраз — например, remove_key.

Как вы заметите, несколько методов начинаются с get. Они называются геттерами. Геттер — это небольшой метод, который позволяет безопасно получить доступ к значению атрибута (например, get_name()), а не обращаться напрямую к объекту. Это делает код более надежным и простым в обслуживании.

Идея ограничения и контроля доступа к атрибутам класса с помощью методов лежит в основе инкапсуляции.

Теперь посмотрим, как класс выглядит в коде Python:

class Room:
    # Конструктор для создания объектов
    def __init__(self, name, description, connections, contains_key=False):
        self.name = name
        self.description = description
        self.connections = connections   # список целых чисел, указывающих на другие комнаты
        self.contains_key = contains_key

    # Геттеры
    def get_name(self):
        return self.name

    def get_description(self):
        return self.description

    def get_connections(self):
        return self.connections

    def has_key(self):
        return self.contains_key

    # Методы для изменения состояния
    def remove_key(self):
        if self.contains_key:
            self.contains_key = False
            return True
        return False

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

Конструктор, __init__, — специальный метод, позволяющий создавать объекты на основе класса. При создании Room, мы передадим значения для параметров этого класса: namedescriptionconnections и contains_key. Параметр contains_key является необязательным, и если значение не предоставлено, по умолчанию принимается значение False.

Объекты

Определив класс Room, можно использовать его, чтобы «оживить» замок с привидениями в виде объектов.

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

Карта замка с привидениями

Теперь, когда план готов, можно перейти к созданию самих объектов-комнат.

# --- Создаем дом с привидениями ---
# Ключи размещены в библиотеке и на кухне
house = [
    Room("Прихожая",
         "длинная, со зловещими портретами на стенах",
         [-1, 1, 3, -1]),
    Room("Кабинет",
         "освещен лучом света, проникающим через треснувшее окно",
         [-1, 2, -1, 0]),
    Room("Библиотека",
         "завалена книгами, разбросанными по полу",
         [-1, -1, -1, 1],
         True),
    Room("Кухня",
         "покрыта плесенью, в воздухе висит затхлый запах",
         [0, 4, 5, -1],
         True),
    Room("Подвал",
         "темный и похожий на пещеру",
         [-1, -1, 6, 3]),
    Room("Внутренний двор",
         "маленький, в нем выделяются три статуи горгулий",
         [3, 6, -1, -1]),
    Room("Сад",
         "заросший, утопает в спутанных цветах роз и кустах ежевики",
         [4, -1, -1, 5])
]

Важно понимать, как связаны между собой список house и атрибут connections каждого объекта Room.

Возьмем первую комнату в списке, с индексом 0, — Hallway (Прихожая). Ее связи (connections) — [-1, 1, 3, -1]. Эти числа представляют возможные выходы в северном (north), восточном (east), южном (south) и западном (west) направлениях:

  • значение -1 означает, что в этом направлении комнаты нет;
  • положительное число указывает на индекс другой комнаты в списке house.

Таким образом, в данном случае:

  • east указывает на комнату 1 (Study — Кабинет);
  • south указывает на комнату 3 (Kitchen — Кухня);
  • north и west равны -1, то есть двигаться в этих направлениях нельзя.

Эта простая система чисел предоставляет гибкий способ описания всей планировки дома.

Игровой цикл

Теперь напишем игровой цикл, который будет работать следующим образом:

Действия игрового цикла (игрок входит в комнату → отображается идентификация и описание комнаты → список возможных действий компилируется и представляется игроку → игрок выбирает действие  → игрок принимает решение, перейти ли ему в другую комнату → если да, игрок входит в комнату; если нет, происходит завершение действия игрока, компиляция и предоставление списка действий игроку).

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

Вот код игрового цикла:

# --- Переменные состояния игры  ---
current_room = 0
keys_collected = 0
game_won = False

# --- Игровой цикл ---
while not game_won:
    room = house[current_room]

    # Показать описание комнаты
    print(f"\nВы находитесь в {room.get_name()}.")
    print(f"Это {room.get_description()}.")
    print(""Отсюда можно пройти:")

    # Сформировать допустимые команды
    valid_commands = []
    neighbours = room.get_connections()

    if neighbours[0] >= 0: valid_commands.append("Север")
    if neighbours[1] >= 0: valid_commands.append("Восток")
    if neighbours[2] >= 0: valid_commands.append("Юг")
    if neighbours[3] >= 0: valid_commands.append("Запад")
    if room.has_key(): valid_commands.append("Взять ключ")
    if current_room == 6: valid_commands.append("Открыть дверь")

    # Показать варианты
    for cmd in valid_commands:
        print(f"- {cmd}")

    # Получить команду игрока
    command = ""
    while command not in valid_commands:
        command = input("\nЧто хотите делать? >> ").strip().title()

    # Обработка перемещения
    if command == "Север": current_room = neighbours[0]
    elif command == "Восток": current_room = neighbours[1]
    elif command == "Юг": current_room = neighbours[2]
    elif command == "Запад": current_room = neighbours[3]

    # Подобрать ключ
    elif command == "Взять ключ":
        if room.remove_key():
            keys_collected += 1
            print(f"You picked up a key! Total keys: {keys_collected}")

    # Попытка открыть финальную дверь
    elif command == "Открыть дверь":
        if keys_collected >= 2:
            print("\nВы вставляете ключи в дверь и медленно ее открываете...")
            game_won = True
        else:
            print("\nУ вас недостаточно ключей, чтобы открыть дверь.")

print("\nПоздравляем! Вы выбрались из жуткого дома живым!")

В Garden (в Саду) есть дверь, которую игрок должен открыть, чтобы выиграть. В нашем случае это «зашито» прямо в основную программу, но вы можете сделать механизм более гибким, добавив дверь как атрибут класса Room. Можете даже создать несколько дверей в разных комнатах, каждую определенного цвета —»Red» (Красная), «Blue» (Синяя), «Green» («Зеленая»), — для открытия которых требуется соответствующий ключ. Это позволит вам блокировать части игры до открытия определенных дверей — классическая механика, используемая во многих видеоиграх. Вспомните хотя бы Resident Evil («Обитель зла»)!

Добавляем призрака, который будет бродить по дому

Фото Erik Müller на Unsplash

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

Как это работает

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

Вот Python-код для призрака:

import random

# Начальное положение призрака
ghost_room = 2

# После каждого хода игрока
ghost_neighbours = [i for i in house[ghost_room].get_connections() if i >= 0]
if ghost_neighbours:
    ghost_room = random.choice(ghost_neighbours)

# Проверить, поймал ли призрак игрока
if ghost_room == current_room:
    print("\nПоявляется призрак! Вы пойманы!")
    game_won = False

Заключение

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

Как упоминалось ранее, эту структуру легко расширить. Можно добавить новые комнаты, предметы, головоломки или несколько призраков. Можно даже ввести двери и ключи с цветовой кодировкой, области, которые можно открыть, или более сложные действия игрока. Это только основа, и у вас достаточно времени, чтобы превратить ее в нечто гораздо большее и страшное, прежде чем наступит очередной Хэллоуин!


Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Mark Andrews: OOP’s Alive! Build a Spooky Text Adventure in Python

Предыдущая статьяСтоит ли использовать Rust для разработки CRUD-ориентированных бэкенд-систем?
Следующая статьяУмные инструменты фронтенда: отлавливание ошибки до того, как ее заметят пользователи