Используем функции и глобальные переменные для сложных квестов

Введение

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

Но нельзя было делать так:

Почему нельзя? Ветки в условном операторе всегда шли строго вниз и никогда не пересекались. Поэтому, если бы нам нужно было попасть в точку, где мы уже были, алгоритм для нее пришлось бы перерисовывать заново в другом месте. А в некоторых ситуациях алгоритм и вовсе становился бы бесконечным...

Решить эту проблему нам помогут функции. Давай же разберемся, как их использовать!

Что такое функция?

Функция - это именованный участок кода на языке программирования, который можно вызывать по имени.

Функции нужны, если часть кода нашей программы повторяется, причем повторяется в разных местах кода.

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

А еще использование функций делает код понятнее: например, вместо перечисления действий для приготовления яичницы, можно сделать функцию приготовить_яичницу, а затем вызвать ее каждый раз, как клиент в виртуальном ресторане заказал яичницу. Когда ваш коллега откроет ваш код, он увидит вызов функции приготовить_яичницу и сразу поймет, что делает ваша программа.

Как определить (описать) функцию и вызвать функцию? Давайте рассмотрим пример:

def say_hello():
    print("Привет!")
    print("Какой чудесный день!")

say_hello()
say_hello()

В коде выше мы сначала описали функцию say_hello, а затем два раза ее вызвали. Для создания функции нужно написать def имя_функции():, а затем, как и в условном операторе с отступом перечислить все операции, которые должна выполнить функция. На блок-схеме это выглядело бы так:

На экране появится:

Привет!
Какой чудесный день!
Привет!
Какой чудесный день!

Код внутри функции называется телом функции, а название - именем функции.

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

Кстати, в питоне уже много готовых функций, которые написали за нас его авторы (например, input и print, round, функции из модулей math, time, random), в дальнейшем мы еще не раз с ними встретимся.

Как использовать функции в квесте?

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

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

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

Идея: для каждой точки сделаем по функции (их можно назвать, например, go_to_start, go_to_room_one, go_to_room_two), и в момент перехода просто будем вызывать функцию нужной нам комнаты.

Кстати, если нам надо завершить квест, в питоне есть встроенная функция, которая завершает алгоритм. Она называется exit().

# сначала опишем по функции для каждой локации
# в этот момент мы просто объясняем питону, что должны делать функции,
# сами функции пока вызываются!
def go_to_start():
    print("Перед вами две двери, какую из них вы хотите проверить?")
    print("1 - Войти в первую дверь.")
    print("2 - Войти во вторую дверь.")

    action = input()

    # если игрок ввел 1 - идем в первую комнату
    if action == "1":
        go_to_room_one()
    # если игрок ввел 2 - во вторую
    if action == "2":
        go_to_room_two()
    # если игрок ввел что-то совсем другое - спрашиваем снова
    if action != "1" and action != "2":
        print("Я вас не понял, попробуйте еще раз.")
        go_to_start()

def go_to_room_one():
    print("Походу, это тупик. Ничего не остается, кроме как...")
    print("1 - Вернутся обратно.")

    action = input()

    # если игрок ввел 1 - возвращаемся на стартовую точку
    if action == "1":
        go_to_start()
    # если игрок ввел что-то совсем другое - спрашиваем снова
    if action != "1":
        print("Я вас не понял, попробуйте еще раз.")
        go_to_room_one()

def go_to_room_two():
    print("Вы нашли выход, поздравляем!")
    # завершаем работу программу
    exit()

# отлично, функции описаны
# сама программа начинается с этого места!
# просто отправляем игрока на старт и запускаем процесс перехода между локациями
go_to_start()

Попробуй запустить это пример и изучить, как он работает. В процессе функции иногда будут вызывать сами себя напрямую или через другие функции (из А в Б, а из Б снова в А), такое явление называется рекурсией. Рекурсия создает возможность сколько угодно раз перемещаться в любую локаю из любой точки квеста.

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

Пример:

def location_A():
    print("Я в локации А, пойду в Б")

    # переход в Б
    location_B()

    print("Я вернулся из Б в А")

def location_B():
    print("Я в локации Б, пойду в Ц")

    # переход в Ц
    location_C()

def location_C():
    print("Я в локации Ц, пожалуй тут и останусь...")
    print("Но погодите, меня куда-то тянет...")

# начинаем с перехода в А
location_A()

Результат:

