Данный файл является частью Руководства по TADS для авторов игр.
Copyright © 1987, 1996 Майкл Дж. Робертс (Michael J. Roberts). Все права защищены.

Руководство было преобразовано в формат HTML Н. К. Гайем (N. K. Guy), компания tela design.

Перевод руководства на русский язык - Валентин Коптельцев


Глава четвертая


Раздел 4.3. Последовательность синтаксического анализа

Оставшаяся часть этой главы посвящена подробному описанию работы синтаксического анализатора. Текст разбит на параграфы, примерно соответствующие этапам работы СА в процессе анализа и выполнения команды игрока.

Когда игрок набирает текст в процессе игры, СА выполняет серию операций по преобразованию этой команды в "элементарные" действия над объектами игровой программы. В ходе выполнения этих операций СА может "советоваться" с игровой программой путем вызовов процедур/функций/методов; в большинстве случаев в СА определены стандартные действия для таких вызовов, поэтому если обработка по умолчанию вас устраивает, вам нет необходимости как-то модифицировать код своей программы. В то же время вы можете "заточить" синтаксическую обработку под свои конкретные нужды, определив подобные "точки входа". Главной целью данной главы является описание взаимодействия СА с игровой программой, чтобы вы могли заменять "умолчальные" действия своими. Кроме того, здесь же описана значительная часть внутренней "кухни" СА, что должно помочь вам лучше понять, каким образом заставить СА работать именно так, как нужно вам.

Примечание переводчика: для RTADS многие из точек входа уже определены для реализации обработки русского языка; описание работы этих точек входа будет приводиться в тексте.

Синтаксический анализатор TADS разделяет процесс интерпретации команды игрока на три основные фазы:

В данном Руководстве рассматривается также четвертая фаза - обработка окончания хода, но она относится скорее не к введенной команде, а представляет собой совокупность неких стандартных шагов, выполняемых (или не выполняемых;) СА после каждого хода.


Первая фаза: разбор слов и фраз

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


Считывание командной строки

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

Вывод приглашения на ввод: функции commandPrompt() и commandAfterRead()

Приглашение на ввод - это первое "умолчальное" действие СА, которое вы можете заменить своим: если в вашей игре определена функция с названием commandPrompt, СА будет вызывать эту функцию каждый раз, когда требуется вывести приглашение на ввод; в противном случае будет выводиться запрос на ввод по умолчанию, представляющий собой знак "больше" (>). Функция commandPrompt вызывается с аргументом, который позволяет отображать разные приглашения на ввод в зависимости от ситуации, если это требуется; по умолчанию приглашение на ввод всегда одинаково, но вы можете использовать разные приглашения для разных типов ввода. Значения аргумента функции commandPrompt могут быть следующими:

0 - Используется для обычных команд.

1 - Игрок ввел слово, неизвестное игре, и СА ожидает ввода новой команды, которая может представлять собой корректирующую инструкцию "oops". Если введенная игроком строка начинается со слова "oops" (или, сокращенно, "o"), СА заменит неизвестное ему слово из предыдущей команды игрока словом, следующим за словом "oops", и попытается обработать эту команду еще раз. В противном случае СА будет рассматривать введенную строку как совершенно новую команду.

Примечание переводчика: такая возможность корректировки введенной команды несколько устарела, и поэтому на ней обычно не заостряют внимание. Слово "oops" можно перевести на русский примерно как "опа!" (есть и более точный, но менее цензурный перевод;), или любое другое междометие, произносимое, когда неожиданно обнаруживаешь собственный промах/ляпсус/ошибку. Инструкция "oops" "встроена" в синтаксический анализатор TADS, т. е. относится к "закрытой", недоступной для модификации автором игры части кода. Однако, начиная с версии TADS 2.5.8, появились возможности обойти это ограничение. Не вдаваясь в детали, можно сказать, что игрок может ввести слово "ой" и разместить после него текст, на который необходимо заменить неизвестное синтаксическому анализатору слово. Подробнее см. далее в этом разделе.

2 - Игрок ввел неоднозначную команду, и СА запрашивает у него дополнительную информацию. СА выводит сообщение вида "Который "книга" вы имеете в виду: красную книгу, синюю книгу, или зеленую книгу?", и ожидает ввода команды, используя приглашение 2. Если игрок ввел что-то, что подходит в качестве ответа на поставленный вопрос (например, "зеленую книгу" или просто "синюю"), СА использует эти данные для устранения неопределенности. Если же ввод игрока невозможно интерпретировать в качестве в качестве уточняющей информации, СА воспринимает введенную строку как новую команду.

3 - игрок ввел команду, требующую "прямого" объекта, но не указал этот объект. СА выводит сообщение типа "Что вы хотите открыть?", после чего выводит приглашение ко вводу 3. Если игрок вводит название объекта, этот объект используется в качестве "прямого" в исходной команде, в противном случае СА считает, что введена новая команда.

4 - То же, что и в предыдущем случае, но для "косвенного" объекта.

Сразу после вызова commandPrompt СА считывает введенный игроком текст. После того, как игрок нажмет клавишу ENTER или RETURN для подтверждения ввода, СА вызывает еще одну определенную в игровой программе функцию - commandAfterRead; эта функция имеет точно такой же формат вызова, что и вызванная ранее commandPrompt. commandAfterRead используется в основном для отмены HTML-эффектов, активированных автором игры при вызове commandPrompt; например, если для вводимой команды вы задали специальный шрифт, используя тэг <FONT> в режиме HTML, то в commandAfterRead можете отключить его тэгом </FONT> (что, собственно, и сделано в версии этой функции, определенной в библиотеке advr.t по умолчанию).

Ниже приведен пример определения функции commandPrompt (взятый практически без изменений из файла stdr.t), при использовании которой в течение первых нескольких ходов отображается развернутое приглашение на ввод, которое затем меняется на сокращенное. Развернутое приглашение не отображается, когда СА переспрашивает игрока о чем-либо.

  commandPrompt: function(code)
{   "\b";
    if (global.prompt_count = nil) global.prompt_count := 0; 		
    global.prompt_count++;						
    if (global.prompt_count < 5)					
	"\nЧто вы хотите сделать сейчас?\b";		
    else if (global.prompt_count = 5)					
	"\nБольше вечный вопрос \"Что делать?\" докучать не будет.\n	
         Теперь готовность программы к приему новой команды будет означать появление значка \">\".\b";
    ">";
}

preparse()

После вызова commandAfterRead СА предоставит вашей игре доступ к новой команде, используя функцию preparse. СА вызывает ее и передает ей в качестве аргумента изначально введенный игроком текст. Если в вашей игре не определено функции с таким названием, СА пропускает этот этап. Если же такая функция определена, она может возвращать одно из следующих трех значений: true - в этом случае обработка команды продолжается обычным образом; nil - команда будет отброшена, и СА запросит у игрока следующую команду; наконец, может быть возвращена текстовая строка - в этом случае далее будет обрабатываться именно эта строка вместо изначально введенной.

preparse() - возвращаемые значения

true - введенная команда остается без изменений

nil - введенная команда отбрасывается

строка - вместо изначально введенной игроком команды обрабатывается возвращенная строка

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

В RTADS функция preparse выполняет следующие действия: заменяет во введенной игроком строке букву "ё" на "е" (это позволяет авторам игры значительно сэкономить трудозатраты при определении синонимов для объектов - не нужно учитывать, что игра должна понимать оба написания); переводит введенную строку в нижний регистр (необходимо, поскольку в стандартном TADS нет соответствия между строчными и заглавными русскими буквами, и, если не предпринять действий по преобразованию к единому регистру, то команды, скажем, "север", "Север" и "сЕВЕр" не будут рассматриваться, как одна и та же команда - пришлось бы определять синонимы для всех возможных комбинаций строчных и заглавных букв, что просто нереально); наконец, если игрок ввел просто "и", preparse заменяет его словом "инвентарь" (также необходимо из-за того, что "и" может использоваться и как союз). Об этих действиях функции preparse в RTADS следует помнить на тот случай, если вам понадобится реализовать какой-нибудь спецэффект (скажем, парольный ввод с учетом регистра символов). Для подавляющего большинства игр трогать ее не потребуется.

Пустой ввод: функция pardon()

Если игрок ввел пустую строку, то СА вызывает функцию с названием pardon, которая должна быть определена в игровой программе (необходимо подчеркнуть, что определение такой функции обязательно). Функция не имеет аргументов; ее единственное назначение - вывести сообщение об ошибке в ответ на пустой ввод. Вот как выглядит определение этой функции в файле stdr.t:

  pardon: function
{
    "Вы что-то сказали?";
}


Разбиение команды на слова

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

Прежде всего СА преобразует всю команду в нижний регистр. (В RTADS это преобразование осуществляет функция preparse, определенная в файле stdr.t, поскольку преобразование регистров в стандартном TADS не работает для кириллицы - примечание переводчика). Затем СА просматривает введенную строку, объединяя группы смежных букв и цифр в слова, учитывая при этом, что строки, заключенные в двойные или одинарные кавычки, всегда должны составлять одно целое, вне зависимости от наличия в них пробелов и других символов разделения.

Пусть, например, игрок ввел следующий текст:

  >Джо, Иди на Северо-запад, затем набери "Всем привет!" на клавиатуре компьютера.

СА преобразует эту строку в следующий список слов/символов:

джо

,

иди

на

северо-запад

,

затем

набери

"всем привет!"

на

клавиатуре

компьютера

.

Обратите внимание, что все знаки препинания считаются отдельными словами, а пробелы игнорируются. Кроме того, тире (дефисы) и одиночные апострофы считаются эквивалентными буквам/цифрам, поэтому, например, слово "северо-запад" считается одной лексемой.

На этом этапе обработки "точек входа" не предусмотрено, и автор игры никак не может повлиять на его выполнение.


Проверка на наличие специальных слов

После разбиения команды на отдельные слова СА проверяет получившийся список слов на наличие в нем специальных слов, которые определены в директиве specialWords в игре. Если в игре эта директива не определена, СА использует некий список по умолчанию, "зашитый" в самом синтаксическом анализаторе (однако, поскольку в библиотеке advr.t эта директива уже определена, в подавляющем большинстве случаев игра будет содержать эту директиву). Любое слово в "командном" списке, соответствующее одному из специальных слов, преобразуется в значение-флаг, указывающее на то, что это то или иное специальное слово.

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

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


Поиск в словаре

После разбиения команды на отдельные слова и выявления специальных слов в ней синтаксический анализатор пытается найти для каждого из слов команды эквивалент в своем словаре. Словарь представляет собой внутреннюю таблицу строк, поддерживаемую СА; каждая строка в этой таблице соответствует объектам, которые используют данную строку в определениях того или иного лексического свойства (глагола (verb), предлога (prepopsition), существительного (noun), прилагательного (adjective), множественного числа (plural) или артикля (article)). Словарь позволяет очень быстро находить все объекты, использующие то или иное слово в лексических свойствах.

Для каждого слова в команде, имеющего словарный эквивалент, СА устанавливает флаги тех частей речи (существительного, прилагательного и т. д.), в качестве которых данное слово фигурирует в словаре. Например, если вы определите слово "больной" в качестве существительного для одного объекта, а для другого объекта - в качестве прилагательного (например, "больной ребенок"), то слово будет присутствовать в словаре дважды, и СА пометит слово "больной", если оно встретится в веденной игроком команде, как являющееся одновременно существительным и прилагательным.

Неизвестные слова

Каждое слово, не найденное в словаре, СА помечает как неизвестное. В общем случае TADS не воспринимает команды с неизвестными словами; однако на данной стадии грамматического разбора команды слово просто помечается как неизвестное, после чего обработка команды продолжается.

В ходе дальнейшей обработки команды TADS вновь вернется к неизвестному ему слову, но к тому моменту у СА будет больше информации о контексте, в котором используется слово. Тогда TADS и выберет одно из следующих действий, наиболее точно соответствующее контексту:


Объединение слов в команды

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

Слово "затем", точка, восклицательный и вопросительный знак понимаются СА однозначно - они используются исключительно для отделения команд друг от друга. В связи с этим первое, что делает СА - просматривает введенную командную строку в поисках одного из этих слов/символов. Если один или несколько таких разделителей обнаруживаются в самом начале строки, они попросту отбрасываются. Если же разделитель оказывается в середине строки, то СА выделяет все слова, стоящие до него, и работает с ними как с отдельной командой. Пусть, например, игрок ввел следующее предложение:

  >Вася, иди на запад, затем открой дверь и окно и иди на восток.

СА рассматривает часть предложения до слова "затем" как отдельную команду. "Хвост" введенного предложения также считается командой. Таким образом, на данном этапе СА разобьет введенную строку на две команды:

  Вася, иди на запад
  открой дверь и окно и иди на восток

Обратите внимание, что слово "и" и запятая неоднозначны, поскольку они могут использоваться как для разделения разных команд, так и объектов внутри одной команды. СА откладывает решение о том, как их интерпретировать, до более поздней стадии обработки; поэтому, хотя в нашем приведенном выше примере вторую часть строки можно также разбить на две отдельных команды, СА на этом этапе считает их за одну.

После "разбивки" СА обрабатывает каждую из полученных команд отдельно. Команды обрабатываются последовательно одна за другой до тех пор, пока не будут обработаны все команды, или не возникнет ошибка, или в процессе обработки игровая команда не вызовет инструкцию exit, exitobj либо abort.


Проверка наличия актера

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

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

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

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

Прежде всего СА просматривает список и проверяет, какие из объектов являются видимыми. С этой целью он для каждого объекта в списке вызывает метод isVisible(parserGetMe()), возвращающий true, если объект виден из заданной точки (parserGetMe() - текущий главный персонаж), и nil в противном случае. Этот шаг используется исключительно для того, чтобы определиться с тем, какое именно сообщение следует выдавать впоследствии в случае возникновения ошибки: объект может быть видимым, но непригодным в качестве актера, и в этом случае требуется иное сообщение по сравнению с ситуацией, когда объект вообще отсутствует.

Далее СА определяет, может ли тот или иной объект выступать в качестве актера. С этой целью СА вызывает метод validActor для каждого из объектов, который возвращает true, если объект может быть актером. Определенная по умолчанию в файле advr.t версия этого метода возвращает true в случае, если объект достижим для игрока. Вы можете заменить это определение для реализации каких-либо специальных эффектов. Например, если у игрока имеется рация, то можно определить всех персонажей в игре, у которых также есть рация, в качестве допустимых актеров вне зависимости от того, видит их игрок или нет, поскольку последний может передавать им команды по радиосвязи.

Также обратите внимание, что метод validActor используется для проверки того, может ли объект в принципе выступать в качестве актера (т. е. можно ли ему в принципе отдать команду), и не контролирует, насколько логичной будет попытка отдать этому объекту команду. В связи с этим метод validActor не делает различий между объектами-персонажами и всеми остальными объектами. Повторим еще раз - метод просто проверяет, можно ли объекту отдать команду в принципе; если, скажем, в одной комнате с игроком имеется кочан капусты, игрок должен иметь возможность отдать ему команду, даже если это не принесет никакого результата.

Для объектов, успешно прошедших проверку метода validActor, СА проверяет, является ли тот или иной объект "предпочтительным" (preferred) актером, посредством вызова метода preferredActor для каждого из них. Данный метод возвращает true, если от объекта в принципе можно ожидать исполнения команд игрока, в противном случае возвращается nil. Определенная в advr.t по умолчанию версия метода возвращает true для всех объектов-потомков класса Actor, для всех остальных объектов возвращается nil.

После удаления из списка всех объектов, "проваливших" тест метода validActor, СА повторно просматривает список, чтобы определить, какие объекты в нем остались.

Если объектов не осталось совсем, это означает, что игрок попытался поговорить с кем-то/чем-то, к кому/чему в данный момент нет доступа (говоря упрощенно, кого/чего нет сейчас в комнате, где находится игрок) либо этот кто-то/что-то не существует вообще. Если ни один из объектов в исходном списке не был видимым, СА выводит ошибку с кодом 9 - "Я не вижу здесь объект "%s". (Обратите внимание, что последовательность "%s" заменяется тем словосочетанием исходной строки, которое СА воспринял как имя персонажа. Скажем, если изначально игрок ввел команду "Вася, иди на запад", то будет выведено сообщение "Я не вижу здесь объект "вася"). Причина, по которой СА использует в сообщении оригинальный "текст" игрока, состоит в том, что анализатор просто не может определить, к какому именно объекту обращается игрок, а следовательно, не может использовать в сообщении описание этого объекта. Также обратите внимание, что введенные игроком слова будут преобразованы в нижний регистр.

Если в списке остается более одного объекта, СА проверяет результаты выполнения метода preferredActor для каждого из объектов. Если для каких-либо объектов этот метод вернул true, СА отбрасывает все остальные объекты; если же было возвращено значение nil для всех объектов, СА далее игнорирует результаты проверки метода preferredActor и не изменяет список. Если метод preferredActor вернул true для одного и только одного объекта, СА использует в качестве актера именно этот объект; процедура определения актера считается успешно завершенной. В противном случае СА вынужден запросить у игрока дополнительную информацию, чтобы понять, какой именно из оставшихся объектов тот имел в виду; этот процесс идентичен для любых объектов (не только актеров), и будет описан далее.


Идентификация глагола

После того, как синтаксический анализатор определил актера, к которому обращена команда (или убедился в его отсутствии), он (анализатор) ищет глагол. Это один из сравнительно менее сложных аспектов синтаксического разбора, поскольку глагол всегда стоит в команде на первом месте. СА просто берет первое слово в команде и проверяет, можно ли его использовать в качестве глагола; если это невозможно, то выводится сообщение с кодом 17 ("В этом предложении нет глагола!"), после чего обработка команды прекращается.

Глагол можно определить с использованием одного или двух слов. Например, для одного и того же глагола "нажимать" определены лексические свойства "нажать" (одно слово) и "нажать на" (2 слова). В английском языке (из которого, напомним, "родом" TADS) вообще есть устойчивые словосочетания "глагол-предлог", при которых предлог фактически является неотделимой частью глагола, полностью меняя первоначальный смысл последнего. Хотя в русском языке такого нет, подобный прием определения глаголов позволяет значительно упростить синтаксический анализ конструкций, где между глаголом и объектом действия располагается некая связка. Обратите внимание, что, когда вы определяете глагол из двух слов, вам необходимо убедиться, что второе из этих слов определено в качестве предлога; например, в нашем примере для "нажать на" слово "на" определено еще и как предлог (пример взят непосредственно из advr.t).

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

После того, как СА идентифицирует глагол (с использованием одного или двух первых слов введенной команды), он проверяет, остались ли в команде еще какие-либо элементы. Если следующее слово команды является разделителем (союз "и" либо запятая), либо в команде больше нет слов, СА считает, что команда окончилась, и выполняет команду, состоящую только из глагола, без каких-либо объектов.

Если следующее слово команды начинает новое словосочетание, СА считывает это словосочетание и затем проверяет, что следует за этим словосочетанием. Если за словосочетанием следует разделитель команд, СА выполняет предшествующую команду, состоящую из глагола с "прямым" объектом.

Если же следующее слово окажется предлогом, СА проверяет, что следует за этим предлогом. Если слово, следующее за ним, является первым словом еще одного словосочетания, СА считывает и это словосочетание, после чего выполняет команду с глаголом, "прямым" и "косвенным" объектом и с предлогом, эти объекты разделяющим.

