cfad47cfa3/t3compiler/tads3/lib/extensions/pathfind.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
/*
2
 *   Copyright 2003, 2006 Michael J. Roberts
3
 *   
4
 *   Path Finder.  This module provides a class that implements Dijkstra's
5
 *   Algorithm for finding the shortest path between two nodes in a graph.
6
 *   The basic path finder class makes no assumptions about the nature of
7
 *   the underlying graph; subclasses must provide the concrete details
8
 *   about the graph being traversed.  We provide a subclass that finds
9
 *   paths in a game-world location map; this can be used for things like
10
 *   making an NPC find its own way to a destination.
11
 *   
12
 *   Everyone is granted permission to use and modify this file for any
13
 *   purpose, provided that the original author is credited.  
14
 */
15
16
#include <tads.h>
17
18
19
/* ------------------------------------------------------------------------ */
20
/*
21
 *   The basic path finder class.  This implements Dijkstra's algorithm to
22
 *   find the shortest path from one node to another in a graph.  This base
23
 *   implementation doesn't make any assumptions about the nature of the
24
 *   underlying graph; each subclass must override one method to provide
25
 *   the concrete graph implementation.  
26
 */
27
class BasePathFinder: object
28
    /* 
29
     *   Find the best path from 'fromNode' to 'toNode', which are nodes in
30
     *   the underlying graph.  We'll return a vector consisting of graph
31
     *   nodes, in order, giving the shortest path between the nodes.  Note
32
     *   that 'fromNode' and 'toNode' are included in the returned vector.
33
     *   
34
     *   If the two nodes 'fromNode' and 'toNode' aren't connected in the
35
     *   graph, we'll simply return nil.  
36
     */
37
    findPath(fromNode, toNode)
