# И снова Хейди

Даже в простой игре игрок может попробовать сделать то, о чём заранее не подумал автор. Часто разные действия игрока должны позволить один и тот же результат. Поэтому автору стоит попробовать ввести в игру все разумные варианты, которые могут прийти игроку в голову. Зачастую описания предметов или локаций прямо таки подсказывают, какие здесь есть объекты, и что можно потенциально сделать. Это тоже обязательно нужно учесть. Сделать игру довольно просто, но основное время уйдёт на продумывание различных второстепенных вариантов.

В этой главе мы рассмотрим некоторые из таких вариантов.

## Послушать птенчика

Рассмотрим пример прямо из игры:

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

    Здесь имеется птенчик.

    > ОСМОТРЕТЬ ПТЕНЧИКА
    Слишком мал, чтобы летать, птенец беспомощно попискивает.

    > ПОСЛУШАТЬ ПТЕНЧИКА
    Никаких необычных звуков нет.

    >

Видна недоработка. Игра сообщает нам, что птенец беспомощно попискивает, но тут же говорит нам, что никаких необычных звуков нет.

В библиотеке есть обширный набор стандартных сообщений-ответов на стандартные действия. «Никаких необычных звуков нет» является стандартным ответом на команду ПОСЛУШАТЬ. Он подходит для ПОСЛУШАТЬ ГНЕЗДО или ПОСЛУШАТЬ ДЕРЕВО, но в данном случае, с птенцом, он неуместен. Нужно добавить собственную реакцию:

    Object bird "птенчик/" forest
        with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
            name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
            before [;
                Listen:
                    print "Жалобный писк испуганной птички разрывает тебе сердце.
                        Надо помочь!^";
                    return true;
            ],
        has male;

Рассмотрим эту часть кода по шагам:

1. Для объекта `bird` мы ввели новое свойство, `before`. Интерпретатор обращается к свойству `before` перед тем, как выполнить конкретное действие с объектом:

       before [; ... ],

2. Значением свойства является локальная функция, в которой есть метка и две инструкции:

       Listen:
           print "Жалобный писк испуганной птички разрывает тебе сердце.
               Надо помочь!^";
           return true;

3. Метка обозначает тип действия, в данном случае `Listen` («послушать»). Меткой мы сообщаем интерпретатору следующее: если действие, которое будет совершено над птенчиком это «послушать», то надо выполнить эти инструкции. В противном случае продолжать как обычно. То есть, если игрок введёт ОСМОТРЕТЬ ПТЕНЦА, ВЗЯТЬ ПТЕНЦА, ПОЛОЖИТЬ ПТЕНЦА В ГНЕЗДО, УДАРИТЬ ПТЕНЦА или ПОГЛАДИТЬ ПТЕНЦА, то он получит стандартный ответ. Если же игрок введёт ПОСЛУШАТЬ ПТЕНЦА, то действие будет «перехвачено», и выполнятся наши инструкции.

4. Выполнятся следующие инструкции:

       print "Жалобный писк испуганной птички разрывает тебе сердце.
           Надо помочь!^";

   что выведет на экран указанную строку (^ выполнит перенос на следующую строку). Далее,

       return true;

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

       > ПОСЛУШАТЬ ПТЕНЧИКА
       Жалобный писк испуганной птички разрывает тебе сердце. Надо
       помочь!

       >

Стоит остановиться подробнее на инструкции `return true`. Свойство `before` перехватывает действие над объектом прежде, чем интерпретатор начнёт с ним что-то делать. В этот момент выполняются инструкции из локальной функции. Если последней инструкцией идёт `return true`, то значит что действие обработано и интерпретатору больше ничего не нужно делать, никаких действий или сообщений. Однако если в конце функции стоит `return false`, тогда интерпретатор продолжит выполнять действие так, будто оно не было перехвачено. Иногда это полезно, но не в нашем случае. Если записать эту секцию следующим образом:

    Object bird "птенчик/" forest
        with description "Слишком мал, чтобы летать, птенец беспомощно попискивает.",
            name 'детеныш' 'птиц' 'птичк' 'птенчик' 'птенц' 'маленьк',
            before [;
                Listen:
                    print "Жалобный писк испуганной птички разрывает тебе сердце.
                        Надо помочь!^";
                    return false;
            ],
        has male;

то интерпретатор выведет сначала нашу строку, а затем стандартный ответ:

    > ПОСЛУШАТЬ ПТЕНЧИКА
    Жалобный писк испуганной птички разрывает тебе сердце. Надо
    помочь!
    Никаких необычных звуков нет.

    >

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