Я в локации А, пойду в Б
Я в локации Б, пойду в Ц
Я в локации Ц, пожалуй тут и останусь...
Но погодите, меня куда-то тянет...
Я вернулся из Б в А

Из А мы переходим в Б, а из Б в Ц где, кажется, игра и завершается. Но на самом деле после завершения функции location_C мы вернемся в функцию location_B, а из нее в вызвавшую ее location_A, где и срабатывает последняя строчка print("Я вернулся из Б в А"). Помнишь дисклеймер в начале? Вот это как раз тот случай 🤔.

Но в другой ситуации такое поведение можно наоборот использовать во благо - например, выделить сражение с боссом в отдельную функцию, а затем продолжить повествование с того же места.

def boss_fight():
    print("Я страшный босс! Не пропущу, пока не отгадаешь загадку!")

    print("Висит груша, нельзя скушать. Что это?")

    answer = input()

    if answer != "лампочка":
        print("Не угадал!")
        # повторяем сражение...
        boss_fight()
    else:
        print("Правильно, иди дальше...")

def location_A():
    print("Я в локации А, но кажется тут босс")

    # запуск сражения с боссом
    boss_fight()

    print("Продолжаю путь, пойду в Б...")

    # переход в Б
    location_B()

def location_B():
    print("Я в локации Б, кажется, тут спокойно.")
    print("Тут и останусь!")

# начинаем с перехода в А
location_A()

Результат:

Я в локации А, но кажется тут босс
Я страшный босс! Не пропущу, пока не отгадаешь загадку!
Висит груша, нельзя скушать. Что это?
слон
Не угадал!
Я страшный босс! Не пропущу, пока не отгадаешь загадку!
Висит груша, нельзя скушать. Что это?
лампочка
Правильно, иди дальше...
Продолжаю путь, пойду в Б...
Я в локации Б, кажется, тут спокойно.
Тут и останусь!

Локальные и глобальные переменные

Раз мы заговорили про функции, то нужно еще раз вспомнить про переменные. Как вы помните, переменная - это именованная область оперативной памяти, в которой можно сохранить какие-либо данные. Переменную можно создать как внутри, так и вне функции.

Если переменная создана вне функции, она называется глобальной, а если внутри - локальной.

# это глобальная переменная
a = 5

def some_function():
    # это локальная переменная
    b = 5

При работе с переменными следует помнить об области видимости переменных:

  1. Локальные переменные видны только внутри функции, в которой они были созданы;
  2. Глобальные переменные видны внутри (и вне) всех функций;
  3. Нельзя менять глобальную переменную внутри функции.
  4. Если все-таки нужно поменять глобальную переменную внутри функции, можно использовать ключевое слово global, но делать так в больших проектах не стоит. Чуть позже мы обсудим почему, а пока позволим себе немного "шалости".

Давайте рассмотрим по примеру на каждое правило.

1. Локальные переменные видны только внутри функции, в которой они были созданы.

def some_function():
    a = 5

some_function()
# это ошибка! Переменная a не существует вне some_function!
print(a)

Вне функции питон просто не знает о существовании переменной a.

2. Глобальные функции видны внутри (и вне) всех функций.

a = 5

def some_function():
    # так можно, питон выведет 5
    print(a)

3. Нельзя менять глобальную переменную внутри функции.

a = 5

def some_function():
    # Ошибка! Нельзя менять глобальную переменную внутри функции!
    a = a + 10

some_function()
print(a)

4. Используем ключевое слово global.

a = 5

def some_function():
    global a
    a = a + 10

some_function()
# выведет 15
print(a)

Добавляем инвентарь

Зачем это было нужно? С рекурсией мы можем делать квесты любой структуры, но все равно чего-то не хватает. В реальных текстовых квестах герои часто могут принимать какие то решения по пути, и эти решения влияют на алгоритм поведения одних и тех же локаций. Т.е. в одной и той же точке результат может зависеть от предыдущих решений.

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

Как запомнить, взяли ли мы ключ? Для этого можно использовать глобальную переменную, которая будет видна во всех функциях. Например, назовем ее has_key (англ. есть_ключ?) и будем хранить в ней значение True (правда, ключ есть) или False (ложь, флага нет). Переменная в которой лежит True или False называется булевой переменной или "флаг" (который поднят или опущен).

# в начале игры ключа у нас нет
has_key = False