Если вслед за предлогом идет разделитель команд, СА рассматривает предлог как часть глагола. Это связано с тем, что в английском языке для "составных" глаголов (комбинаций глагол-предлог) допустимо размещать этот предлог как сразу после глагола, так и в конце предложения. В русском языке такого нет (сказать "нажать кнопку на" нельзя), но, во-первых, такое поведение СА совершенно не мешает ни автору, ни игроку, а во-вторых, если подумать, то аналогии можно подобрать и в русском языке (скажем, предложение типа "поднять руки вверх" или "поднять вверх руки", хотя слово "вверх", строго говоря, не предлог).

Обратите внимание, что зачастую команда может оказаться неоднозначной (применительно к словарю конкретной игры). Приведенные выше правила разбора предложения, применяемые СА, были разработаны для наиболее распространенной интерпретации (принятой в английском языке), однако в некоторых случаях результаты не будут совпадать с ожидаемыми.

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

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

Обратите внимание, что в ходе идентификации глагола СА также "опознает" словосочетания, соответствующие "прямому" и "косвенному" объектам, а также соединяющий их предлог. Для команды "открыть дверь и окно" СА определит следующие составные части команды:

актер (actor): Me (главный персонаж "по умолчанию")

глагол (verb): открыть

"прямой" объект (direct object): дверь и окно

предлог-связка (preposition): отсутствует

"косвенный" объект (indirect object): отсутствует

Для команды "Джо, положи мяч на стол" СА выделит следующие элементы:

актер: Джо

глагол: класть

"прямой" объект: мяч

предлог: на

"косвенный" объект: стол

Примечание переводчика: на самом деле определение составных частей команды, выполненное на данном этапе, носит предварительный характер: впоследствии СА может вернуться к этому шагу обработки повторно (причем, если для "родного" TADS это скорее исключение, то в RTADS это будет происходить сплошь и рядом). Точки входа, инициирующие такую циклическую обработку, будут описаны далее.


Неизвестный глагол или синтаксис: parseUnknownVerb()

Если синтаксический анализатор не может найти для команды подходящий глагол, либо не может отнести команду к одному из стандартных типов (глагол без объектов, глагол - "прямой" объект, глагол - "прямой" объект - предлог - "косвенный" объект) по какой-либо причине, СА пытается вызвать функцию с названием parseUnknownVerb, которая определяется в игровой программе. Заголовок этой функции должен выглядеть следующим образом:

  parseUnknownVerb: function(actor, wordlist, typelist, errnum);

Аргумент actor - это текущий объект-актер. Параметр wordlist определяет список слов команды, который имеет тот же формат, что и для вызова функции preparseCmd. В errnum содержится код ошибки СА, вызвавшей обращение к parseUnknownVerb; это тот же код, который передается функции parseError и связанным с ней.

Аргумент typelist - это список типов, соответствующий словам в списке wordlist; скажем, элемент typelist[3] содержит тип слова wordlist[3] и т. д. Таким образом, количество элементов в этих списках всегда совпадает. Тип слова представляет собой целое число, которое может быть любым из приведенных ниже значений либо их комбинацией, полученной посредством оператора побитного ИЛИ ("|"). Чтобы проверить значение того или иного типа, можно использовать логические выражения следующего вида: ((typelist[3] & PRSTYP_NOUN) != 0). Значения типов определены в advr.t и выглядят следующим образом:

PRSTYP_ARTICLE - слово определено в качестве артикля (article) (в русском языке не используется)

PRSTYP_ADJ - прилагательное (adjective)

PRSTYP_NOUN - существительное (noun)

PRSTYP_PLURAL - множественное число (plural)

PRSTYP_PREP - предлог (preposition)

PRSTYP_VERB - глагол (verb)

PRSTYP_SPEC - специальное слово (".", "и", "для" и т. д.)

PRSTYP_UNKNOWN - такого слова нет в словаре игры

Функция может возвращать значения true, nil, целочисленное значение, а также может выполнять инструкцию abort для прерывания команды.

Возврат значения true означает, что функция успешно обработала команду; СА не выводит никаких сообщений об ошибках, выполняет демоны/запалы, а также функцию endCommand (передавая ей в качестве параметра-глагола nil), после чего в случае необходимости продолжает обработку оставшегося введенного текста (если он отделен точкой или словом "затем").

Возврат числового (большего нуля) значения также означает успешную обработку команды, но одновременно указывает, что обработана лишь ее часть до слова, порядковый номер в команде которого совпадает с возвращенным значением, и что оставшаяся часть введенного предложения (начиная со слова, чей порядковый номер был возвращен) рассматривается как следующая команда. СА выполнит демоны/запалы и функцию endCommand, после чего продолжит обработку этой оставшейся части команды. Вы можете использовать эту возможность, например, в случае, если после только что обработанного словосочетания встретится союз "и", либо если вам встретится новое предложение во введенной команде и вы по каким-либо причинам предпочтете обрабатывать его отдельно. В качестве примера: если parseUnknownVerb успешно обработала первые три слова команды, она должна вернуть значение 4, чтобы сообщить СА, что от него требуется разобрать команду стандартным образом, начиная с четвертого слова.

Возврат значения nil означает, что функция не смогла обработать команду, и сообщает СА, что необходимо вывести стандартное сообщение об ошибке. В этом случае СА выводит сообщение об ошибке обычным образом, в т. ч. обращаясь к функциям parseErrorParam или parseError (если таковые определены), и отбрасывает команду. Запалы и демоны не выполняются, и вызова функции endCommand также не происходит.

Если данная функция использует инструкцию abort для прерывания команды, СА не будет выполнять демоны и запалы и проигнорирует оставшийся введенный текст (если таковой имеется). В то же время СА выполнит вызов функции endCommand, передав ей код состояния EC_ABORT (который указывает на то, что команда была прервана). Разница между завершением parseUnknownVerb с возвращением значения nil и прерыванием команды инструкцией abort состоит в том, что в первом случае СА выведет сообщение об ошибке по умолчанию и не будет вызывать endCommand, а во втором поступит ровно наоборот, т. е. не будет выводить никаких сообщений, но вызовет функцию endCommand.

Функция parseUnknownVerb в TADS вызывается для ошибок со следующими кодами:

