Keyword based actions

Is there a straightforward way to handle noun-based keyword actions in TADS 3?

Say I want the player to be able to type the name of an object and automatically take that object. I tried something like this:

DefineTAction(Keyword);
VerbRule(Keyword)
    [badness 1000]
    singleDobj
    : KeywordAction
    verbPhrase = 'keyword/keywording (what)'
;

That did what I wanted it to: the dobjFor(Keyword) functions were called, which I could remap to dobjFor(Take). However, this completely lobotomized the disambiguation process. Output was something like this:

My current approach involves generating a list of vocab words for all keyword objects in the game, then replacing them with ‘keyword ’ if they are entered as a single command. I also make a half-hearted effort to prevent disambiguation from blowing up. My concern is that this lacks any finesse whatsoever.

[spoiler][code]
DefineTAction(Keyword);
VerbRule(Keyword)
‘keyword’ singleDobj
: KeywordAction
verbPhrase = ‘keyword/keywording (what)’
;

modify Actor
ambigAction = nil
;

modify playerMessages
askMissingObject(actor, action, which)
{
inherited(actor, action, which);
actor.ambigAction = action;
}
;

StringPreParser
doParsing(str,which)
{
str = str.toLower();

    if (!gActor.ambigAction)
    {
        foreach (local keyword in allKeywords.lst())
        {
            if (rexSearch('^' + keyword + '$', str))
                str = str.findReplace(keyword, 'keyword ' + keyword, ReplaceOnce);
        }
    }
    gActor.ambigAction = nil;

    return str;
}

;[/code][/spoiler]

I asked the same question in this thread. Ultimately this, along with the lack of a browser-based interpreter, means I’ll be developing the next Walker & Silhouette game in Inform/Quixe.

What’s badness? :open_mouth: My own implementation was exactly the same as yours except for that line, and it didn’t work half as well. (I mean, if you think disambiguation is broken by your code - I’d have been happy to have it work so well.)

Wow, and this also fixes an issue I had with commands that consisted of a single punctuation mark (I had to modify TAction as a work-around).

How are you creating your list of vocab words? I can’t seem to find a good tokenise or split method. Or are you only allowing one keyword per object?

Yay!

I think badness just tells the parser to match that line last, so other grammar predicates will take precedence. It never quite seems to do what I expect, at least when modifying the built-in predicates, so I may not understand it very well.

Here’s how I generate the list of keywords.

[spoiler][code]
/**********************************************************************************************************************

  • maintain lists of significant objects
    *********************************************************************************************************************/

#define MakeClassList(K, N)
##N : object

lst()
{
if (lst_ == nil)
initLst();
return lst_;
}

