Stupid (adv3) parser tricks: disambiguating between multiple noun-as-verb usages

I have a situation in which I want…or at least I think I want…to have a bare noun phrase typed as a command to be parsed as an action applying to the named object.

That’s straightforward enough to implement. If we want to create a new action, call it FooAction, and we want to have it used when the name of an object is typed by itself as a command, then all we have to do is:

DefineTAction(Foo);
VerbRule(Foo) singleDobj: FooAction verbPhrase = 'foo/fooing (what)';

Then we can just put dobjFor(Foo) handlers on any objects we want to do stuff when invoked this way, probably putting a fallback on Thing so we don’t get a “Nothing obvious happens.” by default.

Okay, so far so good. Now how about if we want to have two actions, each of which applies in different situations? Different actions for different kinds of object, for example. And what if you also want the parser to default to the normal behavior (that is, not treating noun phrases as actions) for everything except a specific class of objects?

The naive solution:

DefineTAction(Foo);
VerbRule(Foo) singleDobj: FooAction verbPhrase = 'foo/fooing (what)';

DefineTAction(Bar);
VerbRule(Bar) singleDobj: BarAction verbPhrase = 'bar/barring (what)';

pebble: Thing 'small round pebble' 'pebble' "A small, round pebble. "
        dobjFor(Foo) {
                verify() { nonObvious; }
                action() { "You foo the pebble. "; }
        }
;
rock: Thing 'ordinary rock' 'rock' "An ordinary rock. "
        dobjFor(Bar) {
                verify() { nonObvious; }
                action() { "You bar the rock. "; }
        }
;
+stone: Thing 'nondescript stone' 'stone' "A nondescript stone. ";

…doesn’t work: >PEBBLE, >ROCK, and >STONE will all be interpreted as calling FooAction.

This turns out to be a somewhat complicated problem. I’m not entirely convinced that I have the optimal solution, but I have something that works without having to hammer too much on the adv3 parser.

The trick, if you want to call it that, is to modify each action’s resolveNouns() method to mark noteWeakPhrasing() on the results object. So if we want FooAction to only apply to instances of the class Pebble:

DefineTAction(Foo);
VerbRule(Foo) singleDobj: FooAction verbPhrase = 'foo/fooing (what)'
        resolveNouns(srcActor, dstActor, results) {
                local r;

                inherited(srcActor, dstActor, results);

                if(dobjList_ == nil) 
                        return;

                r = nil;
                dobjList_.forEach(function(o) {
                        if(o.obj_ && o.obj_.ofKind(Pebble))
                                r = true;
                });

                if(r != true)
                        results.noteWeakPhrasing(100);
        }
;

The important bits to note are that you have to call inherited() first, because that’s where dobjList_ gets populated. We then just test that it contains an instance of the class we care about, and if it doesn’t we call noteWeakPhrasing() on the results object.

That gets us part of the way there. If we do this on all of our noun-as-verb actions, then this will work to disambiguate between them. Pebble instance names typed on the command line will always be handled with FooAction, for example. We could do something similar for Rock instances and BarAction. But then if we have a Stone that’s not a Pebble or a Rock, then it will end up handled by whichever noun-as-verb action happened to be declared last. Which is probably not what we want (it’s not what I want, anyway).

The trick here is that we can define an additional noun-as-verb action that handles no specific class. But we can’t declare it exactly the same way we declare the other noun-as-verb actions (because it would be handled the same way, so we’d still have the same problem). Instead we set an arbitrary “badness” flag on the results that’s not “bad” enough to throw an exception. This will make the parser silently drop us on the floor and continue processing, which is exactly what we want. In this case we use noteBadPrep(), which is normally used for ambiguous prepositional phrases:

DefineTAction(Kludge);
VerbRule(Kludge) singleDobj: KludgeAction verbPhrase = 'kludge/kludging (what)'
        resolveNouns(srcActor, dstActor, results) {
                inherited(srcActor, dstActor, results);
                results.noteBadPrep();
        }
;

This creates an additional action that will never be used, but it will catch all of the noun-as-verb statements that aren’t handled by a different noun-as-verb action (that is, one that doesn’t mark the noun phrase as having weak phrasing).

Adding a couple macros to make declaring things easier and rolling it into a module that you can find here, we can get all of the behaviors we want with:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

#include "nounAsVerb.h"

DefineNounAsVerb(Foo, Pebble);
DefineNounAsVerb(Bar, Rock);

class Pebble: Thing
        dobjFor(Foo) { action() { "You foo the pebble. "; } }
