NPC obeyCommand() with action not currently valid

Got another one. In the game I’m working on there’s a fair amount of NPC scripting, where the NPCs engage in various behaviors in a semi-autonomous fashion. Most of this is handled via agendas. What I’m trying to do is use an order from the player in the form of (for example) ALICE, TAKE THE PEBBLE to trigger an agenda that handles the task, possibly involving multiple steps, some of which may not be in scope when the order is given.

First, an example of the agenda behavior. In this sample game there are two rooms: a starting room containing the player and Alice, and a room to the north containing a box which in turn contains a pebble. The player can TELL ALICE ABOUT PEBBLE, prompting Alice to go on a “quest” to retrieve the pebble with no further interaction with the player. This is a very simplified example and doesn’t handle exceptions (like the player rushing in and grabbing the pebble before Alice can), but it illustrates the general form of what I’m talking about:

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

startRoom:      Room 'Void'
        "This is a featureless void. "
        north = northRoom
;
northRoom:      Room 'North'
        "This is the north room.  Also a void. "
        south = startRoom
;

+ box: OpenableContainer, Fixture 'wooden box' 'box'
        "A small wooden box. "
        location = northRoom
        isListed = true
        contentsListed = nil
        contentsListedInExamine = nil
;
++ pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble. "
;

class PebbleActor: Actor
;

class PebbleAgenda: AgendaItem
        agendaOrder = 99
        initiallyActive = true

        // What we're after
        target = nil

        // All we need is a target
        isReady = (target != nil)

        // True if we're carrying our target, nil otherwise
        haveTarget = ((target != nil)
                && (target.getCarryingActor() == getActor()))

        // The "quest" is over
        resetQuest() { target = nil; }

        // Main logic for the "quest"
        invokeItem() {
                local actor, rm;

                actor = getActor();

                // Should never happen, give up
                if(target == nil)
                        return;

                // Figure out where we want to be
                if(haveTarget()) {
                        // If we have the target, return to the start room
                        rm = startRoom;
                } else {
                        // If we don't have the target, we want to go where
                        // it is
                        rm = target.roomLocation;
                }

                // If we're not where we want to be, figure out how to
                // get there
                if(actor.roomLocation != rm) {
                        // Comically simplistic "path finding"
                        switch(rm) {
                                case startRoom:
                                        newActorAction(actor, South);
                                        break;
                                default:
                                        newActorAction(actor, North);
                                        break;
                        }
                        return;
                }
                // If we have the target and we reached this point, we're
                // where we want to be, so just declare victory
                if(haveTarget()) {
                        "<q>Quest complete,</q> <<actor.name>> declares. ";
                        resetQuest();
                        return;
                }

                // We're in the target location and we don't have it yet,
                // so see if we can just take it
                if(actor.canSee(pebble)) {
                        newActorAction(actor, Take, pebble);
                        return;
                }

                // We can't see the pebble, but we're in the same room as
                // it is, so it must be in the box (or something esoteric
                // beyond the scope of this example code).
                newActorAction(actor, Open, box);
        }
;

alice: PebbleActor 'alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        isHer = true
        isProperName = true
        location = startRoom
;
+ alicePebbleAgenda: PebbleAgenda;
+ PebbleTopic: Topic 'pebble';
+ TellTopic @PebbleTopic
        topicResponse() {
                "<q>I think there's a pebble in the room north of here,</q>
                        you mention casually. ";
                alicePebbleAgenda.target = pebble;
        }
;

me:     Actor
        location = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

This all works as written, but it can’t be directly converted into behavior triggered by a command (i.e. ALICE, TAKE THE PEBBLE instead of TELL ALICE ABOUT PEBBLE), for several reasons: the pebble doesn’t start out in scope (because nobody can see it), the various verify() methods all fail for various reasons, and so on.

Here’s a slightly different example game which does not work the way I want it to, that illustrates some of the problems I’ve identified an how I’ve tried to approach them. Instead of two rooms we’ve been reduced to one to simplify things, and there are a bunch of additional modifications to library classes which I’ll discuss after the code:

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

//#define __DEBUG_PEBBLE

startRoom:      Room 'Void'
        "This is a featureless void. "
;

+ box: OpenableContainer, Fixture 'wooden box' 'box'
        "A small wooden box. "
        location = startRoom
        material = glass
        isListed = true
        contentsListed = nil
        contentsListedInExamine = nil
;
++ pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble. "
        dobjFor(Take) {
                verify() {
                        if(gActor._cmdFlag)
                                return;
                        inherited();
                }
                action() {
                        if(gActor._cmdFlag)
                                return;
                        inherited();
                }
        }
;

modify Action
        objInScope(obj) {
#ifdef __DEBUG_PEBBLE
                "\nobjInScope(<<obj.name>>):  gActor is <<gActor.name>>\n ";
#endif
                return(gActor.hasSeen(obj));
        }
/*
        isConversational(issuingActor) {
                if(issuingActor == me) return(true);
                return(nil);
        }
*/
;

