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

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

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


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


Синтаксический анализатор - основы работы

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

Теоремы

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

Краткий справочник

Опытным авторам может оказаться полезным в качестве справочника по функциям и порядку работы СА раздел Последовательность разбора команды и ее выполнения.


Что делает СА

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

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

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


Встроенные функции и функции, определяемые автором игры

Когда мы говорим о синтаксическом анализаторе, мы подразумеваем некий набор инструкций программного кода, который считывает вводимые игроком команды, интерпретирует их и выполняет соответствующие действия в игре. Этот набор инструкций можно подразделить на две части: инструкции, встроенные в программу-интерпретатор TADS, и инструкции, записанные на языке программирования TADS непосредственно в игре. Кроме того, инструкции на языке программирования TADS можно подразделить на те, которые определены в стандартной библиотеке (например, adv.t или advr.t), и те, которые специфичны только для данной конкретной игры.

Встроенная часть СА и "точки входа"

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

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

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

Код объектных библиотек

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

Создавая игру, вы можете внести в стандартную библиотеку любые изменения; в экстремальном случае вы можете вообще не использовать ее и написать собственную библиотеку с нуля, либо использовать другую библиотеку, созданную сторонними программистами (например, WorldClass, Alt или Pianosa - правда, эти библиотеки являются англоязычными).

Синтаксический анализ, осуществляемый самой игрой

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


Использование стандартных объектов

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

  box: openable
    sdesc = "box"
    noun = 'ящик' 'ящика' 'ящику' 'ящиком' 'ящике' 'ящику#d' 'ящиком#t'
    sdesc = "ящик"
    rdesc = "ящика"
    ddesc = "ящику"
    vdesc = "ящик"
    tdesc = "ящиком"
    pdesc = "ящике"
    isHim=true
    location = startroom
    isopen = true
  ;

Информацию о том, что значат те или иные определения, можно посмотреть в Приложении A (объекты thing и openable).

Объектный класс openable (определенный в библиотеке advr.t) уже обеспечивает необходимые реакции объекта на действия, характерные для контейнера, который можно открывать и закрывать, например:

  >открыть ящик

  >заглянуть в ящик

  >положить все в ящик

  >взять мяч из ящика

  >положить книгу в ящик и закрыть ящик

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

Для вышеприведенного определения объекта мы указали свойство noun, для которого указали список строковых значений, соответствующих слову 'ящик' в разных падежах. Свойство noun имеет особый смысл - это так называемое лексическое свойство. Лексические свойства определяют имена для объекта, при помощи которых игрок может обращаться к этому объекту в своих командах; к лексическим свойствам относятся: noun (для имен существительных), adjective (для прилагательных), verb (для глаголов), preposition (для предлогов) и article (для артиклей, что в русской версии TADS неактуально). В большинстве случаев вы будете определять свойства noun и adjective; остальные вам не потребуются, пока вы не начнете создавать собственные команды.

Отдельно стоит сказать о двух специальных значениях в списке - 'ящику#d' и 'ящиком#t'; необходимость определения этих значений связана с тем, что СА TADS изначально ориентирован на английский язык, структура организации предложений в котором отличается от русского; детально эти отличия будут рассмотрены позже, пока же достаточно запомнить, что для любого объекта, к которому игрок будет обращаться в процессе игры, требуется определить свойство noun как список строковых значений, соответствующих формам названия объекта во всех падежах, а также два специальных значения: название в дательном падеже + #d и название в творительном падеже + #t.

Обратите также внимание, что все значения для свойства noun должны указываться в одинарных кавычках (апострофах).

