Iterating over exits

In TADS3/adv3, is the canonical list of exits from a room buried in exitLister.showExitsWithLister()?

I’m trying to implement a new action that needs to look through the rooms (currently) connected to the present room, and can’t seem to find a library function/method that provides a way (directly) to iterate over the exits. Which seems like an odd omission.

Writing a custom exit lister that in fact lists no exits (but instead uses its showListAll() to do what I need instead of producing output) appears to be the most “straightforward” solution, but a) it feels like a bit of a kludge, and b) doesn’t quite do what I want, because showExitsWithLister() presumes a specific actor and sense context (instead of returning a canonical list of all possible exits).

It also wouldn’t be difficult to just cut and paste most of the loop hidden in showExitsWithLister(). That’s what most other places in the code do—re-implement somewhat simpler versions of the same algorithm from scratch again (roomPathFinder.forEachAdjacent(), for example).

Again, coming up with a solution isn’t that difficult, I’m just making sure that I’m not missing something basic that’s already there in the library.

I think it just iterates over allDirections and adds TConns to a list or vector if there is one…

1 Like

Was just writing this! The Direction.allDirections Vector has the master list, and the dirProp property gives a property pointer for the corresponding Room property.

1 Like

Yeah, you can just do something like:

modify Room
        getExitList(actor?) {
                local c, dst, r;

                r = [];

                if((actor = (actor ? actor : gActor)) == nil)
                        return(r);

                Direction.allDirections.forEach(function(d) {
                        c = self.getTravelConnector(d, actor);
                        if((c == nil) || !c.isConnectorApparent(self, actor))
                                return;
                        dst = c.getDestination(self, actor);

                        r += new DestInfo(d, dst, nil, nil);
                });

                return(r);
        }

