/* Copyright (c) 2001 by Kevin Forchione.  All Rights Reserved. */
/*
 *  TADS ADV.T/STD.T LIBRARY EXTENSION
 *  TRACKACTOR.T				
 *  Version 3.0
 *
 *  THE TRAVEL GRAPH
 *
 *      The travel graph is a multi-graph of weighted directed
 *      edges, that may contain self-loops. To simplify the automation
 *      process only travel properties that resolve to property types 
 *      of _object_ are used to build adjacent vertex and adjacent edge
 *      lists.
 *
 *      Multiple edges between a given pair of vertices are flattened
 *      out by resolving the traversal between the two vertices as 
 *      passable if any of the edges is found to be passable, and 
 *      impassable otherwise. This allows for simple paths to be 
 *      computed by dijkstra's shortest paths algorithm.
 *
 *      Weighting of edges between vertices is based upon the 
 *      actor's ability to pass all of the obstacles that may 
 *      lie along the given edge. Impassable edges are given a 
 *      weight of INFINITY, while edges for which passage is 
 *      possible are give a weight of 1. The edge weights are 
 *      maintained through all path computations for a given 
 *      station (see below), and re-adjudicated only when the
 *      next station is being considered. 
 *
 *      If a passable edge has been found for the two vertices 
 *      then the adjacent vertex itself is examined to determine
 *      if it is accessible to the actor. Inaccessible vertices 
 *      are temporarily removed from path computations. After a 
 *      new path has been computed the bias against the 
 *      inaccessible vertex is removed.
 *
 *  STATIONS
 *
 *      TrackActor class allows the author to set stations as 
 *      points of travelling preference along the graph. Stations 
 *      are used to form the destinations of simple paths computed
 *      by dijkstra's algorithm. 
 *
 *      When a path to a station has been found to be impassable, 
 *      the trackActorDaemon() will remove the impassable vertex 
 *      or edge from its new path computation and attempt to 
 *      traverse this new path. This strategy is continued until 
 *      either a path to the station has been successfully 
 *      traversed; or all possible paths to the station are 
 *      exhausted. If all paths have been exhausted the station 
 *      is abandonned and we proceed to calculate a path from 
 *      the actor's location to the next station in the list.
 *
 *  TRAVERSAL STYLE
 *
 *      In this way, stations are run in sequence as they appear 
 *      in the actor's station list until the end of the list is 
 *      reached. At that point the traversal style used to set 
 *      the station list will determine what happens next. The 
 *      pattern of this sequence can be any of three traversal 
 *      styles: 
 *    
 *     pathTraversal:       the station list is processed once only.
 *     cycleTraversal:      the station list is processed once, returning 
 *                          the actor to the first station when the end has
 *                          been reached.
 *     continuousTraversal: the station list is processed as a continuous 
 *                          loop from beginning to end. 
 *
 *  EXAMPLE
 *
 *          setStation( [loc1, loc2, loc3], continuousTraversal );
 *
 *      The above statement tells TrackActor to set its station
 *      list to [loc1, loc2, loc3] and its traversal style to 
 *      continuousTraversal. The actor's destination will be 
 *      set to loc1and a path computed from the actor's location 
 *      to loc1.
 *
 *      The actor will then follow the computed path (track list) 
 *      until it completes it or finds an edge or vertex it cannot
 *      pass. If this is the case then a new path to its destination
 *      is computed, eliminating the obstructing edge or vertex from
 *      the calculation. 
 *
 *      After the actor has arrived at loc1 (or it has been 
 *      determined that this no paths to the destination exist)
 *      a new path starting from the actor's location to loc2
 *      will be computed; the process repeated for loc3 and
 *      the whole cycle starts over again by the actor returning
 *      from loc3 to loc1.  
 *
 *----------------------------------------------------------------------
 *  REQUIREMENTS
 *
 *      + HTML TADS 2.2.6 or later
 *      + Should be #included after ADV.T and STD.T 
 *      + Requires dijkstra.t
 *
 *----------------------------------------------------------------------
 *  IMPORTANT LIBRARY INTERFACE AND MODIFICATION
 *
 *      + Overrides preinit()
 *
 *----------------------------------------------------------------------
 *  COPYRIGHT NOTICE
 *
 *  	You may modify and use this file in any way you want, provided that
 *		if you redistribute modified copies of this file in source form, the
 *   	copies must include the original copyright notice (including this
 *   	paragraph), and must be clearly marked as modified from the original
 *   	version.
 *
 *------------------------------------------------------------------------------
 *  REVISION HISTORY
 *
 *		28-Jan-01:	Creation.
 *      06-Feb-01:  Major restructuring!
 */