38
    {
39
        local i;
40
        local len;
41
        local cur;
42
        local workingList;
43
        local doneList;
44
        local toEntry;
45
        local ret;
46
        
47
        /* start with set containing the initial node */
48
        workingList = new Vector(32);
49
        workingList.append(new PathFinderNode(fromNode));
50
51
        /*
52
         *   Work through the list.  For each item in the list, add all of
53
         *   the items adjacent to that item to the end of the list.  Keep
54
         *   visiting new elements until we've visited everything in the
55
         *   list once.
56
         *   
57
         *   We'll only add an element to the list if it's not already in
58
         *   the list.  This guarantees that the loop will converge (since
59
         *   the number of items in the graph must be finite).  
60
         */
61
        for (i = 1 ; i <= workingList.length() ; ++i)
62
        {
63
            /* add each adjacent item to the working list */
64
            forEachAdjacent(workingList[i].graphNode,
65
                            new function(adj, dist)
66
            {
67
                /* 
68
                 *   add the adjacent node only if it's not already in the
69
                 *   working list 
70
                 */
71
                if (workingList.indexWhich({x: x.graphNode == adj}) == nil)
72
                    workingList.append(new PathFinderNode(adj));
73
            });
74
        }
75
76
        /* 
77
         *   if the destination isn't in the working list, then there's no
78
         *   hope of finding a path to it 
79
         */
80
        if (workingList.indexWhich({x: x.graphNode == toNode}) == nil)
81
            return nil;
82
83
        /* start with an empty "done" list */
84
        doneList = new Vector(32);
85
86
        /* we know the distance from the starting element to itself is zero */
87
        cur = workingList[1];
88
        cur.bestDist = 0;
89
        cur.predNode = nil;
90
91
        /* keep going while we have unresolved nodes */
92
        while (workingList.length() != 0)
93
        {
94
            local minIdx;
95
            local minDist;
96
            
97
            /* find the working list element with the shortest distance */
98
            minDist = nil;
99
            minIdx = nil;
100
            for (i = 1, len = workingList.length() ; i <= len ; ++i)
101
            {
102
                /* if this is the best so far, remember it */
103
                cur = workingList[i];
104
                if (cur.bestDist != nil
105
                    && (minDist == nil || cur.bestDist < minDist))
106
                {
107
                    /* this is the best so far */
108
                    minDist = cur.bestDist;
109
                    minIdx = i;
110
                }
111
            }
112
113
            /* move the best one to the 'done' list */
114
            cur = workingList[minIdx];
115
            doneList.append(cur = workingList[minIdx]);
116
            workingList.removeElementAt(minIdx);
117
118
            /* 
119
             *   update the best distance for everything adjacent to the
120
             *   one we just finished 
121
             */
122
            forEachAdjacent(cur.graphNode, new function(adj, dist)
123
            {
124
                local newDist;
125
                local entry;
126
                
127
                /* 
128
                 *   Find the working list entry from the adjacent room.
129
                 *   If there's no working list entry, there's nothing we
130
                 *   need to do here, since we must already be finished
131
                 *   with it.  
132
                 */
133
                entry = workingList.valWhich({x: x.graphNode == adj});
134
                if (entry == nil)
135
                    return;
136
                
137
                /* 
138
                 *   calculate the new distance to the adjacent room, if
139
                 *   we were to take a path from the room we just finished
140
                 *   - this is simply the path distance to the
141
                 *   just-finished room plus the distance from that room
142
                 *   to the adjacent room (i.e., 'dist') 
143
                 */
144
                newDist = cur.bestDist + dist;
145
146
                /* 
147
                 *   If this is better than the best estimate for the
148
                 *   adjacent room so far, assume we'll use this path.
149
                 *   Note that if the best estimate so far is nil, it
150
                 *   means we haven't found any path to the adjacent node
151
                 *   yet, so this new distance is definitely the best so
152
                 *   far.  
153
                 */
154
                if (entry.bestDist == nil
155
                    || newDist < entry.bestDist)
156
                {
157
                    /* it's the best so far - remember it */
158
                    entry.bestDist = newDist;
159
                    entry.predNode = cur;
160
                }
161
            });
162
        }
163
    
164
        /*
165
         *   We've exhausted the working list, so we know the best path to
166
         *   every node.  Now all that's left is to generate the list of
167
         *   nodes that takes us from here to there.
168
         *   
169
         *   The information we have in the 'done' list is in reverse
170
         *   order, because it tells us the predecessor for each node.
171
         *   So, first find out how long the path is by traversing the
172
         *   predecessor list from the ending point to the starting point.
173
         *   Note that the predecessor of the starting element is nil, so
174
         *   we can simply keep going until we reach a nil predecessor.  
175
         */
176
        toEntry = doneList.valWhich({x: x.graphNode == toNode});
177
        for (cur = toEntry, len = 0 ; cur != nil ;
178
             cur = cur.predNode, ++len) ;
179
180
        /* create the vector that represents the path */
181
        ret = new Vector(len);
182
183
        /* 
184
         *   Traverse the predecessor list again, filling in the vector.
185
         *   Since the predecessor list gives us the path in the reverse
186
         *   of the order we want, fill in the vector from the last
187
         *   element backwards, so that the vector ends up in the order we
188
         *   want.  In the return vector, store the nodes from the
189
         *   underlying graph (rather than our internal tracking entries).
190
         */
191
        for (cur = toEntry, i = len ; cur != nil ; cur = cur.predNode, --i)
192
            ret[i] = cur.graphNode;
193
194
        /* that's it - return the path */
195
        return ret;
196
    }
197
198
    /* 
199
     *   Iterate over everything adjacent to the given node in the
200
     *   underlying graph.  This routine must simply invoke the given
201
     *   callback function once for each graph node adjacent to the given
202
     *   graph node.
203
     *   
204
     *   The callback takes two arguments: the adjacent node, and the
205
     *   distance from 'node' to the adjacent node.  Note that the distance
206
     *   must be a positive number, as Dijkstra's algorithm depends on
207
     *   positive distances.  If the graph isn't weighted by distance,
208
     *   simply use 1 for all distances.
209
     *   
210
     *   This method isn't implemented in the base class, since we don't
211
     *   make any assumptions about the underlying graph.  Subclasses must
212
     *   provide concrete implementations of this routine to define the
213
     *   underlying graph.  
214
     */
215
    forEachAdjacent(node, func) { /* subclasses must override */ }
216
;
217
218
/* 
219
 *   A node entry for the path finder - this encapsulates the node in the
220
 *   underlying graph, along with the "label" information in the algorithm.
221
 *   Note that this is NOT a node in the underlying graph; rather, this is
222
 *   an internal data structure that we use in the path finder to keep
223
 *   track of the underlying nodes and their status in the work queue.  
224
 */
