Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

TADS: 'go to <location>', implementation (LONG)

3 views
Skip to first unread message

Lars Joedal

unread,
Nov 1, 1993, 2:53:03 AM11/1/93
to
/* ************************************************************ */
/* TADS implementation of "go to <location>", version 1.0 */
/* This code is public domain. */
/* Please report any bugs to */
/* joe...@dfi.aau.dk */
/* Comments are also welcome. */
/* ************************************************************ */

/* Along with this code you should have received a file with
* documentation.
* The following comment is NOT the (full) documentation, it's
* just a terse list of the relevant methods, properties, verbs,
* and classes.
*/

/* Changed methods & properties:
* doorway.destination
* chairitem.roomAction
* New methods & properties you should know of:
* global.justTesting - flag
* movableActor.isWalking - method
* movableActor.stopWalking - method
* New methods & properties you may want to know of:
* room.gtRoomCheck( actor ) - method
* movableActor.gtHasLight - flag
* movableActor.gtMessage( number ) - method
* Other new methods & properties (which you don't have to think of):
* Lots of them, all with the prefix 'gt'. If you have changed the
* method Me.travelTo you should take a look at the method
* moveableActor.travelInDir( dirNo, verboseDesc ).
*
* New verbs:
* goToVerb
* gtLimitVerb
*
* New classes:
* lostroom - a room that can't be gone to, from, or through.
*/

initGoTo: function; // Initializes some variables and lists.
findExitList: function; // Finds exits from current location.
findRoomFromObstacle: function; // Finds the room that an obstacle leads to.
findPath: function; // Finds a way to to a room, if possible.
nowGoToRoom: function; // Leads an actor along a path, one step at a time.


/* New verbs:
* goToVerb: The verb to 'go to' a location.
* gtLimitVerb: A verb to control how long the path can be without the
* player being asked for confirmation first.
*/

goToVerb: deepverb
verb = 'go to'
sdesc = "go to"
doAction = 'GoTo'
isTravelVerb = true
dirNames = [ 'north' 'northeast' 'east' 'southeast' 'south'
'southwest' 'west' 'northwest' 'up' 'down' ]
dirProps = [ &north &ne &east &se &south &sw &west &nw &up &down ]
dirVerbs = [ nVerb neVerb eVerb seVerb sVerb swVerb wVerb nwVerb
uVerb dVerb ]
validDo( actor, obj, seqno ) = {
if ( obj = nil )
return( nil );
else if ( self.validDoList <> nil )
/* If the goToVerb.validDoList has been initialized (normally
* by the function initGoTo) it is assumed that it only contains
* legal objects.
*/
return( obj.isseen );
else
/* If the list is nil the default behaviour is used: Allow only
* real rooms (not "nestedroom"s) and only if they have been
* seen.
*/
return( obj.isroom and obj.isseen and not obj.isnestedroom );
}
validDoList = nil // Not to be confused with the empty list. If the
// list is nil the system does not restrict the
// search to the list.
gtAskLimit = 10 // If the walk will take more than this number of
// moves the player will be asked for confirmation.
// If this property is set to nil the player will
// never be asked. Change it with a call of the
// function initGoTo if you want another value.
;

gtLimitVerb: deepverb
sdesc = "gotolimit"
verb = 'gotolimit'
action( actor ) = {
if (goToVerb.gtAskLimit = nil)
"Currently there is no limit on the number of moves without
confirmation. ";
else {
"Current limit of moves without confirmation is ";
say( goToVerb.gtAskLimit );
". ";
}
}
doAction = 'GtLimit'
;

/* New class: lostroom is a type of room that can't be gone to or from
* with the 'go to' verb. Generally, to be used for mazes - there is
* no fun of a maze if the player can just type 'go to maze exit' or
* something like that.
*/
class lostroom: room
roomCheck( v ) = {
if ( v = goToVerb ) {
// This method is called by the normal system when the player
// gives the command. Meaning that the player stands in this
// room and is trying to walk out.
"Find %your% own way! ";
return( nil );
}
else
pass roomCheck;
}
verDoGoTo( actor ) = {
// The player is GOing TO this room.
"%You%'ll have to find << self.thedesc >> on %your% own. ";
}
gtRoomCheck( actor ) = nil // Do not allow this room to be used on
// 'go to' walks.
;

