Сразу признаюсь: я никогда и не писал уроки и инструкции. Решил попробовать себя в этом деле. Коллектив у нас на форуме немногочисленный, но дружный, поэтому думаю, что вы не будете сильно пинать меня за такой низкокачественный урок, а укажете на проблемы в изложении, скажете как не надо писать. В конце концов это не последний урок, который я хочу написать, поэтому надо учиться.
О задумке:
Решил написать полный разбор демонстрационной игры "Мышки" с постепенным описанием всех конструкций и операторов языка. Не знаю, на сколько это правильный подход. Увидим по отзывам. Начать конечно-же надо со стандартной библиотеки. Стандартная библиотека включает несколько хорошо проработанных классов и других объектов, на основе которых можно довольно быстро создавать текстовые игры для платформы ТОМ. А использовать стандартные библиотеки без знания их подробной структуры почти невозможно.
Способ изложения:
Сначала идёт кусок кода из модуля, затем подробный разбор всех встретившихся в нём операторов и конструкций. Описываются, естественно, только новые и не описанные ранее операторы. Т.е. эти уроки рассчитаны для "самых маленьких", возможно даже, мало знакомых с программированием. В конце разбора подводится итог всего модуля в виде схематического описания структуры всего модуля с описанием назначения каждого объекта и его методов.
Скажу ещё раз, что писалось это из под неподготовленной руки, ночью и отрывками. Поэтому прослеживается несвязность текста и очень много воды. Но ведь лишнее всегда можно убрать.
Итак, поехали!
Начнём с модуля Main.tom.
// ==================================================================== // ASBer(C)2009 ТОМ - Текстовая Основа Миростроения // Модуль включает минимально необходимый набор классов и действий. // v.0.1 - 30.01.2010 // ====================================================================
Это комментарий, описывающий модуль
Комментарии начинаются с // и идут до конца строки
class состояние //вспомогательный класс для обозначения состояний
Это объявление класса "состояние". Лексема "class" задает категорию объекта. Все объекты имеют категорию. Существует всего 8 категорий: класс (class), локация (location), уникальное (unique), счётное (counting), мыслимое (mental), действие (action), фраза (phrase), событие (event).
Наиболее часто используются категории class, location, unique и action. Остальные встречаются реже или не встречались автором статьи вообще. Не будем останавливаться и расписывать назначение всех категорий. Будем изучать их по мере нахождения.
Чтобы создать объект, надо указать категорию и имя объекта:
<категория> <имя>
{
<тело объекта>
}
категория может указываться английскими или русскими буквами (на вкус). Вообще, ТОМ почти на все операторы и слова имеет русские эквиваленты, порой даже несколько.
Если тело объекта пропущено, значит этот объект ещё будет описан ниже. Так делается для предобъявления объекта, чтобы можно было использовать ещё не описанный объект.
Итак, здесь мы встретили категорию класс. Класс отличается от других категорий тем, что не используется непосредственно как объект или явление в программе. Он лишь описывает поведение для других объектов. Зато от класса можно наследовать объекты, которые наследуют от него это поведение. От класса можно наследовать объекты любых категорий, т.е. класс впринципе, может описывать всё что угодно. В этом и заключается его особенность. Одним словом класс - это мощное средство сокращения кода и упрощения программирования.
А пока нам нужно лишь знать, что класс - это некий набор объектов, обладающих одними свойствами (стулья, персонажи, места и т.д.). В модуле Main и описываются самые основные классы.
class нечто_видимое //класс объединяющий предметы, персонажей и локации { //полное описание выводится при первом осмотре объекта если оно задано this.полное_описание = "" this.уже_осмотрено = нет }
Класс "нечто_видимое" содержит всего 2 свойства: "полное_описание" и "уже_осмотрено". Формат описания свойства:
this.<имя_свойства> = <значение_свойства>
В ТОМ-е не надо указывать тип свойств и переменных, он сам определяет их и может менять во время выполнения. Всего существует 5 основных (number, bool, object, string, null), 2 дополнительных (key, lexeme) и 2 производных (object.form, object.item) типа. Типы тоже будем расписывать по мере встречи.
В классе "нечто_видимое" мы видим 2 типа:
"" - string: строка - это любая последовательность символов. В данном случае - пустая. От строки, кстати, отходят 2 дополнительных типа: key и lexeme. (Если честно, то это и не типы вовсе - для парсера это те же строки, но гораздо проще считать их типами).
В этом классе значение свойства "полное_описание" пустое потому, что автор игры должен сам для каждого объекта делать собственное полное описание.
нет - bool: логический - это тип данных, который может принимать лишь 2 значения: да или нет. У этих значений есть по несколько синонимов: да=yes=истина=true, нет=no=ложь=false. Над данными логического типа действуют операции булевой алгебры. О них позже.
Идём дальше...
class место { cls = нечто_видимое title = "<font color=red>{name}</font>" //предлоги this.предлог = "в" //находится в комнате this.пред_в = "в" //вошел в комнату this.пред_из = "из" //вышел из комнаты //описание переопределяется автором this.описание = "это некоторое место - {this}." //st_описание обычно не переопределяется автором this.st_описание = "{описание} {this.item:предмет_по_месту}.{item:персонаж_по_месту}.{menu()}" this.для_чего = нет//склад для запчастей this.из_чего = нет//склад запчастей this.чей = нет//дом рудокопа OnEnter() { %_ %<font color=yellow>{this}</font> if(полное_описание && уже_осмотрено==нет) { %{полное_описание}. this.уже_осмотрено = да } else %{st_описание}. } this.можно_войти = да this.здесь = "здесь; НдСи; Ип; Ип=; Рп=; Дп=; Вп=; Тп=; Пп=;" }
Класс место описывает объекты, обозначающие некоторое ограниченное пространство, в котором будет происходить действие. По идее, от этого класса должны наследоваться объекты категории location (локация), но никто не мешает нам унаследовать от него объект другой категории.
Cls - это специальное свойство, которое задаёт класс объекта. Всего существует 9 специальных свойств объектов: name(имя), loc(лок), cls(класс), ctg(категория), lex(лексема), pat(шаблон), item(элемент), title(наименование), menu_name(заголовок_меню) (В скобках указаны русские эквиваленты). Они по умолчанию определены у всех объектов и их не надо определять или переопределять заново, следовательно пишутся они без this.
"cls = нечто_видимое" означает, что класс "место" унаследован от класса "нечто_видимое" и обладает всеми его свойствами и методами.
Далее идёт специальное свойство title, которое задаёт наименование объекта. Потом наименование для каждого объекта автор должен будет переопределить сам. А здесь указано стандартное наименование, которое возвращает программное имя объекта красным шрифтом:
<font color=red> </font> - это тэги прям как в html, означающие, что текст, содержащийся между ними будет красного цвета (color=red).
{name} - фигурные скобки позволяют ссылаться к различным свойствам и переменным и производить над ними некоторые операции прямо в строке. В данном случае идёт обращение к специальному свойству name, которое возвращает программное имя объекта. В данном случае программным именем является "место". Если унаследовать от этого класса другой объект, то свойство name этого объекта возвратит уже его программное имя.
Затем идёт определение предлогов, с которыми мы ещё встретимся.
"это некоторое место - {this}" - this указывает на объект, которому принадлежит код, который сейчас выполняется. Здесь парсер должен перевести {this} в строку. this здесь - это объект "место". При переводе объекта в строку парсер просто возвращает его свойство title, в данном случае "<font color=red>{name}</font>". Name в свою очередь = "место". Т.е. при обращении к описанию непосредственно из класса платформа выдаст "это некоторое место - <font color=red>место</font>". Если мы унаследуем от класса "место", например объект "пещера" и переопределим в нём свойство title = "странная пещера", описание для пещеры будет "это некоторое место - странная пещера.".
this.st_описание = "{описание} {this.item:предмет_по_месту}.{item:персонаж_по_месту}.{menu()}" - с каждым разом строки всё сложнее , поэтому разжёвываем всё тщательно и не теряем нить понимания:
{описание} - возвращает значение свойства описание.
{this.item:предмет_по_месту} - Начнём с того, что каждый объект может содержать в себе другие объекты. Для каждой категории смысл этих объектов свой, например, для локаций - это содержащиеся в локации объекты. Список этих объектов можно получить через специальное свойство item, которое имеет тип object.item, о котором упоминалось выше. Также вложенные объекты называют элементами объекта. Оператор ":" в данном случае служит для фильтрации этого списка по наличию свойства "предмет_по_месту", т.е. this.item:предмет_по_месту возвратит список объектов, которые содержатся в объекте this и содержат не пустое свойство "предмет_по_месту". При переводе этого списка в строку получится перечисление свойств "предмет_по_месту" всех объектов в списке.
Казалось бы, объекты при переводе в строку возвращают title, а тут вдруг возвращают фильтр. Может это особенность типа object.item - возвращать не title, а свойство, по которому фильтровали список.
{item:персонаж_по_месту} - аналогично предыдущему, только без this, что ставит под сомнение надобность употребления слова this вообще
{menu()} - вызывает метод menu() и возвращает строку, которую возвратил этот метод. Метод menu() возвращает меню локации, если оно задано.
для_чего, из_чего, чей - вспомогательные свойства для генерации текстов.
Дальше идёт описание метода OnEnter().
Есть 7 предопределённых методов: menu(меню), ChkMoveObj(ПроверкаПеремещенияОбъекта), BefMoveObj(ПередПеремещениемОбъекта), OnEnter(НаВход), AftMoveObj(ПослеПеремещенияОбъекта), background(ФоновыеДействия), persbackground(ФоновыеДействияПерсонажа).
За подробностями OnEnter заглянем на вики: "Обработчик события вызывается в случае когда персонаж, управляемый игроком, входит в локацию. Вызов происходит после перемещения персонажа в локацию, но до события AftMoveObj(). Также этот обработчик вызывается при переключении управления на другого персонажа, если персонажи находятся в различных локациях. Метод OnEnter() должен быть определен для локации, но вызывается от имени управляемого персонажа."
Оператор "%" служит для вывода текста на экран.
%_ - "_" при выводе заменяется на пробел. Дело в том, что если написать "% " или просто "%", то парсер посчитает это за пустую строку и не выведет на экран. Также есть другие символы, которые необходимо заменять в строках после "%". Например, "^" заменяется на ".". Полный список замен я, к сожалению, не знаю.
if - условный оператор для создания ветвлений в программе. Формат:
if (<условие>)
<действие, если <условие> истинно>
else
<действие, если <условие> ложно>
либо
если (<условие>)
<действие, если <условие> истинно>
иначе
<действие, если <условие> ложно>
часть else можно не задавать.
(полное_описание && уже_осмотрено==нет)
полное_описание - строковый тип, но при переводе его к типу bool, равен true, если строка не пустая.
&& - логическая операция И (логическое умножение). Имеет ещё синонимы: &, &&, and, и.
== - оператор сравнения. Возвращает true, если "уже_осмотрено" равно false(нет).
полностью выражение означает, что если полное описание задано и объект ещё не осмотрен, то вывести на экран полное_описание и установить свойство уже_осмотрено в true, иначе вывести st_описание на экран.
можно_войти - тут всё ясно.
this.здесь = "здесь; НдСи; Ип; Ип=; Рп=; Дп=; Вп=; Тп=; Пп=;" - здесь мы наблюдаем тип lexeme. Лексема - это строка специального формата, описывающая способы формирования необходимых словоформ. Лексемы широко используются при синтезе выводимого текста и в анализе введенной команды. Лексема состоит из 4х частей: основа лексемы (здесь), ключ постоянных свойств лексемы (НдСи), ключ словоформы по умолчанию (Ип), модификаторы лексемы (Ип=; Рп=; Дп=; Вп=; Тп=; Пп=). Более подробно рассмотрим лексему чуть ниже на более сложном примере.
class предмет { cls = нечто_видимое title = "<font color=red>{name}</font>" //программное имя выводится если title не переопределено //описание переопределяется автором this.описание = "это {обычный*this~рчд} {this}" //st_описание обычно не переопределяется автором this.st_описание = "{описание} {fn_состояние()}" this.показывать_в_инвентаре = "{this} {fn_состояние()}" this.снаружи_персонажа = нет fn_состояние() { if(item:состояние.num) return "({item:состояние*this.lex})" } this.предмет_по_месту = "здесь есть {this} {fn_состояние()}" this.для_чего = нет //ложечка для сахара this.из_чего = нет //моток веревки this.чей = нет //кирка рудокопа this.можно_взять = нет //по умолчанию предметы не берутся! (фиксированы) this.это = "эт%; НдСи; ЕчИп; ИпЕч=о; РпЕч=ого; ДпЕч=ому; ВпЕч=о; ТпЕч=им; ПпЕч=ом; ИпМч=и; РпМч=их; ДпМч=им; ВпМч=и; ТпМч=ими; ПпМч=их;" this.лич_мест = "%; НдСиЛм; ИпМрЕч; МрЕчИп=он; МрЕчРп=его; МрЕчДп=ему; МрЕчВп=его; МрЕчТп=им; МрЕчПп=нём; СрЕчИп=оно; СрЕчРп=его; СрЕчДп=ему; СрЕчВп=его; СрЕчТп=им; СрЕчПп=нём; ЖрЕчИп=она; ЖрЕчРп=её; ЖрЕчДп=ей; ЖрЕчВп=её; ЖрЕчТп=ей; ЖрЕчТп=ею; ЖрЕчПп=ней; МрЕчРпПу=него; МрЕчДпПу=нему; МрЕчВпПу=него; МрЕчТпПу=ним; СрЕчРпПу=него; СрЕчДпПу=нему; СрЕчВпПу=него; СрЕчТпПу=ним; ЖрЕчРпПу=неё; ЖрЕчДпПу=ней; ЖрЕчВпПу=неё; ЖрЕчТпПу=ней; ЖрЕчТпПу=нею; МчИп=они; МчРп=их; МчДп=им; МчВп=их; МчТп=ими; МчПп=них; МчРпПу=них; МчДпПу=ним; МчВпПу=них; МчТпПу=ними;" //Пу - особое уточнение: если перед местоимением стоит предлог, добавляется буква 'н'
Начнём с лексем:
эт%; - основа лексемы. Она может содержать спец.символы % и #. При формировании словоформы символы % замещаются подобранным модификатором, а символы # замещаются числом - количеством объектов.
НдСи; - ключ постоянных свойств. Содержит неизменяемые свойства лексемы. Имеет тип key (морфологический ключ).
Состоит из набора свойств и их значений. Значения и свойства задаются одним символом и чередуются. Здесь свойство "д" имеет значение "Н", а свойство "и" - значение "С". Свойства и их значения не заданы жёстко, а создаются автором игры. В данном примере автор стандартной библиотеки придумал свойства:
д - о[д]ушевлённость ([Н]е одушевлённое, [О]душевлённое)
и - часть речи/[и]мя ([С]уществительное, [П]рилагательное, [Г]лагол и т.д.)
п - [п]адеж ([И]менительный, [Р]одительный, [Д]ательный, [В]инительный, [Т]ворительный, [П]редложный)
есть ещё 2 свойства, заданных в платформе жёстко и требуемых для рассчёта количественных форм:
ч - [ч]исло ([Е]динственное, [М]ножественное)
к - [к]оличество ([1] для чисел заканчивающихся на 1; [2] для чисел заканчивающихся на 2, 3 и 4; [5] для чисел 11, 12, 13, 14 или заканчивающихся на 5, 6, 7, 8, 9, 0)
Эти 2 свойства были заданы жёстко в старых версиях ТОМа. Новые версии сильно изменились в плане обработки количественных значений. На сколько жёстко заданы эти свойства в версии 0.9.4.0 известно лишь автору платформы.
ЕчИп; - ключ свойств по умолчанию. Если какое-то свойство не задано при формировании словоформы, оно берётся из этого ключа.
ИпЕч=о; РпЕч=ого; ВпЕч=о; ТпЕч=им; ПпЕч=ом;
ИпМч=и; РпМч=их; ДпМч=им; ВпМч=и; ТпМч=ими; ПпМч=их; - модификаторы лексемы. Нужный модификатор подставляются к основе при формировании словоформы. Например, если требуется сгенерировать словоформу в дательном падеже, парсер берёт ключ дательного падежа Дп и подставляет к нему недостающие свойства из свойств по умолчанию (в нашем случае свойство числа: Еч). Затем ищет наиболее подходящий модификатор к ключу ДпЕч - ДпЕч=ому; и подставляет в основу вместо % окончание "ому", получая "этому".
Символов % в основе может быть несколько. В этом случае буквосочетания в модификаторе перечисляются через запятую и заменяются в порядке следования. Например "больш% кошк%; СиЖр; ИпЕч; ИпЕч=ая,а; РпЕч=ой,и; ...".
Кроме "=" в модификаторах можно использовать оператор "<", после которого нужно указать словоформу полностью. Например "кла%; ...; ПвНз=л; НвНз=дёт; ПвЗз<положил; ..."
Вот и вся структура лексемы. Согласитесь, не так уж и сложно
В методе fn_состояние() мы видим специальное поле num, возвращающее кол-во объектов. При переводе числа в тип bool оно равняется true, если число не равно нулю. В данном случае if(item:состояние.num) выполняется тогда, когда в списке item есть хотя-бы один объект, имеющий свойство состояние.
{item:состояние*this.lex} - оператор * служит для согласования формы объекта с лингвистическим ключом или с другим объектом. Специальное свойство lex возвращает лексему объекта. Рассмотрим процесс на примере:
Допустим, текущий объект (this) имеет свойство title = "ложк%; СиЖр; ИпЕч; ИпЕч=а; ...". Свойство this.lex возвращает title этого объекта. Оператор * берёт ключ постоянных свойств этой лексемы (СиЖр) и согласует каждую лексему списка item:состояние с этим ключом. Т.е. будут выведены все состояния в женском роде.
Получается, что метод fn_состояние() возвращает список состояний текущего объекта в скобочках.
"это {обычный*this~рчд} {this}" - оператор ~ выделяет часть лингвистического ключа из объекта. В данном случае из this выделяются свойства [р]од, [ч]исло и о[д]ушевлённость и применяются к объекту обычный.
// // метод CalcKey() вызывается парсером или генератором текста для расчета переменной части // морфологического ключа формы объекта // CalcKey(Key) { if(typ=="ordinal") return Key //порядковые числительные не изменяют лексические свойства if(ctg!="counting") return Key //количество влияет на Key только у счетных предметов //расчет ключа числа 'ч' switch(num) case(1) Key = Key+"Еч" //единственное число case(-1) Key = Key+"Еч" //единственное число case(2147483632) Key = "МчБк"+Key //константа "все" case() if(num>2147483633) Key = "МчРпБк"+Key //константы "несколько","половина","треть","четверть" else Key = Key+"Мч" //множественное число //расчет ключа количества 'к' для согласования с числительным if(num>=10 and num<=20 or num<=-10 and num>=-20) Key = Key+"5к" //исключение для 10..20 else { var X = number(right(num,1))//берём последний знак числа if(X==1) Key = Key+"1к" //1 else if(X>=2 and X<=4) Key = Key+"2к" //2,3,4 else Key = Key+"5к" //5,6,7,8,9,0 } return Key //возвращаем получившийся ключ }
CalcKey(Key) - яркий пример недокументированного предопределённого метода. Заметьте, он отсутствует в том списке из 7-ми методов, которые я давал выше. Хорошо, что в комментариях всё довольно понятно описано. Век живи, век учись
typ возвращает строковое значение типа объекта. ordinal, по видимому, некий новый тип, который отсутствует в описании на вики.
ctg возвращает строковое значение категории объекта.
Перечислим операторы сравнения значений:
== - равно
!= или <> - не равно
> - больше
< - меньше
>= - больше либо равно
<= - меньше либо равно
И логические операции:
!, not, не - логическое отрицание
|, ||, or, или - логическое сложение
&, &&, and, и - логическое умножение
оператор выбора switch похож на if, только имеет не два, а множество ветвлений:
switch(<значение>)
case(<случай_1>) <выполняется, если <значение>==<случай_1> >
case(<случай_2>) <выполняется, если <значение>==<случай_2> >
case(<случай_3>) <выполняется, если <значение>==<случай_3> >
case(<случай_4>) <выполняется, если <значение>==<случай_4> >
case() <выполняется, если все другие случаи не подошли>
switch имеет русский эквивалент "выбор", а case - "случай"
number(X) преобразует значение X в числовой тип.
right(X, Y) берёт с конца строки X кол-во символов Y.
Конструкция number(right(234,1)) преобразует число 234 в строку ("234"), берёт с конца 1 символ ("4") и преобразует обратно в число (4).
Итак, функция CalcKey есть в каждом объекте, унаследованном от класса предмет и изменяет ключ объекта, если он имеет категорию счётное и тип не ordinal. Возвращает модифицированный под кол-во объектов ключ.
// // метод ChkClsName() вызывается парсером при решении неоднозначности // по классовым именам объектов // ChkClsName(Round,iRound,iForm,oRound,oForm) { // Round - номер текущего хода // iRound - ход, в котором объект упоминался игроком (input) // iForm - форма, в которой объект упоминался игроком (input) // oRound - ход, в котором объект упоминался игрой (output) // oForm - форма, в которой объект упоминался игрой (output) if(str=="лич_мест") { //личные местоимения var род_число = key~"рч" if(род_число=="Мч") //множественное число не имеет рода { iForm = iForm~"ч" oForm = oForm~"ч" } else { iForm = iForm~"рч" oForm = oForm~"рч" } var Ok = false //предмет должен быть упомянут не далее чем за 10|3 хода switch(iRound and iRound+10>Round, oRound and oRound+3>Round) case(false,false)//не упоминался return "ни один предмет не подошёл к местоимению." case(true,false) //упоминался игроком Ok = род_число==iForm case(false,true) //упоминался игрой Ok = род_число==oForm case(true,true) //упоминался игрой и игроком Ok = род_число==iForm or род_число==oForm //должны совпадать по роду и числу с последним упоминанием предмета if(!Ok) return "местоимение не совпадает с ранее упомянутыми объектами." } if(iRound+10>oRound) //упоминание игроком +10 ходов к приоритету return iRound+11; else return oRound+1; return 1; }
ChkClsName(Round,iRound,iForm,oRound,oForm) - ещё один недокументированный предопределённый метод.
ASBer, сколько же ещё тайн хранит платформа ТОМ?
str/строка - специальное поле значения. Для строки возвращает ту же строку. Для объекта, полученного из парсера, возвращает слово, которым объект назван в команде. Для объекта, полученного функцией set(), возвращает строку, использованную в функции. Для прочих типов возвращает пустую строку. Что оно возвращает в примере if(str=="лич_мест") не совсем понятно.
var/переменная - определяет локальную переменную. Локальная переменная существует до конца выполнения метода.
Метод ChkClsName(Round,iRound,iForm,oRound,oForm) возвращает строку с описанием ошибки, если упоминание объекта не найдено и число, если он недавно упоминался и подходит по форме. Число указывает на то, насколько этот объект подходит к найденному упоминавшемуся. Объект с наибольшим приоритетом будет выбран как наиболее подходящий. В общем, метод служит для обработки таких случаев:
> возьми ключ
> вставь его в отверстие
ты вставил ключ в отверстие
или упоминания со стороны игры:
ты видишь здесь письменную ручку.
> возьми её
ты взял ручку.
// // по умолчанию предметы не могут содержать другие предметы // проверки вызываются перед попыткой положить/взять что-либо на/в/c/из предмета // можно_поместить(Предлог,Что) { return "{actor} не {может*actor} {act.инфинитив} {Что.lex*Вп} {Предлог} {lex*Вп}." } можно_изъять(Предлог,Что) { return "{actor} не {может*actor} {act.инфинитив} {Что.lex*Вп} {Предлог} {lex*Вп}." } }
Два пользовательских метода, вызываемых при попытке положить или взять что либо во вложенные элементы (item) объекта. Если надо, изменяются разработчиком игры для каждого объекта. Если возвращают строку, значит поместить/изъять нельзя. Их вызов и обработку мы ещё встретим ниже.
Продолжение следует...
Ожидалось в одном уроке разобрать весь модуль Main, но после описания всего парочки классов оказалось, что урок уже очень огромен и трудно перевариваем. Решил не дописывать до конца, а узнать мнение, стоит ли вообще писать в таком стиле, или мне вежливо ретироваться в сторонку от написания уроков? Со стороны должно быть виднее.
Отредактировано Alexandr (2010-09-02 20:37:14)