Используем функции и глобальные переменные для сложных квестов
Введение
В прошлом уроке мы сделали квест, вот только при его проектировании я наложил серьезное ограничение - по заданию квест выглядел в виде дерева. Т.е. можно было сделать так:
Но нельзя было делать так:
Почему нельзя? Ветки в условном операторе всегда шли строго вниз и никогда не пересекались. Поэтому, если бы нам нужно было попасть в точку, где мы уже были, алгоритм для нее пришлось бы перерисовывать заново в другом месте. А в некоторых ситуациях алгоритм и вовсе становился бы бесконечным...
Решить эту проблему нам помогут функции. Давай же разберемся, как их использовать!
Что такое функция?
Функция - это именованный участок кода на языке программирования, который можно вызывать по имени.
Функции нужны, если часть кода нашей программы повторяется, причем повторяется в разных местах кода.
Программисты - это очень ленивые люди, поэтому каждый раз, когда один и тот же участок кода используется дважды, они стараются использовать функции. Один раз написал - много раз вызываешь.
А еще использование функций делает код понятнее: например, вместо перечисления действий для приготовления яичницы, можно сделать функцию приготовить_яичницу
, а затем вызвать ее каждый раз, как клиент в виртуальном ресторане заказал яичницу. Когда ваш коллега откроет ваш код, он увидит вызов функции приготовить_яичницу
и сразу поймет, что делает ваша программа.
Как определить (описать) функцию и вызвать функцию? Давайте рассмотрим пример:
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
При работе с переменными следует помнить об области видимости переменных:
- Локальные переменные видны только внутри функции, в которой они были созданы;
- Глобальные переменные видны внутри (и вне) всех функций;
- Нельзя менять глобальную переменную внутри функции.
- Если все-таки нужно поменять глобальную переменную внутри функции, можно использовать ключевое слово
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 баллов - отсутствие ошибок времени выполнения (программа не завершается внезапным красным текстом);