17 - В этом предложении нет глагола! (There's no verb in that sentence!)

18 - Я не понимаю это предложение. (I don't understand that sentence.)

19 - После вашей команды не хватает слова. (There are words after your command I couldn't use.)

20 - Не знаю как использовать слово "%s" таким образом. (I don't know how to use the word "%s" like that.)

21 - После вашей команды есть лишние слова. (There appear to be extra words after your command.)

23 - internal error: verb has no action, doAction, or ioAction (русского эквивалента у этого сообщения нет)

24 - Я не понимаю это предложение. (I don't recognize that sentence.)

Ошибка с кодом 17 указывает на то, что первое слово в предложении не определено в качестве глагола (это может означать, что слово вообще отсутствует в словаре игры, либо что оно определено, но в качестве другой части речи). Код 18 означает некорректное словосочетание, либо то, что указанное в команде сочетание глагола и предлога (например, "нажать на") в игре не определено. Код 19 означает, что в конце предложения имеется предлог, который не удалось связать с глаголом. Код 20 означает, что слово, отделяющее "косвенный" объект, не определено в качестве предлога. Код 21 указывает на то, что после конца предложения (так, как его воспринял СА) следует еще одно слово (например, предложение заканчивается двумя предлогами). Ошибка с кодом 23 возникает в случае, если для глагольного объекта класса deepverb не определен необходимый для обработки команды метод (action, doAction, или ioAction). При этом, в отличие от всех остальных случаев, ошибка с этим кодом является следствием проблем с самой игровой программой, а не с введенной игроком командой (именно поэтому для нее не предусмотрено русского перевода). Код 24 указывает на слишком большое количество объектов, использованных в команде; например, указан "прямой" объект, а для объекта класса deepverb, соответствующего глаголу команды, отсутствует метод doAction; либо определен "косвенный" объект, а у глагола отсутствует метод ioAction.

Назначение данной функции состоит в том, чтобы дать автору игры возможность организовать собственную систему разбора тех команд, которые встроенный СА не смог распознать. Хотя эта функция похожа на preparseCmd, она отличается от последней тем, что выполняется только в случаях, когда СА не может обработать команду самостоятельно; таким образом, функции parseUnknownVerb не требуется принимать решение о том, передавать ли команду на обработку встроенному анализатору или нет. Кроме того, parseUnknownVerb тесно интегрирована в механизм отсчета ходов в игре; в частности, она позволяет управлять выполнением демонов/запалов, вызовами функции endCommand, а также дальнейшим разбором оставшегося необработанного текста введенной команды.

Если в игре не определена функция parseUnknownVerb, СА просто выводит соответствующее сообщение об ошибке и прерывает команду.

Функция parseUnknownVerb позволяет автору игры контролировать весь процесс обработки команды. В некоторых случаях может потребоваться полностью определить весь процесс разбора предложения "с нуля", не обращаясь к встроенному СА. Однако в других случаях бывает необходимо использовать элементы обычного синтаксического анализа - например, для разбора/подбора объектов для словосочетаний. СА предоставляет ряд встроенных функций для обработки введенной строки непосредственно из игры.

В библиотеках RTADS функция parseUnknownVerb в настоящее время не определена.


Словосочетания и объекты

В TADS имеется встроенный анализатор-разборщик словосочетаний; в этом разделе описывается его работа. Однако прежде, чем начать разбирать словосочетания по стандартному алгоритму, TADS пытается вызвать определяемую в игре функцию с названием parseNounPhrase() (если таковая существует); это дает возможность автору игры определить собственный порядок разбора словосочетаний.

"По классике" (т. е. в соответствии с "представлениями" встроенного СА) словосочетание состоит из артикля (которого может и не быть, особенно в русскоязычной игре;), одного или нескольких прилагательных (также опциональных), существительного в единственном или множественном числе, а также (возможно, но не обязательно) из слова "для" (в английском "of") или его эквивалента, определенного при помощи инструкции specialWords, и еще одного словосочетания. В RTADS связки между "подсловосочетаниями", составляющими целое словосочетание, может и не быть (например, "собака Павлова"), но в процессе обработки словосочетание все равно приводится к стандартному виду.

Отдельные специальные слова также могут использоваться в качестве словосочетаний. Слово "все" (или его эквивалент), например, само по себе является вполне "законным" словосочетанием, так же, как местоимения (он, она, оно, они). Конструкции, начинающиеся с "все" или "все из", за которым следует словосочетание во множественном числе (т. е. словосочетание, в котором "главное" слово определено не как существительное (noun), а как множественное число (plural), являются словосочетанием, эквивалентным словосочетанию во множественном числе без предшествующего "все из" или "все". Точно таким же образом можно использовать слово "оба" или "оба из". Также допустимо использование слов "любой", "любой из", за которыми должно следовать словосочетание во множественном числе; такая команда указывает СА на необходимость выбора одного из объектов, к которым может относиться словосочетание во множественном числе, случайным образом.

Числа в качестве количественных числительных

Игрок также может указать для множественного числа и/или слова "любые" количественное числительное. Например, возможны такие словосочетания, как "3 книги", "любые 3 книги"; они будут иметь такой же эффект, как словосочетание "любая книга", но СА случайным образом выберет из набора указанных объектов (в данном случае книг) не одну, а три. Если указано числительное 1, СА позволяет использовать этот формат и для единственного числа: "1 книга" или "любая 1 книга", что будет эквивалентно команде "любая книга".

СА также умеет обрабатывать несколько словосочетаний, отделенных друг от друга союзом "и" и/или запятой.

В ходе первого этапа синтаксического разбора команды, до ее выполнения, СА идентифицирует для каждого словосочетания все объекты, к которым оно применимо. После определения слов, входящих в словосочетание (что выполняется исключительно на основании того, в качестве каких частей речи эти слова определены в игре - см. выше), СА составляет список всех объектов, для которых в качестве "правильных" (т. е. с учетом принадлежности той или иной части речи) лексических свойств определены слова, используемые в словосочетании. Например, для словосочетания "большой красный мяч" в список будут включены все объекты, у которых слово "мяч" определено в качестве лексического свойства-существительного (noun), либо слова "большой" и "красный" - в качестве прилагательных (adjective). Затем СА "проредит" эти списки, оставив только те объекты, для которых определены все три слова.

В большинстве случаев число, используемое в команде, должно интерпретироваться как существительное и ставится в соответствие специальному объекту numObj (см. раздел Разбор словосочетаний). Однако в некоторых случаях может потребоваться, чтобы число являлось частью названия объекта. Например, если у вас в игре есть пятирублевая монета, то число "5" может выступать в качестве признака монеты и должно быть определено в качестве прилагательного (adjective). Другой случай - кнопки лифта, на которых написаны номера этажей.

Числа в качестве порядковых числительных

Синтаксический анализатор позволяет использовать числа в качестве лексических свойств-прилагательных, например:

  button3: floorButton
    noun='кнопка'
    adjective = '3' 'три' 'третья'
    floor = 3
  ;

Если число определено как лексическое свойство-прилагательное, и игрок может обратиться к объекту (объект присутствует в помещении), СА позволяет ставить числительное в команде как до, так и после основного существительного: игрок может обратиться к объекту button3 при помощи словосочетаний "кнопка 3" или "3 кнопка" (а также, например, "третья кнопка").

Числа в качестве порядковых числительных - часть 2-я

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

Пусть, например, в нашей игре имеется картотека с ячейками, пронумерованными от 10000 до 20000. Создавать отдельные объекты для всех десяти тысяч ячеек просто нереально; к счастью, в TADS имеются средства для элегантного решения данной задачи.

Чтобы определить объект, который воспринимал бы в качестве прилагательного любое числительное, укажите в лексическом свойстве adjective специальное значение "#":

  Yatcheyka: fixeditem, container
    noun = 'ячейка'
    plural = 'ячейки'
    adjective = '#'
    location = Kartoteka
    sdesc = "ячейка";

Специальное значение свойства-прилагательного '#' указывает СА на то, что объект можно использовать с любым числительным. СА будет считать эквивалентными словосочетания "ячейка 11000" и "11000 ячейка".

Если игрок обращается к объекту в своей команде, СА требует, чтобы при этом был указан номер объекта. Если игрок не указал номер (например, ввел "заглянуть в ячейку"), СА отреагирует выводом сообщения об ошибке с кодом 160 - "Вам придется подробнее описать какой "%s" Вы имеете в виду." (где "%s" будет заменено на то название объекта, которое ввел игрок - в нашем случае "ячейка"). Это сообщение можно переопределить при помощи функций parseErrorParam или parseError точно так же, как и любое другое стандартное сообщение об ошибке СА.

Если игрок вводит название объекта с номером (например, "ячейка 1100"), СА вызывает метод newNumbered, который должен быть определен в объекте:

  Yatcheyka.newNumbered(actor, verb, num);

Аргумент actor соответствует объекту-актеру, выполняющему команду, verb - это объект-глагол, соответствующий введенной команде, а num - введенный игроком номер. Например, если игрок введет команду "заглянуть в ячейку 1100", вызов метода будет выглядеть так:

  Yatcheyka.newNumbered(Me, lookInVerb, 11000);

Значение num может быть также равно nil; это означает, что игрок обратился к объекту во множественнном числе ("заглянуть в ячейки"), т. е. он хочет выполнить некое действие по отношению ко всему набору предметов, для представления которых используется нумерованный объект.

Метод newNumbered должен возвращать либо объект, либо nil. В последнем случае необходимо предварительно вывести сообщение об ошибке, поскольку СА при этом просто прервет выполнение команды, не выводя никаких дополнительных сообщений. Если метод вернет объект, СА в дальнейшем будет использовать в команде этот объект вместо исходного нумерованного объекта.

Стандартная библиотека advr.t предоставляет класс numberedObject, удобный для реализации нумерованных объектов. Этот класс определяет метод newNumbered, создающий копию объекта с использованием оператора new, и присваивающий свойству value скопированного объекта значение, соответствующее тому числу, которое ввел игрок. Например, если игрок ввел "ячейка 99", то метод newNumbered создаст копию объекта, присвоит свойству value значение 99 и вернет этот новый объект. Класс numberedObject определяет также еще один метод, num_is_valid(num), который вы можете переопределять в дочерних объектах для этого класса. Метод numberedObject.newNumbered вызывает num_is_valid с введенным игроком числом в качестве аргумента, чтобы проверить, возможно ли использование этого номера с данным объектом. По умолчанию этот метод возвращает true (т. е. допустимым будет любой номер); если вы хотите ограничить диапазон допустимых номеров, вам потребуется переопределить этот метод. Например, для нашего примера с ячейками, если требуется ограничить диапазон номеров интервалом от 10000 до 20000, то определение соответствующего объекта могло бы выглядеть так:

  Yatcheyka: fixeditem, container, numberedObject
    noun = 'ячейка'
    plural = 'ячейки'
    adjective = '#'
    location = postOfficeLobby
    sdesc = "ячейка"

    num_is_valid(num) =
    {
      if (num >= 10000 && num <= 20000)
      {
        /* Номер из нашего допустимого диапазона */
        return true;
      }
      else
      {
        /* Номер вне допустимого диапазона */
        "Ячейки имеют номера с 10000 до 20000. Ячейки с номером <<num>> среди них нет. ";
        return nil;
      }
    }
  ;

Метод newNumbered, определенный в классе numberedObject, по умолчанию возвращает исходный объект, если игрок обратился в своей команде к объекту во множественном числе. Если вам требуется, чтобы при таком обращении к исходному объекту использовался другой объект, переопределите метод newNumberedPlural(actor, verb) таким образом, чтобы он возвращал другой объект. Если вы хотите вообще запретить обращение к объекту во множественном числе (например, сделать недопустимой команду "заглянуть в ячейки"), то определите метод newNumberedPlural так, чтобы он выдавал соответствующее сообщение об ошибке и возвращал nil.

Класс numberedObject определяет методы dobjGen и iobjGen, которые не позволяют выполнять действия над объектом, если к этому объекту обращаются во множественном числе; с этой целью они проверяют значение свойства value; если это свойство имеет значение nil, это и означает, что в команде используется исходный объект, т. е. к нему обратились во множественном числе. Эти методы просто выводят сообщение "Нужно более конкретно указать что вы имеете в виду." ("You'll have to be more specific about which one you mean" в английской версии) и игнорируют команду. Вам может потребоваться изменить эту реакцию для некоторых глаголов; например, скорее всего, вам потребуется некое общее описание набора объектов, которое выводилось бы по команде "осмотреть". Такого эффекта можно добиться, дополнив наш объект Yatcheyka следующим определением:

    dobjGen(actor, verb, iobj, prep) =
    {
      if (self.value != nil or verb != inspectVerb)
        inherited.dobjGen(actor, verb, iobj, prep);
      else
      {
        "Ячейки полностью заполняют всю северную стену, образуя аккуратный растр. 
        Они имеют номера с 10000 до 20000.";
        exitobj;
      }
    }

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

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

  doOpen(actor) =
  {
    "Ты выдвигаешь ячейку и заглядываешь внутрь. 
    Обнаружив, что она пуста, ты снова закрываешь ее.";
  }

В некоторых случаях СА использует еще один метод. Если игрок в своей команде обращается к объекту, чей список прилагательных содержит значение "#", с использованием слова "любой", СА вызывает метод anyvalue(n) для этого объекта и проверяет возвращаемое им значение. Аргумент n - это число, указывающее, к какому количеству таких объектов обращается команда. В настоящее время этот аргумент всегда равен 1; в будущем, вероятно, команда типа "нажать любые 3 кнопки" обратится к этому методу три раза, устанавливая аргумент "n" равным, соответственно, 1, 2 и 3 для каждого вызова. (Примечание переводчика: этого не будет, поскольку сейчас Майкл Робертс работает над следующим поколением своей системы - TADS 3, которая кардинально отличается от второй версии). В настоящее время с точки зрения СА команда "нажать любые 3 кнопки" эквивалентна команде "нажать кнопки". "Умолчальная" реализация метода anyvalue(n) в классе numberedObject в advr.t просто возвращает то же число "n", которое было передано ему в качестве аргумента; если диапазон доступных значений номеров для вашего объекта начинается не с единицы, вам необходимо переопределить этот метод так, чтобы он возвращал значение из допустимого диапазона номеров.


Нестандартный разбор словосочетаний: parseNounPhrase()

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

  parseNounPhrase: function(wordlist, typelist, current_index,
                            complain_on_no_match, is_actor_check)

Аргумент wordlist представляет собой список строковых значений, где каждое строковое значение является лексемой команды игрока. Точно такой же список передается также и функции preparseCmd().

typelist - это список типов для лексем из предыдущего аргумента. Типы представляют собой значения, анализируемые побитно, поэтому любой элемент может соответствовать не одному, а целой комбинации типов, объединенных оператором ИЛИ. Для того, чтобы проверить, установлен ли флаг для конкретного типа, следует использовать оператор побитного И, "&"; например, чтобы проверить, яявляется ли вторая лексема в команде существительным (noun), можно использовать следующее выражение:

  ((typelist[2] & PRSTYP_NOUN) != 0)

В advr.t определены следующие типы:

PRSTYP_ARTICLE - слово определено в качестве артикля (article)

PRSTYP_ADJ - прилагательное (adjective)

PRSTYP_NOUN - существительное (noun)

PRSTYP_PLURAL - множественное число (plural)

PRSTYP_PREP - предлог (preposition)

PRSTYP_VERB - глагол (verb)

PRSTYP_SPEC - специальное слово (".", "и", "для" и т. д.)

PRSTYP_UNKNOWN - слово отсутствует в словаре

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

Аргумент complain_on_no_match - это булевское значение (true или nil), указывающее, должна ли функция выводить сообщение об ошибке в случае, если для корректного по форме словосочетания не удалось подобрать объект. Если этот параметр имеет значение true, то вам необходимо позаботиться о том, чтобы функция выводила соответствующее сообщение об ошибке; если же он равен nil, то функция не должна выводить никаких сообщений в этом случае. Подчеркнем, что для синтаксически некорректных словосочетаний сообщение об ошибке следует выводить в любом случае; данный параметр относится только к случаю, когда для синтаксически корректного словосочетания в игре не находится подходящих объектов.

is_actor_check - это булевское значение, указывающее, вызывается ли функция с целью проверки словосочетания, соответствующего актеру. Если этот аргумент равен true, функция должна запрещать синтаксис, однозначно неприемлемый для актера - например, использование слова "все", а также строковых или числовых значений (на самом деле, какой синтаксис считать неприемлемым, должно определяться для каждой конкретной игры - вполне вероятно, что в игре могут присутствовать роботы, различающиеся по номерам, которым игрок может давать команду типа "робот 8, открой ворота"). В случаях некорректного "актерского" синтаксиса функция просто должна возвращать пустой список объектов, сообщая таким образом, что подходящего актера найти не удалось.

Функция parseNounPhrase() может выполнять одно из следующих четырех действий: она может обработать словосочетание и вернуть список подходящих объектов; она может определить, что словосочетание отсутствует; она может сообщить, что словосочетание имеется, но оно содержит синтаксические ошибки; наконец, она может предоставить синтаксическому анализатору самому выполнить "умолчальный" разбор словосочетания.

Если данная функция успешно обработает словосочетание, она должна вернуть список. Первым элементом списка должно быть число, соответствующее порядковому номеру лексемы, следующей сразу за обработанным словосочетанием. Например, если при вызове функции аргумент current_index имел значение 3, а словосочетание состояло из одного слова, то первым элементом возвращенного списка должно быть число 4. Это значение указывает СА, с какого места ему следует возобновить "умолчальную" обработку.

Оставшаяся часть возвращаемого списка состоит из пар элементов; первый элемент в паре определяет объект игровой программы, соответствующий словосочетанию, а второй - это число, содержащее флаги для этого объекта. На данном этапе нет необходимости проверять объект на доступность, видимость, достижимость и т. п. - все это СА выполнит самостоятельно несколько позднее, когда будет больше "знать" о структуре предложения. Все, что требуется от функции parseNounPhrase() - вернуть список всех объектов, к которым применимо (с лексической точки зрения) введенное игроком словосочетание.

Значения флагов для каждого объекта могут комбинироваться посредством оператора побитного ИЛИ ("|"). В файле advr.t определены следующие константы для флагов:

PRSFLG_ALL - объект соответствует слову "все" или его эквиваленту

PRSFLG_EXCEPT - объект исключен из списка, определяемого словом "все"

PRSFLG_IT - объект соответствует местоимению "это"

PRSFLG_THEM - объект соответствует местоимению "они"

PRSFLG_HIM - объект соответствует местоимению "он"

PRSFLG_HER - объект соответствует местоимению "она"

PRSFLG_NUM - объект является числом

PRSFLG_STR - объект представляет собой строковое значение

PRSFLG_PLURAL - объект соответствует множественному числу

PRSFLG_COUNT - объекту предшествует числительное, определяющие количество объектов (например, "пять монет")

PRSFLG_ANY - объект используется в сочетании со словом "любой"

PRSFLG_UNKNOWN - лексема содержит неизвестные (т. е. отсутствующие в словаре игры) слова

PRSFLG_ENDADJ - лексема заканчивается прилагательным

PRSFLG_TRUNC - в лексеме используется сокращенное слово

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

Для слова "все" и для местоимений (он, она, оно, они, это) функция должна вернуть пару значений, состоящую из nil вместо объекта и соответствующего значения флага. Например, если словосочетание просто состоит из слова "все", функция могла бы вернуть следующий список (считаем, что данное словосочетание находится в команде игрока на второй позиции):

  [3 nil PRSFLG_ALL]

Аналогично, если словосочетание состоит из слова "она", функция вернет:

  [3 nil PRSFLG_HER]

Конструкция "все, кроме" также требует специальной обработки. В этом случае первыми элементами возвращаемого списка были бы значения nil и PRSFLG_ALL (как для слова "все"), однако затем к этому списку необходимо было бы добавить объекты для всех слов, перечисленных в команде после "кроме", причем каждому из этих объектов должно соответствовать значение-флаг, включающее PRSFLG_EXCEPT. Например, если игрок ввел команду вида "взять все кроме книги и свечи", возвращаемое значение может иметь следующий вид:

  [7 nil PRSFLG_ALL Kniga PRSFLG_EXCEPT Svecha PRSFLG_EXCEPT]

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

  [3 nil PRSFLG_STR]

Если в словосочетании встретится слово, отсутствующее в словаре, и вы хотите, чтобы СА отработал эту ситуацию стандартным образом, то в соответствующей позиции списка следует вернуть nil и установить флаг PRSFLG_UNKNOWN:

  [4 nil PRSFLG_UNKNOWN]

Самый же простой случай - когда вам просто надо вернуть список объектов, для которых применимо словосочетание:

  [4 Kniga 0 Svecha 0]

СА также позволяет вообще опустить значение-флаг, если его использование с объектом не требуется. Если следующий за объектом элемент - это другой объект или nil, СА считает, что флаг соответствующего (т. е. предшествующего) объекта равен нулю. Таким образом, предыдущий список эквивалентен следующему:

  [4 Kniga Svecha]

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

Если функция обнаруживает, что словосочетание в команде, по всей видимости, имеется, но оно синтаксически некорректно, то функции следует вывести сообщение об ошибке и вернуть значение PNP_ERROR. Не следует программировать вывод сообщения об ошибке, если словосочетание отсутствует вовсе; вместо этого функция просто должна вернуть список, состоящий из единственного числа, равного исходному значению аргумента "current_index", сообщая тем самым, что для команды не выполнялось никакой обработки. Подчеркнем еще раз - выводить сообщение об ошибке и возвращать значение PNP_ERROR нужно только в том случае, если в команде имеется словосочетание с некорректным синтаксисом.

Если в ходе работы функции выяснится, что выполнять специальную обработку (отличную от "умолчальной") словосочетания все-таки не требуется, она просто должна вернуть значение PNP_USE_DEFAULT. Тем самым синтаксическому анализатору сообщается, что словосочетание требуется обработать стандартным образом.

Встроенный в интерпретатор TADS модуль разбора словосочетаний на данном этапе не пытается выполнять подбор объектов или устранять неопределенности. Вместо этого он просто составляет список всех объектов, к которым применимо словосочетание. Причина, по которой СА не пытается подбирать объекты на данном этапе, состоит в том, что для этого у него еще недостаточно информации. Таким образом, СА просто определяет синтаксическую структуру словосочетания и удостоверяется в том, что это словосочетание применимо хотя бы к одному объекту в игре; впоследствии, когда будет полностью закончен анализ предложения и будут известны глагол, предлоги и количество объектов, СА производит подбор объектов и устранение неопределенностей. При написании данной функции вам следует учитывать этот принцип обработки.

В большинстве случаев вам, скорее всего, не потребуется использовать parseNounPhrase() для полной замены встроенного СА. Скорее всего, эта функция будет обрабатывать какие-либо специальные случаи, а большая часть обработки будет выполняться "умолчальным" способом синтаксическим анализатором. При возникновении специального случая (соответствующие проверки закладываются в коде функции) parseNounPhrase() будет выполнять собственную обработку и возвращать соответствующий список объектов; в противном случае функция должна просто возвращать значение PNP_USE_DEFAULT, предоставляя весь дальнейший разбор встроенной системе синтаксического анализа.

В RTADS данная функция выполняет следующие действия. Прежде всего она проверяет, не является ли словосочетание словом "себя" или "себе" (как, например, во фразе "ударить себя молотком"), и, если это так, возвращает в качестве соответствующего объекта текущего актера. Кроме того, функция самостоятельно выполняет подбор объектов с использованием функции parserDictLookup в том случае, если в словосочетании после существительного следует прилагательное (либо другое существительное, которое воспринимается как прилагательное - в словосочетаниях вида "дом генерала"; подробнее об этом см. в следующем разделе). Во всех остальных случаях (а также если для словосочетания не удалось найти объектов в игре), функция возвращает PNP_USE_DEFAULT, предоставляя синтаксическому анализатору использовать алгоритм разбора по умолчанию.


Вторая фаза: разбор объектов

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

preparseCmd()

Прежде, чем синтаксический анализатор начнет подбирать объекты самостоятельно, он попытается вызвать определенную в игре функцию с названием preparseCmd(). Этой функции может и не быть - в последнем случае СА просто продолжит подбор объектов по "умолчальному" алгоритму. При вызове функции ей передается единственный аргумент - список строковых значений (в одинарных кавычках), в котором каждое значение соответствует слову в команде. Например, для команды "открыть дверь и окно" вызов функции будет иметь следующий вид:

  preparseCmd(['открыть' 'дверь' ',' 'окно'])

Обратите внимание, что вместо союза "и" в список была подставлена запятая. Союз "и" наряду с некоторыми другими словами подвергается перед вызовом функции внутреннему преобразованию:

   
   и        ,   (запятая)
   затем    .   (точка)
   все      A
   кроме    X
   это      I
   их       T
   его      M
   ее       R
   любой    Y
   оба      B
   который  N
   которые  P

(Естественно, все падежные формы и лица также учитываются - например, на "N" будут заменяться также слова "которая" и "которое"; на "I" - слова "эта", "этому" и т. п.).

Обратите внимание, что слова "который" и "которые" не всегда преобразуются, соответственно, в "N" или "P"; чаще всего они сохраняются в оригинальном виде. Единственная ситуация, когда такая замена происходит - это случай, когда СА считывает ответ игрока на запрос по устранению неопределенности (Который "стул" вы имеете в виду...). Во всех остальных случаях эти слова сохраняются в исходном виде.

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

Например, пусть игрок вводит следующую команду:

  >напечатать 'привет всем!' на нем

В этом случае СА вызовет preparseCmd() со следующим списком в качестве аргумента:

  ['напечатать' '"привет всем!"' 'на' 'M']

Определенная автором игры функция preparseCmd может возвращать одно из следующих трех значений: true, nil, либо список. Если функция возвращает true, обработка команды продолжается обычным образом. Если она возвращает nil, команда отбрасывается - СА прекращает обработку текущей команды и возвращается к началу процесса обработки, т. е. к запросу новой команды. Если функция возвращает список, этот список должен целиком состоять из строковых значений (в одинарных кавычках); в этом случае СА вернется к шагу проверки наличия актера, описанному выше, заменив исходную команду на список, возвращенный preparseCmd.

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

Кроме того, вызов функции осуществляется последовательно для каждой команды. Имеется в виду следующее: если игрок ввел "иди на север, затем возьми ящик", то СА вначале вызывает preparseCmd для команды "иди на север", затем завершает весь цикл обработки данной команды (как описано далее), и уже после этого вновь вызывает preparseCmd для следующей команды ("возьми ящик").

Функция preparseCmd может изменить команду только один раз. Если функция вернет список значений, то СА начнет обрабатывать этот список в качестве новой команды с самого начала и в результате в конце концов вновь вызовет preparseCmd для измененной команды. При повторном вызове preparseCmd "не разрешается" вновь возвращать список; если это произошло, СА выводит сообщение, говорящее о том, что произошло "зацикливание" функции preparseCmd (сообщение 402) и прекращает обработку команды.

В RTADS функция preparseCmd выполняет преобразование команд, использующих характерный для русского языка синтаксис (например, связка слов при помощи падежей, а не предлогов), в стандартный для TADS формат глагол - "прямой" объект - предлог-связка - "косвенный" объект. При этом творительный падеж (например, "ударить врага кирпичом") преобразуется в комбинацию предлога with + существительное ("ударить врага with кирпичом"), дательный падеж аналогично заменяется комбинацией предлога "to" и существительного, а родительный падеж ("портфель Васи") заменяется конструкцией с использованием специального слова of, а точнее, его русскоязычного синонима "для" ("портфель для Васи"). С точки зрения русского языка, эти конструкции выглядят неграмотно, однако синтаксический анализатор TADS распознает их "как надо", а игрок этого представления своей команды не увидит.

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

Первое слово в команде, как правило, является глаголом. Для глагола определяется падеж, который он требует от существительного (определяется атрибутом type глагольного объекта). Этот атрибут принимает следующие значения: 1 - если команда с данным глаголом может требовать творительного падежа "косвенного" объекта (например, "отпереть дверь ключом"); 2 - если может требоваться дательный падеж ("дать пирог старушке"); 3 - если может использоваться как тот, так и другой падеж; 0 - во всех остальных случаях (т. е. если в команде не может встречаться ни творительный, ни дательный падеж). Значение типа глагола сохраняется в атрибуте glpad объекта global. Позиция глагола в исходном и конечном списках сохраняется, соответственно, в локальных переменных lastverbnum и newlastverbnum.

Если по каким-либо причинам первое слово в команде - не глагол, оно просто помещается в конечный список "как есть".

Далее идет перебор остальных слов в команде (несколько упрощая, можно сказать, что тип каждого слова проверяется при помощи вызова функции parserDictLookup). Здесь возможны следующие варианты:

Когда отработаны все слова в исходном списке, процедура проверяет, был ли в команде предлог, и если был, то где он находится. Если предлог в команде был, и стоит он не сразу после глагола, осуществляется проверка, не образует ли комбинация глагола и предлога новый глагол. (Например, в команде "дать молотком по вазе"сам по себе глагол "дать" может быть интерпретирован, как объект giveVerb, а в сочетании с предлогом "по" получится объект attackVerb). Если дело обстоит именно таким образом, то проверяется список предлогов, содержащихся в атрибуте dispprep нового глагола (т. е. глагола-комбинации). Автор игры должен заранее проанализировать, какие глаголы, использующиеся в командах с "косвенными" объектами, могут менять свой смысл при добавлении предлога, определить для соответствующих глагольных объектов атрибут dispprep и занести в него предлоги, "отвечающие" за изменение смысла глагола. Например, для уже упомянутого глагола attackVerb в advr.t в этом списке определены предлоги "в" и "по". Если предлог в команде будет найден также в атрибуте dispprep, то вся фраза будет соответствующим образом перестроена (при этом предлог будет максимально приближен к глаголу - например, из команды "дать молотком по вазе" получится "дать по вазе молотком"). Кроме того, локальной переменной displaced будет присвоено значение true.

Ну, и в самом конце процедура проверит результаты обработки и на их основании примет решение о том, как действовать дальше. Результаты обработки оцениваются по значениям локальных переменных-флагов changed и displaced. Первая из них указывает на то, что в ходе обработки в команду были добавлены предлоги-связки (для родительного, дательного или творительного падежей), а вторая - что был обнаружен предлог, изменяющий смысл глагола, в связи с чем порядок слов в команде был изменен. Несложные расчеты показывают, что здесь возможны четыре случая:

Вот, собственно, и все о работе preparseCmd в RTADS. Остается лишь добавить, что передача измененной строки на повторную обработку (случаи, когда переменная displaced равна true) может происходить не более пяти раз. В противном случае функция выдает сообщение о зацикливании алгоритма русскоязычного разбора команды и возвращает nil, сообщая СА о том, что обработка команды должна быть завершена.

Идентификация глагольного объекта

После того, как preparseCmd вернет true, синтаксический анализатор проверяет, соответствует ли "глагольной группе" команды корректный глагольный объект. Если не найдется ни одного объекта, определяющего "глагольную группу" в своем лексическом свойстве verb, то выдается сообщение об ошибке (с кодом 18, "Я не понимаю это предложение."), после чего обработка команды прерывается.

Метод roomCheck

Далее СА вызовет метод roomCheck объекта, возвращаемого функцией parserGetMe(). Метод вызывается с единственным аргументом - глагольным объектом. Например, если игрок введет команду "взять книгу", то с учетом того, что глаголу "взять" соответствует глагольный объект takeVerb, вызов метода будет выглядеть так:

  parserGetMe().roomCheck(takeVerb)

Данный метод должен вернуть значение true или nil: если возвращено true, это означает, что команда может обрабатываться дальше, если же nil, то обработка прерывается. В последнем случае метод roomCheck, как правило, должен вывести сообщение о том, почему данная команда невыполнима, поскольку при возврате nil СА не будет отображать никакой информации, а просто прервет обработку команды.

Метод roomCheck предназначен для предварительной, грубой проверки, возможно ли в принципе выполнение введенной команды для данного актера. Метод вызывается перед тем, как СА подберет объекты для всех слов в команде; вызов осуществляется только один раз для каждой команды, независимо от числа "прямых" объектов, упомянутых в ней. Основное его предназначение - полный запрет тех или иных команд в определенных обстоятельствах. Например, можно "запретить" игроку брать объекты, когда он находится в темной комнате. Метод вызывается до того, как будут подобраны объекты; этим удается избежать вывода нежелательных сообщений, которые могут генерироваться в ходе процесса устранения неопределенности, и которых игрок вообще-то не должен видеть.

Поясним это на примере: предположим, в темной комнате игрок бросит книгу (это действие не запрещено), а потом попытается поднять ее. При этом в комнате, помимо брошенной, имеются и другие книги. Если команду "взять" запретить на более поздней стадии, то в ходе обработки команды игрок увидит запрос СА: "Которую "книга" Вы имеете в виду..." с перечислением всех имеющихся в комнате книг, а после выбора нужной книги его все равно обломают, заявив, что "кругом тьма". Ранний вызов метода roomCheck позволяет избежать подобных нежелательных эффектов.

"Умолчальный" метод roomCheck, определенный для класса basicMe в файле advr.t, просто возвращает метод roomCheck той комнаты, в которой находится игрок (метод roomCheck класса movableActor работает точно так же, только обращается к соответствующему методу локации актера). Метод roomCheck для класса room в advr.t просто возвращает true, а для класса darkroom (соответствующего темной комнате) - возвращает nil за исключением случаев, когда в комнате имеется источник света, или когда глагол является "темным" (его атрибут isDarkVerb равен true), т. е. соответствующее действие может выполняться в темноте. Такими "темными" глаголами являются все системные команды (например, "сохранить"), а также некоторые другие (например, ";ждать").

Обратите внимание, что СА всегда вызывает roomCheck для текущего главного персонажа, даже если команда отдана другому актеру. Скажем, СА обратится к методу parserGetMe().roomCheck и для команды "взять книгу", и для команды "Петя, возьми книгу". Причина этого состоит в назначении метода roomCheck: этот метод проверяет, может ли игрок в принципе отдать такую команду в текущий момент.


Прерывание команды: инструкции exit, exitobj и abort

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

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

Полное прерывание всех команд: инструкция abort

Инструкция abort полностью прерывает выполнение текущей команды, а также отбрасывает все прочие команды, введенные игроком в той же командной строке. Кроме того, при этом пропускаются все демоны и запалы для текущей команды, вследствие чего количество сделанных игроком ходов не увеличивается. Тем не менее, СА осуществляет вызов функций postAction и endCommand после выполнения abort.

Пусть, например, игрок ввел следующую команду:

  >открыть люк и идти на север

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

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

Прерывание одной команды: инструкция exit

Инструкция exit отменяет оставшиеся шаги по обработке текущей команды и немедленно переходит к выполнению функции postAction (если таковая определена в игре) и затем демонов и запалов. После завершения выполнения запалов и демонов СА вызывает функцию endCommand, после чего переходит к обработке следующей команды, введенной в текущей командной строке.

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

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

Прерывание команды для одного объекта: exitobj

Инструкция exitobj схожа с exit, но вместо того, чтобы переходить сразу к следующей команде, СА вызывает функцию postAction, после чего обрабатывает следующий объект текущей команды, если игрок указал более одного "прямого" объекта ("взять книгу и свечу").

Данная инструкция полезна в ситуациях, когда произошло "раннее" завершение обработки команды (в том смысле, что вам не требуется выполнять вызов всего набора методов, "положенного" для данной команды). Например, если вы завершили процесс обработки объекта методом actorAction, то использование exitobj позволит вам не выполнять вызовы всех остальных методов для данного объекта, выполнив при этом команду для всех оставшихся объектов, заданных в командной строке.


"Шаблоны" команд

Теперь СА должен определить, каким образом будет исполняться команда. Первый шаг - это идентификация глагольного объекта, которому соответствует глагол в команде. Для этого СА просто просматривает словарь игры и подбирает объект, для которого этот глагол определен в свойстве verb.

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

Глагольные шаблоны в TADS - довольно тонкий момент, поскольку эти шаблоны не определяются в глагольном объекте в явном виде; скорее, их наличие подразумевается в зависимости от того, определены ли для объекта свойства action, doAction и ioAction. Если для глагольного объекта определено свойство action, это означает неявное определение для него шаблона команды, состоящей только из глагола. Если для глагольного объекта определено свойство doAction, это автоматически подразумевает наличие шаблона для команды вида "глагол - "прямой" объект". Наконец, при существовании свойства ioAction (указывается всегда для конкретного предлога-связки) для глагольного объекта неявным образом определяется также и шаблон "глагол - "прямой" объект - заданный в ioAction предлог - "косвенный" объект".

Например, определенному в advr.t глагол "ждать" (объект waitVerb) соответствует только шаблон команды без объектов:

  waitVerb:  darkVerb
    verb = 'ж' 'ждать' 'подождать' 'жди' 'подожди'
    sdesc = "ждать"
    action(actor) =
    {
        "Прошло некоторое время...\n";
    }
  ;

Глагол "запереть" (объект lockVerb) определяет шаблоны только с "прямым", а также с "прямым" и "косвенным" объектами, но не имеет "безобъектного" шаблона:

  lockVerb: deepverb
    type=1
    verb = 'замкнуть' 'запереть' 'замкни' 'запри'
    sdesc = "замкнуть"
    ioAction(withPrep) = 'LockWith'
    doAction = 'Lock'
    prepDefault = withPrep
  ;

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

  ioAction(aboutPrep) = [disambigDobjFirst] 'TellAbout'

Флаг-модификатор [disambigDobjFirst] (на данный момент это единственный возможный флаг) указывает на то, что СА в первую очередь должен устранить неопределенность по "прямому" объекту, и только затем - по "косвенному". При стандартном порядке разбора команд такого формата СА сперва устраняет неопределенность по "косвенному" объекту, а потом по "прямому" (с известным "косвенным" объектом). Для некоторых команд такой порядок разбора неудобен, поскольку в процессе подбора "косвенного" объекта "прямой" объект будет неизвестен. Данный флаг позволяет контролировать порядок устранения неопределенности.

Чтобы выбрать тот или иной шаблон команды, СА использует те составные части команды, которые были определены ранее (см. раздел Идентификация глагола). Если единственным непустым элементом команды окажется глагол, СА выберет "безобъектный" шаблон; если для глагольного объекта определен метод action, это означает, что для него существует и шаблон "только глагол", а значит, допустимы и соответствующие команды.

Если в команде имеется глагол и "прямой" объект, но отсутствуют предлог-связка и "косвенный" объект, СА использует шаблон с одним ("прямым") объектом. Если для глагольного объекта определено свойство doAction, то для него имеется и "однообъектный" шаблон, а следовательно, он допускает и команды с одним объектом. Свойство doAction определяет метод-верификатор и метод-действие для команды: TADS присоединяет строковое значение, определяемое свойством doAction, к префиксу verDo, чтобы образовать название метода-верификатора, и к префиксу do, чтобы образовать название метода-действия. Например, для определения doAction = 'Take' метод-верификатор будет носить название verDoTake, а метод-действие - doTake.

Если команда содержит глагол "прямой" объект, предлог-связку и "косвенный" объект, СА использует "двухобъектный" шаблон. Для одного глагольного объекта может быть определено несколько шаблонов с двумя объектами, отличающихся друг от друга предлогами-связками. Шаблоны определяются свойством ioAction - для каждого свойства ioAction указывается свой предлог. Если игрок ввел команду "копать землю лопатой" (которая в RTADS будет преобразована к виду "копать землю with лопатой" - см. раздел про preparseCmd), то СА будет искать в объекте, соответствующем глаголу "копать" (digVerb) определение вида ioAction(withPrep). (Еще раз необходимо подчеркнуть, что и имя глагольного объекта (digVerb), и имя объекта-предлога (withPrep) может быть на самом деле любым - СА все равно ищет объекты не по имени, а по значению их лексических свойств (соответственно, verb и preposition). В данном примере мы ориентировались на конкретные определения в файле advr.t).

Теперь СА необходимо "подогнать" введенную команду под один из шаблонов, определенных для использующегося в ней глагола. Прежде, чем заняться этим, СА проверяет, правильно ли определен глагол: для него должен быть определен хотя бы один шаблон из описанных выше. Если глагольный объект не имеет ни одного шаблона (т. е. для него не определено ни одно из свойств action, doAction, или ioAction), СА выводит сообщение об ошибке № 23 и прекращает обработку.


Вариант 1: команда без объектов

Если введенная игроком команда состоит из одного глагола (т. е. не содержит объектов), СА проверяет, определен ли в глагольном объекте метод action; если определен, то СА выбирает "безобъектный" шаблон и в соответствии с ним обрабатывает команду.

Если введенная команда не содержит объектов, но в глагольном объекте не определен метод action, СА пытается вызвать метод doDefault глагольного объекта. В качестве аргументов при вызове передаются актер, предлог и nil (поскольку "косвенный" объект на этот момент еще неизвестен). Например, если игрок просто наберет "взять", СА выберет в качестве глагольного объекта takeVerb и, обнаружив, что для него не определен метод action, попытается подобрать объект по умолчанию, используя следующий вызов:

  takeVerb.doDefault(parserGetMe(), nil, nil)

Этот метод предназначен для того, чтобы возвращать список объектов, используемых командой по умолчанию; определение этого метода для глагольного объекта takeVerb возвращает все мобильные объекты (т. е. те, которые можно взять) в локации игрока. Если этот метод не возвращает списка, СА просто просит игрока указать ему "прямой" объект (см. далее). Если же метод вернет список объектов, СА проверит значение атрибута prepDefault для глагольного объекта; если оно не равно nil, значит, оно ссылается на объект-предлог, который и становится предлогом текущей команды.

Например, если игрок введет просто "копать", СА получает список "прямых" объектов по умолчанию, используя метод doDefault глагольного объекта digVerb, затем проверит значение атрибута digVerb.prepDefault. Предположим, prepDefault ссылается на объект withPrep. В этом случае элементы команды будут выглядеть так:

актер: главный персонаж

глагол: копать

"прямой" объект: (список, возвращенный doDefault)

предлог: with (соответствует русскоязычной связке "при помощи" либо творительному падежу)

"косвенный" объект: nil (поскольку в данный момент он еще неизвестен)

СА проверяет, существует ли определенный новыми элементами команды шаблон для данного глагола. Если нет, то СА пропускает проверку возвращенного doDefault списка, сразу запрашивая "прямой" объект у игрока.

Если в результате выполнения doDefault будет возвращен список "прямых" объектов и при этом изменившийся шаблон будет допустимым, СА просмотрит этот возвращенный список, проверяя каждый его элемент при помощи соответствующего метода-верификатора "прямого" объекта, определенного либо свойством doAction, либо соответствующим выбранному шаблону свойством ioAction глагола. Если шаблон является "однообъектным" (при этом используется doAction), СА вызывает метод-верификатор соответствующего объекта, передавая ему актера в качестве аргумента. При "двухобъектном" шаблоне (используется ioAction) СА передает методу-верификатору в качестве аргументов актера и nil вместо "косвенного" объекта (поскольку он в данный момент еще неизвестен). Если же при использовании ioAction для этого свойства определен флаг [disambigDobjFirst], то второй параметр (nil) пропускается вовсе. Вот несколько примеров:

object.verDoTake(Me) (глагол "взять")

object.verDoLockWith(Me, nil) (глагол "запереть при помощи")

object.verDoTellAbout(Me) (глагол "рассказать о")

Для третьего примера мы предполагаем, что для глагольного объекта tellVerb у свойства ioAction определен флаг [disambigDobjFirst] (хотя для стандартной библиотеки advr.t это не так).

Методы-верификаторы вызываются "втихую", т. е. все выдаваемые ими сообщения скрываются от игрока (точно так же, как это происходит при устранении неопределенности). СА, однако, фиксирует, пытались ли верификаторы выводить что-либо; напомним, что вывод сообщения в процессе выполнения верификатора означает, что объект не прошел верификацию. Все непрошедшие верификацию объекты из первоначального списка отбрасываются.

Если из возвращенного методом doDefault списка проверку верификаторами прошел один-единственный объект, этот объект выбирается в качестве "умолчального" для текущей команды. СА показывает игроку, какой объект будет использован: если в игре определена функция parseDefault(), СА вызовет ее, передав ей "умолчальный" объект в качестве аргумента, и будет считать, что она сама "позаботится" о том, чтобы вывести сообщение, какой объект был выбран в качестве "умолчального". В противном случае СА выведет сообщение с номером 130 ("("), затем вызовет метод thedesc "умолчального" объекта, а затем ошибку с кодом 131 (")"). После этого он повторно "прогоняет" данную стадию обработки с новым набором элементов команды.

Ниже приведен пример реализации функции parseDefault. В таком виде эта функция просто дублирует системное сообщение.

parseDefault: function(obj, prp)
  {
    "(";
    if (prp) "<< prp.sdesc>> ";
    obj.thedesc;
    ")";
  }

Начиная с версии TADS 2.5.8, вместо parseDefault() используется аналогичная ей функция parseDefaultExt() с расширенным списком аргументов:

parseDefaultExt: function(actor, verb, obj, prp)
  

Аргументы obj и prp соответствуют одноменным аргументам parseDefault(), actor - это объект-актер, к которому обращена команда, а verb - глагол. Если в игре определены и parseDefault(), и parseDefaultExt(), то если игра откомпилирована с использованием TADS версии 2.5.8 и выше, будет использоваться parseDefaultExt(), а если версия компилятора более ранняя, то parseDefault(). Если в игре определена только parseDefault(), именно она и будет использоваться независимо от версии компилятора.

В RTADS определены обе эти функции; они обеспечивают правильное склонение названия объекта при подборе; например, если игрок ввел команду "открыть", и в результате подбора объектов СА найдет один-единственный подходящий (скажем, в комнате имеется закрытая коробка), то именно одна из этих функций обеспечит подбор нужного падежа:

     >открыть
      (коробку)
      
      Ты откидываешь крышку коробки. Внутри ничего нет.
  

Функция parseDefaultExt() практически полностью дублирует parseDefault() за исключением того, что позволяет более точно подобрать падеж для случая, когда предлог отсутствует (аргумент obj равен nil); parseDefault() в этом случае всегда использует винительный падеж, что не вполне корректно (пример - глагол "рассказать").

В случае, если проверку не пройдет ни один объект, у СА нет возможности предложить "умолчальный" объект. Если проверку пройдет более одного объекта, СА также не сможет выбрать один из них, поскольку команда игрока не содержит никаких указаний на этот счет. В любом из этих двух случаев СА запрашивает "прямой" объект у игрока; см. далее.


Вариант 2: команда с одним объектом

Если в команде игрока присутствует "прямой" объект, но она не содержит предлога и/или "косвенного" объекта, СА проверяет глагольный объект на наличие шаблона с одним объектом (иначе говоря, определено ли для него свойство doAction). Если шаблон имеется, СА вначале пытается устранить неопределенность по "прямым" объектам (см. раздел Разбор словосочетаний). Если неопределенность устранена успешно, СА сохраняет "прямой" объект (или список "прямых" объектов, если их больше одного) таким образом, чтобы игрок мог обратиться к ним при помощи местоимений "это", "эти" (а также "он" или "она", смотря по ситуации), и продолжает выполнение команды.

Если же "однообъектного" шаблона для глагола не определено, СА проверяет значение свойства prepDefault глагольного объекта, используя его в качестве предлога для команды, и вызывает метод ioDefault для данного глагола, передавая ему в качестве аргументов текущего актера и подобранный "умолчальный" предлог. Например, если игрок введет команду "положить мяч", и для данного глагола предлогом по умолчанию будет "в", СА вызовет метод ioDefault следующим образом:

  putVerb.ioDefault(Me, inPrep)

Если данный метод вернет значение, не являющееся списком, СА проигнорирует его и запросит "косвенный" объект у игрока (см. далее). В противном случае СА проверяет, неопределенность для какого объекта ("прямого" или "косвенного") требуется устранить в первую очередь - если для свойства ioAction определен флаг [disambigDobjFirst], первым устраняется неопределенность для "прямого" объекта. Если это так и есть, СА устраняет неопределенность "прямого" объекта обычным образом (см. раздел Устранение неопределенности). Далее, СА просматривает возвращенный ioDefault список объектов и "по-тихому" вызывает для каждого из них соответствующий метод-верификатор. Чаще всего вызов имеет следующий формат:

  object.verIoPutIn(Me)

Если для свойства ioAction определен флаг [ disambigDobjFirst], формат вызова несколько меняется:

  object.verIoPutIn(Me, directObject)

Как уже говорилось ранее, данные вызовы осуществляются "втихую" - все выводимые ими сообщения скрываются от пользователя, при этом СА считает попытку вывода сообщения свидетельством того, что объект не прошел верификацию, и удаляет этот объект из первоначального списка, возвращенного ioDefault.

Если по завершении проверки в списке останется ровно один объект, СА считает его "косвенным" объектом по умолчанию и продолжает выполнение команды с этим объектом. Вначале, однако, он должен сообщить игроку, что в команде используется именно этот объект. Система тут примерно та же, что и в описанном выше случае с "прямым" объектом, при этом также может использоваться функция parseDefault. Единственное, что остается добавить - определение parseDefault должно обеспечивать возможность вызова этой функции с разным количеством аргументов: для "прямого" объекта она вызывается с одним (сам объект), а для "косвенного" - с двумя (сам объект и предлог) аргументами. Кроме того, аргумент-предлог теоретически может иметь значение nil, хотя на практике этого происходить не будет (в стандартном TADS предусмотрен механизм для обработки команд с двумя объектами без предлога - например, "дать бутылку Васе", однако в RTADS он не используется, и все подобные команды преобразуются к стандартному виду "глагол - прямой объект - предлог - косвенный объект", как было описано ранее).

Если после верификации в списке не осталось объектов совсем или осталось более одного объекта, СА не сможет подобрать "косвенный" объект по умолчанию и запросит его у игрока (см. далее).


Вариант 3: команда с двумя объектами

Если игрок ввел команду с двумя объектами, но без предлога, СА считает первый из них "косвенным", а второй - "прямым", и использует шаблон с двумя объектами. Если введена команда такого типа, СА проверяет значение свойства nilPrep глагольного объекта. Это свойство должно вернуть объект-предлог, который следует использовать по умолчанию в такой ситуации. Если свойство не возвращает объект-предлог, СА считает, что это предлог "to", и ищет объект-предлог, определяющий соответствующее лексическое свойство. В любом случае СА прежде всего преобразует команду из формата глагол - "косвенный" объект - "прямой" объект в формат глагол - "прямой" объект - предлог - "косвенный" объект и в дальнейшем обрабатывает команду так, как если бы она сразу была введена в "правильном" формате.

Если игрок введет команду в формате ГЛАГОЛ ПРЕДЛОГ ОБЪЕКТ_1 ОБЪЕКТ_2, то СА преобразует ее к виду ГЛАГОЛ ОБЪЕКТ_2 ПРЕДЛОГ ОБЪЕКТ_1, т. е. объект, идущий в команде первым, считается "прямым", а вторым - "косвенным". После преобразования СА продолжает обработку команды так, как если бы она сразу была введена в "правильном" формате. Такой формат команды используется в некоторых отличных от английского языках (но не в русском;) - примечание переводчика).

Если игрок введет команду с предлогом и одним или двумя объектами, СА ищет определение свойства ioAction, которое подходило бы для указанного в команде предлога. Если такового не обнаружится, СА выводит сообщение с кодом 24 ("Я не понимаю это предложение") и прерывает выполнение команды.

Если игрок указал в команде только один объект (указав при этом и предлог), СА считает этот объект "косвенным" и пытается подобрать "прямой" объект. Механизм точно такой же, как для варианта 1 - сперва ищется объект по умолчанию, а если такового обнаружить не удалось, выводится запрос игроку. Такое сокращение команды оправдано, если "прямой" объект ясен из контекста (или, иначе говоря, глагол предъявляет серьезные требования к "прямым" объектам, ограничивая, таким образом, их количество): например, если в комнате, кроме игрока, есть только один персонаж, то команда "спросить о преступлении" автоматически будет обращена к этому персонажу (он станет ее "прямым" объектом).

Если игрок указал два объекта, СА устраняет неопределенность по обоим. Если для свойства ioAction указан флаг [disambigDobjFirst], то сначала будет устраняться неопределенность "прямому" объекту; в противном случае - по "косвенному". Затем отрабатывается оставшийся объект. Подробнее см. в разделе Разбор словосочетаний.

СА ни при каких обстоятельствах не допускает использование в команде более одного "косвенного" объекта. В подобном случае выдается сообщение с кодом 25 ("Нельзя использовать много косвенных объектов"), и выполнение команды прерывается. Если сначала устраняется неопределенность по "прямому" объекту (определен флаг [disambigDobjFirst]), СА ограничивает также и число "прямых" объектов; если игрок укажет для такой команды более одного "прямого" объекта, СА прервет команду, выведя сообщение с кодом 28 ("Эту команду нельзя применять к множеству объектов").

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


Запрос объекта

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

Для этого СА сначала проверяет, определена ли в вашей игре соответствующая функция:

Функция parseAskobj() поддерживается только в целях совместимости со старыми играми, написанными до того, как в TADS был реализован механизм с использованием функций parseAskobjActor() и parseAskobjIndirect(). Если определить наряду с этими функциями еще и parseAskobj(), последняя будет попросту игнорироваться.

parseAskobjIndirect()

Как уже было сказано, при запросе "косвенного" объекта СА в первую очередь вызывает parseAskobjIndirect(), если таковая определена. Этой функции передаются дополнительные аргументы, описывающие словосочетание (словосочетания), которые игрок использовал для описания "прямого" объекта в своей команде, что позволяет сформировать текст запроса с использованием этой информации.

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

Функция parseAskobjIndirect вызывается с четырьмя аргументами:

  parseAskobjIndirect(actor, verb, prep, noun_info);

Аргумент "actor" указывает объект, соответствующий актеру, выполняющему данную команду; "verb" - это глагольный объект, а "prep" - объект-предлог.

Аргумент "noun_info" - это список, содержащий информацию о словосочетаниях, которые игрок использовал в своей команде. Данный список разбит на "подсписки", каждый из которых содержит информацию об одном словосочетании, использующемся в исходной команде игрока (т. е. если игрок в своей команде вводит несколько словосочетаний, отделяя их друг от друга запятыми или союзом "и", то для каждого словосочетания будет создан свой подсписок).

В свою очередь, каждый подсписок содержит три "суб-подсписка":

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

Чтобы определить, сколько различных словосочетаний ввел игрок, используйте вызов length(noun_info). Чтобы получить список строковых значений, объектов и флагов для n-ного словосочетания в команде игрока, используйте следующий код:

  strs := noun_info[n][1];
  objs := noun_info[n][2];
  flags := noun_info[n][3];

Чтобы вывести n-ное словосочетание, введенное игроком (без учета специальных преобразований введенных слов, например, превращения "А" во "все"), используйте код примерно следующего вида:

  strs := noun_info[n][1];
  for (i := 1 ; i <= length(strs) ; ++i)
    "<<strs[i]>> ";

Если вам нужно перебрать весь список объектов, соответствующих n-ному словосочетанию, это можно сделать следующим образом:

  objs := noun_info[n][2];
  for (i := 1 ; i <= length(objs) ; ++i)
  {
    if (objs[i] = nil)
      "nil ";
    else
      "<<objs[i].sdesc>> ";
  }

Обратите внимание, что элементы в объектном суб-подсписке могут быть равны nil (см. строку objs[i] = nil в предыдущем примере); они принимают это значение, если игрок использовал в своей команде слово "все", какое-либо местоимение, а также строковое ("закавыченное") значение или число.

parseAskobjActor()

Если в вашей игре определена функция с названием parseAskobjActor(), СА вызывает эту функцию, передавая ей актера в качестве первого аргумента, глагольный объект в качестве второго и предлог в качестве необязательного третьего аргумента (этот аргумент требуется только для команд с "косвенным" объектом, и даже в этом случае может быть равен nil). Эта функция предназначена для того, чтобы вывести запрос игроку на ввод "прямого" объекта для команды.

Обратите внимание, что третий аргумент передается функции parseAskobjActor() только в том случае, если происходит запрос "косвенного" объекта. Поскольку возможен вызов этой функции как с двумя, так и с тремя аргументами, она должна быть определена со списком аргументов переменной длины. Чтобы определить, с двумя или тремя аргументами вызывалась функция, можно использовать псевдопеременную argcount.

В RTADS, в файле errorru.t, определена функция parseAskobjActor(), выполняющая следующие действия. Вначале она проверяет, сколько аргументов (два или три) было передано при вызове. Для трех аргументов производится формирование первой части вопроса, состоящей из переданного предлога и местоимения "что" в соответствующем падеже (падеж определяется предлогом). Единственное исключение из данного алгоритма формирования начала запроса - когда передан предлог toPrep - при этом выводится либо "Кому" (если атрибут type переданного глагола имеет значение 2), либо "Чему" (во всех остальных случаях). Затем функция выводит "Вы хотите", если команда адресована главному персонажу и игра написана во 2-м лице, либо "<имя персонажа> должен (должна, должно, должны)", если команду должен выполнить другой персонаж в игре или повествование в игре ведется от третьего лица. За этим фрагментом выводится название переданного в качестве второго аргумента глагола. Завершающим элементом запроса будет либо "этого" (для глагола "спрашивать" - например, "О чем Вы хотите спросить этого?"), либо "этому" (для глагола "сказать" - "О чем Вася должен сказать этому?"), либо просто "это" (во всех остальных случаях - "Кому Бобик должен отдать это?").

Для двух переданных аргументов все проще: начальный фрагмент определяется свойством vopr глагольного объекта, затем формируется блок "Вы хотите <глагол>/персонаж должен <глагол>"; например, для команды "встать" последует запрос, "На что Вы хотите встать?".

parseAskobj()

Если в вашей игре не определена функция parseAskobjActor(), но при этом вы определяете функцию с названием parseAskobj(), СА будет вызывать последнюю. parseAskobj() служит для тех же целей, что и parseAskobjActor(), но ей не передается в качестве аргумента текущий актер. Эта функция поддерживается исключительно в целях совместимости с играми, созданными при помощи TADS версии 2.2 и более ранними. В современных играх (а под RTADS все игры будут современными - примечание переводчика) рекомендуется использовать parseAskobjActor(), так как ей можно передать больше информации, а следовательно, добиться более гибких эффектов.

Запрос по умолчанию

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

Если игра не определяет ни одной из вышеописанных функций, СА выводит запрос по умолчанию. Если игрок не указал в своей команде персонажа, который должен ее выполнить, СА выводит сообщение с кодом 140 ("What do you want to" (Что Вы хотите)), если же актер указан, выводится сообщение ("What do you want"), затем выводит значение свойства thedesc (которое соответствует короткому описанию с определенным артиклем) для актера команды, а потом - сообщение с кодом 149 ("to") (русский эквивалент всей этой конструкции звучит примерно как "Что данный персонаж должен"). После этого СА вызывает свойство "sdesc" (короткое описание) глагольного объекта. Затем, если запрашивается "косвенный" объект, СА выводит "правильное" местоимение для "прямого" объекта, затем обращается к свойству sdesc объекта-предлога (если предлог равен nil, выводится "to"), а затем - сообщение 143 ("?").

Если в запросе требуется местоимение для "прямого" объекта, СА проверяет каждый объект в списке "прямых" объектов. Если игрок в своей команде явным образом указал несколько "прямых" объектов (путем перечисления или используя слово "все"), либо если (для TADS версии 2.5.2 и выше) у всех подходящих с точки зрения лексики объектов свойство isThem ("отвечающее" за множественное число) имеет значение true, СА выводит сообщение с кодом 144 ("them" - их). В противном случае СА проверяет для каждого объекта значение свойств isHim (мужской род) и isHer (женский род); если каждый объект в списке возвратит для обоих этих свойств true, будет выведено сообщение с кодом 144 ("them"); если true будет возвращено всеми объектами только для isHim, то выводится сообщение 145 ("him" - его), а если только isHer, то сообщение 146 ("her" - ее); наконец, если ни для одного из этих свойств не было возвращено true всеми объектами, используется сообщение с кодом 141 ("it" - оно, это).

Считывание ответа игрока

После вывода запроса объекта (формируемого при помощи одной из вышеописанных функций либо правил по умолчанию) СА дает игроку возможность ввести новую команду, используя функцию commandPrompt() с аргументом, равным 3 при запросе "прямого" и 4 - при запросе "косвенного" объекта. После ввода команды СА вызывает функцию commandAfterRead() с тем же значением аргумента. Если СА обнаруживает, что новая команда состоит из словосочетания, СА подставляет это словосочетание вместо объекта, пропущенного в первоначальной команде, и возвращается к началу текущей фазы обработки команды. Обратите внимание, что игрок при вводе данного словосочетания может вводить все, что он мог бы ввести в исходной команде (т. е. возможен ввод списка из нескольких объектов, а также специальных слов - например, слова "все").

Если СА запрашивет "косвенный" объект, игрок может (но не обязан) начать свой ввод с предлога-связки данной команды, например:

   >копать землю
   С помощью чего Вы хотите копать это?

   >с помощью лопаты

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

Функция preparseExt()

Начиная с версии TADS 2.5.8, текст, введенный игроком в ответ на запрос игры и/или сообщение об ошибке вида "Я не знаю слова...", передается на обработку функции preparseExt(), если таковая определена в игровой программе. Функция имеет следующий формат:

preparseExt(actor, verb, str, typ)

actor и verb - это, соответственно, объект-актер и глагол для текущей команды. str соответствует текстовой строке, введенной игроком, а typ - это тип запроса, на который отвечает игрок; этот аргумент полностью соответствует тем значениям, которые передаются функциям commandPrompt() и commandAfterRead(). Функция может вернуть строковое значение - в этом случае это значение будет использоваться вместо первоначально введенной игроком строки; true - в этом случае будет использован тот текст, который ввел игрок; наконец, может быть возвращено значение nil - при этом введенный текст будет интерпретироваться как совершенно новая команда, и никаких дополнительных попыток по его интерпретации в качестве ответа на запрос игры предприниматься не будет.

В RTADS данная функция в основном выполняет те же действия, что и preparse(); дополнительно она обеспечивает распознавание исправлений с помощью команды oops. Для этого она проверяет первые три символа введенного игроком текста и, если команда начинается с "ой ", заменяет их на "oops".


Разбор словосочетаний

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

Строковые и числовые значения

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

  >набрать "Всем привет!" на клавиатуре

Всем строкам ставится в соответствие специальный объект strObj, который должен быть определен в вашей игровой программе (в файле advr.t имеется класс basicStrObj, который можно использовать в качестве родительского (причем в большинстве случаев без каких-либо изменений) для вашего строкового объекта). В вышеприведенном примере СА выбирает strObj в качестве "прямого" объекта команды и присваивает свойству value этого объекта строковое значение 'Всем привет!' (обратите внимание на замену двойных кавычек в исходной команде на одинарные).

Точно таким же образом игрок может вводить числа:

  >набрать 911 на телефоне

Числам ставится в соответствие специальный объект numObj, также определяемый в игровой программе. Принцип тут такой же, как и для строковых значений; в библиотеке advr.t имеется класс basicNumObj, который может быть использован в качестве родительского для объекта-числа.

Местоимения

Специальные слова-местоимения (это, эти, его, ее и т. д.) ссылаются на "прямой" объект предыдущей команды. (Точнее говоря, автор игры может указать самостоятельно, к какому объекту должны будут относиться эти местоимения, используя встроенную функцию setit(), однако по умолчанию система будет автоматически ставить в соответствие этим местоимениям "прямой" объект последней введенной команды). Прежде, чем применить местоимение, СА проверяет доступность объекта (для "прямого" объекта это будет метод validDo, для "косвенного" - validIo), чтобы удостовериться в том, что к объекту все еще можно обратиться. (Пример, иллюстрирующий, зачем нужна такая проверка: у игрока имеется в инвентаре спичка, он дает команду "зажечь спичку", и на следующем ходе спичка сгорает. Понятно, что команда, скажем, "бросить ее", где местоимение "ее" будет относиться к спичке, утратит смысл).

"Все"

Специальное слово "все" (и его синонимы) заменяется списком объектов, возвращаемым свойством doDefault глагольного объекта. СА вызывает это свойство и подставляет в команду в качестве "прямых" все объекты из возвращаемого этим свойством списка. Если некоторые объекты исключаются с использованием другого специального слова - "кроме" (например, "взять все кроме бутылки и воблы"), СА удаляет исключенные объекты из списка, возвращенного doDefault.

Прочие словосочетания

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

Проверка доступности/пригодности:
Прежде всего, мы выделяем те объекты, которые доступны. Для этого СА вызывает свойство validDoList (или validIoList, если подбираемый объект - "косвенный") глагольного объекта. При вызове методам передаются актер, предлог и второй объект, задействованный в команде; предлог может быть равен nil в случае команды с одним объектом, а второй объект может быть равен nil, если он еще не подобран либо если его нет в команде (при вызове validDoList для команд с одним объектом аргумент, соответствующий второму объекту, всегда будет равен nil). Данные методы возвращают либо список объектов, либо nil; если возвращен список объектов, то он сравнивается со списком объектов, которые подходят к данному словосочетанию с точки зрения лексических свойств, и СА оставляет только те объекты, которые присутствуют в обоих списках. Если метод вернул nil, СА сохраняет первоначальный список объектов, подходящих по лексическим свойствам. Обратите внимание, что методы validDoList и validIoList возвращают все объекты, являющиеся пригодными, но не только их - в связи с тем, что результирующий список объектов будет далее проверяться на предмет пригодности методом validDo или validIo, не будет никакого криминала, если среди пригодных объектов будет возвращено некоторое количество непригодных.

Далее осуществляется вызов метода validDo (или validIo) глагола для всех объектов, прошедших предыдущую проверку. В качестве аргументов этим методам передаются актер, объект, пригодность которого проверяется, а также "порядковый номер". Порядковые номера начинаются с единицы и увеличиваются на 1 для каждого следующего объекта в списке. (Порядковый номер можно использовать, например, в том случае, если вы хотите произвольным образом выбрать один объект из списка - просто верните true, скажем, для первого объекта в списке и nil для всех остальных). Как вы, наверное, уже поняли, если данные методы возвращают true, это означает, что объект является пригодным для данной команды - т. е. он доступен игроку с точки зрения текущей команды. Если, например, игрок пытается взять объект, то этот объект должен быть пригодным тогда и только тогда, когда он достижим для игрока. Для некоторых других команд достижимость не является обязательным условием пригодности объекта - примером являются команды "осмотреть", где критерием пригодности будет видимость объекта, либо "спросить о" (для "косвенного" объекта), для которой пригодным будет, вообще говоря, любой объект в игре. Как бы там ни было - СА исключает из рассмотрения те объекты, для которых метод validDo/validIo вернет nil.

Верификация:
Теперь мы проверяем каждый "уцелевший" объект на предмет его логичности для данной команды. С этой целью мы используем "тихий" вызов метода-верификатора. Если вы помните, для каждого из шаблонов команд с объектами, описанных ранее, определена пара специальных методов - метод-верификатор и метод-действие - которые будут вызываться в "прямом" и "косвенном" объекте. Например, глагол "взять" может содержать определение doAction = 'Take', которое означает, что "прямой" объект должен определять (или наследовать) метод-верификатор verDoTake и метод-действие doTake. Вызов верификатора на данном этапе называется "тихим", поскольку все сообщения, выводимые этим методом, будут перехватываться и скрываться от игрока. В то же время СА обращает внимание на то, пытался ли метод что-нибудь вывести, или нет; если такая попытка предпринималась, объект считается не прошедшим верификацию. В результате у нас должен получиться список объектов, которые прошли как проверку пригодности, так и верификацию.

Таким образом, у нас получится два списка: список пригодных объектов (прошедших тест на пригодность) и список логичных с точки зрения текущей команды объектов (прошедших верификацию).

Подходящих объектов нет:
Если оба списка пусты, то применить глагол к введенному игроком словосочетанию нет возможности - мы не смогли найти объект, пригодный для нашей команды. В этом случае проверяем, можно ли применить команду к каким-либо видимым объектам; если нет, то просто выводится сообщение с кодом 9 ("Я не вижу здесь объект "%s".", где "%s" заменяется на введенное игроком словосочетание), и обработка команды прерывается. В противном случае у нас следующая ситуация: игрок обратился к одному или нескольким объектам, которые видны, но не пригодны с точки зрения текущей команды. В этом случае проверяем, определен ли для глагольного объекта метод cantReach; если определен, то этот метод вызывается с четырьмя аргументами - актером, списком недостижимых "прямых" объектов, списком недостижимых "косвенных" объектов и предлогом. В реальности только один из вышеперечисленных списков будет непустым - вместо второго будет передаваться nil; например, если идет подбор "прямого" объекта, то и передаваться будет только список "прямых" объектов, а список "косвенных" объектов всегда будет пустым. При подборе "косвенного" объекта все будет ровно наоборот. Аргумент-предлог будет равен nil, если команда соответствует однообъектному шаблону.

Например, если в закрытом стеклянном ящике имеются красная книга и синяя книга, а игрок вводит команду "взять книгу", слово "книга" можно будет отнести к любой из двух книг в ящике, однако ни одна из них не будет пригодна для команды "взять". СА вызовет метод cantReach глагола "взять" следующим образом:

  takeVerb.cantReach(parserGetMe(), [krasnayaKniga sinyayaKniga], nil, nil)

Если объект-глагол не определяет и не наследует метод cantReach, СА использует иной механизм. Для каждого видимого объекта (при условии, что в списке более одного такого объекта) СА вначале вызывает свойство multisdesc, если оно определено, или свойство sdesc в противном случае, после чего выводит сообщение с кодом 120 (двоеточие). В результате получится ровно тот же вступительный текст, который СА выводит при обработке команды, обращенной сразу к нескольким объектам (подробнее см. в разделе Обработка нескольких объектов). Затем СА вызывает метод cantReach, определенный в самом объекте (не в глаголе) с единственным аргументом-актером. СА повторяет эти действия для каждого объекта в списке. (Механизм с использованием метода cantReach, определенным в глаголе, появился позднее, чем механизм с вызовом cantReach для каждого объекта - именно поэтому СА пытается использовать его в первую очередь).

Имеются как пригодные объекты, так и объекты, прошедшие верификацию:
Если ни один из объектов, признанных пригодными, не прошел верификацию, СА при дальнейшей обработке ориентируется на список пригодных объектов. Если оба списка непустые, СА использует только список объектов, прошедших верификацию.

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

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

Примечание переводчика: предыдущий абзац скорее применим для англоязычных игр; в русском языке, к сожалению, не все так радужно. Дело в том, что у нас гораздо чаще, чем в английском, возможны случаи, когда форма множественного числа совпадает с одной из падежных форм единственного числа (например, "книги" может быть как множественным числом, так и родительным падежом единственного числа); собственно, такое совпадение является в русском нормой. Падежные формы единственного числа прописываются в свойстве noun, которое при обработке имеет приоритет перед свойством plural (проверено экспериментально;). Поэтому в вышеприведенном примере, даже если определить слово "книги" в свойстве plural для всех объектов-книг, СА все равно попросит указать только один объект ("Которое "книги" Вы имеете в виду:..."). Единственным полноценным решением было бы, вероятно, полное перелопачивание алгоритма синтаксической обработки с целью реализации полноценной поддержки падежей (и в т. ч., например, отслеживанием падежа, требуемого глаголом). Мало того, что это потребовало бы колоссальных трудозатрат от автора стандартных библиотек, это неминуемо усложнило бы работу и автора игр, при этом удобство для игрока повысилось бы незначительно, а кое в чем игра стала бы даже менее удобной (например, при существующем положении вещей команды с "перепутанными" падежами (типа "взять книга") обрабатываются совершенно нормально, а при реализации полноценной поддержки падежей СА, скорее всего, перестал бы ее понимать). Поэтому наиболее безболезненный выход из данной ситуации - просто относиться к невозможности указывать множественное число не как к багу, а как к фиче;), и по возможности стараться избегать возникновения подобных ситуаций в игре (собственно, они на самом деле возникают не слишком часто, если только не создавать их специально).

Точно так же, если словосочетание содержало слово "любой" (например, "взять любую книгу"), это означает, что игрок в явной форме попросил нас случайным образом выбать один из подходящих объектов. СА при этом просто выбирает первый объект из своего сформированного внутреннего списка (порядок, в котором объекты будут занесены в этот внутренний список, непредсказуем).

Кроме того, если объекты являются неотличимыми друг от друга, СА также выбирает один из них случайным образом (точно так же, как в случае использования игроком слова "любой"). Объекты являются неразличимыми при выполнении следующих двух условий: во-первых, они должны быть наследниками строго одного и того же класса - т. е. непосредственный родительский класс для всех объектов должен быть один и тот же; и, во-вторых, для всех объектов должен быть определен атрибут isEquivalent, равный true.

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

Справочная таблица по определению требуемого количества объектов

Если имеется словосочетание такого типа... ...то будут выбраны такие объекты:
Единственное число ("журнал") Требуется один объект, и нам необходимо точно определить, какой именно игрок имел в виду
Множественное число (“журналы”) Все подходящие объекты
"любой" ("любая монета") Любой из подходящих объектов, выбирается произвольным образом
Указание количества в явном виде N ("5 монет") Любые N из подходящих объектов, выбираются произвольным образом
Неразличимые объекты (все подходящие объекты имеют атрибут isEquivalent = true и общий родительский класс) Любой из подходящих объектов, выбирается произвольным образом

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

В первую очередь СА пытается обратиться к методу disambigDobj (для "прямых" объектов) или disambigIobj (при разборе "косвенных" объектов), определенному в объекте-глаголе текущей команды. Вызов метода осуществляется в следующем формате:

  verb.disambigDobj(actor, prep, iobj, verprop, 
                    wordlist, objlist, flaglist,
                    numberWanted, isAmbiguous, silent);

Обратите внимание, что если этот метод определен, СА вызывает его каждый раз при подборе объектов для словосочетания, вне зависимости от того, является ли словосочетание неоднозначным (иначе говоря, требует ли оно устранения неопределенности). Это позволяет автору игры предусмотреть особые алгоритмы подбора для определенных объектов и глаголов, даже для случаев, когда по умолчанию СА не стал бы устранять неопределенность. В то же время СА не вызывает этот метод при разборе словосочетаний с использованием слова "все", а также если словосочетание состоит целиком из числа (если только число не указано прописью и не совпадает с лексическим свойством каких-либо объектов в игре, доступных для текущей команды - в этом случае это число рассматривается как обычное словосочетание), строкового значения или местоимения.

Аргумент actor - это актер, задействованный в команде.

prep соответствует объекту-предлогу, использующемуся в качестве связки для "косвенного" объекта; если в команде отсутствует такой предлог, этот аргумент будет равен nil.

iobj соответствует "косвенному" объекту, если таковой имеется; если шаблоном команды он не предусмотрен, либо подбор этого объекта не завершен на момент вызова disambigDobj, этот аргумент будет равен nil.

verprop определяет наименование метода-верификатора (verDoГлагол) "прямого" объекта для данного глагола; этот аргумент необходимо также передавать при вызове, поскольку один и тот же глагол может использовать разные верификаторы для разных предлогов (например, команды "положить x на y" и "положить x в y" по умолчанию используют разные методы-верификаторы).

wordlist - это список строковых значений, соответствующих лексемам, составляющим разбираемое словосочетание команды игрока. Этот аргумент полностью соответствует аналогичному списку, передаваемому функции preparseCmd.

objlist - это список тех объектов, которые СА подобрал, ориентируясь на их лексические свойства.

flaglist представляет собой список числовых значений, каждое из которых определяет набор флагов для соответствующего по порядковому номеру элемента objlist (например, значение flaglist[3] будет соответствовать элементу objlist[3]).

Значения флагов для списка flaglist определены в advr.t. Флаги анализируются побитно, т. е. в одном числовом значении могут быть объединены несколько флагов. Чтобы проверить, установлен ли тот или иной флаг, используйте побитный оператор AND (И, обозначается символом &). Например, чтобы проверить, установлен ли флаг множественного числа (PLURAL) для второго объекта в списке, можно использовать следующее выражение:

  ((flaglist[2] & DISAMBIG_PLURAL) != 0) 

Используются следующие флаги:

PRSFLG_COUNT - указывает на то, что объект подходит для словосочетания с количественным числительным. Например, если введено словосочетание "3 золотые монеты" и список objlist содержит один или несколько объектов, подобранных по словосочетанию во множественном числе "золотые монеты", то для каждого объекта будет установлен флаг PRSFLG_COUNT, отмечающий, что словосочетание содержало числительное. В подобных случаях первый элемент списка wordlist всегда должен быть строковым представлением введенного игроком числительного (в нашем примере '3').

PRSFLG_PLURAL - объект подобран по словосочетанию во множественном числе.

PRSFLG_ANY - словосочетание начиналось со слова "любой".

PRSFLG_ENDADJ - объект подобран по прилагательному без существительного. Пусть, например, в игре имеется сидящий в клетке хомяк; хомяку соответствует объект Hamster, для которого определено лексическое свойство noun = 'хомяк' (и все падежные формы), а клетке - объект HamsterCage, для которого в качестве прилагательного определено adjective = 'хомяка' 'хомяка#r' (чтобы можно было использовать конструкцию вида "клетка хомяка"). Пусть игрок ввел команду "взять хомяка"; в этом случае список objlist будет содержать объекты Hamster и HamsterCage, при этом для HamsterCage будет установлен флаг PRSFLG_ENDADJ. Использование этого флага позволяет при устранении неопределенности избежать выдачи "тупого" запроса, который будет раздражать игрока (очевидно, что в нашем примере игрок имел в виду именно хомяка, и запрос типа "Который "хомяка" Вы имели в виду - ангорского хомяка или хомячью клетку?" будет совершенно неуместным).

PRSFLG_TRUNC - объект подобран с учетом сокращения. Имеется в виду следующее: если для объекта определено длинное лексическое свойство (более 6-ти букв), СА позволяет игроку сокращать ввод, распознавая такое слово в случае, если игрок ввел не менее 6-ти первых букв (например, вместо "рефрижератор" игрок может ввести "рефриж" или "рефриже"). Однако при этом СА установит флаг PRSFLG_TRUNC, указывая на то, что полного соответствия между вводом игрока и лексическими свойствами объекта все-таки нет.

numberWanted - это количество объектов, которое, с точки зрения СА, должно остаться в списке для того, чтобы считать процесс подбора законченным. Для словосочетания в единственном числе ("взять мяч") это будет 1. Для словосочетания во множественном числе ("бросить монеты") данный аргумент будет равен количеству объектов в первоначальном списке (objlist). Если игрок указывает в команде число объектов ("взять 3 коробки"), этот аргумент будет соответствовать указанному числу. Для словосочетания с использованием слова "любой" ("открыть любую дверь") numberWanted будет равен 1. При обработке команды необязательно возвращать строго то число объектов, которое соответствует этому аргументу, однако в ряде случаев данная информация может оказаться полезной.

Аргумент isAmbiguous равен true, если СА считает, что введенное словосочетание неоднозначно, в противном случае он равен nil. Например, если игрок указал словосочетание в единственном числе, для которого СА подобрал два объекта, то аргумент isAmbiguous будет равен true. Конечно, определить однозначность или неопределенность словосочетания можно, проверяя значение аргумента numberWanted и флаги объектов, однако это довольно неудобно. Использовать аргумент isAmbiguous намного проще. Если вы определяете метод disambigDobj или disambigIobj исключительно для устранения неопределенности и не добиваетесь каких-либо спецэффектов, то можете просто проверять этот аргумент на входе и сразу возвращать значение DISAMBIG_CONTINUE, если он равен nil.

Аргумент silent указывает, будет ли процесс устранения неопределенностей интерактивным или нет. Если этот аргумент равен true, то метод не должен выводить сообщений и/или запросов игроку. Если же он равен nil, то вывод сообщений и запросов допустим.

Метод disambigIobj в целом имеет тот же формат вызова:

  verb.disambigIobj(actor, prep, dobj, verprop,
                    wordlist, objlist, flaglist,
                    numberWanted, isAmbiguous, silent);

Единственным отличием от disambigDobj является то, что вместо "косвенного" объекта ему передается "прямой" объект (аргумент dobj). Обратите внимание, что в большинстве случаев "прямой" объект на момент подбора "косвенного" объекта еще неизвестен, и этот параметр будет иметь значение nil. Единственным исключением из данного правила будет случай, когда для глагола определен флаг-модификатор [disambigDobjFirst].

Встроенный алгоритм устранения неопределенностей синтаксического анализатора вызывает эти методы после того, как попытается сделать все возможное для самостоятельного однозначного подбора объекта, и перед тем, как обратиться к игроку. Таким образом, до вызова этих методов СА выполнит все обычные проверки пригодности объектов (validDo, validDoList, и т. д.), а также их "тихую" верификацию (verDoГлагол, verIoГлагол), исключив все объекты, не прошедшие эти проверки. Только в том случае, если этих проверок окажется недостаточно для подбора объектов, СА обратится к методам disambigDobj or disambigIobj, если они определены для данного глагольного объекта.

Эти методы возвращают один из нижеприведенных кодов состояния либо список значений (см. далее). Возвращаемые коды состояния:

DISAMBIG_CONTINUE - продолжить процесс устранения неопределенности обычным образом.

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

DISAMBIG_PROMPTED - продолжить обработку обычным образом, но не выводить запрос игроку ("который "предмет" Вы имеете в виду..."), поскольку метод disambigDobj или disambigIobj уже вывел соответствующее сообщение. Это позволяет автору игры определять в этих методах собственный формат запроса (используя большее количество информации, чем может использовать функция parseDisambig), но при этом не отказываться от использования встроенных средств интерпретации ответа игрока и синтаксического разбора.

DISAMBIG_PARSE_RESP - указывает, что метод вывел запрос игроку и считал его ответ (например, с использованием встроенной функции input()), но при этом вы хотите использовать обычный (встроенный) механизм синтаксического анализа для интерпретации этого ответа. Для корректной обработки метод не должен возвращать этот код сам по себе; вместо этого должен возвращаться список, первым элементом которого будет данный код, а вторым - строковое значение, которое необходимо будет обработать.

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

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

При необходимости вы можете указать флаговое значение для каждого из объектов (хотя и не обязательно). Чтобы сделать это, просто разместите флаг в возвращаемом списке сразу за соответствующим ему объектом, например:

  return [DISAMBIG_DONE KrasnayaKorobka PRSFLG_PLURAL SinyayaKorobka PRSFLG_PLURAL];

Т. е. флаговое значение "привязывается" к тому объекту, за которым оно следует.

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

  [DISAMBIG_CONTINUE KrasnayaKorobka 0 SinyayaKorobka 0]
  [DISAMBIG_CONTINUE KrasnayaKorobka SinyayaKorobka]

Указывать код возврата для списка также необязательно; если его пропустить, СА будет по умолчанию считать код возврата равным DISAMBIG_CONTINUE; при этом исходный список будет заменен возвращенным, после чего процедура устранения неопределенности будет продолжена.

Обратите внимание, что в некоторых случаях СА не будет вызывать методы disambigDobj или disambigIobj: в частности, при разборе местоимений ("он", "она", "они", "это"); строковых значений; чисел, которым не соответствуют лексические свойства (т. е. которым соответствует объект numObj); а также словосочетаний с использованием слова "все". В то же время СА вызывает эти методы для словосочетаний, содержащих специальное слово "кроме".

Запрос игроку:
СА выполняет все рассмотренные выше шаги, чтобы найти единственный объект, который соответствует введенному игроком словосочетанию: проверку пригодности, верификацию, вызов определенных в игре методов disambigDobj или disambigIobj. Если после всего этого СА так и не подберет уникального объекта, ему остается последнее средство: обратиться за помощью к игроку, попросив его выбрать объект.

Для этого СА вначале проверяет, определена ли в игровой программе функция с названием parseDisambig. Если да, то эта функция вызывается с двумя аргументами: строковым (в одинарных кавычках) значением, соответствующим введенному игроком словосочетанию; и списком объектов, "переживших" все проверки. Если такая функция в игре не определена, СА выводит сообщение по умолчанию (с кодом 101 ("Который "%s" вы имеете в виду", где "%s" заменяется на текст, введенный игроком), после чего вызывает свойство thedesc для каждого из оставшихся объектов, а затем выводит сообщение с кодом 104 (вопросительный знак). Перечисляемые объекты отделяются друг от друга сообщением с кодом 102 (запятая), а после предпоследнего объекта в списке выводит сообщение с кодом 103 ("или").

В RTADS функция parseDisambig определена (в файле errorru.t). Ниже в качестве примера приведен код этой функции:

parseDisambig: function(str, lst, ...) 
{
   local i, tot, cnt;
   "Котор";
   if (lst[1].isactor) ok(lst[1],'ых','ого','ое','ую'); else
    if (lst[1].isThem) "ые"; else
     if (lst[1].gender=1) "ый"; else
      if (lst[1].gender=2) "ую"; else "ое";
   " \"<< str >>\" Вы имеете в виду: ";
   for (i := 1, cnt := length(lst) ; i <= cnt ; ++i)
   {
      if (dToS(lst[i],&vdesc)!=str) lst[i].vdesc; else lst[i].disamvdesc;
      if (i < cnt - 1) ", ";
      if (i + 1 = cnt) " или ";
   }
   "?";
}

После вывода запроса СА ждет ответа от пользователя (при этом используя функции commandPrompt и commandAfterRead, вызываемые с кодом 2). Если игрок вводит что-либо, похожее на словосочетание, СА пытается использовать полученную информацию для выбора одного из оставшихся объектов. Если игрок введет "все" или "оба", СА выберет все оставшиеся объекты, а если "любой", то один из объектов будет выбран случайным образом. В любом случае процесс подбора объектов на этом будет считаться завершенным, так как СА удастся определить объекты, которые игрок хотел использовать в своей команде.

Если игрок введет прилагательные и/или существительные, СА отберет из списка оставшихся объектов те, к которым можно отнести все заново введенные лексические свойства. Если таковых не обнаружится совсем, СА выведет сообщение с кодом 16 ("Я не вижу здесь этого."). Если останется ровно один объект, это означает, что процесс устранения неопределенности успешно завершился, поскольку СА удалось подобрать объект для словосочетания. Точно так же, если игрок в ответ на запрос СА ввел словосочетание, относящееся к нескольким объектам (например, "красный и синий"), и в списке объектов после дополнительного отбора останется ровно столько объектов, сколько содержится во вновь введенном словосочетании, процесс подбора считается успешно завершенным. В других случаях команда по-прежнему считается неоднозначной, и СА возвращается на шаг назад и вновь выводит запрос игроку. Обратите внимание, что если функция parseDisambig в игре не определена, СА выведет перед повторным запросом сообщение с кодом 100 - "Let's try it again:" ("Давайте попробуем еще раз:"). Впрочем, в RTADS эта функция по умолчанию определена, так что никаких отличий между исходным и повторным запросами не будет.

Следует также учесть, что СА вновь вызовет методы disambigDobj или disambigIobj (если они определены) после того, как ограничит в соответствии с новым вводом игрока список оставшихся объектов.

Если в ответ на запрос СА игрок введет что-либо, никак не напоминающее словосочетание, СА считает, что это новая команда - при этом обработка текущей команды будет прервана, и СА начнет разбор введенного текста с самого начала.


Разбор словосочетаний с неизвестными словами: parseUnknownXobj

Если команда игрока содержит неизвестное слово на позиции, где СА ожидает встретить словосочетание, СА пытается вызвать определенный в игре метод, чтобы "расшифровать" это слово.

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

Обратите внимание, что СА рассматривает любое неизвестное слово, встретившееся в словосочетании, как совершенно нейтральную часть речи, т. е. неизвестное слово "привязывается" к соседним известным существительным и прилагательным, образуя общее с ними словосочетание. Это связано с тем, что СА, не зная слова, не может решить, рассматривать его как существительное или как прилагательное, и просто считает его нейтральной частью речи, свободно комбинируемой с любыми другими словами.

Разбор словосочетаний протекает обычным образом до тех пор, пока СА вновь не встретит одно из неизвестных слов. Таким образом, СА вызывает методы для проверки пригодности (validDo, validDoList) и верификаторы (verDoГлагол) для всех словосочетаний, состоящих из известных слов, как обычно.

Однако, встретив неизвестное слово в ходе разбора словосочетания, СА проверяет, определен ли в игровой программе для глагольного объекта текущей команды метод parseUnknownDobj (для "прямого" объекта) или parseUnknownIobj (для "косвенного" объекта). Если соответствующий метод не определен, СА выводит обычное сообщение об ошибке (с кодом 2) и прерывает команду. Если же такой метод имеется, СА вызывает его следующим образом:

  parseUnknownDobj(actor, prep, iobj, wordlist)
  

или

  parseUnknownIobj(actor, prep, dobj, wordlist) 

В обоих случаях аргумент actor - это объект-актер, которому адресована команда; prep - это предлог-связка для "косвенного" объекта, либо nil, если в команде нет предлога; wordlist - это список строковых (заключенных в одинарные кавычки) значений, содержащий все - известные и неизвестные - слова, составляющие разбираемое словосочетание.

Аргумент iobj (для метода parseUnknownDobj) соответствует "косвенному" объекту, если таковой имеется и подобран; dobj - это, соответственно, "прямой" объект для метода parseUnknownIobj. Если тот или иной объект в команде отсутствует или для него еще не завершен процесс устранения неопределенности, то соответствующий ему объект dobj или iobj будет равен nil.

Данные методы могут возвращать следующие значения:

nil - указывает на то, что методу не удалось успешно закончить подбор объекта, и СА должен использовать умолчальную обработку - соответственно, будет просто выведено сообщение о неизвестном слове.

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

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

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

Если ваша игра не определяет для данного глагольного объекта методов parseUnknownDobj и/или parseUnknownIobj, СА просто выводит сообщение о неизвестном слове и прекращает обработку.

Ниже приведен пример по изменению команды СПРОСИТЬ О таким образом, чтобы спрошенный персонаж всегда отвечал на вопрос, даже если игрок использовал слово, отсутствующее в словаре игры. С этой целью для глагольного объекта askVerb определяем метод parseUnknownIobj. Этот метод будет возвращать специальный объект unknownAskIobj, который используется исключительно в качестве объекта-заглушки, позволяющего СА продолжить обработку команды "спросить о" даже при использовании неизвестных игре слов. Для этого объекта мы определим свойство wordlist, которое должно будет содержать список неизвестных слов; объект будет использовать эти слова, чтобы сформировать свое краткое описание (свойство sdesc).

  #pragma C-

  /*
   *   Специальный объект, используемый в качестве "косвенного" для команды 
   *   "СПРОСИТЬ О", если в ней имеются неизвестные слова
   */
  unknownAskIobj: thing
      wordlist = []
      sdesc =
      {
          local i;

          for (i := 1 ; i <= length(wordlist) ; ++i)
          {
              if (i != 1)
                  " ";
              say(self.wordlist[i]);
          }
      }
  ;

  /*
   *   Используем для команды "спросить о" специальный алгоритм обработки, 
   *   позволяющий спрашиваемому персонажу реагировать "впопад" на неизвестные слова. 
   */
  modify askVerb
      parseUnknownIobj(actor, prep, dobj, words) =
      {
          /* если мы спрашиваем О чем-либо, пусть персонаж отвечает */
          if (prep = aboutPrep)
          {
              /* Используем наш специальный объект для неизвестных слов */
              unknownAskIobj.wordlist := words;
              return unknownAskIobj;
          }
          else
          {
              /* 
               *   У нас другой предлог (т. е. это не команда "спросить о"; возвращаем nil,
               *   т. е. используем обработку по умолчанию
               */
              return nil;
          }
      }
  ; 


Третья фаза: исполнение

На этой стадии, если мы до нее дошли, о команде известно все необходимое: СА подобрал для введенных игроком слов набор объектов, каждый из которых имеет свое назначение в команде (актер, глагол, "прямой" объект, предлог, "косвенный" объект). СА также определил шаблон команды, а следовательно, и набор методов, которые будут вызываться для того или иного объекта в определенные моменты обработки. Итак, повторимся - все необходимые исходные данные у нас есть, и можно приступать к обработке команды.


команда "Again"

Прежде, чем начать исполнение команды, СА проверяет, не является ли глагольный объект глаголом "again" ("повтор"), который требует специальной обработки. Для реализации возможности повтора последней команды в игровой программе RTADS требуется определить объект с названием againVerb, указав для него в лексических свойствах слова для вызова этой команды. Обнаружив, что глагольным объектом текущей команды является объект againVerb, все дальнейшие необходимые действия по повторному вызову последней команды СА сможет выполнить самостоятельно.

Если игрок набирает команду "повтор" самой первой в игре, СА выводит сообщение с кодом 26 ("Нет команды для повторения") и завершает обработку. В противном случае СА берет информацию предыдущей команды (актер, глагол, "прямой" объект, предлог, "косвенный" объект) и использует эти объекты для новой. Перед тем, как начать обработку, СА вызывает при необходимости методы validDo и validIo глагольного объекта, чтобы убедиться в том, что "прямой" и "косвенный" объекты по-прежнему являются пригодными. Если хотя бы одна из этих проверок дает отрицательный результат, СА выводит сообщение с кодом 27 ("Эту команду нельзя повторить") и прерывает обработку.

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


Начало выполнения: preCommand

Синтаксический анализатор начинает выполнение команды с того, что дает возможность самой игровой программе "ознакомиться" с этой командой, включая весь список подобранных "прямых" объектов. Это достигается путем вызова функции preCommand(), определяемой в игровой программе. Формат вызова имеет следующий вид:

  preCommand(actor, verb, dobj_list, prep, iobj);

Аргумент dobj_list - это список, каждый элемент которого соответствует одному "прямому" объекту, подобранному для команды; эти элементы расположены в том же порядке, в котором соответствующие объекты были расположены в команде (в этом же порядке СА будет производить выполнение команды). Аргумент actor соответствует актеру, verb - глагольному объекту, prep - предлогу-связке, а iobj - "косвенному" объекту (последние два аргумента будут равны nil, если "косвенного" объекта в команде нет).

По умолчанию, после завершения работы данной функции СА продолжает обработку команды; если в функции встречается инструкция exit, то после нее обработка прерывается; если же в ней имеется инструкция abort, команда отбрасывается. В последнем случае функция endCommand() вызывается, но демоны и запалы не выполняются.

Обратите внимание, что использование инструкции exit в preCommand() несколько отличается от ее использования в других местах: в данном случае в результате выполнения этой инструкции будут пропущены все "прямые" объекты в команде. Связано это с тем, что preCommand работает со всем списком "прямых" объектов, а не с каждым из них в отдельности.

При использовании инструкций exit или abort в функции preCommand() функция postAction() не вызывается. Это связано с тем, что команда прерывается до того, как СА начинает обработку отдельных "прямых" объектов, а postAction() как раз и вызывается для каждого такого объекта в отдельности.

Одним из применений preCommand() может быть объединение двух или более "прямых" объектов из исходного списка в один. С этой целью вы можете заново построить список "прямых" объектов, после чего использовать функцию execCommand() для обработки модифицированного списка, а затем завершить обработку с помощью инструкции exit.

В RTADS данная функция не используется.


Обработка нескольких объектов

Как известно, в команде может быть задействовано несколько "прямых" объектов; например, игрок может попросить игру "взять ящик и мяч". К счастью, автору игры не приходится заморачиваться с обработкой нескольких объектов, так как эту задачу берет на себя синтаксический анализатор. Достаточно просто написать методы-обработчики для отдельных объектов, а уж СА сам вызовет их столько раз, сколько потребуется.

Разрешение или запрещение использования нескольких объектов

Когда игрок задействует в своей команде несколько "прямых" объектов, путем ли их перечисления или с использованием специального слова "все", СА проверяет, допускает ли глагольный объект текущей команды использование нескольких "прямых" объектов. (В некоторых случаях использование множественных объектов будет запрещено до того, как СА дойдет до этого этапа обработки. Например, использование более одного "косвенного" объекта запрещено в любом случае; кроме того, не допускается использование множественных "прямых" объектов для шаблонов команд, у которых определен флаг [disambigDobjFirst]). Чтобы проверить, допускает ли данная команда использование множественных объектов, СА вызывает метод rejectMultiDobj глагольного объекта, передавая ему в качестве аргумента объект-предлог (или nil, если предлог для данной команды отсутствует). Если метод возвращает true, СА прекращает дальнейшую обработку команды. Обратите внимание, что при прерывании обработки СА не будет выводить никаких сообщений, ожидая, что всю соответствуюшую работу метод rejectMultiDobj должен выполнить сам. Этот метод бывает удобен, когда нужно воспрепятствовать игроку "обманывать" игру, когда та пытается скрыть, какие объекты имеются в комнате, так как игрок в этом случае может задать команду типа "осмотреть все" и получить список объектов, например:

  Туман
  Ты находишься в тумане. Сквозь него проглядывают контуры каких-то предметов, 
  но каких именно, сказать трудно.

  > осм все

  дерево: этого не видно из-за тумана.
  пень: этого не видно из-за тумана.
  и т. д.
  

Использование метода rejectMultiDobj предоставляет элегантный выход из подобной ситуации.

Примечание переводчика (вернее, даже два примечания). Во-первых, касательно предыдущего примера: как известно, в стандартной библиотеке TADS (и RTADS) advr.t имеется объект darkroom (темная комната), который, казалось бы, дает готовое решение для подобной ситуации, основанное на ином принципе. Выскажу свое личное мнение: хотя часто этот объект действительно можно использовать без каких-либо переделок, в некоторых случаях предоставляемых им возможностей недостаточно. Принцип работы этого объекта, грубо говоря, состоит в том, что запрещаются все "потенциально опасные" (т. е. могущие быть использованными для "раскрытия" списка объектов) действия (например, "взять", "осмотреть" и т. д. - игрок получает ответ "Кругом тьма"), причем "фильтрация" происходит еще на этапе, когда о команде, кроме глагола, неизвестно ничего (см. метод roomCheck). Прежде всего, такой подход неоправданно ограничивает использование некоторых глаголов (скажем, если игрок в темной комнате держит в руках мяч и наберет команду "ощупать мяч", то также получит ответ "кругом тьма", что, конечно, не есть хорошо); в случаях же, подобных вышеуказанному примеру (видимость ограничена, но не отсутствует полностью) он и вовсе неприменим (игрок наверняка введет команду "осмотреть контуры", и получит ответ, скажем, "этого не видно из-за тумана"). "Доработка напильником" объекта darkroom, безусловно, возможна, но часто довольно трудоемка; использование метода rejectMultiDobj часто бывает гораздо проще и элегантнее.

И второе примечание: в файле-библиотеке extendr.t, который не является "канонической" библиотекой оригинального TADS, но, тем не менее, входит в состав дистрибутива RTADS, объект deepverb "оттюнингован" таким образом, что запрещает по умолчанию использование слова "все" (не задействуя для этого метод rejectMultiDobj). Что-то мне подсказывает (хотя специально я это не проверял), что в 1996 г., когда писалась эта библиотека, такого средства, как метод rejectMultiDobj, для TADS еще не существовало или, как минимум, оно не было задокументировано. В то же время я ни в коей мере не хочу как-то унизить достоинство автора этой библиотеки или высказаться однозначно в пользу использования исключительно механизма с rejectMultiDobj - чем больше существует воможных средств для решения одной и той же задачи, тем лучше. В любом случае, обратите на некоторые нюансы в работе "оттюнингованного" deepverb - в отличие от "официального" решения, он ограничивает только использование слова "все", не запрещая перечисление объектов через запятую.

Итерационный просмотр списка объектов

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

Отображение названия объекта в качестве префикса

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

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

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

   >взять канделябр и спичечный коробок 
   канделябр: Взят.
   спичечный коробок: Ты уже перегрузил свои руки.

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

Самый новый механизм вывода префиксов: prefixdesc

"Последним словом префиксоотображения" является вызов в соответствующем объекте метода с названием prefixdesc. СА вызывает этот метод во всех случаях - как для команды с одним "прямым" объектом, так и для случая нескольких таких объектов, использования слова "все" и других специальных слов. Метод вызывается с четырьмя аргументами:

   prefixdesc(show, current_index, count, multi_flags);

Аргумент show указывает, стал бы СА по умолчанию выводить префикс объекта; этот флаг равен true для всех вышеперечисленных случаев, когда СА отображает префикс (множественные объекты, использование слов "все", "они", "любой"), во всех остальных случаях он равен nil.

Аргумент current_index указывает позицию текущего "прямого" объекта в списке (нумерация начинается с 1), а count соответствует общему количеству объектов в списке.

Обратите внимание, что СА не всегда будет вызывать prefixdesc для всех объектов списка, начиная с позиции 1 до count. В частности, если в процессе работы СА обнаружит, что некоторые из объектов, подобранных с использованием слова "они", окажутся недоступны, то сначала метод prefixdesc будет вызываться только для номеров элементов, соответствующих недоступным объектам; на данном этапе СА не будет вызывать метод для доступных объектов, так как для этих объектов пока не требуется выводить сообщений об ошибках. После вывода всех сообщений об ошибках для недоступных объектов СА далее будет работать только с доступными объектами; при этом он пересчитает их заново с номера 1, и аргумент count будет уменьшен так, чтобы соответствовать количеству элементов в новом списке "прямых" объектов.

Аргумент multi_flags содержит флаги специальных слов, соответствующие словосочетанию "прямого" объекта. Он формируется по тому же принципу, что и соответствующий аргумент функции parseNounPhrase(), и использует те же определения констант PRSFLG_xxx, однако может содержать только следующие флаги: PRSFLG_ALL, PRSFLG_THEM и PRSFLG_ANY. Данный аргумент позволяет глубже проанализировать причины, по которым СА принял решение отобразить или, наоборот, не отображать название объекта в качестве префикса.

Ниже приведен пример реализации простейшего варианта метода prefixdesc, который попросту дублирует "умолчальное" поведение СА:

   prefixdesc(show, curidx, count, flags) =
   {
      if (show)
         "<<self.sdesc>>: ";
   }

Более старый механизм вывода префиксов: multisdesc

Следующий (в обратном хронологическом порядке) вариант вывода префикса - это вызов метода объекта с названием multisdesc, после чего будет выводиться сообщение с кодом 120 (двоеточие). СА использует этот механизм только в случае, если для объекта не определен метод prefixdesc; в противном случае СА вызовет именно последний, а multisdesc проигнорирует. Кроме того, СА задействует этот механизм только в случае, если решит, что требуется отобразить префикс (как принимается такое решение, описано выше).

Наиболее старый механизм вывода префиксов: sdesc

Наконец, если объект не определяет и не наследует ни один из методов prefixdesc или multisdesc, СА просто вызовет краткое описание объекта (свойство sdesc), а затем выведет сообщение с кодом 120 (двоеточие). В остальном этот механизм не отличается от работы с методом multisdesc.

Сравнение различных механизмов отображения префикса

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

Работа метода prefixdesc в двух ключевых моментах отличается от multisdesc и sdesc. Во-первых, prefixdesc вызывается в любом случае, в то время как остальные механизмы СА задействует только в том случае, когда решит, что префикс отображать нужно. Методу prefixdesc же просто передается аргумент-флаг, указывающий, стал бы СА по умолчанию отображать префикс или нет, однако принятие решения об отображении префикса возлагается на метод. Во-вторых, после завершения prefixdesc СА ничего не будет дополнительно выводить, а для двух остальных механизмов он выведет сообщение с кодом 120 (двоеточие). Таким образом, методу prefixdesc (а точнее, автору игры) придется самостоятельно озаботиться, помимо вывода собственно названия объекта, еще и отображением разделителя, отделяющего префикс от описания реакции объекта.

Примечание переводчика: на самом деле, утверждение о том, что использование prefixdesc всегда является предпочтительным, требует оговорок. Прежде всего, тема отображения названия объекта в качестве префикса является далеко не самой животрепещущей при написании игр (хотя, с другой стороны, я думаю, что Майкл Робертс добавил в свою систему соответствующий инструмент не от нечего делать, а как ответ на возникшую у кого-то конкретную проблему). Поведение СА по умолчанию (отображение краткого описания sdesc) будет совершенно адекватным как минимум в 95% игровых ситуаций, причем без каких-либо дополнительных усилий со стороны автора игры (ведь свойство sdesc для объектов приходится определять по-любому). В оставшихся 5-ти процентах случаев для реализации сравнительно простых спецэффектов (например, если просто требуется, чтобы отображаемый префикс отличался от "обычного" краткого описания объекта) лично я бы выбрал метод multisdesc - просто в силу того, что его определение менее громоздко и не требует прописывать длинный "хвост" из аргументов. К prefixdesc же, опять-таки с моей точки зрения, имеет смысл прибегать только в ситуациях, когда нужного эффекта невозможно добиться другими, более простыми средствами (скажем, автор игры хочет, чтобы префикс выводился всегда, а не только для команд с несколькими объектами). Резюмируя, можно сказать - prefixdesc позволяет делать все то же, что остальные два механизма, и кое-что сверх того, но требует некоторых дополнительных трудозатрат; насколько они будут оправданы, необходимо решать в каждом конкретном случае.


Повторная проверка

Для команды с несколькими "прямыми" объектами могут возникать ситуации, когда объект, изначально бывший пригодным для команды, становится непригодным. Например, рассмотрим следующую ситуацию:

  >осмотреть коробку
  Большая коробка закрыта.

  >открыть ее
  Открыв большую коробку, ты обнаружил маленькую коробку.

  >осмотреть маленькую коробку
  Маленькая коробка закрыта.

  >открыть ее
  Открыта.

  >инвентарь
  У тебя есть большая коробка. В большой коробке видна 
  маленькая коробка.

  >закрыть большую коробку, маленькую коробку
  Большая коробка: Закрыта.
  Маленькая коробка: Здесь этого не видно.

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

СА подбирает все объекты для команды до выполнения последней, поэтому он видит, что и большая, и маленькая коробки доступны. Однако после того, как команда будет выполнена для первого "прямого" объекта (большой коробки), второй "прямой" объект (маленькая коробка) станет недоступной.

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

  глагол.validDo(actor, directObject, 1)
  глагол.validIo(actor, indirectObject, 1)

Если один из этих методов вернет nil, СА выводит сообщение об ошибке и прерывает обработку. В противном случае он обрабатывает команду до конца для всего текущего списка объектов.

При выдаче сообщения о непригодности объекта СА проверяет, является ли ставший недоступным объект видимым, вызывая для этого его метод isVisible. Если объект видим, СА использует метод cantReach для выдачи сообщения об ошибке точно так же, как если бы он не обнаружил подходящих объектов в процессе подбора объектов. Если же объект невидим, СА выводит сообщение с кодом 38 ("Здесь больше этого не видно").


Анализ реакции глагола: verbAction

Далее СА проверяет реакцию глагола на команду путем вызова метода verbAction, определенного в игровой программе для соответствующего глагольного объекта:

  глагольный_объект.verbAction(actor, dobj, prep, iobj);

Все аргументы, кроме actor, могут быть равны nil. В частности, для команды без объектов все три оставшихся аргумента будут равны nil, а для команды только с "прямым" объектом это значение будут иметь аргументы prep и iobj.

Если необходимо, чтобы обработка команды заканчивалась этим методом, необходимо использовать внутри него инструкции abort, exit, или exitobj. Среди причин, по которым вам может потребоваться прервать обработку команды на этом этапе, основными являются следующие:

В любом случае, если метод предусматривает прерывание команды, он должен отобразить соответствующую информацию, так как, встретив одну из инструкций abort, exit или exitobj, СА просто "молча" закончит работу.

Если в глагольном объекте не определен метод verbAction, СА сразу переходит к следующему шагу.

Использование метода verbAction для "подмены" команды

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

Для подмены команды можно использовать встроенную функцию execCommand(), которая позволяет выполнить команду, отличную от введенной игроком.

Пусть, например, вы хотите обрабатывать команду "наполнить предмет x предметом y" аналогично команде "налить y в x" (например, "наполнить бутылку водой" должно обрабатываться так же, как "налить воду в бутылку").

Один из способов реализации такого эффекта - это использовать один и тот же код для методов ioFillWith (для глагола "наполнить") и doPutIn (для глагола "налить"). Такой путь возможен, но потребует рутинного копирования кода туда-сюда (особенно, если эффект надо реализовать для большого количества объектов).

Значительно менее утомительный подход - это перехват всех команд с глаголом "наполнить" в методе verbAction соответствующего глагольного объекта и замена их на команды с глаголом "налить". Вот пример реализации:

  fillVerb: deepverb
    verb = 'наполнить'
    sdesc = "наполнить"
    ioAction(withPrep) = 'FillWith'
    verbAction(actor, dobj, prep, iobj) =
    {
      /* проверяем, используется ли формат "наполнить x y", по предлогу */
      if (prep = withPrep)
      {
        /* наш случай - подменяем команду на "налить y в x" */
        execCommand(actor, putVerb, iobj, inPrep, dobj);

        /*
         *   Команда закончена - дальше ее не обрабатываем для текущего объекта, но при наличии 
         *   нескольких объектов переходим к следующему по списку
         */
        exitobj;
      }
    }
  ;

Обратите внимание, что вам, тем не менее, потребуется определить какие-либо простейшие методы-верификаторы verIoFillWith и verDoFillWith (например, в определении класса thing), чтобы обеспечить нормальную работу процедуры устранения неопределенности для команд с использованием глагола "наполнить".


Анализ реакции актера: actorAction

Следующим шагом будет проверка того, готов ли актер выполнить команду. Каждая команда адресуется актеру (персонажу в игре) - если даже игрок не указал актера в явном виде, она по умолчанию выполняется главным персонажем (которого можно получить при помощи функции parserGetMe()). СА вызывает метод actorAction в объекте, соответствующем актеру, передавая этому методу четыре аргумента: объект-глагол, "прямой" объект, объект-предлог и "косвенный" объект (последние три аргумента могут быть равны nil для команд соответствующего формата). Например, для команды "взять черный ящик" вызов может выглядеть так:

  parserGetMe().actorAction(takeVerb, blackBox, nil, nil)

Метод actorAction может либо принять команду, либо запретить ее. Чтобы принять команду, ему вообще ничего не нужно делать (именно таким образом определен этот метод для класса basicMe в advr.t). Чтобы запретить команду, метод должен использовать одну из инструкций abort, exit, или exitobj. Инструкция exit запрещает команду, но ход в игре считается сделанным. exitobj действует аналогично, но если exit сразу переходит к выполнению функции postAction вне зависимости от количества "прямых" объектов в команде, то exitobj для множественных "прямых" объектов переходит к обработке следующего объекта в списке. При использовании abort счет ходов не увеличивается, а демоны и запалы, соответственно, не обрабатываются (хотя функция postAction все равно будет вызываться). В большинстве случаев при запрете команды в методе actorAction имеет смысл использовать инструкции exit или exitobj, поскольку попытка дать команду персонажу, по логике вещей, все равно должна засчитываться за ход. Инструкция abort предназначена скорее для "системных" манипуляций - сохранения/восстановления игры и т. п. - которые происходят как бы за пределами игрового мира и не должны считаться за ход в игре.

Обратите внимание, что СА сам по себе не выводит никаких сообщений, когда метод actorAction прерывает обработку команды одним из описанных выше способов, поэтому данная задача возлагается на сам метод (пример реализации см., например, класс movableActor в advr.t). Кроме того, этот метод не возвращает никакого значения.


Анализ реакции комнаты: roomAction

Если актер "не возражает" против выполнения команды, то следующим шагом будет проверка того, сможет ли он выполнить эту команду в той комнате, в которой в данный момент находится. Для этого СА получает текущую локацию игрока (свойство location) и вызывает для нее метод roomAction. Если свойство location актера не ссылается на объект, этот шаг будет пропущен (хотя, по идее, такого происходить не должно). Метод roomAction вызывается с пятью аргументами, соответствующими актеру, глаголу, "прямому" объекту, предлогу и "косвенному" объекту. Например, если игрок ввел команду "Вася, положи мяч в коробку", вызов может выглядеть так:

  Vasya.location.roomAction(Vasya, putVerb, redBall, inPrep, smallBox)

Как и в случае с actorAction, метод roomAction может запретить команду, прервав ее с использованием инструкций exit, exitobj или abort. Кроме того, оба этих метода сходны еще и тем, что для roomAction автор игры сам должен позаботиться о выводе соответствующего сообщения при прерывании команды, а также тем, что оба метода не возвращают никакого значения.


Обобщенные методы объекта: xobjCheck и xobjGen

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

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

Затем, если в команде имеется "косвенный" объект и этот объект не определяет явным образом метод-верификатор (verIoГлагол) или метод-действие (ioГлагол) для текущего глагола, СА вызывает определенный в нем обобщенный метод-действие iobjGen с тем же набором аргументов, что и iobjCheck.

Далее, если в команде имеется "прямой" объект, СА вызывает метод dobjCheck этого объекта, передавая ему в качестве аргументов актера, глагол, "косвенный" объект и предлог.

После этого, если в команде имеется "прямой" объект и этот объект не определяет явным образом метод-верификатор (verIoГлагол) или метод-действие (ioГлагол) для текущего глагола, СА вызывает определенный в нем обобщенный метод-действие dobjGen с тем же набором аргументов, что и dobjCheck.

Так же, как и roomAction и actorAction, любой из этих методов может прервать обработку команды, используя одну из инструкций exit, exitobj или abort. Принципы использования этих инструкций также совпадают с таковыми для ранее описанных методов.

Обратите внимание, что обобщенные методы-действия dobjGen и iobjGen не будут вызываться, если объект "замещает" их для определенного глагола (иначе говоря, определяет - именно определяет в явном виде, а не наследует - для этого глагола верификатор или метод-действие). Данные методы являются своеобразными "универсальными перехватчиками"; если СА обнаружвает для какого-либо глагола обработчик, определенный объектом, он считает, что соответствующая команда не должна обрабатываться "универсальным перехватчиком" и не вызывает его.

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

 distantItem: fixeditem
   dobjGen(a, v, iobj, p) =
   {
      "Это слишком далеко!";
      exitobj;
   }
   verDoInspect(actor) =
   {
      inherited.verDoInspect(actor);
   }

 targetDistantItem: distantItem
   verIoThrowAt(actor) =
   {
      inherited.verIoThrowAt(actor);
   }
 ;

В этом случае все потомки объекта targetDistantItem будут замещать обобщенный обработчик dobjGen для глагола "бросать" (throw) собственным верификатором verIoThrowAt. Точно так же обстоит дело с глаголом "осмотреть" (inspect), поскольку обработчик для него определен в том же объекте distantItem, который определяет и обобщенный метод-действие.

При этом СА всегда будет вызывать методы dobjCheck и iobjCheck, вне зависимости от того, определены ли в соответствующем объекте свои обработчики для глагола текущей команды. Это фактически единственное принципиальное различие между методами dobjCheck и dobjGen, iobjCheck и iobjGen: методы xobjGen могут замещаться специализированными обработчиками, а методы xobjCheck - нет. Таким образом, если у вас имеется ряд действий, которые необходимо выполнить для объекта вне зависимости от того, какую команду ввел игрок, определяйте эти действия в методах xobjCheck; если же требуется выполнить код, который должен выполняться только тогда, когда для объекта не определена специфическая реакция на команду, следует использовать xobjGen.

Необходимо также еще раз отметить, что для всех описанных выше методов (начиная от verbAction и заканчивая xobjGen) действует следующее правило: если тот или иной метод определен, но в нем не встречается инструкция по прерыванию команды, то после того, как этот метод отработал, команда передается следующему методу по цепочке. Рассмотрим следующий пример:

    modify takeVerb: deepVerb // Дополняем определение глагола "взять"
    verbAction(actor, dobj, prep, iobj)={"Итак, нужно что-то ВЗЯТЬ...";}	
    ;
    Vasya: Actor  // Тестовый актер
    noun='вася'
    actorAction(v, d, p, i) = {"\bВася не против выполнить команду...";}
    location=TestRoom
    ;
    TestRoom: room   // Тестовая комната
    roomAction(a, v, d, p, i)={"\bКоманда выполняется в тестовой комнате...";
                               pass roomAction;
                              }
    ;
    TestObject: thing  // Тестовый объект (мяч)
    noun='мяч'
    dobjCheck(a, v, i, p)={"\bОтработка метода dobjCheck мяча...";}
    dobjGen(a, v, i, p)={"\bМетод dobjGen прерывает обработку.";
             exit;
            }
    location=TestRoom
    ;
  

Если теперь игрок введет команду "Вася, возьми мяч", на экран будет выведено следующее:

  Итак, нужно что-то ВЗЯТЬ...
  Вася не против выполнить команду...
  Команда выполняется в тестовой комнате...
  Отработка метода dobjCheck мяча...
  Метод dobjGen прерывает обработку.
  

Таким образом, отрабатывают все методы до того момента, пока не встретится инструкция прерывания команды. Если бы в нашем примере мы не определили такую инструкцию в методе dobjGen, то в конце СА обычным образом отработал бы метод-верификатор и метод-действие для "прямого" объекта (см. следующий раздел).


Верификация действия и собственно действие

Если обработка команды не была прервана на одном из предыдущих этапов, СА непосредственно применяет ее к задействованным в ней объектам.

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

Такая проверка осуществляется путем вызова метода-верификатора, определяемого шаблоном глагола, как было описано ранее. Если команда содержит только глагол, верификация не требуется, и СА сразу переходит к выполнению действия. В других случаях СА вначале верифицирует "прямой" объект, а затем "косвенный" (если таковой имеется).


Вариант 1: объектов нет

Если команда не содержит объектов, то будет попросту вызван метод-действие, определенный глагольным объектом. Этому методу передается один-единственный аргумент: актер. Этот метод "отвечает" за выполнение команды; он же должен отобразить соответствующий текст, извещающий игрока об удачном или неудачном действии, поскольку является единственным обработчиком команды.

Например, если игрок введет "ждать", СА выполнит следующий вызов:

  waitVerb.action(parserGetMe())

Метод action не возвращает никакого значения. Теоретически в нем можно использовать инструкции exit или abort для прерывания дальнейшего выполнения команды (и exitobj тоже, но эта инструкция для команды без объектов, очевидно, аналогична инструкции exit). Однако на практике они используются редко (разве что abort для системных команд), поскольку прерывать-то особо нечего - никаких других обработчиков после action СА не вызывает.

Альтернативный способ завершить метод action - использование инструкции askdo. Данная инструкция позволяет запросить у игрока "прямой" объект, используя в точности тот же механизм, который применяет СА, когда определяет, что команда требует "прямого" объекта. В частности, askdo сама подбирает "прямой" объект по умолчанию, руководствуясь теми же принципами, что и СА. Подробное описание этих принципов см. в разделе "Объекты по умолчанию и использование слова 'все'".


Вариант 2: команда с одним объектом

Если в команде присутствует только один объект, СА вызывает для него метод-верификатор. Этот метод, носящий название verDoГлагол (Глагол заменяется на значение свойства doAction глагольного объекта - например, для глагола "взять" метод-верификатор будет иметь название verDoTake), получает при вызове один аргумент: актера.

Если объект не определяет и не наследует метод-верификатор, СА прерывает команду и выводит сообщение об ошибке. Для вывода этого сообщения СА вначале проверяет, определена ли в игре функция с названием parseError2(). Если определена, то этой функции передаются четыре аргумента: глагол, "прямой" объект, предлог, "косвенный" объект (последние два аргумента могут быть равны nil); parseError2() и должна будет вывести сообщение об ошибке. Если данная функция не определена, СА сам извещает игрока об ошибке: он выводит сообщение с кодом 110 ("I don't know how to " - Я не знаю, как), затем вызывает краткое описание (свойство sdesc) глагольного объекта. Затем, если в команде имеется только "прямой" объект, выводится сообщение с кодом 111 (пробел), а затем - свойство thedesc "прямого" объекта; если же имеется также "косвенный" объект, СА выведет сообщение с кодом 112 ("anything" - что-либо), затем вызывает свойство sdesc объекта-предлога (или выводит сообщение с кодом 113 ("to" - к), если в команде нет предлога), затем выведет сообщение с кодом 114 (пробел), и, наконец, вызовет свойство thedesc "косвенного" объекта.

Как можно заметить, "умолчальный" формат сообщения практически невозможно хоть как-то приспособить к русскому языку, даже модифицируя сообщения об ошибках. Поэтому в RTADS определена функция parseError2(), выполняющая эту задачу. Посмотреть ее реализацию можно в файле errorru.t, входящем в дистрибутив RTADS.

Отсутствие явно определенного или унаследованного метода-верификатора полностью аналогично случаю, когда метод-верификатор "бракует" команду путем вывода сообщения; единственное отличие состоит в способе формирования этого сообщения.

Если метод-верификатор выводит сообщение, обработка команды прерывается; СА продолжает обработку для следующего "прямого" объекта в текущей команде, если таковой имеется, или переходит к обработке следующей команды, если их было введено несколько. Метод-верификатор не возвращает никакого значения - он сигнализирует о неудачной верификации путем вывода сообщения, а об успешной - тем, что ничего не выводит на экран.

Еще раз обратите внимание, что методы-верификаторы ни при каких обстоятельствах не должны вносить изменения в состояние игры - в частности, они не должны менять значения свойств объектов, а также вызывать функции с побочными эффектами (например, не следует осуществлять вызов команд restart() или askfile() из метода-верификатора). Как уже говорилось ранее, связано это с тем, что верификаторы вызываются СА "втихую" в процессе подбора объектов, при этом все выводимые ими на экран сообщения скрываются от игрока. Поскольку на этом этапе СА просто, грубо говоря, перебирает возможности перед тем, как принять окончательное решение о том, какой объект использовать, эти "тихие" вызовы верификаторов не должны отражаться на состоянии игры.

Если верификация прошла успешно - иначе говоря, метод-верификатор не вывел никаких сообщений, - СА вызывает метод-действие "прямого" объекта. Метод-действие носит название doГлагол (например, для глагола "взять" он носит название doTake). Методу-действию передается один аргумент - текущий актер.

Метод-действие "отвечает" непосредственно за выполнение команды. Он должен также вывести соответствующее сообщение - неважно, успешным было действие или нет, - поскольку является единственным обработчиком команды.

Обратите внимание, что метод-действие может запретить команду даже в том случае, если метод-верификатор разрешил ее.

Например, если игрок введет команду "взять красную книгу", СА вызовет следующие методы:

  KrasnayaKniga.verDoTake(Me)
  KrasnayaKniga.doTake(Me) 

Вариант 3: "прямой" и "косвенный" объекты

При наличии в команде как "прямого", так и "косвенного" объекта верификации подвергаются оба. Вначале СА проверяет, имеются ли для этих объектов (определенные в явном виде или унаследованные) методы-верификаторы; если нет, команда прерывается с сообщением об ошибке (порядок его формирования описан в предыдущем разделе).

Сначала СА верифицирует "прямой" объект путем вызова метода verDoГлагол для этого объекта. В общем случае, если для глагола не определен флаг [disambigDobjFirst], этот метод вызывается с двумя аргументами: актером и "косвенным" объектом. Если же этот флаг определен, то методу в качестве аргумента передается только актер.

Если верификация "прямого" объекта прошла успешно, СА вызывает верификатор verIoГлагол для "косвенного" объекта. Если для глагола не определен флаг [disambigDobjFirst], этот метод вызывается с одним аргументом - актером, а если определен, то с двумя - актером и "прямым" объектом.

Если любой из верификаторов выводит сообщение, команда считается не прошедшей верификацию, и СА переходит к следующему "прямому" объекту в списке или к следующей команде, если больше "прямых" объектов нет.

Если оба метода-верификатора завершились успешно, СА вызывает метод-действие в соответствии с шаблоном команды. Для команды с двумя объектами СА всегда вызывает метод-действие "косвенного" объекта. Метод носит название ioГлагол и вызывается с двумя аргументами: актером и "прямым" объектом. (Обратите внимание, что "прямой" объект при вызове метода-действия передается в любом случае, вне зависимости от наличия у глагола флага [disambigDobjFirst]). Как и в случае команды с одним объектом, с этого момента метод-действие полностью "отвечает" за обработку команды, включая вывод соответствующих сообщений - как в случае успешного выполнения команды, так и в случае неудачи.

Обратите внимание, что для команд такого формата СА не будет самостоятельно вызывать никаких обработчиков в "прямом" объекте. Если это требуется, необходимо включить вызов соответствующего метода в метод-действие "косвенного" объекта.

Например, для команды "положить мяч в коробку" набор вызываемых СА методов может выглядеть так:

  Myach.verDoPutIn(Me, Korobka)
  Korobka.verIoPutIn(Me)
  Korobka.ioPutIn(Me, Myach)

Обработка после завершения действия

Начиная с TADS версии 2.5, СА после завершения работы метода-действия вызывает еще одну функцию, postAction.

Точнее говоря, СА вызывает postAction для каждой команды, обработка которой доходит до фазы выполнения (т. е. для которой вызывается метод verbAction глагола). СА вызывает postAction даже в том случае, если обработка команды прервана инструкцией abort, exit, или exitobj. Однако, если фаза выполнения для команды не достигнута (например, потому, что не удалось подобрать объекты), СА не будет вызывать postAction.

СА вызывает postAction сразу после того, как соответствующий метод-действие (глагол.action для команды без объектов, прямой_объект.doAction для команды только с "прямым" объектом, косвенный_объект.ioAction для команды с "прямым" и "косвенным" объектами) завершит свою работу. Если команда прерывается с использованием инструкций exit, exitobj, или abort, postAction вызывается сразу после прерывания.

Функция postAction относится к цепочке обработки индивидуального объекта, которая начинается с verbAction, поэтому при обработке команды с несколькими "прямыми" объектами он вызывается для каждого из них.

Формат вызова функции postAction (если она определена в игровой программе) имеет следующий вид:

    postAction(actor, verb, dobj, prep, iobj, status);

Первые пять аргументов соответствуют объектам текущей команды; любые из них, кроме актера (actor) и глагола (verb) могут быть равны nil. Аргумент status имеет тот же смысл, что и коды возврата встроенной функции execCommand, и может принимать одно из следующих значений (они определены в библиотеке advr.t):

EC_SUCCESS   Команда выполнена успешно, при этом обработка завершилась выполнением одного из методов-действий (глагол.action, прямой_объект.doAction, или косвенный_объект.ioAction, в зависимости от формата команды).
EC_EXIT   В процессе обработки команды встретилась инструкция exit, метод-действие не вызывался.
EC_EXITOBJ   В процессе обработки команды встретилась инструкция exitobj, метод-действие не вызывался.
EC_ABORT   В процессе обработки команды встретилась инструкция abort, метод-действие не вызывался.

Функция postAction не возвращает значения.

В стандартных библиотеках RTADS данная функция не определена.


Четвертая фаза: обработка окончания хода

Когда СА закончит обработку (в пределах цепочки вызовов методов от verbAction до postAction) для каждого объекта в команде, он переходит к обработке окончания хода.

До того, как начать обработку окончания хода, СА проверяет, каким образом была завершена фаза исполнения команды:

Обработка окончания хода состоит из двух шагов: (1) выполнение демонов и запалов и (2) вызов функции endCommand(). СА определяет, какие именно из этих шагов необходимо выполнить, руководствуясь следующими правилами:

Выполняютя ли демоны и запалы? Вызывается ли endCommand()?
Команда завершилась успешно Да Да
Команда завершена по exit или exitobj Да Да
Команда завершена по abort Нет Да
Фаза исполнения для команды не была начата Нет Нет

Для команды с несколькими "прямыми" объектами ход считается оконченным тогда, когда СА применил команду по очереди ко всем "прямым" объектам. Если командная строка содержит пакет из нескольких команд, каждой из этих команд соответствует ход в игре. В качестве иллюстрации рассмотрим следующий пример. Пусть игрок ввел такую команду:

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

Эта команда занимает два хода::

    >положить книгу и мяч в коробку
    >идти на север

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

Обратите внимание, что первая команда содержит два "прямых" объекта, поэтому фаза исполнения будет пройдена ею дважды (по одному разу для книги и для мяча). Таким образом, если разбить весь цикл обработки нашей команды на отдельные шаги, получим следующую картину:

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

Демоны и запалы

Первым шагом обработки окончания хода является выполнение демонов и запалов. Как было сказано ранее, СА выполняет демоны и запалы в конце хода только в том случае, если для команды была начата фаза исполнения и при этом она не была прервана инструкцией abort.

Запал инициируется при помощи встроенной функции setfuse(), либо встроенной функции notify(), которой при вызове передается ненулевой третий аргумент. Демон инициируется при помощи встроенной функции setdaemon(), либо вызовом notify() с нулевым третьим аргументом.

Порядок, в котором СА вызывает инициированные демоны и запалы, четко не определен, и при программировании не стоит рассчитывать на то, что тот или иной запал или демон будет совершенно точно вызван позже (или раньше) другого демона/запала. В частности, порядок вызова может быть различным для разных версий интерпретатора TADS, разных операционных систем, и даже меняться при повторном запуске одной и той же игры на одной и той же машине.

Каждый активный экземпляр демона выполняется один раз в конце хода.

При обработке окончания хода СА проверяет счетчики запалов; если счетчик запала достиг нуля, СА выполняет этот запал и удаляет его. На этом этапе СА игнорирует все ненулевые счетчики запалов, т. е. он не уменьшает их; обратный отсчет для запалов происходит лишь при вызове встроенных функций incturn() или skipturn().


Функция endCommand

После выполнения всех демонов и запалов СА вызывает функцию endCommand(), если она определена в игре. Данная функция вызывается после каждого хода при условии, что для введенной команды была начата фаза исполнения.

СА вызывает endCommand() с теми же аргументами, что и функцию preCommand в начале хода, к которым добавляется дополнительный аргумент status:

    endCommand(actor, verb, dobj_list, prep, iobj, status);

Этот дополнительный аргумент имеет тот же смысл, что и код статуса, передаваемый функции postAction.

Функция endCommand() не возвращает никаких значений.

Обратите внимание, что endCommand() была введена, начиная с TADS версии 2.5; в более ранних версиях автоматического вызова этой функции синтаксическим анализатором не происходило.

В стандартных библиотеках RTADS данная функция не определена.

От кальсон отказался, выразив протест хриплыми криками: "в очередь, сукины дети, в очередь!"
МИХАИЛ АФАНАСЬЕВИЧ БУЛГАКОВ, Собачье сердце (1925)


Раздел 4.2 Содержание Раздел 4.4