## Вход в домик

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

    Перед домом
    Ты стоишь около избушки, на восток от которой раскинулся лес.

    > ИДТИ ВНУТРЬ
    Этот путь недоступен.

    >

Опять видим не самый лучший ответ. Но это легко исправить:

    Object before_cottage "Перед домом"
        with description
                "Ты стоишь около избушки, на восток от которой раскинулся лес.",
            e_to forest,
            in_to "Такой славный денек... Он слишком хорош, чтобы прятаться внутри.",
            cant_go "Единственный путь ведет на восток.",
        has light;

Обычно свойство `in_to` вело бы в другую комнату, как, например `e_to`, но если указать строку, то интерпретатор выведет эту строку, когда игрок попытается пойти ВНУТРЬ. Если пойти в другом неуказанном направлении, например НАВЕРХ или ИДТИ НА СЕВЕР, то игрок всё равно получит ответ «Этот путь недоступен», но это также легко изменить, добавив свойство `cant_go` с соответствующей строкой. Получим более дружелюбное поведение игры:

    Перед домом
    Ты стоишь около избушки, на восток от которой раскинулся лес.

    > ВНУТРЬ
    Такой славный денек... Он слишком хорош, чтобы прятаться внутри.

    > СЕВЕР
    Единственный путь ведет на восток.

    > ВОСТОК

    В лесной чаще
    ...

Здесь есть и другая проблема — мы не реализовали саму избушку, поэтому ОСМОТРЕТЬ ИЗБУШКУ выдаст игроку «Этого предмета здесь нет». Добавим объект cottage и сделаем его с атрибутом `scenery`, аналогично дереву:

    Object cottage "маленьк/ий домик/" before_cottage
        with description "Домик мал и неказист, но ты очень счастлива, живя здесь.",
            name 'маленьк' 'дом' 'изб' 'терем' 'коттедж' 'хат' 'небольш' 'строен'
                'домик' 'избушк' 'теремок' 'хатк',
            has scenery male;

Это решает проблему, но приводит к ещё одному неподходящему ответу:

    Перед домом
    Ты стоишь около избушки, на восток от которой раскинулся лес.

    > ВОЙТИ В ДОМИК
    Но на/в маленький домик невозможно войти, встать, сесть или лечь.

Это решается аналогично тому, как мы сделали ПОСЛУШАТЬ ПТЕНЧИКА:

    Object cottage "маленьк/ий домик/" before_cottage
        with description "Домик мал и неказист, но ты очень счастлива, живя здесь.",
            name 'маленьк' 'дом' 'изб' 'терем' 'коттедж' 'хат' 'небольш' 'строен'
                'домик' 'избушк' 'теремок' 'хатк',
            before [;
                Enter:
                    print_ret "Такой славный денек...
                        Он слишком хорош, чтобы прятаться внутри.";
                ],
            has scenery male;

При помощи свойства `before` мы перехватываем действие `Enter` (ВОЙТИ), которое применяется к объекту `cottage`. Правда, в этот раз мы воспользовались только одной инструкцией, а не двумя. Просто необходимость «вывести строку, сделать перевод строки, и затем `return true`» встречается так часто, что для этого есть отдельная инструкция, `print_ret`. То есть:

    print_ret "Такой славный денек...
               Он слишком хорош, чтобы прятаться внутри.";

идентично

    print "Такой славный денек...
               Он слишком хорош, чтобы прятаться внутри.^";
    return true;

Заметьте, что в `print_ret` нам не понадобился символ переноса `^`.

## Залезть на дерево

На полянке предполагается, что игрок введёт ВВЕРХ. Но игрок с большой вероятностью попробует ЗАЛЕЗТЬ НА ДЕРЕВО, но получит в ответ лишь «Забираться на высокий платан бессмысленно». Вновь воспользуемся свойством `before`, но чуть по-другому.

    Object tree "высок/ий платан/" clearing
        with description
            "Величавое дерево стоит посреди поляны.
             Кажется, по его стволу будет несложно влезть наверх.",
            name 'высок' 'платан' 'дерев' 'ствол' 'величав',
            before [;
                Climb:
                    PlayerTo(top_of_tree);
                    return true;
            ],
        has scenery male;

Здесь мы перехватываем действие `Climb` (ЗАЛЕЗТЬ), применяемое к объекту `tree`, но не для того, чтобы вывести своё сообщение, а для того, чтобы переместить игрока в другую комнату, так как если бы он ввёл ВВЕРХ. Перемещать игрока вручную довольно сложно, но, к счастью, в библиотеке есть стандартная функция, которая делает всё за нас.

