Symconn (Symmetrical Connectors)

Overview

The purpose of the Symconn extension is to simplify the process of setting up travel connectors when they are symmetrical (that is when travel from room1 in a certain direction leads to room2, and travel in the reverse direction from room2 leads to room1), which is by far the most common case in most IF. If this extension is included then defining a direct connection from one room to another will automatically lead to reverse connection being defined from the second room to the first. This extension also defines a number of classes to simplify defining two-way travel connectors and doors, so that, for example, a door can be defined using one object instead of two.


New Classes, Objects and Properties

In addition to a number of properties intended purely for internal use, this extension defines the following new classes, objects and properties:

Usage

Include the symconn.t file after the library files but before your game source files.

When the symconn extension is present, it will automatically set up reverse connections between rooms. In the standard library, if you set hall.east to study, say, you would also have to set study.west to hall if you wanted the player character to be able to move back and forth between the two rooms (as most of the time you probably would). The symconn extension looks after this for you, so that if you set hall.east to study, this extension will then automatically set study.west to hall, unless you have already defined study.west to be something other than nil.

In cases where you don't want a connection between rooms to be symmetrical, you need to define the reverse connection back from the second room to be something other than nil. In such cases you could define the reverse connection to be a different room (if you were trying to create a confusing maze, for example), or you could define it to be a single-quoted or double-quoted string explaining why travel back that way isn't possible, or you could define it to be noExit, which then mimics there being no connector back in that direction. So, for example, if it was possible to go down from the cliff to the ravine, but not to go up from the ravine to the cliff, you could define cliff.down = ravine and ravine.up = noExit (or else perhaps a string explaining that the cliff was too steep to climb.)


Symmetrical Travel Connectors

This extension also defines the SymConnector class and its two subclasses, SymPassage and SymDoor.

SymConnector is a type of TravelConnector (from which it descends by inheritance). A SymConnector can be traversed in both directions, and defining a SymConnector on a direction property of one room automatically attaches it to the reverse direction property of the room to which it leads. Otherwise, a SymConnector behaves much like any other TravelConnector, and can be used to define travel barriers or the side-effects of travel in much the same way. For example, we could define:

 defile: Room 'Narrow Defile'
        
    east: SymConnector -> precipice
    "You just manage to squeeze through. "     
    {        
        
        canTravelerPass(traveler)
        {
            return traveler.getCarriedBulk < 4;                
        }
        
        explainTravelBarrier(traveler)
        {
            "You can't squeeze through carrying so much stuff. ";
        }
    }
 

This would cause the same SymConnector (the very same object, not just a copy of it) to be attached to the west property of the precipice room, so that travelling east from defile or west from precipice would be subject to precisely the same restriction on the maximum bulk carried by the traveler, and would result in exactly the same "You just manage to squeeze" through message being displayed if travel were allowed in either direction. Travelling west from precipice would then take the traveler back to the defile. (If you needed to vary the details of what happened according to the direction of travel you could always test the value of traveler.getOutermostRoom to determine which end traveler was starting from).

Internally a SymConnector defines a room1 property and a room2 property, room1 and room2 being the two rooms reciprocally connected by the SymConnector. At preinit the symconn extension automatically sets the room1 property of a SymConnector to the room one of whose direction properties is attached to that SymConnector, and the room2 property of the SymConnector to the destination of the SymConnector. In the example about the -> precipice in the template defines the destination property (directly) and hence the room2 property (indirectly). So, to set up a SymConnector you'd typically use the coding pattern illustrated above: attach it to the direction property of one room and leave the extension to set up the reverse connection from the other room.

The SymDoor class lets you define a door using one object instead of the usual two. Using the standard adv3Lite library you'd typically set up a door between two rooms like this:

 redRoom: Room 'Red Room'
   "A door leads south. "
   
   south = blackDoor1
 ;
 
 + blackDoor1: Door 'black door'
    "It's black. "
    otherSide = blackDoor2
 ;
 
 greenRoom: Room 'Green Room'
   "A door leads north. "
   north = blackDoor1  
   