#ifndef _TRACK_ACTOR_T_
#define _TRACK_ACTOR_T_

#include <dijkstra.t>

#pragma C+

sayDirName: function;
randomElement: function;

TrackActor: object
    isActive            = nil
    traversalStyle	    = nil
    destination 	    = nil
    movementCount       = 0
    stationPos		    = 1
    trackPos 		    = 1
    stationList		    = []
    trackList		    = []
    failedVertexList    = []
    failedEdgeList      = []
    failedStationList   = []

    /*
     *  Stations are vertices within the game world multi-graph which
     *  form the destinations for the tracks the actor must traverse.
     *  This method sets the actor in motion by setting its station
     *  list, calculating the track list for the first station, and
     *  notifying the trackActorDaemon.
     */
    setStation( vertexList, type ) = {  
        self.stationList        = [];
        self.traversalStyle     = type;
        
	    if (length( vertexList ) == 1
	    && self.traversalStyle != pathTraversal )
	        self.stationList    += self.location; 

	    self.stationList        += vertexList;
	        
	    self.stationPos		    = 1;
	    self.stationListCycles  = 0;
     	povStack.push(self);
    	self.isActive 		    = true;
	    self.failedVertexList 	= [];
	    self.failedEdgeList     = [];
	    self.setPath( self.location, self.stationList[ self.stationPos ] );
	    povStack.pop;
    }

    /*
     *  Creates the track list for the destination. Track lists are
     *  computed using dijkstra's shortest paths algorithm.
     */
    setPath( start, dest ) = {
    	self.trackPos 		= 1;
    	self.destination	= dest;
       	self.trackList      = Path.shortestPathVertices(start, dest);
    }

    trackActorDaemon = {
        local cur, nxt, edge, povSave;

        if ( !self.isActive ) return;
        
        /* set the point of view to this actor */
        povStack.push(self);

        /* Reset movement count */
        self.movementCount = 0;
        
        while( self.hasMore )
        {
        
            /* Get the actor's current and next locations */
            cur = self.location;
            nxt = self.trackList[ self.trackPos ];
 
            /*
             *  Retrieve a passable edge for the current and next 
             *  vertices.  
             */    
            edge = self.getEdge( cur, nxt );
            if ( edge != nil )
            {
	            if ( self.canMoveToVertex( nxt ) )
                {
                    /*
                     *  We remove the nxt vertice from the failed
                     *  station list, if it has been added from an
                     *  earlier iteration.
                     */
                    if ( nxt == self.destination )
                        self.failedStationList -= nxt;

                    self.traverseEdge( edge );
                    
                    break;
                }

                /* 
                 *  If this vertex is temporarily blocked to the actor we 
                 *  need to recalculate the path, eliminating this vertex
	             *  from our calculations -- temporarily.
                 */
	            if ( nxt != self.destination )
	            {
                    self.failedVertexList += nxt;
        	        self.setPath( cur, self.destination );
                    self.handleInvalidVertex( nxt );
        	        continue;
	            }
            }

            /* 
             *  The actor's current location is the next location in the
             *  track list. We simply increment the track position and start
             *  the process over. This is checked after edge and vertice 
             *  validation to allow for graph self-loops.
             */        
            if ( cur == nxt )
                continue;

            /* 
             *  This route is blocked to the actor. We need to recalculate
             *  the path, eliminating the failed edge list from our 
             *  calculations.
             */
            self.setPath( cur, self.destination );
            
	        if ( nxt == self.destination
            && length( self.trackList ) == 0 )
            {
                local tmp = [ nxt ];
	        	self.failedStationList += (tmp - self.failedStationList);
            }
            self.handleNoValidEdges( cur, nxt );
        }
        
        /* Reset the pov */
        povStack.pop;
    }
    
    hasMore = {
        ++self.trackPos;

        ++self.movementCount;
        
        if ( length(self.stationList - self.failedStationList) == 0)
        {
            /* 
             *  Dijkstra didn't compute a path for any of the stations,
             *  so shut the actor down.
             */
            self.handleNoValidStations;
            return nil;
        }
        
        /*
         *  When we reach the end of the track list we check to see if
         *  we need to stop or go to the next station. 
         */
        if ( length(self.trackList) < self.trackPos )
        {
            /* Ask traversal style is we should stop moving */
            if ( self.traversalStyle.trackListCompleted(self, 
		        self.stationListCycles) )
                return nil;
            
            /* increment the station position */
            ++self.stationPos;
            
            /* 
             *  If we've reached the end of the station list we need to
             *  determine if we should stop or recycle through the list.
             */
            if ( length(self.stationList) < self.stationPos )
            {
               /* Ask traversal style is we should stop moving */
               if ( self.traversalStyle.stationListCompleted(self) )
                    return nil;
                
                /*
                 *  Increment the number of times we've been thru the
                 *  station cycle list.
                 */
                ++self.stationListCycles;
                
                /* set the station position back to 1 */
                self.stationPos = 1;
            }
            
            /* Reset the failed vertex and edge lists */
            self.failedVertexList   = [];
            self.failedEdgeList     = [];
            
            /* Compute the track list for the new station */
            self.setPath( self.location, self.stationList[
                self.stationPos ] );
        }

        return true;
    }
    
    getEdge( cur, nxt ) = {
        local i, len, edge, obsList;
        local edgeList = [], obstacleEdgeList = [];

        /* 
         *  Create an edge list containing each edge object that matches
         *  the actor's current and next locations. Build a separate
         *  list from the edge list that contains all edges that have
         *  obstacles.
         */
        len = length( cur.adjacentEdgeList );
        for ( i = 1; i <= len; ++i )
        {
            edge = cur.adjacentEdgeList[ i ];
            if ( edge.vertexFrom == cur && edge.vertexTo == nxt )
            {
                edgeList += edge;
                if ( length( edge.obstacleList ) > 0 )
                {
                     obstacleEdgeList += edge;
                }
            }
        }
        
        /* remove all edges with obstacles from the edge list */
        edgeList -= obstacleEdgeList;
        
        /* Set edge to nil */
        edge = nil;

        if ( length( edgeList ) > 0 )
        {
            /* we have at least 1 passable edge */
            edge = randomElement( edgeList );
            return edge;
        }

        /*
         *  All edges have obstacles. Check each edge to see if the
         *  actor can pass all of the obstacles for the given edge. If
         *  the actor can pass all of the obstacles then travel to the
         *  first obstacle.
         */
        len = length( obstacleEdgeList );
        for ( i = 1; i <= len; ++i )
        {
            obsList = obstacleEdgeList[ i ].obstacleList;
            if ( self.canPassEdge( obsList ) )
            {
                edge = obstacleEdgeList[ i ];
                return edge;
            }
        }

        /* Add the obstacle edge list to the failed edge list */
        self.failedEdgeList += obstacleEdgeList;
        return nil;
    }
    
    /*
     *  Test whether the actor can pass all of the obstacles for a
     *  given edge. Returns true if the actor can; otherwise returns
     *  nil.
     */
    canPassEdge( obstacleList ) = {
        local i, len, o;
        
        len = length( obstacleList );
        for ( i = 1; i <= len; ++i )
        {
            o = obstacleList[i];
            if ( !self.canPassObstacle( o ) )
            {
                /* we can't pass the obstacle */
                return nil;
            }
        }
        
        /* No obstacle has stopped passage */
        return true;
    }

    /*
     *  Test whether the actor can pass the specified obstacles for
     *  a given edge. The method should return true if passage is
     *  allowed; nil otherwise. The default simply tests to see 
     *  if the obstacle is closed and locked or not auto open.
     */
    canPassObstacle( o ) = {
        if ( !o.isopen
        && ( o.islocked || o.noAutoOpen ) )
        {
            /* we can't pass the obstacle */
            return nil;
        }
        
        /* The obstacle has not stopped passage */
        return true;
    }

    /*
     *  This method is used to determine if the vertex
     *  is, for some reason, off-limits to the actor.
     */
    canMoveToVertex( v ) = {
        return true;
    }

    isFirstPass = { return (self.movementCount == 1); }
    
    // move to a new location, notifying player of coming and going
    traverseEdge( edge ) = {
        local old_loc_visible;
        local new_loc_visible;
        local room, obs;
        
        obs = car(edge.obstacleList);
        
        if ( obs )
            room = obs;
        else
            room = edge.vertexTo;
        
        /* if it's an obstacle, travel to the obstacle instead */
        while( room != nil && room.isobstacle )
            room = room.destination;
        
        /* do nothing if going nowhere */
        if ( room == nil )
            return;

        /* note whether the actor was previously visible */
        old_loc_visible = (self.location != nil
                            && parserGetMe().isVisible(self.location));

        /* note whether the actor will be visible in the new location */
        new_loc_visible = parserGetMe().isVisible(room);

        /* 
         *   if I'm leaving the player's location, and I was previously
         *   visible, and I'm not going to be visible after the move, and
         *   the player's location isn't dark, show the "leaving" message 
         */
        if (parserGetMe().location != nil
            && parserGetMe().location.islit
            && old_loc_visible
            && !new_loc_visible)
            self.sayLeaving( edge.dirPtr );
        
        /* move to my new location */
        self.moveInto( room );

        /* 
         *   if I'm visible to the player at the new location, and I
         *   wasn't previously visible, and it's not dark, show the
         *   "arriving" message 
         */ 
        if (parserGetMe().location != nil
            && parserGetMe().location.islit
            && new_loc_visible
            && !old_loc_visible)
            self.sayArriving( edge.dirPtr );
    }
    
    // sayLeaving and sayArriving announce the actor's departure and arrival
    // in the same room as the player.
    sayLeaving( dirPtr ) = {
        self.location.dispParagraph;
        "\^<<self.thedesc>> leaves";
        switch( dirPtr )
        {
            case &up:
            case &down:
            case &in:
            case &out:
                " the area. ";
                break;
            default:
                " to the <<sayDirName( dirPtr )>>. ";
        }
    }
    
    sayArriving( dirPtr ) = {
        self.location.dispParagraph;
        "\^<<self.thedesc>> enters ";
        switch( dirPtr )
        {
            case &up:
            case &down:
            case &in:
            case &out:
                " the area. ";
                break;
            default:
                " from the <<sayDirName( dirPtr )>>. ";
        }
    }
    
    // Hooks for special transition handling
    
    /*
     *  This method is called when no valid edges have been found and
     *  after a new path has been calculated weighting the edges between
     *  cur and nxt at INFINITY.
     */
    handleNoValidEdges( cur, nxt ) = {}
    
    /*
     *  This method is called when the next vertex is found to be
     *  impassable to the actor and after a new path has been calculated
     *  by removing the vertex from dijkstra's considerations. By
     *  default we remove the vertex from the failed vertex list.
     */
    handleInvalidVertex( nxt ) = {
        self.failedVertexList -= nxt;
    }
    
    /* 
     *  This method is called when all of the stations in the actor's
     *  station list have been added to the failed station list. This 
     *  means that the actor has failed to find paths for all of the
     *  stations. By default we stop the actor's attempts to move.
     */
    handleNoValidStations = {
        self.isActive = nil;
    }

    /*
     *  This method is called when the traversal style is cycleTraversal
     *  and the cycle has been completed. By default we stop the actor's 
     *  attempts to move.
     */
    handleCompletedCycle = {
        self.isActive = nil;           
    }

    /*
     *  This method is called when the traversal style is pathTraversal
     *  and the path has been completed. By default we stop the actor's
     *  attempts to move.
     */
    handleCompletedPath = {
        self.isActive = nil;
    }