def go_to_start():
    print("Перед вами две двери, какую из них вы хотите проверить?")
    print("1 - Войти в первую дверь.")
    print("2 - Войти во вторую дверь.")

    action = input()

    # если игрок ввел 1 - идем в первую комнату
    if action == "1":
        go_to_room_one()
    # если игрок ввел 2 - во вторую
    if action == "2":
        go_to_room_two()
    # если игрок ввел что-то совсем другое - спрашиваем снова
    if action != "1" and action != "2":
        print("Я вас не понял, попробуйте еще раз.")
        go_to_start()

def go_to_room_one():
    # мы собрались менять has_key, если игрок возьмет ключ, а значит - используем global
    global has_key

    print("Походу, это тупик. Но на полу лежит небольшой ржавый дверной ключ. В любом случае, стоит вернуться обратно.")
    print("1 - Вернутся обратно.")
    print("2 - Взять ключ, а затем вернуться.")

    action = input()

    # если игрок ввел 1 - возвращаемся на стартовую точку
    if action == "1":
        go_to_start()
    # если игрок ввел 2 - меняем has_key
    if action == "2":
        has_key = True
        print("Вы взяли ключ. Идея хорошая, возможно, он пригодится.")
        go_to_start()
    # если игрок ввел что-то совсем другое - спрашиваем снова
    if action != "1":
        print("Я вас не понял, попробуйте еще раз.")
        go_to_room_one()

def go_to_room_two():
    # если ранее мы подобрали ключ
    if has_key == True:
        print("Вы видите перед собой дверь, но она заперта. Вы пробуете подобранный ключ, и, кажется, дверь открывается.")
        print("Поздравляем! Это выход!")
        exit()
    # иначе, если ключа нет - возвращаемся.
    else:
        print("Перед вами дверь, но она заперта. Нужно попробовать поискать ключ.")
        print("1 - Вернутся обратно.")

        action = input()

        # если игрок ввел 1 - возвращаемся на стартовую точку
        if action == "1":
            go_to_start()
        if action != "1":
            print("Я вас не понял, попробуйте еще раз.")
            go_to_room_two()

# в начале - отправляем игрока на старт
go_to_start()

Ниже приведен пример прохождения нашего квеста.

Перед вами две двери, какую из них вы хотите проверить?
1 - Войти в первую дверь.
2 - Войти во вторую дверь.
1
Походу, это тупик. Но на полу лежит небольшой ржавый дверной ключ. В любом случае, стоит вернуться обратно.
1 - Вернутся обратно.
2 - Взять ключ, а затем вернуться.
1
Перед вами две двери, какую из них вы хотите проверить?
1 - Войти в первую дверь.
2 - Войти во вторую дверь.
2
Перед вами дверь, но она заперта. Нужно попробовать поискать ключ.
1 - Вернутся обратно.
1
Перед вами две двери, какую из них вы хотите проверить?
1 - Войти в первую дверь.
2 - Войти во вторую дверь.
1
Походу, это тупик. Но на полу лежит небольшой ржавый дверной ключ. В любом случае, стоит вернуться обратно.
1 - Вернутся обратно.
2 - Взять ключ, а затем вернуться.
2
Вы взяли ключ. Идея хорошая, возможно, он пригодится.
Перед вами две двери, какую из них вы хотите проверить?
1 - Войти в первую дверь.
2 - Войти во вторую дверь.
2
Вы видите перед собой дверь, но она заперта. Вы пробуете подобранный ключ, и, кажется, дверь открывается.
Поздравляем! Это выход!

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

Применяем знания на практике

Пришла пора сделать что-то реально интересное! Усовершенствуй свой квест из прошлого урока (альфа-версию), теперь ты практически не ограничен в возможностях.

Используй хотя бы 5 игровых локаций (5 точек на графе) и хотя бы один возврат назад или переход в соседнюю ветку (когда в локацию на графе входят две стрелочки) плюс добавь несколько предметов инвентаря.

Каждую локацию оформи в виде функции. Удачи! 🤞

Критерии для самопроверки:

  • 5 баллов - программа запускается без ошибок, содержит не менее 5 функций;
  • 5 баллов - нелинейность сюжета (наличие обратных переходов, случайных событий);
  • 5 баллов - изменяемость карты / инвентарь (возможность подбирать предметы, используется несколько глобальных переменных);
  • 5 баллов - отсутствие ошибок времени выполнения (программа не завершается внезапным красным текстом);