Теорема 1: Значения лексических свойств должны заключаться в одинарные кавычки.

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

  say(box.noun);

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

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

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

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

  booklet: item
    location = startroom
    sdesc = "брошюра"
    ldesc = "Это тонкая брошюра, озаглавленная \"TADS - комментарии разработчиков\"."
    noun = 'брошюра' 'брошюры' 'брошюре' 'брошюру' 'брошюрой' 'брошюре#d' 'брошюрой#t' 
           'буклет' 'буклета' 'буклету' 'буклетом' 'буклете' 'буклету#d' 'буклетом#t'
           'комментарии' 'комментариев' 'комментариям' 'комментариями' 'комментариях' 'комментариям#d' 'комментариями#t'
    adjective = 'tads' 'tads#r' 'тонкая' 'тонкой' 'тонкую' 'тонкой#d' 'тонкой#t' 'тонкий' 'тонкого' 'тонкому' 'тонким' 'тонком' 
                'тонкому#d' 'тонким#t' 'разработчиков' 'разработчиков#r'
  ;

Обратите внимание, что для каждого синонима (как существительных, так и прилагательных) следует определять все падежные формы, а также служебные значения с суффиксами #d и #t. Кроме того, если используемые синонимы-существительные (в нашем случае "брошюра" и "буклет") имеют разный род, то для прилагательных следует определять формы для каждого используемого рода (в нашем случае "тонкая" и "тонкий"). Наконец, для словосочетаний вроде "комментарии разработчиков" слово, стоящее вторым (и выполняющее роль поясняющего для основного первого слова), необходимо занести в список значений свойства adjective (несмотря на то, что это слово является, вообще говоря, существительным), продублировав его служебным значением с суффиксом #r (см. пример). Необходимость данных действий опять-таки вызвана тем, что TADS изначально ориентирован на ангийские грамматические конструкции; подробно механизм обработки команд будет рассмотрен позднее.

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


Реакции на команды

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

У любой воспринимаемой СА команды существует набор методов, которые она вызывает для тех объектов, которые участвуют в команде (метод - это просто функция или процедура, инкапсулированная в определенном объекте - см. соответствующую главу). Когда игрок вводит команду, состоящую из глагола и существительного, СА на основании введенного глагола определяет названия методов, которые требуется вызвать, и затем вызывает эти методы для объекта, определяемого существительным. Например, для глагола "открыть" имеется два метода, которые вызываются в открываемом объекте: метод-верификатор с названием verDoOpen и метод-действие с названием doOpen ("do" в названиях методов происходит от английского "direct object" - "прямой" объект, т. е. объект, на который непосредственно направлено действие (в команде он должен стоять вторым после глагола); оно не имеет ничего общего с глаголом "do" - делать). Метод-верификатор используется для того, чтобы определить, логично ли применить данную команду к данному объекту - для нашего глагола "открыть" применение будет логичным, если объект может открываться/закрываться и если он не открыт в данный момент. Метод-верификатор не проверяет, имеет ли игрок в настоящее время доступ к объекту, поскольку эта проверка осуществляется отдельно; единственное его назначение - проверить, имеет ли смысл применение данной команды к объекту. Метод-действие просто выполняет команду; этому методу не требуется проверять, имеет ли данная команда смысл, поскольку это уже сделал верификатор.

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

Если мы сделаем табакерку наследником класса openable, как мы это сделали ранее с ящиком, то ее открывание и закрывание будут обрабатываться автоматически. Однако нам потребуются некоторые дополнительные действия - когда объект открывают и в нем находится чертик, то чертик должен выскочить. Для этого мы замещаем (override) метод doOpen, определяемый классом openable (в целях экономии места здесь и далее в определениях объектов будут опускаться падежные формы в лексических свойствах, а также свойства rdesc, ddesc, vdesc, tdesc и pdesc, "отвечающие" за вывод названия объекта в разных падежах):

 SnuffBox: openable
   sdesc = "табакерка" 
   noun = 'табакерка'
   doOpen(actor) =
   {
      inherited.doOpen(actor);
      if (deuce.isIn(self))
      {
         " Когда ты открываешь табакерку, из нее выскакивает чертик на пружинке!
         От неожиданности ты роняешь ее. ";
         self.moveInto(actor.location);
         deuce.moveInto(actor.location);
      }
   }
 ;

