// A* Search Algorithm Module // // Provides a fairly general implementation of the A* search algorithm // that can be used to search arbitrary problem spaces. A* search is a // modification of best-first search that uses a heuristic to determine // which paths to preferentially search, frequently based on coordinate-space // distance from the path endpoint to the goal. It is commonly regarded as // the best general algorithm for pathfinding within a Cartesian space. // See http://en.wikipedia.org/wiki/A*_search_algorithm for more. // // This module's official home: http://lostsouls.org/grimoire_astar // New versions will be posted there. // // You may do anything you like with this code so long as these two lines, // the thirteen preceding and the thirty-two following are retained intact. // // v1.0 2007-05-11, Chaos of Lost Souls, http://lostsouls.org/ // v1.1 2007-05-29, Chaos; factored astar.h out of astar.c // v1.2 2007-06-13, Chaos; moved all state to pathfind data structure // v1.3 2007-07-09, Chaos; added edge costs, code cleanup // v1.4 2007-08-12, Chaos; added active edge to pathfind structure // v1.4.1 2007-08-31, Chaos; improved comments // v1.5 2007-09-03, Chaos; made node not considered visited until valid // v1.5.1 2007-09-04, Chaos: improved comments // v1.5.2 2007-10-06, Chaos: moved sample implementation into its own file // v1.5.3 2007-10-06, Chaos: minor comment cleanup // v1.5.4 2008-12-29, Chaos: run tracking improvements, formatting >80 col // v1.5.5 2009-04-26, Chaos: single-argument 'extra', pathfind as callback // argument, pathfind as astar_find_path() return value, restructuring // v1.5.6 2009-06-30, Chaos: improved cache implementation // v1.5.7 2009-07-29, Chaos: reordered validate key calculations // v1.5.8 2009-10-02, Chaos: refactoring and cleanup // v1.5.9 2009-10-13, Chaos: cache refactoring and tweaking // v1.5.10 2009-10-26, Chaos: LDMud 3.3 warning elision // v1.6 2011-01-28, Chaos: added pathfind no-continue flag, flags in start, // scheduling rule support allowing usage of call_out() to be overridden // v1.7 2013-08-18, Chaos: cohered terminology with standard graph theory // v1.7.1 2014-07-07, Chaos: reduced memory allocation, minor tweaks // v1.7.2 2014-09-07, Chaos: added neighbor retrieval by count and index, // specific handling of no-op case // v1.7.3 2014-12-27, Chaos: added tolerance of failure case from neighbor // retrieval by index // v1.7.4 2015-06-09, Chaos: removed reliance on generic sorting in favor // of custom insertion sorting, reduced memory allocation via chunking // v2.0.0 2015-06-14, Chaos: changed path node & edge list and visited list // tracking to use chunked allocation, which changes the output format and // adds Astar_Path_Length to it // This module is written for LPC as implemented by the LDMud driver, // http://www.bearnip.com/lars/proj/ldmud.html. An effort has been made // to avoid highly driver-specific features, and the module should be // portable to e.g. MudOS with moderate effort. // This is not a toy or demonstration version of A* search; it is a module // written to be used (and in active use) in a production environment that // needs to deploy A* in a variety of contexts. I've worked to make it // clear and well-documented, but its primary purpose is not to explain A* // search, it is to provide a clear, well-documented module that implements // a working practical version of the algorithm. In Lost Souls, the module // is presently used to perform pathfinding in 2D and 3D coordinate-space // and hybrid coordinate-space/arbitrary-linkage MUD areas, as well as // searching in the problem space made up of A* results from those instances. // In order to work properly with A* search as implemented by this module, // your situation needs to be describable in terms of a few crucial concepts. // // The first concept is nodes: there need to be things resembling locations, // states, options, or the like that can be represented in some consistent // fashion. One common kind of node is a point in a Cartesian coordinate // space, an X, Y or X, Y, Z location; another is a MUD room filename. The // concept of a node isn't limited to describing location; nodes could as // easily be behaviors, frames of mind, geometrical arrangements, goals, or // anything else. You should be able to generally figure out some sort of // distance (more generally, a cost-to-reach estimate) between two nodes // without any other information; a Cartesian space works well for this part. // It's okay if there are some nodes you can find a distance for and some // you can't, as with a MUD area that has some rooms on a Cartesian grid and // some off of it -- this module is set up to deal with that -- but if you // can't find a distance for anything and all of your nodes have the same // cost to move between them, then you're not getting any benefit out of // the A* algorithm and may as well just do an exhaustive search. // // The second concept is 'edges': there need to be quantifiable ways to // move between adjacent nodes. These might be simply lists of offsets in // a coordinate space, e.g. ({ 0, 1, 0 }), they may be directions like // "north" or commands like "enter portal", or for more esoteric nodes they // may be abstract link identifiers or other ways of describing change. // // The third concept is 'costs'. Edges have an associated cost; the lower // the cost of the edge, the more desirable it is to use it. This may // represent relative difficulty of terrain, varying time delays between // exits, or anything else that affects the desirability of using a edge. // If all edges are equally desirable, just use 1 for their cost. // // The information you *must* be able to provide the algorithm in order for // it to function is the answer to the question "which nodes are adjacent // to this node and how do I get to them from here?" Everything else is // optional. // // Take a look at the file astar_2d_example.c for a sample implementation. // This module uses macros from the file astar.h, which should be found // accompanying this code, for its 'path' and 'pathfind' data structures // and its result codes. astar.h should be placed in a suitable include // directory. #include <astar.h> // SECTION: Instance configuration // // These configuration functions are used by an application of the astar // module to define its behavior and allow it to retrieve the information // it works with. Most of them are passed a closure (function pointer) // pointing to a function you have defined, which the astar module then // calls when it needs to. Check the sample implementation in // astar_2d_example.c for how it looks in practice. // Neighbors rule // // The neighbors rule is used to retrieve all nodes adjacent to a specified // node and the edges used to reach them. It is called with an astar // pathfind data structure (as defined by the Astar_Pathfind_* macros in // astar.h) as argument; this data structure is the same one used by the // algorithm for tracking the pathfinding attempt in general, which means // that 1) the fields in it all contain valid information and can be used // to examine the pathfind, and 2) the fields in it should not be changed // or you will almost certainly cause errors. Some fields in the data // structure will be set specifically for this retrieval: // // pathfind[Astar_Pathfind_Active_Node] // Will be set to the node whose neighbors we want to retrieve. // pathfind[Astar_Pathfind_Active_Edge] // Will be set to the edge from the previous node in the path // to the active node. Set to 0 at the beginning of a path. // pathfind[Astar_Pathfind_Active_Path] // Will be set to the entire path leading to the active node. // This is a path data structure as defined by the Astar_Path_* // macros in astar.h. // // The return value that the neighbors rule should provide is an array of // three-element arrays in which the first element is a node, the second // element is the edge used to reach it, and the third element is the cost // of the edge. That means something like this: // // return ({ // ({ // FIRST_NODE, // FIRST_EDGE, // COST_OF_FIRST_EDGE, // }), // ({ // SECOND_NODE, // SECOND_EDGE, // COST_OF_SECOND_EDGE, // }), // }); // // Alternately, the neighbors rule may return Astar_Result_Processing, // which tells the algorithm to retry the request slightly later via the // scheduling rule. // // A neighbors rule is the only one that absolutely must be defined in order // to allow the algorithm to function. private closure neighbors_rule; void set_astar_neighbors_rule(closure val) { neighbors_rule = val; } closure query_astar_neighbors_rule() { return neighbors_rule; } // Neighbor count rule and neighbor by index rule // // As a less memory-allocation-intensive alternative to the neighbors rule, // an implementation may use a neighbor count rule and a neighbor by index // rule. // // The neighbor count rule is called exactly like the neighbors rule, and // is expected to return the number of neighbors the current node has, or // a negative number to indicate that the algorithm should retry the request // later via the scheduling rule. // // The neighbor by index rule will then be called for each neighbor we want // to retrieve. Its return value should be true if the neighbor should be // used, false if it should be ignored. The arguments passed to it are: // 1) the astar pathfind structure set up as per the neighbors rule // 2) the index (starting with 0) of the neighbor we want to retrieve // 3) a reference argument that should be set to the neighbor's node // 4) a reference argument that should be set to the neighbor's edge // 5) a reference argument that should be set to the neighbor's edge cost // The values placed in the reference arguments are ignored if the function // returns false. private closure neighbor_count_rule; void set_astar_neighbor_count_rule(closure val) { neighbor_count_rule = val; } closure query_astar_neighbor_count_rule() { return neighbor_count_rule; } private closure neighbor_by_index_rule; void set_astar_neighbor_by_index_rule(closure val) { neighbor_by_index_rule = val; } closure query_astar_neighbor_by_index_rule() { return neighbor_by_index_rule; } // Distance rule // // The distance rule is used to determine the distance (for non-locational // problem spaces, think of it as a cost estimate) from a specified node // to the target node. It is called with the astar pathfind data structure // structure; see the notes on the neighbors rule for more on this. Fields // of particular concern to the distance rule are: // // pathfind[Astar_Pathfind_To] // The destination node of the pathfind; we are trying to find // the distance from the active node to here. // pathfind[Astar_Pathfind_Active_Node] // The node whose distance from the destination node we want to // know. // // The return value needed is a int or float value indicating the distance // (or estimating the cost), or -1 if the distance cannot be determined. // // Using a distance rule is optional but strongly encouraged. private closure distance_rule; void set_astar_distance_rule(closure val) { distance_rule = val; } closure query_astar_distance_rule() { return distance_rule; } // Node rule // // The node rule is used to convert the representation of a node into the // form you want. If, for instance, the pathfinder routine might wind up // called with a string filename or an object for its nodes, and you want to // use the filename for how you're going to work with nodes internally, you // can define this hook in order to perform the conversion. It is called // with a node representation as its argument; this is provided by the code // requesting the pathfind and can be anything at all. The return value // needed is the final desired representation for the node. private closure node_rule; void set_astar_node_rule(closure val) { node_rule = val; } closure query_astar_node_rule() { return node_rule; } // Node key rule // // The node key rule is used to represent a node for purposes of checking // whether it has been visited. You will usually need to use this if your // nodes are normally represented as pointers (arrays or mappings), unless // the pointers for a given node can be guaranteed to always be the same. // Otherwise, the system can't tell which nodes it's visited. It is called // with a node as argument. The return value needed is the "flat" // representation to use for the node, most typically a string or int. private closure node_key_rule; void set_astar_node_key_rule(closure val) { node_key_rule = val; } closure query_astar_node_key_rule() { return node_key_rule; } // Completion rule // // The completion rule can be used to determine whether an acceptable path // destination has been found; if one is not supplied, equivalency between // the node keys of the prospective completion node and the target node // is checked. It is called with the astar pathfind data structure as // argument; see the notes on the neighbor rule for more about this. Fields // of particular relevance to this rule are: // // pathfind[Astar_Pathfind_To] // The destination node of the pathfind. // pathfind[Astar_Pathfind_Active_Node] // The node being checked. // // It should return true if the node is a valid completion node. private closure completion_rule; void set_astar_completion_rule(closure val) { completion_rule = val; } closure query_astar_completion_rule() { return completion_rule; } // Cycle process // // The cycle process is executed at the beginning of every pathfinding // 'cycle'. A new cycle is begun when the pathfind starts and when it // continues via the scheduling rule, if applicable. It is called with // the astar pathfind data structure as argument; see the notes on the // neighbor rule for more about this. Fields of particular relevance to // this rule are: // // pathfind[Astar_Pathfind_Cycle_Start] // The utime() when the current pathfinding cycle began. // pathfind[Astar_Pathfind_Cycle_Index] // The serial number of the current pathfinding cycle. // // Its return value is ignored. private closure cycle_process; void set_astar_cycle_process(closure val) { cycle_process = val; } closure query_astar_cycle_process() { return cycle_process; } // Run limit rule // // The run limit rule is used to check whether the algorithm has run for too // long and should be rescheduled or aborted. It is called with the astar // pathfind data structure as argument. The return value needed is any true // value if the run limit has been exceeded, false if not. private closure run_limit_rule; void set_astar_run_limit_rule(closure val) { run_limit_rule = val; } closure query_astar_run_limit_rule() { return run_limit_rule; } // Caching // // The cache retains the pathfinding results that have been obtained so they // do not have to be recalculated after being requested once, at the cost of // some memory usage. One would use set_astar_caching(1) in the instance to // turn on caching. // // If your neighbors rule does not always return the same results for a given // node, for instance if you use extra arguments to provide varying results // as with terrain difficulty that varies by the person traversing it, then // you should generally not turn on caching, because the cached paths may not // remain valid and could be returned in inappropriate circumstances. private int caching; private mapping cache; void set_astar_caching(int val) { caching = val; } int query_astar_caching() { return caching; } mapping query_astar_cache() { return cache; } // Validate key rule // // The validate key rule is only meaningful if you have caching turned on. // It can be used to provide mapping-key-usable representations of closures // passed to astar_find_path() as its 'validate' argument. A common way to // construct the rule is to return the to_string() of the closure; this // implies that (and should only be done if) a particular 'validate' closure // will always return the same way for a particular node. This is used for // caching pathfinding results with 'validate' rules. If you cannot guarantee // consistent results for a given 'validate' function, you should return 0 // from the validate key rule so that the system knows not to cache the path // using it. If this rule is not defined, paths generated with 'validate' // rules will not be cached. The validate key rule is called with the astar // pathfind data structure as argument, with the most relevant field being: // // pathfind[Astar_Pathfind_Validate] // The 'validate' closure provided for the pathfinding attempt // // The validate key rule should return the representation to use for the // 'validate' closure -- usually a string or int, or 0 if no representation is // appropriate or available. private closure validate_key_rule; void set_astar_validate_key_rule(closure val) { validate_key_rule = val; } closure query_astar_validate_key_rule() { return validate_key_rule; } // Scheduling rule // // The astar module uses its scheduling rule to request a future call of a // function, for purposes of continuing pathfinding that cannot be completed // in the current execution because of a run limit rule or other conditions. // The rule is passed arguments of 1) the function that should be called // 2) approximately how many seconds later it should be called 3) an astar // pathfind data structure that should be passed as the argument to the // function called. The default scheduling rule is #'call_out, i.e. the // purpose of this rule is to allow you to override the module's default usage // of call_out() for scheduling purposes. private closure scheduling_rule; void set_astar_scheduling_rule(closure val) { scheduling_rule = val; } closure query_astar_scheduling_rule() { return scheduling_rule; } // SECTION: Internal support functions // // These are functions used by the A* module. Instances do not need to // interact with them. // astar_distance() // // Distance retrieval process. The distance rule is allowed to return -1 // if, for whatever reason, it doesn't know how far it is between nodes; // the cost of the new path is set to be one greater than the cost of the // path it extends. private float astar_distance(mixed * pathfind) { if(!distance_rule) return pathfind[Astar_Pathfind_Active_Path][Astar_Path_Distance] + 1.0; mixed res = funcall(distance_rule, pathfind); if(res == -1) return pathfind[Astar_Pathfind_Active_Path][Astar_Path_Distance] + 1.0; else return res; } // astar_key() // // Node key retrieval process. Finds the representation to use for the // node in checking visited status. private mixed astar_key(mixed node) { return node_key_rule ? funcall(node_key_rule, node) : node; } // astar_cached_path() // // Cached path retrieval. private mixed astar_cached_path(mixed * pathfind) { if(!caching) return 0; closure validate = pathfind[Astar_Pathfind_Validate]; mixed validate_key = validate && validate_key_rule && funcall(validate_key_rule, pathfind); if(validate && !validate_key) return 0; mapping validate_cache = cache && cache[validate_key]; if(!validate_cache) return 0; mapping from_cache = validate_cache[astar_key(pathfind[Astar_Pathfind_From])]; if(!from_cache) return 0; mixed entry = from_cache[astar_key(pathfind[Astar_Pathfind_To])]; if(!entry) return 0; entry[Astar_Cache_Hits]++; entry[Astar_Cache_Timestamp] = time(); return entry; } // astar_pathfind_done() // // Internal function for handling the end of a pathfind. private void astar_pathfind_done(mixed * pathfind, mixed result) { pathfind[Astar_Pathfind_Result] = result; if(pathfind[Astar_Pathfind_Callback] && !(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_Silent)) funcall(pathfind[Astar_Pathfind_Callback], pathfind); } // astar_pathfind_close() // // Internal function for handling the end of a pathfind. private void astar_pathfind_close(mixed * pathfind, mixed result) { if(caching && !(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_Uncache)) { // Calculate validate key beforehand in case the callback changes anything that interferes with generating it closure validate = pathfind[Astar_Pathfind_Validate]; mixed validate_key = validate && validate_key_rule && funcall(validate_key_rule, pathfind); astar_pathfind_done(pathfind, result); if((validate_key || !validate) && !(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_Uncache)) { cache ||= ([]); mapping validate_cache = cache[validate_key] ||= ([]); mixed from_key = astar_key(pathfind[Astar_Pathfind_From]); mapping from_cache = validate_cache[from_key] ||= ([]); mixed to_key = astar_key(pathfind[Astar_Pathfind_To]); mixed entry = allocate(Astar_Cache_Fields); entry[Astar_Cache_Path] = pointerp(result) && result; entry[Astar_Cache_Timestamp] = time(); from_cache[to_key] = entry; } } else { astar_pathfind_done(pathfind, result); } } // astar_insert_path() // // Inserts a path into a tracking list at the appropriate place based on its cost. Returns the tracking list, which may have // been extended if there were no empty entries in it. private mixed * astar_insert_path(mixed * track, mixed * path) { int target_index; if(track) { target_index = member(track, 0); if(target_index == -1) { target_index = sizeof(track); track += allocate(Astar_Path_List_Allocation_Chunk); } if(target_index > 0) { float cost = path[Astar_Path_Cost]; int zero_index = target_index; int last_index = zero_index - 1; mixed * last_path = track[last_index]; float last_cost = last_path[Astar_Path_Cost]; int high_index = last_index; float high_cost = last_cost; int low_index = 0; float low_cost = track[low_index][Astar_Path_Cost]; if(cost <= low_cost) { target_index = 0; } else if(cost >= high_cost) { target_index = zero_index; } else { target_index = to_int(cost * last_index / last_cost + 0.5); for(;;) { mixed * check_path = track[target_index]; float check_cost = check_path[Astar_Path_Cost]; if(check_cost > cost) { int prev_index = target_index - 1; if(prev_index < 0) break; mixed * prev_path = track[prev_index]; float prev_cost = prev_path[Astar_Path_Cost]; if(prev_cost <= cost) break; if(high_index > prev_index) { high_index = prev_index; high_cost = prev_cost; } int new_target_index = low_index + to_int((cost - low_cost) * (prev_index - low_index) / (prev_cost - low_cost)); if(new_target_index >= target_index) target_index--; else target_index = new_target_index; } else if(check_cost < cost) { int next_index = target_index + 1; if(next_index >= zero_index) { target_index = zero_index; break; } mixed * next_path = track[next_index]; if(!next_path) raise_error("Internal inconsistency"); float next_cost = next_path[Astar_Path_Cost]; if(next_cost >= cost) { target_index = next_index; break; } if(low_index < next_index) { low_index = next_index; low_cost = next_cost; } int new_target_index = high_index - to_int((high_cost - cost) * (high_index - next_index) / (high_cost - next_cost)); if(new_target_index <= target_index) target_index++; else target_index = new_target_index; } else { break; } } } if(target_index < zero_index) { int index; for(index = zero_index; index > target_index; index--) track[index] = track[index - 1]; } } } else { track = allocate(Astar_Path_List_Allocation_Chunk); target_index = 0; } track[target_index] = path; return track; } // astar_clear_paths() // // Clears stored paths out of a tracking list. private void astar_clear_paths(mixed * track) { int track_size = sizeof(track); int ix; for(ix = 0; ix < track_size; ix++) if(track[ix]) track[ix] = 0; else break; } // astar_pathfinder() // // Performs the actual work of path calculation; takes a fully // initialized (and maybe partially processed) pathfind data structure // as argument. Can resume from any point in the pathfinding // process, which is what lets the algorithm suspend activity when // a run limit is reached and resume via the scheduling rule. private void astar_pathfinder(mixed * pathfind) { if(cycle_process) funcall(cycle_process, pathfind); if(pathfind[Astar_Pathfind_Cycle_Index]) { if(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_Terminate) return astar_pathfind_done(pathfind, Astar_Result_Terminated); if(!(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_Uncache)) { mixed path = astar_cached_path(pathfind); if(path) return astar_pathfind_done(pathfind, path[Astar_Cache_Path] || Astar_Result_Impossible); } } pathfind[Astar_Pathfind_Cycle_Start] = utime(); pathfind[Astar_Pathfind_Cycle_Index]++; pathfind[Astar_Pathfind_Cycle_Iterations] = 0; mixed to_key = astar_key(pathfind[Astar_Pathfind_To]); mixed * extended = 0; for(;;) { pathfind[Astar_Pathfind_Cycle_Iterations]++; if(run_limit_rule && funcall(run_limit_rule, pathfind)) { if(pathfind[Astar_Pathfind_Callback] && !(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_No_Continue)) { pathfind[Astar_Pathfind_Cycle_Index]++; pathfind[Astar_Pathfind_Result] = Astar_Result_Processing; funcall(scheduling_rule, #'astar_pathfinder, 2, pathfind); } else { pathfind[Astar_Pathfind_Result] = Astar_Result_Cut_Off; } return; } mixed * paths = pathfind[Astar_Pathfind_Paths]; int path_track_size = sizeof(paths); // Get the cost of the best path on hand; we only want to deal with paths of this cost. float cost = paths[0][Astar_Path_Cost]; int ix; mixed * final = 0; // Check for possible extensions on as many paths as we have that are at our best cost. for(ix = 0; ix < path_track_size && paths[ix] && paths[ix][Astar_Path_Cost] <= cost; ix++) { mixed * path = paths[ix]; pathfind[Astar_Pathfind_Active_Path] = path; mixed * nodes = path[Astar_Path_Nodes]; mixed * edges = path[Astar_Path_Edges]; int len = path[Astar_Path_Length]; int pos = len - 1; pathfind[Astar_Pathfind_Active_Node] = nodes[pos]; pathfind[Astar_Pathfind_Active_Edge] = pos && edges[pos - 1]; mixed neighbors = 0; mixed neighbor_count; if(neighbor_count_rule && neighbor_by_index_rule) { // Retrieve neighbor count. neighbor_count = funcall(neighbor_count_rule, pathfind); if(!intp(neighbor_count)) raise_error("Invalid return value from neighbor count rule"); if(neighbor_count < 0) { if(pathfind[Astar_Pathfind_Callback] && !(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_No_Continue)) { pathfind[Astar_Pathfind_Cycle_Index]++; pathfind[Astar_Pathfind_Result] = Astar_Result_Processing; funcall(scheduling_rule, #'astar_pathfinder, 2, pathfind); } else { pathfind[Astar_Pathfind_Result] = Astar_Result_Cannot_Continue; } return; } } else { // Retrieve the list of neighbor nodes and edges to reach them. neighbors = funcall(neighbors_rule, pathfind); if(!pointerp(neighbors)) { if(neighbors == Astar_Result_Processing) { if(pathfind[Astar_Pathfind_Callback] && !(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_No_Continue)) { pathfind[Astar_Pathfind_Cycle_Index]++; pathfind[Astar_Pathfind_Result] = Astar_Result_Processing; funcall(scheduling_rule, #'astar_pathfinder, 2, pathfind); } else { pathfind[Astar_Pathfind_Result] = Astar_Result_Cannot_Continue; } return; } else { raise_error("Invalid return value from neighbors rule"); } } neighbor_count = sizeof(neighbors); } int neighbor_ix; mixed node; mixed edge; mixed ncost; for(neighbor_ix = 0; neighbor_ix < neighbor_count; neighbor_ix++) { if(neighbors) { node = neighbors[neighbor_ix][0]; edge = neighbors[neighbor_ix][1]; ncost = neighbors[neighbor_ix][2]; } else { funcall(neighbor_by_index_rule, pathfind, neighbor_ix, &node, &edge, &ncost); // If we don't have a node after all, never mind. if(!node) continue; } mixed key = astar_key(node); // If we've already been here, never mind. mixed * visited = pathfind[Astar_Pathfind_Visited]; int visited_len = pathfind[Astar_Pathfind_Visited_Length]; int visit_ix = member(visited, key); if(visit_ix != -1 && visit_ix < visited_len) continue; mixed mnode = pathfind[Astar_Pathfind_Active_Node]; mixed medge = pathfind[Astar_Pathfind_Active_Edge]; // Register node and edge in pathfind structure. pathfind[Astar_Pathfind_Active_Node] = node; pathfind[Astar_Pathfind_Active_Edge] = edge; // If we have a validation rule for nodes, check against it. if(pathfind[Astar_Pathfind_Validate] && !funcall(pathfind[Astar_Pathfind_Validate], pathfind)) { pathfind[Astar_Pathfind_Active_Node] = mnode; pathfind[Astar_Pathfind_Active_Edge] = medge; continue; } // Okay, then, now we've been here. if(visited_len >= sizeof(visited)) { visited += allocate(Astar_Node_Visit_List_Allocation_Chunk); visited[visited_len] = key; pathfind[Astar_Pathfind_Visited] = visited; } else { visited[visited_len] = key; } pathfind[Astar_Pathfind_Visited_Length] = ++visited_len; // This is now a valid extension. Assemble the extended path with this node added to it, and calculate the // distance and cost. mixed * active_path = pathfind[Astar_Pathfind_Active_Path]; mixed * ext_path = deep_copy(active_path); if(len >= sizeof(nodes)) { nodes += allocate(Astar_Node_Edge_List_Allocation_Chunk); edges += allocate(Astar_Node_Edge_List_Allocation_Chunk); } else { nodes = copy(nodes); edges = copy(edges); } nodes[len] = node; edges[len - 1] = edge; ext_path[Astar_Path_Nodes] = nodes; ext_path[Astar_Path_Edges] = edges; ext_path[Astar_Path_Length] = len + 1; ext_path[Astar_Path_Distance] = astar_distance(pathfind); // The cost of the extended path is its distance from the target node, plus the portion of the base path's cost // that is not based on its distance, plus the cost of the edge. ext_path[Astar_Path_Cost] = active_path[Astar_Path_Cost] - active_path[Astar_Path_Distance] + ext_path[Astar_Path_Distance] + ncost; // If the node we just reached is the target, add this path to the list of final paths and stop tracking // path extensions; otherwise, track path extension, if they are being tracked. if(completion_rule ? funcall(completion_rule, pathfind) : (key == to_key)) final = astar_insert_path(final, ext_path); else if(!final) extended = astar_insert_path(extended, ext_path); pathfind[Astar_Pathfind_Active_Node] = mnode; pathfind[Astar_Pathfind_Active_Edge] = medge; } } // If we have any final paths, we're done. if(final) return astar_pathfind_close(pathfind, final[0]); // Get rid of the paths that we examined, and add the newly extended paths to our list. if(extended && extended[0]) { if(ix >= path_track_size || !paths[ix]) { paths = extended; extended = 0; } else { int shift; for(shift = 0; shift < path_track_size; shift++) { if(!paths[shift]) break; int shift_ix = shift + ix; paths[shift] = (shift_ix >= path_track_size) ? 0 : paths[shift_ix]; } int transfer; int extended_track_size = sizeof(extended); for(transfer = 0; transfer < extended_track_size; transfer++) { mixed * transfer_path = extended[transfer]; if(!transfer_path) break; paths = astar_insert_path(paths, transfer_path); } astar_clear_paths(extended); } } else { // If we no longer have any paths to examine, we're out of luck. if(ix <= 0) return astar_pathfind_close(pathfind, Astar_Result_Impossible); int shift; for(shift = 0; shift < path_track_size; shift++) { if(!paths[shift]) break; int shift_ix = shift + ix; paths[shift] = (shift_ix >= path_track_size) ? 0 : paths[shift_ix]; if(!shift && !paths[shift]) return astar_pathfind_close(pathfind, Astar_Result_Impossible); } } pathfind[Astar_Pathfind_Paths] = paths; if(pathfind[Astar_Pathfind_Control_Flags] & Astar_Pathfind_Control_Flag_Terminate) return astar_pathfind_done(pathfind, Astar_Result_Terminated); } } // SECTION: Operational interface // astar_find_path() // // Performs pathfinding starting with the 'from' node, searching for the // 'to' node. // // 'validate' can be used to provide a closure that checks whether a node // is valid to include in the path. It is called with the astar pathfind // data structure as argument; see the notes on the neighbor rule for more // on this. Fields of particular concern for the validation call are: // // pathfind[Astar_Pathfind_Active_Node] // Set to the node being examined. // pathfind[Astar_Pathfind_Active_Edge] // The edge to reach the active node from the previous node. // pathfind[Astar_Pathfind_Active_Path] // The entire path leading to the previous node; this is a path // data structure as defined by the Astar_Path_* macros in astar.h. // // The 'validate' function should return true if the node being examined // should be included in the path. // // 'callback' can be used to provide a closure to be called at the completion // of pathfinding. It will be called with the astar pathfind data structure // as argument; the most relevant field is: // // pathfind[Astar_Pathfind_Result] // The final result of the pathfind; an astar path data structure // (from astar.h) if pathfinding was successful, or an Astar_Result_* // code (also from astar.h) for their respective conditions. // // Providing a callback allows pathfinding to be carried out in parts via // the scheduling rule; if no callback is provided, or if the control flag // Astar_Control_Flag_No_Continue is used, then pathfinding will execute // until it reaches the run limit defined by the instance, if any, or it hits // the driver eval limit. With a callback and the control flag not in use, // pathfinding runs until it reaches the run limit, then continues via the // scheduling rule, normally two seconds later, continuing in this fashion // until a path is found, all possible paths have been searched, or some other // condition terminates the pathfind. // // The fifth argument is an integer that may contain control flags, as defined // by the Astar_Pathfind_Control_Flag_* macros in astar.h. // // The sixth argument is an arbitrary user-supplied value. It will be // passed to 'callback' after the path argument, and is accessible as // Astar_Pathfind_Extra in the pathfind data structure. // // The return value is the astar pathfind data structure (from astar.h) that // defines the pathfind request. It can be manipulated (for example, by doing // pathfind[Astar_Pathfind_Control_Flags] |= Astar_Pathfind_Control_Flag_Terminate) // to alter or inspect the pathfind request. If pathfind[Astar_Pathfind_Result] // is anything other than Astar_Result_Processing, the pathfind request has // completed. varargs mixed * astar_find_path(mixed from, mixed to, closure validate, closure callback, int control_flags, mixed extra) { // Constrain our representation of our 'from' and 'to' nodes if(node_rule) { from = funcall(node_rule, from); to = funcall(node_rule, to); } // Default scheduling rule to #'call_out if none has been set scheduling_rule ||= #'call_out; // Set up pathfinding data structure mixed * pathfind = allocate(Astar_Pathfind_Fields); pathfind[Astar_Pathfind_From] = from; pathfind[Astar_Pathfind_To] = to; pathfind[Astar_Pathfind_Validate] = validate; pathfind[Astar_Pathfind_Callback] = callback; pathfind[Astar_Pathfind_Extra] = extra; pathfind[Astar_Pathfind_Start_Time] = utime(); pathfind[Astar_Pathfind_Cycle_Index] = 0; pathfind[Astar_Pathfind_Active_Node] = from; pathfind[Astar_Pathfind_Active_Edge] = 0; pathfind[Astar_Pathfind_Control_Flags] = control_flags; // Check for origin and destination being the same if(from == to) { pathfind[Astar_Pathfind_Result] = Astar_Result_No_Op; if(callback) funcall(callback, pathfind); return pathfind; } // Check for a cached path mixed path = astar_cached_path(pathfind); if(path) { pathfind[Astar_Pathfind_Result] = path[Astar_Cache_Path] || Astar_Result_Impossible; if(callback) funcall(callback, pathfind); return pathfind; } // Set up our starting point based on the 'from' node mixed * start = allocate(Astar_Path_Fields); pathfind[Astar_Pathfind_Active_Path] = start; mixed * start_nodes = allocate(Astar_Node_Edge_List_Allocation_Chunk); mixed * start_edges = allocate(Astar_Node_Edge_List_Allocation_Chunk - 1); start_nodes[0] = from; start[Astar_Path_Nodes] = start_nodes; start[Astar_Path_Edges] = start_edges; start[Astar_Path_Distance] = start[Astar_Path_Cost] = astar_distance(pathfind); start[Astar_Path_Length] = 1; // Starting point becomes our path list, mapping to track where we've visited starts out populated with starting node pathfind[Astar_Pathfind_Paths] = astar_insert_path(0, start); mixed * start_visited = allocate(Astar_Node_Visit_List_Allocation_Chunk); start_visited[0] = astar_key(from); pathfind[Astar_Pathfind_Visited] = start_visited; pathfind[Astar_Pathfind_Visited_Length] = 1; astar_pathfinder(pathfind); return pathfind; } // astar_clear_cache() // // Clears out the contents of the cache. This can be useful for allowing // caching in spaces that you otherwise couldn't use caching with because // some sort of dynamicism in them (changing exits, shifting graph // connectivity, etc.) would invalidate cached paths. Using this, you can // call astar_clear_cache() when changes occur, so that no outdated paths // will be returned. void astar_clear_cache() { if(!caching) raise_error("astar_clear_cache() called with caching off"); cache = 0; } // astar_prune_cache() // // Prunes entries from the cache. The optional argument, threshold, // influences how aggressively the pruning occurs. A cache entry will be // dropped if its most recent hit was longer ago, in seconds, than // 'threshold' plus the number of hits it has had times a tuning factor. // 'threshold' defaults to Astar_Prune_Cache_Default_Threshold, 7200. // The tuning factor is Astar_Prune_Cache_Hit_Factor, 60. // // It is generally appropriate to call this function from reset() of an // object that inherits this module and uses caching. varargs void astar_prune_cache(int threshold) { if(!caching) raise_error("astar_prune_cache() called with caching off"); if(!cache) return; threshold ||= Astar_Prune_Cache_Default_Threshold; int cutoff = time() - threshold; foreach(string validate_key, mapping validate_cache : cache) { foreach(string from_key, mapping from_cache : validate_cache) { foreach(string to_key, mixed entry : from_cache) if(entry[Astar_Cache_Timestamp] + (entry[Astar_Cache_Hits] * Astar_Prune_Cache_Hit_Factor) < cutoff) map_delete(from_cache, to_key); if(!sizeof(from_cache)) map_delete(validate_cache, from_key); } if(!sizeof(validate_cache)) map_delete(cache, validate_key); } }