/* Changes to existing classes and objects: The classes movableActor and
* room have been changed and extended to accomodate the new goToVerb.
* The classes darkroom, nestedroom, doorway, and chairitem have also
* been changed, but only slightly. And basicNumObj has been extended
* to recognize gtLimitVerb.
*/

modify movableActor
gtTravelInDir( dirNo, verboseDesc ) = {
/* The method gtTravelInDir is a kind of new "travelTo" method,
* tailored to the 'go to' verb. The actor takes a step in the
* direction specified. If successful, true is returned, nil
* otherwise. Suitable messages are shown. If verboseDesc is
* true a full lookaround is done, otherwise only a terse one.
*/
local nextLoc, dirVerb;

"\b(Going ";
say( goToVerb.dirNames[dirNo] );
")\n";
nextLoc := self.location.( goToVerb.dirProps[dirNo] );
if (nextLoc <> nil and nextLoc.isobstacle)
nextLoc := findRoomFromObstacle(self, nextLoc);

/* Now nextLoc contains the next location (unless something unusual
* has happened).
*/
if (nextLoc = nil) {
/* For some reason or another the actor is NOT able to go by
* the laid-out route (e.g., a non-player character could have
* locked a door). Give the player a message to the effect
* and stop walking.
*/
self.gtMessage(4);
exit;
}
/* Normal move.
* Since a lot of these moves are done in a single command TADS
* doesn't get a chance to do all the normal checks for each move.
* Instead the checks are done here explicitly.
* To make the whole thing fit smoothly into the rest of the game
* each move is simulated as a normal movement command.
*/
dirVerb := goToVerb.dirVerbs[dirNo];
if ( not self.roomCheck( dirVerb ) )
exit;
self.actorAction( dirVerb, nil, nil, nil );
self.location.roomAction( self, dirVerb, nil, nil, nil );
if ( not (self.location.islit or nextLoc.islit) ) {
darkTravel();
return(nil);
}
/* All the checks are done. Now the actor can be moved. This
* is done in a way very similar to the Me.travelTo method.
* There are differences, though:
* - the descriptions on the route are short, even if the player
* is playing in VERBOSE mode. Long descriptions all along would
* just be confusing. Only at the final location is the long
* description shown.
* - there is no reason to set the room.isseen flag since only
* already seen rooms can be part of the path
*/
self.location.leaveRoom(self);
self.moveInto(nextLoc);
self.location.lookAround(verboseDesc);
return(true);
}
gtMessage( number ) = {
/* This method is called when:
* - the path is found (to allow for a message to that effect)
* - the path could not be found (report the error)
* - the found path turned out to be unusable (see gtTravelInDir)
*/
switch( number ) {
case 0: // No error. No message
break;
case 1: // Just not found.
"Sorry, I couldn't find that location from here.\n";
break;
case 2: // No exits from here.
self.location.noexit;
break;
case 3: // The starting location is dark. Being more hard on
// this for 'go to' travelling than normal travelling
// (see darkroom.gtRoomCheck) we count this as an error.
darkTravel();
break;
case 4: // Panic! The actor could not walk along the specified
// path!
"Something blocks %your% way!\n";
break;
default: // We should never end here, but just in case...
"Sorry, something went wrong.\n";
break;
}
}
gtWalkTurn = -1 // Holds current turn count if actor is walking.
gtAllowDifference = nil // "true" allows gtWalkTurn to be off by one.
gtStopWalking = nil // "true" means the current walking should stop NOW.
isWalking = {
/* Returns true if the actor is currently walking, nil otherwise.
* "Currently walking" means that a 'go to' command is active and
* that the last step has not been taken. Thus if this method is
* called when the fuses and daemons are run for the last time (after
* the last step) nil is returned.
*/
local countDif;

countDif := self.gtWalkTurn - global.turnsofar;
if (self.gtAllowDifference) countDif++;
return( countDif >= 0 );
}
stopWalking = {
/* Stops the current walk.
* Example: A fuse decides that the player's lamp is now running
* out. So it tells the player this and turns off the lamp. Since
* this will in general hinder walking ('go to' walking as well as
* normal movement commands) the fuse calls Me.isWalking to see if
* the player happens to be in the middle of a walk. It finds out
* that the player is indeed walking, and calls Me.stopWalking.
* The fuse does NOT end with an exit or abort statement, which is
* quite normal as other fuses should have the chance to burn down,
* too.
* Summary: Calling this method is the proper way to stop the
* player from walking, as opposed to using exit or abort.
*/
self.gtStopWalking := true; // This stops the main 'go to' loop (in
// the function nowGoToRoom).
}
;