+ blackDoor2: Door 'black door'
    "It's black. "
    otherSide = blackDoor1
;   
 

Using the SymDoor class this could be reduced to this:

 redRoom: Room 'Red Room'
   "A door leads south. "
   
   south = blackDoor
 ;
 
 blackDoor: SymDoor 'black door'
    "It's black. "
    room2 = greenRoom
 ;
 
 greenRoom: Room 'Green Room'
   "A door leads north. "
 ;    
 

Or even this:

 redRoom: Room 'Red Room'
   "A door leads south. "
   
   south: SymDoor  
   {
    -> greenRoom 'black door'
    "It's black. "
   }
 ;
  
 greenRoom: Room 'Green Room'
   "A door leads north. "
 ;    
 

A SymDoor inherits all the behaviour of a SymConnector and works in much the same way, but in addition it's a physical object that's present in both the locations it connects. A SymDoor is also a MultiLoc, which the symconn extension automatically places in its two locations at preinit.

There are a few limitations, however. The main one is that a SymDoor must have the same locking mechanism on both sides (if you want it to be lockable). You can define the lockability property of a SymDoor just as you can for a Door, but since a SymDoor represents both sides of the door, both sides have to behave in the same way. Likewise both sides of a SymDoor must have the same name ('black door' in the example above). You can, however, give the two sides of a SymDoor different descriptions if you wish by defining its room1desc and room2desc properties instead of its desc property (as you would expect, room1desc and room2desc will then be the descriptions of the door as seen from room1 and room2 respectively, where room1 and room2 have the same meaning as they have on a SymConnector).

It's sometimes convenient to refer to a door by the direction it leads in (e.g. "The west door" or "The north door"). The symconn extension takes care of this for you automatically. For example, the black door in the example above can be referred to by the player as 'south door' when the player character is in redRoom and as 'north door' when the player character is greenRoom and the game will know which door is meant, without the game author having to take any steps to make this happen. If, however, you want to suppress this behaviour on a particular SymDoor, you can do so simply by overriding its attachDir property to nil (attachDir is a method that works out which direction property a SymDoor is attached to in the player character's location, which is used by the DirState State object to add the appropriate direction name adjectives, such as 'north', to the SymDoor's vocab).

Finally, a SymPassage is very like a SymDoor, except that it can't be opened or closed (at least, not via player commands). The SymPassage class can be used to define passage-like objects such as passageways and archways that connect one location to another. A SymPassage is otherwise defined in exactly the same way as a SymDoor; from a player's perspective it is functionally equivalent to a Passage, the differences from the game author's point of view being that it can be defined using one game object instead of two and that this extension automatically takes care of setting up the connection in the reverse direction.

The SymPassage class does define the isOpen property which is true by default. The symconn extension makes no use of this property on SymPassage, but game could use it to simulate a passage that starts out blocked, e.g.:

 cave: Room 'Cave'
    "A passage runs off to the west. "
    west: SymPassage
    {
       -> cave2
       'passage; narrow'
       "<<isOpen>>It's quite narrow, but you should be able to squeeze through. <<else>>It's blocked by a fall of rock. <<end>>"
       
       isOpen = nil
       canTravelerPass(traveler) { return isOpen; }
       
       explainTravelBarrier(traveler)
       {
           "The passage is blocked by rubble from a rockfall. ";
       }
    } 
 

Then at some later point in the game when the player managed to unblock the passage you would call cave.west.makeOpen(true);

Note that SymPassage is a subclass of SymConnector (and MultiLoc) and the superclass of SymDoor, but that SymPassage does not inherit from Passage, or SymDoor from Door.

Further Considerations