Первое, что делает новый метод-действие - он наследует (inherit) "поведение" родительского метода doOpen, определяемого классом openable в advr.t. При замещении метода возможность наследования исходного метода часто бывает крайне полезной (особенно, если мы не хотим глобально менять реакцию объекта, а лишь подправить или дополнить какие-либо детали); эту возможность предоставляет ключевое слово inherited. Унаследованный метод doOpen открывает табакерку как обычно. После того, как это сделано, мы проверяем, находится ли чертик в табакерке; если это действительно так, мы выполняем наши специальные действия: выводим для игрока сообщение о выскакивающем чертике и затем перемещаем и чертика, и табакерку в комнату, где находится игрок.

В данном примере следует обратить внимание еще на пару вещей. Одна из них - это параметр actor. Каждый раз при вызове верификатора или метода-действия система передает ему в качестве параметра актера (или персонажа), которому предназначалась команда. Если игрок вводит команду, не указывая персонажа, которому ее надо выполнить, то в этом параметре передается объект, соответствующий текущему главному персонажу (определяется встроенной функцией parserGetMe()). Задержимся немного на этой функции: с ее помощью вы, например, всегда сможете получить текущую локацию игрока путем вызова parserGetMe().location (обратите внимание, что при этом будет возвращен тот объект, в котором главный персонаж находится непосредственно; т. е. если, скажем, он сидит на стуле в кухне, то будет возвращен объект, соответствующий стулу, а не кухне). Также вы сможете определить, какие предметы главный персонаж несет в данный момент с собой, используя вызов parserGetMe().contents. Возвращаясь к нашему методу doOpen: за счет того, что персонаж, который должен выполнить действие, передается соответствующим методам-верификаторам и методам-действиям, обеспечивается возможность отдачи игроком команд другим персонажам. В случае нашего примера это означало бы, что чертик и табакерка переместились бы не в локацию игрока (parserGetMe().location), а в локацию персонажа actor, который выполнил действие (actor.location).

Если Вы хотите сделать команду еще более универсальной с точки зрения выполнения разными персонажами, то можете заменить некоторые участки текста специальными "форматными строками":

  "Когда %ты% открыва%ешь% табакерку, из нее выскакивает чертик на пружинке!
         От неожиданности %ты% роня%ешь% ее. ";

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

 
     formatstring 'ты' fmtTy;
     formatstring 'ешь' fmtYesh;
     Boy: Actor
     sdesc="мальчик"
     noun='мальчик'     
     actorAction(v, d, p, i)={}  // Указывает, что персонаж может выполнять команды игрока - подробности см. 
                                 // в других разделах документации
     fmtTy="он"
     fmtYesh="ет"
     ... // Прочие необходимые определения
     ;     
 

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

> мальчик, открой табакерку,

то будет выведено следующее сообщение:

  Когда он открывает табакерку, из нее выскакивает чертик на пружинке! От неожиданности он роняет ее. 
  

Следует сказать, что в английском языке форматные строки применять намного проще из-за того, что в английском гораздо меньше форм для каждого слова (за счет сокращенного числа падежей, почти полного отсутствия изменения окончаний глаголов при спряжении и т. п.). Поэтому, например, там, где в английском можно обойтись определением одной форматной строки (скажем, для местоимения "you"), в русском приходится учитывать также все падежные формы (ты, тебе, тебя и т. д.). Окончаний глаголов существует также несколько видов - в зависимости от спряжения, а также времени. Поэтому при последовательной реализации форматных строк в руской игре немудрено в них запутаться, да в общем-то данная функция и не стоит таких трудов - по причинам, разъясненным в следующем абзаце. Именно поэтому, вероятно, ГрАнд и не стал этого делать;). Следует также учитывать одну особенность: для английского языка механизм форматных строк автоматически выбирает строчные или заглавные буквы в зависимости от того, с какой буквы написана форматная строка (например, если для персонажа имеется определение fmtYou="he", и имеется форматная строка "%You% drop%s% the box", то для данного персонажа будет выведено "He drops the box."). В русском языке это не работает, и форматные строки %ты% и %Ты% будут восприниматься, как относящиеся к разным словам. В принципе, это ограничение легко обходится при помощи функции ZAG, но помнить о нем стоит.

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

