Merge pull request #592 from Yvar/Astarfix

A-star replacement
This commit is contained in:
Carnie
2013-05-19 20:10:47 -07:00
5 changed files with 142 additions and 183 deletions
+10 -6
View File
@@ -134,12 +134,16 @@ proc/listclearnulls(list/list)
L.Cut(picked,picked+1) //Cut is far more efficient that Remove()
//Returns the top(last) element from the list and removes it from the list (typical stack function)
/proc/pop(list/listfrom)
if (listfrom.len > 0)
var/picked = listfrom[listfrom.len]
listfrom.len--
return picked
return null
/proc/pop(list/L)
if(L.len)
. = L[L.len]
L.len--
/proc/sorted_insert(list/L, thing, comparator)
var/pos = L.len
while(pos > 0 && call(comparator)(thing, L[pos]) > 0)
pos--
L.Insert(pos+1, thing)
/*
* Sorting
+1 -1
View File
@@ -117,7 +117,7 @@ datum/controller/lighting/proc/Recover()
var/msg = "## DEBUG: [time2text(world.timeofday)] lighting_controller restarted. Reports:\n"
for(var/varname in lighting_controller.vars)
switch(varname)
if("tag","bestF","type","parent_type","vars") continue
if("tag","type","parent_type","vars") continue
else
var/varval1 = lighting_controller.vars[varname]
var/varval2 = vars[varname]
+1 -1
View File
@@ -287,7 +287,7 @@ datum/controller/game_controller/proc/Recover() //Mostly a placeholder for now.
var/msg = "## DEBUG: [time2text(world.timeofday)] MC restarted. Reports:\n"
for(var/varname in master_controller.vars)
switch(varname)
if("tag","bestF","type","parent_type","vars") continue
if("tag","type","parent_type","vars") continue
else
var/varval = master_controller.vars[varname]
if(istype(varval,/datum))
+126 -171
View File
@@ -1,185 +1,140 @@
//This file was auto-corrected by findeclaration.exe on 25.5.2012 20:42:31
/proc/cmp_pathnodes(pathnode/A, pathnode/B)
return (A.f_score - B.f_score)
/*
A Star pathfinding algorithm
Returns a list of tiles forming a path from A to B, taking dense objects as well as walls, and the orientation of
windows along the route into account.
Use:
your_list = AStar(start location, end location, adjacent turf proc, distance proc)
For the adjacent turf proc i wrote:
pathnode
var/turf/me //turf
var/pathnode/parent //pathnode this turf was discovered from
var/f_score //total path-length of this turf
var/g_score //length of path to this turf
New(m,p,f,g)
me = m
parent = p
f_score = f
g_score = g
/********************
/A* pathfinding algorithm
/This version updated May 2013 by Yvarov
/Returns a list of turfs along the discovered path, in order from start to finish.
/
/A* is more complex than can be easily explained here, but basically
/it checks tiles, assigning a score f = g + h where
/g = distance from start & h = minimum distance to end, and repeats with
/new tiles starting with the smallest f_score. It's capable of finding
/a path (assuming one exists), without getting stuck in wrong turns or
/unnecessarily checking unpromising tiles.
/
/The syntax for calling remains roughly the same as the version this replaces,
/minus two arguments (ones that were never given by any of
/the procs that called it anyway).
Possible procs to use for adjacency (there may be others):
/turf/proc/AdjacentTurfs
And for the distance one i wrote:
/turf/proc/Distance
So an example use might be:
/turf/proc/AdjacentTurfsSpace
/turf/proc/CardinalTurfsWithAccess (best for most occasions, four-directional)
src.path_list = AStar(src.loc, target.loc, /turf/proc/AdjacentTurfs, /turf/proc/Distance)
Possible procs to use for adjacency:
/turf/proc/Distance_cardinal (requires four-directional adjacency proc)
/turf/proc/Distance (requires eight-directional adjacency proc)
Note: The path is returned starting at the END node, so i wrote reverselist to reverse it for ease of use.
Arguments:
/turf/start_turf: The turf to start the path from.
/turf/dest_turf: The turf to finish the path on.
adjacent_proc: The proc used to find adjacent tiles. When called, id is provided as an argument.
For the best path to be found, it must only output tiles that could be reached in
one move AND aren't occupied by something else, like a wall.
distance_proc: Formula to use for calculating distance.
maxnodes: Maximum _depth_ of nodes to examine; currently used only by disease.
Not equal to the number of nodes to examine (lpct, internal) or the maximum path
length returned (max_length below).
max_length: If the path is longer, return only this many elements of it (starting from the front).
It can only be shortened after finding the whole path, so doesn't save any time,
but as different values were provided throughout the code it's kept for compatibility.
id: Supplied to the adjacency proc for doors/etc. Default is null.
/var/turf/exclude: Turfs to avoid when finding a path. Default is null.
src.path_list = reverselist(src.pathlist)
Nodes not kept at all from previous version (which were never supplied):
"Mintargetdist: Minimum distance to the target before path returns, could be used to get
near a target, but not right to it - for an AI mob with a gun, for example."
"Minnodedist: Minimum number of nodes to return in the path, could be used to give a path a minimum
length to avoid portals or something i guess?? Not that they're counted right now but w/e."
*********************/
proc/AStar(turf/start_turf, turf/dest_turf, adjacent_proc, distance_proc, maxnodes, max_length, id=null, var/turf/exclude=null)
set background=1 //allows proc to sleep should it take more than a single frame to complete
var/const/DEPTH_BIAS = 0.5 //preference of discovered paths. decrease with extreme caution and don't set >1!
//var/evaluations = 0 //Total number of tiles examined, including discards and duplicates. Used for debugging
var/lpct = 0 //Loop counter - turfs properly examined. Debugging and infinite loop prevention.
var/list/checked = list() //list of pathnodes already examined
var/list/priorityqueue = list() //list of pathnodes not yet examined, sorted descending by f_score
var/pathnode/current //current pathnode being examined
var/next_g //calculated g_score of a potential turf
var/next_f //calculated f_score of a potential turf
var/skip //used for internal logic branching, don't touch.
var/pathnode/temp //temporary pathnode used for comparison, don't touch.
var/list/path = list() //return value
Then to start on the path, all you need to do it:
Step_to(src, src.path_list[1])
src.path_list -= src.path_list[1] or equivilent to remove that node from the list.
//sanity check to avoid a huge waste of CPU time on impossible tasks
for(var/obj/O in dest_turf)
if(O.density && (!istype(O, /obj/machinery/door) || !(O.flags & ON_BORDER))) //something is blocking the destination
return path
priorityqueue += new /pathnode(start_turf, null, call(start_turf,distance_proc)(dest_turf), 0)//add the starting turf and manhattan distance to the queue!
Optional extras to add on (in order):
MaxNodes: The maximum number of nodes the returned path can be (0 = infinite)
Maxnodedepth: The maximum number of nodes to search (default: 30, 0 = infinite)
Mintargetdist: Minimum distance to the target before path returns, could be used to get
near a target, but not right to it - for an AI mob with a gun, for example.
Minnodedist: Minimum number of nodes to return in the path, could be used to give a path a minimum
length to avoid portals or something i guess?? Not that they're counted right now but w/e.
*/
for(lpct=0, lpct<1800, lpct++) //maximum for DEPTH_BIAS=0.5 is 958 loops to Research Division (originally 2743)
if(!priorityqueue.len) break
// Modified to provide ID argument - supplied to 'adjacent' proc, defaults to null
// Used for checking if route exists through a door which can be opened
current = pop(priorityqueue)
checked.Add(current)
// Also added 'exclude' turf to avoid travelling over; defaults to null
next_g = current.g_score + DEPTH_BIAS
//if(maxnodes && next_g > (maxnodes*DEPTH_BIAS)) continue
if(!current) //no more possibilities, or an unhandled error
//world << "Impossible path"
return path //FAILURE
PriorityQueue
var/L[]
var/cmp
New(compare)
L = new()
cmp = compare
proc
IsEmpty()
return !L.len
Enqueue(d)
var/i
var/j
L.Add(d)
i = L.len
j = i>>1
while(i > 1 && call(cmp)(L[j],L[i]) > 0)
L.Swap(i,j)
i = j
j >>= 1
if(current.me == dest_turf) //reached the destination
while(current) //recreate path by looping through parents
path.Insert(1, current.me)
current = current.parent
//world << "Path found! ([path.len] steps, [lpct] loops, [evaluations] evaluations)"
Dequeue()
if(!L.len) return 0
. = L[1]
Remove(1)
path.len = min(path.len, max_length-1)
return path //SUCCESS
Remove(i)
if(i > L.len) return 0
L.Swap(i,L.len)
L.Cut(L.len)
if(i < L.len)
_Fix(i)
_Fix(i)
var/child = i + i
var/item = L[i]
while(child <= L.len)
if(child + 1 <= L.len && call(cmp)(L[child],L[child + 1]) > 0)
child++
if(call(cmp)(item,L[child]) > 0)
L[i] = L[child]
i = child
else
break
child = i + i
L[i] = item
List()
var/ret[] = new()
var/copy = L.Copy()
while(!IsEmpty())
ret.Add(Dequeue())
L = copy
return ret
RemoveItem(i)
var/ind = L.Find(i)
if(ind)
Remove(ind)
PathNode
var/datum/source
var/PathNode/prevNode
var/f
var/g
var/h
var/nt // Nodes traversed
New(s,p,pg,ph,pnt)
source = s
prevNode = p
g = pg
h = ph
f = g + h
source.bestF = f
nt = pnt
var/adjacent = call(current.me,adjacent_proc)(id)
for(var/next_turf in adjacent)
//evaluations++
if(next_turf == exclude)
continue
skip = 0
next_f = next_g + call(next_turf,distance_proc)(dest_turf)
var/i = 0
while(i < checked.len && !skip)
i++
temp = checked[i]
if(temp.me == next_turf) //if this turf has already been checked
skip = 1
if(temp.f_score > next_f) //BUT the current path to it is better
checked[i] = new /pathnode(next_turf, current, next_f, next_g)
//then update it
if(!skip) //if it hasn't been checked
i = 0
while(i < priorityqueue.len && !skip)
i++
temp = priorityqueue[i]
if(temp.me == next_turf) //if this turf was already on the openlist
if(temp.f_score > next_f) //BUT the current path to it is better
priorityqueue.Cut(i,i+1) //then remove it
skip = 2
else
skip = 1 //otherwise leave it and don't add this version
if(!(skip % 2))
sorted_insert(priorityqueue, new /pathnode(next_turf, current, next_f, next_g), /proc/cmp_pathnodes)
datum
var/bestF
proc
PathWeightCompare(PathNode/a, PathNode/b)
return a.f - b.f
AStar(start,end,adjacent,dist,maxnodes,maxnodedepth = 30,mintargetdist,minnodedist,id=null, var/turf/exclude=null)
// world << "A*: [start] [end] [adjacent] [dist] [maxnodes] [maxnodedepth] [mintargetdist], [minnodedist] [id]"
var/PriorityQueue/open = new /PriorityQueue(/proc/PathWeightCompare)
var/closed[] = new()
var/path[]
start = get_turf(start)
if(!start) return 0
open.Enqueue(new /PathNode(start,null,0,call(start,dist)(end)))
while(!open.IsEmpty() && !path)
{
var/PathNode/cur = open.Dequeue()
closed.Add(cur.source)
var/closeenough
if(mintargetdist)
closeenough = call(cur.source,dist)(end) <= mintargetdist
if(cur.source == end || closeenough)
path = new()
path.Add(cur.source)
while(cur.prevNode)
cur = cur.prevNode
path.Add(cur.source)
break
var/L[] = call(cur.source,adjacent)(id)
if(minnodedist && maxnodedepth)
if(call(cur.source,minnodedist)(end) + cur.nt >= maxnodedepth)
continue
else if(maxnodedepth)
if(cur.nt >= maxnodedepth)
continue
for(var/datum/d in L)
if(d == exclude)
continue
var/ng = cur.g + call(cur.source,dist)(d)
if(d.bestF)
if(ng + call(d,dist)(end) < d.bestF)
for(var/i = 1; i <= open.L.len; i++)
var/PathNode/n = open.L[i]
if(n.source == d)
open.Remove(i)
break
else
continue
open.Enqueue(new /PathNode(d,cur,ng,call(d,dist)(end),cur.nt+1))
if(maxnodes && open.L.len > maxnodes)
open.L.Cut(open.L.len)
}
var/PathNode/temp
while(!open.IsEmpty())
temp = open.Dequeue()
temp.source.bestF = 0
while(closed.len)
temp = closed[closed.len]
temp.bestF = 0
closed.Cut(closed.len)
if(path)
for(var/i = 1; i <= path.len/2; i++)
path.Swap(i,path.len-i+1)
return path
//world << "Didn't find a path within [evaluations] evaluations and [lpct] loops."
return path //FAILURE
//This return should only be reached if the path is too long, and the while loop gives up.
+4 -4
View File
@@ -201,7 +201,7 @@ var/global/mulebot_count = 0
if(5,6)
dat += "<span class='average'>Calculating navigation path</span>"
if(7)
dat += "<span class='bad'>Unable to locate destination</span>"
dat += "<span class='bad'>Unable to reach destination</span>"
dat += "</div>"
dat += "<b>Current Load:</b> [load ? load.name : "<i>none</i>"]<BR>"
@@ -225,7 +225,7 @@ var/global/mulebot_count = 0
if(load)
dat += "<A href='byond://?src=\ref[src];op=unload'>Unload Now</A><BR>"
dat += "<div class='notice'>The maintenance hatch is closed</div>"
dat += "<div class='notice'>The maintenance hatch is closed.</div>"
else
if(!ai)
@@ -590,7 +590,7 @@ var/global/mulebot_count = 0
blockcount++
mode = 4
if(blockcount == 3)
src.visible_message("[src] makes an annoyed buzzing sound", "You hear an electronic buzzing sound.")
src.visible_message("[src] makes an annoyed buzzing sound.", "You hear an electronic buzzing sound.")
playsound(src.loc, 'sound/machines/buzz-two.ogg', 50, 0)
if(blockcount > 5) // attempt 5 times before recomputing
@@ -608,7 +608,7 @@ var/global/mulebot_count = 0
return
return
else
src.visible_message("[src] makes an annoyed buzzing sound", "You hear an electronic buzzing sound.")
src.visible_message("[src] makes an annoyed buzzing sound.", "You hear an electronic buzzing sound.")
playsound(src.loc, 'sound/machines/buzz-two.ogg', 50, 0)
//world << "Bad turf."
mode = 5