;

/*
 *  TraversalStyle indicates whether movement is to stop
 *  at the points when the end of the track list or station
 *  list has been reached.
 */
class TraversalStyle: object
    trackListCompleted(actor, stationListCycles) = { return nil; }
    stationListCompleted(actor) 			 = { return nil; }
;

pathTraversal: TraversalStyle
    /* movement stops when we reach the end of the station list */
    stationListCompleted(actor) = {
        actor.handleCompletedPath;
        return true;
    }
;

cycleTraversal: TraversalStyle
    /* movement stops when we've cycled around the station list once */
    trackListCompleted(actor, stationListCycles) = {
        if ( stationListCycles > 0 )
        {
            actor.handleCompletedCycle;
            return true;
        }
        return nil;
    }
;

continuousTraversal: TraversalStyle
;

class Edge: object
    vertexFrom      = nil
    vertexTo        = nil
    dirPtr          = nil
    obstacleList    = []

    instantiate( vf, vt, d, o ) = {
        local e = new Edge;

        e.vertexFrom    = vf;
        e.vertexTo      = vt;
        e.dirPtr        = d;
        e.obstacleList  = o;

        return e;
    }
;

/*
 *  Say the direction name for the direction pointer.
 */
sayDirName: function( dirPtr )
{
    switch( dirPtr )
    {
        case &north:
            "north";
            break;
            
        case &ne:
            "northeast";
            break;
            
        case &nw:
            "northwest";
            break;
            
        case &south:
            "south";
            break;
      
        case &se: 
            "southeast";
            break;
            
        case &sw:
            "southwest";
            break;
            
        case &east:
            "east";
            break;
            
        case &west:
            "west";
            break;
            
        case &up:
            "up";
            break;
            
        case &down:
            "down";
            break;
            
        case &in:
            "in";
            break;
            
        case &out:
            "out";
            break;
    }
}

