Adjusting scope, but only for specific actions

Say I have some objects that I want to always be in scope. Then all I need to do is twiddle the objects (and/or their parent class(es)) to add themselves to scope via getExtraScopeItems() (alternately, the objects can be added by whatever Actor class needs to have the objects in scope for their actions). That’s straightforward.

Now let’s say I want to include/exclude the object(s) for specific group(s) of actions. How does one do this?

We can’t just check gAction (either for identity or for class membership) because although gAction is defined when getExtraScopeItems() is called, it apparently isn’t configured yet (that is, if the command is >EXAMINE PEBBLE then gAction will be an instance of Action but it will not yet be an instance of ExamineAction).

This is (reasonably) straightforward if only one Action or Thing needs to be tweaked, because then it could be hashed out in a dobjFor() on the object for the specific action, or something like that. But I want something like “pebbles are always in scope, except when the action is one of [ some list ]”.

For some cases, this “just works” because of additional checks on the action (for example, checking for reachability in TakeAction). But in other cases it doesn’t, and having extra objects in scope produces unexpected behaviors (if the additional object is a Person, then things like >ASK ABOUT PEBBLE will force a disambiguation even if one person is present).

Simple illustration of the issue. Here we define a _foozle property on Action which defaults to nil but is explicitly set to a unique numeric value for ExamineAction and TakeAction:

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

modify Person
        getExtraScopeItems(actor) {
                local lst;

                lst = inherited(actor);
                if((actor != self) || !gAction)
                        return(lst);
                if(gAction.ofKind(ExamineAction)) "ExamineAction\n ";
                if(gAction.ofKind(TakeAction)) "TakeAction\n ";
                "gAction._foozle = <<gAction._foozle ? toString(gAction._foozle)
 : 'nil'>>\n ";
                return(lst);
        }
;

modify Action _foozle = nil;
modify ExamineAction _foozle = 1;
modify TakeAction _foozle = 2;

startRoom:      Room 'Void'
        "This is a featureless void. "
;
+me:    Person;
+pebble: Thing 'small round pebble' 'pebble'
        "A small, round pebble. "
        dobjFor(Examine) {
                action() {
                        inherited();
                        if(gAction.ofKind(ExamineAction)) "action: ExamineAction\n ";
                        if(gAction.ofKind(TakeAction)) "action: TakeAction\n ";
                        "action: gAction._foozle = <<gAction._foozle ? toString(gAction._foozle) : 'nil'>>\n ";
                }
        }
;


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
;

Sample transcript:

Void
This is a featureless void.

You see a pebble here.

>x pebble
gAction._foozle = nil

A small, round pebble.  action: ExamineAction
action: gAction._foozle = 1

The output for >X PEBBLE just demonstrates that gAction isn’t “complete” when getExtraScopeItems() is called, but it is by the time pebble.dobjFor(Examine) is called.

1 Like

I haven’t tested or double-checked anything I’m about to say, but I’d be really surprised if twiddling getExtraScopeItems is all you need to do. I would probably think along the lines of

modify Action
    objInScope(obj) { 
        if(self.baseActionClass is in(ExemptAction1,ExemptAction2...))
            return inherited(obj);
        else return inherited(obj) || obj.ofKind(OmnipresentClass); 
    }

And if you didn’t like the if-clause in such generic code, then you’d probably have to rewrite ExemptAction::objInScope to reflect what is written in adv3, which is usually just finding the obj in gActor.scopeList
Just firing from the hip there, I haven’t tested any cases…

2 Likes

I’m saying that because I’ve tried to use getExtraScopeItems alone to bring something into or out of view, and it did not work as desired…

Yeah, there’s additional twiddling you need to do if you want an action to succeed with an object that would otherwise be out of scope. That’s not the problem I’m trying to address, though. What I’m trying to fix is that even if the action would subsequently fail (because of a precondition check or something like that), an object added to scope (that would otherwise not be in scope) will still trigger disambiguation prompts. Which I don’t want.

So for example, I add defined actors to the player’s scope, and then twiddle preconditions to give more sensible failure reports when an actor is the target of an action and they aren’t present. Things like responding to >X ALICE (when Alice is in another room) with something like “Alice isn’t here right now.
The last place you saw her was in the library around a half an hour ago.” versus the default “You see no alice here” or “You can’t see her.” (depending on the actor definition).

But then if there’s an Alice and a Bob actor in the game, then >ASK ABOUT PEBBLE will always trigger a disambiguation prompt. Even if only Alice is present, and >ASK BOB ABOUT PEBBLE would fail.

So, in this case, I’d want the additional objects (actors) added to scope only for a number of actions in which either the default is going to be replaced or where I actually want to handle the action is some particular way.

This is a kinda kludgy approach that kinda works, but it feels very brittle and very kludgy:

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

modify Actor 
        // Updated on any turn where we're in the pseudo scope
        _pseudoScopeTurn = nil

        // Returns boolean true iff we're in pseudo scope this turn
        inPseudoScope() { return(_pseudoScopeTurn == libGlobal.totalTurns); }
;