modify class room
/* Expand the definition of a room, so it responds to the goToVerb. */
verDoGoTo( actor ) = {
if (self = actor.location)
"But %you're% here already!\n";
}
doGoTo( actor ) = {
local pathList;
local rem, cur, tot, i;

/* To speed up operations with "darkroom"s we here find out whether
* the actor has a light source, so it will not have to be considered
* each time a darkroom.gtRoomCheck is tested.
*/
rem := global.lamplist;
tot := length( rem );
actor.gtHasLight := nil; // This property holds the information of
// whether the actor has a lamp or not.
// It is only to be used in routines special
// for the 'go to' verb!
for( i := 1; i <= tot; i++ ) {
cur := rem[i];
if ( cur.islit and cur.isIn(actor) ) {
actor.gtHasLight := true;
break;
}
}

/* Now find the path. */
pathList := findPath( actor, self );
if ( pathList <> nil ) {
local ok, lim;

lim := goToVerb.gtAskLimit;
if (lim <> nil and length(pathList) > lim ) {
"To go there will take ";
say(length(pathList));
" turns. Do you want to go there?\ ";
if (yorn() = 1) ok := true;
else ok := nil;
}
else
ok := true;
if ( ok ) nowGoToRoom(actor, pathList);
}
}
gtRoomCheck( actor ) = {
/* This method is used by the 'go to' verb to check out a room. If
* the room objects it should return nil, true otherwise.
* The method is meant to work somewhat like a combined version of
* the normal "roomCheck" and "roomAction" methods. The differences
* are:
* - Only one method.
* - Only called when the verb in question is 'go to'.
* - No global variables must be changed. Whereas a normal "room-
* Action" can kill the player, abort the current command, etc.
* if it wants to do that, this method is only allowed to express
* is dissatisfaction by returning nil. This is because the player
* has not actually done the thing, the system is just testing if
* it would be a possible (and fair) thing to do.
* - The method may produce output if it wants to, but the output
* is discarded. This can be useful when calling some other
* "normal" methods that may produce output (they are not allowed
* to change any variables, though).
* - The method can't count on the player being in this room. This
* affects the way a dark room must check for presence of light
* sources, for example (see modifications of darkroom).
* - On the other hand this method CAN count on the actor having a
* property gtHasLight that tells whether the actor is carrying a
* light source or not (this is only done to make light-searching
* faster).
*
* Thus this method can block the 'go to' command from using this
* room. The method is only called in the test-phase, though.
* When/if a path is found and the player walks toward his/her
* destination, it will be the normal "roomCheck" and "roomAction"
* that are in charge, and the steps will be done as if the player
* had walked around in the normal way.
*/
return( true ); // Answer for standard rooms is "no problem"
}
isroom = true
cantReach( actor ) = {
/* Called if the room was specified, but it was not accepted. This
* generally means it has not been seen, or it is a nestedroom.
*/
"Sorry, but I fail to see where %you% %are% trying to go. ";
}
;

modify class darkroom
/* It is not allowed to walk through a dark room with a 'go to <location>'
* command. This is a bit stricter than for normal walk (where the
* walk is allowed if either the starting point or the destination is
* lit), but it ensures the player will see the route s/he is using.
*/
gtRoomCheck( actor ) = {
/* Don't allow 'go to' to use this room if it is dark. A special
* check is needed, partly for speed, partly because the actor is
* not necessarily in the room (so self.islit doesn't see any light
* source the actor is carrying).
*/
if ( actor.gtHasLight ) return( true );
else return( self.islit );
}
;

modify class nestedroom
isnestedroom = true
;