…which is more or less just a abridged version of the loop in exitLister.showExitsWithLister(). There are similar loops elsewhere in adv3. I’m just kinda surprised there isn’t just a library function (or existing method on Room/BasicLocation that handles it already.

1 Like

Hmm. Two related questions.

Is there a (generic, straightforward) way to enumerate all the non-standard exits in a room. If you have, for example an AskConnector, iterating over the directions won’t non-trivially get you all the potential exits.

Simple demonstration: three doors on the north wall, each leading to a different room. Also a new action >FOOZLE which will list all of the exits (by enumeration via Direction.allDirections, which in this case will fail):

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

DefineSystemAction(Foozle)
        execSystemAction() {
                startRoom.reportExits();
        }
;
VerbRule(Foozle) 'foozle': FoozleAction verbPhrase = 'foozle/foozling';

class ThreeDoor: Door
        noteTraversal(t) {
                "{You/he} {go|goes} through the door, closing it behind
                        {you/him}. ";
                makeOpen(nil);
        }
;

startRoom:      Room 'Void'
        "This is a featureless void. There are three doors on the north wall. "
        north: AskConnector {
                travelAction = EnterAction
                travelObjs = [ leftDoor, rightDoor, centerDoor ]
                askDisambig(targetActor, promptTxt, curMatchList, fullMatchList,
                        requiredNum, askingAgain, dist) {
                        _doorPrompt();
                }
                askMissingObject(actor, action, which) {
                        _doorPrompt();
                }
                _doorPrompt() {
                        "Do you want to enter the left, right, or center
                                door? ";
                }
        }
        in asExit(north)

        reportExits() {
                "Exits from <<self.roomName>>:<.p> ";
                _matchExits.forEach(function(o) {
                        "\t<<o.dir_.name>> -&gt;
                                <<(o.dest_ ? o.dest_.roomName : 'nil')>>\n ";
                });
        }

        _matchExits(actor?, cb?) {
                local c, dst, r;

                r = new Vector(Direction.allDirections.length());
                if((actor = (actor ? actor : gActor)) == nil)
                        return(r);
                Direction.allDirections.forEach(function(d) {
                        c = self.getTravelConnector(d, actor);
                        if((c == nil) || !c.isConnectorApparent(self, actor))
                                return;
                        dst = c.getDestination(self, actor);
                        if((cb != nil) && ((cb)(d, dst) != true))
                                return;
                        r.append(new DestInfo(d, dst, nil, nil));
                });
                return(r);
        }
;
+leftDoor: ThreeDoor '(left) door' 'left door'
        "It's the left door. "
        destination = leftRoom
;
+rightDoor: ThreeDoor '(right) door' 'right door'
        "It's the right door. "
        destination = rightRoom
;
+centerDoor: ThreeDoor '(center) door' 'center door'
        "It's the center door. "
        destination = centerRoom
;
+me: Person;

leftRoom: Room 'Left Room'
        "It's the left room, such as it is. "
        south = leftDoorInside
        out asExit(south)
;
+leftDoorInside: ThreeDoor ->leftDoor '(left) door' 'door';

rightRoom: Room 'Right Room'
        "It's the right room, such as it is. "
        south = rightDoorInside
        out asExit(south)
;
+rightDoorInside: ThreeDoor ->rightDoor '(right) door' 'door';

centerRoom: Room 'Center Room'
        "It's the center room, such as it is. "
        south = centerDoorInside
        out asExit(south)
;
+centerDoorInside: ThreeDoor ->centerDoor '(center) door' 'door';

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

Second question: is there a less…wordy to do what the example code does with the three doors? That is, if you don’t care about modelling the doors as individual game objects (so open archways or something instead of doors that can be opened and closed), is there any way to declare “non-directional” exits like this, or just throw anonymous travel connectors on a room that can only be accessed by their name?

It would be simple enough (I think) to implement this sort of thing by just entirely bypassing all the TravelConnector stuff and just adding an array on Room that holds a list of rooms the room is connected to and then defining a new set of travel verbs that check it. But that would break a whole bunch of other code in adv3, which is presumably undesireable.

Like say the protagonist gets conked on the head and wakes up in an unfamiliar location. The “standard” way of handling this is to just keep orienting everything via north/south/east/west as usual, trusting that all adventurers have an utterly indefatigable sense of direction. But let’s say instead we want to (temporarily) deprive the player of the ability to navigate this way, and instead rely on picking exits via (for example) left, right, straight, or back. Is there anything that supports this sort of thing already (for adv3), or is this a whole implement-from-scratch kind of behavior?

Replying to myself, but actually…

Rooms are always TravelConnectors, usually to themselves. TravelConnector > RoomConnector > RoomAutoConnector > Room.

This means you can do something like:

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

modify playerActionMessages
        cantEnterNotArea = '{You/he} can\'t enter {the dobj/him}. '
        cantEnterAlready = '{You/he} {are} already in {the dobj/him}. '
        cantEnterTooFar = '{You/he} can\'t enter {the dobj/him} from here. '
;

class AreaTravelAction: Action
        objInScope(obj) {
                local r;

                if((r = inherited(obj)) != nil)
                        return(r);

                return(objInScopeEnterArea(obj));
        }
        objInScopeEnterArea(obj) {
                if((obj == nil) || !obj.ofKind(Area))
                        return(nil);
                return(true);
        }
;

class AreaTravelTAction: AreaTravelAction, TAction;

DefineTActionSub(EnterArea, AreaTravelTAction);
VerbRule(EnterArea)
        'enter' singleDobj
        : EnterAreaAction
        verbPhrase = 'enter/entering (what)'
;

modify Thing dobjFor(EnterArea) { verify() { illogical(&cantEnterNotArea); } };

class Area: Room
        areaConnections = nil

        dobjFor(EnterArea) {
                verify() {
                        local src;

                        src = gActor.getOutermostRoom();
                        if(src == self)
                                illogical(&cantEnterAlready);
                        if(!src.getAreaConnectorTo(self))
                                illogical(&cantEnterTooFar);
                }
                action() {
                        replaceAction(TravelVia, gActor.getOutermostRoom()
                                .getAreaConnectorTo(self));
                }
        }
        getAreaConnectorTo(rm) {
                if((rm == nil) || !rm.ofKind(Area))
                        return(nil);

                if(areaConnections.indexOf(rm) == nil)
                        return(nil);

                return(rm);
        }
;

Area template 'roomName' 'destName'? 'vocabWords'? 'name'? "desc"?;

startRoom: Area 'Void' 'the void room' 'void'
        "This is a featureless void.  It's connected to the other room. "

        areaConnections = static [ otherRoom ]
;
+me: Person;
+pebble: Thing 'small round pebble' 'pebble';

otherRoom: Area 'Other Room' 'the other room' 'other room'
        "This is the other room.  It's connected to the void. "

        areaConnections = static [ startRoom ]
;

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

…without it being any more broken than the three doors example I posted earlier. And this ends up working like:

Void
This is a featureless void.  It's connected to the other room.

You see a pebble here.

>enter other room
Other Room
This is the other room.  It's connected to the void.

>enter void
Void
This is a featureless void.  It's connected to the other room.

You see a pebble here.

…with no “cardinal direction” connectors between the two rooms.

I had considere for awhile of implementing left, right etc as new Directions, never did though…

How would you do that, out of curiosity? Doesn’t left and right depend on your orientation? If you enter a room from the east, right is north. If you enter the same room from the west, right is south. You’d have to invent some sort of orientation tracking system for that to work, wouldn’t you?

Hunter, in Darkness and a few other games experiment with relational directions (left/right, etc.). It’s difficult to pull off in a large-scale game, esp. if you want to account for which direction the player is facing at any turn. (If they go left, turn around, and go back the way they came, the old “left” exit is now behind them, etc.)

I have a quarter-started, now-abandoned game which did away with directions entirely. All locations were referred to by name. For assistance, the names of the exits in the room description were hyperlinked, so the player can simply click to navigate. The result was similar to what you’ve got, above, but doesn’t require a new verb.

As I recall, the way I did this was by making all rooms familiar by default. That permits >GO TO MAIN CORRIDOR (or even simply >MAIN CORRIDOR) for traveling. (There may be some other tweaks, I’d have to go back and look.) I had to modify the GoTo verb handler to include a room’s exits in its scope. And I turned off fastGoTo to prevent players from “leaping” across the map. (I turn that off in general, though.)

I also modified the exit lister to list exits by hyperlinked names rather than directions. Each room had a list of exits, rather than cardinal or relational directions, similar to your areaConnectors:

Room 'foyer'
  exits = [ mainCorridor, elevatorLobby, frontDesk ]
;

As I recall, that was about the sum of it.

3 Likes

That sounds adjacent to what I’ve been thinking about, kinda.

The idea I’ve been toying with is to have something like a >TRAVEL TO [whatever] mechanism for “world” travel, and then use “positional” descriptions for anything “inside” an room/region/whatever.

So say a travelling carnival comes to town. Pseudocodishly, we could have something like:

class TravelRoom: Room
        travelDestination = nil
;
class Carnival: TravelRoom
        travelDestination = ticketBooth
;
class OnMidway: Carnival
        localExits = [ midway ]
;
ticketBooth: OnMidway;
ferrisWheel: OnMidway;
hallOfMirrors: OnMidway;
midway: Carnival
        localExits = [ ferrisWheel, hallOfMirrors, ticketBooth ]
;

So elsewhere (probably restricted to “outdoor”/“in town” locations) you can >TRAVEL TO CARNIVAL (or even >TRAVEL TO FERRIS WHEEL, or any named location inside the carnival) and that’ll take you to the ticket booth. Inside the carnival, you’d use (hand waving here) >RIDE FERRIS WHEEL or >ENTER HALL OF MIRRORS or (prompted by description) >GO BEHIND HALL OF MIRRORS or whatever.

Whenever travelling to the target destination would require more than one “step”, it’s done inside a big try/catch/finally block using, under the hood newActorAction() (for all but the final “step”) and replaceAction() (for the final step), so travel “costs” the same number of turns as if entered manually and everything else in the game gets to take a turn for every “step”. During travel, we have something like:

class TravelException: Exception
        travelExceptionMsg = nil
        construct(msg?) { travelExceptionMsg = (msg ? msg : nil); }
;
#define terminateTravel(msg) throw new TravelException(msg)
class TravelStop: Thing
        travelStopMsg = nil

        afterTravel(actor, conn) {
                inherited(actor, conn);
                terminateTravel(travelStopMsg ?  travelStopMsg : nil);
        }
;

…and in our catch() block:

                catch (TravelException ex) {
                        if(ex.travelExceptionMsg)
                                extraReport(ex.travelExceptionMsg);
                }

…then we can put TravelStop as a mixin on anything we want to interrupt travel—NPCs, a shiny piece of metal on the ground, specific locations, whatever—and travel will be interrupted with the message defined on the object causing the stop.

This is kinda-sorta similar to how automatic travel works in some roguelikes (like nethack), where you can just keep heading in a specific direction or down a corridor until you encounter something “interesting”. You could even make what you consider “interesting” configurable…so you might ignore NPCs you don’t already know, or pass by most NPCs but stop if you pass by officer Alice when you’re fleeing Bob the killer or whatever.

Doing this sort of thing does require implementing bespoke pathfinding, however, since the Dijkstra implementation in pathfind.t relies on the Direction.allDirections sort of thing discussed earlier in this thread.

Yeah, but that’s actually not too complicated. At least from a technical implementation standpoint. I think the real problem would be in implementing something that wasn’t overly confusing/annoying to the player. But the nuts and bolts are a pretty well-explored problem: that’s how virtually every first-person dungeon crawl game handles player position/movement.

Just for giggles, here’s a toy implementation in TADS3/adv3 to illustrate the concept(s):

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

modify playerActionMessages
        okayWizTurnDir(dir, rot) {
                return('{You/he} turn{s} <<rot>>.  {You/he} {are} now
                        facing <<dir>>. ');
        }
        wizStepFailed = '{You/he} can\'t go that way. '
        cantWizNotWizard =  '{You/he} can\'t do that. '
;

class WizTurnAction: IAction
        wizTurnStr = nil
        wizRotation = nil
        execAction() {
                if(!gActor.ofKind(WizActor)) {
                        reportFailure(&cantWizNotWizard);
                        exit;
                }
                gActor.wizRotate(wizRotation);
                defaultReport(&okayWizTurnDir, gActor.wizDirName(), wizTurnStr);
        }
;
DefineAction(WizLeft, WizTurnAction)
        wizTurnStr = 'left'
        wizRotation = -1
;
VerbRule(WizLeft) 'left': WizLeftAction verbPhrase = 'turn/turning';
VerbRule(WizRight) 'right': WizRightAction verbPhrase = 'turn/turning';
DefineAction(WizRight, WizTurnAction)
        wizTurnStr = 'right'
        wizRotation = 1
;

DefineIAction(WizForward)
        execAction() {
                local c, dir, dst, rm;

                if(!gActor.ofKind(WizActor)) {
                        reportFailure(&cantWizNotWizard);
                        exit;
                }

                dir = gActor.wizDirName();
                rm = gActor.getOutermostRoom();
                Direction.allDirections.forEach(function(d) {
                        if(d.name != dir) return;
                        c = rm.getTravelConnector(d, gActor);
                        if(!c || !c.isConnectorApparent(rm, gActor))
                                return;
                        dst = c.getDestination(rm, gActor);
                });
                if(dst) {
                        replaceAction(TravelVia, dst);
                } else {
                        reportFailure(&wizStepFailed);
                }
        }
;
VerbRule(WizForward) 'step': WizForwardAction
        verbPhrase = 'step/stepping';

wizCfg: object
        _dirName = static [ 'north', 'east', 'south', 'west' ]
        rotate(dir, rot) {
                dir += rot;
                while(dir > 4) dir -= 4;
                while(dir < 1) dir += 4;
                return(dir);
        }
        dirName(idx) { return(_dirName[idx]); }
;

#define gWizRotate(dir, rot) wizCfg.rotate(dir, rot)
#define gWizDirName(v) wizCfg.dirName(v)

class WizActor: Actor
        wizDir = 1      // start pointing north, for some reason
        wizDirName() { return(gWizDirName(wizDir)); }
        wizRotate(rot) { wizDir = gWizRotate(wizDir, rot); }
;

startRoom:      Room 'Void'
        "This is a featureless void.  The south room is south of here. "
        south = southRoom
;
+me: WizActor;

southRoom: Room 'South Room'
        "This is the south room.  The void is north of here. "
        north = startRoom
;

versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;

We define a couple new intransitive verbs, >LEFT, >RIGHT, and >STEP (>FORWARD already exists in adv3 with a different meaning, but that’s something that could be sorted out in a real game). We also define an WizActor class for actors that are allowed to move this way.

Thrilling transcript:

Void
This is a featureless void.  The south room is south of here.

>left
You turn left.  You are now facing west.

>right
You turn right.  You are now facing north.

>right
You turn right.  You are now facing east.

>right
You turn right.  You are now facing south.

>step
South Room
This is the south room.  The void is north of here.

Orientation is just a numeric value between 1 and 4, inclusive, WizActor.wizDir. Right is (arbitrarily) positive rotation and Left is negative, and the new actor facing is the old one plus the rotation mod 4 (with the caveat that TADS3 idiosyncratically indexes from one instead of zero, so we’re a) not using an actual % operator and b) our value is actually dir = ((dir + rot) % 4) + 1…if you care about that sort of thing). This all takes substantially more effort to explain than implement.

