Данный файл является частью Руководства по TADS для авторов игр.
Copyright © 1987, 1996 Майкл Дж. Робертс
(Michael J. Roberts). Все права защищены.
Руководство было преобразовано в формат HTML Н. К. Гайем (N. K. Guy), компания tela design.
Перевод руководства на русский язык - Валентин Коптельцев
В данной главе подробно описаны все ключевые слова, операторы, синтаксические элементы, встроенные функции и специализированные средства языка программирования (ЯП) TADS.
В данной главе используются следующие условные обозначения, позволяющие исключить неоднозначности при описании языка TADS:курсивом выделяется описание элемента, который при написании кода заменяется наименованием конкретного экземпляра элемента. [Квадратные скобки] означают, что заключенные в них элементы являются необязательными. Моноширинным шрифтом выделяются элементы, написание которых должно переноситься в код буквально, включая выделенные таким же образом знаки препинания.
За годы разработки язык TADS обрастал новыми функциями и возможностями, но отследить, какие возможности были доступны в той или иной версии, в прошлом было проблематично. Однако начиная с выхода релиза 2.2.4, в TADS была добавлена возможность проверки, какая именно версия программы-интерпретатора используется. В связи с этим для всех функций и возможностей, добавленных в версии 2.2.4 и позже, указано, начиная с какой версии они появились. Если такой информации не приведено, то соответствующий элемент был доступен в TADS до релиза 2.2.4.
Система разработки TADS включает в себя компилятор, интерпретатор, а также несколько файлов исходного кода на ЯП TADS, содержащих ряд стандартных определений для приключенческих игр.
При написании игры на TADS автор использует текстовый редактор (например, BBEdit, UltraEdit, emacs и т. д. - не входит в дистрибутив TADS) для создания и редактирования файлов исходников, а затем компилирует их, используя компилятор TADS. Компилятор проверяет весь исходный код на наличие синтаксических ошибок, после чего создает новый файл, представляющий собой двоичное представление исходников. Эта форма представления (называемая файлом игры) обеспечивает более высокую эффективность исполнения инструкций программы, за счет этого ваша игра будет требовать меньше памяти и работать быстрее.
После успешной компиляции игру можно запустить, используя интерпретатор TADS. Интепретатор содержит синтаксический анализатор команд игрока, а также все другие необходимые средства для взаимодействия с пользователем.
В качестве исходных файлов компилятор TADS использует файлы в простом текстовом формате (ASCII). С точки зрения компилятора цепочка из пробелов любой длины эквивалентна одному пробелу, при этом пробелами считаются также символы табуляции и перевода строки. Пробелы могут располагаться между любыми двумя синтаксическими элементами, но необходимы только для разделения идентификаторов или ключевых слов, не отделенных друг от друга никакими иными знаками препинания. Компилятор "знает", что знаки препинания не являются частью ключевого слова или идентификатора, и способен разделить знаки пунктуации даже в том случае, если они следуют друг за другом без пробелов. Однако использование пробелов рекомендуется для лучшей читаемости кода.
Директива #include позволяет включить содержимое одного файла в другой:
#include "file.t" #include <file.t>В данном примере file.t - это имя файла, который включается в код. Включаемый файл может в свою очередь включать в себя текст другого файла и т. д. - всего до десяти уровней вложения. Обратите внимание, что значок решетки # должен размещаться с начала строки; ведущие пробелы не допускаются. Как правило, файлы advr.t (и stdr.t) включаются в начале файла с исходным кодом игры, что позволяет получить доступ к многочисленным определениям функций/объектов, необходимым для подавляющего большинства текстовых квестов. Эти определения носят довольно общий характер; при необходимости они могут модифицироваться в исходном коде самой игры.
Обратите внимание, что, когда имя файла заключено в кавычки, компилятор вначале ищет файл в текущем каталоге, а затем в специальном каталоге для "включаемых" файлов (который задается опцией -i для версий компилятора, работающего из командной строки (для большинства операционных систем), либо специальным пунктом меню в комбинированном компиляторе/отладчике TADS). Если же имя файла заключено в угловые скобки, то компилятор не будет просматривать текущий каталог, а сразу перейдет к поиску в каталоге включаемых файлов. Как правило, имена файлов системных библиотек (например, того же advr.t) указываются в угловых скобках, а все остальные размещаются в текущем каталоге и указываются в кавычках. Это может быть особенно удобно, когда автор ведет работу одновременно над несколькими играми, используя для них общие файлы системных библиотек.
Еще раз обратите внимание, что перед директивой #include не должно быть пробела, строка должна начинаться со значка #. Об этой особенности необходимо помнить, когда вы вставляете в свою игру готовый код из другого источника - например, из Интернет-браузера. В противном случае ваш код просто не будет компилироваться.
Компилятор TADS отслеживает названия файлов, включаемых в исходник игры, и игнорирует избыточные директивы #include. Однако обратите внимание, что работа данной функции основана исключительно на имени файла. Если вы обращаетесь к одному и тому же файлу под разными именами (например, указывая при первом включении полный путь к файлу, а во втором используя угловые скобки), компилятор окажется не в состоянии отследить такое "дублирование", и текст файла будет включен в исходный код игры дважды, что приведет к выдаче огромного множества сообщений об ошибках. Вследствие этого предпочтительным является использование директивы #include с угловыми скобками, без указания путей к файлам. Помимо всего прочего, это обеспечит лучшую переносимость исходных файлов игры на другие компьютеры (в том числе кроссплатформенную, так как не требуется учитывать специфические соглашения относительно правил задания путей, действующие в разных операционных системах).
Обратите внимание, что имена включаемых файлов, уже предварительно скомпилированные в бинарный файл, подгружаемый при указании опции -l компилятора, также записываются в бинарный файл. Таким образом, вам не потребуется удалять директиву #include из вашего исходного кода только потому, что вы уже используете предварительно скомпилированную версию этого файла. Более подробную информацию об использовании прекомпилированных исходных файлов можно найти в разделе, посвященном компилятору.
Две косые черты-слэша (//), расположенные вне закавыченного строкового значения, указывают на то, что остаток строки - это комментарий, пропускаемый компилятором.
Кроме того, возможно использование комментариев в стиле языка Си; они начинаются /*, заканчиваются */ и могут охватывать сразу несколько строк.
Примеры:
// Эта строка - просто комментарий./* Это комментарий, занимающий несколько строк. */
Идентификаторы должны начинаться с (латинской) буквы в верхнем или нижнем регистре, и могут состоять из (опять-таки латинских) букв, цифр, знаков доллара и подчеркивания. Идентификатор может иметь длину до 39 символов. Верхний и нижний регистр букв различаются, т. е. MyID и myID - это разные идентификаторы.
Имена всех объектов и функций являются идентификаторами глобального действия. Такие идентификаторы не могут использоваться многократно; иначе говоря, два объекта не могут быть названы одинаково, идентификатор, служащий именем функции, не может быть одновременно именем объекта и т. д.
Наименование свойств - это также глобальные идентификаторы. Название, используемое для свойства, не может служить именем объекта или функции; обратное также верно. В то же время одно и то же название свойства может использоваться в нескольких разных объектах. Поскольку имя свойства никогда не используется само по себе, а только совместно с именем объекта, которому это свойство принадлежит, компилятор TADS в состоянии определить, к какому именно свойству обращается игра, даже если объектов с одинаковыми названиями свойств в игре несколько.
Имена аргументов функций и локальных переменных действуют только внутри функции/метода, в которой они определены. В принципе допустимо использование глобального идентификатора в качестве названия аргумента функции или локальной переменной, в этом случае данная переменная замещает его внутри функции. В то же время это не рекомендуется, поскольку может приводить к путанице.
В общем виде определение объекта выглядит так:
идентификатор: object [список-свойств] ;Это - определение объекта, не имеющего родительского класса. Альтернативно, объект можно определить и как имеющий родительский класс:
идентификатор: имя-класса [, имя-класса [...]] [список-свойств] ;Здесь имя-класса - это идентификатор, определенный где-либо в ином месте программы в качестве объекта или класса (без разницы, с родительским классом или без оного), и задает родительский класс определяемого объекта; если определение объекта содержит несколько родительских классов, их идентификаторы отделяются друг от друга запятыми. Новый объект, имя которого определяется идентификатором, наследует все свойства родительского класса/классов. Если свойство включено в (опциональный) список-свойств и одновременно содержится в списке свойств родительского класса (или родительского класса более высокого порядка), новое свойство заменяет наследуемое.
Если свойство может наследоваться более чем от одного родительского класса (и при этом не переопределяется непосредственно в объекте), то наследование происходит от родительского класса, стоящего в списке ближе к началу. Пусть, например, вы определяете объект следующим образом:
vase: container, fixeditem ;Если оба класса, container и fixeditem, определяют некое свойство m1, а объект vase его не определяет, то будет унаследовано свойство m1 класса container, так как название этого класса находится в списке раньше, чем fixeditem.
Возможен и более сложный случай, хотя на практике он почти не встречается. Скорее всего, вы с ним никогда не столкнетесь, поэтому просто пропустите этот абзац и не заморачивайтесь по его поводу, если он покажется вам слишком запутанным. Предположим, в вышеприведенном примере оба родительских класса, container и fixeditem, в свою очередь, яляются потомками класса item, при этом для классов item и fixeditem определено свойство m2, а для класса container и объекта vase свойства с таким названием не определено. На первый взгляд, кажется, что объект vase унаследует свойство m2 класса container (а следовательно, и класса item), поскольку этот класс стоит в списке первым. На самом деле это не так; поскольку класс fixeditem также наследует свойство m2 от класса item и затем переопределяет его, объект vase унаследует именно этот переопределенный метод. Таким образом, полная версия правила наследования звучит так: в случае множественного наследования наследуется свойство родительского класса, стоящего в списке ближе всего к началу (левее), при условии, что это свойство не переопределяется одним из следующих стоящих в списке классов.
Список свойств имеет следующую форму:
определение-свойства [список-свойств](Данная запись является формализованным способом указания того, что вы можете объединить любое количество определений свойств в списке свойств, размещая их друг за другом).
Определение свойства может выглядеть так:
идентификатор = элемент-данныхОбратите внимание, что после определения свойства не ставится точка с запятой. Точкой с запятой заканчивается определение всего объекта, а не отдельных его свойств.
Примечание переводчика: Свойства, которые определены как простые поля данных, вообще говоря, называются атрибутами (в отличие от методов - см. далее). В то же время так уж сложилось, что данный мануал этому соглашению о терминологии следует далеко не всегда.
Свойство может определять данные следующих типов: число, строка в двойных кавычках, строка в одинарных кавычках, список, объект, а также два специальных значения - nil и true.
Число представляет собой простую последовательность цифр. По умолчанию они записываются в десятичном виде, однако возможно также представление чисел в восьмеричной и шестнадцатиричной транскрипции. Восьмеричные числа начинаются с ведущего нуля; таким образом, например, 035 - это восьмеричная запись числа 29. Шестнадцатиричные числа начинаются с 0x, например, 0x3a9.
Допустимы числовые значения в (десятичном) диапазоне с -2147483647 по 2147483647 включительно. Возможно использование только целых чисел, т. е. числа с плавающей точкой не допускаются.
Строка в двойных кавычках - это произвольная последовательность символов, обрамленных двойными кавычками, например:
"Пример строки в двойных кавычках. "При обращении к строке в двойных кавычках происходит ее вывод на экран. Например, если двойная строка встречается в коде, то при выполнении этого кода эта строка будет выведена на экран. Если же значением какого-либо свойства является строка в двойных кавычках, эта строка будет выводиться при каждом обращении к данному свойству.
Примечание переводчика: строго говоря, строки в двойных кавычках следовало бы отнести к методам, так как они представляют собой особую, сокращенную форму записи процедуры вывода текста на экран. Однако, подозреваю, в силу исторических причин, этот тип свойств считается атрибутом и описывается здесь.
Последовательности из нескольких пробелов подряд в таких строках сжимаются до одиночных пробелов. Это правило распространяется также на переводы строки; строка в двойных кавычках может занимать несколько строк. Например, следующая форма записи совершенно корректна:
"Это - строковое значение, занимающее несколько строк. "Обратите внимание, что, как уже было сказано выше, TADS сжимает все "пустое пространство", включая пустые строки, до одного пробела между словами. Таким образом, вы можете набивать строку текста, не беспокоясь о том, как она будет выведена на экран; TADS выполнит всю работу по сокращению лишних пробелов и переносу строк самостоятельно.
Однако в некоторых случаях вам может потребоваться отформатировать выводимый текст специальным образом, отличным от принятого по умолчанию. Поскольку TADS преобразует все пустое пространство в строке в одиночный пробел, то специальные опции форматирования необходимо задавать в явном виде. Для этих целей в TADS имеются специальные последовательности символов, описанные далее.
\t Знак табуляции, соответствует четырем пробелам. Бывает полезным, если вам необходимо вывести последовательность из нескольких пробелов. Эквивалентом данной последовательности в HTML-TADS является тэг <TAB MULTIPLE=4>. \n Переход на новую строку (перевод каретки). Заканчивает текущую строку и переходит на следующую. Обратите внимание, что повторение этой последовательности несколько раз подряд не будет иметь эффекта, так как блок, отвечающий за форматирование выводимого текста, попросту игнорирует "лишние" переводы строк. Обычно это удобно, поскольку вам не требуется отслеживать количество переводов строк, например, в случае, если текст на экран выводится при обращении к разным свойствам нескольких объектов. Если вам необходимо принудительно задать одну или несколько пустых строк, используйте последовательность \b. Эквивалентный тэг в HTML-TADS - <BR HEIGHT=0>.
\b Заканчивает текущую строку, вставляя за ней пустую. При использовании цепочки таких последовательностей будет выведено соответствующее число пустых строк. Практически полным эквивалентом данной последовательности в HTML-TADS является тэг <P> (за исключением того, что при использовании нескольких таких тэгов подряд не произойдет вставки нескольких пустых строк).
\" Знак двойных кавычек. Обратите внимание, что это "простые" кавычки; если требуется вывести "типографские" кавычки, необходимо использовать возможности HTML-TADS. \' Одинарные кавычки (апостроф). Обратите внимание, что это "простой" апостроф; если требуется использовать его "типографский" эквивалент, необходимо задействовать возможности HTML-TADS. \\ Обратная косая черта (обратный слэш). \< Левая угловая скобка (знак "меньше"). Для вывода одиночного значка эта последовательность в принципе не требуется, однако если необходимо вывести цепочку таких значков, то всем символам в цепочке, начиная со второго, должны предшествовать обратные слэши, поскольку в противном случае TADS воспримет это как попытку включения в строку вычисляемого выражения (см. далее). Таким образом, если вам требуется вывести строку вида <<<<<, то в коде ее надо будет задать следующим образом: "<\<\<\<\<". Также обратите внимание, что в HTML-TADS эта последовательность выводится как левая угловая скобка и не воспринимается как начало тэга. \^ (Т. е. обратный слэш, за которым следует "крышечка" или значок "стрелка вверх"; на большинстве клавиатур с англоязычной раскладкой этот символ выводится нажатием SHIFT и клавиши с цифрой 6). Данная последовательность переводит следующий выводимый на экран символ в верхний регистр. Как правило, эта последовательность используется непосредственно перед обращением к функции или методу, которые должны вывести текст, располагающийся в начале предложения. Действие данной последовательности полностью аналогично действию встроенной функции caps().
Примечание переводчика: к сожалению, как и функция caps(), данная последовательность преобразует регистр только латинских букв, в связи с чем ее применение в русскоязычной игре будет весьма ограниченным. Для символов кириллицы рекомендуется использовать определенные в файле advr.t функции ZA и ZAG.\v Действие этой последовательности противоположно "\^": в то время как "\^" преобразует следующий за ней символ в верхний регистр, "\v" преобразует его в нижний регистр. Действие этой последовательности полностью эквивалентно действию встроенной функции nocaps().
Примечание переводчика: как и предыдущая последовательность, "\v" преобразует регистр только для символов латиницы. Аналога для кириллицы пока не существует - видимо, в связи с тем, что необходимость в таком преобразовании встречается гораздо реже, чем для обратной задачи. В то же время в advr.t определена функция loweru, выполняющая преобразование в нижний регистр для всей строки.\пробел (Т. е. обратный слэш, за которым следует пробел). "Фиксированный" пробел. Бывает полезным для достижения некоторых спецэффектов, так как позволяет отменить некоторые умолчальные опции форматирования, действующие при выводе текста на экран: в частности, обходит "сжатие" цепочки пробелов, а также вывод двух пробелов после точки в некоторых версиях интерпретатора TADS. Например, если необходимо вывести чье-либо имя с сокращенными инициалами, лишний пробел после инициалов, как правило, нежелателен. В этом случае вы можете гарантировать вывод только одного пробела независимо от версии интерпретатора, например, так: "Майкл Дж.\ Робертс". (Имейте также в виду, что некоторые версии интерпретатора могут выводить как один, так и два пробела после точки - в зависимости от пользовательских настроек). \( Начало выделения текста. Текст после последовательности "\(" будет выделяться либо цветом, либо полужирным шрифтом - в зависимости от версии интерпретатора. Для некоторых систем эта последовательность вообще не окажет никакого действия. Обратите внимание, что данная последовательность не может иметь несколько уровней вложения, т. е. все последующие последовательности "\(" в тексте, для которого уже включено выделение, будут просто игнорироваться. Данная опция форматирования является предтечей мультимедийных средств HTML-TADS, которые обеспечивают намного более широкие и гибкие возможности по форматированию текста. \) Окончание выделения текста. Текст после данной последовательности не будет выделяться. Обратите внимание, что одна-единственная последовательность "\)" отключает выделение текста вне зависимости от того, сколько последовательностей "\(" ей предшествовало. \- Передать два байта. Данная последовательность представляет собой знак дефиса, следующий за обратным слэшем, и сообщает блоку вывода текста, что следующие два байта необходимо передать "как есть", не пытаясь их интерпретировать. Эта опция может быть полезной для многобайтных наборов символов (например, компьютера Macintosh с японской локалью), где каждый символ представляется двумя байтами. По большей части вы можете использовать одно- и двухбайтовые символы в тексте в любых комбинациях без каких-либо дополнительных усилий. Однко некоторые двухбайтовые символы содержат в качестве кода одного из байтов ASCII-код обратного слэша; в таких случаях блок вывода текста некорректно воспринимает байт, код которого соответствует "\", как начало специальной последовательности форматирования. Размещая перед такими символами "\-", вы препятствуете такой некорректной интерпретации, и символ отображается правильно. \H+ Включить HTML-возможности интерпретатора. HTML-TADS предоставляет значительно более богатые возможности по форматированию текста по сравнению со стандартным TADS, однако для этого используются не специальные последовательности, а теги. Более подробную информацию вы найдете в соответствующем разделе документации. \H- Отключить HTML-возможности интерпретатора. Принципиального ограничения на длину строки в двойных кавычках не существует, хотя компилятор в ряде случаев может ограничивать ее в зависимости от конкретной конфигурации системы и особенностей кода игровой программы.
TADS предоставляет удобное средство, позволяющее включать в строки выражения, не прерывая эту строку. Всякий раз, когда при выводе строки интерпретатору TADS встречаются две левых угловых скобки (знака "больше"), интерпретатор приостанавливает вывод и считает все, что идет после угловых скобок, выражением, пытаясь вычислить его значение. Конец выражения обозначается двумя правыми угловыми скобками (знаками "меньше"), после которых вывод строки продолжается как обычно. Это позволяет значительно упростить определение свойств, которые должны ссылаться на другие свойства; например, определение
itsHere = "<< self.sdesc >> находится здесь."эквивалентно следующему более подробному определению:
itsHere = { self.sdesc; " находится здесь."; }Включенное в строку выражение может иметь значение числового типа, строки в одинарных кавычках или строки в двойных кавычках. Значения иных типов недопустимы. В строку может быть включено любое количество выражений. Включенное в строку выражение не может содержать инструкции (такие, как, например, if), а для отделения друг от друга нескольких выражений нельзя использовать знаки точки с запятой, а только запятые.
Для этого средства ЯП существует ряд ограничений. Во-первых, недопустимо включать в строку в качестве выражения другую строку, которая, в свою очередь, также содержит вложенное выражение. Во-вторых, включение выражений можно использовать только для строк в двойных кавычках (а не в одинарных).
Поскольку некоторым авторам игр может потребоваться вывод в строке нескольких левых угловых скобок подряд, в TADS имеется служебная последовательность "\<", описанная выше.
Обратите внимамание, что использование включаемых в строку выражений полностью совместимо с HTML-TADS и не вызывает проблем, несмотря на использование символов < и >. Это возможно благодаря тому, что соответствующие символы интерпретируются компилятором в ходе компиляции игры и уже не появляются в конечном тексте, выводимом игрой.
Строки в одинарных кавычках по своему внешнему виду (и применяемым служебным последовательностям) во многом аналогичны рассмотренным выше строкам в двойных кавычках, только, конечно, заключены в апострофы. Чтобы включить апостроф в "тело" строки, используется последовательность \'.
Строки в одинарных кавычках не выводятся автоматически на экран при обращении к ним, а используются как строковые значения (аналогично таковым в других ЯП). Для работы с ними существует ряд встроенных функций, одна из которых, say, как раз служит для вывода их на экран. Вообще, выражение say('Строка для вывода') практически полностью эквивалентно записи "Строка для вывода" (за исключением использования включенных выражений).
Обратите также внимание, что все лексические свойства являются значениями строкового типа (или списками, состоящими из строковых значений).
Список - это конгломерат из других типов данных. Списки записываются в виде набора значений, обрамленного квадратными скобками (обратите внимание, что в данном случае квадратные скобки - это не признак опциональности, а обязательный элемент кода:
[ список-значений ]Список-значений может не содержать ничего; в этом случае список будет пустым, например:
[ ]Он также может содержать набор значений (числовых, строковых, логических, объектов или списков). В большинстве случаев списки состоят из элементов с одинаковым типом данных, но это не является обязательным требованием языка (например, такие "разношерстные" списки активно используются в парсерных функциях - примечание переводчика).
Примеры списков:
[ 1 2 3 ] [ 'привет' 'пока' ] [ [1 2 3] [4 5 6] [7 8 9] ] [ vase goldSkull pedestal ]Элементы списка необязательно должны быть константами. Единственное исключение - если весь список целиком должен быть константой (например, при определении значения свойства); в этом случае и все его элементы тоже должны быть константами (т. е. в нем нельзя будет использовать локальные переменные и выражения с переменным значением). Однако список, включаемый напрямую в код (например, при присвоении списочного типа значения локальной переменной) может содержать среди своих элементов в том числе и переменные значения. Например, следующая функция создает список из трех выражений в динамическом режиме:
f: function(x, y, z) { return([x+1 y+1 z+1]); }Переменные элементы списка в данном случае допустимы, поскольку список используется в теле функции ("кода"). Обратите также внимание, что вышеописанное определение функции полностью эквивалентно следующему:
f: function(x, y, z) { return((([ ] + (x+1)) + (y+1)) + (z+1)); }Во втором примере вычисляемое выражение начинается с пустого списка, к которому по очереди присоединяются вычисленные выражения x+1, y+1 и т. д. В обоих примерах результирующие списки будут полностью идентичными. В то же время по возможности рекомендуется использовать первый вариант записи как намного более эффективный: в нем создается всего один список, тогда как во втором таких списков последовательно создается целых четыре. Первый вариант будет выполняться быстрее и потребует меньше памяти.
В TADS предопределены две специальные константы, nil и true. В основном они используются в качестве логических (или булевских) значений, при этом nil соответствует значению "ложь", а true - "истина". Например, выражение 1 > 3 имеет значение nil, а 1 < 3 - true. Кроме того, nil используется в качестве признака отсутствия значения; например, nil возвращается при попытке получить первый элемент пустого списка, а также при обращении к свойству объекта, которое для него не определено.
Рекомендуется использовать в качестве логических значений именно nil и true, а не их числовые эквиваленты (например, 0 и 1), поскольку это сделает код вашей программы более наглядным.
При определении свойства может использоваться не просто константа, а выражение. Если свойство определено при помощи выражения, это выражение должно быть заключено в круглые скобки. Значение выражения будет вычисляться при каждом обращении к свойству. Пример:
exprObj: object x = 1 y = 2 z = (self.x + self.y) ;При каждом обращении к exprObj.z будет вычисляться и возвращаться сумма текущих значений exprObj.y и exprObj.z.
Обратите внимание, что свойство, определенное таким образом, может иметь в своем определении параметры (аргументы), точно так же, как методы (собственно, такое свойство и является частным случаем метода), например:
exprObj2: object w(a, b) = (a * b) ;При обращении к exprObj2.w ему требуется передать два параметра, которые будут перемножены, а полученное выражение - возвращено в вызывающий код. Например, при обращении exprObj2.w(3, 4) будет возвращено значение 12.
Обратите также внимание, что свойство z из предыдущего примера можно было бы определить без использования префикса self. перед x и y:
z = (x + y)Связано это с тем, что при использовании имен свойств без ссылки на объект по умолчанию считается, что имеется в виду текущий объект self.
Специальной категорией свойств являются лексические, которые определяют набор слов, при помощи которых игрок может обращаться к объекту. Совокупность лексических свойств всех объектов образует словарь игры. Названия лексических свойств зарезервированы системой.
Всего существует пять лексических свойств (по частям речи): noun - существительное, adjective - прилагательное, verb - глагол, preposition - предлог и article - артикль (в русском языке, понятное дело, не используется). Лексические свойства должны иметь строковое значение (в одинарных кавычках) или представлять собой список таких строковых значений, записываемых через пробелы без дополнительных разделителей. Использование других типов может привести к непредсказуемым эффектам.
Как правило, область применения лексических свойств, соответствующих разным частям речи, выглядит так: noun и adjective используются для большинства объектов (предметов) в игровом мире; verb - для глаголов, с помощью которых игрок манипулирует предметами; preposition - для всевозможных предлогов-связок. Определение для одного объекта лексических свойств из разных категорий (скажем, noun и verb) в принципе возможно, но не рекомендуется, особенно если у вас нет большого опыта работы с (R)TADS. Такое объединение может иметь смысл, если автор хочет добиться некого спецэффекта и при этом очень четко представляет себе, какого именно.
Одной из особенностей лексических свойств является их механизм наследования: они наследуются только в том случае, если родительский класс не является объектом в игре, а определен с помощью директивы class. В принципе, такой подход понятен: иметь в игре два или несколько предметов с идентичным набором лексических свойств (как это происходило бы, если бы TADS поддерживал их наследование от экземпляров объекта) в подавляющем большинстве случаев нежелательно.
Чаще всего набор лексических свойств задается статически на этапе компиляции, однако в TADS есть средства для изменения словаря в ходе самой игры. Подробнее об этом см. здесь.
В определениях свойств, наряду с прочими типами данных, можно использовать код. Такое свойство называется методом. Другими словами, метод - это код внутри объекта. Когда происходит обращение к свойству, выполняется соответствующий код. Возвращаемое значение считается значением метода, но зачастую код также производит какие-либо дополнительные действия.
Код метода заключается в фигурные скобки {}. Любые конструкции, допустимые для функций, можно использовать и в определениях методов. Методы могут определять локальные переменные, им также могут передаваться аргументы. Если метод содержит аргументы, их список (который в принципе аналогичен списку аргументов функции) перечисляется в скобках после названия метода, например:
obj1: object f(x) = { return(x + 1); } ;При обращении к такому методу список аргументов также указывается после названия метода:
f1: function { say(obj1.f(123)); }
Функция - это структурно законченный фрагмент кода, который, в отличие от метода, не является частью объекта. Определение функции в обобщенном виде выглядит так:
идентификатор: function [ ( список-аргументов ) ] { тело-функции }Список-аргументов может отсутствовать, т. е. наличие аргументов у функции необязательно. Однако, если аргументы все-таки имеются, их список выглядит следующим образом:
идентификатор [, список-аргументов ]Идентификаторы могут использоваться так же, как локальные переменные, определенные внутри функции.
Вероятно, обобщенная запись в приведенных выше примерах выглядит несколько запутанной, поэтому приведем пример определения функции:
addlist: function(list) // сложить номера в списке { local sum, count, i; i := 1; // устанавливаем счетчик на первый элемент списка sum := 0; // сбрасываем сумму count := length(list); // получаем количество элементов в списке while (i < count) // пока список не кончился... { sum := sum + list[i]; // прибавляем текущий элемент i := i + 1; // переходим к следующему элементу списка } return(sum); }
В TADS имеется возможность определить функцию, которой можно будет передавать переменное количество аргументов. В определении такой функции вы можете задать обязательный минимум аргументов, передающихся функции в любом случае (этим минимумом может быть и отсутствие аргументов), а затем указать, что опционально могут быть переданы и другие аргументы. Для этих целей используется многоточие ("..."), которое ставится в качестве последнего элемента в списке аргументов функции. Например, определение функции, которая может вызываться с любым количеством аргументов - от нуля до бесконечности - будет выглядеть так:
f: function(...) { }А это - определение функции, которой всегда передается как минимум один аргумент, но могут передаваться и дополнительные аргументы:
g: function(fmt, ...) { }Если функции может передаваться переменное количество аргументов, вы можете определить, сколько именно аргументов было ей передано при текущем вызове, используя псевдо-переменную argcount. Значение этой переменной соответствует числу аргументов, переданных текущей функции.
Чтобы получить аргумент, используйте встроенную функцию getarg(argnum). Параметр argnum - это номер аргумента, который вы хотите получить; getarg(1) возвращает первый переданный аргумент, getarg(2) - второй и т. д. Обратите внимание, что если для функции перед знаком многоточия определены обязательные аргументы, getarg будет возвращать значения и для них. Например, в вышеприведенном примере для функции g, getarg(1) вернет значение аргумента fmt.
Еще одна встроенная функция, datatype(значение), может использоваться для определения типа данных переданного аргумента.
Следующая функция выводит на экран любое количество переданных ей значений:
displist: function(...) { local i; for (i := 1 ; i <= argcount ; i++) { say(getarg(i)); " "; } "\n"; }
Альтернативная форма записи инструкции function позволяет заранее объявить имя функции, не определяя саму функцию. Формат такого предварительного объявления выглядит так:
идентификатор: function;Обратите внимание, что при этом функция не определяется; таким образом мы просто сообщаем компилятору, что где-то дальше будет объявлена функция с именем, соответствующим объявленному идентификатору. Это позволяет зарезервировать идентификатор в качестве имени функции; если этого не сделать, компилятор может "подумать", что идентификатор относится к объекту.
В большинстве случаев предварительное объявление не требуется, за исключением ситуаций, когда используется setdaemon() и подобные ей встроенные функции. В принципе, когда в коде определен вызов функции, компилятору "понятно" из самого синтаксиса этого вызова, идет ли речь о функции или об объекте. Однако при использовании setdaemon() и других функций той же группы из синтаксиса невозможно определить, происходит ли обращение к объекту или к функции; в этом случае (при отсутствии предварительно объявленной функции) по умолчанию считается, что происходит обращение именно к объекту.
В этом разделе мы рассмотрим правила языка для описания тела методов и функций. Функции и методы состоят из серии инструкций; в общем случае инструкции выполняются последовательно в том порядке, в котором они записаны в исходном коде программы. Далее будут рассмотрены инструкции, которые можно использовать.
Каждая инструкция в (R)TADS завершается точкой с запятой.
В самом начале определения тела функции или метода вы можете объявить локальные переменные, которые будут действительны только в текущем блоке кода. Пример записи соответствующей инструкции:
local список-идентификаторов ;Список-идентификаторов, в свою очередь, должен иметь следующий вид:
идентификатор [ инициализация ] [, список-идентификаторов ]Инициализация является опциональной и в общем виде выглядит так:
:= выражение, где выражение - это любое корректное выражение, которое может содержать аргументы, передаваемые функции/методу, а также любые локальные переменные, объявленные до той переменной, которая инициализируется при помощи данного выражения. Вычисление выражения и присвоение его значения соответствующей переменной происходит до выполнения инициализации следующей переменной (если таковая имеется), а также до того, как будет выполнена первая инструкция в теле функции/метода, следующая за local. Локальные переменные с инициализацией и без нее могут определяться в рамках одной инструкции local в произвольном порядке. Локальным переменным, для которых инициализация не выполнялась, при выполнении программы автоматически присваивается значение nil.
Еще раз отметим, что определяемые таким образом переменные "видны" только внутри тела функции/метода, где они объявлены. Кроме того, переменные, объявленные при помощи инструкции local, замещают в том блоке кода, где они объявлены, любые глобальные переменные с теми же именами.
Инструкция local может располагаться в начале блока (блоком называется совокупность инструкций, заключенных в фигурные скобки); кроме того, один блок может содержать несколько инструкций local, однако все они должны предшествовать первой исполяемой инструкции в блоке. Иначе говоря, единственной инструкцией, которая может предшествовать инструкции local в блоке, является другая инструкция local.
Ниже приведен пример объявления локальных переменных с использованием нескольких инструкций local, в том числе с инициализацией.
f: function(a, b) { local i, j; /* переменные не инициализируются */ local k := 1, m, n := 2; /* некоторые переменные инициализируются, некоторые нет */ local q := 5*k, r := m + q; /* q можно использовать сразу после того, как она инициализирована */ for (i := 1 ; i < q ; i++) { local x, y; /* локальные переменные могут объявляться в начале любого блока */ say(i); } }
В (R)TADS используется алгебраическая запись переменных. Операторы имеют разный смысл для различных типов данных. Список основных операторов (в порядке убывания приоритета) приведен ниже.
Возвращает "адрес" функции или свойства. Другими словами, этот оператор позволяет обращаться к функции/свойству без ее/его вызова.
Имя функции/свойства должно следовать за оператором & без пробела. Возвращаемое значение может быть присвоено локальной переменной либо передано в качестве аргумента другой функции/методу. Впоследствии этот "адрес" может использоваться для вызова функции/свойства. См. также раздел "Непрямой вызов функций и методов" далее в этой главе. (Примечание: в более старых версиях TADS для получения адреса функции использовался оператор #. Этот оператор и сейчас понимается системой, однако в современных версиях рекомендуется использовать &).
. (Точка). Обращается к свойству объекта. С левой стороны точки записывается выражение, значением которого является объект, а справа - имя свойства. Если свойство является методом, требующим передачи аргументов, эти аргументы записываются в круглых скобках после названия свойства (точно так же, как при вызове функции). С правой стороны можно использовать также имя локальной переменной или выражение, заключив их в круглые скобки; в этом случае они должны ссылаться на свойство объекта.
[] Индексация списка; ставится справа от выражения, значение которого - список. Между квадратными скобками должно размещаться выражение со значением числового типа. Если индекс (то самое значение в квадратных скобках) равен n, то данный оператор возвращает n-ный элемент списка. Первый элемент списка имеет индекс 1 (т. е. индексация списка ведется с единицы, а не с нуля). Например, выражение ['один' 'два' 'три'][1+1] вернет в качестве значения строку 'два'. Обратите внимание, что выход числа n за границы списка (т. е. если n окажется меньше единицы или больше числа элементов в списке, возвращаемого функцией length, приведет к возникновению ошибки исполнения.
++ Инкремент; увеличение на единицу числовой переменной или свойства. Выражение, к которому применяется оператор ++, должно допускать присваивание ему значения (иначе говоря, это должно быть такое выражение, которое допустимо использовать с левой стороны оператора присваивания, :=. Оператор ++ может либо предшествовать выражению-операнду, либо следовать за ним. Если оператор предшествует операнду (например, ++i), то операнд увеличивается на единицу, и значением выражения будет значение операнда после этого увеличения. Если же оператор следует за операндом (например, i++), значением выражения будет значение операнда до увеличения. Так, если в наших примерах i имеет значение 3, то ++i будет иметь значение 4, в то время как i++ будет иметь значение 3. Иначе говоря, представьте, что вы читаете выражение слева направо, и при этом учтите, что значение выражения соответствует значению переменной i в тот момент, когда вы ее прочитываете, а операция инкремента производится, когда вы прочитываете оператор ++. Тогда для выражения ++i вы сначала считываете оператор инкремента, который прибавляет единицу к i, а затем получаете значение i, которое в этот момент уже равно 4. Для записи i++ вы сначала считываете переменную i, которая на тот момент еще равна 3, и уже после этого вы видите оператор ++, который присваивает i значение 4.
Примечание переводчика: очень тонкое различие;). Честно говоря, на практике я не сталкивался с ситуациями, где бы использовались особенности расположения оператора ++ относительно операнда.
-- Декремент; уменьшение на единицу числовой переменной или свойства. В целом полностью аналогичен оператору ++, за исключением того, что уменьшает операнд, а не увеличивает его.
= Равенство. Операнды по обеим сторонам должны принадлежать одному и тому же типу данных, но не могут быть списками. Единственное исключение: на равенство nil можно проверять данные любого типа. Если оба операнда равны, выражение равно true, в противном случае оно равно nil.
<> Неравенство. Равно true, если операнды с правой и левой стороны неравны (при этом оба операнда должны иметь один и тот же тип данных).
> Операция "больше". Как и другие операторы сравнения, может применяться только к данным числовых и строковых типов. Возвращает true или nil. Обратите внимание, что при применении к строковым данным сравнение основывается на принятом порядке таблицы символов, используемой в вашем компьютере (например, ASCII).
< Операция "меньше". >= Операция "больше или равно". <= Операция "меньше или равно".
, Оператор-запятая просто позволяет вам начать новое выражение внутри другого выражения. С левой и с правой стороны от запятой ставятся выражения. Эти выражения полностью независимы; запятая между ними не выполняет никаких вычислительных действий над ними. Возвращаемым значением пары выражений, разделенных запятыми, будет значение второго (правого) выражения. Оператор-запятая полезен обычно в случаях, когда необходимо только одно выражение, но вы хотите вычислить их несколько ради каких-либо побочных эффектов. Например, при инициализации инструкции for вам может потребоваться инициировать несколько переменных; вы можете сделать это, разделив их запятыми. Примечание: оператор-запятая отличается от запятой, используемой для разделения аргументов при вызове функции/метода. В функции или методе разделяются разные выражения, являющиеся аргументами; в обычном же выражении оператор-запятая отделяет части одного и того же выражения. Как уже было сказано, операторы приведены в порядке убывания приоритета. Таким образом, операторы-запятые выполняются в последнюю очередь, после того, как будут выполнены операторы с более высоким приоритетом. Обратите внимание, что некоторые операторы в списке разбиты на группы; например, сгруппированы все операторы сравнения. Внутри каждой группы операторы имеют равный приоритет. Операторы с равным приоритетом выполняются слева направо; например, выражение 3-4+5 эквивалентно (3-4)+5. Единственным исключением является оператор присваивания, для которого действует обратный порядок выполнения; например, в выражении a := b := 2 значение 2 сначала присваивается переменной b, а затем переменной a присваивается значение b.
Для изменения порядка выполнения операторов в выражении можно использовать круглые скобки.
Операция присваивания не является специальной инструкцией; по сути, это выражение, использующее оператор присваивания:
сущность := выражение;Здесь сущность может быть локальной переменной, элементом списка или свойством объекта. Если значение присваивается свойству объекта, ссылка на объект может быть выражением. Точно так же, если значение присваивается элементу списка, то выражениями могут быть как ссылка на список, так и индекс элемента в списке. При этом, однако, обратите внимание, что нельзя присваивать значение элементу списка с номером большим, чем размер списка. Иными словами, индекс элемента при присваивании всегда должен лежать в границах существующего списка. Несколько примеров присваивания:
a: function(b, c) { local d, e, lst; d := b + c; // присваивание значения локальной переменной obj3.prop1 := 20; // присваивание значения свойству prop1 объекта obj3 e := obj3; // присваивание локальной переменной значения объектного типа e.prop1 := obj2; // присваивание свойству prop1 объекта obj3 значения объектного типа obj2 e.prop1.prop2 := 20; // присваивание свойству prop2 объекта obj2 числового значения 20 fun1(3).prop3 := 1 + d; // присваивание значения свойству prop3 объекта, возвращаемого выражением fun1(3) lst := [1 2 3 4 5]; // присваивание значения списочного типа локальной переменной lst[3] := 9; // теперь lst имеет значение [1 2 9 4 5]... lst[5] := 10; // ...а теперь - [1 2 9 4 10] /* lst[6] := 7 - такая операция будет некорректной, так как в списке всего пять элементов */ }
Пользователи (R)TADS, которые "по совместительству" являются программистами на C, часто находят удобным общее сходство между TADS и C, однако те незначительные отличия, которые все-таки существуют между языками, служат для таких пользователей постоянным источником ошибок и недоразумений при переходе с одного языка на другой. В связи с этим в (R)TADS имеется возможность использовать операторы в стиле C. Обратите внимание, что, если вы не являетесь опытным программистом на C, вам скорее всего не потребуется читать этот раздел.
Прежде всего, (R)TADS поддерживает весь набор операторов C.
a % b Возвращает остаток от деления a на b a %= b Присваивает значение (a % b) переменной a a != b Эквивалентно (a <> b)
!a Эквивалентно (not a)
a & b Побитное И a &= b Устанавливает a равным (a & b) (результату операции побитного И для a и b) a | b Побитное ИЛИ a |= b Устанавливает a равным (a | b) (результату операции побитного ИЛИ для a и b) a && b Эквивалентно (a and b) a || b Эквивалентно (a or b) a ^ b побитное XOR (исключающее ИЛИ, сложение по модулю 2) a и b a ^= b Устанавливает a равным (a ^ b) (результату операции побитного XOR для a и b) ~a побитное отрицание (инверсия) a a << b Сдвинуть a влево на b битов a <<= b Устанавливает значение a равным (a << b) (результату операции сдвига a на b битов влево) a >> b Сдвинуть a вправо на b битов a >>= b Устанавливает значение a равным (a >> b) (результату операции сдвига a на b битов вправо)
Некоторые из этих операторов, такие как !, &&, and ||, являются просто альтернативной формой записи операторов (R)TADS. "Побитные" операторы работают не с логическими, а с числовыми значениями; они "рассматривают" операнды как битовые векторы (массивы, состоящие из отдельных бит), и осуществляют соответствующую операцию к каждому биту двоичного представления чисел. Например, выражение 3 & 2 вернет значение 2, поскольку в двоичном представлении операнды будут записываться как "011" и "010", соответственно. Операции побитного смещения по своему действию аналогичны делению или умножению на степень двойки; 1 << 5 вернет значение 32, поскольку оно эквивалентно умножению 1 на 2 в пятой степени.
Во-вторых, в (R)TADS возможен режим работы, в котором используется оператор присваивания языка C. Обычный оператор присваивания (R)TADS имеет вид :=, а оператор равенства =. В C эти операторы выглядят, соответственно, как = и ==. Если вам удобнее работать с записью операторов, принятой в C, вы можете указать (R)TADS, что будете использовать именно такие операторы вместо обычных. По умолчанию (R)TADS использует "родные" версии операторов. Перейти на запись, принятую в C, можно двумя способами: используя ключ командной строки при запуске либо вставив директиву компилятора #pragma в исходный текст программы.
Чтобы откомпилировать всю игру целиком "в стиле C", используйте ключ командной строки -C+ (пользователи "Макинтошей" могут воспользоваться соответствующим пунктом меню "Options"; при выборе этого пункта компиляция будет происходить по правилам записи, принятым в C, а при сбросе - по стандартным правилам (R)TADS). Использование ключа -C+ означает применение правил записи языка C для всего исходного кода компилируемой игры. (Ключ -C- явным образом отключает этот режим; именно так происходит компиляция по умолчанию).
Чтобы указать компилятору, что определенный файл необходимо компилировать с применением правил записи C, вы можете использовать директиву #pragma C+. "Парная" ей директива #pragma C- указывает на необходимость применения обычных операторов (R)TADS. Эти директивы могут располагаться где угодно в файле исходного кода; они должны быть единственными директивами в строке и должны начинаться с первого символа строки (т. е. перед ними не должно быть пробелов).
Директива #pragma оказывает влияние только на текущий файл с исходным кодом, а также на файлы, которые включены в него директивой #include. Оба стандартных библиотечных файла, входящих в комплект поставки RTADS, advr.t и stdr.t, используют стандартную форму записи операторов TADS, и для них в явном вмде задана директива #pragma C-. Однако в связи с тем, что эта директива распространяется только на сами эти файлы, вы спокойно можете включать их в состав файла игры, использующего запись операторов в стиле C. Просто используйте, как обычно, директиву #include adv.t - даже если ваш файл использует режим C, файл advr.t будет откомпилирован правильно, поскольку для него режим записи по правилам, принятым в C, будет отключен, а по окончании обработки advr.t компилятор (R)TADS автоматически восстановит режим компиляции исходного файла.
Еще раз обратите внимание, что установка C-совместимого режима оказывает влияние только на операторы присваивания и равенства. Все остальные операторы C (такие, как операторы побитовых действий) могут использоваться в любом режиме.
Если компилятор работает в режиме совместимости с C, то он выдает сообщение "possibly incorrect assignment" ("возможно, некорректная операция присваивания") во всех случаях, когда встречает выражение следующего вида:
if ( a = 1 ) ...Синтаксически такая инструкция совершенно корректна, однако при использовании записи операторов в стиле C она означает, что переменной a присваивается значение 1; поскольку возвращаемым значением операции присваивания всегда будет то значение, которое присваивается, то услоие данного оператора if всегда будет возвращать true. Написание подобного оператора присваивания в случаях, когда на самом деле требуется сравнить два значения - это очень распространенная ошибка среди программистов на C (независимо от их опыта). Более того, изначально я специально выбрал ":=" в качестве оператора присваивания в TADS, чтобы избежать таких ошибок. Впоследствии же, когда в (R)TADS появилась возможность переключаться с использования "родных" операторов на операторы языка C, я ввел вышеописанное предупреждающее сообщение о некорректной операции присваивания, чтобы оперативно отлавливать такие ошибки. Компилятор будет отслеживать операции присваивания, встречающиеся в условиях операторов if, while и do, а также оператора for. Чтобы избежать вывода этого сообщения, вы можете явным образом выполнить сравнение результата оператора присваивания, например:
if ( ( a = 1 ) != 0 ) ...Отдельные C-подобные операторы могут вызвать некоторые проблемы.
Прежде всего, оператор >> нельзя использовать в строках с конструкциями вида << >>, поскольку в этом случае он будет восприниматься как завершение вычисляемого в строке выражения. Даже использование скобок в этом случае не поможет, поскольку компилятор распознает конструкции << >> до того, как проверяет включенные в них выражения. Таким образом, код следующего вида не будет работать:
myprop = "x divided by 128 is << (x >> 7) >>! " // неправильный кодВместо этого следует использовать следующую форму записи:
myprop = { "x divided by 128 is "; x >> 7; "! "; } // правильный кодВо-вторых, оператор & может интерпретироваться не только как унарный, но и как бинарный. В большинстве случаев проблем с этим не будет, за исключением одной ситуации: в списках. В своей игре вы можете использовать списки, имеющие следующий вид:
mylist = [ &prop1 &prop2 &prop3 ]В прошлых версиях TADS такой список толковался однозначно, поскольку оператор & был исключительно унарным. Однако поскольку теперь возможно использование с этим оператором двух операндов, то такая запись может интерпретироваться и как три выражения с применением унарного оператора &, и как единое выражение, включающее в себя один унарный и два бинарных оператора &.
В целях совместимости с предыдущими версиями (R)TADS в вышеназванном примере воспримет операторы & как унарные. Однако, встретив такую конструкцию, он выдаст предупреждающее сообщение о том, что она неоднозначна (новое сообщение TADS-357, "operator "&" interpreted as unary in list." - "оператор "&" интерпретирован в списке как унарный"). Вы можете избежать вывода этого сообщения двумя способами. Во-первых, можно исключить неоднозначность списка. Для этого между его элементами надо вставить запятые:
mylist = [ &prop1, &prop2, &prop3 ]Обратите внимание, что, если вы действительно хотите, чтобы оператор воспринимался как бинарный, вам надо просто заключить все выражение в скобки:
mylist = [ ( 2 & 3 & 4 ) ]Другой способ избавиться от данного предупреждающего сообщения - это новая опция компилятора -v-abin, которая отключает вывод этого предупреждения. При указании этой опции командной строки компилятор по-прежнему интерпретирует оператор & как унарный, но не сообщает вам об этом.
Обратите внимание, что (R)TADS будет воспринимать любой оператор, имеющий как унарную, так и бинарную версию, как унарный, если встретит его в списке. Для оператора - это соглашение является новым по сравнению с более старыми версиями TADS, где минус в списках всегда интерпретировался как бинарный (операция вычитания). Я не думаю, что это будет серьезной проблемой совместимости, поскольку старая бинарная интерпретация всегда была скорее нежелательной, и, по-моему, пользователи ее избегали. Однако, если у вас имеется древняя игра, то, возможно, стоит скомпилировать ее хотя бы один раз без указания опции -v-abin и проверить все строки, для которых выдается предупреждение TADS-357 (для операторов + и -) - это позволит вам убедится, что поведение игры не изменится при компиляции в новой версии (R)TADS. Все предупреждения TADS-357 для оператора & для игры, созданной с использованием старой версии TADS, можно спокойно игнорировать.
Примечание переводчика: плюсы и минусы в списках, увы - это проблема не только древних версий, как описано выше. Даже во вполне современной игре запись вида
[i-1]всегда будет интерпретироваться как[i, -1].Если имеется в виду именно список, состоящий из единственного элемента со значением, равным переменной i, уменьшенной на единицу, запись должна выглядеть так:[(i-1)].
Вызов функции - это выражение, включающее в себя название вызываемой функции и список передаваемых ей аргументов:
имя-функции( [ список-аргументов ] );Здесь имя-функции - это название функции, определенное где-то в коде программы. Список аргументов (если он задан) передается функции при вызове, значения этих аргументов используются при выполнении функции. (Разумеется, количество передаваемых при вызове аргументов в общем случае должно совпадать с тем количеством, которое задано в определении функции). Каждый из аргументов может быть произвольным выражением; аргументы должны отделяться друг от друга запятыми.
Скобки после названия функции требуются вне зависимости от того, передаются ли ей аргументы или нет; собственно, скобки необходимы для того, чтобы сообщить компилятору, что в этой строке происходит вызов функции. (Может возникнуть резонный вопрос - а зачем в принципе использовать имя функции, если не происходит ее вызова? Ответ таков: некоторые специализированные встроенные функции, такие как setdaemon, которая описывается ниже, позволяют указать функцию, которая, возможно, будет вызываться впоследствии, однако в момент указания ее имени обращения к ней не проиходит. В подобных случаях имя функции используется без скобок).
Если функция возвращает значение, то при таком формате вызова это значение попросту отбрасывается. Подобный формат вызова означает, что функция вызывается ради действий, которые она выполняет, а не ради возвращаемого ею значения.
Пример:
showlist(a+100, 'Список: ', list1);
Функции и методы можно также вызвать "непрямым образом" - иначе говоря, вы можете определить указатель/ссылку для вызова функции или метода. Данную ситуацию можно также рассматривать следующим образом: вы обращаетесь к функции, не выполняя ее. Это бывает полезным, когда необходимо определить некую общую функцию, которая, в свою очередь, вызывает разные функции в зависимости от переданных ей параметров.
Чтобы вызвать функцию с использованием ссылки на нее, просто присвойте переменной значение, соответствующее адресу функции, а впоследствии используйте при вызове функции имя этой переменной, заключенное в скобки, например:
g: function(a, b, c) { return(a + b + c); } f: function { local fptr, x; fptr := &g; /* получить адрес функции g */ x := (fptr)(1, 2, 3); /* вызвать функцию с использованием указателя */ }Указатель на свойство используется точно так же.
f: function(actor, obj) { local propPtr := &doTake; /* получить указатель на свойство doTake */ obj.(propPtr)(actor); /* обратиться к свойству при помощи указателя */ }Пусть, например, вам необходимо реализовать способ задавания вопросов персонажам в игре относительно разнообразных предметов. Одно из возможных решений - создать обобщенный обработчик "ask" ("спросить"), которому будут передаваться в качестве аргументов персонаж и предмет. Для каждого объекта определим свойство, которое будет выводить информацию, соответствующую знаниям того или иного персонажа о данном объекте. Далее, для персонажей определим при помощи указателя, к какому свойству предмета необходимо будет обратиться, чтобы получить ответ персонажа про этот предмет. Если для объекта такое свойство не определено, это будет означать, что персонаж ничего про этот предмет не знает.
Наш обобщенный обработчик получится довольно простым (точнее, весьма компактным):
ask: function(actor, obj) { local propPtr; /* определить, к какому свойству предмета надо будет обращаться */ propPtr := actor.askPropPtr; /* проверить, определено ли это свойство для предмета */ if (defined(obj, propPtr)) { /* оно определено - осуществляем его непрямой вызов */ obj.(propPtr); } else { /* оно не определено - используем ответ по умолчанию */ actor.dontKnow; } }Таким образом, для каждого объекта-персонажа необходимо просто определить свойство, "отвечающее" за объем знаний персонажа о том или ином предмете, и поместить ссылку на это свойство в свойство персонажа под названием askPropPtr. Кроме того, для персонажа должен быть определен также ответ по умолчанию в свойстве dontKnow. Вот пример:
joe: Actor sdesc = "Джо" noun = 'джо' dontKnow = "Джо лишь пожимает в ответ плечами, скребя в затылке. " askJoe = "Пожалуй, выслушивать историю жизни Джо - это чересчур. " askPropPtr = askJoe ;Наконец, для каждого предмета необходимо определить соответствующее свойство askJoe, если Джо должен что-либо знать об этом предмете. Аналогичным образом можно определить необходимый набор свойств и для других персонажей. Достоинство такого подхода в том, что при этом удается локализовать всю информацию, относящуюся к объекту (и в том числе знания о нем персонажей), внутри этого объекта. Кроме того, обобщенный обработчик ask имеет на редкость несложную структуру. Безусловно, сам описанный здесь механизм может с первого взгляда показаться запутанным, однако конечное решение получается простым, удобным в работе и легко масштабируется.
Функция может вернуть значение в ту часть кода, откуда осуществлялся ее вызов, при помощи следующей конструкции:
return [ выражение ];Вне зависимости от того, указано выражение или нет, выполнение функции будет прервано, и программа продолжает выполняться с того места, откуда выполнялся вызов. Если выражение указано, то значение этого выражения передается в вызывающий код в качестве значения функции.
Ниже приведен пример функции, которая вычисляет сумму элементов списка и возвращает эту сумму в качестве своего значения.
listsum: function(lst) { local i, sum, len := length(lst); for (i := 1, sum := 0 ; i <= len ; i++) sum += lst[i]; return(sum); }Обратите внимание, что скобки указывать необязательно. Последняя строка функции могла бы выглядеть и так:
return sum;В действительности последние версии стандартного файла-библиотеки advr.t в основном используют форму записи без скобок.
Обобщенная форма записи условного оператора в (R)TADS выглядит так:
if ( выражение ) инструкция [ else инструкция ]Здесь и далее инструкция может толковаться и как одиночная инструкция, и как набор (последовательность) инструкций, заключенных в фигурные скобки. Значение выражения должно быть либо числовым (в этом случае нулевое значение считается эквивалентом "нет", а любое другое - эквивалентом "да"), либо логическим - true или nil.
Обратите внимание, что опциональный подоператор else считается относящимся к последнему предшествующему ему оператору if в том случае, если используются вложенные условные операторы. Рассмотрим следующий пример:
if (self.islit) // В комнате горит свет if (film.location = Me) // Игрок несет пленку "Опа! Вы засветили пленку! "; else "В этой комнате темно. ";Очевидно, что в этом примере автор хотел, чтобы подоператор else относился к первому оператору if, однако на самом деле компилятор отнесет его ко второму (как к последнему предшествующему else). Чтобы избежать таких ситуаций, следует применять фигурные скобки, явно задавая с их помощью группировку операторов:
if (self.islit) // В комнате горит свет { if (film.location = Me) { "Опа! Вы засветили пленку! "; } } else { "В этой комнате темно. "; // Игрок несет пленку }
Инструкция switch позволяет вам в ряде случаев организовать эквивалент большого конгламерата из условных операторов if-else, который будет проще записываться, легче читаться и исполняться программой более эфффективно. Инструкция switch осуществляет сравнение некоего выражения с несколькими значениями и выполнять разные группы инструкций в зависимости от результатов этой проверки.
Форма записи инструкции switch имеет следующий вид:
switch ( выражение ) { [ список-подоператоров-case ] [ подоператор-default ] }Форма записи списка-подоператоров-case:
case выражение-константа : [ инструкции ] [ список-подоператоров-case ]Форма записи подоператора-default is:
default: [ инструкции ]В этих обобщенных формах записи инструкции означают, что за case или default может следовать от нуля и более инструкций. Подоператоры case можно и вовсе опустить, так же, как и default.
Выражение может быть числового, строкового, объектного, списочного типа либо true/nil. Значение выражения сравнивается с со значениями, следующими за каждым подоператором case. Если одно из сравнений даст положительный результат, будут выполнены все инструкции, следующие за соответствующим подоператором case. Если ни одно из case-сравнений не даст положительного результата и при этом определен подоператор default, будут выполнены инструкции после default. Если же нет ни "удачных" сравнений, ни default, то вся инструкция switch будет пропущена и выполнение программы возобновится после закрывающей фигурной скобки switch.
Обратите внимание, что выполнение инструкции switch не прерывается, когда выполнение доходит до следующего подоператора case. Вместо этого продолжается выполнение инструкций, следующих за этим case. Если вы хотите прервать выполнение инструкций внутри конструкции switch после выполнения группы инструкций, относящихся к одному подоператору case, вам необхоимо использовать инструкцию break.
Инструкция break при использовании внутри конструкции switch несет специальную функцию: она указывает, что выполнение конструкции switch должно быть прервано и возобновлено инструкциями, следующими после закрывающей фигурной скобки конструкции switch.
Ниже приведен пример конструкции switch.
f: function(x) { switch(x) { case 1: "x равен одному"; break; case 2: case 3: "x равен 2 или 3"; break; case 4: "x равен 4"; case 5: "x равен 4 или 5"; case 6: "x равен 4, 5, или 6"; break; case 7: "x равен 7"; break; default: "x находится вне интервала от 1 до 7"; } }
Инструкция while определяет цикл, т. е. набор инструкций, которые выполняются снова и снова до тех пор, пока выполняется некое условие.
while ( выражение ) инструкцияКак и для инструкции if, инструкция может быть как отдельной инструкцией, так и набором инструкций, объединенным при помощи фигурных скобок. Выражение должно быть либо числовым (в этом случае нулевое значение рассматривается как "ложь", а любое другое - как "истина"), либо логическим (true/nil).
Выражение вычисляется до того, как цикл выполнится в первый раз; если в этот момент оно примет значение "ложь", то цикл не будет выполняться вовсе. В противном случае будут выполнены все инструкции, после чего вновь будет проверено значение выражения; если оно по-прежнему имеет значение "истина", то инструкции будут выполнены еще раз, а затем снова будет выполняться проверка значения и т. д. Как только выражение принимает значение "ложь", выполнение программы возобновляется с инструкции, следующей сразу за циклом.
Инструкция do-while определяет цикл несколько иной конструкции, чем while. Этот тип цикла также выполняется до тех пор, пока некое контрольное выражение не примет значение "ложь" (точнее, 0 или nil), однако вычисление значения выражения здесь производится в конце цикла. За счет этого цикл в любом случае будет выполнен хотя бы один раз, поскольку первая проверка значения контрольного выражения произойдет после первого выполнения цикла.
Обобщенная форма записи этой инструкции:
do инструкция while ( выражение );Инструкция может быть как отдельной инструкцией, так и набором инструкций, объединенным при помощи фигурных скобок. Выражение должно быть либо числовым (в этом случае нулевое значение рассматривается как "ложь", а любое другое - как "истина"), либо логическим (true/nil).
Инструкция for определяет наиболее общий и наиболее мощный тип цикла. Любой цикл for всегда можно заменить циклом на основе инструкции while, однако цикл for при аналогичной работе зачастую позволяет получить намного более компактный и читабельный код.
Обобщенная форма записи для данной инструкции:
for ( нач-выраж ; усл-выраж ; повт-выраж ) инструкцияКак и для других цикловых конструкций, инструкция может быть как отдельной инструкцией, так и набором инструкций, объединенным при помощи фигурных скобок.
Первое выражение, нач-выраж - это так называемое "выражение начальной инициализации". Его значение вычисляется один раз, перед первым проходом цикла. Оно используется для того, чтобы присвоить начальные выражения переменным, задействованным в цикле.
Второе выражение, усл-выраж, носит название условия цикла. Оно имеет точно такое же назначение, что и выражение условия цикла while. Это выражение вычисляется перед каждым проходом цикла. Если его значение соответствует логическому "да" (ненулевое числовое или true), то выполнение цикла продолжается; в противном случае цикл прерывается, и выполнение программы продолжается инструкцией, следующей сразу за циклом. Обратите внимание, что, как и условие цикла while, усл-выраж вычисляется в том числе и перед первым проходом цикла (однако после вычисления выражения нач-выраж); следовательно, цикл for не будет выполнен ни разу, если усл-выраж при начале цикла будет иметь значение, соответствующее логическому "нет".
Третье выражение, повт-выраж - это "выражение повторной инициализации". Это выражение вычисляется после каждого прохода цикла. Если оно возвращает некое значение, это значение отбрасывается; единственное назначение повторной инициализации - изменить необходимым образом переменные, задействованные в цикле. Как правило, выражение повторной инициализации увеличивает переменную-счетчик цикла или выполняет какое-либо аналогичное действие.
Любые из вышеназванных выражений (и даже все они вместе) могут отсутствовать. Отсутствие усл-выраж эквивалентно использованию в качестве условия цикла константы true; таким образом, цикл с заголовком for ( ;; ) будет выполняться вечно либо до тех пор, пока в теле цикла не встретиться инструкция break. Цикл типа for, не определяющий выражения начальной и повторной инициализации, полностью идентичен циклу while.
Ниже приведен пример использования инструкции for. Эта функция реализует простой цикл, выполняющий суммирование элементов списка.
sumlist: function(lst) { local len := length(lst), sum, i; for (sum := 0, i := 1 ; i <= len ; i++) sum += lst[i]; }Обратите внимание, что цикл, выполняющий абсолютно те же действия, можно записать с пустым телом, возложив суммирование на выражение повторной инициализации; присваивание начального значения переменной len также может осуществляться в заголовке цикла (выражением начальной инициализации).
sumlist: function(lst) { local len, sum, i; for (len := length(lst), sum := 0, i := 1 ; i <= len ; sum += lst[i], i++); }
Используя инструкцию break, можно добиться, чтобы программа завершила выполнение цикла досрочно:
break;Это бывает полезно, когда требуется выйти из цикла до его "нормального" завершения. Выполнение программы возобновляется с инструкции, следующей непосредственно за тем циклом, в котором определена инструкция break.
Кроме того, инструкция break используется также для выхода из конструкции switch. В этом случае программа переходит к выполнению инструкции, непосредственно следующей за закрывающей скобкой тела инструкции switch.
Инструкция continue передает управление к началу (заголовку) того цикла, в котором определена. Ее можно использовать в цикловых конструкциях for, while и do-while.
В цикле for инструкция continue передает управление выражению повторной инициализации. Иначе говоря, вначале вычисляется значение третьего выражения в заголовке цикла (если оно присутствует), а затем - второго (условного) выражения (опять-таки, если оно имеется в наличии). Если значение второго выражения соответствует логическому "да" либо это выражение отсутствует, выполнение программы продолжается первой инструкцией тела цикла; в противном случае начинают выполняться инструкции, следующие непосредственно за цикловой конструкцией.
Инструкция goto используется для безусловного перехода внутри функции/метода, где она определена. Для того, чтобы переход осуществился, необходимо задать для "целевой" инструкции метку. Метку определяют, указывая перед инструкцией имя метки с двоеточием на конце.
Обратите внимание, что действие метки всегда распространяется на всю функцию/метод, где она определена. В этом состоит отличие от локальных переменных, которые действуют в пределах блока (группы инструкций, ограниченных фигурными скобками), в котором они определены. Обратиться к метке из-за пределов определяющей ее функции/метода нельзя.
Пример использования инструкции goto:
f: function { while (true) { for (x := 1 ; x < 5 ; x++) { /* выполнение неких действий */ if (myfunc(3) < 0) /* возникла ошибка? */ goto exitfunc; /* да, ошибка - покидаем функцию */ /* выполнение еще каких-то действий */ } } /* сюда мы попадаем, если что-то пошло не так */ exitfunc: ; }Подобное использование goto избавляет от необходимости вводить дополнительную проверку условия во внешнем цикле while, что позволяет несколько упростить код и сделать его более наглядным.
Среди цивилизованных специалистов по вычислительной технике инструкция goto считается вредным и порочным пережитком древних, вышедших из употребления языков, и репутации TADS в кругах серьезных разработчиков языков программирования, несомненно, был нанесен непоправимый ущерб тем, что эта инструкция все же была в него включена. Таким образом, вам следует использовать ее как можно реже, а еще лучше вообще не использовать, в особенности если вы хотите произвести впечатление своими программистскими опытами на какого-нибудь цивилизованного специалиста по вычислительной технике. Однако многие программисты-практики рассматривают goto как очень полезный инструмент в руках опытного профессионала, и посмеиваются над категоричностью обвинительного суждения всезнающих академиков, большинство из которых, скорее всего, не написало ни строчки кода с тех пор, как их собственные преподаватели пинали их за использование goto в программах на Паскале, за исключением, может быть, отдельных алгоритмов на псевдо-коде, которые, впрочем, все равно обычно заканчиваются словами "написание оставшейся части предоставляется читателю в качестве упражнения". Автор TADS не собирается принимать ничью сторону в этом ожесточенном противостоянии (да, это чувствуется - примечание переводчика), однако надеется, что сможет удовлетворить оба воинствующих лагеря, дав одним возможность использовать goto где можно и где нельзя, а другим - насладиться собственной добродетельностью оттого, что они могли бы применить goto, но перебороли этот грязный соблазн. При работе в (R)TADS выбор остается за вами.
Метод может с какого-то момента унаследовать поведение одноименного метода родительского класса, для этого используется инструкция pass:
pass имя-метода;Имя-метода должно совпадать с именем того метода, где определена инструкция pass. При выполнении инструкции pass вызывается тот метод родительского класса, который бы вызывался в том случае, если бы текущий объект его не переопределил. При вызове используются те же аргументы, что и при вызове метода, в котором определена инструкция pass. При этом объект self не изменяется (т. е. ссылается на тот объект, метод которого вызывался изначально, до обращения к pass).
В ходе обработки команды игрока игра может прервать нормальный ход событий, вернувшись к запросу/получению следующей команды у игрока, тремя разными способами. Инструкция abort прерывает обработку полностью и запрашивает следующую команду; обычно она используется в системных командах (например, сохранение/восстановление игры), которые не должны считаться за ход, в связи с чем они не вызывают демоны/запалы. С другой стороны, инструкция exit пропускает всю дальнейшую обработку команды, сразу переходя к выполнению демонов и запалов; обычно она используется, когда автору требуется прервать обработку (как вариант - команда обработана "досрочно"). Наконец, exitobj по своему действию полностью аналогична exit, за исключением того, что пропускает обработку только для одного объекта в команде, переходя к следующему.
Обратите внимание, что все сказанное про демоны/запалы относится как к демонам, запускаемым при помощи функции notify, так и к демонам и запалам, запускаемым при помощи setdaemon и setfuse.
Вы можете использовать инструкцию abort внутри демона/запала, при этом она будет работать "как задумано" (прервет текущую обработку и пропустит выполнение всех оставщихся демонов/запалов после данного хода). В прежних версиях TADS этого делать было нельзя.
Примечание переводчика: честно говоря, не рекомендовал бы размещать abort в демонах/запалах, и вот почему. Порядок инициации и выполнения демонов и запалов в TADS в общем случае непредсказуем (т. е. никто, в том числе и сам автор TADS, не может дать твердых гарантий, что (а) порядок выполнения демонов всегда соответствует порядку их инициации и (б) остается неизменным при каждом прохождении игры). Поскольку за увеличение счетчика ходов в игре обычно также отвечает демон, то, возможно, демон с abort будет иногда выполняться раньше него, а иногда - позже. "Нерегулярное", а то и непредсказуемое изменение числа ходов в игре вполне может вызвать у игрока недоумение. Кроме того, выполнение запалов также может меняться от одной игрвой сессии к другой непредсказуемым образом.
Эти инструкции прерывают текущую обработку наподобие abort, пропуская также выполнение демонов и запалов, однако позволяют игроку вводить дополнительную информацию. Инструкция askdo просит у игрока ввести "прямой" объект; система при этом выводит соответствующий запрос. Игрок при этом может ввести либо запрашиваемый объект, либо новую команду. Например, для глагола "взять" при выполнении askdo будет выведен запрос "Что вы хотите взять?", после чего система будет ожидать ответа игрока. Инструкция askio действует аналогично, но ей в качестве аргумента передается предлог; этим предлогом дополняется запрос "косвенного" объекта. Например, если используется глагол "отпереть" и выполняется следующая команда:
askio(withPrep);, то система выведет запрос "При помощи чего вы хотите отпереть это?", при этом игрок может ввести "косвенный" объект или новую команду. Инструкция askio может использоваться только в том случае, если "прямой" объект уже определен.
В любом случае, если игрок в ответ на запрос вводит что-либо, что может интерпретироваться как указание на объект, система пытается применить текущую команду снова. В противном случае его ввод считается новой командой, которая и будет выполнена вместо исходной. В любом случае, независимо от того, что ввел игрок, инструкции, следующие за askdo или askio, выполняться не будут, так как цикл обработки команды начнет выполняться сначала.
О принципах формирования запросов на ввод объектов и функциях-"точках входа", которые за это отвечают, можно почитать здесь.
Обратите внимание, что в некоторых случаях система не будет запрашивать новый объект у игрока, а попытается найти подходящий объект по умолчанию, используя для этого стандартные механизмы синтаксического анализатора.
При обращении к свойствам объектов система определяет специальный объект под названием self. Этот специальный объект ссылается на тот объект, которому принадлежит данное свойство. С первого взгляда это кажется довольно бессмысленным, однако представьте себе ситуацию, когда родительский класс объекта определяет свойство, обращающееся к другим свойствам объекта:
class book: object description = { "Эта книга << self.color >>."; } ; redbook: book color = "красная" ; bluebook: book color = "синяя" ;В этом примере обобщенный объект, book, "знает", каким образом описать книгу в зависимости от ее цвета. Определяемые объекты-наследники, красная книга (redbook) и синяя книга (bluebook) просто определяют свой цвет в предназначенном для этого свойстве вместо того, чтобы заново переопределять свойство-описание. Таким образом, при обращении к свойству-описанию красной книги (redbook.description) мы получим следующее сообщение:
Эта книга красная.
Специальный псевдо-объект под названием inherited позволяет обращаться к свойствам родительского класса текущего объекта self. Этот объект по своему действию несколько напоминает инструкцию pass, но в ряде случаев предоставляет гораздо больше возможностей. Прежде всего, inherited позволяет просто вызвать метод родительского класса и вернуть управление текущему свойству после того, как выполнение вызываемого метода закончилось; инструкция pass же не позволяет вернуться обратно в вызывающий метод. Во-вторых, вы можете обращаться к inherited из выражения благодаря тому, что возвращаемое вызываемым методом значение не теряется и не отбрасывается, а может быть использовано при дальнейших вычислениях. В-третьих, вызываемому при помощи inherited методу можно передавать аргументы, отличные от таковых текущего метода.
Вы можете использовать inherited во всех тех же случаях, что и self.
Ниже приведен пример использования псевдо-объекта inherited.
myclass: object sdesc = "myclass" prop1(a, b) = { "Это свойство prop1 класса myclass. self = << self.sdesc >>, a = << a >>, b = << b >>.\n"; return(123); } ; myobj: myclass sdesc = "myobj" prop1(d, e, f) = { local x; "Это свойство prop1 объекта myobj. self = << self.sdesc >>, d = << d >>, e = << e >>, f = << f >>.\n"; x := inherited.prop1(d, f) * 2; "Возвращаемся в свойство prop1 объекта myobj. x = << x >>\n"; } ;При вызове myobj.prop1(1, 2, 3) будет выведен следующий текст:
Это свойство prop1 объекта myobj. self = myobj, d = 1, e = 2, f = 3. Это свойство prop1 класса myclass. self = myobj, a = 1, b = 3. Возвращаемся в свойство prop1 объекта myobj. x = 246.Обратите внимание на одну особенность inherited, объединяющую этот псевдообъект с инструкцией pass: в обоих случаях объект self остается тем же и в вызывающем (методе-наследнике), и в вызываемом методе. В этом заключается принципиальная разница между использованием inherited и прямым вызовом родительского метода (при котором в качестве имени объекта вместо inherited используется имя родительского объекта).
Начиная с версии TADS 2.2.4 синтаксис обращения к inherited был расширен - теперь можно указывать также родительский класс, чей метод надо наследовать, однако в остальном правила использования данного псевдо-объекта не изменились:
inherited fixeditem.doTake(actor);В приведенном выше примере мы указываем, что хотим унаследовать поведение метода doTake родительского класса fixeditem, хотя бы даже стандартные правила наследования определяли бы в качестве родительского другой класс. Это бывает полезным в случаях множественного наследования, когда может понадобиться более полный контроль над тем, от каких именно родителей должен наследовать свои реакции объект-потомок.
Псевдопеременная argcount возвращает количество аргументов, переданных текущей функции. Ее можно использовать для определения количества аргументов, переданных при конкретном вызове функции, которая позволяет задавать переменное количество аргументов. Обратите внимание, что argcount можно использовать как обычную переменную, с единственным ограничением: ей нельзя присваивать значений.
Большинство авторов игр сталкиваются с тем, что при написании игры более-менее приличного размера им так или иначе не избежать редактирования стандартной библиотеки advr.t. Хотя ничего принципиально порочного в такой практике нет, при выходе новой версии TADS могут возникать проблемы: в таком случае вам придется либо по-прежнему пользоваться вашей старой, но измененной вами копией advr.t, что автоматически означает добровольный отказ от всех улучшений и исправлений ошибок, выполненных в новой версии TADS; либо провести анализ и синхронизировать изменения обеих версий библиотеки, что потребует значительных затрат труда и времени. Использование инструкций replace и modify может помочь вам в решении этой проблемы.
Эти инструкции позволяют вносить изменения в ранее определенные объекты. Другими словами, вы можете использовать стандартную библиотеку advr.t при помощи инструкции #include, а затем указать изменения для объектов, которые компилятор уже обработал. При помощи этих инструкций можно задавать изменения трех типов: полную замену функции, полную замену объекта, а также добавление новых или замену существующих свойств объекта.
Для замены ранее определенной функции вам достаточно поставить перед новым определением инструкцию replace. Само определение ничем не отличается от обычного. В следующем примере показано альтернативное определение функции scoreStatus из библиотеки advr.t, выполняющей нестандартное форматирование строки статуса:
#include <adv.t> replace scoreStatus: function( points, turns ) { setscore( cvtstr( pts ) + ' очков/' + cvtstr( turns ) + ' ходов' ); }Вы можете выполнить точно такие же действия и для объектов. Например, вы можете полностью заменить глагол пристегнуть ("пристегнуть"), определенный в advr.t:
#include <adv.t> /* Отменяем глагол "пристегнуться" */ replace fastenVerb: deepverb verb = 'пристегнуть' sdesc = "пристегнуть" prepDefault = toPrep ioAction( toPrep ) = 'FastenTo' ;При полной замене объекта предыдущее определение удаляется, включая всю информацию о наследовании свойств и записи в словаре. Остаются только те свойства объекта, которые указаны в его новом определении; первоначальное определение отбрасывается целиком.
Вы также можете модифицировать объект, сохранив его исходное определение (в том числе информацию о наследовании свойств, записи в словаре и т. п.). При этом вы можете определять для объекта новые свойства, а также замещать отдельные его свойства новыми, просто указывая свойства с тем же названием в новом определении.
Наиболее распространенной модификацией объектов является, вероятно, определение новых лексических свойств и глагольных обработчиков.
modify pushVerb verb = 'нажимай' ioAction( withPrep ) = 'PushWith' ;Обратите внимание на ряд особенностей, которые иллюстрирует данный пример. Во-первых, в инструкции modify вы не можете указывать родительские классы - они сохраняются теми же, что и для исходного определения объекта. Далее, обратите внимание на указанное дополнительно лексическое свойство. Оно не заменяет исходные лексические определения, а добавляется к ним. Наконец, обратите внимание, что при модификации определения объекта допустимо указывать новые псевдосвойства-обработчики глаголов (таких как doДействие и ioДействие). Все вновь определяемые обработчики трактуются так, как если бы они были сразу указаны в исходном определении объекта.
В методе, который вы переопределяете с помощью инструкции modify, вы можете использовать инструкцию pass для обращения к исходной версии метода в оригинальном определении объекта. Резюмируя, можно сказать, что механизм действия modify следующий: оригинальный объект переименовывается, а вместо него создается новый объект под тем же именем, который является потомком исходного (который становится безымянным). После применения modify вы уже не сможете напрямую обратиться к исходному объекту; единственный способ задействовать его - косвенный, через заменивший его объект. Ниже приведен пример использования pass совместно с modify.
class testClass: object sdesc = "testClass" ; testObj: testClass sdesc = { "testObj..."; pass sdesc; } ; modify testObj sdesc = { "модифицированный testObj..."; pass sdesc; } ;При обращении к testObj.sdesc на экран будет выведено:
модифицированный testObj...testObj...testClassВы также можете заменить какое-либо свойство целиком, удалив все следы его первоначального определения. При этом при использовании pass или inherited произойдет обращение к методу, унаследованному объектом от родительского класса. Для такой операции необходимо разместить инструкцию replace непосредственно перед названием заменяемого свойства. Рассмотрим вариацию на тему предыдущего примера:
modify testObj replace sdesc = { "модифицированный testObj..."; pass sdesc; } ;При этом выводимый при обращении к testObj.sdesc текст изменится следующим образом:
модифицированный testObj...testClassИтак, повторим еще раз - инструкция replace, размещенная перед определением свойства, сообщает компилятору о том, что предыдущее определение этого свойства должно быть удалено полностью (вместо "скрытого наследования", которое произойдет, если не указать replace). Разница проявится в том, что pass и inherited будут обращаться не к исходному определению свойства, а к соответствующему свойству родительского класса.
В языке (R)TADS определен набор встроенных функций, облегчающих жизнь программисту. Эти функции описаны в данном разделе. Обращение к ним в принципе не отличается от вызова обычных функций, за исключением того, что встроенные функции, которым не передаются аргументы, не требуют обязательного указания скобок.
Вызов: addword(объект, &часть_речи, слово)
Ставит в соответствие объекту слово (которое должно быть строковым значением в одинарных кавычках) в качестве указанной части_речи. Часть_речи может принимать значение noun (существительное), adjective (прилагательное), plural (множественное число), verb (глагол), article (артикль - в русском языке, по понятным причинам, вряд ли будет использоваться), либо preposition (предлог). Вы можете использовать эту функцию как применительно к объектам, определенным статически в коде игры, так и к объектам, динамически созданным в ходе ее выполнения.
Примеры использования данной функции можно найти в разделе "Динамический словарный запас" главы, посвященной синтаксическому анализатору.
Вызов: askfile(текст_запроса, код_типа_запроса, код_типа_файла, флаг)
История: аргументы с кодами типов появились, начиная с TADS 2.3.0. Аргумент флаг появился в TADS 2.5.0.
Данная функция выводит запрос пользователю на ввод имени файла, причем вид запроса зависит от конкретной системы. Обычно выводится соответствующее стандартное диалоговое окно операционной системы; если же таковое отсутствует, то будет просто выведен текстовый запрос. В зависимости от соглашений, действующих в конкретной операционной системе, для формирования запроса может использоваться (а может и не использоваться) аргумент текст_запроса (строковое значение в одинарных кавычках); для систем без стандартного диалогового окна запроса имени файла именно этот аргумент будет выведен в качестве текстового приглашения на ввод.
Эта функция наиболее полезна для таких операций, как сохранение/восстановление игры, где требуется, чтобы игрок ввел имя файла. Строковые выражения, передаваемые через этот аргумент, могут содержать последовательности \n и \t, которые будут правильно преобразованы при отображении текста запроса.
В TADS 2.3 появились два дополнительных, опциональных аргумента, которые позволяют автору игры точнее определить, какой именно тип запроса следует отображать и какой тип файла запрашивать. Эти аргументы являются своего рода "подсказками" для кода операционной системы, отвечающего за вывод диалогового окна "Открыть"; указывая эти аргументы, вы позволяете системе более правильно настроить вид диалога.
Аргумент код_типа_запроса указывает, происходит ли открывание существующего файла или сохранение файла. В некоторых операционных системах (включая Windows и MacOS) для открытия существующего файла используется один тип диалога, а для сохранения - другой; вы можете использовать данный аргу