modify doorway
/* Change the standard of 'destination' to recognize the flag
* global.justTesting. If global.justTesting = true no properties
* must be changed. There is nothing wrong in printing text, though.
*/
replace destination = {
if ( self.isopen ) return( self.doordest );
else if ( not self.islocked and not self.noAutoOpen ) {
if ( not global.justTesting ) {
self.isopen := true;
if ( self.otherside )
self.otherside.isopen := true;
}
"(Opening << self.thedesc >>)\n";
return( self.doordest );
}
else {
"%You%'ll have to open << self.thedesc >> first. ";
if ( not global.justTesting )
setit( self );
return( nil );
}
}
;

modify class chairitem
roomAction( actor, v, dobj, prep, io ) = {
/* Expanded roomAction that allows the 'go to' verb. If this
* method was not changed the player would get the error "You
* can't reach <location> from here!". Now the player instead
* gets the more suitable message "You don't get anywhere until
* you get out of that chair!" (when it is found that the chair
* has no exits).
*/
if ( v<>goToVerb )
pass roomAction;
// Else: Do nothing.
}
;

modify basicNumObj
verDoGtLimit( actor ) = {}
doGtLimit( actor ) = {
if ( self.value >= 1 ) {
"New limit of moves without asking for confirmation is ";
say(self.value);
". ";
goToVerb.gtAskLimit := self.value;
}
else {
"There is now no limit of moves without asking for confirmation. ";
goToVerb.gtAskLimit := nil;
}
}
;


initGoTo: function(useLists, extraList, askLimit)
{
/* Intializes a few variables relevant for goToVerb. Call this function
* in the preinit function.
* In fact, everything is set up so that this function does not HAVE
* to be called. Not calling it will produce the same results as calling
* initGoto(nil, nil, 10). Still, it's more clean to call it.
* If useLists = true the list goToVerb.validDoList is set to a list
* of all rooms in the game, excluding "lostroom"s and "nestedroom"s.
* If the parameter is false, the list is set to nil, meaning that
* TADS uses the old (version 1.x) method: All objects with matching
* vocabulary words are tried.
* If the abovementioned list is not enough the extraList parameter can
* be used. If extraList <> nil it is added to the list computed as
* above (only if useLists = true). Thus e.g. certain nestedrooms can
* be allowed by use of this parameter.
* The limit of how long a path can be without the system having to
* ask the player for confirmation is set by askLimit. 0 or nil means
* no limit.
*/

if (useLists) {
local roomList, lst, curRoom;

roomList := [];
curRoom := firstobj(room);
while (curRoom <> nil) {
/* "nestedroom"s are excluded because they normally can't be
* gone to with the normal movement commands, and besides they
* may not be real rooms. For instance, vehicles act more like
* normal game objects than like game locations.
*/
if ( not curRoom.isnestedroom )
roomList += curRoom;
curRoom := nextobj(curRoom, room);
}
if (extraList <> nil) {
// Remove any duplicates and then add the list to roomList.
lst := intersect(roomList, extraList);
roomList += extraList - lst;
}
goToVerb.validDoList := roomList;
}
else
goToVerb.validDoList := nil;

if (askLimit = nil or askLimit < 0)
goToVerb.gtAskLimit := nil;
else
goToVerb.gtAskLimit := askLimit;
}


findRoomFromObstacle: function(actor, obst)
{
/* Given the obstacle "obst" this function returns the room that is
* connected to the obstacle by the "destination" property. If a room
* was found that is returned, nil otherwise. Closed doors that can be
* opened without problems count as passed.
* The global variable global.justTesting is used to determine whether
* this is just a test (doors must not be opened, no text should be
* shown, etc.) or real. If globalJustTesting = nil doors are opened
* if necessary and possible, and suitable text is shown whether the
* function returns a room or nil.
*/
local loc, success;

if ( obst.isdoor and not obst.isopen ) {
/* Open the door if possible. */
if (not global.justTesting) "(Opening the door first) \ ";
outhide(true);
obst.verDoOpen(actor);
if ( outhide(nil) ) {
/* There has been some output, meaning that verDoOpen complained.
* If this is not just a test then repeat that error message (so
* the player can see it) and give up.
*/
if (not global.justTesting) obst.verDoOpen(actor);
loc := nil;
}
else if (not global.justTesting) {
/* No problem with opening the door. Do it! */
obst.doOpen(actor);
"\n";
loc := obst.destination;
}
else {
/* The door can be opened, but this is a test. Since we can't
* open the door (that would change the game's state) the room
* behind the door can't be found by the "destination" property.
* But if the door had been opened that should return the value
* of the property "doordest", so we evaluate that instead.
*/
loc := obst.doordest;
}
}
else {
// It's not a closed door.
if (global.justTesting) outhide(true);
loc := obst.destination;
if (global.justTesting) outhide(nil);
}
/* At this point "loc" contains what the obstacle leads to, if it could
* be passed, and nil otherwise.
* If it is a room or nil, return that. If it is a new obstacle, try
* to pass that. If it is something else: Strange; return nil to be on
* the safe side.
*/
if (loc = nil or loc.isroom) return( loc );
else if (loc.isobstacle) return( findRoomFromObstacle(actor,loc) );
else return( nil );
}