Функция называется `PlayerTo`, и её нужно вызвать с параметром — идентификатором комнаты, куда мы хотим переместить игрока. При вызове параметр указывается внутри скобок: `PlayerTo(top_of_tree)`. Ранее мы встретились с функцией `Initialise`, эта функция не принимает никаких параметров, поэтому мы сказали, что её можно было бы вызвать как `Initialise()`.

Мы переместили игрока, но всё ещё находимся в перехватчике действия `Climb`. И поскольку мы уже обработали действие самостоятельно, нам не нужен стандартный ответ, и мы выполняем `return true`.

## Бросить предмет с дерева

В любой комнате если игрок введёт ПОЛОЖИТЬ (БРОСИТЬ) предмет, который он несёт с собой, то он упадёт в той же локации рядом на землю. Это поведение работает убедительно везде, кроме верхушки дерева — там предмет не должен падать рядом, а должен падать вниз на опушку.

Нам нужно перехватить действие `Drop`, но несколько иначе, чем мы делали раньше. Во-первых, действие должно срабатывать не для конкретных `bird` или `nest`, но в общем, то есть для любых предметов. И во-вторых, нужно учесть, что не все предметы можно бросить: например, нельзя БРОСИТЬ СУК.

Для решения второго пункта нужно перехватить действие `Drop` не до, а после того, как оно произошло. Так мы даём библиотеке разобраться с объектами, которые вообще нельзя бросить, или которых нет в руках у игрока, и вступаем только тогда, когда предмет уже был брошен. А для решения первого пункта мы будем перехватывать `Drop` не для наших объектов, а прямо на локации, где это происходит, то есть на верхушке дерева, `top_of_tree`:

    Object top_of_tree "На верхушке дерева"
        with description "На этой высоте цепляться за ствол уже не так удобно.",
            d_to clearing,
            after [;
                Drop:
                    move noun to clearing;
                    return false;
            ],
        has light;

Рассмотрим этот код по шагам:

1. Для нашей комнаты мы добавили свойство `after`. Интерпретатор обращается к этому свойству после того, как выполнит любое действие в этой комнате:

       after [; ... ],

2. Значение свойства является локальной функцией, содержащей метку и две инструкции:

       Drop:
           move noun to clearing;
           return false;

3. Метка обозначает имя действия, в данном случае `Drop`. Мы сообщаем интерпретатору следующее: если только что было совершено действие `Drop`, то выполни эти инструкции перед тем, как сообщить игроку, что действие завершено. Если произошло другое действие, то продолжи как обычно.

4. Сначала выполняется инструкция

       move noun to clearing;

   которая берёт объект, который был перенесён из объекта игрока, `player`, в объект `top_of_tree` (так как выполнилось действие `Drop`), и переносит его ещё раз, в объект `clearing`. В инструкции `noun` является библиотечной переменной, в которой будет храниться идентификатор объекта, к которому применяется действие. То есть если игрок вводит БРОСИТЬ ГНЕЗДО, то `noun` укажет на `nest`, а если БРОСИТЬ ПТЕНЦА, то `noun` станет `bird`. Далее мы исполняем

       return false;

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

   Вот что мы получим в игре:

       На верхушке дерева
       На этой высоте цепляться за ствол уже не так удобно.

       Здесь имеется надежный толстый сук.

       > БРОСИТЬ ГНЕЗДО
       Птичье гнездо положено.

       > ОСМОТРЕТЬСЯ

       На верхушке дерева
       На этой высоте цепляться за ствол уже не так удобно.

       Здесь имеется надежный толстый сук.

       > ВНИЗ

       Полянка

       Здесь имеется птичье гнездо (где имеется птенчик).

       >

Конечно, сообщение «Птичье гнездо положено». совсем не информативно в данном случае, поэтому можно сделать так:

    Object top_of_tree "На верхушке дерева"
        with description "На этой высоте цепляться за ствол уже не так удобно.",
            d_to clearing,
            after [;
                Drop:
                    move noun to clearing;
                    print_ret "Предмет упал вниз на землю.";
            ],
        has light;

Здесь `print_ret` выводит более корректное сообщение и возвращает `true`, что означает что интерпретатор больше не должен ничего выводить на экран.

## Птица в гнезде

Игра заканчивается, когда игрок кладёт гнездо на сук. Мы предположили, что птенец уже находится в гнезде, но это может быть не так. Необходимо также проверить, лежит ли птенец в гнезде. Это легко сделать:

    Object branch "надежн/ый толст/ый сук/" top_of_tree
        with description "Сук достаточно ровный и крепкий, чтобы на нем надежно
                          держалось что-то не очень большое.",
            name 'надежн' 'ровн' 'толст' 'крепк' 'сук' 'ветк',
            each_turn [; if (bird in nest && nest in branch) deadflag = 2; ],
        has static supporter male;

