Roger Firth's IF pages

Home

InfAct -- about Inform NPCs

Email
Back up

When describing the verb grammars using the creature token, we deferred discussion of the Ask, Tell and Answer actions until later. This is because these actions also use a topic token, which is treated by the parser as a bit of a special case.

 
Verb 'ask'
        * creature 'about' topic    -> Ask;
Verb 'tell'
        * creature 'about' topic    -> Tell;
Verb 'answer' 'say' 'shout' 'speak'
        * topic 'to' creature       -> Answer;

Whereas the parser happily maps a command like SHOW THE BLACK VELVET CLOAK TO THE USHER into the variables action=Show, noun=cloak object and second=usher object, it's rather less helpful with the similar SAY THE BLACK VELVET CLOAK TO THE USHER. You still get action=Show and second=usher, but noun just points to the first dictionary word which isn't THE; in our example it holds 'black', which isn't all that informative. Two additional variables are set -- consult_from=2 and consult_words=4 -- so that effectively the parser is saying: the topic of this conversation occupies four words starting at the second word in the input buffer; making some real sense of those words is down to you. The handling of Ask and Tell is similar, except that in these cases it's second which points to the first dictionary word which isn't THE.

So, if you plan to allow conversation in your game -- and there's only so many deaf/indifferent/uncomprehending NPCs you can get away with -- you need to devise a strategy for dealing with topics. The rest of this segment looks at some of the possibilities.

Using the first parsed word

The first approach, viable only if you anticipate very limited conversation, is simply to use the pointer to the first non-THE word. For example, we could code the Ask action as follows (remember, you could replace the rfalse with a print statement of your own to provide exception handling):

 
        life [;
        Ask: switch(second) {
            'black','velvet','cloak': "~It'll be quite safe in here.~";
            'smart','uniform': "~The management here is very particular.~";
            'hook','hooks': "~Yes, a few more would be useful.~";
            }
            rfalse;
        Give:
            dots
            ],

It works, up to a point. Note how the perfectly sensible question about HIS UNIFORM isn't handled properly, because we didn't allow for 'his' in the switch statement:

 
>ASK THE USHER ABOUT THE VELVET CLOAK
"It'll be quite safe in here."

>ASK HIM ABOUT THE HOOKS
"Yes, a few more would be useful."

>ASK HIM ABOUT HIS UNIFORM
There is no reply.

Looking at all the words

Slightly more ambitiously, you can look at all the topic words before deciding what to do. Here's a routine ScanTopic() which uses the consult_from and consult_words values to fetch all the topic, discarding minor noise words and words not in the dictionary, and storing the remainder in a topicWordtable.

 
Constant topicLimit 7;
Array   topicWord table topicLimit;

[ ScanTopic x y z;
        y = z = 0;
        wn = consult_from;
        while (consult_words--) {
            x = NextWord();
            if (x == 0) ++z;
            else if (x ~= 'the' or 'a' or 'an' or 'some'
                    or 'this' or 'that' or 'these' or 'those'
                    or 'in' or 'of' or 'at' or 'on' or 'with'
                    or 'my' or 'his' or 'her' or 'its' or 'their'
                    or 'is' or 'are' or 'has' or 'have'
                    ) if (y < topicLimit) topicWord-->(++y) = x; else ++z;
            }
        @storew topicWord 0 y;  ! -S doesn't allow topicWord-->0 = y;
        return z;
        ];

ScanTopic() returns the number of topic words which weren't in the dictionary, so that you can estimate how reliably the words in the table match what the user intended. Here's an Ask handler which uses ScanTopic():

 
        life [;
        Ask:
            if (ScanTopic()<2) for ( : topicWord-->0 : (topicWord-->0)--)
                switch(topicWord-->(topicWord-->0)) {
                'black','velvet','cloak': "~It'll be quite safe in here.~";
                'smart','uniform': "~The management here is very particular.~";
                'hook','hooks': "~Yes, a few more would be useful.~";
                }
            "~I'm not sure about that.~";
        Give:
            dots
            ],

In this code, up to one unknown word is acceptable in the topic text. The words in the table are tested in reverse order, though you could go forwards if you wished.

 
>ASK THE USHER ABOUT THE VELVET CLOAK
"It'll be quite safe in here."

>ASK HIM ABOUT HIS UNIFORM
"The management here is very particular."

>ASK HIM ABOUT THE OPERA HOUSE
"I'm not sure about that."

Parsing as an object

One problem with the previous approaches is that we're trying to parse the input text ourselves; what we'd rather do is have the much clever library parser do it for us. Here's a routine ParseTopic() which parses the topic using the library routine NounDomain():

 
[ ParseTopic x;
        wn = consult_from;
        x = NextWord();
        if (x == 'the') {consult_from++; consult_words--; } else wn--;
        x = NounDomain(actor, actors_location, NOUN_TOKEN);
        if (x == REPARSE_CODE) {
            Tokenise__(buffer, parse);
            wn = consult_from;
            x = NounDomain(actor, actors_location, NOUN_TOKEN);
            if (x == REPARSE_CODE) x = 0;
            }
        return x;
        ];

The strength of this approach is that we're applying the full leverage of the browser to resolve the topic into a reference to an object rather than to a dictionary word. Sadly, this is also its weakness, since we need to provide a scope (here, actor and actor_location) where that object may be found. This works well for the cloak and the hook, but doesn't work at all for the usher's uniform, which is merely mentioned, not instantiated as an object. Here's the new simplified Ask handler:

 
        life [;
        Ask: switch(ParseTopic()) {
            cloak: "~It'll be quite safe in here.~";
            hook: "~Yes, a few more would be useful.~";
            }
            "~I'm not sure about that.~";
        Give:
            dots
            ],