The symconn extension doesn't enable you to do anything you couldn't do without it; it just makes setting up (most) connections a bit less work. The main upside is that it gives you a bit less typing to do; a corresponding potential downside is that it may make your code less clear, since some of the connections between rooms will be implicitly added by the extension rather than shown explicitly in your code (for example, if you define hall.east as study and leave this extension to define study.west = hall, then when you come to look at your code later it may not be apparent that there's an exit west from the study to the hall (especially if the definitions of the study and the hall are some way apart in your code). You could, of course, add a comment to that effect, but then you might as well have defined study.west = hall in your code. A subsidiary advantage of using this extension is that should you forget to define a connection back (e.g. you define hall.east as study but forget to define study.west as hall), this extension will take care of it for you. The corresponding disadvantage is that you'd have to remember to explicitly define study.west = noExit if for some reason you didn't want the connection back, although since this is unlikely to occur very often, in this instance the advantage might clearly outweigh the disadvantage.

Being able to define doors with one (SymDoor) object instead of two (Door) objects may well be a welcome saving of labour, especially if the majority of doors in your game are the same both sides, as may often be the case. To make your code clearer, you may prefer to define the room1 and room2 properties on your SymDoors (and SymPassages) explicitly; for example, instead of:

blackDoor1: SymDoor 'black door'
    "It's black. "
    room2 = greenRoom
 ; 
 

You could write:

blackDoor1: SymDoor 'black door'
    "It's black. "
    room1 = redRoom
    room2 = greenRoom
 ; 
 

Or, using an alternative form of the SymPassage/SymDoor template:

blackDoor1: SymDoor 'black door' @redRoom @greenRoom
    "It's black. "    
 ; 
 

Which may make it clearer in your code which two rooms the black door is connecting. Note, however, that if you do this you must also define redRoom.south = blackDoor (and you could also optionally define greenRoom.north = blackDoor if you wished for the sake of clarity). Or, more generally, if you explicitly define the room1 property on a SymDoor or a SymPassage, you must (normally) also remember to assign the SymDoor or SymPassage to a direction property of room1.

A possible exception to this, where you would have to define the room1 and room2 properties explicitly without assigning the SymDoor to a direction property of room1 would be if you were trying to model the presence of more than one door leading in the same direction. For example, suppose there were two doors, a red door and a green door both leading west from the same room. One way you could model this might be as follows:

 livingRoom: Room 'The Living Room' 
    "There are two doors on the west side of the room, one red and
     the other green. "
        
    west: TravelConnector
    {
        getDestination(origin)
        {
            switch(pcRouteFinder.currentDestination)
            {
            case redRoom:
                return redRoom;
            case greenRoom:
                return greenRoom;
            default:
                return nil;
            }
        }
        
        execTravel(actor, traveler, conn)
        {
            local dest = getDestination(traveler.getOutermostRoom);
            if(dest == redRoom)
                redDoor.execTravel(actor, traveler, conn);
            else if(dest == greenRoom)
                greenDoor.execTravel(actor, traveler, conn);
            else            
            {
                "There are two doors to the west, a red one and a green one. ";
                askChooseObject(GoThrough, DirectObject, 'Which one do you want
                    to go through? ');
            }
        }
    }
    
    
;

redDoor: SymDoor 'red door' @livingRoom @redRoom
;

greenDoor: SymDoor 'green door' @livingRoom @greenRoom
;
 

Here, most of the complication on the TravelConnector defined on livingRoom.west is simply to allow the routefinder to find a route to the red room and the green room through the red and green doors. If you weren't concerned about that, you could simply define:

   west()
   {
       "There are two doors to the west, a red one and a green one. ";
        askChooseObject(GoThrough, DirectObject, 'Which one do you want
        to go through? ');
   }
 
 

But then GO TO RED ROOM and GO TO GREEN ROOM might fail to work as expected.

Overall it's up to you as a game author to weigh up the pros and cons of various different approaches, including whether or not to use this extension, and then make your own decision about how you want to work.

This covers most of what you need to know to use this extension. For additional information see the source code and comments in the symconn.t file.