Then our >STEP command just figures out what direction the actor is pointing in and then either attempts to go that way or reports the failure.

This implementation assumes a four point compass (so left of north is west, not northwest), but extending it to include other directions would be straightforward. There’s also no instrumentation…in a real game you’d want a verb to repeat the current facing, maybe put it in the status line, provide custom travel reports, and so on.

But it should illustrate the basic mechanics, if that’s what you’re interested in.

1 Like

Speaking of nontraditional exit arrangements:

What’s the “correct” way of supplying a game-specific exit lister? In principle exits.t is an optional module, but it’s included by default in adv3.tl (at least in frobTADS). Is there a cleaner way to exclude it than supplying your own makefile for adv3 itself?

1 Like

How did you handle treating nouns (specifically room names) as verbs?

It’s easy enough to do something like:

VerbRule(NounAsVerb)
        singleDobj
        : NounAsVerbAction
        verbPhrase = 'verb/verbing (what)'
;

And then use NounAsVerbAction.objInScope() to add whatever additional stuff (known rooms, for example) to the scope.

The problem being that then non-Room objects are either a) excluded by scope, in which case just typing their name (>PEBBLE) will produce a nonintuitive out of scope failure report (“You see no pebble here,” even if there’s a pebble present), or b) they’re included in scope, in which case you have to do a bunch of juggling in verify() to replicate the failure report(s) that would have been generated if you didn’t have a grammatical rule that matches bare objects.

