Table of Contents |
Extensions >
Symconn
Symconn (Symmetrical Connectors)
Overview
The purpose of the Symconn extension is to simplify setting up two-way travel connectors, that is single objects that represent an abstract travel connection, passage, door, or stairway between two locations (rather than needing to do this with pairs of objects, as in the standard library). This extension can also automate the creation of reverse connections;if this extension is included then defining a direct connection from one room to another will automatically lead to the creation of a reverse connection from the second room to the first, unless the game already defines a connection back or the reverse direction property has already been defined on the second room.
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:
- Classes: SymConnector, SymPassage, SymPathPassage, SymDoor, SymStairway.
- Objects: noExit.
- Properties/methods on SymConnector:
room1
,room2
,byRoom()
,inRoom1
, inRoom2,room1Dir
,room2Dir
,dirName
- Properties/methods on SymPassage:
room1Desc
,room2Desc
,room1Vocab
,room2Vocab
,attachedDir()
. - Properties/methods on SymDoor:
room1Lockability
,room2Lockability
. - Properties/methods on SymStairway:
upperEnd
,lowerEnd
,inUpper
,inLower
,upOrDown
,byEnd()
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 where game code doesn’t already do so.
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
or you’ve
defined some other way back from room2 to room1.
In cases where you don’t want a connection between rooms to be
symmetrical, you need to define the connection back from the second room
yourself (e.g. if you want the connection from the second room to the
first to be to the southeast when the connection from the first room to
the second is to the west). 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). If you want there to be no way from the
second room to the first, you need to define something on the reverse
direction of the second room (for example, on the
east
property of a second room that’s to the
west of the first). You could define this 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.)
If you want to stop the symconn extension setting up reverse connections
altogether (for example, because you want to use the classes symconn
defines without this automatic reverse connection behaviour) you can
override the autoBackConnections on the Room class to nil. Setting
autoBackConnections
on an individual room to
nil will prevent symconn from automatically creating any reverese
connections back to that room. (This is defined on the Room class, since
it’s the Room class that’s responsible for setting up these automatic
reverse connections).
Symmetrical Travel Connectors
This extension also defines the SymConnector class and its three subclasses, SymPassage, SymDoor and SymStairway.
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.
However, you can also use a SymConnector
(including a SymPassage, SymDoor or
SymStairway., for which see below) to set up an
asymmetric connection, by defining a different direction from the
obvious reverse one on the second room to point back to the same
SymConnector. For example, if you have room1’s
west
property and room2’s
southeast
property point to the same
SymConnector, this extension won’t then attempt to make room2’s
east
property do so. If you don’t want there
to be any way back from room2 to room1 then there’s little point using a
SymConnector at all; you’d be better off using an ordinary
TravelConnector to set up the one-way
connection.
A SymPassage is a kind of SymConnector representing a
physical object that’s present in both the locations it connects. A
SymPassage is also a MultiLoc, which
the symconn extension automatically places in its two locations at
preinit. It 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.
There are a number of ways a SymPassage object can be defined. The first (which is particularly useful if you want the passage to be a nested objectas in the cave example below) is to set the direction property of the first room to point to it and then to set its own destination property to point to the second room, like so:
firstRoom: Room 'First Room'
"A narrow passage leads east. "
east = myPassage
;
myPassage: SymPassage 'narrow passage'
destination = secondRoom // or we could the template to just have -> secondRoom
;
secondRoom: Room 'Second Room'
"A narrow passage leads west. "
;
This will work, since the extension will take care of locating the
passage in both the rooms and pointing the second room’s west property
back to the same passage. But, particularly if the three object
definitions are quite widely separated in game code, it may be less than
obvious later what the connections are. The recommended coding pattern,
therefore is to set the passage’s room1
and
room2
properties explicitly (which can be done
via a template, as shown below) and to set the back connection on the
second room explicitly, like this:
firstRoom: Room 'First Room'
"A narrow passage leads east. "
east = myPassage
;
myPassage: SymPassage 'narrow passage' @firstRoom @secondRoom
;
secondRoom: Room 'Second Room'
"A narrow passage leads west. "
west = myPassage
;
It’s possible that the passage looks the same from both ends, in which
case you can just set its desc
property in the
normal way, but the two ends of passage may look different, in which
case you can define a separate room1Desc
and
room2Desc
to define how it looks from each end
(which is another reason you might want to define the
room1
and room2
properties explicitly, so you can see which is which).
The change in appearance might, however, lead to a change in the way the
passage should be referred to; for example the passage might be narrow
at one end but broaden out at the other. To handle this you can use the
room1Vocab
and/or
room2Vocab
properties explicitly. You don’t
need to define both, since if you define one, the extension will use the
passage’s initial vocab
property for the
other. For example:
firstRoom: Room 'First Room'
"A narrow passage leads east. "
east = myPassage
;
myPassage: SymPassage 'narrow passage; leading east' @firstRoom @secondRoom
room1Desc = "The passage leading east is quite narrow, but looks like it broadens out further along. "
room2Desc = "The passage leading west is quite broad, but looks like it narrows further along. "
room2Vocab = 'wide passage; broad leading west'
;
secondRoom: Room 'Second Room'
"A wide passage leads west. "
west = myPassage
;
Having to define two description properties where the descriptions don’t vary all that much feel like a lot of busywork. An alternative is to define simply the one desc property and vary it using embedded expressions (the << >> syntax). SymConnector and its descendants define a number of properties and methods to help with this. The inRoom1 and inRoom2 properties are true if the player character is in room1 or room2 respectively. The room1Dir and room1Dir properties give the direction of travel (as a Direction object) the player character must go in to traverse this room from room1 and room2 respectively, while the dirName property gives the name of that direction from the perspective of the player character’s current location (assuming this is either room1 or room2. This would enable us to define the myPassage object aboce as:
myPassage: SymPassage 'narrow passage; leading east' @firstRoom @secondRoom
"The passage leading <<dirName>> is quite <<inRoom1 ? 'narrow' : 'broad'>> but looks like it <<inRoom1 ? 'broadens out' : 'narrows'>> further along. "
room2Vocab = 'wide passage; broad leading west'
;
This can be compressed a bit further by using the byRoom method for the desc property thus:
"The passage leading <<dirName>> is quite <<byRoom(['narrow', broad'])>> but looks like it <<byRoom(['broadens out', 'narrows])'>> further along. "
Note that the byRoom() method takes a single argument that should be a list of two strings, the first to be displayed for room1 and the second for room2. This allows all alternative method of achieving the same result using a string template thus:
"The passage leading <<dirName>> is quite <<['narrow', broad']) by room>> but looks like it <<['broadens out', 'narrows'] by room>> further along. "
The use of these techniques is not restricted to the desc property; they
can be employed on any property/method of a SymmConnector (such as
travelDesc
) you may want to vary according to
the room or direction of travel (travelDesc is invoked while the player
character is still in the starting location, so if the player is in
room1 travel is towards room2 and vice versa). Note that the byRoom()
method is also defined on Thing, where is simply returns an empty
string. Game code could thus use it elsewhere than with the symconn
extension for whatever purpose a game author deems convenient, passing a
list of whatever length is useful for the purpose.
Note, however, that all these techniques assume that the player character is either in room1 or room2 when viewiing the SymConnector in question. This may mot be the case if the SymCommector is visible from a remote location that’s part of a SenseRegion also containing the player character. This shouldn’t matter too much provided suitable care is taken in defining the SymConnector’s remoteDesc(pov) appropriately.
The SymPassage
class also defines the
isOpen
property which is true by default. The
symconn extension makes no use of this property on
SymPassage
, but a 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);
A SymPathPassage is a kind of SymPassage representing a path, road, or track (the kind of thing you might find in an outside location as opposed to a passage indoors). It behaves exactly like a SymPassage except that players can also GO ALONG, GO UP, GO DOWN, or FOLLOW it, all of which have the same effect, namely of travelling via the SymPathPassage.
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' @redRoom @greenRoom
"It's black. "
;
greenRoom: Room 'Green Room'
"A door leads north. "
north = blackDoor
;
You don’t actually need to define north =
blackDoor
on greenRoom
here, since the
symconn extension will do this for you, but, as mentioned above, you
might find it easier to understand what’s going on in your code if you
do.
Since SymDoor inherits from SymPassage, you can set it up in just the same way. In particular you can use the room1Desc, room2Desc, room1Vocab and room2Vocab properties just as you would on a SymPassage. In addition, on a SymDoor you can define room1Lockability and room1Lockability properties to make the lockability work in different ways on each side of the door (although if you want the same lockability on both sides of the door you can just override its lockability property). The one thing you can’t do is define different keys to work on the different sides of the door. If you really wanted to do that you’d be better off using the regular Door class to define the door as two different objects.
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).
The SymStairway class lets you define a stairway using one object
instead of the usual two (a paired StairwayUp and StairwayDown). In
essence you can define a SymStairway in just the same way as a
SymPassage, using the same properties to customize its
two ends if and as desired. In addition, however, a SymStairway needs to
know which is its upper end and which its lower end, which it does via
its upperEnd
and
lowerEnd
properties (which should contain the
rooms at its upper and lower ends respectively). Provided the
SymStairway is attached to the up
or
down
property of at least one of the rooms it
connects, the extension can work out which end is which for itself and
there’s no need for you to specify this in your game code. The extension
can also do this if the connection to the up
or down
property is indirect, via an
asExit()
macro, e.g., up
asExit(west)
where the SymStairway is directly attached to the
west
property. If, however, the SymStairway is
only reachable via a compass direction (or in or out) then you’ll need
to define its upperEnd
and
lowerEnd
properties yourself.
On the subject of the asExit() macro, if you define
up asExit(west)
on the lower room, it’s
generally a good idea to define down
asExit(east)
(or whatever direction takes you back down) on the
upper room, since if the player reaches the upper room with the command
UP s/he’ll expect be to able to reverse that travel with the command
DOWN. For that reason, if this extension finds an UnlistedProxyConnector
(as Exit()) on the up or down property of a room, it will ensure that
there’s a matching UnlistedProxyConnector on the corresponding down or
up property of the destination room, unless that property has been
already overriden in game code or there’s no other way back down defined
on the destination room. Normally this will be what you will want, but
if you don’t (for example, because there are several ways down from the
destination room), you can block this by defining the down direction on
the upper room (say) as noExit
or else,
perhaps, as a string explaining why DOWN is not an appropriate
direction, for example, dpwn = 'From here staircases
lead down to north, east and west; which way do you want to go?'
.
In addition to the shortcut methods for writing descriptions on other kinds of SymCommector, SymStairway defines inUpper and inLower properties which evaluate to true or nil depending on whether the player character is at the upper end or lower end of the SymStairway in question. The upOrDOwn evaluates to ‘down’ if we’re at the upper end and ‘up’ if we’re at the lower end (these being the ways the stairway will run from either end). Finally, the byEnd() method works analogously to the byRoom() method, taking as its argument a list of two strings, and returning the first if we’re at the upper end of the stairway and the second otherwise. So for example one could define:
testStairs: SymStairway 'staircase; ; stairs' @startroom @orangeRoom
"The stairs <<['climb', 'descend'] by room>> <<upOrDown>> to the <<dirName>>. "
travelDesc = "{I} {run} <<upOrDown>> the stairs. "
;
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
, or
SymStairway
from
StairwayUp
or
StairwayDown
.
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.
So one way to use this extension might be to define all directional connections you want on each room explicitly (so it’s clear in your code where every direction leads) but to take advantage of the SymConn, SymPasaage and SymDoor classes to avoid having to define pairs of objects where a single object can do the job perfectly well, while regarding the extension’s ability to supply any missing back connections as an added bonus in cases of accidental omission.
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.
Adv3Lite Manual
Table of Contents |
Extensions >
Symconn