/*
 *  Returns an element of the list selected at random.
 */
randomElement: function( lst ) {
	local r, len = length( lst );
	
	if ( len == 1 )
	    r = 1;
	else
	    r = _rand( len );

	return lst[ r ];
}

povStack: object
    povList     = []
    currentPov  = nil
    
    push( val ) = {
        local tmpList   = self.povList;
        
        self.currentPov = val;
        self.povList    = [ val ] + tmpList;
    }
    
    pop = {
        local cp, val;
        
        val = car(self.povList);
        if ( val == nil ) {
            val = parserGetMe();
            self.currentPov = val;
        }
        else
        {
            self.povList = cdr(self.povList);
        
            cp = car(self.povList);
            if ( cp == nil )
                self.currentPov = parserGetMe();
            else
                self.currentPov = cp;
        }
        
        return val;
    }
;

/*
 *  Modify Path to weight edges that are found to be unpassable to the
 *  track actor. If we find a corresponding edge for v & w we return a
 *  value that will compute to INFINITY for comparison of the edge
 *  between the two vertices. If we don't find an edge in the track
 *  actor's failed edge list then we return 1.
 */
modify Path
    computeEdge( v, w ) = {
        local i, len, e;
        local edgeList = [];
                
        edgeList = povStack.currentPov.failedEdgeList;
        
        len = length( edgeList );
        for ( i = 1; i <= len; ++i )
        {
            e = edgeList[ i ];
            if ( v.vertex == e.vertexFrom && w.vertex == e.vertexTo )
                return INFINITY;
        }
        
        return 1;
    }