The way I think this wants to be approached is to #define a new macro for something like singleRoom (or whatever) to use in the VerbRule (instead of singleDobj), but I’m not familiar enough with writing low-level grammatical productions for the parser to hammer that out.

1 Like

I dug up the old code, which used adv3Lite, and then dug through adv3 to see if there’s analogous machinery. I couldn’t find any, unfortunately.

adv3Lite uses a different parser (MJR’s Mercury parser). Its Parser class has a DefaultAction property:

    /* 
     *   The action to be tried if the parser can't find a verb in the command
     *   line and tries to parse the command line as the single object of a
     *   DefaultAction command instead.
     */
    
    DefaultAction = ExamineOrGoTo

This is the comment for ExamineOrGoTo:

/* 
 *   The ExamineOrGoTo action is used as the default action by the Parser; it is
 *   not an action the player can directly command. If the player enters a room
 *   (and nothing else) on the command line, and the room is known to the player
 *   character, and the player character is not already in the room, the command
 *   will be treated as a GoTo action; otherwise it will be treated as an
 *   Examine action.
 */

And then I modified GoTo to include a room’s exits in the Action’s scope list.

2 Likes

Ah, cool. Thanks for looking it up.

Amusingly, the default behavior of that parser is similar to one of the alternatives I considered: VerbRule(NounAsVerb) singleDobj : NounAsVerbAction [...] handles “bare” object names as a call to NounAsVerbAction with the given object as the direct object of the verb; the action definition includes all locations in scope but otherwise inherits default scope behavior; Room.dobjFor(NounAsVerb) handles NounAsVerb with replaceAction(TravelVia, gDobj) and Thing.dobjFor(NounAsVerb) handles it with replaceAction(Examine, gDobj).