And this all behaves as you'd expect.

 
>ASK THE USHER ABOUT THE VELVET CLOAK
"It'll be quite safe in here."

>ASK HIM ABOUT THE HOOK
"Yes, a few more would be useful."

>ASK HIM ABOUT HIS UNIFORM
"I'm not sure about that."

Parsing as a topic

A good way forward would be to combine the strengths of the two previous approaches: use the power of the parser, but reset its scope to embrace a family of 'topic' objects. This turns out to be surprisingly easy. First we define those objects:

 
Object  Topics "conversational topics";
Object  -> t_cloak    with name 'handsome' 'dark' 'black' 'velvet' 'cloak';
Object  -> t_uniform  with name 'smart' 'uniform';
Object  -> t_hook     with name 'small' 'brass' 'hook' 'peg' 'hooks';
Object  -> t_yes      with name 'yes' 'please' 'ok';
Object  -> t_no       with name 'no' 'thanks';
Object  -> t_hello    with name 'hello' 'hi';
Object  -> t_goodbye  with name 'goodbye' 'good-bye' 'good' 'bye' 'cheerio';

Next, we adjust the grammar to bring the objects into scope:

 
[ TopicScope; switch(scope_stage) {
        1: rfalse;
        2: ScopeWithin(Topics); rtrue;
        3: "At the moment, even the simplest questions are confusing.";
        } ];

Extend 'look' first
        *                                   -> Look
        * 'up' scope=TopicScope 'in' noun   -> Consult reverse;
Extend 'consult' first
        * noun 'about' scope=TopicScope     -> Consult;
Extend 'ask' first
        * creature 'about' scope=TopicScope -> Ask;
Extend 'tell' first
        * creature 'about' scope=TopicScope -> Tell;
Extend 'answer' first
        * scope=TopicScope 'to' creature    -> Answer;

And finally we set up handlers in the NPC's life property as usual:

 
        life [;
        Ask: switch(second) {
            t_cloak: "~It'll be quite safe in here.~";
            t_uniform: "~The management here is very particular.~";
            t_hook: "~Yes, a few more would be useful.~";
            }
            "~I'm not sure about that.~";
        Tell: switch(second) {
            t_cloak: "~That's absolutely fascinating.~";
            }
            print_ret (The) self, " expresses mild interest.";
        Answer: switch(noun) {
            t_yes,t_no: "~I'm sure you're right.~";
            t_hello: print_ret (CTheyreorThats) self, " pleased to see you.";
            t_goodbye: "~Come again soon.~";
            }
            print_ret (The) self, " doesn't respond.";
        Give:
            dots
            ],

And this time we've got a conversational system that performs fairly well.

 
>SAY HELLO TO THE USHER
He's pleased to see you.

>SHOW THE CLOAK TO THE USHER
"I don't think I've ever seen one as dark as that!"

>TELL USHER ABOUT CLOAK
"That's absolutely fascinating."

>ASK USHER ABOUT CLOAK
"It'll be quite safe in here."

>ANSWER YES TO USHER
"I'm sure you know best."

In fact, there's a library package Info.h which does exactly this.

Canned conversations

As an aside, an alternative approach which is sometimes appropriate is to replace the ASK/TELL/ANSWER mechanism by one which simply replays a pre-programmed speech or dialogue. Here's a fairly crude example, showing a new TALK verb:

 
Object  usher "gentleman usher" cloakroom
  with  name 'usher' 'gentleman' 'gentle' 'man',
        description "The usher is smartly uniformed.",
        talk_number 0,
        life [;
        Order,Ask,Tell,Answer: print_ret "Just use T[ALK] [TO ", (the) self, "].";
        Talk:
        if (self.talk_number == 0) {
            if (cloak in player)            self.talk_number = 10;
            else if (self hasnt general)    self.talk_number = 20;
            else if (message.number > 0)    self.talk_number = 30;
            }
        switch (self.talk_number++) {
            10: move cloak to hook; give cloak ~worn;
                "~Would you mind hanging up my cloak?~^
                ^~With pleasure, sir. There you are now.~";
            11:
                "~Thank you very much.~^
                ^~You're most welcome. Looks like a bad night out there?~";
            12:	self.talk_number = 20;
                "~Yes, it's raining quite hard at the moment.~^
                ^~Well, you'll be warm and dry in here. Enjoy the opera.~";
            20:
                "~What is tonight's performance?~^
                ^~It's Don Giovanni.~";
            21:
                "~Ah, Verdi!~^
                ^~Indeed, sir. And tomorrow night we're doing Figaro,
                    another, ahem, Mozart gem.~";
            22: give self general;
                "You decide a dignified silence is your best response.";
            30:
                "~It's very dark in the bar!~^
                ^~I'm sorry to hear that, sir.
                    I believe there was a problem earlier.~";
            31:
                "~Yes, I almost stumbled over something on the floor.~^
                ^~I think you'll find things are now back to normal.~";
            default: self.talk_number = 0;
            }
        ],
  has   animate male;
dots
[ TalkSub;
        if (noun == player) "Nothing you hear surprises you.";
        if (RunLife(noun,##Talk) ~= 0) rfalse;
        "At the moment, you can't think of anything to say.";
        ];

Verb    'talk' 't//' 'converse' 'chat' 'gossip'
        * 'to'/'with' creature       -> Talk
        * creature                   -> Talk;

Here's the first part in action:

 
>TALK TO THE USHER
"Would you mind hanging up my cloak?"

"With pleasure, sir. There you are now."

>AGAIN
"Thank you very much."

"You're most welcome. Looks like a bad night out there?"

>AGAIN
"Yes, it's raining quite hard at the moment."

"Well, you'll be warm and dry in here. Enjoy the opera."

There's more on conversation to follow.