;

class Rock: Thing
        dobjFor(Bar) { action() { "You bar the rock. "; } }
;

startRoom: Room 'Void' "This is a featureless void. ";
+me: Person;
+pebble: Pebble 'small round pebble' 'pebble' "A small, round pebble. ";
+rock: Rock 'ordinary rock' 'rock' "An ordinary rock. ";
+stone: Thing 'nondescript stone' 'stone' "A nondescript stone. ";

versionInfo:    GameID;
gameMain:       GameMainDef initialPlayerChar = me;

That is, >PEBBLE is handled as “foo the pebble”, >ROCK is handled as “bar the rock”, and >STONE is handled as “parse this as if none of this noun-as-verb nonsense was here”:

Void
This is a featureless void.

You see a stone, a pebble, and a rock here.

>pebble
You foo the pebble.

>rock
You bar the rock.

>stone
The story doesn't understand that command.

This is a bit of a kludge and, notably, it doesn’t have any disambiguation method for objects that match multiple noun-as-verb actions (in our example, an object that’s an instance of both Pebble and Rock).

But figuring out this much was involved enough that I figured I’d put it out there anyway.

3 Likes

I may have missed your intent, but does this not work?

DefineTAction(Verbless) ...
modify Thing
   dobjFor(Verbless) { verify { illogical('The default response. '); } }
;
modify Pebble
   dobjFor(Verbless) { verify { }
      action { if(ofKind(Stone)) "The case where we're both, but Pebble was last in the superclass list. "; 
          else "The pebble response.  "; } }
;
modify Stone
   dobjFor(Verbless) { verify { }
      action { if(ofKind(Pebble)) "The case where we're both, but Stone was last in the superclass list. ";
         else "The pebble response.  "; } }
;

Or skip the if(ofKind) bits, and just customize action() for hybrid objs. Make a subclass of Pebble, Stone and it has its own dobjFor

1 Like

That’s more or less where I started out (only it was a travel action for moving to adjacent rooms by typing their name instead of a compass direction).

One of the things that a solution of that form doesn’t do is handle multiple actions. You can kinda fake it by just making one actual action behave as any number of “virtual” actions by implementing the different behaviors via conditional spaghetti in the action handlers. But then you’ve got the confusing usage where a single action can mean completely different things depending on context (Verbless might be “move to an adjacent room” in one case or “comment on something the guy you’re in conversation with just said”, to use two examples I’m actually working on).

It also doesn’t fall through to the normal parser behavior. In this case the thing that would end up handling the command is the illogical() in the verify() method in Thing.dobjFor(Verbless). You could fake this as well (by having the verify method output playerMessages.commandNotUnderstood(gActor), for example). But I want processing to actually fall through, because I don’t necessarily know what else might handle the thing if I don’t. So just general coding campground etiquette (leave everything in the condition you found it) when you’re not handling a case you actually want to handle.

Basically the thing you’re talking about is what I started out with, which gets you like 90% of the way there. And then doing the remaining “little” bits ended up being way more complicated that it seems like it ought to be.

1 Like

Lol, well now I have more refactoring ahead! I ‘solved’ that problem with my favorite crutch StringPreParser.

StringPreParser
	doParsing(str, which) {
        local nounVerbPebbleRegex = R'<NoCase>^<Space>*(pebble)<Space>*$';
        local nounVerbRockRegex = R'<NoCase>^<Space>*(rock)<Space>*$';

        if (rexMatch(nounVerbPebbleRegex, str))
             str = 'foo ' + rexGroup(1)[3];
        else if (rexMatch(nounVerbRockRegex, str))
             str = 'bar ' + rexGroup(1)[3];
		return str;
     }
;

Where foo and bar are traditionally defined Verbs.

Obviously, this has the downside of needing a full repeat of the corresponding vocabWords’ nouns in the Regex. While I could programmatically derive it from the object’s vocabWords string, that would still not accommodate dynamically added vocabulary. Next step would be to develop the object’s raw noun list. In my case, the application is fairly surgical, and the objects in question are one-word noun ones, so didn’t bother.

IAC, I know I have a PreParserInit problem, and this will be another step on my road to recovery.

2 Likes

Yeah, that’s another road that I walked down for awhile—using a PreinitObject to iterate over all objects, creating a hash table of their vocabulary, with the leaf nodes containing a callback method defined on the individual object. In addition to being a headache to get working with runtime modified vocabulary, it also (in my case) creates a bunch of disambiguation problems if you’ve got any objects with identical vocabulary.