Возвращаясь к нашему примеру: обратите внимание, что нам потребовалось переопределить только метод-действие (doOpen), поскольку верификатор (verDoOpen) и в стандартном варианте (унаследованном от класса container) делает все, что нам необходимо.

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

  isClean = nil
  verDoClean(actor) =
  {
     if (self.isClean)
        "Чище уже некуда. ";
  }
  doClean(actor) =
  {
     "Ты стираешь грязь с крышки табакерки и видишь, что на ней выгравирована надпись.";
     self.isClean := true;
  }
  verDoRead(actor) =
  {
     if (not self.isClean)
        "Ты не можешь разобрать, что написано на крышке, под слоем пыли и грязи. ";
  }
  doRead(actor) =
  {
     "Надпись гласит, \"Чертик в табакерке\". ";
  }

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

Теорема 2: Никогда не меняйте состояние игры внутри метода-верификатора.

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

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

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

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

class ColourDoor: fixedItem
       // Для дочерних объектов необходимо будет определить свойство OtherDoor, смысл которого станет ясен далее
       isopen=nil
       verDoOpen(actor)={if(self.isopen)
                           {"<<ZAG(self, &sdesc)>> уже открыта!"
                           }
                         else
                           {// Пусть при открывании одной двери в комнате захлопывается другая дверь (определяемая свойством OtherDoor)
                            "Когда ты открываешь <<self.ddesc>>, порыв сквозняка захлопывает <<OtherDoor.vdesc>>."
                            OtherDoor.isopen:=nil;
                            self.isopen=true;
                           }
                         }
       ;
       BlueDoor: ColourDoor
       noun='дверь'
       adjective='синяя'       
       sdesc="синяя дверь"
       isopen=nil
       OtherDoor=RedDoor
       location=startroom
       ;
       RedDoor: ColourDoor
       noun='дверь'
       adjective='красная'       
       sdesc="красная дверь"
       isopen=true
       OtherDoor=BlueDoor
       location=startroom
       ;
  

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

Итак, СА "втихую" вызывает верификатор verDoOpen для объекта RedDoor. Поскольку для этого объекта isopen=true, то СА перехватывает вывод верификатора (который звучит как "Красная дверь уже открыта!") и делает вывод, что для данного объекта команда нелогична. Затем осуществляется вызов того же верификатора для синей двери (BlueDoor). Для нее isopen=nil, и выполняется другая ветвь условного оператора. При этом СА опять перехватывает вывод верификатора (сообщение о том, что вторая дверь захлопывается сквозняком) и решает, что команда нелогична и для синей двери. Однако, помимо этого, верификатор также меняет состояние обеих дверей ("закрывая" объект OtherDoor - для синей двери это красная дверь - и "открывая" саму синюю дверь). Напомним, что на экран при этом ничего не выводится, и игрок остается не в курсе этих изменений.

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

Которую "дверь" Вы имеете в виду: красную дверь, или синюю дверь?
  

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

Синяя дверь уже открыта!
  

Нетрудно догадаться, что это сообщение вызовет у игрока некоторое недоумение - ведь на тот момент, когда он давал команду открыть дверь, синяя дверь была закрыта, и его никто не проинформировал об изменениях в ее состоянии!

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

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

Я не знаю как положить красную дверь.
  

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


Создание новых команд

Когда вы начнете писать собственную игру, вы вскоре наверняка обнаружите, что существующих глаголов не хватает, и начнете создавать собственные команды. Одной из особенностей TADS, придающей этой системе особую гибкость, является тот факт, что здесь отсутствуют "встроенные" глаголы; все глаголы определены в библиотеке advr.t, а не в исходном коде самой системы TADS, и поэтому автор игры может заменять, удалять, переопределять любые глаголы, ну и, конечно, добавлять новые.