findExitList: function(actor, loc, searchNo)
{
/* Returns a list of exits from the room 'loc'. The order is:
* North, south, east, west, ne, nw, se, sw, up, down. If there is
* no exit in some direction the list contains 'nil' for that direction.
*/
local locList;
local i, listLength;
local newLoc;

global.justTesting := true; /* Signals that we don't want any permanent
* changes to happen when evaluating direction
* methods (e.g. evaluating 'destination' for
* an autoopening door will normally open the
* door).
*/
listLength := length( goToVerb.dirProps );
locList := [];
outhide(true); /* Some of the following evaluations may show text
* (specifically error messages when wrong directions
* are tested), so we turn off any output.
*/
for( i := 1; i <= listLength; i++ )
locList += loc.( goToVerb.dirProps[i] );
outhide(nil); // Turn on output again.

/* Clean up the list. This means removing locations already visited
* during this search and locations that the player haven't seen yet.
* And if a new 'location' turns out to actually be a door or another
* obstacle it needs special treatment.
*/
for( i := 1; i <= listLength; i++) {
newLoc := locList[i];
if (newLoc = nil)
continue; // No exit in that direction. Go on with next direction.
if (newLoc.isobstacle) {
newLoc := findRoomFromObstacle(actor, newLoc);
locList[i] := newLoc;
if (newLoc = nil) continue; // No room found.
}
/* So, there's a room. */
if ( (not newLoc.isseen) or (newLoc.gtSearchMark = searchNo) ) {
// This room is not interesting for this search.
locList[i] := nil;
continue;
}
/* Everything is turning out fine. The only thing that can go
* wrong now is that the room objects.
*/
outhide(true);
if ( not newLoc.gtRoomCheck( actor ) ) locList[i] := nil;
else newLoc.gtSearchMark := searchNo; // Mark the room as seen
// during this search.
outhide(nil);
}
global.justTesting := nil; // Back to normal state.

return( locList );
}


