Данный файл является частью Руководства по 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(), если таковая определена. Этой функции передаются дополнительные аргументы, описывающие словосочетание (словосочетания), которые игрок использовал для описания "прямого" объекта в своей команде, что позволяет сформировать т