Мы уже упоминали о лексическом свойстве с названием verb (глагол); как вы, наверное, уже догадались, при добавлении новых команд используют именно его. У вас может возникнуть вопрос, для объектов какого типа определяется данное свойство. Ответ таков: в библиотеке advr.t имеется специальный класс deepverb, потомки которого и являются объектами-командами. Этим объектам не соответствуют предметы в игровом мире; это просто абстрактные объекты, используемые для внутренних целей системы (а конкретнее, для хранения данных и методов, необходимых для реалиации той или иной команды).

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

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

  whistleVerb: deepverb
    verb = 'свистнуть' 'свистеть' 'свистни' 'свистните' 'свисти' 'свистите'
    sdesc = "свистнуть"
    action(actor) =
    {
       "Засунув четыре пальца в рот, ты громко свистишь, раздувая щеки. 
       Эх, денег у тебя все-таки не будет!";
    }
  ;

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

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

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

Это работает примерно так: вы присваиваете свойству doAction объекта-потомка deepverb некое значение строкового типа (т. е. заключенное в одинарные кавычки). Добавляя к этому значению приставку 'verDo', СА получает название метода-верификатора, а 'do' - метода действия. Если, к примеру, вы определяете команду "ругаться" (на кого-либо), и указываете для соответствующего объекта-глагола doAction = 'Railat', то метод-верификатор для этой команды будет носить название verDoRailat, а метод-действие - doRailat. Определение самого глагольного объекта может выглядеть, например, так:

  railatVerb: deepverb
    sdesc = "ругаться на"
    verb = 'ругаться на'
    doAction = 'Railat'
  ;

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

В этом примере стоит обратить внимание еще на пару вещей.

Во-первых, значение лексического свойства verb определено с использованием двух слов. Это характерно только для свойства verb - ни для каких других лексических свойств (noun, adjective, preposition...) так делать нельзя. Если вы определяете глагол с использованием двух слов, то должны разделить эти слова пробелом, при этом второе слово должно быть определено как предлог (т. е. соответствующее ему значение должно содержаться в списке для лексического свойства preposition (предлог) одного из объектов). Если это не сделано в файле advr.t, вам следует включить соответствующее определение в свою игру. Использование глаголов, состоящих из двух слов - это "тяжелое наследие" изначальной ориентации TADS на английский язык. В английском предлоги могут зачастую кардинально изменять смысл глагола (например, глагол pick имеет значения "ковырять", "клевать", "выбирать", а глагол pick up - "поднимать", "брать"). Кроме того, для английского языка характерна конструкция, совершенно недопустимая в русском - когда предлог "заносится" в конец предложения; например, команды "pick up key" и "pick the key up" совершенно равнозначны и означают "взять ключ". В русском языке, в принципе, можно было бы реализовать анализ глаголов с предлогами по-другому, однако это потребовало бы значительно больших трудозатрат, которые в общем-то не очень нужны, поскольку и "английский" вариант вполне работоспособен; главное - не забывать соответствующим образом определять глаголы, которые сочетаются с существительными посредством предлогов, например, "перепрыгнуть через", "навести на", "выстрелить в".

Второе, на что следует обратить внимание - это использование заглавных букв в строковом значении свойства doAction. По соглашениям, принятым в файле advr.t, с заглавной буквы всегда начинается часть строки, соответствующая имени глагола; название предлога иногда тоже пишется с большой буквы, но только для глаголов, использующих "косвенный"объект. Например, в advr.t определено свойство ioAction для глагола "дать" в виде GiveTo, поскольку английский предлог "to" в данном случае является связкой между командой и "косвенным"объектом (в команде вида "дать деньги продавцу"; в русском языке этот предлог заменяется падежом, однако в процессе обработки команды СА в RTADS все равно используется внутреннее представление с использованием предлога - этот вопрос будет подробнее рассмотрен далее). Для нашего глагола "ругаться" "косвенный" объект не нужен, поэтому мы пишем название предлога (в данном случае русскому предлогу "на" соответствует английский "at") со строчной буквы.