findPath: function(actor, toLoc)
{
/* Find a route from the actor's current location to "toLoc". It's
* assumed that both fromLoc and toLoc are rooms and that toLoc is
* not the actor's current location (actor.location <> toLoc). If a
* route was found that is returned as a list of directions, where
* 1=north, 2=ne, 3=east, 4=se, 5=south, 6=sw, 7=west, 8=nw, 9=up,
* 10=down, corresponding to the lists in goToVerb.
* If no route was found nil is returned.
*/
local fromLoc;
local roomList, newRoomList;
local pathFound, pathLength;
local failureType;

fromLoc := actor.location;
if (not fromLoc.islit) {
/* The 'go to' command can only use lightened rooms, that also
* counts for the initial location.
*/
actor.gtMessage(3);
exit;
}

/* Find new value of global.gtSearchNumber for this search. */
if (global.gtSearchNumber = nil) global.gtSearchNumber := 1;
else global.gtSearchNumber++;

/* Mark the start location as seen. */
fromLoc.gtSearchMark := global.gtSearchNumber;

/* Search any rooms that can be reached from the current room. Then
* search any new rooms that can be reached from these rooms, and so on.
* The search continues until the correct room is found or we run out
* of rooms to search.
*/
roomList := [] + fromLoc;
pathFound := nil;
pathLength := 0;
failureType := 0;
while( length(roomList) > 0 and not pathFound ) {
local i, roomListLength;

pathLength++;
/* For each room in the current roomList, find all accessible rooms
* and store them.
*/
newRoomList := [];
roomListLength := length(roomList);
for( i := 1; i <= roomListLength; i++) {
local j, lst, lstLength;
local thisLoc;

lst := findExitList( actor, roomList[i], global.gtSearchNumber );
thisLoc := roomList[i];
/* For each room in the list, store (in the room) how we got
* there, i.e. from what room did we come and by going in what
* direction.
*/
lstLength := length(lst);
for( j := 1; j <= lstLength; j++) {
local nextLoc;

nextLoc := lst[j];
if( nextLoc <> nil ) {
/* Make the new room point back to the old room. */
nextLoc.gtPreviousRoom := thisLoc;
nextLoc.gtPreviousDir := j;
if (nextLoc = toLoc) {
/* Yeah, the room has been found!!! */
pathFound := true;
break; // No reason to search any further.
}
/* Since we got here it was not the right room. Put
* this room in the list of rooms that will be searched
* in the next iteration of the outer loop.
*/
newRoomList += nextLoc;
}
}
}
roomList := newRoomList; // Make list ready for next iteration.

if (pathLength = 1 and length(roomList) = 0) {
/* This is the end of the first iteration and no new locations
* has been found. In other words, there are no (normal) exits.
* Set the variable "failureType" to indicate this.
*/
failureType := 2;
break; // There is no point in continuing.
}
}

if (not pathFound) {
/* Path was not found. Let the actor give a message and then
* return nil to signal failure.
*/
if (failureType > 0)
actor.gtMessage(failureType); // Error type has already
// been specified.
else
actor.gtMessage(1); // General error.
return( nil );
}
else // The path has been found - let the actor display a message if it
// wants to do that.
actor.gtMessage(0); // Success.

/* At this place the room has been found. Since each room visited can
* tell from where it was found and what direction was chosen there
* a path can be put together. Here a path means a list of directions
* (coded as north=1, ne=2, etc.) that should be followed, starting
* from the initial room.
*/
{
local pathList, i, loc;

/* Following the chain of rooms gives the path in reverse order.
* To make everything easy first a list of the right length is
* made, then it is filled up from the end.
*/
pathList := [];
for( i := 1; i <= pathLength; i++ )
pathList += 0; // Extend list with something.
loc := toLoc;
for( i := pathLength; i > 0; i-- ) {
pathList[i] := loc.gtPreviousDir;
loc := loc.gtPreviousRoom;
}

return( pathList );
}
}


nowGoToRoom: function(actor, pathList)
{
/* Given an actor and a path to follow this function moves the actor
* along the path from his/her current location. At each new location
* the normal "checkVerb" and "roomAction" methods are called with
* a verb that corresponds to the movement
* To make the travel look as much as possible like a series of normal
* movements the methods are called AS IF IT HAD BEEN A NORMAL MOVEMENT.
* This means that if the actor is about to go north because the goToVerb
* has found that direction "checkVerb" is called with nVerb. Thus if
* there is any special code for that movement it will be executed just
* like it would be if the player had typed "go north" at that location.
* If the player for some reason can't travel any longer (a door might
* be locked, for instance) the travel is stopped.
*/
local i, pathLength, verboseDesc;

actor.gtStopWalking := nil;
pathLength := length(pathList);
for( i := 1; i <= pathLength; i++ ) {
local dirNo;

if( actor.gtStopWalking)
exit;
actor.gtWalkTurn := global.turnsofar;
actor.gtAllowDifference := nil;
dirNo := pathList[i];
/* The descriptions on the route are short. But if the player
* is using VERBOSE mode the final description should be long.
*/
verboseDesc := (i = pathLength) and global.verbose;
if (not actor.gtTravelInDir( dirNo, verboseDesc ) )
exit; // Do not continue if something came in the way.
/* To make every step count as a turn we run the daemons and fuses
* now. If this was the last move, though, this task is left to
* the normal run of daemons and fuses following a command.
*/
if (actor.gtStopWalking)
exit;
if (i < pathLength) {
actor.gtAllowDifference := true;
rundaemons();
runfuses();
actor.gtAllowDifference := nil;
if (actor.gtStopWalking)
abort; // The daemons and fuses should not run again, taking
// yet another turn.
}
}
/* And that was it. */
}

0 new messages