I think I prefer the idea of “falling through” to having non-Room Things handled as if NounAsVerbAction wasn’t defined at all, but having a “default verb” for Things makes a kind of sense in that it seems like something that would be vaguely “intuitive” to anyone who’s played, for example, a point and click or choice-based game.

1 Like

Adding another random comment/tip about coding nontraditional exits:

If you want to override/change the behavior of the “compass” actions (north, south, and so on) you can’t get there via something like:

// THIS DOESN'T WORK.
modify NorthAction
        // THIS IS NOT A WORKING EXAMPLE, DO NOT COPY IT.
        verifyAction() {
                reportFailure('You have vowed to never travel north. ');
                exit;
        }
;

…because of peculiarities of how directional movement commands are parsed—NorthAction, SouthEastAction, exist primarily for ease of reference rather than being needed for implementing the commands.

Instead you have to do something like:

modify playerActionMessages
        cannotGoThatWayCompassDisabled = 'Directional movement disabled. '
;

modify TravelAction
        verifyAction() {
                local dir;

                // If we're not a travel action with a compass direction,
                // just use the default behavior.
                dir = getDirection();
                if((dir == nil) || !dir.ofKind(CompassDirection)) {
                        inherited();
                        return;
                }

                // Report the fact that compass travel is disabled.
                reportFailure(&cannotGoThatWayCompassDisabled);
                exit;
        }
;

…although probably with a better failure message.

Just a PSA because I spent way too long backtracking through the adv3 source to figure this out.

2 Likes

Yeah, I had my time fuddling to eventually figure out that NorthAction etc. are primarily conveniences for replaceAction…