initLst()
{
lst_ = new Vector(50);
local obj = firstObj();
while (obj != nil)
{
if(obj.ofKind(##K))
lst_.append(obj);
obj = nextObj(obj);
}
lst_ = lst_.toList();
}

lst_ = nil \

MakeClassList(Room, allRooms);
MakeClassList(Weapon, allWeapons);
MakeClassList(Armor, allArmor);
MakeClassList(Person, allPeople);
MakeClassList(Keyword, allKeywords);

modify allKeywords
initLst()
{
local keyWords = new Vector(200);
local keyNouns = allRooms.lst() + allWeapons.lst() + allArmor.lst() + allPeople.lst();
local vocab, toks;

    foreach (local keyNoun in keyNouns)
    {
        if (keyNoun.vocabWords)
        {
            vocab = keyNoun.vocabWords.toLower();
            vocab = vocab.findReplace('/',' ', ReplaceAll);
            vocab = vocab.findReplace('*',' ', ReplaceAll);
            toks = Tokenizer.tokenize(vocab);

            for (local i = 1, local cnt = toks.length() ; i <= cnt ; ++i)
                keyWords.appendUnique(getTokVal(toks[i]));
        }
    }

    lst_ = keyWords.toList();
}

;
[/code][/spoiler]

It will match every token in vocabWords for every object of Room, Person, Armor, and Weapon in the game. Keyword is a throwaway class so I could get extra mileage out of the macro.

For room keywords to work correctly, I had to fiddle with scope a bit.

modify KeywordAction objInScope(obj) { return true; } getScopeList() { return scope_ + allRooms.lst(); } ;

I also undefined the majority of verbs and rolled my own Enter and Exit actions, which could be helpful in avoiding unwanted clashes with other verbs.

This is for a port of the Treasures of an Unspecified Realm or District code from S. John Ross, so I have a pretty free hand in demolishing the parser’s vocabulary. I can post more of it if you’re interested, though it likely has a lot of bugs at this stage - I’ve only been at it for a couple days.

It makes more sense to rewrite failed commands as TAKE + failed_command instead of messing with vocabulary. For example, if you type DOOR, that will fail. When it fails, rewrite the command as TAKE DOOR and try again.

When a command fails, notifyParseFailure(issuingActor, messageProp, args) is called on the actor that would perform the failed action. I assume you only want this for the implicit “me” actor. So when defining your “me” actor, override the default implementation of the above method with something like:

me: Person
    // ...
    notifyParseFailure(issuingActor, messageProp, args)
    {
        callWithSenseContext(nil, nil, new function()
        {
            if (issuingActor.isPlayerChar()) {
                if (issuingActor != self && !canTalkTo(issuingActor)) {
                    cannotRespondToCommand(issuingActor, messageProp, args);
                } else {
                    // ***** Replace the command here.
                }
            } else {
                issuingActor.notifyIssuerParseFailure(self, messageProp, args);
            }
        });
    }

The above is the default implementation except for the line marked with “*****”. There, instead of printing an error message, which is what the default does, you can replace the current command with a new one. You do that by throwing a ReplacementCommandStringException exception. For example, if you would like to replace the current command with “TAKE DOOR”, you would write:

throw new ReplacementCommandStringException('take door', nil, nil);

However, I don’t know how to get the text the player typed :stuck_out_tongue: This should be possible and I hope someone knows how. Assuming that a hypothetical function currentText() does that, the above would be:

throw new ReplacementCommandStringException('take ' + currentText(), nil, nil);

Note that the above only deals with commands that contain an object but no verb. Typing DOOR triggers the above. But typing QWERTY does not. And I assume this is exactly what you want; only try TAKE if the player actually typed a word that the game recognizes.

I assume we could use a StringPreParser for that, right? Give it a sufficiently high runOrder so it runs after all the others.

modify Actor
    lastCmdText = nil
;

StringPreParser
    doParsing(str,which)
    {
        gActor.lastCmdText = str;
        return str;
    }
    runOrder = 1000
;
throw new ReplacementCommandStringException('take ' + self.lastCmdText, nil, nil);

This looks perfect, thank you very much!

Here is the full source for the Keyword action.

modify Actor
    notifyParseFailure(issuingActor, messageProp, args)
    {
        callWithSenseContext(nil, nil, new function()
        {
            if (issuingActor.isPlayerChar())
            {
                if (issuingActor != self && !canTalkTo(issuingActor))
                {
                    cannotRespondToCommand(issuingActor, messageProp, args);
                }
                else
                {
                    local keyword = self.lastCmdText;
                    if (keyword)
                    {
                        self.lastCmdText = nil;
                        throw new ReplacementCommandStringException('keyword ' + keyword, nil, nil);
                    }
                    else
                    {
                        getParserMessageObj().(messageProp)(self, args...);
                    }
                }
            }
            else
            {
                issuingActor.notifyIssuerParseFailure(self, messageProp, args);
            }
        });
    }
    lastCmdText = nil
;

StringPreParser
    doParsing(str,which)
    {
        gActor.lastCmdText = str;
        return str;
    }
    runOrder = 1000
;

DefineTAction(Keyword);
VerbRule(Keyword)
    'keyword' singleDobj
    : KeywordAction
    verbPhrase = 'keyword/keywording (what)'
;

Small optimization you can do here so that a string copy and local variable creation happen less often:

                    if (self.lastCmdText)
                    {
                        local keyword = self.lastCmdText;
                        self.lastCmdText = nil;

Thanks for the tip! I also had better luck using executeCommand() instead of throwing an exception; under certain circumstances - when using ’ all’ or ‘again’ - the error handling doesn’t seem to work the same way, and the ReplacementCommandStringException goes uncaught, leading to a nasty runtime error.

                    //...
                    if (issuingActor.lastCmdText)
                    {
                        local keyword = issuingActor.lastCmdText;
                        local toks = Tokenizer.tokenize('keyword ' + keyword);
                        issuingActor.lastCmdText = nil;
                        executeCommand(issuingActor, issuingActor, toks, true);
                    }