modify PreCondition
        checkPreCondition(obj, allowImplicit) {
                if(isThisAnOrder(obj))
                        return(nil);

                return(inherited(obj, allowImplicit));
        }
        verifyPreCondition(obj) {
#ifdef __DEBUG_PEBBLE
                "\nverifyPreCondition(<<obj.name>>):
                        gActor is <<gActor.name>>\n";
#endif
                if(isThisAnOrder(obj))
                        return;

                inherited(obj);
        }
        isThisAnOrder(obj) {
                if(gActor._cmdFlag) return(true);
                return((gIssuingActor != nil) && (gActor != gIssuingActor));
        }
;

class PebbleActor: Actor
        _cmdFlag = nil

        obeyCommand(fromActor, action) {
                if(action.ofKind(TakeAction)) {
#ifdef __DEBUG_PEBBLE
                        "obeyCommand():  accepting order\n ";
#endif
                        _cmdFlag = true;
                        return(true);
                }
                return(inherited(fromActor, action));
        }

        resetCmd() {
                _cmdFlag = nil;
        }
        actorAction() {
                inherited();
                if(_cmdFlag) {
#ifdef __DEBUG_PEBBLE
                        "actorAction() for <<gActor.name>>\n ";
                        "\tgDobj is <<gDobj.name>>\n ";
#endif
                        alicePebbleAgenda.target = gDobj;
                        resetCmd();
                        //throw new TerminateCommandException();
                        exit;
                }

                resetCmd();
        }
;
class PebbleAgenda: AgendaItem
        agendaOrder = 99
        initiallyActive = true

        // What we're after
        target = nil

        // All we need is a target
        isReady = (target != nil)

        // True if we're carrying our target, nil otherwise
        haveTarget = ((target != nil)
                && (target.getCarryingActor() == getActor()))

        // The "quest" is over
        resetQuest() { target = nil; }

        // Main logic for the "quest"
        invokeItem() {
                local actor;

                actor = getActor();

                // Should never happen, give up
                if(target == nil)
                        return;

                // If we have the target and we reached this point, we're
                // where we want to be, so just declare victory
                if(haveTarget()) {
                        "<q>Quest complete,</q> <<actor.name>> declares. ";
                        resetQuest();
                        return;
                }

                // We're in the target location and we don't have it yet,
                // so see if we can just take it
                if(actor.canSee(pebble)) {
                        newActorAction(actor, Take, pebble);
                        return;
                }

                // We can't see the pebble, but we're in the same room as
                // it is, so it must be in the box (or something esoteric
                // beyond the scope of this example code).
                newActorAction(actor, Open, box);
        }
;

alice: PebbleActor 'alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        isHer = true
        isProperName = true
        location = startRoom
;
+ alicePebbleAgenda: PebbleAgenda;
+ PebbleTopic: Topic 'pebble';
+ TellTopic @PebbleTopic
        topicResponse() {
                "<q>I think there's a pebble in the box,</q>
                        you mention casually. ";
                alicePebbleAgenda.target = pebble;
        }
;

me:     Actor
        location = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

The first issue is scope. When issuing a command of the form ALICE, TAKE THE PEBBLE the parser will almost immediately give up because nobody can see the pebble. In this example the box now has material = glass so everybody can see its contents, but what I want is to rely on NPC knowledge (or lack of knowledge) of where the pebble is. The code modifies Action to include

        objInScope(obj) {
                return(gActor.hasSeen(obj));
        }

…which is approximately what I want, but gActor is always the player when the scope is initially evaluated, even when the context is the player issuing a command to another actor. So that’s problem number one: what I want is to evaluate the scope for the actor receiving the command, but if there’s some way to do that early enough in the process that the command won’t immediately fail (with “Alice sees no pebble.”) I don’t know about it.

The next problem is the implicit action that the parser will immediately add to the actor’s pending action queue. What I’m attempting, and failing, to do in the code is to use obeyCommand() to flag that the NPC’s current action is them following a command. If this flag is set, the intent is to bypass verify() and PreCondition checks as well as the default action() behavior. Processing will eventually reach actorAction() where we can trigger the agenda.

The problem is that the implicit action logic means that by the time we reach actorAction() the gDobj is box, not pebble…because the parser has figured out that we need to open the box to get to the pebble.

But at this point I’m wondering if this basic approach is just completely off-base. I also explored, but made no particular progress with, trying to modify Action's isConversational() method to try to handle ALICE, TAKE THE PEBBLE as a conversational item instead of an order.

Because I really just want to tell the parser to skip the normal “issuing an order” logic and just let me get at the parsed input late enough in the process that i.e. gDobj has been set (it is not set when obeyCommand is called, or we could just call the agenda from there and then abort the rest of the command processing).

I hope this is all reasonably clear. The main moving parts of the problem I’m having trouble with are:

  • Tweaking the object scope to reflect NPC knowledge when the NPC is following an order (it’s straightforward when the actor is initiating the action on their own)
  • Passing a currently-invalid command through processing far enough to be able to use it to trigger an agenda

If there’s a library or something that handles this sort of thing, a pointer to it would be appreciated.

