Данный файл является частью Руководства по TADS для авторов игр.
Copyright © 1987, 1996 Майкл Дж. Робертс
(Michael J. Roberts). Все права защищены.
Руководство было преобразовано в формат HTML Н. К. Гайем (N. K. Guy), компания tela design.
Перевод руководства на русский язык - Валентин Коптельцев
В настоящем разделе рассматривается ряд более сложных технических средств синтаксического анализатора TADS. Приемы, описанные здесь, позволят вам практически полностью переопределить стандартные реакции СА, обеспечив, таким образом, специфический порядок обработки вводимых команд, обычно не выполняемый TADS.
Если вы пока только знакомитесь с основами TADS, то будьте уверены - при разработке игры средней сложности описанные здесь приемы вам вряд ли понадобятся. Даже наиболее сложные игры обычно используют лишь небольшую часть этих приемов. Тем не менее, если вы хотите получить представление о всех возможностях системы, данный материал будет вам весьма полезен. В любом случае настоятельно рекомендуется ознакомиться с основами работы СА перед прочтением данного раздела.
TADS может обрабатывать только предлоги, состоящие из одного слова. В то же время, например, в английском языке часто встречаются ситуации, когда один предлог заменяется целой конструкцией из нескольких слов (например, во фразе "take the book out of the bag" - "достать книгу из сумки" - в роли связки, эквивалентной одному предлогу, используется сразу два слова - "out" и "of"). В русском языке прямую аналогию такой конструкции подобрать сложно, но, тем не менее, и здесь встречаются случаи, когда необходимо рассматривать два слова, как одно: возьмем, скажем, глагол с наречием "плясать вприсядку" - это словосочетание выступает как единая конструкция.
TADS не позволит вам определить шаблон команды таким образом, чтобы понимать подобные глагольные конструкции. Тем не менее, здесь имеются средства для того, чтобы обойти это ограничение: вы можете задать набор слов, которые будут фактически "склеены" в одно в случае, если встретятся вместе. В описанном выше примере со словосочетанием "плясать вприсядку" вы могли бы запрограммировать СА таким образом, чтобы всякий раз, когда это словосочетание встречалось бы в команде игрока, оно рассматривалось бы как единая конструкция.
Чтобы определить пару слов таким образом, используют инструкцию compoundWord. Для нашего примера это выглядело бы так:
compoundWord 'плясать' 'вприсядку' 'плясатьвприсядку';Такое определение сообщает синтаксическому анализатору, что, когда ему встречается слово "плясать", за которым сразу же следует слово "вприсядку", то эти слова будут заменены на слово 'плясатьвприсядку'. Отметим, что в данном случае мы следуем соглашению, принятому в библиотеке advr.t, согласно которому при определении сочлененных слов результирующее слово получается простым слиянием исходных; на самом деле это результирующее слово может быть любым; например, вполне допустимо определение
compoundWord 'плясать' 'вприсядку' 'плясать_вприсядку';или даже
compoundWord 'плясать' 'вприсядку' 'любое_другое_слово';Обратите внимание, что слово "плясатьвприсядку" должно быть определено в одном из лексических свойств (в данном случае в одном из глагольных объектов). То есть даже в случае, если составляющие части сложного слова уже определены в лексических свойствах (т. е. распознаются игрой), результирующее слово также необходимо в явном виде определить в одном из лексических свойств. Дело в том, что СА не выполняет никакого смыслового анализа пар слов - он просто, встретив такую пару слов, заменяет ее на результирующее слово.
Определять составные слова, состоящие из трех и более частей, напрямую нельзя, однако вы можете получить такое определение, используя несколько инструкций compoundWord. Скажем, если в игре необходимо будет сидеть на корточках, можно использовать следующую конструкцию:
compoundWord 'на' 'корточках' 'накорточках'; compoundWord 'сидеть' 'накорточках' 'сидетьнакорточках';Встретив словосочетание "сидеть на корточках", СА сначала преобразует слова "на корточках" в составное слово "накорточках". Затем он повторно проверяет получившееся словосочетание и, встретив пару слов "сидеть" и "накорточках", преобразует их в конструкцию "сидетьнакорточках".
Обратите внимание, что во втором примере составные слова были определены в обратном порядке - мы сначала "склеили" вторую и третью составные части нашей конструкции, а не первую и вторую. Связано это с тем, что слова "сидеть на" могут встречаться и в таких командах, где "склеивания" не требуется ("сидеть на стуле" и т. п.). Если бы мы сначала объединяли слова "сидеть" и "на", такие "нормальные" команды обрабатывались бы некорректно.
Остается добавить, что теоретически сочлененные слова можно использовать и для реализации обработки словосочетаний вида "собака в будке", однако на практике в RTADS уже определены средства грамматически корректного разбора таких конструкций. Об этом будет рассказано в соответствующей главе.
Синтаксический анализатор воспринимает ряд слов как специальные зарезервированные слова. Эти слова нельзя в полной мере отнести к лексическим свойствам, поскольку они выполняют несколько иную роль, чем обычные части речи, используемые в TADS-командах - существительные, прилагательные, глаголы и предлоги. В связи с этим вы не сможете определить их с использованием обычных лексических свойств; вместо этого используется специальная инструкция specialWords.
Эта инструкция может стоять в любом месте, где разрешено использование и других подобных специальных инструкций (таких как formatstring и compoundWord) (грубо говоря, она должна располагаться вне определений объектов и классов). Вслед за инструкцией specialWords перечисляются все зарезервированные слова, которые должны следовать в определенном, строго заданном порядке. Слова перечисляются в порядке, приведенном ниже, при этом для каждой позиции должно быть определено хотя бы одно слово. При этом можно определить и большее число слов для каждой позиции, ставя между синонимами знак равенства (=). Список специальных слов по умолчанию выглядит так (он определен в файле advr.t):
specialWords 'of'='для'='против'='типа'='из'='под'='от', /* используется во фразах типа "корзина для бумаг" */ 'и' = 'а' = 'and' , /* союз для объединения списков существительных ("взять мыло и веревку"), а также для отделения друг от друга команд ("открыть дверь и идти на север" */ 'затем' ='после'= 'потом' = 'then' , /* союз для отделения друг от друга команд ("взять ключ затем открыть дверь" */ 'все'='всех'='all' = 'everything' , /* обращение ко всем доступным объектам ("взять все") */ 'оба'='обе'='обоих'='both' , /* используется с множественным числом или для ответа на запрос СА для устранения неопределенностей */ 'кроме' = 'но' = 'but' = 'except' , /* используется для исключения предметов из списка, обычно используется совместно со словом "ВСЕ" ("взять все кроме ружья") */ 'который'='которая'='которое'='one' , /* используется для ответов на запросы СА вида "Которую "книга" вы имеете в виду:..." (более органично для английского языка; в русском обычно не используется) */ 'которые' = 'ones', /* то же самое для множественного числа */ 'это' = 'этот' = 'тот'='этим'='этому'='it' = 'there', /* обращение к последнему использованному в команде одиночному "прямому" объекту (команды с использованием нескольких "прямых" объектов или слова "все" не учитываются) */ 'их' = 'эти'='те'='them', /* обращение к последнему использованному в команде списку "прямых" объетов */ 'его'='него'='him'='нем', /* обращение к последнему использованному в команде предмету или персонажу мужского рода */ 'ее'='her'='нее', /* обращение к последнему использованному в команде предмету или персонажу женского рода */ 'любой'='любая'='любое'='любые'='любую'='любому'='любым'='any' = 'either'='любого' /* в процессе устранения неопределенностей это ключевое слово, введенное в ответ на запрос СА, позволяет выбрать объект из предлагаемого списка случайным образом */ ;За исключением слов, соответствующих позициям "of", "который (one)" и "которые (ones)", слова из этого списка вообще не могут использоваться в обычных лексических свойствах - во всех случаях предполагается, что они имеют специальное значение, как описано в этом разделе. Для позиций же, соответствующих "of", "который (one)" и "которые (ones)", использование в качестве обычных лексических свойств не запрещено; они приобретают специальное значение только в случае, если СА ожидает этого, исходя из контекста команды.
Обратите внимание (это, правда, касается только авторов англоязычных игр), что последняя позиция в этом списке ("любой (any)") добавилась сравнительно недавно; при определении специальных слов инструкцией specialWords вы можете вообще не упоминать ее, при этом система в соответствующей ситуации будет использовать "умолчальные" слова "any" и "either".
Совместно с инструкцией specialWords можно использовать команды replace и modify. Если ваша инструкция начинается с конструкции replace specialWords, то все предыдущие определения специальных слов отбрасываются, и используется новый список этих слов. (Обратите внимание, что, если вы включите в исходный код своей программы вторую инструкцию specialWords, не предваряя ее ни replace, ни modify, то в игре будет использоваться также новый список слов, однако компилятор выдаст предупреждение о том, что для определенности следует использовать директиву replace). Если ваше определение набора специальных слов начинается с конструкции modify specialWords, то компилятор добавляет специальные слова, определенные в новом списке, к определенному ранее списку. При использовании modify вы можете указывать nil в тех позициях, которые не хотите менять. Таким образом, эта директива позволяет вам добавить одно или несколько специальных слов в отдельные позиции списка, не переопределяя весь набор целиком.
В принципе TADS никак не выделяет объект, соответствующий главному персонажу игры (этот объект возвращается функцией parserGetMe()), по сравнению с другими персонажами/актерами в игре. Когда игрок набирает команду, которая не обращена явным образом никакому персонажу (например, "взять мяч"), синтаксический анализатор считает, что команду должен обрабатывать объект, возвращаемый parserGetMe(), однако собственно обработка команды ничем не отличается от случая, когда команда отдана неглавному персонажу (например, "Бобик, возьми мяч").
Всем методам, вызываемым СА в процессе обработки команды, в качестве одного из аргументов передается объект, соответствующий персонажу, которому эта команда была дана. Это позволяет при написании этих методов обеспечивать обработку независимо от выполняющего команду актера - или же, наоборот, задать выполнение специальных действий для того или иного персонажа. Методы-обработчики глагольных объектов, определенные в advr.t, как правило, учитывают актера, выполняющего команду. Например, метод doTake (для глагола "взять"), определенный в классе thing, перемещает взятый объект не в инвентарь главного персонажа, а в инвентарь актера, переданного ему в качестве аргумента. Это позволяет использовать один и тот же код для обработки команд, скажем, "взять кирпич" и "прораб, возьми кирпич.".
Если обработчик команд написан так, что не зависит от персонажа, выполняющего эту команду, то он должен "уметь" отображать сообщения не только от первого лица (от лица игрока), но и от лица соответствующего актера. Каждый раз прописывать при определении обработчика для каждого сообщения actor.thedesc (ну, или actor.sdesc для русскоязычных игр) довольно утомительно, поэтому в TADS предусмотрен гораздо более удобный механизм вывода "универсальных" сообщений - так называемые форматные строки.
Примечание переводчика: описанный ниже механизм форматных строк действительно хорош для английского языка, удобство же его для русскоязычных игр спорно - по причинам, подробнее рассмотренным здесь. По крайней мере, в текущей версии RTADS все обработчики описаны именно названным выше "неудобным" способом, т. е. с использованием свойств объекта (типа actor.thedesc, хотя конкретно это свойство не используется), "отвечающих" за вывод названия этого объекта либо соответствующего ему местоимения в нужном падеже, а также за согласование глагола; форматные строки в advr.t не используются вообще. Также необходимо сделать оговорку, что данная проблема скорее актуальна для разработчика стандартной библиотеки, чем для автора конкретной игры, поскольку в самой игре, как правило, все действия будут выполняться только главным персонажем; так происходит в большинстве игр - случаи, когда игрок по ходу игры может отдавать приказы другим персонажам (и они эти приказы будут выполнять!), достаточно редки, при этом выполнение этих приказов обычно ограничено одним-двумя действиями. В этой ситуации гораздо проще не заморачиваться этой проблемой и не городить, может быть, и очень изящную с программистской точки зрения систему "подмены" сообщений, разработка которой потребует примерно столько же труда, сколько и вся остальная игра, но которую игрок все равно практически не сможет оценить, а просто предусмотреть в соответствующих обработчиках эти два-три специальных случая. Впрочем, наличие нескольких способов для решения одной и той же проблемы недостатком никогда не считалось, и вполне возможно, что кому-то механизм форматных строк покажется полезным.
Форматной строкой называется специальная последовательность символов, вместо которой встроенная процедура форматирования выводимого текста подставляет некое свойство объекта, соответствующего актеру, выполняющему текущую команду. Вместо местоимений "ты" и тому подобных методы в advr.t используют при выводе сообщений форматные строки.
При определении форматной строки некоей последовательности символов ставится в соответствие название свойства. Когда в сообщении присутствует форматная строка (она обрамляется знаками процента (%)), процедура форматирования вывода подставляет вместо форматной строки значение свойства, которое ей соответствует. Скажем, определение форматной строки может выглядеть так:
formatstring 'ты' fmtYou;Эта инструкция сообщает системе, что каждый раз, когда в выводимом на экран сообщении встречается строка "%ты%", эту строку следует заменить на значение свойства с названием fmtYou для текущего актера.
Предположим, в вашей игре игрок может отдавать приказы другому персонажу, и вы решили реализовать вывод сообщений с использованием форматных строк. Возможный способ реализации (для одного частного случая) может выглядеть так:
formatstring 'ты' fmtTy; formatstring 'тебе' fmtTebe; formatstring 'ешь' fmtEsh; /* Пусть персонажа, которому игрок отдает команды, зовут Вася */ Vasya: Actor noun='Вася' sdesc="Вася" /* ... */ fmtTy='он' fmtTebe='ему' fmtEsh='ет' ; /* Для объекта, соответствующего главному персонажу, значения "форматных" свойств также потребуется определить (пусть они будут равны, соответственно, 'ты', 'тебе' и 'ешь') */ /* ... */ /* Определяем некую комнату */ Komnata_S_Lestnizey: room sdesc="Чулан" ldesc="Это мрачный полутемный чулан. Крайне хлипкая на вид лестница, у которой, похоже, отсутствует половина ступеней, ведет вниз, в темноту." /*Определяем свойства-направления*/ north=Drugaya_Komnata /* Единственный выход ведет на север */ south=noexit /* Все остальные направления также приравняем noexit */ noexit={"Сквозь стену %тебе% не пройти.";} /* Первая форматная строка */ /* Для лестницы (направления вниз) определим специальное сообщение */ down="Без хорошего фонаря %ты% не смож%ешь% спуститься по этой лестнице." ;Теперь, если главный персонаж и актер Вася окажутся в комнате, то при вводе команды
> идти на югигра выведет сообщение
Сквозь стену тебе не пройти.Если же игрок наберет
> Вася, иди на юг,то получит ответ
Сквозь стену ему не пройти.Соответственно, при вводе команды на спуск вниз для главного персонажа и Васи игра соответственно ответит "Без хорошего фонаря ты не сможешь спуститься по этой лестнице" и "Без хорошего фонаря он не сможет спуститься по этой лестнице"
Как видите, форматные строки можно определять не только для местоимений, но и для других частей речи и слова (например, для окончаний глаголов, как в нашем примере).
Следует обратить внимание еще на одну тонкость: при определении форматных строк с использованием латинских букв регистр подставляемой форматной строки будет определяться автоматически, т. е., скажем, для следующего определения:
formatstring 'you' fmtYou;сообщение, содержащее форматную строку %You%, подставит вместо нее значение свойства fmtYou текущего персонажа, начав его с заглавной буквы. Для русского алфавита этот механизм не работает; чтобы добиться подобного эффекта, вам потребуется либо дополнительно определять форматные строки для случаев, когда подставляемые слова будут начинаться с заглавной буквы, либо использовать конструкцию с функцией ZAG.
Большинство объектов в текстовых играх являются уникальными (в пределах каждой игры). Однако в некоторых случаях возникает необходимость в создании группы "взаимозаменяемых" объектов, у которых может отличаться их местоположение и отдельные другие свойства, но в основном они совпадают. В TADS существует механизм для реализации подобных групп: так называемые неразличимые объекты.
Неразличимые объекты бывают особенно полезны, когда требуется динамически создать набор объектов (при помощи оператора new), поскольку каждый новый создаваемый вами объект будет экземпляром одного и того же родительского класса. Хотя вы можете добиться уникальности объектов, (например, также динамически определив для них лексические свойства при помощи встроенной функции addword), во многих случаях желательно, чтобы новые объекты были эквивалентными.
Например, вы можете захотеть использовать в вашей игре деньги и для этой цели определите набор монет. Конечно, можно присвоить каждой монете какой-нибдь уникальный признак (например, создать золотую монету, бронзовую, серебряную и т. п.), однако такой подход вскоре заведет вас в тупик: во-первых, периодическая система элементов все-таки ограничена, а во-вторых, покупки при такой реализации денег станут для игрока тяжким испытанием. Вот здесь-то на помощь и приходят неразличимые объекты.
Для реализации идеи неразличимых объектов в TADS предусмотрено специальное свойство, установив которое равным true, вы можете сообщить синтаксическому анализатору, что данный объект не требуется отличать от других объектов, принадлежащих тому же родительскому классу. Название данного свойства - isEquivalent. Иными словами, если для некоего объекта свойство isEquivalent возвращает true, то СА считает, что вместо него можно использовать любой другой объект - непосредственный потомок того же родительского класса. Если игрок ссылается в своей команде на такой объект, СА просто выберет случайным образом из набора доступных эквивалентных объектов один и подставит его в команду, не делая дополнительных запросов игроку.
Если игрок использует в команде существительное, применимое как к набору эквивалентных объектов, так и к одному или более объектов, не являющихся эквивалентными, СА потребуется устранить неопределенность обычным образом. В подобных случаях в запросе СА эквивалентные объекты будут перечислены только один раз. Предположим, например, что в нашей игре имеется пять золотых монет, эквивалентных друг другу (т. е. их свойство isEquivalent имеет значение true и они являются непосредственными потомками одного и того же родительского класса). Предположим далее, что в одной комнате с золотыми монетами имеются также серебряная и бронзовая монеты.
Сокровищница Вы находитесь в сокровищнице, в которой, очевидно, кто-то уже побывал до Вас. Кто-то жадный. Вы видите здесь бронзовую монету, пять золотых монет и серебряную монету. >взять монету Которую "монету" Вы имеете в виду: бронзовую монету, золотую монету или серебряную монету?Обратите внимание, что (эквивалентные) золотые монеты упомянуты в запросе только один раз, и при этом в единственном числе. Собственно, для золотых монет команда "взять монету" фактически будет воспринята СА как "взять одну из золотых монет".
СА также позволяет игроку манипулировать некоторым количеством эквивалентных объектов. Скажем, игрок мог бы ввести команду "взять 3 золотых монеты", при этом СА случайным образом выбрал бы из всего набора золотых монет три и переместил их в инвентарь игрока.
При определении неразличимых объектов важно определить для них свойство pluraldesc. СА (а также служебные функции, определенные в advr.t используют это свойство для того, чтобы отображать одним словом всю группу неразличимых объектов. Например, для нашего примера с золотыми монетами мы могли бы ввести определение pluraldesc = "золотых монет" (обратите внимание, что свойство определяет словосочетание в винительном падеже - ведь игра будет отображать что-то вроде "пять золотых монет").
Для работы с русскими описаниями в RTADS вместоpluraldesc предпочтительно использовать новое свойство rpluraldesc, "отвечающее" за вывод названий неразличимых объектов на русском языке, поскольку в функцию listcontgen внесены изменения, ориентированные на использование именно этого свойства. Нельзя не отметить остроумное решение, при помощи которого ГрАнд обошел очередной подводный камень русского языка - изменяемость описания предметов в зависимости от их числа (в нашем примере с монетами: сравните - "три монеты" и "восемь монет"). В свойстве rpluraldesc следует определить слово в винительном падеже множественного числа для количества предметов, большего 4 ("монет"). Для количества же предметов от 2-х до 4-х используется свойство rdesc, "по совместительству" служащее для вывода родительного числа единственного числа (в русском языке эти формы совпадают).
В стандартной библиотеке advr.t использование неразличимых объектов поддерживается следующими базовыми служебными функциями:
Функция listcont(obj) "сжимает" наборы неразличимых объектов в списке в один объект, которому предшествует числительное. Если, скажем, некий объект (например, кошелек) содержит пять золотых и десять серебряных монет, то функция выведет "пять золотых монет и десять серебряных монет", а не будет перечислять каждую монету.
Функция listcontcont(obj) "сжимает" наборы неразличимых объектов таким же образом, как и listcont().
Функция itemcnt(list) возвращает количество различимых объектов в списке. Каждый набор неразличимых объектов засчитывается как один объект.
Функция isIndistinguishable(obj1, obj2) проверяет два объекта, определяя, являются ли они неразличимыми; если это так, она возвращает true, в противном случае (т. е. если объекты различимы) возвращается nil.
Функция sayPrefixCount(count) отображает число следующим образом: если число не больше двадцати, оно выводится прописью (например, sayPrefixCount(5) выведет слово "пять"), а если больше, то выводятся цифры.
Кроме того, встроенная функция firstsc(obj) также пригодится при работе с неразличимыми объектами. Эта функция возвращает объект, который является непосредственным родительским классом для объекта obj. Поскольку объекты являются неразличимыми в случае, если их свойства isEquivalent имеют значение true, и оба этих объекта являются потомками одного и того же родительского класса, функция firstsc необходима для реализации проверки эквивалентности двух (и более!) объектов.
Большинству функций и методов, вызываемых синтаксическим анализатором, вся информация, которую им следует "знать" о текущей команде, передается в виде аргументов. Скажем, метод actorAction в качестве аргументов получает текущий глагол, предлог, а также "прямой" и "косвенный" объекты. Однако в игре могут возникать ситуации, когда необходимо знать, какие объекты задействованы в команде, а передаваемые аргументы этих данных не содержат.
Одним из самых ярких примеров такой ситуации являются методы-направления (скажем, методы north и south класса room, соответствующие северу и югу): эти методы не имеют аргумента actor, с помощью которого им можно было бы сообщить, кто именно пытается идти в данном направлении. В большинстве случаев эта информация не нужна, поскольку комната, расположенная к северу от текущей, как правило, не зависит от того, кто именно в нее перемещается. Но иногда при перемещении игрока могут возникать побочные эффекты. Например, переходя по раскачивающемуся веревочному мостику, персонажи могут ронять предметы из своего инвентаря. Также возможно, что актер не сможет идти в некотором направлении, если в его инвентаре имеется определенный предмет. В подобных случаях метод-направление должен "знать", какой именно актер/персонаж идет в указанном направлении.
СА содержит встроенную функцию, позволяющую вам получить актера, глагол, "прямой" объект, предлог и "косвенный" объект для текущей команды, вне зависимости от того, были ли какие-либо из этих объектов переданы текущему методу в качестве аргументов. Данная функция носит название parserGetObj() и имеет один аргумент, который определяет, какой именно объект вам нужен, с использованием следующих констант (они определены в advr.t):
PO_ACTOR - актер (персонаж) текущей команды;
PO_VERB - объект класса deepverb, соответствующий глаголу текущей команды;
PO_DOBJ - "прямой" объект;
PO_PREP - объект-предлог, служащий связкой с "косвенным" объектом;
PO_IOBJ - "косвенный" объект;
PO_IT - объект, на который ссылается местоимение "это" (или "оно");
PO_HER - объект, на который ссылается местоимение "она";
PO_HIM - объект, на который ссылается местоимение "он";
PO_THEM - объект, на который ссылается местоимение "они".
Возвращаемое значение представляет собой либо объект, либо nil, если такого объекта не содержится в текущей команде. Например, для команды, содержащей только "прямой" объект, функция вернет nil для аргументов PO_PREP и PO_IOBJ.
Ниже приведен пример использования parserGetObj для получения "прямого" объекта текущей команды:
local obj := parserGetObj(PO_DOBJ);Функция parserGetObj() возвращает полезную информацию при вызове в процессе обработки команды, охватывающей период от метода actorAction и по метод doVerb или ioVerb включительно. Вызов данной функции вне этих пределов не имеет смысла; если же он будет осуществлен, будет просто возвращено значение nil. Причиной такого ограниченного "срока жизни" этой функции является то, что до вызова метода actorAction СА попросту "не знает" окончательных значений для объектов в команде, поскольку подбор объектов, соответствующих словам команды, продолжается как раз вплоть до вызова этого метода. Обратите внимание на специальные случаи, когда parserGetObj() не работает: при вызове функций init() и preinit(); в процессе устранения неопределенности (в т. ч. в процессе "тихих" вызовов verDoVerb и verIoVerb); при вызове roomCheck; в процессе выполнения preparse() и preparseCmd(); а также в процессе обработки демонов и запалов.
Обратите внимание на одно важное исключение из правила ограничения "срока жизни": объект, соответствующий актеру, можно получить на любой стадии после выполнения функции preparse(). Это возможно благодаря тому, что определение актера выполняется СА на самых ранних стадиях обработки команды.
Функция parserGetObj() возвращает информацию, соответствующую текущей команде. Если происходит рекурсивный вызов команды (с использованием execCommand()), то parserGetObj() возвращает информацию по вложенной команде; как только выполнение execCommand() заканчивается, возвращаемые parserGetObj() данные вновь будут относиться к "внешней" команде.
В TADS реализован механизм, который позволяет игровой программе получать из СА определенные данные о глаголе. Это происходит при помощи встроенной функции verbinfo. Этой функции передаются два аргумента: первый соответствует объекту класса deepverb, для которого требуется получить информацию; вторым (необязательным) аргументом может быть объект-предлог. При вызове verbinfo с единственным аргументом-глаголом она вернет значения свойств для методов верификации и действия, определяемых свойством doAction данного глагола. При вызове же с двумя аргументами будут возвращены значения методов верификации и действия, определяемые свойством ioAction данного глагола для данного предлога.
Типом значения, возвращаемым verbinfo, является список. Содержание списка зависит от того, с каким количеством аргументов осуществлялся вызов функции.
При вызове verbinfo с одним аргументом (глаголом) возвращаемый список содержит два элемента:
[1] = указатель (pointer) на метод-верификатор (verDoXxxx)
[2] = указатель (pointer) на метод-действие (doXxxx).При вызове verbinfo с аргументами, соответствующими глаголу и предлогу, возвращается список из четырех элементов:
[1] = указатель на метод-верификатор "прямого" объекта (verDoXxxx)
[2] = указатель на метод-верификатор "косвенного" объекта (verIoXxxx)
[3] = указатель на метод-действие "косвенного" объекта (ioXxxx)
[4] = true, если для ioAction установлен флаг [disambigDobjFirst], nil в противном случаеВ любом случае, если для глагола не определены doAction либо ioAction, verbinfo возвращает nil.
Обратите внимание, что в будущих версиях TADS, возможно, будут введены дополнительные флаги наподобие disambigDobjFirst; при этом длина списка будет увеличиваться, чтобы была возможность отобразить информацию и по новым флагам. Поэтому, в целях обеспечения совместимости с будущими версиями, не рекомендуется использовать в игровых программах управляющие операторы (условные, цикла и т. п. ), использующие в качестве условия длину возвращаемого списка. Также следует отметить, что размер списка в будущем может измениться только в одну сторону - в сторону увеличения.
Для глагола removeVerb, определенного в advr.t, будет возвращен следующий список значений:
при вызове verbinfo(removeVerb) будет возвращено [&verDoUnwear &doUnwear]
при вызове verbinfo(removeVerb, fromPrep) будет возвращено [&verDoRemoveFrom &verIoRemoveFrom &ioRemoveFrom nil]
при вызове verbinfo(removeVerb, aboutPrep) будет возвращено nil, так как для предлога aboutPrep объект-глагол removeVerb не определяет соответствующего метода ioAction.
Как правило, слова, которые игрок может использовать в игре, определяются статически, с использованием лексических свойств объектов (noun, adjective и др.). Эти слова образуют словарный запас или словарь игры, который "встраивается" в игру на этапе компиляции и впоследствии уже не меняется.
Однако в некоторых случаях вам может потребоваться изменить лексические свойства для того или иного объекта на этапе выполнения игровой программы. Например, если у вас в игре игрок находит собаку, которой он может дать кличку; очевидно, что впоследствии у игрока должна быть возможность обращаться к собаке при помощи этой клички.
В TADS имеются встроенные функции, позволяющие динамически изменять набор слов, соответствующих объекту, в процессе выполнения игровой программы.
Функция addword добавляет новое лексическое свойство для объекта. Эта функция имеет три аргумента: сам объект, указатель на лексическое свойство (существительное noun или прилагательное
adjective), а также значение строкового типа. Строковое значение будет добавлено в словарь в качестве части речи, определяемой указателем на лексическое свойство, и поставлено в соответствие объекту. Пример: local dogName; "Как вы назовете собаку? "; dogName := input(); addword(doggy, &noun, dogName);
Действие функции delword противоположно addword: функция удаляет лексическое свойство, ранее определенное для объекта. Функция имеет те же аргументы, что и addword. Можно удалять как лексические свойства, добавленные динамически при помощи addword, так и определенные статически в исходном коде игры.
delword(doggy, &noun, dogName);Примечание переводчика: в русском языке данные функции применимы, но с некоторыми оговорками: дело в том, что, в отличие от английского языка, необходимо учитывать также падеж и добавлять не одно слово, а сразу несколько для всех падежных форм. Скажем, если в нашем примере игрок решит назвать собаку Шариком, потребуется, помимо слова 'Шарик', добавить также 'Шарика', 'Шарику', 'Шарике' и т. п. Аналогичную проблему потребуется решать и при удалении лексических свойств. Простых путей здесь нет, в каждом конкретном случае придется изобретать что-то свое. Ну, по крайней мере пока кто-нибудь не напишет библиотеку, выполняющую подобные функции;).
Функция getwords() позволяет получить список слов, соответствующих определенному лексическому свойству заданного объекта. Эта функция особенно полезна при работе с динамически измененными лексическими свойствами, но вполне применима и для статических определений этих свойств. Функция вызывается с двумя аргументами: один соответствует объекту, а другой является указателем на лексическое свойство. Функция возвращает список строковых значений, соответствующих словам, определенным для данного лесического свойства данного объекта. Например, если мы определим объект под названием redBook и определим для него прилагательные (adjective) 'красная', 'маленькая' и 'тонкая', то при следующем вызове:
getwords(redBook, &adjective);
будет возвращен следующий список:
['маленькая' 'красная' 'тонкая']
Обратите внимание, что порядок слов в возвращаемом списке будет произвольным (и непредсказуемым), поэтому при использовании данной функции рассчитывать на выборку слов в определенной последовательности не следует.
При запуске игры СА "предполагает", что главному персонажу (ГП) соответствует объект Me, определяемый самой игрой. (Файл stdr.t, входящий в состав дистрибутива TADS, содержит определение этого объекта, которое вполне удовлетворительно подходит для большинства игр).
В большинстве игр используется только один ГП, и в большей части случаев вполне можно обойтись только этим объектом Me. Однако в некоторых играх используется более одного ГП, при этом "вид от первого лица" будет передаваться другому персонажу в игре.
В каждый момент времени существует один-единственный объект, который СА рассматривает как текущего ГП. Команды, набираемые без указания конкретного персонажа (типа "Ллойд, иди на восток"), передаются на обработку объекту, соответствующему текущему ГП.
Изменить текущего главного персонажа можно, используя встроенную функцию parserSetMe(). В качестве единственного аргумента этой функции передается тот объект, который вы хотите сделать новым ГП. Например, чтобы сделать новым главным персонажем некий объект Lloyd, можно использовать следующий вызов:
parserSetMe(Lloyd);Вы можете в любой момент получить от СА объект, соответствующий текущему ГП, используя функцию parserGetMe() (она не имеет аргументов):
local curPlayer := parserGetMe();Если вы изменяете ГП в ходе игры, то внимательно следите за тем, чтобы объект Me не использовался в вашем программном коде в явном виде. Вместо этого всегда используйте parserGetMe() для получения текущего ГП. Конечно, в случае, если вам по каким-либо причинам нужно произвести некие действия именно над объектом Me, безотносительно того, какой объект в данный момент соответствует ГП, вам потребуется явно упомянуть Me. Все стандартные функции, определенные в библиотеке advr.t, всегда используют для получения текущего ГП вызов parserGetMe.
В стандартной библиотеке advr.t определена функция под названием switchPlayer(), реализующая высокоуровневый механизм смены ГП. Если вы в вашей игре используете библиотеку advr.t, то вместо непосредственного вызова parserSetMe() лучше использовать switchPlayer(), поскольку эта функция, наряду с переключением ГП посредством parserSetMe(), выполняет еще ряд необходимых дополнительных действий относительно объектов, соответствующих старому и новому ГП. В качестве аргумента функции switchPlayer() передается объект, соответствующий новому ГП, например:
switchPlayer(Lloyd);
В TADS предусмотрен ряд удобных средств, облегчающих создание определенных видов общих определений.
Во-первых, TADS позволяет определять глаголы-синонимы для конкретного объекта. Иногда встречаются ситуации, когда желательно, чтобы два (или более) разных глагола взаимодействовали с неким объектом одинаковым образом; например, если у вас в игре есть пульт управления с сенсорной панелью (она же тач-пад), то игрок может попытаться взаимодействовать с ней при помощи разных глаголов, а именно: "нажать", "надавить", "коснуться" (на самом деле, еще много синонимов можно придумать; для примера ограничимся этими). Глаголы "нажать" и "надавить" уже определены как синонимы (им соответствует один и тот же глагольный объект pushVerb), но глаголу "коснуться" соответствует другой объект (touchVerb). Поскольку глагол "коснуться" отличается от глагола "нажать", то в данном случае придется найти способ "заставить" нашу сенсорную панель одинаково реагировать на оба метода-верификатора, а также оба метода-действия для данных глаголов. Один из возможных способов будет таким:
verDoPush(actor) = { self.verDoTouch(actor); } doPush(actor) = { self.doTouch(actor); }Но это довольно утомительный путь - в особенности, если у вас имеется больше двух-трех глаголов, которые должны работать как синонимы. Вместо подобных громоздких определений можно просто использовать средство TADS для определения глаголов-синонимов на уровне объекта. Для этого применяется специальное псевдо-свойство doSynonym:
doSynonym('Touch') = 'Push'Это простое определение действует точно так же, как и приведенные до нее громоздкие конструкции. Это определение следует читать так: глагол "Push" ("нажать") будет синонимом для глагола "Touch" ("коснуться") для данного "прямого" объекта.
Для глагола можно указывать и более одного синонима, просто перечислив эти синонимы в данном определении справа от знака равенства, разделяя их пробелами. Скажем, если в нашем примере с сенсорной панелью мы хотим, чтобы игрок мог манипулировать ею, потерев ее, нам потребуется определить новый глагол (пусть он будет называться rubVerb), а наше определение видоизменить следующим образом:
doSynonym('Touch') = 'Push' 'Rub'Данное определение читается следующим образом: синонимами глагола "Touch" ("коснуться") для данного "прямого" объекта будут глаголы "Push" ("нажать") и "Rub" ("потереть"). Обратите внимание, что возможность использования в определении нескольких глаголов может помочь лучше запомнить, как правильно формулировать такие определения: все глаголы-синонимы ставятся в соответствие одному-единственному глаголу, поэтому этот "основной" глагол - т. е. глагол, обработчик для которого будет вызываться при использовании в введенной игроком команде того или иного его синонима - должен стоять там, где допускается использование только одного глагола, т. е. в скобках (или слева от знака равенства). Таким образом, если игрок введет команду "нажать на панель", СА будет вызывать обработчики verDoTouch and doTouch (соответствующие глаголу "коснуться").
Конструкция с использованием ioSynonym работает также, но для обработчиков типа verIoГЛАГОЛ и ioГЛАГОЛ. В то время как doSynonym используется для "синонимизации" глаголов применительно к "прямому" объекту, ioSynonym делает глаголы синонимами, если данный объект используется совместно с ними в качестве "косвенного".
Необходимо еще раз подчеркнуть, что синонимы, определенные с использованием doSynonym и ioSynonym, являются таковыми лишь для того объекта, который содержит соотетствующее определение. Другие объекты не будут затронуты данным определением, так что если в нашем примере кроме сенсорной панели будет присутствовать, скажем, дверь, то для нее (а также и для всех остальных объектов в игре) глаголы "коснуться" и "нажать" останутся различными. В то же время определения глаголов-синонимов наследуются точно так же, как и все прочие свойства (и, в частности, обработчики глаголов).
Второй удобный инструмент позволяет указать, что, если глагол направлен на определенный объект, то он должен применяться к некоторому другому объекту. Это часто бывает удобным, когда вы используете в вашей игре сложные объекты, состоящие из нескольких составных частей. Пусть, например, у нас имеется стол с выдвижным ящиком. Стол и ящик реализованы как различные объекты, но вполне возможно, что игрок наберет "открыть стол", пытаясь открыть ящик. Чтобы реализовать такое "перенаправление" действия от стола к ящику, можно использовать, например, такое определение (объект desk соответствует столу, а объект deskDrawer - ящику):
desk: fixeditem noun = 'стол' sdesc = "стол" location = office verDoOpen(actor) = { deskDrawer.verDoOpen(actor); } doOpen(actor) = { deskDrawer.doOpen(actor); } ;Эта конструкция вполне работоспособна, но использование ее для "перенаправления" нескольких глаголов будет несколько утомительным, а ведь даже в нашем, достаточно простом, примере вам наверняка потребуется переопределить глагол "открыть", а также (весьма вероятно) "заглянуть в" и "положить в". Сократить объем связанной с этим монотонной работы по копированию в буфер/вставке/печати позволит следующий синтакс определения "перенаправления" глагола (разумеется, оно должно размещаться внутри объекта desk):
doOpen -> deskDrawer(Обратите внимание, что символ "->" составлен из знака дефиса, за которым следует знак "больше"). Эта единственная строка заменяет вышеприведенные определения методов verDoOpen и doOpen. Она означает, что при вызове любого из этих методов для объекта, соответствующего столу (desk), будет вызван соответствующий метод объекта-ящика (deskDrawer).
Когда в процессе обработки команды синтаксическим анализатором происходит ошибка, СА сообщает игроку, в чем именно она состоит, используя сообщение, по возможности наиболее точно описывающее суть этой ошибки. Одной из задач при разработке СА было сделать его удобным для пользователя; хотя сообщения об ошибках по определению не являются чем-то дружественным пользователю, исключить их полностью нельзя, поэтому необходимо было по крайней мере наполнить их информационным содержанием. Игрок всегда должен понимать, почему его команда не была принята игрой, чтобы быть в состоянии иначе сформулировать эту команду.
СА позволяет вам задавать в вашей игровой программе собственные, "нестандартные" сообщения об ошибках. Большинство ошибок обрабатываются определяемой самой игрой функцией parseErrorParam или parseError. Для большинства ошибок действует следующий алгоритм: если в вашей игре определена функция parseErrorParam, СА вызывает именно ее; в противном случае, если определена функция parseError, то будет вызываться она; в иных случаях (т. е. если ни одна из этих функций не определена) будет отображаться стандартное сообщение об ошибке.
Примечание переводчика: в дистрибутив RTADS входит файл errorru.t, который, помимо ряда других вещей, использует именно эти функции для замены стандартных сообщений об ошибках TADS. Описание работы всех функций этого модуля (и вообще особенности обработки команды в русской версии TADS по сравнению с английской) будет включено в раздел, посвященный последовательности синтаксического разбора команд.
Некоторые из сообщений СА более специфичны, и используют несколько другие функции. Список сообщений об ошибках см. в приложении F.
Если в игре не определена функция parseErrorParam, СА вызывает вместо нее parseError (если функция с таким названием имеется). Обратите внимание, что parseError никогда не будет вызываться, если в игре помимо нее определена и функция parseErrorParam.
Функция parseError вызывается СА с двумя аргументами, соответствующими номеру сообщения и тексту сообщения по умолчанию. Номер сообщения - это фактически код ошибки; каждому коду ошибки, как правило, должно соответствовать свое сообщение. Текст сообщения по умолчанию соответствует тексту того сообщения, которое отобразил бы для данного кода ошибки СА, если бы функция parseError в игре не была определена.
Вероятно, в других частях настоящего руководства вам уже встречались упоминания кодов/номеров сообщений; они относятся именно к кодам ошибок, принятым для функции parseError. Подробный список кодов ошибок будет приведен далее.
СА вызывает функцию parseErrorParam (если таковая определена), передавая ей два или более аргументов. Первый из них - это номер сообщения (код ошибки), второй - текст сообщения по умолчанию; смысл этих аргументов такой же, как для функции parseError. Все прочие аргументы определяют значения, подставляемые вместо переменных значений в сообщении по умолчанию. Поскольку данной функции может передаваться переменное количество аргументов, при ее определении список аргументов следует закончить многоточием после второго аргумента:
parseErrorParam: function(errnum, str, ...) { // наберите здесь свой программный код }Сообщение по умолчанию может содержать одну или более последовательностей, начинающихся с "%" (подстановочных последовательностей). За символом "%" в них следует символ, определяющий тип значения, которое должно будет подставляться вместо этого значения. Если вы программировали в C, то должны быть знакомы с этими форматными символами: "%s" задает значение строкового типа (string), а "%d" - целочисленное значение (decimal). (В языке C имеется целый ряд других форматных символов, а также набор модификаторов, но в TADS используются только "%d" и "%s").
Для каждой подстановочной последовательности в сообщении по умолчанию функции parseErrorParam будет передаваться дополнительный аргумент, определяющий конкретное значение, на которое будет заменяться эта последовательность. Например, для ошибки с кодом 2, соответствующей сообщению 'Я не знаю слова "%s".' ("I don't know the word '%s'" в английском варианте), функции будет дополнительно передаваться третий аргумент - строка, содержащая неизвестное синтаксическому анализатору слово.
Значения, возвращаемые parseErrorParam и parseError, формируются по одному и тому же принципу для обеих функций.
Ваша функция должна возвращать либо nil, либо строковое значение (заключенное в апострофы). Если функция возвращает nil, это означает, что вы хотите использовать сообщение по умолчанию - эффект будет такой же, как если бы функции parseError и parseErrorParam не были определены совсем. Если функция возвращает строковое значение, то эта строка будет отображена вместо сообщения, выводимого СА по умолчанию.
Обратите внимание, что некоторые из сообщений по умолчанию содержат подстановочную последовательность '%s'. При выводе сообщения эта последовательность заменяется неким строковым значением; скажем, в сообщении с кодом 2 ("Я не знаю слова...") она заменяется словом, которое СА не смог распознать. Аналогичным образом последовательность '%c' в сообщении заменяется одним символом, а последовательность '%d' - числом. В любом сообщении, определяемом вами для замены сообщения по умолчанию, вы можете использовать последовательность '%&', если сообщение по умолчанию использует подстановочную последовательность того же вида.
Сообщения с номерами, меньшими 100, являются "полноценными" - т. е. они не являются частями более сложных сообщений. Сообщения с кодами 100 и выше несколько отличаются; они представляют собой фрагменты других сообщений; при составлении целого сообщения используется комбинация нескольких таких фрагментов (иногда к ним добавляется и дополнительный текст). Таким образом, если вы хотите специальным образом отформатировать выводимое сообщение (например, заключать все выводимые СА сообщения в квадратные скобки), то для "полноценных" сообщений (с кодами менее 100) это не вызовет никаких проблем, а вот с сообщениями-фрагментами придется быть осторожнее, т. к. скобки могут оказаться внутри окончательного сообщения; чтобы избежать этого, достаточно добавить условный оператор, который проверял бы код сообщения выполнял бы необходимое форматирование только в том случае, если этот код окажется меньше 100.
Смысл большей части сообщений в первой группе понятен из них самих (русский текст сообщений об ошибках можно посмотреть далее в тексте либо в файле errorru.t, входящем в дистрибутив RTADS; последнее в некоторых случаях может оказаться предпочтительнее, так как системные библиотеки RTADS меняются чаще, чем данная документация), однако некоторые требуют дополнительных разъяснений.
Сообщения с номерами 3 , 10 и 11 выводятся, когда одно из слов, использованных игроком в команде, может быть отнесено к числу объектов в игре, превышающему некоторый лимит. Обратите внимание, что лимит действует вне зависимости от того, присутствуют ли все эти объекты в текущем помещении (иначе говоря, доступны ли они все для игрока) - данное ограничение распространяется на все объекты, определенные в игре. (Именно поэтому объявление лексического свойства для какого-либо из общих классов - типа thing или item - является нежелательным). Начиная с версии TADS 2.2, максимальное количество объектов, к которым может быть применено одно и то же лексическое свойство, составляет 200; в более ранних версиях оно было равно 100.
Сообщение с кодом 9 выводится, если набор слов, использованных игроком в команде, нельзя применить целиком к одному объекту. Имеется в виду следующее: пусть в игре имеются объекты, соответствующие синей книге и красной коробке, при этом для них определены лексические свойства - прилагательные "синяя" и "красная", существительные "книга" и "коробка". Если игрок наберет, скажем, "осмотреть красную книгу", то словосочетание "красная книга" будет воспринято СА как вполне корректное по форме, но ему не будет соответствовать ни одного объекта. При этом и будет выведено сообщение 9.
Сообщения 13 и 14 используются для местоимений. Если игрок использует в команде одно из местоимений в единственном числе (он/она/оно), и окажется, что объект, на который ссылается это местоимение, более недоступен, то будет выведено сообщение 13. Сообщение 14 используется в тех же целях с местоимением "они".
Сообщение 15 выводится, если игрок использовал в своей команде слово "все", но подходящих объектов не оказалось.
Сообщение 28 соответствует случаю, когда игрок указывает в своей команде несколько "прямых" объектов с глаголом, требующим устранения неопределенности перед выполнением обработчиков. Такие глаголы допускают использование только одного "прямого"объекта в команде.
Сообщение 30 отображается, когда игрок вводит команду типа "взять три монеты", а на самом деле доступно меньшее число соответствующих объектов.
Сообщение 38 выводится, когда объект, изначально успешно прошедший проверку пригодности, впоследствии не проходит повторную проверку (это может происходить, как правило, при использовании нескольких объектов в команде).
Сообщение 39 отображается, когда при выполнении функции execCommand какой-либо объект оказывается невидимым и недоступным.
Когда игрок адресует команду некому актеру в игре (не главному персонажу), причем актер виден игроку (его метод isVisible(parserGetMe()) возвращает true), но не является допустимым актером (его метод validActor возвращает nil), СА отображает сообщение 31.
В скобках приведены оригинальные английские тексты сообщений об ошибках синтаксического анализа. Русский текст сообщений определен в библиотеке errorru.t, входящей в дистрибутив RTADS. Если при компиляции игры исключить эту библиотеку (ну и, соответственно, не определить где-нибудь в игре альтернативную функцию parseError или parseErrorParam), игра будет отображать англоязычные сообщения об ошибках.
1 - Я не понимаю такую пунктуацию: "%c".
(I don't understand the punctuation "%c".)
2 - Я не знаю слова "%s".
(I don't know the word "%s".)
3 - Слово "%s" относится к слишком большому числу объектов.
(The word "%s" refers to too many objects.)
4 - Я думаю, Вы собирались написать после существительного определение.
(I think you left something out after "all of".)
5 - Я думаю, Вы собирались написать определение после "оба".
(There's something missing after "both of".)
6 - Я ожидал существительное после "вида".
(I expected a noun after "of".)
7 - Ошибка номер7. Кто это увидел, сообщите как она возникла!
(An article must be followed by a noun.)
(На самом деле, это сообщение о том, что в команде используется артикль без существительного. Поскольку в русском языке артикли не используются, возникновение такой ошибки в русскоязычной игре теоретически исключено).
8 - (You used "of" too many times.)
Примечание: у данного сообщения отсутствует русский эквивалент, поскольку оно является "тяжелым наследием" ранних версий TADS и более не используется.
9 - Я не вижу здесь объект "%s".
(I don't see any %s here.)
10 - Вы ссылаетесь на слишком большое количество объектов словом "%s".
(You're referring to too many objects with "%s".)
11 - Вы ссылаетесь на слишком большое количество объектов.
(You're referring to too many objects.)
12 - Вы можете говорить только с одной персоной одновременно.
(You can only speak to one person at a time.)
13 - Я не знаю на что Вы ссылаетесь словом "%s".
(I don't know what you're referring to with '%s'.)
14 - Я не знаю на что Вы ссылаетесь.
(I don't know what you're referring to.)
15 - Я не вижу то, на что Вы ссылаетесь.
(I don't see what you're referring to.)
16 - Я не вижу здесь этого.
(I don't see that here.)
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.)
22 - Похоже, после вашей команды есть лишние слова.
(There seem to be extra words in your command.)
23 - (internal error: verb has no action, doAction, or ioAction)
Примечание: у данного сообщения отсутствует русский эквивалент, поскольку оно является внутренним. Игрок его видеть не должен; если оно появляется, значит, проблема в самой игровой программе (конкретнее, оно означает, что для глагола не определены методы action, doAction или ioAction).
24 - Я не понимаю это предложение.
(I don't recognize that sentence.)
25 - Нельзя использовать много косвенных объектов.
(You can't use multiple indirect objects.)
26 - Нет команды для повторения.
(There's no command to repeat.)
27 - Эту команду нельзя повторить.
(You can't repeat that command.)
28 - Эту команду нельзя применять к множеству объектов.
(You can't use multiple objects with this command.)
29 - Я думаю, Вы собирались написать определение после "любой".
(I think you left something out after "any of".)
30 - Я вижу только %d из них.
(I only see %d of those.)
31 - С этим нельзя разговаривать.
(You can't talk to that.)
32 - (Internal game error: preparseCmd returned an invalid list)
Примечание: у данного сообщения отсутствует русский эквивалент, поскольку оно является внутренним. Игрок его видеть не должен; если оно появляется, значит, проблема в самой игровой программе (конкретнее, оно означает, что функция preparseCmd вернула некорректный список).
33 - (Internal game error: preparseCmd command too long)
Примечание: у данного сообщения отсутствует русский эквивалент, поскольку оно является внутренним. Игрок его видеть не должен; если оно появляется, значит, проблема в самой игровой программе (конкретнее, оно означает, что команда слишком длинна для preparseCmd).
34 - (Internal game error: preparseCmd loop)
Примечание: у данного сообщения отсутствует русский эквивалент, поскольку оно является внутренним. Игрок его видеть не должен; если оно появляется, значит, проблема в самой игровой программе (конкретнее, оно означает зацикливание функции preparseCmd).
38 - Здесь больше этого не видно.
(You don't see that here any more.)
39 - Здесь этого не видно.
(You don't see that here.)
Следующим ошибкам не соответствует сообщений, они лишь возвращают код. Они в основном используются для передачи информации от системы устранения неопределенностей игровой программе (при помощи функции parseNounList), поэтому их не следует использовать в обращениях к parseError или parseErrorParam; они приведены здесь лишь для полноты картины.
40 (сообщение отсутствует) - невозможно создать новый нумерованный объект
41 (сообщение отсутствует) - disambigXobj возвратила неправильный код
42 (сообщение отсутствует) - запрос на устранение неопределенности возвратил пустой список
43 (сообщение отсутствует) - повторить устранение неопределенности объекта из самой команды
44 (сообщение отсутствует) - неопределенность объектов не устранена
Следующий набор сообщений используется для того, чтобы задавать игроку дополнительные вопросы, когда введенная им (игроком) команда может относиться более чем к одному объекту. СА в таких случаях должен спрашивать игрока, какой именно из возможных объектов он имеет в виду. Обратите внимание, что приведенные здесь сообщения будут использоваться только в случае, если в вашей игре не определена функция parseDisambig; если последняя имеется, то при устранении неопределенностей будет использована именно она (и вопросы СА будут определяться тоже в ней), а не указанные здесь сообщения/коды ошибок. Поскольку в RTADS по умолчанию функция parseDisambig определена (в том же файле errorru.t), то данная группа сообщений не используется и не имеет русского эквивалента (в скобках приведен перевод (там, где он требуется) просто для справки). Вам придется плотно работать с ними лишь в том случае, если вы решите отказаться от использования parseDisambig.
100 - Let's try it again: (Давайте попробуем еще раз:)
101 - Which %s do you mean, (Который "%s" вы имеете в виду)
102 - ,
103 - or (или)
104 - ?
Следующий блок сообщений используется, если глагол не может быть применен к объекту. Эти сообщения используются, если игрок использует глагол с объектом, но в этом объекте не определены (и не наследуются от родительского класса) соответствующие методы-верификаторы (verDoГлагол или verIoГлагол). Кодам №№ 111 и 114 соответствуют сообщения, состоящие из одного пробела.
Обратите внимание, что приведенные здесь сообщения будут использоваться только в случае, если в вашей игре не определена функция parseError2; если последняя имеется, то вместо вывода соответствующего сообщения об ошибке СА будет вызывать именно ее. Поскольку в RTADS по умолчанию функция parseError2 определена (в файле errorru.t), то данная группа сообщений не используется и не имеет русского эквивалента (в скобках приведен перевод (там, где он требуется) просто для справки).
110 - I don't know how to (Я не знаю, как)
Следующее сообщение используется в качестве разделителя при выводе списка объектов. Например, если игрок введет команду "взять все", то СА попытается применить глагол "взять" поочередно ко всем доступным объектам; перед тем, как применить глагол к очередному объекту, СА сначала выводит (с новой строки) название этого объекта, а затем данное сообщение-разделитель. Таким образом, игрок наглядно может видеть, каков именно был результат его действия для каждого из объектов.
120 - :
Следующие сообщения используются для того, чтобы пометить объекты, используемые в команде по умолчанию. Если игрок при вводе команды пропустил объект, а СА удалось определить, что данный глагол требует определенного объекта, СА выведет название этого объекта в обрамлении этих сообщений. Скажем, если игрок наберет просто "копать", то СА, вполне вероятно, сможет определить, что наиболее подходящим "прямым" объектом для этой команды будет объект, соответствующий почве, в качестве предлога (связки) подойдет "при помощи", а "косвенным" объектом может быть лопата; в этом случае он выведет сообщение 130, затем название "прямого" объекта (почвы), затем сообщение 131; после этого он снова выведет сообщение 130, название предлога, сообщение 132 (по умолчанию оно состоит из одного пробела), затем название "косвенного" объекта (лопаты), а затем снова сообщение 131. Для игрока это будет выглядеть примерно так (в идеале;):
>копать (почву) (при помощи лопаты)Эти сообщения не будут использоваться, если в вашей игре определена функция parseDefault. Поскольку в RTADS по умолчанию она определена (в файле errorru.t), то вам не потребуется как-то их определять/модифицировать/учитывать.
Если игрок пропускает в своей команде объект, но СА не сможет найти подходящий объект по умолчанию, то игроку будет выдан запрос относительно объекта с использованием следующих сообщений.
Обратите внимание, что эти сообщения не будут использоваться, если в вашей игре определена функция parseAskobj, parseAskobjActor, или parseAskobjIndirect. Поскольку в RTADS (в файле errorru.t) определена функция parseAskobjActor, данные сообщения здесь по умолчанию использоваться не будут. Русский перевод сообщений в скобках приведен для справки.
140 - What do you want to (Что вы хотите)
Следующее сообщение отображается, если игрок обратился к объекту в игре, определенному при помощи общего прилагательного-нумератора (adjective = '#'), но при этом не указал в своей команде номер этого объекта.
160 - Вам придется подробнее описать какой "%s" Вы имеете в виду.
(You'll have to be more specific about which %s you mean.)
Примечание: начиная с версии TADS 2.5.1, сообщение с кодом 200 более не используется. (В более ранних версиях, когда игрок использовал в команде слова, которые могли относиться более чем к одному объекту, и эти объекты были видимы, но недоступны, СА вызывал метод cantReach для каждого объекта после того, как выводил название этого объекта (в соответствии со значением свойства sdesc), за которым следовало сообщение с кодом 200. СА в настоящее время использует метод multisdesc и сообщение 120, т. е. используется точно такой же механизм вывода информации, как и для любых других списков объектов.
200 - : (более не используется)
Ниже приведен пример функции parseError, которая выводит стандартные сообщения СА, заключая их в квадратные скобки. Она обрабатывает только сообщения, код которых меньше 100, поскольку остальные сообщения представляют собой фрагменты более сложных сообщений, и поэтому не должны обрабатываться как целые сообщения.
parseError: function(num, str) { if (num < 100) return '[' + str + ']'; else return nil; }Прекрасный пример функции parseError, выполняющей значительно более сложную обработку, пользователи RTADS могут посмотреть в файле errorru.t;).
В некоторых случаях вам может потребоваться выполнить отдельные этапы стандартной синтаксической обработки над некоей абстрактной строкой (неважно, введенной ли игроком, или полученной из других источников). Для синтаксического анализатора TADS реализован целый ряд функций, с помощью которых вы можете обращаться к тем или иным его модулям.
Если вы хотите разбить строку на лексемы непосредственно из игры, то можно воспользоваться следующей встроенной функцией СА:
local tokenList; tokenList := parserTokenize(commandString);Параметр commandString представляет собой произвольную текстовую строку. Данная функция просматривает строку и разбивает ее на лексемы, которые и возвращает в виде списка строковых значений. Строки-лексемы подчиняются тем же правилам, что и список лексем, передаваемый функции preparseCmd(), включая преобразование специальных слов (таких, как "это" и "и").
Если строка содержит недопустимые символы (такие, как знаки пунктуации, которые не должны присутствовать в лексемах), функция возвращает nil, не выводя никаких сообщений.
После разбиения строки на отдельные слова-лексемы вы можете получить список типов этих лексем. Встроенная функция parserGetTokTypes просматривает словарь СА и возвращает список типов лексем.
Вызов parserGetTokTypes осуществляется следующим образом:
typeList := parserGetTokTypes(tokenList);, где tokenList - список лексем.
Возвращаемым значением будет список чисел. Каждый элемент результирующего списка будет содержать тип соответсвующего элемента списка лексем (скажем, в нашем примере typeList[3] содержит данные о типе лексемы tokenList[3] и т. д.).
Типы в результирующем списке получаются путем комбинирования следующих значений, определенных в advr.t:
PRSTYP_ARTICLE - артикль (в английском языке a, an, the, в русском языке не используется)
PRSTYP_ADJ - прилагательное (adjective)
PRSTYP_NOUN - существительное (noun)
PRSTYP_PREP - предлог (preposition)
PRSTYP_VERB - глагол (verb)
PRSTYP_SPEC - специальное слово ("это", "все" и т. п.)
PRSTYP_PLURAL - множественное число
PRSTYP_UNKNOWN - неизвестное (т. е. отсутствующее в словаре) слово
Данные коды типов формируются (и анализируются) побитно, поэтому они могут комбинироваться при помощи оператора побитного сложения (ИЛИ) (оператор ("|"). Например, для лексемы, присутствующей в словаре как в качестве существительного, так и прилагательного, будет возвращено значение типа, равное (PRSTYP_ADJ | PRSTYP_NOUN).
Поскольку в одном элементе списка типа может быть возвращено более одного кода PRSTYP_xxx, вам необходимо будет использовать оператор побитного умножения (И) ("&") для вычленения конкретного типа. Например, если вам требуется проверить, является ли некоторая лексема в том числе и существительным, то можно использовать следующее выражение:
((typeList[3] & PRSTYP_NOUN) != 0)
СА содержит функцию под названием parserDictLookup(), которая позволяет получить список объектов, определяющих тот или иной набор лексических свойств. Эту функцию можно использовать для организации собственной, "нестандартной" обработки словосочетаний. Вызов функции осуществляется так:
objList := parserDictLookup(tokenList, typeList);Аргумент tokenList содержит список строковых значений - лексем, которые вы хотите найти в словаре; этот список строится по тому же формату, что и список, возвращаемый функцией parserTokenize(), поэтому результат выполнения последней фунции можно непосредственно использовать в качестве аргумента для parserDictLookup().
Аргумент typeList является списком типов лексем. Каждый элемент списка typeList определяет тип соответствующего элемента списка tokenList. В этом списке используются те же коды PRSTYP_xxx, которые возвращаются функцией parserGetTokTypes(), однако каждому элементу списка typeList должно соответствовать только одно значение PRSTYP_xxx (т. е. элементы в списке типов не могут быть комбинациями нескольких таких значений).
Поскольку элементы списка typeList должны содержать "индивидуальные" (т.е. однозначные) коды PRSTYP_xxx, а не их комбинации, то в общем случае использовать результат выполнения функции parserGetTokTypes() непосредственно в качестве аргумента для parserDictLookup() нельзя. Вместо этого вам необходимо определить, каким образом должно будет интерпретироваться то или иное слово в списке лексем; это выполняется путем назначения каждому слову отдельного типа. Каким образом вы назначите этот тип, зависит только от вас. Например, если вы хотите проанализировать словосочетание, то можете указать, что все элементы списка лексем, кроме последнего, должны быть прилагательными, а последний элемент должен быть существительным. Назначение того или иного типа будет зависеть от того порядка синтаксического анализа, который вы хотите организовать, и от тех правил синтаксиса, которые вы собираетесь применить к вводимому тексту.
parserDictLookup() возвращает список всех объектов в игре, в определении лексических свойств которых присутствуют все переданные слова (tokenList), причем соответствующих типов (typeList). Если таких объектов не найдется, будет возвращен пустой список.
Для глаголов, использующихся в комбинации с предлогами (например, "посмотри в"), применяется специальная форма строки-лексемы. Чтобы просмотреть информацию о глаголе, состоящем из двух слов, необходимо передать строку, включающую оба этих слова, разделенных пробелом. Функция parserTokenize() таких строк не возвращает в принципе, поскольку разбивает входящую команду на отдельные слова, поэтому такого рода лексемы вам потребуется восстанавливать самостоятельно. Например, чтобы определить объект класса deepverb, соответствующий глаголу "положить в", можно использовать следующий оператор:
objList := parserDictLookup(['положить в'], [PRSTYP_VERB]);Обратите внимание, что функция parserDictLookup() фактически просто анализирует словарь игры. Она не выполняет никаких дополнительных проверок (таких как проверка доступности, видимости объекта или устранение неопределенностей).
Функция parserDictLookup полезна при написании вашего собственного, полностью отличающегося от стандартного синтаксического анализатора, поскольку она обеспечивает прямой досуп к словарю игры. Во многих случаях, однако, требуется частично использовать стандартную синтаксическую обработку словосочетаний, а не выполнять всю работу по реализации этой проверки самому. Функция parseNounList обеспечивает доступ к подсистеме обработки словосочетаний встроенного СА, что позволит вам разобрать словосочетание или даже целый список существительных/словосочетаний, ограничившись простым вызовом функции.
Из вашей игровой программы функция должна вызываться следующим образом:
ret := parseNounList(wordlist, typelist, startingIndex, complainOnNoMatch, multi, checkActor);Аргумент wordlist представляет собой список строковых значений, составляющих команду. typelist - это список типов слов; каждый элемент этого списка является числовым кодом, определяющим тип соответствующего по номеру элемента wordlist. Значения элементов списка typelist формируются по тем же правилам (константы PRSTYP_XXX), что и при вызове других функций СА (получении типов лексем, просмотре словаря), а также при использовании функции parseUnknownVerb ; список кодов можно посмотреть здесь.
Аргумент startingIndex задает номер элемента в списках wordlist и typelist, с которого необходимо начинать анализ; все предшествующие элементы списков будут игнорироваться. Таким образом, вы можете анализировать только часть списка лексем, и осуществлять, например, разбор словосочетаний поэтапно - следующее словосочетание будет разбираться вслед за той порцией фразы, которая уже проанализирована.
complainOnNoMatch - это аргумент-флаг; если его установить равным true, то функция будет выводить сообщение об ошибке при анализе даже синтаксически правильного словосочетания, если для слов, в него входящих, не найдется подходящих объектов в игре. Чтобы подавить вывод этого сообщения, установите этот флаг равным nil. При этом обратите внимание, что функция будет в любом случае выводить сообщения о синтаксических ошибках. Чтобы подавить вывод любых сообщений об ошибках, используйте перехват сообщений при помощи функций outhide() или outcapture().
Аргумент multi указывает, нужно ли осуществлять разбор нескольких словосочетаний (например, разделенных союзом "и"), или только одного. Если multi равен true, функция осуществит анализ всех словосочетаний, имеющихся в переданном списке; если же он равен nil, функция обработает только одно словосочетание и завершит свою работу, как только встретит слово-разделитель (например, "и").
Аргумент checkActor определяет, надо ли осуществлять проверку персонажа (актера). Если он равен true, то функция будет считать недопустимыми слово "все", строки в кавычках (двойных), словосочетания с использованием слов "оба" и "любой"; обрабатываться будет только одно словосочетание (независимо от значения аргумента multi); также она не будет выводить сообщения об ошибке при отсутствии подходящих под данную команду объектов. Встроенный СА использует этот режим для внутренней проверки начала команды, чтобы выяснить, адресована ли эта команда актеру в игре; вероятно, это единственный случай, когда этому аргументу необходимо присваивать значение true. В большинстве случаев checkActor следует установить равным nil. Обратите внимание, что не стоит вызывать функцию с checkActor равным true только из-за того, что словосочетание может или должно быть обращено к актеру; этот режим следует использовать ислючительно для установки определенного порядка реакции на ошибки. Также обратите внимание, что использование этого режима не приводит к тому, что СА будет отбрасывать команды, обращенные к объектам, не являющимся актерами; это просто флаг, определяющий порядок реакции на ошибки и никак не связанный с конкретными объектами, соответствующими тому или иному слову в команде.
Если СА обнаруживает синтаксическую ошибку, функция возвращает nil. Это указывает на то, что функция вывела сообщение об ошибке (вне зависимости от значения аргумента complainOnNoMatch), и что слова не образуют синтаксически правильного словосочетания.
Если СА считает, что первое слово в списке не может являться частью словосочетания (т. е. оно не является ни артиклем, ни прилагательным, ни существительным, ни числительным, ни местоимением, ни одним из специальных слов, которые могут использоваться в словосочетаниях - например, "все" или "любой"), то функция возвращает список, состоящий из одного элемента, содержащего значение аргумента startingIndex. При этом сообщений об ошибках не выводится. Функция возвращает startingIndex в качестве единственного элемента списка, чтобы указать на то, что ни одно из слов в исходном списке не было обработано, но что ошибки при этом не произошло. Отказ от вывода сообщения об ошибке на этом этапе обработки связан с тем, что в некоторых случаях даже в корректной команде может не быть словосочетаний. Таким образом, при вызове данной функции СА исходит из того, что выполняется только одна из возможных проверок, и при "неуспехе" оставляет на усмотрение автора игры, считать ли команду некорректной или выполнить дополнительные контрольные действия. Если бы на этом месте выводилось сообщение об ошибке, то возможность дополнительных проверок отсекалась бы полностью.
Если СА обнаруживает синтаксически правильное словосочетание, но не находит в игре объектов, к которым оно могло бы относиться, то функция возвращает список, состоящий из одного-единственного числа. Это число соответствует номеру в исходном списке слова, следующего за словосочетанием. Пусть, например, у нас имеется такой список слов:
['поймать' 'красный' 'мяч' 'посредством' 'сетки']Будем считать, что мы начали разбор со второго элемента списка (слова 'красный'), и что слова 'красный' и 'мяч' определены в словаре игры соответственно как прилагательное (adjective) и существительное (noun). СА сможет обработать словосочетание "красный мяч", использовав два слова из списка. Теперь предположим, что в игре нет объектов, для которых были бы определены оба этих слова одновременно (скажем, в игре есть "белый мяч" и "красный флаг", а вот красного мяча нет). Чтобы показать, что синтаксически корректное словосочетание имеется, но применить его не к чему, функция вернет следующий список:
[4]Это число соответствует порядковому номеру в исходном списке слова, которое следует непосредственно за словосочетанием (в нашем примере это будет слово "посредством").
Если СА обнаруживает синтаксически корректное словосочетание и находит один или несколько подходящих для этого словосочетания объектов, он возвращает список этих подходящих объектов. Первым элементом в списке, как и в примере, рассмотренном выше, будет порядковый номер в исходном списке слова, непосредственно следующего за обработанным словосочетанием. Все остальные элементы списка представляют собой набор подчиненных списков.
Каждый подчиненный список содержит информацию по одному словосочетанию. Если при вызове параметр multi был задан равным nil, функция сформирует только один такой подчиненный список. Если же этот параметр равен true, то подчиненные списки будут сформированы для каждого словосочетания (каждое словосочетание будет отделяться от предыдущего посредством, например, союза "и" или другого разделителя). Первым элементом подчиненного списка будет номер по порядку в исходном списке того слова, с которого начинается соответствующее словосочетание, а вторым - номер слова, которым это словосочетание заканчивается. Словосочетание образуется из слов, следующих в исходном списке подряд с первого номера по последний (включительно); таким образом, последний номер всегда будет больше или равен первому. После этих двух элементов, задающих границы словосочетания, в подчиненном списке расположены пары значений: определяемый словосочетанием объект и соответствующие ему флаги. Определяемым словосочетанием (или подходящим для словосочетания) объектом будет любой объект в игре, в лексических свойствах которого определены все слова, составляющие словосочетание, причем в соответствии с типами, определяемыми параметром typelist.
Значения флагов для каждого из подходящих объектов представляют собой комбинацию из любого количества значений набора PRSFLG_xxx (их можно посмотреть в описании функции parseNounPhrase()). Значения флагов могут комбинироваться при помощи операторов побитового ИЛИ ("|"), поэтому для "вычленения" определенного значения целесообразно использовать оператор побитового И, например: ((flag & PRSFLG_EXCEPT) != 0).
Словосочетание отсутствует (первое слово не является существительным, прилагательным, артиклем и т. д.) Возвращаемое значение: [число], где число соответствует переданному при вызове начальному порядковому номеру. Сообщений об ошибке не выводится. Синтаксически некорректное словосочетание Возвращаемое значение: nil. Выводится сообщение об ошибке. Синтаксически корректное словосочетание, подходящие объекты отсутствуют Возвращаемое значение: [число], где число соответствует порядковому номеру в исходном списке слова, которое следует сразу за разобранным словосочетанием. Если при вызове аргумент complainOnNoMatch равен true, СА выведет сообщение о том, что подходящих объектов не обнаружено, в противном случае никаких сообщений выводиться не будет. Синтаксически корректное словосочетание, подходящие объекты имеются Выводит список подходящих объектов, как было описано выше. Поскольку возвращаемый список значений довольно сложен по своей структуре, будет полезно рассмотреть некоторые примеры.
Предположим, у нас имеется следуюший список слов:
['взять' 'нож' ',' 'картонную' 'коробку']Предположим также, что мы начали просмотр со второго элемента списка (т. е. со слова "нож"), и что слова "нож", "картонный" и "коробка" в игре определены.
Теперь предположим, что в игре имеются следующие объекты:
rustyKnife: item // ржавый нож noun='нож' adjective='ржавый' ; sharpKnife: item // острый нож noun='нож' adjective='острый' ; dagger: item // кинжал noun='кинжал' 'нож' ; box: item // коробка noun='коробка' adjective='картонная' ;С учетом всего вышесказанного, возвращаемый список будет выглядеть следующим образом:
[6 [2 2 rustyKnife 0 sharpKnife 0] [4 5 box 0]]Первый элемент указывает на то, что следующее слово в списке после разобранного словосочетания будет иметь номер 6; поскольку в исходном списке было всего пять элементов, это автоматически означает, что словосочетание включает в себя все слова до конца списка.
Следующими двумя элементами будут подчиненные списки, по одному для каждого словосочетания:
[2 2 rustyKnife 0 sharpKnife 0] [4 5 box 0]Первый из этих списков относится к словосочетанию, в которое входят слова со 2-го по 2-ое (т. е. просто слово "нож"). Оставшиеся пары элементов в этом списке указывают на подходящие для этого словосочетания объекты, которыми являются rustyKnife и sharpKnife (для обоих флаги равны 0).
Второй подчиненный список относится к словосочетанию, в которое входят слова с 4-го по 5-ое (т. е. "картонная" и "коробка"). Подходящим объектом для этого словосочетания будет объект box (с флагами, равными 0).
Для интерпретации возвращаемого списка можно использовать, например, такой код:
if (ret = nil) { /* словосочетание содержит синтаксические ошибки; закончить обработку */ return; // или другое действие, которое необходимо выполнить при ошибке } "Следующее слово: номер = <<ret[1]>>\b"; if (length(ret) = 1) { /* Корректное словосочетание, но подходящие объекты для него отсутствуют */ "Я не вижу этого здесь."; return; } /* Обрабатываем каждый подчиненный список отдельно */ for (i := 2 ; i <= length(ret) ; ++i) { local sub; local firstWord, lastWord; local j; /* Получаем текущий подчиненный список */ sub := ret[i]; /* Получаем номера начального и завершающего слова для этого подчиненного списка */ firstWord := sub[1]; lastWord := sub[2]; /* Выводим список слов (или делаем что-то еще, поскольку в данном случае это просто пример) */ "\bСловосочетание #<<i>> состоит из: '"; for (j := firstWord ; j <= lastWord ; ++j) { say(wordlist[j]); if (j != lastWord) say(' '); } "'\n"; /* Просмотр объектов в списке - каждому объекту соответствует два элемента списка */ for (j := 3 ; j <= length(sub) ; j += 2) { /* Вывод объекта и его флагов */ "Подходящий объект = <<sub[j].sdesc>>, флаги = <<sub[j+1]>>\n"; } }Обратите внимание, что во многих случаях вам не потребуется напрямую интерпретировать этот список; вместо этого вы можете просто передать его функции parserResolveObjects() - встроенной функции, осуществляющей подбор объектов и устранение неопределенности. Возвращаемый список имеет именно тот формат, который используется в качестве входного для данной функции.
Эта функция непосредственно задействует разборщик словосочетаний встроенного СА, т. е. тот самый код, который выполняется СА при разборе команды игрока. Разборщик словосочетаний, в свою очередь, вызовет вашу функцию parseNounPhrase(), если вы определили такую функцию в игре. Таким образом, вам следует убедиться в том, что вы не инициировали бесконечную рекурсию (это происходит, если в своей функции parseNounPhrase() вы задали вызов parserResolveObjects()).
После того, как вы закончили разбор словосочетания ("собственными силами" или с использованием parseNounList), вам может потребоваться подобрать для словосочетания один или несколько объектов в игре. Этот момент является одним из самых сложных во встроенным СА TADS; наиболее правильным будет использование стандартного механизма, если только вам не потребуется реализовать какие-либо особые спецэффекты. К счастью, благодаря специальной встроенной функции вы можете получить доступ к этому механизму из вашей программы.
В стандартном TADS'овском СА подбор объектов осуществляется сразу после того, как будет проверен синтаксис предложения, т. е. анализатору будут известны глагол, все словосочетания, а также связующие предлоги. Получив всю эту информацию, СА может "интеллектуально" определить объекты, к которым относится то или иное словосочетание. В связи с таким порядком обработки модуль подбора объектов (или просто подборщик объектов) СА требует при вызове указания всех параметров, описывающих все аспекты структуры предложения.
Вызов функции-подборщика объектов осуществляется так:
resultList := parserResolveObjects(actor, verb, prep, otherobj, usageType, verprop, tokenList, objList, silent);Аргумент actor соответствует объекту-актеру для команды, для которой требуется выполнить подбор объектов. Аргумент verb - это объект класса deepverb, задействованный в команде. Параметр prep - это объект-предлог, выполняющий роль связки с "косвенным" объектом. Если в команде нет "косвенного" объекта или предлога, то этот аргумент должен быть равен nil.
Аргумент usageType определяет тип объекта, который требуется подобрать. В качестве значения следует использовать одну из следующих констант, определенных в файле advr.t:
PRO_RESOLVE_DOBJ - "прямой" объект
PRO_RESOLVE_IOBJ - "косвенный" объект
PRO_RESOLVE_ACTOR - актер: используйте это значение, если вам необходимо подобрать объект для персонажа, которому игрок дает команду.
verprop задает адрес метода-верификатора, название которого имеет вид verDoГлагол. Его нужно указывать дополнительно к аргументу verb в связи с тем, что одному глаголу может соответствовать больше одного метода-верификатора (например, для команд "положить яблоко на коробку" и "положить яблоко в коробку" используются разные наборы методов-верификаторов и методов-действий, хотя глагол в обоих случаях будет один и тот же - putVerb). Для "прямого" объекта (яблока) из второй команды ("положить яблоко в коробку") аргумент verprop будет иметь значение &verDoPutIn.
Если вы проверяете пригодность актера (не "прямого" или "косвенного" объекта, который "по совместительству" является персонажем в игре, а того актера, кому адресована команда игрока и который должен выполнить эту команду), СА обычно использует для этого следующие значения аргументов: verprop = &verDoTake и verb = takeVerb. Используются именно эти значения, а не те, которые определяются глаголом, реально задействованным в команде, поскольку основная цель проверки пригодности - удостовериться, что игрок имеет доступ к персонажу, а не что игрок может применить текущую команду к персонажу. Попытка "взять" актера обеспечивает все необходимые проверки для того, чтобы удостовериться, что данному персонажу действительно можно отдать команду.
Короткий пример, иллюстрирующий предыдущий абзац. Предположим, игрок вводит команду "Вася, спроси Петю о пиве". Если бы для проверки пригодности использовались значения аргументов verprop и verb, определяемые тем глаголом, который действительно используется в команде ("спросить" - ему соответствует объект AskVerb), то в данной ситуации был бы возможен случай, когда персонаж был бы признан пригодным, даже если бы Васи не было в одном помещении с игроком - по умолчанию глагол "спросить" считает пригодным в качестве "косвенного" любой объект в игре, вне зависимости от того, где именно он находится. Использование в качестве аргумента глагола "взять" позволяет избежать подобных проблем.
Примечание: может показаться странным, что для проверки пригодности актеров используются takeVerb and &verDoTake (соответствуют глаголу "взять"), в то время как большинство актеров принадлежит классу fixeditem, и игрок их взять не может. Однако следует иметь в виду, что в этом случае СА выполняет лишь проверку пригодности, а не верификацию (разница между этими понятиями описана в соответствующем разделе). В этом контексте мы просто удостоверяемся, что проверяемый объект достижим для игрока, т. е. игрок не обязательно должен быть способен действительно взять объект.
К тому же в большинстве случаев глагол, который вы укажете, вообще не будет использоваться. Проверяя пригодность актера, СА всегда пытается использовать соответствующий метод объекта (validActor) вместо механизма, основанного на глагольной проверке. Проверка на основе глагола используется только в случае, если для объекта не определен метод validActor. Для любой игры, написанной с использованием современной версии библиотеки adv.t (при том, что для advr.t это условие выполняется автоматически), метод validActor будет определен для всех актеров, и именно его СА будет использовать для проверки пригодности. Единственной причиной, по которой механизм проверки с использованием глагола по-прежнему поддерживается, является совместимость с более старыми играми.
Аргумент tokenList представляет собой список лексем, в результате разбора которого получился исходный список объектов. Если этот список объектов был получен при помощи функции parseNounList(), то в качестве данного аргумента просто используйте тот же список лексем, что и для parseNounList(). Этот аргумент может понадобиться в связи с тем, что, как было показано на примере parseNounList(), список объектов содержит ссылки на номера слов в списке лексем (т. е. если в процессе обработки потребуется проверить список слов, то для этого и будет использоваться аргумент tokenList).
objList - это исходный список объектов. Функция-подборщик использует этот список для получения окончательного списка. objList имеет точно такой же формат, что и список, возвращаемый parseNounList(), поэтому последний можно использовать в качестве аргумента objList. Если вместо этого вы используете собственную функцию разбора словосочетаний, вам необходимо будет сформировать список по тем же (необычайно сложным) правилам.
Аргумент silent определяет, работает ли функция-подборщик в интерактивном режиме или нет. Если этот параметр равен true, то игрок не увидит никаких сообщений об ошибке и не получит никаких запросов по устранению неопределенностей; вместо этого функция просто вернет код ошибки. Если же silent равен nil, то сообщения об ошибках будут выводиться, а для устранения неопределенностей будет использоваться обычный механизм запросов игроку ("Который кирпич вы имеете в виду...").
Возвращаемым значением этой функции всегда будет список. Первым элементом списка всегда будет число, определяющее код ошибки (состояния). Коды состояний имеют те же значения и смысл, что и коды, передаваемые функциям parseError() и parseErrorParam().
Код состояния PRS_SUCCESS (эта константа, так же, как упомянутые ниже константы PRSERR_xxx, определены в файле advr.t) указывает на то, что процесс подбора объектов завершился успешно. В этом случае оставшиеся элементы списка - это просто подобранные объекты:
[PRS_SUCCESS goldCoin shoeBox]Значение PRSERR_AMBIGUOUS указывает на то, что полученный список неоднозначен. Этот код будет возвращен только в том случае, если аргумент silent равен true, поскольку в ином случае функция-подборщик просто не завершает работу, пока игрок не устранит все неопределенности в интерактивном режиме, либо не возникнет какая-либо ошибка. Если функция возвращает этот код состояния, то оставшиеся элементы списка содержат подобранные объекты в "расширенном составе"; подборщик ограничит этот набор, насколько это возможно, включив в него только те объекты, которые доступны для данного персонажа (с учетом введенного глагола), однако этот список потребует дальнейших шагов по устранению неопределенностей, прежде чем примет окончательный вид (т. е., проще говоря, будет содержать некоторое количество "лишних" объектов).
[PRSERR_AMBIGUOUS goldCoin silverCoin shoeBox cardboardBox]PRSERR_DISAMBIG_RETRY указывает на то, что в ответ на запрос об устранении неопределенности игрок просто ввел новую команду. Это возможно лишь в случае, если аргумент silent равен nil, поскольку в противном случае СА не будет задавать игроку никаких вопросов. При возврате этого значения список будет содержать, помимо самого кода состояния, лишь один дополнительный элемент - строковое значение, соответствующее введенной игроком новой команде. Если необходимо выполнить эту новую команду, то можно использовать функцию parserReplaceCommand() для отказа от текущей команды и выполнения вместо нее новой.
[PRSERR_DISAMBIG_RETRY 'иди на север']Любое иное значение кода состояния означает ошибку, в результате которой подбор объектов оказался невозможен. В этом случае в возвращаемом списке не будет никаких других элементов.
Обратите внимание, что данная функция обращается к тому же самому внутреннему коду СА, который используется и при обычной обработке команды игрока. В некоторых случаях подборщик объектов вызывает методы disambigDobj и disambigIobj, определенные в объекте класса deepverb. Вследствие этого вам не следует вызывать функцию-подборщик объектов из вышеуказанных методов, поскольку это приведет к зацикливанию (бесконечной рекурсии).
В приведенном ниже примере использованы несколько функций для доступа к СА, включая parserResolveObjects(). Эта демонстрационная функция считывает строку, введенную с клавиатуры, разбивает ее на лексемы, определяет типы лексем, разбивает список лексем на словосочетания и затем подбирает объекты для словосочетаний.
askForObject: function { local str; local toklist, typelist; local objlist; /* вводим текст с клавиатуры */ "Введите название объекта: "; str := input(); /* разбиваем введенный текст на лексемы */ toklist := parserTokenize(str); if (toklist = nil) { "Недопустимое название объекта!"; return nil; } /* получаем типы лексем */ typelist := parserGetTokTypes(toklist); /* разбираем список лексем как набор словосочетаний*/ objlist := parseNounList(toklist, typelist, 1, true, nil, nil); if (objlist = nil) return nil; if (length(objlist) = 1) { "Этого ты здесь не видишь. "; return nil; } if (objlist[1] <= length(toklist)) { "После названия объекта имеются слова, которые я не могу использовать. "; return nil; } /* подбираем объекты и устраняем неопределенности */ objlist := parserResolveObjects(Me, takeVerb, nil, nil, PRO_RESOLVE_DOBJ, &verDoTake, toklist, objlist, nil); if (objlist[1] = PRS_SUCCESS) { /* удачное завершение! возвращаем список объектов (он следует непосредственно за кодом состояния) */ return cdr(objlist); } else if (objlist[1] = PRSERR_DISAMBIG_RETRY) { /* запускаем новую команду, которая является вторым элементом списка */ parserReplaceCommand(objlist[2]); } else { /* функция выполнялась в интерактивном режиме, поэтому сообщение об ошибке уже было отображено */ return nil; } }
Встроенная функция execCommand() предоставляет игровой программе прямой доступ к системе выполнения команд синтаксического анализатора. Следует подчеркнуть, что эта функция позволяет получить доступ не к части СА, отвечающей за разбор введенного текста, а к его исполнительной части. execCommand() берет объекты, задействованные в команде, и выполняет эту команду, осуществляя проверку пригодности объектов (validDo, validIo), глагольную обработку (verbAction), персонажную обработку (actorAction), локационную обработку (roomAction), проверку "прямых" и "косвенных" объектов (dobjCheck и iobjCheck, соответственно), вызов общих обработчиков объектов (dobjGen и iobjGen), верификацию объектов (verIoГлагол и verDoГлагол), а также собственно действия над объектами (ioГлагол, doГлагол, либо Глагол.action, по ситуации).
Вызов execCommand() осуществляется следующим образом:
errorCode := execCommand(actor, verb, dobj, prep, iobj, flags);Аргумент actor - это объект (обычно принадлежащий классу Actor), соответствующий тому персонажу, который должен выполнить команду; если команду должен выполнить главный персонаж, то в качестве этого аргумента следует передать parserGetMe(). Аргумент verb - это объект класса deepverb, соответствующий глаголу, задействованному в команде. dobj - это "прямой" объект команды, причем это должен быть именно один объект, а не список; если требуется выполнить одну и ту же команду с разными "прямыми" объектами, просто организуйте вызов execCommand() в цикле. Если команда не использует "прямой" объект, в качестве этого аргумента следует передать nil. prep - это объект-связка (обычно принадлежит классу Prep) (предлог) для "косвенного" объекта; если в команде нет предлога, он должен быть равен nil. iobj соответствует "косвенному" объекту команды, или равен nil, если такого объекта нет.
Аргумент flags позволяет вам задать порядок обработки команды синтаксическим анализатором. Для этого аргумента анализируются отдельные биты, т. е. приведенные ниже константы можно произвольно комбинировать, используя оператор побитового ИЛИ ("|"). Нижеприведенные константы определены в файле advr.t.
EC_HIDE_SUCCESS - если задан этот флаг, СА не будет выводить никаких сообщений, если команда завершилась успешно. СА считает, что команда завершена успешно, если execCommand вернула значение 0 (см. далее). Если этот флаг не задан, то при успешном завершении команды все сообщения будут выдаваться. Обратите внимание, что этот флаг действует независимо от флага EC_HIDE_ERROR; иначе говоря, при неудачном завершении команды этот флаг не будет оказывать никакого действия.
EC_HIDE_ERROR - если задан этот флаг, СА не будет отображать сообщения, которые генерируются выполняемой командой при неудачном завершении. СА считает, что выполнение команды окончилось неудачей, если execCommand вернула значение, отличное от нуля (см. далее). Если этот флаг не установлен, а команда выполнилась неудачно, то соответствующие сообщения об ошибках будут выведены на экран. Обратите внимание, что этот флаг действует независимо от флага EC_HIDE_SUCCESS; иначе говоря, при успешном завершении команды этот флаг не будет оказывать никакого действия.
EC_SKIP_VALIDDO - если задан этот флаг, СА пропускает этап проверки пригодности "прямого" объекта. Если данный флаг сброшен, СА выполняет обычную процедуру проверки пригодности объекта. Вы можете использовать этот флаг, если хотите выполнить команду даже в том случае, если персонаж не имеет доступа к "прямому" объекту.
EC_SKIP_VALIDIO - если задан этот флаг, СА пропускает этап проверки пригодности "косвенного" объекта. Если данный флаг сброшен, СА выполняет обычную процедуру проверки пригодности этого объекта.
Если вам требуется выполнить команду "по-тихому", чтобы игрок не увидел никаких сообщений, выдаваемых игрой, то следует указать сразу оба флага EC_HIDE_SUCCESS и EC_HIDE_ERROR:
err := execCommand(actor, takeVerb, ball, nil, nil, EC_HIDE_SUCCESS | EC_HIDE_ERROR);В некоторых случаях вам может потребоваться выводить сообщения только в случае возникновения ошибок. Это бывает особенно полезным, если вы хотите автоматизировать действия игрока, избавив его от рутинных операций (таких, например, как открывание двери при перемещении в некотором направлении). В этом случае, действительно, вывод сообщения при успешном завершении операции не нужен, поскольку при проходе через дверь в игровой программе, как правило, уже задан некий текст, описывающий открывание двери, а вот в случае ошибки (например, если дверь заперта) вывод сообщения потребуется. В этих целях можно использовать EC_HIDE_SUCCESS, например, следующим образом:
"(Сначала ты открываешь дверь)\n"; // Это сообщение описывает наше неявное действие (открывание двери) при перемещении в соответствующем направлении err := execCommand(actor, openVerb, steelDoor, nil, nil, EC_HIDE_SUCCESS); // Собственно открывание двери реализуется здесь; если произойдет ошибка, будет выведено соответствующее сообщение if (err = EC_SUCCESS) { // Дверь успешно открылась, продолжаем перемещение... }Все аргументы execCommand после аргумента verb можно опустить, в этом случае dobj, iobj и prep по умолчанию будут приняты равными nil, а flags - нулю. Кроме того, вы можете задать аргумент flags, но пропустить любой (любые) из аргументов dobj, iobj и prep - при этом соответствующий аргумент будет приравнен nil.
Функция execCommand возвращает код ошибки, определяющий результаты работы синтаксического анализатора. Возвращаемый код ошибки, равный нулю, означает, что в процессе выполнения команды синтаксический анализатор не выдал собщений об ошибках. В то же время это не означает, что команда сработала так, как вы рассчитывали: это означает только то, что все методы проверки пригодности и верификации завершились успешно, включая dobjCheck, iobjCheck, dobjGen, iobjGen, roomAction, actorAction, verDoГлагол и verIoГлагол, и что при этом не встретились инструкции exit или abort. Однако в некоторых случаях метод-действие, как, например, doVerb, или ioVerb, будет выполнять собственные проверки и выдавать сообщения об ошибках; в таких случаях execCommand все равно вернет нулевое значение, хотя бы выполнение команды и закочилось неудачно. В этом случае вам может понадобиться специальная проверка на предмет того, выполнилась ли команда именно так, как вы предполагали.
Данная функция может возвращать следующие значения (они опРеделены в файле advr.t:
EC_SUCCESS - удачное выполнение - методы doГлагол и ioГлагол были вызваны штатным образом, и при этом не происходило вызовов инструкций exit или abort.
EC_EXIT - при выполнении команды встретилась инструкция exit. Обычно это означает, что команда была заблокирована одним из следующих методов: roomAction, actorAction, xobjCheck, либо xobjGen.
EC_ABORT - при выполнении команды встретилась инструкция abort.
EC_INVAL_SYNTAX - означает, что исходное сочетание глагола, объектов ("прямого" и "косвенного"), а также предлога не образуют синтаксически корректной команды. В этом случае СА не отображает сообщения об ошибке; данный случай указывает на ошибку в исходном коде вашей игры, поскольку вы пытаетесь использовать глагольную конструкцию, которая не определена в вашей игре.
EC_VERDO_FAILED - неудачное выполнение метода verDoГлагол. Метод-верификатор "прямого" объекта выведет соответствующий текст.
EC_VERIO_FAILED - неудачное выполнение метода verIoГлагол. Метод-верификатор для "косвенного" объекта выведет соответствующий текст.
EC_NO_VERDO - для объекта не определен метод-верификатор verDoГлагол. Этот случай почти аналогичен EC_VERDO_FAILED, но указывает на то, что было отображено сообщение СА по умолчанию, так как объект не определяет и не наследует соответствующий метод-верификатор.
EC_NO_VERIO - для объекта не определен метод-верификатор verIoГлагол. СА отображает сообщение по умолчанию.
EC_INVAL_DOBJ - "прямой" объект не прошел проверку пригодности. Это означает, что "прямой" объект недоступен для использования с данным глаголом; СА выведет соответствующее сообщение (через механизм с использованием метода cantReach) перед завершением работы функции.
EC_INVAL_IOBJ - "косвенный" объект не прошел проверку пригодности, т. е. он недоступен для использования с данным глаголом; СА выведет соответствующее сообщение перед завершением работы функции.
Обратите внимание, что СА не проверяет, находится ли выполняющий команду персонаж в одной комнате с текущим главным персонажем, и вообще не выполняет никакой проверки того, может ли игрок дать команду этому персонажу. Это позволяет использовать execCommand() для автономного выполнения скриптов персонажей, без учета того, может ли игрок отдавать соответствующие команды напрямую.
Текущим персонажем считается персонаж, переданный в аргументе actor; это сделано для того, чтобы работал механизм форматных строк.
Функция execCommand() не запускает демоны и запалы. Рекурсивная команда считается выполненной в пределах того же хода (иначе говоря, отдельный ход на нее не тратится).
Не следует использовать вызовы execCommand() в методах-верификаторах (verIoГлагол и verDoГлагол), поскольку execCommand() может вносить изменения в состояние объектов в игре, а метод-верификатор делать этого не должен (объяснение, почему, см. в предыдущей главе). Также обратите внимание, что эти изменения будут вноситься вне зависимости от значений флагов EC_HIDE_SUCCESS и EC_HIDE_ERROR - эти флаги лишь разрешают или блокируют вывод командой сообщений, но никак не влияют на выполнение командой других действий.
execCommand() удобно использовать, например, для того, чтобы обеспечить "понимание" игрой разных формулировок одной и той же команды. Пусть, например, у вас в игре имеется банка-аэрозоль с краской, и вам нужно, чтобы игроки могли окрашивать предметы в игре при помощи команд "обрызгать цель краской" и "разбрызгать краску на цель". Чтобы сделать эти команды эквивалентными, можно использовать следующий механизм (считаем, что глаголу "обрызгать" соответствует объект obryzgatVerb, а глаголу "разбрызгать" - razbryzgatVerb, с соответствующими названиями методов-верификаторов и действий): определить методы verIoRazbryzgatOn, verDoRazbryzgatOn, ioRazbryzgatOn и doRazbryzgatOn, после чего фактически продублировать код этих методов в их эквивалентах для ObryzgatWith (здесь суффикс On означает использование совместно с соответствующим глаголом предлога "на" (объект onPrep в файле advr.t), а With - связку "посредством" (передается также творительным падежом) (объект withPrep)). Кроме того, можно было бы вызывать методы группы RazbryzgatOn из методов группы ObryzgatWith (или наоборот); однако в данном случае такое "перенаправление" является нетривиальной задачей в связи с принятой в TADS асимметричной верификацией "прямых" и "косвенных" объектов - ведь в нашем примере эти объекты меняются местами.
При помощи же execCommand такое перенаправление организовать очень просто. В первую очередь следует выбрать "каноническую" формулировку - т. е. ту команду, в которой непосредственно будут "зашиты" все обработчики. Пусть в нашем случае такой канонической формулировкой будет "разбрызгать краску на цель". Далее, для канонической формы реализуются все методы-обработчики (самым обычным образом, как и для любой другой команды): мы определим методы verIoRazbryzgatOn и ioRazbryzgatOn для всех объектов, которые можно окрашивать, и метод verDoRazbryzgatOn для объектов, которые могут использоваться в качестве распылителя краски. Пусть, например, нашему распылителю соответствует объект Raspylitel, тогда для него метод-верификатор verDoRazbryzgatOn мог бы выглядеть так:
Raspylitel: item // Здесь определяем краткое и подробное описания (sdesc, ldesc), лексические свойства и т. п. verDoRazbryzgatOn(actor, iobj) = { } //нет никаких ограничений на разбрызгивание краски из распылителя ;Далее, мы продолжим реализацию этой команды ("разбрызгать краску на цель"), определив все остальные необходимые обработчики, пока она не станет работать так, как требуется. После отладки канонической команды можно приступать к реализации ее перенаправления для иных формулировок. Вместо того, чтобы использовать сложный набор перенаправлений каждого обработчика (с вызовом обработчиков друг из друга), можно разрешить любой команде вида "обрызгать цель краской" выполняться вплоть до обработчика ioObryzgatWith, и осуществить перенаправление команды в этой точке. Поскольку мы хотим, чтобы перенаправление работало для любой пары объектов, то можно все обработчики определить в классе thing (стандартный родительский класс для всех предметов в игре):
modify thing /* Разрешаем ЛЮБЫЕ команды вида "обрызгать <цель> краской" */ verIoObryzgatWith(actor) = { } verDoObryzgatWith(actor, iobj) = { } /* Подменяем команду "обрызгать <цель> краской" на "разбрызгать краску на <цель>" */ ioObryzgatWith(actor, dobj) = { execCommand(actor, razbryzgatVerb, self, onPrep, dobj); } ;Собственно, это все, что от нас требуется - поскольку execCommand() обеспечит выполнение всей последовательности синтаксического разбора заново сформулированной команды, нам не придется заботиться о какой-лмбо дополнительной верификации неканонической команды. Обратите внимание, что вызов execCommand() надо поместить именно в обработчик ioObryzgatWith, а не в один из методов-верификаторов verXoObryzgatWith, поскольку в последнем случае наша рекурсивная команда может оказаться выполненной неконтролируемое число раз при вызовах "втихую", осуществляемых СА при устранении неопределенности. Также обратите внимание, что при желании мы можем отказаться от эквивалентности двух формулировок команды ("обрызгать <цель> краской" и "разбрызгать краску на <цель>") для отдельных объектов, соответствующим образом переопределив для этих объектов методы группы ObryzgatWith. В то время как конкретно в нашем примере это вряд ли потребуется, в некоторых случаях данная возможность может оказаться полезной: например, команды "поместить x в y" и "наполнить x y" должны быть эквивалентны для жидкостей и соответствующих емкостей, но не для других объектов.
Примечание переводчика: в оригинале вышеописанный пример несколько отличается. Связано это с тем, что в английском языке глаголам "обрызгать" и "разбрызгать" соответствует один и тот же глагол "spray", а различный смысл команд достигается за счет использования разных предлогов (соответственно, "with" для "обрызгать" и "on" для "разбрызгать"). Соответственно, группы методов будут называться SprayWith и SprayOn, и будут относиться к одному глаголу, а не к разным. Никаких других отличий в реализации данного механизма перенаправления команд нет.
Другая область использования execCommand() - реализация "неявных" команд, т. е. действий, выполняемых игрой автоматически и не требующих ввода соответствующей команды от игрока, поскольку для выполнения той команды, которую игрок ввел, безусловно необходимо выполнить и их. Один из самых типичных случаев - автоматическое открывание незапертых дверей на пути игрока, когда он идет в соответствующем направлении.
Рассмотрим следующий пример. Пусть в нашей игре имеется комната, войти в которую игрок может, только одев солнцезащитные очки. Проще всего просто проверять на входе в помещение, одел ли игрок очки, и, если нет, не пускать его в комнату. Диалог с игрой может при этом выглядеть так:
>идти на север Прежде, чем войти в помещение реактора, надо надеть солнцезащитные очки. >одеть очки Хорошо, Вы одели солнцезащитные очки. >идти на север ...Такой способ вполне работоспособен, но неудобен для игрока (а в наше время к тому же практически считается плохим стилем написания игр - прим. переводчика), поскольку игра в точности говорит игроку, какую команду надо отдать, и все же заставляет игрока набирать эту команду. Многие люди предпочитают думать (несмотря на обилие доказательств, говорящих об обратном), что компьютеры - слуги нам, а не хозяева, и такая "лень" со стороны игры может вызвать у них раздражение.
Однако еще большее раздражение, только уже у автора, вызывала необходимость написания соответствующего "автоматизирующего" кода традиционным способом. Проблема при этом состоит в том, что приходится фактически дублировать все проверки, выполняемые синтаксическим анализатором для того, чтобы убедиться, что очки можно будет одеть, и, кроме того, отслеживать, чтобы были реализованы все побочные эффекты.
Функция execCommand() значительно упрощает реализацию такого рода задач, поскольку позволяет вам использовать в точности тот же самый код, который отрабатывается СА при выполнении команды, "в явном виде" введенной игроком. В конечном итоге это позволяет непосредственно вызывать очевидные неявные команды, а не требовать от игрока ввести их вручную. В нашем случае с очками это могло бы выглядеть так:
outsideChamber: room // Обычные описания, лексические свойства и т. п. north = { /* Если очки не одеты, пытаемся их одеть */ if (sunglasses.isIn(parserGetObj(PO_ACTOR)) && !sunglasses.isworn) { /* * Очки здесь, но не одеты - одеваем их. Сообщаем игроку, какое действие выполняется, * затем выполняем это действие. Обратите внимание, что мы используем флаг EC_HIDE_SUCCESS * при вызове execCommand, чтобы скрыть от игрока обычное подтверждение в случае успешного * завершения действия - нам нужно сообщение только в случае, если игрок по какой-либо * причине не смог одеть очки. */ "(Сначала %you% одева"; glok(actor,2,2); " солнцезащитные очки)\n"; if (execCommand(parserGetObj(PO_ACTOR), wearVerb, sunglasses, nil, nil, EC_HIDE_SUCCESS) != 0) { /* * Произошла неудача; поскольку execCommand уже вывела сообщение об ошибке, * нам не нужно дополнительно объяснять игроку, что произошло; просто возвращаем * nil, чтобы запретить перемещение игрока */ return nil; } } /* Если игрок без очков, запрещаем вход */ if (!(sunglasses.isIn(parserGetObj(PO_ACTOR)) && sunglasses.isworn)) { /* Объясняем, в чем проблема */ "%You% дела"; glok(actor,2,2); " пару шагов, но свет в камере настолько яркий, что %you% вынужден"; yao(actor); " отступить. Чтобы войти, придется как-то защитить глаза."; /* Запрещаем вход */ return nil; } /* Солнечные очки одеты - можем войти */ return fusionChamber; } ;Обратите внимание, что в этом примере для определения персонажа, который перемещается, вместо parserGetMe() используется функция parserGetObj(), поскольку перемещающийся персонаж - необязательно главный. Кроме того, во всех выводимых сообщениях используются форматные строки (например, "%You%"). Благодаря этим двум моментам данный метод можно использовать и при перемещении неглавных персонажей; это может быть особенно важным, если вы планируете использовать execCommand() для реализации скриптов, выполняемых персонажами, поскольку при этом при выполнении рекурсивной команды текущим актером как раз будет объект, соответствующий второстепенному персонажу.
В некоторых случаях вместо того, чтобы выполнять рекурсивную команду (при помощи функции execCommand), может оказаться предпочтительнее вообще отбросить текущую команду и выполнить вместо нее совершенно другую. В СА имеется встроенная функция именно для этой цели.
Встроенная функция parserReplaceCommand() позволяет отказаться от выполнения текущей команды и выполнить вместо нее другую, определяемую заданной текстовой строкой. Формат вызова функции выглядит так:
parserReplaceCommand(commandString);Эта функция не возвращает никакого значения - она фактически выполняет инструкцию abort, чтобы прервать текущую команду. Передаваемая в аргументе commandString строка помещается во внутренний буфер СА, после чего система осуществляет разбор и выполнение команды так, как если бы игрок ввел ее сам с клавиатуры.
Функция parserReplaceCommand() особенно полезна в случаях, если игроку задается вопрос, а игрок вместо ответа вводит новую команду. Именно такая ситуация возникает, когда СА выдает дополнительный запрос на устранение неопределенности, а игрок набирает не название объекта, а команду; в этом случае функция parserResolveObjects() возвратит значение PRSERR_DISAMBIG_RETRY. Подобный случай может произойти, если вы используете функцию input() для считывания ввода игрока; если вы хотите "разрешить" игроку проигнорировать ваш вопрос и ввести новую команду, как раз и можно использовать parserReplaceCommand().
В следующем примере игрока просят ответить на вопрос да или нет, при этом любой другой ответ расценивается как новая команда.
dragon: Actor // Дракон // Описания (sdesc, ldesc), лексические свойства и т. п... verDoKill(actor) = { } doKill(actor) = // Глагол "убить" { local response; local ret; /* Спрашиваем, что игрок хочет сделать */ "Что, вот так, голыми руками?\b>"; response := lower(input()); /* Проверяем, ответил ли игрок "да" или "нет" */ ret := reSearch(' *(да|нет) *', response); if (ret = nil) { /* Ни "да", ни "нет" - считаем, что это - новая команда */ parserReplaceCommand(response); } /* Проверяем, "да" или "нет" */ if (reGetGroup(1)[3] = 'да') { "Схватив дракона за горло, ты прижимаешь его к земле! После короткой схватки дракон вопит, \"Сдаюсь!\", и обещает пропустить тебя через мост. Ты отпускаешь дракона, и он забивается в угол, провожая тебя угрюмым взглядом. "; self.isPouting := true; } else { "Несомненно, мудрое решение. "; } } ;
TADS содержит ряд встроенных функций для поиска в текстовых строках "по шаблону". Хотя чисто технически эти средства не являются частью СА, вам они вполне могут пригодиться, если вы захотите усовершенствовать обработку команд в своей игре. В частности, поиск по шаблонам может быть полезным при использовании функций preparse() и preparseCmd().
Шаблон, или "регулярное выражение" (буквальный перевод термина "regular expression") - это набор правил, позволяющий описать значительный набор строковых значений одной сравнительно короткой записью. По своей сути шаблоны напоминают те обозначения замен символов ("wildscards"), которые используются утилитами командной строки во многих операционных системах, однако шаблоны обладают значительно большими возможностями.
В TADS используется синтаксис шаблонов, схожий с тем, который применяется в команде grep в операционной системе UNIX и ей подобных. Шаблон представляет собой последовательность символов. Для большинства символов действует правило: если этот символ задан в шаблоне, будет искаться только этот символ. Скажем, при задании шаблона "abc" будут искаться только строковые значения "abc", и никакие другие. Однако ряд символов имеет специальное значение. Эти символы приведены ниже.
| Альтернативный поиск: будут искаться строки, соответствующие либо выражению с левой стороны от вертикальной черты, либо с правой. При этом выражением будут считаться все символы до ближайшей скобки (или до конца/начала шаблона). ( ) Группирует выражение. + Указывает на то, что предыдущий символ или выражение в скобках повторяется один или несколько раз. * Указывает на то, что предыдущий символ или выражение в скобках повторяется ноль или более раз. ? Указывает на то, что предыдущий символ или выражение в скобках может встретиться не более одного раза. . (точка) Символ обобщения: соответствует любому одиночному символу. ^ Соответствует началу строки. $ Соответствует концу строки. [ ] Указывает на список символов или интервал. Интервал может быть задан путем указания знака "-" после символа, а также еще одного символа; выражение-интервал означает, что будут искаться все символы в интервале между первым и вторым символом. Например, выражение [a-z] соответствует любой букве латинского алфавита в нижнем регистре, а [0-9] - любой цифре. Интервалы и отдельные символы могут комбинироваться; например, шаблон [a-zA-Z] соответствует любой латинской букве (как прописной, так и строчной). Чтобы включить в список закрывающую квадратную скобку, поместите ее сразу за открывающей скобкой; чтобы включить в список знак минуса, поместите его непосредственно перед закрывающей скобкой. Например, шаблон []] соответствует "]", [-] "-", а []-] соответствует и "]", и "-". [^ ] Список или интервал исключаемых символов. Будет искаться любой символ, кроме перечисленных в скобках. Например, по шаблону [^0-9] будут найдены все строки, состоящие из единственного символа, если этот символ - не цифра. % При размещении перед символами специального назначения (такими, как | . ( ) * ? + ^ $ % [) эти символы интерпретируются как обычные, т. е. утрачивают свой особый смысл. При размещении перед обычным символом данный знак игнорируется. Кроме того, значок процента используется в качестве вводного символа ряда специальных последовательностей, описанных далее. %1 Означает поиск того же самого текста, который был найден в соответствии с первым выражением в скобках, имевшемся в шаблоне. Рассмотрим, например, шаблон "(a*).*%1". Строка "aaabbbaaa" будет найдена по этому шаблону, поскольку первые три символа "подпадают" под выражение "a*" (символ a, повторяющийся ноль или более раз), вследствие чего последовательность "%1" автоматически "окучивает" последние три символа a. Средние три символа соответствуют шаблону ".*" (любой повторяющийся символ). %2 Поиск текста, которому соответствует второе выражение в скобках. Аналогичная последовательность формируется для третьего, четвертого и т. д., вплоть до... %9 ...девятого выражения в скобках. %< Ищет текст в начале слова. Словами считаются непрерывные последовательности букв и цифр. Примечание переводчика: Тут надо быть осторожным: буквы имеются в виду латинские! Работа шаблонов с символами кириллицы, скорее всего, непредсказуема. Например, на моем компьютере (операционная система Windows 2000 русская) в шаблонах в качестве допустимых символов для слов признаются только заглавные русские буквы, строчные таковыми не считаются. Это уже само по себе нельзя считать полноценной поддержкой кириллицы, однако, что гораздо хуже - нет никаких гарантий, что на других операционных системах шаблоны будут работать аналогично. Поскольку автор игры не может знать заранее конфигурацию компьютера игрока, то наилучшей рекомендацией будет, пожалуй, следующая: там, где предполагается работа с кириллицей, лучше вообще отказаться от использования в шаблонах выражений, "привязанных" к словам (%<, %>, %w, %W, %b, %B), а если соответствующий поиск все-таки необходим, то реализовать его отдельно уже в коде самой игры.
%> Ищет текст в конце слова. Обратите внимание, что шаблоны %< и %> сами по себе не добавляют символов в шаблоны для поиска - они просто указывают, что соответствующая последовательность символов должна располагаться по той или иной границе слова. %w Любой символ, допустимый в слове (буква или цифра). %W Любой символ, недопустимый в слове (т. е. любой символ, кроме букв и цифр). %b Искомая последовательность должна располагаться по любой границе слова (т. е. в начале или в конце). %B Искомая последовательность не должна располагаться на границе слова. Любой другой символ, не приведенный в этом списке, соответствует самому себе. Например, по шаблону "a" и будет найдено только "a", и ничего более.
Для наглядности ниже приведены некоторые примеры простых шаблонов:
abc|def Ищется либо "abc", либо "def". (abc) Ищется "abc" abc+ "abc", "abcc", "abccc" и т. д. abc* "ab", "abc", "abcc", "abccc" и т. д. abc? "ab" или "abc" . любой одиночный символ ^abc "abc", но только в начале строки abc$ "abc", но только в конце строки %^abc ищется именно последовательность символов "^abc" [abcx-z] "a", "b", "c", "x", "y" или "z" []-] "]" или "-" [^abcx-z] любой символ, кроме "a", "b", "c", "x", "y" или "z" [^]-q] любой символ, кроме "]", "-" или "q" А вот несколько более сложных примеров:
(%([0-9][0-9][0-9]%) *)?[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]Это выражение соответствует телефонному номеру в североамериканской транскрипции, с или без указания в скобках кода города. При наличии кода города этот код может отделяться от остальных цифр номера пробелами, а может и не отделяться: будут найдены записи вида "(415)555-1212", "555-1212", "(415) 555-1212".
[-+]?([0-9]+%.?|([0-9]*)%.[0-9]+)([eE][-+]?[0-9]+)?Данное выражение соответствует записи числа с плавающей точкой в транскрипции, принятой в C и ряде других языков программирования: либо строка из цифр, последний символ в которой может быть (но не обязательно) точкой, либо ноль или более цифр, за которыми следует точка, после которой следует одна или более цифр; за этими цифрами может следовать символ экспоненты (обозначается буквой "E"), знак плюс или минус (также необязательно), а также еще одна или более цифр; всей этой конструкции может предшествовать знак. Будут найдены следующие варианты записи: "3e9", ".5e+10", "+100", "-100.", "100.0", "-5-e9", "-23.e+50".
^ *tell%>(.*)%<to%>(.*)Будет найдено слово "tell", расположенное в начале строки (ему могут предшествовать ноль или более пробелов), за которым следует любой текст, за которым следует слово "to", за которым опять же следует текст. В данном случае это соответствует английской конструкции вида "tell bob to go north" - "сказать Бобу, чтобы он шел на север". В этом примере сознательно использован английский, а не русский язык в связи с ограничениями поддержки кириллицы, описанными выше.
Встроенная функция reSearch() ищет первое вхождение соответствующего шаблону образца внутри заданной строки.
Формат вызова:
ret := reSearch(pattern, string_to_search);Параметр "pattern" - это строка-шаблон, а "string_to_search" - строка, внутри которой ведется поиск.
reSearch() возвращает nil, если образец по шаблону не найден. Если образец найден, функция возвращает список из трех элементов: первый элемент соответствует позиции найденного образца внутри строки (первому символу строки соответствует позиция 1), второй элемент содержит длину (количество символов) найденного образца, а третий элемент - собственно найденный образец.
Вот пример программной реализации:
ret := reSearch('d.*h', 'abcdefghi'); if (ret = nil) "Ничего не найдено."; else "Начальная позиция = <<ret[1]>>, длина = <<ret[2]>>, текст = \"<<ret[3]>>\". ";При запуске на исполнение этого кода будет выведено:
Начальная позиция = 4, длина = 5, текст = "defgh".
Встроенная функция reGetGroup() позволяет вернуть текст, предварительно найденный для сгруппированного в скобках выражения внутри шаблона. Работа reGetGroup() основана на результатах последнего вызова функции reSearch(). Функция вызывается с одним-единственным аргументом, соответствующим порядковому номеру заключенного в круглые скобки выражения (также называемого группой), значение которого требуется вернуть. В случае, если эти выражения в шаблоне вложены друг в друга, то для определения порядкового номера выражения используется открывающая скобка (выражение, которому соответствует самая левая открывающая скобка, будет иметь номер 1).
reGetGroup() возвращает nil, если в шаблоне, передававшемся при последнем вызове reSearch(), было меньше выражений в скобках, чем переданный порядковый номер, либо если по этому шаблону не было найдено текста. В противном случае reGetGroup() возвращает список из трех элементов. Первый элемент является числом, определяющим положение в исходной строке первого символа текста, соответствующего выражению (группе) с заданным номером. Второй элемент соответствует длине текста, найденного для группы, а третий содержит сам этот текст.
Вот пример программной реализации:
Shablon:='d(.*)h'; Stroka:='abcdefghi'; ret := reSearch(Shablon, Stroka); if (ret != nil) { grp := reGetGroup(1); if (grp != nil) "Начальная позиция = <<grp[1]>>, длина = <<grp[2]>>, текст = \"<<grp[3]>>\". "; } else { "Ничего не найдено."; }При выполнении будет выведено следующее:
Начальная позиция = 5, длина = 3, текст = "efg".Если в этом примере присвоить переменной Shablon значение, скажем, 'abc', то будет выведено сообщение "Ничего не найдено.", поскольку в шаблоне не окажется выражений в скобках. Если шаблон оставить без изменений, а переменной Stroka присвоить значение, например, '123542', то результат будет таким же, так как в такой строке reSearch не найдет текста, соответствующего шаблону.
Вы можете использовать группировку внутри шаблонов для выполнения сложных преобразований над строками при относительно небольших затратах труда на программирование. Поскольку расположение того или иного сгруппированного выражения шаблона внутри исходной строки определено совершенно точно, то вы вполне можете "выдергивать" такие группы из строки и затем помещать их обратно в командную строку, но, например, уже в другом порядке (или с иными изменениями).
Пусть вам требуется написать функцию preparse(), которая искала бы во введенной игроком команде конструкции вида "приказать персонажу выполнить команду", и преобразовывала бы эти конструкции к стандартному для TADS формату отдачи команды персонажу ("персонаж, выполни команду"). Один из путей решения данной задачи - использование группировки внутри шаблонов с последующей перестановкой найденных групп в нужном порядке. Для упрощения примера считаем, что (1) игрок набирает команду только строчными буквами, (2) что используется только глагол "приказать", и только в неопределенной форме (никаких "приказываю", "прикажу" и т. п.), и (3) что имя персонажа состоит из одного слова.
ret := reSearch('^ *приказать +(.*) +(.*)', str); if (ret != nil) cmd := reGetGroup(1)[3] + ', ' + reGetGroup(2)[3];Разберем вкратце работу этого кода. По шаблону ищется фрагмент текста, начинающийся от первого символа строки словом "приказать" (либо перед словом "приказать" имеется любое количество пробелов) - конструкция '^ *приказать'. После слова "приказать" должен стоять как минимум один пробел (конструкция ' +') - этим мы гарантируем, что "приказать" у нас является отдельным словом. Далее следует любое количество символов (оно же является первой группой - конструкция '(.*)'), за которым следует не менее одного пробела; считаем, что это - обращение к персонажу. Наконец, все, что идет за последним пробелом и до конца строки, мы выделяем во вторую группу (конструкция '(.*)') и считаем командой, отдаваемой игроку. В условном операторе проверяется, была ли найдена во введенной игроком строке описываемая шаблоном конструкция; если да, то входная строка преобразуется к виду первая найденная по шаблону группа - запятая - вторая найденная по шаблону группа. Если, например, игрок ввел команду "приказать собаке взять мяч", то переменной cmd будет присвоено значение "собаке, взять мяч". Не стоит обращать внимания на некорректную с точки зрения грамматики конструкцию этой команды - игрок ее все равно не увидит, а синтаксический анализатор успешно обработает (конечно, при условии, что вы правильно указали для своих объектов лексические свойства).
Другой пример: допустим, в вашей игре есть телефон, и вы хотите, чтобы игрок мог набирать номер, указывая пробелы между отдельными группами разрядов телефонного номера (что-то типа "123 45 67"). По умолчанию СА не позволит этого сделать, так как он будет интерпретировать записанный таким образом номер как несколько слов. Эту проблему можно решить, опять-таки используя функцию preparse(): после того, как игрок введет команду, эта функция должна будет искать во введенной строке все, что хоть как-то напоминает телефонный номер, и, если найдет, заключить всю найденную конструкцию в кавычки. После этого СА будет рассматривать номер как строку в кавычках, и вы сможете определить глагол "набирать" так, чтобы в качестве "прямого" объекта использовался объект класса strObj. Вот как эта функция могла бы выглядеть:
/* Ищем телефонный номер */ ret := reSearch('(%([0-9][0-9][0-9]%) *)?' + '[0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]', cmd); /* Нашли телефонный номер - преобразуем его в строку */ if (ret != nil) { /* Получаем информацию по групповым выражениям шаблона, относящимся к телефонному номеру */ ret := reGetGroup(1); /* Заключаем телефонный номер в кавычки */ cmd := substr(cmd, 1, ret[1] - 1) + ' "' + ret[3] + '" ' + substr(cmd, ret[1] + ret[2], length(cmd)); }
В связи с высокой сложностью встроенных модулей синтаксического анализатора, а также сложностью взаимодействия встроенных модулей и "точек входа", определенных в игре, иногда затруднительно предсказать точную последовательность событий, происходящих в процессе обработки той или иной команды. Это, в свою очередь, зачастую не позволяет сразу определить, почему СА не обрабатывает команду так, как вы предполагали.
Самым эффективным методом отслеживания взаимодействия СА с точками входа является использование Отладчика TADS. Данный инструмент позволяет следить за ходом выполнения программы построчно, контролируя также значения свойств и переменных.
Однако в некоторых случаях вам может потребоваться также знать, что происходит внутри встроенных модулей СА. Хотя ни код игры, ни Отладчик TADS не имеют доступа к внутреннему коду СА, синтаксический анализатор сам по себе содержит средство отладки. Чтобы запустить его, требуется выполнить из кода игры вызов следующей встроенной функции:
debugTrace(1, true); // Включаем отладку СА // Некий код... debugTrace(1, nil); // Отключаем отладку САФормат вызова debugTrace(1, true) сообщает СА о том, что необходимо включить отладочный режим. В этом режиме СА выводит набор сообщений о процессе обработки в ходе анализа каждой команды; эти сообщения отображаются в основном окне игры и содержат информацию о внутреннем представлении СА слов команды, введенной игроком.
После того, как вы включите режим отладки, СА остается в этом режиме до тех пор, пока не встретит вызов debugTrace(1, nil).
Обратите внимание, что отладка СА доступна всегда - независимо от того, выполняется ли игра из Отладчика TADS, или обычным клиентом TADS. При вызове с вышеуказанными аргументами debugTrace не возвращает никакого значения.
Она говорит, как большая, но причудливый ее разговор. Прислушиваешься - как будто все то же самое, что мы с вами сказали бы, а у нее то же, да не совсем так.
АЛЕКСАНДР СТЕПАНОВИЧ ГРИН, Алые паруса (1922)
Раздел 4.1 | Содержание | Раздел 4.3 |