Мы расширили инструкцию `if`:

    if (bird in nest && nest in branch) deadflag = 2;

Её можно прочитать так: «Если `bird` находится на/в гнезде, и `nest` находится на/в `branch`, то установить `deadflag` равным 2. В противном случае ничего не делать».

## Заключение

К этому моменту вы уже поняли, что в игре обязательно надо реализовывать не только свои действия, но и те необязательные, которые могут прийти в голову игроку. Игрок может попробовать вообще что-то глупое, но ему будет интересно получить нестандартный ответ. Непроработка ответов может вообще разрушить эффект погружения.

В этой главе мы рассмотрели:

### Свойства объектов

У объектов может быть свойство `before`, и если оно есть, то интерпретатор обращается к нему перед выполнением действия над объектом. Аналогично есть свойство `after`, куда происходит обращение уже после выполненного действия, но ещё до того, как выведется сообщение игроку. И `before`, и `after` могут быть как у объектов-предметов (где перехватываются действия применимые к этим объектам), так и у комнат (где перехватываются действия, которые происходят с объектами в комнате).

Значениями этих двух свойств являются локальные функции. Если функция заканчивается `return false`, то интерпретатор продолжит выполнение, а если `return true` — то интерпретатор больше ничего делать не будет. Так можно либо частично изменить выполнение действия, либо заменить его полностью.

Ранее свойства соединений комнат указывали на объект комнаты, куда произойдёт перемещение. В этой главе также было показано, что это может быть строка (в которой указана причина, почему перемещение невозможно). Также было рассмотрено свойство `cant_go`, которое учитывает все неуказанные направления:

    e_to forest,
    in_to "Такой славный денек... Он слишком хорош, чтобы прятаться внутри.",
    cant_go "Единственный путь ведет на восток.",

### Функции и параметры

В библиотеке есть много удобных функций, и мы воспользовались функцией `PlayerTo`, которая позволяет перенести игрока в другую комнату, не обязательно соседнюю.

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

    PlayerTo(clearing);

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

    PlayerTo(clearing, 1);

Здесь 1 подавляет вывод описания новой комнаты.

### Инструкции

Нам встретились новые инструкции:

    return true;
    return false;

Они использовались для управления интерпретатором в локальных функциях.

    print "строка";
    print_ret "строка";

Инструкция `print` выводит указанную строку, а `print_ret` выводит строку, делает перенос и выполняет `return true`.

    if (условие && условие) ...

Мы расширили условие в инструкции `if`. `&&` — оператор «И», который используется для проверки выполнения нескольких условий сразу. Также существуют оператор `||` «ИЛИ» и оператор `~~` «НЕ».

    move объект to родитель;

Инструкция `move` изменяет дерево объектов, устанавливая для объекта нового родителя.

### Действия

Мы говорили о перехвате таких действий как `Listen`, `Enter`, `Climb` и `Drop`. Действие это представление того, что должно быть сделано, в зависимости от глагола, который введёт игрок. Например, ПОСЛУШАТЬ и ПРИСЛУШАТЬСЯ суть одно и то же, поэтому им соответствует действие `Listen`. Аналогично ВОЙТИ, ЗАЙТИ, СЕСТЬ НА, ЛЕЧЬ НА приводят к действию `Enter`, а ПОЛОЖИТЬ, ВЫБРОСИТЬ означают `Drop`. Так облегчается работа писателя-программиста, потому что различных глаголов может быть множество, но количество разных действий гораздо меньше.

В библиотеке каждому действию соответствует номер, и этот номер хранится в переменной `action`. Также есть переменная `noun`, в которой хранится идентификатор объекта, над которым производится действие, а также `second` для второго такого объекта (если он есть).

Вот несколько примеров:

Ввод игрока | действие | noun | second
--- | --- | --- | ---
СЛУШАТЬ | `Listen` | `nothing` | `nothing`
СЛУШАТЬ ПТЕНЧИКА | `Listen` | `bird` | `nothing`
ПОДНЯТЬ ПТЕНЧИКА | `Take` | `bird` | `nothing`
ПОЛОЖИТЬ ПТЕНЧИКА В ГНЕЗДО | `Insert` | `bird` | `nest`
БРОСИТЬ ГНЕЗДО | `Drop` | `nest` | `nothing`
ПОЛОЖИТЬ ГНЕЗДО НА СУК | `PutOn` | `nest` | `branch`

`nothing` — это встроенная константа, обозначающая отсутствие объекта.

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