本書はTADS製作者用手引きの一部です。
Copyright (C) 1987 - 2000 by
Michael J. Roberts. All rights reserved.
第4章
本章はTADSに取り組み始めるために知っておく必要があるコマンド解析に関する事柄:オブジェクトを作り、それにプレーヤーがコマンドの中で使用する名前を与える方法、オブジェクトのコマンドに対する反応の仕方を調整する方法、独自の新しいコマンドを作る方法等の概要です。TADSの素晴しい特長の一つは、ゲームをすぐに書き始められ、進みながら自分の知識を詳しく書き込めることです。本章はあなたの出発を支援します―あなたに必要な最小限の情報から始めて、より高度な特長へ移行します。
私が2年以上前にTADSのバージョン2.2を発表したとき、私はパーサーのプログラム上のインターフェースが肥大化し、TADS Authors Manualのパーサーを解説している章が、それはTADSバージョン2.0用に最後の改訂を受けていたものですが、絶望的に古くさく書き直しの必要に迫られていると感じました。そのために、私はその章のまったく新しい更新したバージョンを書き上げ、New Book of the Parserと名付けてバージョン2.2リリースノートの一部としました。
TADSパーサーはその後じっとしていたことはありませんでしたが、TADS2.4については、パーサーの文書を現在の真実を反映すべく整備し、ゲーム製作者にパーサー全体を網羅する情報源を一つ、望みとして体系化された形で提供する時が再び訪れていました。
TADSの2.4より前のバージョンを使ったことがない新しいゲーム製作者の方は、2.4のパーサーの完全な情報に関してこのTADS Parser Manual以外を参照する必要はありません。特に2.0の製作者用手引きの第4章、同様に2.2のNew Bookは無視してください。経験者であるTADSゲーム製作者の方は、TADSのあなたにとっての前バージョンからの変更点のリストを求めてリリース・ノート(TADSVER.HTM)を参照したいかもしれません。しかし、この手引きは新旧を問わずパーサーの全機能についての完全なレファレンスを提供するものです。
パーサーマニュアルの今回の更新は、オリジナルのTADS製作者用手引きの「Writing Adventures」という章に取って代わったTADS 2.2からのNew Book of the Parserを基にしています。もしあなたが以前の編集を読んだことがあるなら、元の資料の大半が今回の更新に持ち越されていることに気づくと思います。とはいえ、その多くは改訂と拡張を受けています。さらに、このバージョンは内容をもっと親しみやすく有用なものにするために、2.2版に比べて大幅に再構成されています。
TADSパーサーの完全な説明を現在構成する資料の分量の事情により、また、より適当な編集を追及するために、パーサー章はパーサーマニュアルになりました。この導入部の後の最初の章はパーサーがゲームといかにやり取りをするのか、その見取図を提供します。それに続く章はパーサーの使い方、プログラムの仕方に関する増加しつつある詳細を解説します。最終章はパーサーのプログラム上のインターフェースの概要を提供します。
この導入部にあたる章の始まりにおいてあなたはパーサーマニュアル全体の包括的な目次を目にします。
あなたはコマンド解析の基礎原理に関する章を通じて、私たちが気楽に「原理」と称しているものを知るでしょう。そして、その原理はパーサーに関する格別に重要かつ繊細な事実を誘導します。ほとんどの場合、それらの原理は初めてTADSを学ぶゲーム製作者をしばしば混乱させる特長を指します。あなたがこの手引きに初めて目を通しているのなら、それらの点に特別な注意を払ってください。原理は決して完全な要約ではありませんが、最も微妙な話題を強調するようきちんと意図しています。
経験のあるゲーム製作者にとってはコマンド解析と実行手順の要約のセクションがパーサーの操作に関する有用なクイックレファレンスになるでしょう。
私は新たな特長と改良についての提案を寄せてくれたすべての人に感謝したいと思います。TADS要望リスト上にあった事項のいくつかがTADS2.4で達成されているのをあなたが発見するよう祈ります。続けられているパーサーの改良はほとんど、TADSへの取り組みにおいて真剣に新しい事柄を考え出してくれるゲーム製作者の大胆な想像によるものです。
Stephen Granade、TenthStone、Pieter Spronck、Neil K. GuyそしてKevin Forchioneには彼らの助言と多くの優れたアイデアを理由として特に感謝したいと思います。NeilにはTADS製作者用手引き及び今回のアップデートの基礎として供給された当初のNew Book of the ParserのHTML変換により、TADS文書を現代的にしてくれた点、それから、Kevinにはこの新しいバージョンの編集におけるかなりの支援の点でも感謝しています。
バージョン2.5と2.5.1でのパーサーの変更点は現在この手引きに反映されています。その結果として、私はターンの始まりとターンの終わりの処理についての説明を少しまとめ直しました。それ以外はバージョン2.4とほとんど変わっていません。
もしもあなたがテキストアドベンチャーゲームを作りたいと思ったとき、TADSのようなシステムを持っていないとしたら、プレーヤーコマンドパーサーをデザインするという威圧的な眺望に直面することになるでしょう。その作業はかなり大がかりなものになり、ゲームの他のどの部分よりもパーサーのデザインと実装に費やす時間が長くなるでしょう。
幸運にも、これを読む時間を取っているのなら、少なくともTADSでゲームを組み立てることを考慮に入れていることになります。つまり、自分でパーサーを書かなくてもいいということです。既存のパーサーを使えば膨大な労力を省けますが、あなたの作業を全面的には除去してくれません。自前のパーサーを書く必要はありませんが、少なくとも自分のゲームを書くために使用しているパーサーに関して少々学習しなくてはいけません。
TADSにおいて、あなたに必要な学習量はあなたの望むことに依存します。パーサーについて何も学ばなくてもTADSで非常に多くのことができます。既存のクラス・ライブラリ(TADSと共に供給される標準ライブラリのadv.tなど)を利用することにより、パーサーとのやり取りがすでに定義されているオブジェクトを作成できるので、あなたがしなければいけないのは、オブジェクトに関する簡単な情報、その名前や説明などを記入することだけです。ですが、TADSゲームを記述する基本がお手のものになれば、既存の動詞に対して独自の応答をする自家製のオブジェクトクラスを作りたくなるはずです。これを行うには、新しい自家製オブジェクトがある特定のコマンドに応答するためのコードをそれらのオブジェクトに追加できるよう、パーサーのコマンド処理について少々学ぶ必要があるでしょう。きっと独自の動詞も作れるようになりたいと思われるでしょうが、その場合、独自のコマンドを定義する方法を学ぶことが求められます。これらすべてを行った後に、もっと複雑な変更を行いたくなるかもしれません。そうなるとパーサーのもっと秘密めいた側面について学ばなければいけません。
私たちが「パーサー」について語るとき、プレーヤーからのコマンドを読み、コマンドを解釈し、特定のゲーム行動を実行するプログラムコードの集合について語っていることになります。コードの集合は二つの部分に分かれます。一つは、TADSインタプリタに組み込まれている部分で、もう一つは、ゲームのTADSコードの中で定義されている部分です。さらに言うと、TADSコード中に定義されている部分は通常さらに分けられます。つまり、このコードの一部は普通、adv.tのような標準ライブラリからやって来るもので、別の部分はそのゲームに特有なものです。
備え付けのパーサーはコマンド解析における「文法解析」部分、例えば、一つのコマンドを複数の独立した言葉(つまり「トークン」)へ分割すること、複数のコマンドを含むコマンド行の文章の区切りを判断すること、動詞と前置詞を特定する語を理解すること、そして名詞句を特定することなどの大半を扱います。備え付けの部分も、コマンドに属する行動を実行するためにコマンドに含まれるオブジェクト内の一連のメソッドを呼ぶことによって実行過程の定義としています。
TADSインタプリタに組み込まれているパーサー部分は明らかに、TADSゲームプログラムによって直接変更することができません。しかし、組み込みパーサーにはカスタマイズするための「ホック」が多くあり、それらはゲームが独自の振る舞いを定義できる、解析プロセス上の特別な場所になっています。カスタマイゼーション・ホックの数は増加し、それにしたがって強力になりました。TADSの最近のバージョンでは、上書きできないパーサー部分はごくわずかです。
パーサー・カスタマイゼーション・ホックはいくつかの形式を取ります。一部のホックは「all」や「except」のような特別な語の代わりにパーサーが使用する文字列を定義することを可能にします。ほとんどのホックは解析過程のあるステージでパーサーが呼び出す関数またはオブジェクトメソッドを定義するためにあり、そのステージの働き方を改変するために使うことができます。ほとんどの場合、カスタマイゼーション・ホックによって組み込みパーサーの様子を完全に変えてしまえます。
組み込みパーサーは独自の動詞、前置詞、または目的語をなんら用意していません。なぜなら、これはゲームプログラムの仕事だからです。たいていのゲームは普通、基本的なコマンドとオブジェクトクラスの巨大なセットを持っていますので、TADSはその標準ライブラリ(adv.tファイル内で定義されている)内に多くの動詞及び前置詞を含んでいます。
ゲームプログラムはライブラリ内のすべてを変更することができます。ですから、あなたはそのライブラリを完全に捨て去って自前のものを作る、または、他の誰かがデザインしたライブラリ(例えばWorldClass)を利用することができるのです。
ほとんどのゲームは若干でも独自の動詞を定義することになります。なぜなら、定義済みライブラリは一般的なものとしてデザインされていてありうるすべての動詞を含んではいないからです。それに加えて、一部のゲームはそのゲーム特有の特別な効果を達成しようと、組み込みパーサーの既定の振る舞いを変更するために一つ以上のカスタマイゼーション・ホックを使用します。
ゲームを書くためにあなたがパーサーについて最低限学ばなければいけないことは、なにもありません! それは、すでに定義されているオブジェクトだけを使ってゲームを書けるからです。例えば、共通アドベンチャーオブジェクトのライブラリでTADSに付随するadv.t内で定義されているオブジェクトを利用することができます。開閉できる(ただし初めは開いている)箱を作るには、次のコードを書くだけです:
box: openable sdesc = "box" noun = 'box' location = startroom isopen = true ;openableクラス(adv.t内で定義されている)はすでに、開け閉めができる容器にとって適切なコマンドに反応する準備が完了しています―プレーヤーからのそうしたコマンドのすべてに対する反応の仕方を自動的に知っているのです:
>open the box >look in the box >put everything in the box >take the ball out of the box >put the book in the box, and close the boxadv.tで定義されているオブジェクトを利用するのなら、パーサーの仕事に関してなにも知らなくてもよいとすでに述べましたが、実際は、少しだけ知っておく必要があります。というのは、オブジェクトに名前をつける方法を知る必要があるのです。
上のオブジェクトの中では、nounというプロパティーを定義し、それを'box'と設定しました。nounプロパティーは特別なもので―ボキャブラリープロパティーの一つです。ボキャブラリープロパティーはプレーヤーがコマンド中に使用するオブジェクトに名前を割り当てます。ボキャブラリープロパティーにはnoun、 adjective、verb、preposition、article、verbがあります。ほとんどの部分で、noun及びadjectiveプロパティーを定義することになりますが、他のものはコマンドを独自に定義するようになるまでは使用する必要がありません。ボキャブラリープロパティーに関して留意すべき重要な特徴:ボキャブラリープロパティーの値は必ずシングル引用符で挟んだ文字列であること。
原理Theorem 1: ボキャブラリー文字列は必ずシングル引用符でくくること。 ボキャブラリープロパティーは普通のプロパティーと違います。その唯一の機能はプレーヤーがオブジェクトを差し示すために使用する言葉を定義することです。普通のプロパティーと違い、ボキャブラリープロパティーはゲームプログラムの中から利用されることはありません。例えば、次のコードは何も表示しません:
say(box.noun);box.nounプロパティーは実行時になんの値も持ちません。実行時においてあるオブジェクトのボキャブラリーワードを取得するためにボキャブラリープロパティーを利用することはできないのです。ただし、組み込み関数のgetwords()を使うことによって、これらのプロパティーを得ることが可能です。
あるオブジェクトにボキャブラリープロパティーを割り当てるとき、あなたはプレーヤーがコマンド内で使用するある言葉を考えだし、その言葉に「品詞」を割り当てます。ある語の品詞は、日常的な発言と同様に、その語があるセンテンスの中でいかに使用されるのかを決定します。ある語をnounプロパティーに定義した場合、名詞が文法的に有効であるときにはいつでも、その語をプレーヤーのコマンド内で使用できることを明言したことになります。同時に、その語がこの特定のオブジェクトを参照することも明言します。
同じ言葉を複数のオブジェクトに割り当てることもできます。例えば、ゲーム中にいくつかのオブジェクトがあり、それらすべてが「box」という名詞を持っているようにできます。さらに、同じ言葉を複数の品詞として使用することもできます。例えば、ゲーム内に「ペーパー・タオル」というオブジェクトと「ターム・ペーパー(学期末のレポート)」というオブジェクトがあるなら、「ペーパー」という語はこの場合、名詞と形容詞のどちらにも使われるのです。
一つのオブジェクトについて同じ品詞を複数定義することもできます。多くの場合、一つのオブジェクト用の語として複数の同意語を用意したくなります。これは簡単に行えます─単に各品詞のボキャブラリープロパティー中にすべての同意語を列挙するだけです。例えば、ゲームの中にブックレットがある場合、プレーヤーがそのオブジェクトを指し示すために使ういくつかの似通った言葉を用意したいはずです。
booklet: item location = startroom sdesc = "booklet" ldesc = "It's a small booklet labeled \"TADS Release Notes\". " noun = 'book' 'booklet' 'notes' adjective = 'tads' 'release' 'small' ;各行の各語がシングル引用符で挟まれていることに注目してください。この定義によって、ブックレットを"release notes,"または"small tads booklet,"、あるいは他のこれらの形容詞と名詞の組み合わせで示すことができるでしょう。
Adv.tはアドベンチャーゲームに共通している多くの種類のオブジェクトを定義しています、ですから、おそらくゲームのかなりの部分をこうしたオブジェクトだけを使って書けるでしょう。しかし、adv.tのオブジェクトを含めただけのゲームはとても退屈なものなので、あなたはすぐにコマンドに対して新しい反応をする独自のオブジェクトを作りたくなるでしょう。これを行うには、動詞がどういうふうにオブジェクトに適用されるのかを理解する必要があります。
パーサーが理解する各コマンドは、そのコマンドを構成するオブジェクト内にパーサーが呼び出すメソッドのセットを持っています(メソッドとは単に特定のオブジェクトと結びついた関数またはサブルーチン)。プレーヤーが動詞と名詞を含むコマンドをタイプすると、パーサーはその動詞に基づいてどのメソッドを呼び出すべきか理解し、続いて、名詞によって特定されたオブジェクト内のメソッドを呼び出します。例えば、動詞の"open"は、開けられているオブジェクト内にパーサーが呼び出す二つのメソッド:verDoOpenというベリフィケーション(証明)メソッドと、doOpenというアクションメソッドを持ちます。(これらの名称中の"do"は"direct object"を意味するもので、動詞の"do."ではない) ベリフィケーションメソッドはそのオブジェクトがその動詞と整合するかどうかを見極めるために使用されます─"open,"について言えば、あるオブジェクトが最初の場所で開閉でき、しかもそれがまだ開けられていない場合に整合します。ベリフィケーションメソッドはそのオブジェクトがプレーヤーにとってアクセス可能であるかを知るために検査する必要はありません。なぜなら、この検査は別々に行われるからです。ですから、このメソッドはオブジェクトがその動詞にとって意味を成すのかどうかだけ検査すればいいのです。アクションメソッドは動詞を実行しますが、そのオブジェクトがコマンドに対して意味を持つかどうかを検査する必要はありません。ベリフィケーションメソッドがこれをすでに行っているからです。
開いたときに何か変わったことをするオブジェクトを作ってみたいとしましょう。例えば、開けられると飛び出すばね仕掛けのヘビが入った缶を持ちたいとします。そのためには、缶の"open"コマンドの取り扱いを調製しなければなりません。
もしも、私たちが箱について行ったようにその缶をopenableにするのなら、缶の開け閉めの自動的取り扱いを得られるでしょう。しかし、私たちは付加的振る舞いを得たいのです─オブジェクトが開けられたときに、しかもそれにヘビが入っている場合、そのヘビを飛び出させたいわけです。これを行うために、私たちはopenableによって定義されているdoOpenメソッドをオーバーライドします:
snakeCan: openable sdesc = "can" noun = 'can' doOpen(actor) = { inherited.doOpen(actor); if (snake.isIn(self)) { " As you open the can, a spring-loaded fake snake leaps out! You're so surprised that you drop the can. "; self.moveInto(actor.location); snake.moveInto(actor.location); } } ;新しいアクションメソッドが行う最初の事柄は、adv.t内でopenableによって定義された規定のdoOpenを継承することです。メソッドをオーバーライドする際に、元のメソッドをそのクラスから取り込むことができるのはしばしば役立ちますが、inheritedというキーワードがこれを可能にします。継承された標準設定のdoOpenは通常通りに缶を開けます。それが行われた後で、ヘビが缶の中にいるのかどうかを確認し、いた場合には特別な行動を取ります。つまり、プレーヤーにヘビのことを説明し、缶とヘビ双方をプレーヤーの居場所に移動させるのです。
この実例の中には注意すべき事柄がもう少しあります。一つはactorというパラメータです。ベリフィケーションまたはアクションメソッドが呼ばれたときには常に、システムはそのコマンドの対象となるところのアクターを引数として含有しています。プレーヤーがアクターを特定せずにコマンドをタイプした場合、parserGetMe()によって得られる現行のプレーヤーキャラクタが自動的に用いられます。プレーヤーの現在の位置はparserGetMe().locationによっていつでも得られます(これはプレーヤーが直に接しているコンテナになるので注意してほしい。部屋ではなく部屋にあるベッドのように入れ子状の物でありうる)。また、プレーヤーの持ち物はparserGetMe().contentsで知ることができます。ですが、パラメータとして渡されるアクターオブジェクトの立場からdoOpenメソッドを記述することにより、プレーヤーがゲーム中の他の登場人物にコマンドを投げかけ、そのコマンドを適切に働かせることを可能にすることもできます。ですから、私たちはヘビと缶をparserGetMe().locationへ移動させるのではなく、actor.locationへそれらを移したのです(プレーヤーが彼または彼女自身で缶を開けるのなら両者は同じことになる)。
コマンドを他のキャラクターに対しても適応できるものにするには、メッセージ内のテキストの一部を特別な「フォーマット・ストリングス」を用いて置き換えます:
"As %you% open%s% the can, a spring-loaded fake snake leaps out! %You're% so surprised that %you% drop%s% the can. ";「%you%」のようなパーセント記号でくくられた特別な文字列は現行のアクターに基づいてシステムにより自動的に適切な語へ変換されます。コマンドがプレーヤーアクターによって実行される場合、表示されるメッセージは元になるものと同じになります。ただし、コマンドがゲーム内の別のキャラクターへ向けられる場合には、言葉がそのキャラクターにとって適当なものにすべて置き換えられます。ほとんどの場合、非プレーヤーキャラクターは特定の選択されたコマンドを実行できるだけですから、あるキャラクターに向けられうるコマンドを書いているときにこの点に留意するだけでいいと思います。adv.t内のすべてのコマンド・ハンドラがこのように記述されていますが、非プレーヤーキャラクターによって実行されうると判っているものを気にかけるだけでかまいません。
アクションメソッド(doOpen)だけを変えればいいのであって、ベリフィケーションメソッド(verDoOpen)に対しては何もしない、なぜならcontainerから継承したものが私たちの望むことをすでに行っているから、ということに注意してください。
ここで、他の動詞、といっても今回はオブジェクトがそのクラスから継承しないもの、を取り扱いたいということにしましょう。缶をきれいにした後で初めて缶のラベルを読める場合を想像してみてください。このために私たちは新しい動詞「read」と「clean」が必要になります。どちらもadv.t内で定義されています。「open」と同様、これらの動詞はそれぞれベリフィケーションおよびアクションメソッドを持っています。缶に次のようなコードを加えましょう:
isClean = nil verDoClean(actor) = { if (self.isClean) "You can't make it much cleaner. "; } doClean(actor) = { "You wipe off the label. It's much more readable now. "; self.isClean := true; } verDoRead(actor) = { if (not self.isClean) "It's far too dirty. "; } doRead(actor) = { "It's labeled \"Snake in a Can.\" "; }ベリフィケーションメソッドはいささか変わっています。というのは、なにかしているように見えないからです─そのコマンドが意味をなすのかどうか検査し、もしなさないならメッセージを表示する、これが役目のすべてです。実際に、これこそがベリフィケーションメソッドが行うべきことのすべてなのです。ベリフィケーションメソッドについて最重要の記憶すべき事柄は、それがゲームの状態をどんなかたちであれけっして改変しないということです。例えば、プロパティーの値を設定したりするようなことはけっしてありません。アクションメソッドだけがゲーム状態を変更します。
Theorem 2: いかなるゲーム状態もベリフィケーションメソッドで変更してはならない。 The reason that verification methods shouldn't change any game state is that these methods are sometimes called "silently" by the parser; that is, the parser turns off the output, and calls a verification method. The player will never know that the method has been called at all, because any text it displayed was suppressed - this is what we mean when we say that the method is called "silently." The parser makes these invisible calls to verification methods when it's trying to figure out which of several possible objects the player means. For example, suppose the player is in a room with two doors, one red and one blue, and the red door is open and the blue door is closed; if the player types "open door," the parser calls the verification method on each door to determine which one makes the most sense. The red door's verification method will display a message saying that you can't open it because it's already open; the blue door's method will say nothing. These calls are made with the text output turned off, because the parser isn't really trying to open anything at this point - it's merely trying to figure out which door makes more sense with the command. Once it's decided which one to open, the parser will run the verification method with output turned on, and then it will run the action method.
How does a verification method tell the parser that the command is not logical? Simple: it displays a message. So, the verification method's only function is to display an error. If the verb is logical for the object, the verification method doesn't do anything at all - since the verb makes sense, there's no error to display. If the verb does not make sense, the verification method displays an error message telling the player why the verb can't be used. The parser sees the message, and knows that it indicates that the verb can't be used, so it won't try to run the action method.
There's another important feature of the verification method: if an object doesn't have a verification method defined at all - which means that the object neither defines the verification method itself nor inherits the method from a superclass - the command automatically fails, and the parser generates the message "I don't know how to verb the object." So, if you want an object to be able to carry out a command, you must either define the verification method directly in the object, or make sure the object inherits the verification method from a class. It's always a good idea to add a verification method to the thing class for each new verb you define, even if your method only says "I don't know how to do that"; at the very least, this lets you customize the default response to the verb.
Once you start writing your game, you'll quickly want to start adding your own new commands. One of the features that makes TADS so powerful is that it has no built-in verbs - all of the verbs you've seen so far are defined in adv.t, not within TADS itself, so you can change, replace, or remove any of them, and, of course, add new verbs of your own.
We've already mentioned that there's a vocabulary property called verb; as you might guess, this is the vocabulary property that you use to add a new command. You may wonder what sort of object you attach the verb property to. The answer is that there's a special kind of object defined in adv.t, called deepverb, that you can use as a command object. This isn't an object that the player sees as part of your game - it's merely an abstract, internal object, whose purpose is to contain the data and methods that define a new command.
TADS allows you to create three basic types of commands: a command that consists simply of a verb, such as "look" or "jump"; a command that has a verb and a direct object, such as "take book" or "turn on the flashlight"; and a command that has a verb, a direct object, a preposition, and an indirect object, such as "put the ball in the box" or "clean the lens with the tissue." The same word can be used in more than one form of command; for example, you might define commands such as "lock door" and "lock door with key."
For the first type of sentence, which has only a verb, you define the entire command in the deepverb object. You specify the action that the verb carries out with a method called action that you define in the deepverb object. For example, to define a verb called "scream" that displays a message, you could do this:
screamVerb: deepverb verb = 'scream' sdesc = "scream" action(actor) = { "Aaaaaiiiiieeeee!!!\n"; "(You feel much better now.) "; } ;The presence of the action method in the object that defines the verb allows the command to be used without any objects. If you define a deepverb that doesn't have an action method, and the player types the verb without any objects, the parser asks the player to supply a direct object, because the parser can't execute the command without any objects when no action method is present.
For the second type of sentence, which has a verb and a direct object, you must set up a deepverb object - but, as you have already seen, the command is carried out by the direct object, not by the verb itself. The deepverb object serves to tell the parser about the verb and how it works - it contains the verb property, and also has the doAction property. The doAction property is a special property that tells the system that the command can be used with a direct object (the "do" in doAction stands for "direct object," just as it did with methods such as doOpen), and tells the system what the verification and action methods are called.
Here's how it works: you define the property doAction in the deepverb as a single-quoted string. The parser takes the string 'verDo' and puts it in front of your string - this becomes the name of the verification property. The parser then takes 'do' and puts it in front of your string - this becomes the name of the action property. For example, if you define a verb to scream at someone, and you specify doAction = 'Screamat', then the verification method for your new command is verDoScreamat, and the action method is doScreamat. Here's how your deepverb definition would look:
screamatVerb: deepverb sdesc = "scream at" verb = 'scream at' doAction = 'Screamat' ;If the player types "scream at the phone," the parser first calls the verDoScreamat method in the phone object, to verify that the command is sensible; if the verification method is defined, and succeeds (which means that it doesn't display any messages), the parser proceeds to call the doScreamat method, which should carry out the action of the command.
There are two more things to notice in this example.
The first is that the verb property we defined has two words. This is a special ability of the verb property - you can't do anything similar for noun, adjective, or any of the other vocabulary properties. If you define a two-word verb, you must separate the two words by a space, and you must define the second word as a preposition. (In this case, "at " is already defined as a preposition in adv.t, as are most English prepositions. If you add a two-word verb, be sure that the second word is defined as a preposition; if it's not so defined in adv.t, you must define it in your game's source code.) Two-word verbs are useful, because many English verbs take this form. When a verb has a direct object, TADS lets the player use the second word immediately after the verb, or at the end of the sentence. With "scream at," the latter doesn't make much sense, but for many verbs it does - for example, you might say "pick up the book," but it would be equally valid to say "pick the book up." TADS understands both.
The second thing to notice is the use of capitalization in the doAction string. The convention used by adv.t is to capitalize the first letter of a preposition, but only for verbs that take an indirect object. For example, adv.t defines an ioAction of GiveTo for the command "give object to actor"; the To is capitalized because "to" is the preposition that introduces an indirect object. For our verb here, we don't have an indirect object - the preposition is part of the verb, rather than a word that introduces an indirect object - so we do not capitalize the "at" in Screamat. You don't have to use this convention, since the string that you define in a doAction is entirely up to you, but it may help you to make sense of adv.t if you know the convention it uses.
The third type of sentence has both a direct object and an indirect object, and has a preposition that introduces the indirect object. For example, you might want to create a command such as "burn paper with torch." To define this command, you use the ioAction property of the deepverb. This property is similar to doAction, but it defines three new methods, and also associates a preposition with the command:
burnVerb: deepverb sdesc = "burn" verb = 'burn' ioAction(withPrep) = 'BurnWith' ;Notice that 'BurnWith' follows the capitalization convention described earlier. Since the preposition introduces an indirect object, and isn't merely a part of the verb, we capitalized its first letter.
The association of the preposition is done with the withPrep in the parentheses - withPrep is an object, defined in adv.t, which specifies a preposition property that defines the vocabulary word "with." The withPrep object is of class Prep, which is similar to deepverb: it's an abstract type of object that isn't visible to a player, and its only function is to define the vocabulary word and associate it with an object that can be referenced from within your game program.
This definition tells the parser that sentences that start with the verb "burn" must have a direct object, and an indirect object, and that the objects are separated with the preposition "with." When a sentence matching this pattern is entered by the player, the parser will use this verb definition to execute the command.
We mentioned that an ioAction defines three methods. Recall that doAction defines two methods from its root string: a verification method (starting with verDo), and an action method (starting with do). The ioAction property similarly defines verification and action methods, but since two objects are involved, it must generate not one but two verification methods: one for the direct object, and another for the indirect object. The methods are called verIoBurnWith and verDoBurnWith. As you might guess from the names, verIoBurnWith is sent to the indirect object, and verDoBurnWith goes to the direct object - and they're called in that order, with the indirect object going first. If both verification methods are defined and successful, then the parser calls the action method, ioBurnWith - this is called only for the indirect object. The parser never calls an action method in the direct object for a two-object command.
The verification and action methods for verbs with two objects are a little different than those for single-object verbs. Whereas a single object verb's verification and action methods only take the actor as a parameter, two of the two-verb methods get an additional parameter: the other object. The remaining method only gets the actor. The definitions should look like this:
verIoBurnWith(actor) = { ... } verDoBurnWith(actor, iobj) = { ...} ioBurnWith(actor, dobj) = { ... }This may seem strange, but there's a reason. Since the parser calls verIoBurnWith first, it doesn't always know what the direct object is when it calls verIoBurnWith. As a result, it can't provide it as a parameter. By the time it calls verDoBurnWith, though, the indirect object is known, so the extra parameter is provided. ioBurnWith is called later still, so the extra object is also provided as a parameter there.
Theorem 3: verIoXxxx does not have a dobj parameter. You may notice that there are some methods defined in adv.t that might make you think that the parser also calls doBurnWith. In fact, the parser will never call doBurnWith itself, but this is a perfectly valid method name which you can call from your own code. Sometimes, you will want the action of a two-object verb to be carried out by the indirect object, and other times you'll want to use the direct object. Which way is better depends entirely on the situation - in general, the better way is the one that requires you to write less code. If you find a situation where you want to handle a two-object command's action in the direct object rather than in the indirect object, simply write an indirect object action method that looks like this:
ioBurnWith(actor, dobj) = { dobj.doBurnWith(actor, self); }The parser will never call doBurnWith, but there's nothing to stop you from calling it.
Theorem 4: The parser never calls doXxxx for a two-object verb. If you find yourself in a situation where the standard order of verification for a two-object verb is wrong, TADS does let you change this default ordering. We're moving into advanced territory here, so you probably won't need to know this for quite a while, but you might at least want to note that it's possible. When you define an ioAction, you can include a special modifier that tells TADS to change the ordering:
ioAction = [disambigDobjFirst] 'BurnWith'The modifier is the strange-looking word in square brackets; it tells TADS that you want to reverse the normal ordering. When you include this modifier, the parser will process the verification and action methods in a different order than usual:
verDoBurnWith(actor) = { ...} verIoBurnWith(actor, dobj) = { ... } ioBurnWith(actor, dobj) = { ... }Note that the parameters changed along with the order. Since the direct object is processed first for this command, the direct object verification method is called before the indirect object is known, and hence no indirect object is passed to it; and since the indirect object verification method is called after the direct object is already known, the direct object is supplied as a parameter to this method.
As you add your own new commands, you will probably find that you want to augment the definition of one of the verbs defined in adv.t. For example, you may want to add a new command such as "open crate with crowbar." Since a verb for "open" is already defined in adv.t, you can't create another "open" verb in your own game. Fortunately, TADS lets you supplement an existing verb without having to change the original definition's source code.
To change an existing verb, you can use the TADS modify statement. This statement lets you add properties to an object that has already been defined elsewhere in your game (even in a separate file that you include, such as adv.t). For example, to modify the "open" verb defined in adv.t to add an "open with" command, you would do this:
modify openVerb ioAction(withPrep) = 'OpenWith' ;We strongly recommend that you use the modify mechanism whenever possible, rather than making source changes to adv.t. If you edit adv.t directly, you may find that you have a lot of work to do when you upgrade to a new version of TADS, because we will probably modify adv.t - if you also modify it, you will have to figure out how to apply your changes to the new version of adv.t If you use the modify mechanism instead, you should be able to use future versions of adv.t without any additional work.
Theorem 5: Don't edit adv.t directly - use modify instead.
The TADS parser and the standard library (adv.t) define several related concepts that control which objects the player can use in a command. The objects that are available to the player vary from turn to turn, since the player's location in the game and the state of objects in the game can change on each turn, and can also depend on which command the player is issuing, since different commands have different criteria.
An object is visible if the player can see the object from the current location (or, more precisely, from the current location of the player character object, as indicated by parserGetMe()).
The parser sees what the player sees. When the player enters a noun phrase, the parser relates the noun phrase to the objects that are visible to the player character object.
Visibility is controlled by the isVisible(vantage) method defined for each object. In adv.t, object.isVisible(vantage) is defined in the thing class to return true if object is in the same location as vantage, or object is in a container whose contents are visible (as indicated by the container's contentsVisible property) and the object.container.isVisible(vantage).
adv.t provides a helper function called visibleList(object, actor). This function returns a list of the objects contained within object that are visible to actor.
An object is reachable if the player can touch the object.
The method isReachable(actor) determines whether an object is reachable. This method is defined for each object. In adv.t, the thing class defines object.isReachable(actor) to return true if object is in actor's location's reachable list (reachable is a property that can be defined for each room as a list of objects that are explicitly reachable to any actor in that location), or if actor is in the same location as object, or if object's location's contents are reachable (as defined by the container's contentsReachable property) and object's location is reachable to the actor object.
Note that the parser never directly requires that an object be reachable. The parser merely requires that an object be valid for a verb in order to apply the verb to the object; the game may in turn require the object to be reachable in its definition of a validity method, but this requirement is created by the game and not by the parser.
The parser considers an object accessible or valid for a verb if the object passes the validDo (for a direct object) or validIo (for an indirect object) test for the verb. validDo and validIo are defined on the deepverb object for the verb.
An object may be valid for one verb, but not valid for a different verb, because each verb has its own requirements for validation. Most verbs fall into one of these categories:
At any given time, an object may be valid for one verb but not for another. For example, if an object is locked inside a glass case in the player's current location, the object would be visible but not reachable; hence, the player could examine the object, but couldn't take it, move it, or open it.
Note that the validDoList and validIoList methods are related to the validDo and validIo methods, but are less precise. The validDoList and validIoList methods exist to make the parser run faster: rather than slogging through the entire list of objects in the game, the parser concerns itself when resolving a noun phrase only with the objects returned by the appropriate validDoList or validIoList method. All of these objects are then further validated through validDo or validIo, so validDoList and validIoList can (and do) err on the side of returning too many objects: these methods often return objects that are not, in fact, valid for the verb. The reason these methods don't return precise lists is that doing so would require exact duplication of the logic in validDo and validIo; it's much easier and more efficient for these methods to make a rough guess that overestimates the set of valid objects, then let the parser sort things out using validDo and validIo as final checks.
The parser has one more level of object suitability: an object is logical for a command if it passes the appropriate verDoVerb or verIoVerb method. An object passes this test if the method doesn't display any text; if the method displays text, the parser assumes that the text is an error message, and considers the object to be illogical for the verb.
We usually refer to this logic test as verification.
If an object is not visible, the parser doesn't allow the player to refer to the object at all. The parser responds to any mention of the object with error 16, "I don't see any noun phrase here."
If an object is visible but isn't valid for a verb, the parser doesn't allow the player to use the verb on the object. However, because the object is visible, the parser can't say that it doesn't see the object here; instead, the parser uses the cantReach method defined by the game to explain the problem.
If an object is visible and valid, but fails verification, the parser simply relies on the message that the verification method displays to explain the problem.
As you build your game, you will probably add multiple objects that all go by the same name. When you give the same noun to two or more objects, you create the possibility that a player can type in a command with an ambiguous object name - that is, the words that the player uses refer to more than one object. For example, if you create several books in your game, you will probably give them all the noun "book"; a command such as "take book" could refer to any of these objects. Fortunately, the TADS parser has a powerful system that can resolve ambiguous object names; in many cases, the parser can disambiguate objects automatically, without needing any help from the player.
Version 2.4 adds a customization hook that lets you write your own disambiguation code, if you want to override the built-in processing. This is described in detail in the disambiguation discussion.
The first step in disambiguating an object name is to apply the visibility and access rules. These rules, which are defined by objects in your game program, determine whether an object is visible to the player, and if so whether it can be used with a particular verb. If an object is neither visible, nor accessible for use by the player with the given verb, the object isn't considered further. If it's visible but not accessible, it will be considered only to the extent that there isn't another object which is accessible.
Visibility is determined by the method isVisible, which is defined for each game object. The general-purpose class thing defined in adv.t provides a definition of this method suitable for most objects; you will probably not need to make any changes to this method unless you need some special effect. To determine if an object is visible to the player, the parser makes this call:
object.isVisible(parserGetMe())Accessibility is determined by the deepverb object. For a direct object, the parser first calls the method validDoList, which returns a list of all objects which are possibly valid for the verb; the objects in the list aren't necessarily valid, but any objects not in the list are not valid. If validDoList returns nil, it means that all objects are possibly valid and should be considered. A similar method, validIoList, returns a list of possibly-valid indirect objects.
After eliminating any objects that aren't in the validDoList (or validIoList, as appropriate), the parser submits each surviving object to the method validDo, also defined in the deepverb object. This method takes the object as a parameter, and returns true if the object is valid for the verb, nil if not. The parser keeps only objects that are valid.
If any objects remain, the parser turns off output (so that any text displayed is hidden from the player) and calls the appropriate validation method in each object. This is the "silent" validation call that we mentioned earlier. If only one of the objects passes the validation test (that is, the validation method doesn't attempt to display any messages), that object is chosen. Otherwise, the parser gives up and asks the player which of the objects to use.
The silent validation test lets TADS be very intelligent about disambiguation. For example, if there's an open door and a closed door in the room, and the player types "open door," the validation test lets the system determine that the player means to open the closed door - since the verDoOpen method fails for the open door (it displays "It's already open" if the default verification method from adv.t is used), but succeeds for the closed door, the parser knows to choose the closed door.
When the parser decides that it can't disambiguate a noun phrase without help from the user, it displays a question like this:
Which book do you mean, the blue book, or the red book?
The player can respond with something like "the red one" (or simply "red") to tell the parser which object to choose.
The object names in the question ("blue book" and "red book") are displayed by the sdesc properties of the possible matching objects. For this reason, it's important that you give each object a distinguishing sdesc property; if both of those books simply had an sdesc of "book", your player would see a message like this:
Which book do you mean, the book, or the book?
This obviously isn't good. Whenever you create more than one object with the same noun, be sure to give each object some kind of distinguishing feature, and describe it in the object's sdesc.
Theorem 6: Always give your objects unique sdescs. The disambiguation discussion has full details of how disambiguation works.
The parser also tries to be intelligent about supplying defaults when the player provides only partial information. The parser supplies default direct and indirect objects for commands that omit one of these objects, using rules defined by the objects in your game program.
To determine a verb's default direct object, the parser calls the method doDefault in the deepverb object. This method returns a list of default direct objects. The verbs in adv.t generally return a list of all accessible objects; a couple of verbs return more restricted lists (for example, "take" returns a list of accessible objects that aren't already in the player's inventory). Similarly, the parser calls the deepverb method ioDefault to get a list of default indirect objects. When doDefault (or ioDefault, as appropriate) returns a list that consists of exactly one object, the parser uses this object as the default for the command. If the list is empty, or consists of more than one object, the parser will ask the player to supply the object, because the command doesn't imply a particular object.
The parser's automatic default system can be especially convenient for indirect objects. Certain commands imply the use of particular indirect objects; for example, "dig" implies a shovel or similar tool. For such commands, it's a good idea to define ioDefault methods that return a list of accessible objects that satisfy particular criteria; for "dig," you would return a list of accessible objects that are usable for digging.
The doDefault method has another use: the parser calls these methods to determine the meaning of "all." When the player uses "all" as a direct object, the parser replaces "all" by the list returned by doDefault. Since the parser doesn't allow using a command to include multiple indirect objects, "all" cannot be used as the indirect object, hence ioDefault will never be used to supply a list for "all." Note that you can disallow using "all" (and multiple direct objects in general) for a particular verb by defining the property rejectMultiDobj to be true in the deepverb. Here's an example of using rejectMultiDobj to prevent the player from being able to look at everything in the room all at once:
modify inspectVerb rejectMultiDobj(prep) = { "You can only look at one thing at a time. "; return true; } ;
After processing the verification and action methods, the command is finished. The parser goes back and processes the next direct object in the same command, as described earlier. The steps above are repeated for each direct object in the list.
Once all of the direct objects have been processed for a particular command, the turn is over. Even if the command line has additional commands following the current command, the parser considers the current command to be a complete turn - the additional commands will be processed, but will be counted as additional turns. So, once all of the direct objects have been processed for a command, the parser executes the daemons and fuses.
The parser first executes the daemons. The order in which the daemons are executed is arbitrary; it depends on the (unpredictable) order in which the system builds its internal lists as daemons are added and removed. The parser executes all active daemons set with the setdaemon() built-in function, then executes all active daemons set with the notify() built-in function.
Next, the parser executes any active fuses that has "burned down." As with daemons, the order in which fuses are executed is arbitrary and unpredictable. The system first executes any fuses set with the setfuse() function, then executes any fuses set with the notify() function. Before executing each fuse, the parser removes it from the list of active fuses. Only fuses that have burned down are executed; others are left to be processed when they're ready.
Note that the fuse timer is entirely under the control of your game program. The only time that fuse timers are advanced is when you call the incturn() built-in function. This function advances all fuse timers by one turn (or by a specified number of turns, if you provide an argument), and marks any fuses that burn down as being ready for execution. The incturn() function doesn't actually execute any fuses, but merely advances their timers.
The function skipturn is similar to incturn, but rather than marking expiring fuses for execution, skipturn simply deletes any fuses that burn down in the specified interval. Deleted fuses are never executed; this allows you to entirely bypass any timed events that would normally have happened in the given number of turns. skipturn requires one argument, which is the number of turns to skip.
Chapter Three | Table of Contents | Chapter Five |