225
class PathFinderNode: object
226
    construct(node)
227
    {
228
        /* remember the underlying node */
229
        graphNode = node;
230
    }
231
    
232
    /* the underlying node in the graph */
233
    graphNode = nil
234
235
    /* 
236
     *   The best estimate of the shortest distance from the starting
237
     *   point.  We use nil to represent infinity here.
238
     */
239
    bestDist = nil
240
241
    /* the best-path predecessor for this path element */
242
    predNode = nil
243
;
244
245
/*
246
 *   Room path finder.  This is a concrete implementation of the path
247
 *   finder that finds a path from one Room to another in the game-world
248
 *   map.
249
 *   
250
 *   This implementation traverses rooms based on the actual connections in
251
 *   the game map.  Note that this isn't appropriate for all purposes,
252
 *   since it doesn't take into account what the actor knows about the game
253
 *   map.  This "omniscient" implementation is suitable for situations
254
 *   where the actor's knowledge isn't relevant and we just want the actual
255
 *   best path between the locations.  
256
 */
257
roomPathFinder: BasePathFinder
258
    /* find the path for a given actor from one room to another */
259
    findPath(actor, fromLoc, toLoc)
260
    {
261
        /* remember the actor */
262
        actor_ = actor;
263
264
        /* run the normal algorithm */
265
        return inherited(fromLoc, toLoc);
266
    }
267
268
    /* 
269
     *   iterate over the nodes adjacent in the underlying graph to the
270
     *   given node 
271
     */
272
    forEachAdjacent(loc, func)
273
    {
274
        /* 
275
         *   run through the directions, and add the apparent destination
276
         *   for each one 
277
         */
278
        foreach (local dir in Direction.allDirections)
279
        {
280
            local conn;
281
            local dest;
282
            
283
            /* 
284
             *   if there's a connector, and it has an apparent
285
             *   destination, then the apparent destination is the
286
             *   adjacent node 
287
             */
288
            if ((conn = loc.getTravelConnector(dir, actor_)) != nil
289
                && (dest = getDestination(loc, dir, conn)) != nil
290
                && includeRoom(dest))
291
            {
292
                /* 
293
                 *   This one seems to go somewhere - process the
294
                 *   destination.  The standard room map has no concept of
295
                 *   distance, so use equal weightings of 1 for all
296
                 *   inter-room distances.  
297
                 */
298
                (func)(dest, 1);
299
            }
300
        }
301
    }
302
303
    /*
304
     *   Get the location adjacent to the given location, for the purposes
305
     *   of finding the path.  By default, we return the actual
306
     *   destination, but subclasses might want to use other algorithms.
307
     *   For example, if a subclass's goal is to make an NPC find its own
308
     *   way from one location to another, then it should use the APPARENT
309
     *   destination, from the NPC's perspective, rather than the actual
310
     *   destination, since we'd want to construct the path based on the
311
     *   NPC's knowledge of the map.  
312
     */
313
    getDestination(loc, dir, conn)
314
    {
315
        /* return the actual destination for the connector */
316
        return conn.getDestination(loc, actor_);
317
    }
318
319
    /*
320
     *   For easier customization, this method allows the map that we
321
     *   search to be filtered so that it only includes a particular
322
     *   subset of the map.  This returns true if a given room is to be
323
     *   included in the search, nil if not.  By default, we include all
324
     *   rooms.  Note that this is only called to begin with for rooms
325
     *   that have apparent connections to the starting room, so there's
326
     *   no need to filter out areas of the map that aren't connected at
327
     *   all to the starting search location.  
328
     */
329
    includeRoom(loc) { return true; }
330
331
    /* the actor who's finding the path */
332
    actor_ = nil
333
;
334
335
/*
336
 *   An NPC goal-seeking path finder.  This is a subclass of the basic room
337
 *   path finder that 
338
 */
339
npcRoomPathFinder: roomPathFinder
340
    /* 
341
     *   Get the destination.  Unlike the base class implementation, we
342
     *   take into the NPC's knowledge of the map by only using connector
343
     *   destinations that are APPARENT to the NPC. 
344
     */
345
    getDestination(loc, dir, conn)
346
    {
347
        /* return the apparent destination of the connector */
348
        return conn.getApparentDestination(loc, actor_);
349
    }
350
;
351