Здесь необходимо сделать небольшое отступление о формировании имен обработчиков в свойствах doAction и ioAction. По умолчанию (поскольку большинство глаголов было уже определено в английской версии TADS, а в RTADS эти определения в основном только расширялись и дополнялись) наименование обработчика составляется из названия глагола по-английски и (если требуется) предлога, также по-английски (о принципах использования строчных и заглавных букв рассказано абзацем ранее). В принципе, вы можете не соблюдать эти соглашения, называя, например, глаголы и предлоги русскими именами (но обязательно в латинской транскрипции: конкретно для нашего примера можно было бы указать определение типа doAction='Rugatsyana'), а то и вовсе назначая обработчикам произвольные имена (например, хоть doAction='brFhgjLQQQDfnmsDYAA'). Однако рекомендуется все-таки придерживаться указанных соглашений, поскольку иначе вы просто запутаетесь. Даже с русскими именами глаголов в транслите (хотя ГрАнд и использует их для некоторых из вновь определенных в RTADS глаголов) вполне возможна неразбериха из-за возникающей при этом неоднозначности написания; для того же глагола "ругаться" возможны созвучные транслитные написания "rugatsya", "rugatsja" и даже "rugaza" (и, наверно, еще с полдюжины вариантов, которые часто зависят от того, кто какие иностранные языки изучал в школе;), и, особенно при больших перерывах в работе над игрой, вы вполне можете забыть, какой именно вариант использовали.

В командах третьего типа присутствует как "прямой", так и "косвенный" объект, а также предлог-связка для "косвенного" объекта (он может присутствовать в явном или скрытом виде). Например, вам нужно создать команду типа "поджечь бумагу посредством факела". Чтобы определить подобную команду, вам следует использовать свойство ioAction объекта-потомка deepverb. Это свойство во многом аналогично doAction, но определяет три новых метода, а также ставит в соответствие команде предлог:

  burnVerb: deepverb
    sdesc = "поджечь"
    verb = 'поджечь'
    ioAction(withPrep) = 'BurnWith'
  ;

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

Предлог ставится глаголу в соответствие посредством аргумента withPrep в скобках: withPrep - это объект, определенный в advr.t, для которого определено лексическое свойство preposition, в котором и указаны словоформы для предлога "посредством". Объект withPrep является потомком класса Prep, аналогичного по своей сути классу deepverb: это - абстрактный класс, которому не соответствует ни один предмет игрового мира, единственной целью которого является определение лексических свойств для предлогов, чтобы игрок мог использовать их в своих командах.

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

Мы упомянули о том, что определение ioAction порождает три метода. Вспомним, что doAction определяет два метода, названия которых формируются с использованием строкового значения этого свойства: метод-верификатор (с префиксом verDo) и метод-действие (с префиксом do). Свойство ioAction таким же образом определяет методы-обработчики, но, поскольку в команде задействованы два объекта, при этом порождается не один, а два верификатора: по одному для "прямого" и "косвенного" объектов. В нашем случае эти методы будут носить названия verIoBurnWith ("косвенный" верификатор) и verDoBurnWith ("прямой" верификатор). Методы вызываются именно в том порядке, в котором они перечислены (т. е. сначала вызывается верификатор "косвенного" объекта). Если оба верификатора определены и выполняются успешно (без вывода сообщений), то будет выполнен метод-действие с названием ioBurnWith (как видно из названия, он определен для "косвенного" объекта). Обратите внимание, что, даже если вы определите для "прямого" объекта метод doBurnWith, этот метод не будет вызываться СА при обработке команды.

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

  verIoBurnWith(actor) = { ... }
  verDoBurnWith(actor, iobj) = { ...}
  ioBurnWith(actor, dobj) = { ... }

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

Теорема 3: Верификаторы verIoXxxx не имеют аргумента, соответствующего "прямому" объекту (dobj).

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

  ioBurnWith(actor, dobj) =
  {
     dobj.doBurnWith(actor, self);
  }

