lib/a_star: fix check for max_cost (is more reliable)
[nit.git] / lib / a_star.nit
1 # This file is part of NIT (http://www.nitlanguage.org).
2 #
3 # Copyright 2011-2013 Alexis Laferrière <alexis.laf@xymus.net>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Services related to pathfinding of graphs using A*
18 # A single graph may have different properties according to the `PathContext` used
19 #
20 # Usage:
21 #
22 # # Weighted graph (letters are nodes, digits are weights):
23 # #
24 # # a -2- b
25 # # / /
26 # # 3 1
27 # # / /
28 # # c -3- d -8- e
29 # #
30 # var graph = new Graph[Node,WeigthedLink[Node]]
31 #
32 # var na = new Node(graph)
33 # var nb = new Node(graph)
34 # var nc = new Node(graph)
35 # var nd = new Node(graph)
36 # var ne = new Node(graph)
37 #
38 # var lab = new WeightedLink(graph, na, nb, 2)
39 # var lac = new WeightedLink(graph, na, nc, 3)
40 # var lbd = new WeightedLink(graph, nb, nd, 1)
41 # var lcd = new WeightedLink(graph, nc, nd, 3)
42 # var lde = new WeightedLink(graph, nd, ne, 8)
43 #
44 # var context = new WeightedPathContext(graph)
45 #
46 # var path = na.path_to(ne, 100, context)
47 # assert path != null else print "No possible path"
48 #
49 # while not path.at_end_of_path do
50 # print path.step
51 # end
52 module a_star
53
54 redef class Object
55 protected fun debug_a_star: Bool do return false
56 private fun debug(msg: String) do if debug_a_star then
57 stderr.write "a_star debug: {msg}\n"
58 end
59 end
60
61 # General graph node
62 class Node
63 type N: Node
64
65 # parent graph
66 var graph: Graph[N, Link]
67
68 init(graph: Graph[N, Link])
69 do
70 self.graph = graph
71 graph.add_node(self)
72 end
73
74 # adjacent nodes
75 var links: Set[Link] = new HashSet[Link]
76
77 # used to check if node has been searched in one pathfinding
78 private var last_pathfinding_evocation: Int = 0
79
80 # cost up to in current evocation
81 # lifetime limited to evocation of path_to
82 private var best_cost_up_to: Int = 0
83
84 # source node
85 # lifetime limited to evocation of path_to
86 private var best_source: nullable N = null
87
88 # is in frontier or buckets
89 # lifetime limited to evocation of path_to
90 private var open: Bool = false
91
92
93 # Main functionnality, returns path from `self` to `dest`
94 fun path_to(dest: Node, max_cost: Int, context: PathContext): nullable Path[N]
95 do
96 var cost = 0
97
98 var nbr_buckets = context.worst_cost + context.worst_heuristic_cost + 1
99 var buckets = new Array[List[Node]].with_capacity(nbr_buckets)
100
101 for i in [0 .. nbr_buckets[ do
102 buckets.add(new List[Node])
103 end
104
105 graph.pathfinding_current_evocation += 1
106
107 # open source node
108 buckets[0].add(self)
109 open = true
110 self.last_pathfinding_evocation = graph.pathfinding_current_evocation
111 self.best_cost_up_to = 0
112
113 loop
114 var frontier_node: nullable Node = null
115
116 var bucket_searched: Int = 0
117
118 # find next valid node in frontier/buckets
119 loop
120 var current_bucket = buckets[cost % nbr_buckets]
121
122 if current_bucket.is_empty then # move to next bucket
123 debug "b {cost} {cost % nbr_buckets} {buckets[cost % nbr_buckets].hash}"
124 cost += 1
125 if cost > max_cost then return null
126 bucket_searched += 1
127
128 if bucket_searched > nbr_buckets then break
129 else # found a node
130 debug "c {cost}"
131 frontier_node = current_bucket.pop
132
133 if frontier_node.open then break
134 end
135 end
136
137 # no path possible
138 if frontier_node == null then
139 return null
140
141 # at destination
142 else if frontier_node == dest then
143 debug "picked {frontier_node}, is destination"
144
145 var path = new Path[N](cost)
146
147 while frontier_node != self do
148 path.nodes.unshift(frontier_node)
149 frontier_node = frontier_node.best_source.as(not null)
150 end
151
152 return path
153
154 # adds all next nodes to frontier/buckets
155 else
156 frontier_node.open = false
157
158 debug "w exploring adjacents of {frontier_node}"
159
160 for link in frontier_node.links do
161 var peek_node = link.to
162 debug "v {context.is_blocked(link)} {peek_node.last_pathfinding_evocation != graph.pathfinding_current_evocation} {peek_node.best_cost_up_to > frontier_node.best_cost_up_to + context.cost(link)}, {peek_node.best_cost_up_to} > {frontier_node.best_cost_up_to} + {context.cost(link)}"
163 if not context.is_blocked(link) and
164 (peek_node.last_pathfinding_evocation != graph.pathfinding_current_evocation or
165 (peek_node.open and
166 peek_node.best_cost_up_to > cost + context.cost(link)))
167 then
168
169 peek_node.open = true
170 peek_node.last_pathfinding_evocation = graph.pathfinding_current_evocation
171 peek_node.best_cost_up_to = cost + context.cost(link)
172 peek_node.best_source = frontier_node
173
174 var est_cost = peek_node.best_cost_up_to+context.heuristic_cost(link.from, peek_node)
175 var at_bucket = buckets[est_cost % nbr_buckets]
176 at_bucket.add(peek_node)
177
178 debug "u putting {peek_node} at {est_cost} -> {est_cost % nbr_buckets} {at_bucket.hash}, {cost}+{context.cost(link)}"
179 end
180 end
181 end
182 end
183 end
184
185 # Find closes node with matching caracteristic
186 # TODO remove closures
187 fun find_closest(max_to_search: Int): nullable N !with(n: N): Bool
188 do
189 if with(self) then return self
190
191 var frontier = new List[N]
192 graph.pathfinding_current_evocation += 1
193 var current_evocation = graph.pathfinding_current_evocation
194
195 frontier.add(self)
196 self.last_pathfinding_evocation = current_evocation
197
198 var i = 0
199 while not frontier.is_empty do
200 var node = frontier.shift
201
202 for link in node.links do
203 var to = link.to
204 if to.last_pathfinding_evocation != current_evocation then
205 if with(to) then return to
206
207 frontier.add(to)
208 to.last_pathfinding_evocation = current_evocation
209 end
210 end
211
212 i += 1
213 if i > max_to_search then return null
214 end
215
216 return null
217 end
218 end
219
220 class Link
221 type N: Node
222 type L: Link
223
224 var graph: Graph[N, L]
225
226 var from: N
227 var to: N
228
229 init(graph: Graph[N, L], from, to: N)
230 do
231 self.graph = graph
232 self.from = from
233 self.to = to
234
235 graph.add_link(self)
236 end
237 end
238
239 # General graph
240 class Graph[N: Node, L: Link]
241 var nodes: Set[N] = new HashSet[N]
242 var links: Set[L] = new HashSet[L]
243
244 fun add_node(node: N): N
245 do
246 nodes.add(node)
247
248 return node
249 end
250
251 fun add_link(link: L): L
252 do
253 links.add(link)
254
255 link.from.links.add(link)
256
257 return link
258 end
259
260 # used to check if nodes have been searched in one pathfinding
261 var pathfinding_current_evocation: Int = 0
262 end
263
264 # Result from pathfinding, a walkable path
265 class Path[N]
266
267 var total_cost: Int
268
269 var nodes = new List[N]
270
271 init (cost: Int) do total_cost = cost
272
273 var at: Int = 0
274 fun step: N
275 do
276 assert nodes.length >= at else print "a_star::Path::step failed, is at_end_of_path"
277
278 var s = nodes[at]
279 at += 1
280
281 return s
282 end
283
284 fun peek_step: N do return nodes[at]
285
286 fun at_end_of_path: Bool do return at >= nodes.length
287 end
288
289 # Context related to an evocation of pathfinding
290 class PathContext
291 type N: Node
292 type L: Link
293
294 var graph: Graph[N, L]
295
296 # Worst cost of all the link's costs
297 fun worst_cost: Int is abstract
298
299 # Get cost of a link
300 fun cost(link: L): Int is abstract
301
302 # Is that link blocked?
303 fun is_blocked(link: L): Bool is abstract
304
305 # Heuristic
306 fun heuristic_cost(a, b: N): Int is abstract
307
308 fun worst_heuristic_cost: Int is abstract
309 end
310
311 #
312 ### Additionnal classes, may be useful
313 #
314
315 # Simple context with constant cost on each links
316 # Warning: A* is not optimize for such a case
317 class ConstantPathContext
318 super PathContext
319
320 redef fun worst_cost do return 1
321 redef fun cost(l) do return 1
322 redef fun is_blocked(l) do return false
323 redef fun heuristic_cost(a, b) do return 0
324 redef fun worst_heuristic_cost do return 0
325 end
326
327 class WeightedPathContext
328 super PathContext
329
330 redef type L: WeightedLink
331
332 init(graph: Graph[N, L])
333 do
334 super
335
336 var worst_cost = 0
337 for l in graph.links do
338 var cost = l.weight
339 if cost >= worst_cost then worst_cost = cost + 1
340 end
341 self.worst_cost = worst_cost
342 end
343
344 redef var worst_cost: Int
345
346 redef fun cost(l) do
347 return l.weight
348 end
349 redef fun is_blocked(l) do return false
350 redef fun heuristic_cost(a, b) do return 0
351 redef fun worst_heuristic_cost do return 0
352 end
353
354 class WeightedLink
355 super Link
356
357 var weight: Int
358
359 init(graph: Graph[N, L], from, to: N, weight: Int)
360 do
361 super
362
363 self.weight = weight
364 end
365 end