Hmmm. A couple of things. First, a simple-ish way to handle the scope issue appears to be modifying the PebbleActor class to do something like:

        getExtraScopeItems(actor) {
                if(actor.ofKind(PebbleActor) && actor.hasSeen(pebble))
                        return(inherited(actor) + pebble);
                return(inherited(actor));
        }

The syntax is slightly awkward (a method on an Actor class taking itself as an argument) but this “works” because every object in scope has its getExtraScopeItems() called with the actor as an argument as part of noun resolution. Since the actor is always in its own scope, this should work reliably unless there’s some other parser weirdness I’m missing. Some parser weirdness that affects this I mean.

Beyond that, I think the reason it’s still failing with the object added to the scope is because adv3’s stock PreCondition classes aren’t well-behaved with respect to inheritance. So my attempt to monkey patch them with something like

modify PreCondition
        verifyPreCondition(obj) {
                doSomething();
                inherited(obj);
        }
;

is doomed to fail because some of the stock PreCondition instances overwrite the check methods in ways that completely replace the parent class’ method. So that approach needs to be abandoned.

The alternative, that appears to work at least in this case, is to overwrite the logic in Action that enumerates the PreCondition objects to use instead of changing the behavior of the objects themselves. That looks something like:

        callVerifyPreCond(resultSoFar) {
                if(gActor._cmdFlag) return(resultSoFar);
                return(inherited(resultSoFar));
        }
        getObjPreConditions(obj, preCondProb, whichObj) {
                if(gActor._cmdFlag) return([]);
                return(inherited(obj, preCondProb, whichObj));
        }
;

There’s still a number of sharp corners and implementation warts to work out, but this is a revised sample game that appears to mostly work:

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

startRoom:      Room 'Void'
        "This is a featureless void. "
;

+ box: OpenableContainer, Fixture 'wooden box' 'box'
        "A small wooden box. "
        location = startRoom
        isListed = true
        contentsListed = nil
        contentsListedInExamine = nil
;
++ pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble. "
;

modify Action
        callVerifyPreCond(resultSoFar) {
                if(gActor._cmdFlag) return(resultSoFar);
                return(inherited(resultSoFar));
        }
        getObjPreConditions(obj, preCondProb, whichObj) {
                if(gActor._cmdFlag) return([]);
                return(inherited(obj, preCondProb, whichObj));
        }
;

class PebbleActor: Actor
        _cmdFlag = nil

        obeyCommand(fromActor, action) {
                if(action.ofKind(TakeAction)) {
                        _cmdFlag = true;
                        "<q>Okay,</q> says <<name>>. ";
                        return(true);
                }
                return(inherited(fromActor, action));
        }

        resetCmd() {
                _cmdFlag = nil;
        }
        actorAction() {
                inherited();
                if(_cmdFlag) {
                        alicePebbleAgenda.target = gDobj;
                        resetCmd();
                        exit;
                }

                resetCmd();
        }
        getExtraScopeItems(actor) {
                if(actor.ofKind(PebbleActor) && actor.hasSeen(pebble))
                        return(inherited(actor) + pebble);
                return(inherited(actor));
        }
;
class PebbleAgenda: AgendaItem
        agendaOrder = 99
        initiallyActive = true

        // What we're after
        target = nil

        // All we need is a target
        isReady = (target != nil)

        // True if we're carrying our target, nil otherwise
        haveTarget = ((target != nil)
                && (target.getCarryingActor() == getActor()))

        // The "quest" is over
        resetQuest() { target = nil; }

        // Main logic for the "quest"
        invokeItem() {
                local actor;

                actor = getActor();

                // Should never happen, give up
                if(target == nil)
                        return;

                // If we have the target and we reached this point, we're
                // where we want to be, so just declare victory
                if(haveTarget()) {
                        "<q>Quest complete,</q> <<actor.name>> declares. ";
                        resetQuest();
                        return;
                }

                // We're in the target location and we don't have it yet,
                // so see if we can just take it
                if(actor.canSee(pebble)) {
                        newActorAction(actor, Take, pebble);
                        return;
                }

                // We can't see the pebble, but we're in the same room as
                // it is, so it must be in the box (or something esoteric
                // beyond the scope of this example code).
                newActorAction(actor, Open, box);
        }
;

alice: PebbleActor 'alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        isHer = true
        isProperName = true
        location = startRoom
;
+ alicePebbleAgenda: PebbleAgenda;
+ PebbleTopic: Topic 'pebble';
+ TellTopic @PebbleTopic
        topicResponse() {
                "<q>I think there's a pebble in the box,</q>
                        you mention casually. ";
                alicePebbleAgenda.target = pebble;
        }
;

me:     Actor
        location = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

It’s still one room to make testing easier, but the box is opaque and works as expected:

>alice, take pebble
Alice sees no pebble.

>open box;close box
Opening the box reveals a pebble.

Closed.

>alice, take pebble
"Okay," says Alice.

>z
Time passes...

Alice opens the box, revealing a pebble.

>z
Time passes...

Alice takes the pebble.

>z
Time passes...

"Quest complete," Alice declares.
3 Likes