Как уже было сказано выше, сам СА в процессе обработки команды не будет вызывать doBurnWith, но вам ничто не мешает использовать этот вызов при обработке команды.

Теорема 4: Синтаксический анализатор никогда сам не осуществляет вызов обработчиков doXxxx для команд с участием двух объектов.

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

  ioAction = [disambigDobjFirst] 'BurnWith'

Модификатор - это то самое непонятное слово в квадратных скобках. При таком определении TADS будет вызывать верификаторы в обратном порядке по сравнению с обычным:

  verDoBurnWith(actor) = { ...}
  verIoBurnWith(actor, dobj) = { ... }
  ioBurnWith(actor, dobj) = { ... }

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


Расширение определения существующего глагола

Добавляя новые команды, вы в ряде случаев наверняка обнаружите, что вам требуется дополнить и расширить определение глагола, уже существующего в advr.t. Например, в вашей игре будет использоваться команда "открыть сундук посредством лома". Глагол "открыть" в advr.t уже определен, и вы не сможете создать еще один глагол с таким же названием в своей игре. К счастью, TADS позволяет модифицировать реакции для существующих глаголов, не внося изменений в исходный код их определений.

Для изменения определения существующего глагола можно использовать директиву TADS modify. Эта директива позволяет вам добавить или переопределить свойства для объекта, уже определенного где-либо в игре, в том числе и в отдельном файле, включаемом вами в игру - например, advr.t. Например, модифицировать глагол "открыть", уже определенный в advr.t, можно следующим образом:

  modify openVerb
     ioAction(withPrep) = 'OpenWith'
  ;

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

Теорема 5: не редактируйте напрямую файл advr.t - используйте вместо этого директиву modify.


Видимость, достижимость, доступность, пригодность (легитимность)

Синтаксический анализатор TADS совместно со стандартной библиотекой (advr.t) задают несколько взаимосвязанных принципов, определяющих, какие объекты игрок может использовать в своих командах. Набор объектов, к которым может обращаться игрок, меняется от хода к ходу, поскольку с каждым ходом может меняться местоположение игрока и состояние объектов; кроме того, этот набор может зависеть и от конкретной команды, даваемой игроком, поскольку разные команды предъявляют различные критерии к объектам.

Видимость

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

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

Видимость определяется методом isVisible(vantage), который определен для всех объектов. В advr.t этот метод определен в классе thing таким образом, что объект считается видимым (isVisible возвращает true) при выполнении одного из следующих условий: объект находится в той же локации, что и объект vantage, либо объект находится в контейнере, содержимое которого видно (что определяется свойством contentsVisible контейнера) и уже для этого контейнера isVisible возвращает true.

В файле advr.t имеется вспомогательная функция visibleList(object, actor). Эта функция возвращает список объектов, содержащихся в объекте object, которые видны персонажу actor.

Достижимость

Объект является достижимым, если игрок может к нему прикоснуться.

Достижимость объекта определяется методом isReachable(actor). Как и в случае видимости (см. предыдущий абзац), этот метод определен в библиотеке advr.t для класса thing. Этот стандартный вариант isReachable возвращает true (что означает, что объект достижим), если этот объект включен в список reachable текущей локации персонажа actor (reachable - это специальное свойство, определяемое для всех комнат в игре; содержит список объектов, достижимых для актера, находящегося в этой комнате), а также в случае, если содержимое локации (контейнера) этого объекта достижимо для персонажа actor (определяется свойством contentsReachable контейнера) и одновременно сама локация этого объекта достижима для персонажа actor.

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

Доступность и пригодность (легитимность)

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

СА считает объект доступным или пригодным (легитимным) для того или иного глагола, если этот объект успешно проходит проверку методов validDo (для "прямого" объекта) или validIo (для "косвенного" объекта) этого глагола. Методы validDo и validIo определены в библиотеке advr.t для класса deepverb, который является родительским для всех глагольных объектов, и, таким образом, все глаголы наследуют эти методы.

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

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

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

Верификация

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