;

modify room
    adjacentVertexList  = []
    adjacentEdgeList    = []
    
    setAdjacentVertices = {
        local i, dPtr, w, len, e, obsList;
        local dirList = [ &north, &south, &east, &west, &ne, &nw, &se,
            &sw, &up, &down, &in, &out ];

        len = length( dirList );
        for ( i = 1; i <= len; ++i )
        {
            if ( proptype( self, dirList[ i ] ) == DTY_OBJECT )
            {
                dPtr = dirList[i];
                w = self.(dPtr);

                /* build a list of all obstacles */
                obsList = [];
                while( w != nil && isclass(w, obstacle) )
                {
                    obsList += w;
                    w = w.doordest;
                }

                e = Edge.instantiate(self, w, dPtr, obsList);
                self.adjacentEdgeList += e;

                /* Ensure that the adjacent vertex list is unique */
                w = [ w ];
                self.adjacentVertexList += (w - self.adjacentVertexList);
            }
        }
    }
    
    getAdjacentVertices = {
        return (self.adjacentVertexList - 
            povStack.currentPov.failedVertexList);
    }
;

initTravelGraph: function {
    local o;

    o = firstobj(room);
    while( o != nil )
    {
        o.setAdjacentVertices;
        o = nextobj( o, room );
    }
    povStack.push(parserGetMe());
}

replace preinit: function
{
    local o;

    global.lamplist = [];
    o = firstobj();
    while(o != nil)
    {
        if (o.islamp)
            global.lamplist = global.lamplist + o;
        o = nextobj(o);
    }
    initSearch();
    initTravelGraph();
}

#pragma C-

#endif  /* _TRACK_ACTOR_T_ */