modify Action
        // By default, actions don't use pseudo scope
        _usePseudoScope = nil

        objInScope(obj) {
                local r;

                // If we're already in scope, we don't have anything to do
                r = inherited(obj);
                if(r)
                        return(r);

                return(objInPseudoScope(obj));
        }

        // Determine if the object is in the pseudo scope
        objInPseudoScope(obj) {
                // See if this action uses pseudo scope and the object is in it.
                if(_usePseudoScope && obj.inPseudoScope())
                        return(true);
                
                return(nil);
        }
;

// Mark Examine as using pseudo scope
modify ExamineAction _usePseudoScope = true;

// Change the default failure message(s) for actions targetting absent actors.
modify playerActionMessages
        actorNotHere(obj) {
                gMessageParams(obj);
                return('{That/she obj} {is obj}n\'t here at the moment. ');
        }
;

modify objVisible
        verifyPreCondition(obj) {
                if((obj != nil) && !gActor.canSee(obj)) {
                        inaccessible(&actorNotHere, obj);
                }
        }
;

// Class for Actors for whom other actors are always in pseudo scope
class ActorWithMemory: Actor
        getExtraScopeItems(actor) {
                local lst;

                lst = inherited(actor);
                if(actor != self)
                        return(lst);
                lst = _getExtraScopeItemsActors(lst);
                return(lst);
        }
        // All actors are always in scope
        _getExtraScopeItemsActors(lst) {
                forEachInstance(Actor, function(o) {
                        if(o == self) return;
                        if(lst.indexOf(o) != nil)
                                return;
                        o._pseudoScopeTurn = libGlobal.totalTurns;
                });
                return(lst);
        }
;

startRoom:      Room 'Void'
        "This is a featureless void. "
        north = aliceRoom
        south = bobRoom
;
+me:    ActorWithMemory, Person;
aliceRoom:      Room 'Alice\'s Room'
        "This is Alice\'s room. "
        south = startRoom
;
+alice: Actor, Person 'Alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        isProperName = true
        isHer = true
;
++pebbleTopic: Topic 'pebble';
++AskTellTopic @pebbleTopic
        "<q>About the pebble....</q> you begin.
        <.p><q>Not interested,</q> Alice says. "
;
bobRoom:        Room 'Bob\'s Room'
        "This is Bob's room. "
        north = startRoom
;
+bob: Actor, Person 'Bob' 'Bob'
        "He looks like a Robert, only shorter. "
        isProperName = true
        isHim = true
;
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 uses getExtraScopeItems() to mark objects for possible inclusion in the “pseudo scope” but doesn’t add anything to the scope directly. Then the objInScope() on the action can check to see if the object is marked and add it to the scope if it’s an action that wants to do that (in this case it’s controlled by a _usePseudoScope flag on the action).

In this case the only action that has the flag set is ExamineAction, which means that examining actors that aren’t present will trigger the custom failure messages but actors out of the normal scope won’t make >ASK ABOUT PEBBLE prompt for disambiguation:

Void
This is a featureless void.

>x alice
She isn't here at the moment.

>n
Alice's Room
This is Alice's room.

Alice is standing here.

>ask about pebble
(asking Alice)
"About the pebble...." you begin.

"Not interested," Alice says.

The really kludgy bit is using the turn number to mark whether or not an object is eligible to be in scope. This should probably be a flag that cleared at the start of action processing but I don’t know of a convenient hook for that kind of thing.

You can also do something like an Unthing, MultiLoc, NameAsOther. buildLocationList can iterate through all Rooms and add itself, target obj is Alice, and there shouldn’t be any disambig since it’s an Unthing. Just fill out notHereMsg; subclass it and instances just have to list their target actor…
Edit: could just set initialLocationClass to Room

The problem with using something like Unthing is that it doesn’t really scale. It’s fine if you’ve got one object you want to handle like this (just Alice) but it gets out of hand if you want to use it as a general case (all actors). It also has the disadvantage that the behavior of an object is not not encapsulated in the object itself, but instead some other widget that points to it. Which is again something that works(-ish) as a one-off and can turn out to be a handful if you’re applying the same logic to a whole bunch of objects.

It really feels like it wants to be a whole separate usage case for the parser to handle (like the difference between >TAKE PEBBLE and >ALICE, TAKE PEBBLE) but I don’t really have any intuition for how complicated it would be to implement “grammatical scope” or whatever you want to call it as a general case (that is, for all objects known but not currently in scope). It’s one of those things that’s fairly “intuitive” (in that it’s more or less how we interact with real objects in our day to day life) but doesn’t really map well to the room-centric framing implicit in most IF.

I’m not sure in which sense you’re meaning that it gets out of hand, but you could modify Person to have “myUnperson = perInstance(new Unperson(self))” and then there’s no more coding involved. It seemed from your example like the notHereMsg was the primary functionality you were trying to cover, so I feel like this is a decently solid TADSish way of accomplishing it…
If you have deeper interactivity in mind than what’s been mentioned, I guess that’s a different matter…

Yeah, the sample code was just to get a “working” example that demonstrates the basic mechanics, not all the gameplay I want to accomplish with it. Like earlier I mentioned replacing the default “You see no alice here.” with something like “Alice isn’t here right now. The last place you saw her was in the library around a half an hour ago.” You can make that work with something like an Unthing, but you end up either coding a bunch of special cases or building a class of Unthing whose instances remember who their parent Things are and delegate most of their functionality to the parent objects.

And one of the cases I want to be able to handle (as a general case) is where different actors can have different ideas of, for example, who Alice is.