Этот тест на логичность мы будем называть верификацией.

Взаимосвязь видимости, допустимости и верификации

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

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

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


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

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

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

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

Видимость объекта определяется методом isVisible, который определен для каждого объекта в игре. Класс общего назначения thing в библиотеке advr.t содержит определение этого метода, подходящее для большинства объектов; вам, скорее всего, не понадобится как-то менять его, если только вы не хотите добиться какого-либо специального эффекта. Чтобы определить, виден ли некий объект object игроку, СА выполняет вызов следующего вида:

  object.isVisible(parserGetMe()) 

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

После того, как будут отброшены все объекты, не входящие в возвращаемый методом validDoList (или validIoList, смотря по обстоятельствам) список, СА для каждого из оставшихся объектов вызывает метод validDo, который также определен в классе deepverb. Проверяемый объект передается данному методу в качестве аргумента; в случае успешного прохождения проверки (т. е. когда объект легитимен) возвращается true, иначе - nil. СА оставит в списке только те объекты, которые пройдут проверку.

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

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

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

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

Которую "книга" Вы имеете в виду: синюю книгу, или красную книгу?

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

Отображаемые СА в запросе имена объектов берутся из свойств vdesc этих объектов. Поэтому очень важно, чтобы каждый объект в вашей игре имел уникальные значения свойства sdesc (соответствующего краткому описанию объекта), а также всех падежных свойств (rdesc, ddesc, vdesc, tdesc и pdesc, "отвечающие" за краткое описание в родительном, дательном, винительном, творительном и предложном падежах соответственно). В противном случае может возникнуть ситуация, когда игрок не сможет различить объекты. Если, скажем, в нашем вышеприведенном примере с книгами обе книги использовали бы в качестве краткого описания (и всех падежных форм) просто слово "книга", игрок увидел бы следующий запрос:

Которую "книга" Вы имеете в виду: книгу, или книгу?

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

Теорема 6: Всегда определяйте для своих объектов уникальные описательные свойства (sdesc и формы падежных свойств).

Более подробное описание процесса разрешения неопределенностей вы найдете в соответствующем разделе.


Объекты по умолчанию и использование слова 'все'

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

Чтобы найти "прямой" объект по умолчанию для некоторого глагола, СА вызывает метод doDefault, определенный в глагольном объекте-наследнике класса deepverb. Для большинства глаголов, определенных в advr.t, их методы doDefault возвращают список всех доступных объектов; для отдельных глаголов возвращается несколько более сокращенный список (например, глагол "взять" (take) возвращает список всех доступных объектов, не находящихся в инвентаре игрока). Аналогично, для получения списка "косвенных" объектов, используемых по умолчанию, вызывается метод ioDefault. Если эти методы (doDefault или ioDefault, в зависимости от ситуации) возвращают список, состоящий ровно из одного объекта, СА и подставляет его команду. Если же список окажется пустым или будет содержать два и более объектов, СА попросит игрока указать конкретный объект, так как сама команда не позволит это сделать.

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

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

  modify inspectVerb
    rejectMultiDobj(prep) =
    {
       "Ты можешь осмотреть за один раз не более одного предмета. ";
       return true;
    }
  ;


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

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

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

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

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

Непосредственное управление течением времени в игре: функции incturn и skipturn

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

Функция skipturn аналогична incturn, но вместо того, чтобы помечать запалы, "сработавшие" в пределах заданного интервала прошедших ходов, на исполнение, она просто удаляет эти запалы. Удаленные запалы уже не выполняются; этот механизм позволяет авторам игры полностью "сбрасывать" все события, которые должны были произойти за указанное число ходов. При вызове skipturn обязательно указывается один аргумент - число пропускаемых ходов.


Это ты так говоришь.
ТИМ РАЙС (TIM RICE), ЭНДРЬЮ ЛЛОЙД ВЕББЕР (ANDREW LLOYD WEBBER), Иисус Христос - суперзвезда (1970)


Глава третья Содержание Раздел 4.2