PageRenderTime 47ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/resources/posts/2010-06-01-path-finding-using-astar-in-clojure.org

https://github.com/nakkaya/nakkaya.com
Org | 277 lines | 241 code | 36 blank | 0 comment | 9 complexity | 32602482ee241e6fa7b3ab31e8eac1ef MD5 | raw file
Possible License(s): BSD-3-Clause, GPL-3.0
  1. #+title: Path Finding Using A-Star in Clojure
  2. #+tags: clojure path-finding
  3. For a recent project, I had to implement A* (A-Star) in Clojure, since
  4. it's a very popular path finding algorithm used in gaming I thought it
  5. might be interesting to other clojurians too.
  6. AStar uses best-first search to find the the least-cost path from a
  7. given initial node to one goal node (out of one or more possible
  8. goals). Functions,
  9. - *g(x)* - cost of getting to that node from starting node.
  10. - *h(x)* - cost of getting to the goal node from current node.
  11. - *f(x)* - *g(x)+h(x)*
  12. are used to determine the order in which search visits nodes. Beginning
  13. with the start node, we keep track of two lists, open and closed, open
  14. list contains the list of nodes to traverse sorted by their *f(x)* cost,
  15. closed list contains the list of nodes that we have processed. At each
  16. step, algorithm removes the first node on the open list, calculate
  17. *f(x)*, *g(x)* and *h(x)* values for its neighbors and add the ones that
  18. are not on the closed list to the open list. This is done until goal
  19. node has been found or no nodes are left on the open list.
  20. In a nutshell we will,
  21. - Add the starting node to the open list.
  22. - Loop
  23. - Remove the node with the lowest *f(x)* from the open list.
  24. - Add it to closed list.
  25. - Calculate 8 adjacent squares.
  26. - Filter neighbors that are not on the closed list and walkable.
  27. - For each square
  28. - If it is not on the open list, calculate F, G and H costs, make
  29. the current square parent of this square and add it open list.
  30. - If it is on the open list, check to see if this path to that
  31. square is better using the G cost, a lower G indicates a better
  32. path if so change its parent to this square and recalculate F G and H
  33. costs.
  34. - Until
  35. - Target node is added to the closed list indicating a path
  36. has been found.
  37. - No more nodes left in the open list indicating there is no path
  38. between nodes.
  39. #+begin_src clojure
  40. (def maze1 [[0 0 0 0 0 0 0]
  41. [0 0 0 1 0 0 0]
  42. [0 0 0 1 0 0 0]
  43. [0 0 0 1 0 0 0]
  44. [0 0 0 0 0 0 0]])
  45. #+end_src
  46. Surface is represented using a 2D vector of 0s and 1s, 0 denoting
  47. walkable nodes and 1 denoting non walkable nodes.
  48. #+begin_src clojure
  49. (defn manhattan-distance [[x1 y1] [x2 y2]]
  50. (+ (Math/abs ^Integer (- x2 x1)) (Math/abs ^Integer (- y2 y1))))
  51. (defn cost [curr start end]
  52. (let [g (manhattan-distance start curr)
  53. h (manhattan-distance curr end)
  54. f (+ g h)]
  55. [f g h]))
  56. #+end_src
  57. Quality of the path found will depend on the distance function used to
  58. calculate F, G, and H costs, for this implementation I choose to use
  59. [[http://en.wikipedia.org/wiki/Taxicab_geometry][Manhattan distance]] since it is cheaper to calculate then [[http://en.wikipedia.org/wiki/Euclidean_distance][Euclidean
  60. distance]] but keep in mind that different distance metrics will produce
  61. different paths so depending on your condition expensive metrics can
  62. produce more natural looking paths.
  63. #+begin_src clojure
  64. (defn edges [map width height closed [x y]]
  65. (for [tx (range (- x 1) (+ x 2))
  66. ty (range (- y 1) (+ y 2))
  67. :when (and (>= tx 0)
  68. (>= ty 0)
  69. (<= tx width)
  70. (<= ty height)
  71. (not= [x y] [tx ty])
  72. (not= (nth (nth map ty) tx) 1)
  73. (not (contains? closed [tx ty])))]
  74. [tx ty]))
  75. #+end_src
  76. For each node we take from the open list, we need to build a list of
  77. nodes around it. We filter them by checking if the node contains a 1
  78. in its place on the map which means we can't go over it or it is
  79. already in the closed list which means we have already looked at it.
  80. #+begin_src clojure
  81. (defn path [end parent closed]
  82. (reverse
  83. (loop [path [end parent]
  84. node (closed parent)]
  85. (if (nil? node)
  86. path
  87. (recur (conj path node) (closed node))))))
  88. #+end_src
  89. When we hit our target node, we need to work backwards starting from
  90. target node, go from each node to its parent until we reach the starting
  91. node. That is our path.
  92. #+begin_src clojure
  93. (use '[clojure.data.priority-map])
  94. (defn search
  95. ([map start end]
  96. (let [[sx sy] start
  97. [ex ey] end
  98. open (priority-map-by
  99. (fn [x y]
  100. (if (= x y)
  101. 0
  102. (let [[f1 _ h1] x
  103. [f2 _ h2] y]
  104. (if (= f1 f2)
  105. (if (< h1 h2) -1 1)
  106. (if (< f1 f2) -1 1)))))
  107. start (cost start start end))
  108. closed {}
  109. width (-> map first count dec)
  110. height (-> map count dec)]
  111. (when (and (not= (nth (nth map sy) sx) 1)
  112. (not= (nth (nth map ey) ex) 1))
  113. (search map width height open closed start end))))
  114. ([map width height open closed start end]
  115. (if-let [[coord [_ _ _ parent]] (peek open)]
  116. (if-not (= coord end)
  117. (let [closed (assoc closed coord parent)
  118. edges (edges map width height closed coord)
  119. open (reduce
  120. (fn [open edge]
  121. (if (not (contains? open edge))
  122. (assoc open edge (conj (cost edge start end) coord))
  123. (let [[_ pg] (open edge)
  124. [nf ng nh] (cost edge start end)]
  125. (if (< ng pg)
  126. (assoc open edge (conj [nf ng nh] coord))
  127. open))))
  128. (pop open) edges)]
  129. (recur map width height open closed start end))
  130. (path end parent closed)))))
  131. #+end_src
  132. Search function is where it all happens and it pretty much summarizes
  133. all of the above steps. Open list is a priority map that will keep its
  134. items sorted by /f/ when there is a tie it is broken using the /h/
  135. value, closed is a map of nodes to parents.
  136. We keep calling search until no elements are left on the open list or
  137. first node on the open list is our goal node. Unless we are done we
  138. remove the first item on the open list, put it to closed list and
  139. process nodes around it.
  140. After we get the list of adjacent nodes, they need to be added to the open
  141. list for further exploration, for nodes that are not on the open list,
  142. we calculate their costs and append them to the open vector, for nodes
  143. that are already on the open list, we check which one, the one on the
  144. open list or the one we just calculated has a lower G cost if the new
  145. one has a lower G cost we replace the one on the list with the new
  146. one.
  147. #+begin_src clojure
  148. (defn draw-map [area start end]
  149. (let [path (into #{} (time (search area start end)))
  150. area (map-indexed
  151. (fn [idx-row row]
  152. (map-indexed
  153. (fn [idx-col col]
  154. (cond (contains? path [idx-col idx-row]) \X
  155. (= 1 col) \#
  156. :default \space))
  157. row))
  158. area)]
  159. (doseq [line area]
  160. (println line))))
  161. #+end_src
  162. #+begin_src clojure
  163. (def maze1 [[0 0 0 0 0 0 0]
  164. [0 0 0 1 0 0 0]
  165. [0 0 0 1 0 0 0]
  166. [0 0 0 1 0 0 0]
  167. [0 0 0 0 0 0 0]])
  168. (draw-map maze1 [1 2] [5 2])
  169. #+end_src
  170. #+begin_example
  171. astar.core=> "Elapsed time: 10.938 msecs"
  172. ( X )
  173. ( X # X )
  174. ( X # X )
  175. ( # )
  176. ( )
  177. #+end_example
  178. #+begin_src clojure
  179. (def maze2 [[0 0 0 0 0 0 0]
  180. [0 0 1 1 1 0 0]
  181. [0 0 0 1 0 0 0]
  182. [0 0 0 1 0 0 0]
  183. [0 0 0 1 0 0 0]])
  184. (draw-map maze2 [1 3] [5 2])
  185. #+end_src
  186. #+begin_example
  187. astar.core=> "Elapsed time: 10.162 msecs"
  188. ( X X X )
  189. ( X # # # X )
  190. ( X # X )
  191. ( X # )
  192. ( # )
  193. #+end_example
  194. #+begin_src clojure
  195. (def maze3 [[0 1 0 0 0 1 0]
  196. [0 1 0 1 0 1 0]
  197. [0 1 0 1 0 1 0]
  198. [0 1 0 1 0 1 0]
  199. [0 0 0 1 0 0 0]])
  200. (draw-map maze3 [0 0] [6 0])
  201. #+end_src
  202. #+begin_example
  203. astar.core=> "Elapsed time: 8.98 msecs"
  204. (X # X # X)
  205. (X # X # X # X)
  206. (X # X # X # X)
  207. (X # X # X # X)
  208. ( X # X )
  209. #+end_example
  210. #+begin_src clojure
  211. (def maze4 [[0 0 0 0 0 0 0 0]
  212. [1 1 1 1 1 1 1 0]
  213. [0 0 0 1 0 0 0 0]
  214. [0 0 0 1 0 0 0 0]
  215. [0 0 0 1 0 0 0 0]
  216. [0 0 0 1 1 1 0 1]
  217. [0 0 0 0 0 1 0 1]
  218. [0 0 0 0 0 1 0 1]
  219. [0 0 0 0 0 0 0 1]
  220. [1 1 1 1 0 1 1 1]
  221. [0 0 0 1 0 0 0 0]
  222. [0 0 0 1 0 0 0 0]
  223. [0 0 0 0 0 0 0 0]])
  224. (draw-map maze4 [0 0] [0 12])
  225. #+end_src
  226. #+begin_example
  227. astar.core=> "Elapsed time: 20.136 msecs"
  228. (X X X X X X X )
  229. (# # # # # # # X)
  230. ( # X )
  231. ( # X )
  232. ( # X )
  233. ( # # # X #)
  234. ( # X #)
  235. ( # X #)
  236. ( X #)
  237. (# # # # X # # #)
  238. ( # X )
  239. ( # X )
  240. (X X X X )
  241. #+end_example