misc/vim: inform the user when no results are found
[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 #
21 # Usage:
22 #
23 # ~~~
24 # # Weighted graph (letters are nodes, digits are weights):
25 # #
26 # # a -2- b
27 # # / /
28 # # 3 1
29 # # / /
30 # # c -3- d -8- e
31 # #
32 # var graph = new Graph[Node,WeightedLink]
33 #
34 # var na = new Node(graph)
35 # var nb = new Node(graph)
36 # var nc = new Node(graph)
37 # var nd = new Node(graph)
38 # var ne = new Node(graph)
39 #
40 # var lab = new WeightedLink(graph, na, nb, 2)
41 # var lac = new WeightedLink(graph, na, nc, 3)
42 # var lbd = new WeightedLink(graph, nb, nd, 1)
43 # var lcd = new WeightedLink(graph, nc, nd, 3)
44 # var lde = new WeightedLink(graph, nd, ne, 8)
45 #
46 # var context = new WeightedPathContext(graph)
47 #
48 # var path = na.path_to(ne, 100, context)
49 # assert path != null else print "No possible path"
50 #
51 # assert path.step == nb
52 # assert path.step == nd
53 # assert path.step == ne
54 # assert path.at_end_of_path
55 # ~~~
56 module a_star
57
58 # General graph node
59 class Node
60 # Type of the others nodes in the `graph`
61 type N: Node
62
63 # parent graph
64 var graph: Graph[N, Link]
65
66 init
67 do
68 graph.add_node(self)
69 end
70
71 # adjacent nodes
72 var links: Set[Link] = new HashSet[Link]
73
74 # used to check if node has been searched in one pathfinding
75 private var last_pathfinding_evocation: Int = 0
76
77 # cost up to in current evocation
78 # lifetime limited to evocation of `path_to`
79 private var best_cost_up_to: Int = 0
80
81 # source node
82 # lifetime limited to evocation of `path_to`
83 private var best_source: nullable N = null
84
85 # is in frontier or buckets
86 # lifetime limited to evocation of `path_to`
87 private var open: Bool = false
88
89 # Main functionnality, returns path from `self` to `dest`
90 fun path_to(dest: N, max_cost: Int, context: PathContext): nullable AStarPath[N]
91 do
92 return path_to_alts(dest, max_cost, context, null)
93 end
94
95 # Find a path to a possible `destination` or a node accepted by `alt_targets`
96 fun path_to_alts(destination: nullable N, max_cost: Int, context: PathContext,
97 alt_targets: nullable TargetCondition[N]): nullable AStarPath[N]
98 do
99 var cost = 0
100
101 var nbr_buckets = context.worst_cost + context.worst_heuristic_cost + 1
102 var buckets = new Array[List[N]].with_capacity(nbr_buckets)
103
104 for i in [0 .. nbr_buckets[ do
105 buckets.add(new List[N])
106 end
107
108 graph.pathfinding_current_evocation += 1
109
110 # open source node
111 buckets[0].add(self)
112 open = true
113 self.last_pathfinding_evocation = graph.pathfinding_current_evocation
114 self.best_cost_up_to = 0
115
116 loop
117 var frontier_node: nullable N = null
118
119 var bucket_searched = 0
120
121 # find next valid node in frontier/buckets
122 loop
123 var current_bucket = buckets[cost % nbr_buckets]
124
125 if current_bucket.is_empty then # move to next bucket
126 cost += 1
127 if cost > max_cost then return null
128 bucket_searched += 1
129
130 if bucket_searched > nbr_buckets then break
131 else # found a node
132 frontier_node = current_bucket.pop
133
134 if frontier_node.open then break
135 end
136 end
137
138 # no path possible
139 if frontier_node == null then
140 return null
141
142 # at destination
143 else if frontier_node == destination or
144 (alt_targets != null and alt_targets.accept(frontier_node)) then
145
146 var path = new AStarPath[N](cost)
147
148 while frontier_node != self do
149 path.nodes.unshift(frontier_node)
150 frontier_node = frontier_node.best_source.as(not null)
151 end
152
153 return path
154
155 # adds all next nodes to frontier/buckets
156 else
157 frontier_node.open = false
158
159 for link in frontier_node.links do
160 var peek_node = link.to
161 if not context.is_blocked(link) and
162 (peek_node.last_pathfinding_evocation != graph.pathfinding_current_evocation or
163 (peek_node.open and
164 peek_node.best_cost_up_to > cost + context.cost(link)))
165 then
166 peek_node.open = true
167 peek_node.last_pathfinding_evocation = graph.pathfinding_current_evocation
168 peek_node.best_cost_up_to = cost + context.cost(link)
169 peek_node.best_source = frontier_node
170
171 var est_cost
172 if destination != null then
173 est_cost = peek_node.best_cost_up_to + context.heuristic_cost(peek_node, destination)
174 else if alt_targets != null then
175 est_cost = peek_node.best_cost_up_to + alt_targets.heuristic_cost(peek_node, link)
176 else est_cost = 0
177
178 var at_bucket = buckets[est_cost % nbr_buckets]
179 at_bucket.add(peek_node)
180 end
181 end
182 end
183 end
184 end
185 end
186
187 # Link between two nodes and associated to a graph
188 class Link
189 # Type of the nodes in `graph`
190 type N: Node
191
192 # Type of the other links in `graph`
193 type L: Link
194
195 # The graph to which belongs `self`
196 var graph: Graph[N, L]
197
198 # Origin of this link
199 var from: N
200
201 # Endpoint of this link
202 var to: N
203
204 init
205 do
206 graph.add_link(self)
207 end
208 end
209
210 # General graph
211 class Graph[N: Node, L: Link]
212 # Nodes in this graph
213 var nodes: Set[N] = new HashSet[N]
214
215 # Links in this graph
216 var links: Set[L] = new HashSet[L]
217
218 # Add a `node` to this graph
219 fun add_node(node: N): N
220 do
221 nodes.add(node)
222
223 return node
224 end
225
226 # Add a `link` to this graph
227 fun add_link(link: L): L
228 do
229 links.add(link)
230
231 link.from.links.add(link)
232
233 return link
234 end
235
236 # Used to check if nodes have been searched in one pathfinding
237 private var pathfinding_current_evocation: Int = 0
238 end
239
240 # Result from path finding and a walkable path
241 class AStarPath[N]
242
243 # The total cost of this path
244 var total_cost: Int
245
246 # The list of nodes composing this path
247 var nodes = new List[N]
248
249 private var at: Int = 0
250
251 # Step on the path and get the next node to travel
252 fun step: N
253 do
254 assert nodes.length >= at else print "a_star::AStarPath::step failed, is at_end_of_path"
255
256 var s = nodes[at]
257 at += 1
258
259 return s
260 end
261
262 # Peek at the next step of the path
263 fun peek_step: N do return nodes[at]
264
265 # Are we at the end of this path?
266 fun at_end_of_path: Bool do return at >= nodes.length
267 end
268
269 # Context related to an evocation of pathfinding
270 class PathContext
271 # Type of the nodes in `graph`
272 type N: Node
273
274 # Type of the links in `graph`
275 type L: Link
276
277 # Graph to which is associated `self`
278 var graph: Graph[N, L]
279
280 # Worst cost of all the link's costs
281 fun worst_cost: Int is abstract
282
283 # Get cost of a link
284 fun cost(link: L): Int is abstract
285
286 # Is that link blocked?
287 fun is_blocked(link: L): Bool is abstract
288
289 # Heuristic
290 fun heuristic_cost(a, b: N): Int is abstract
291
292 # The worst cost suggested by the heuristic
293 fun worst_heuristic_cost: Int is abstract
294 end
295
296 #
297 ### Additionnal classes, may be useful
298 #
299
300 # Simple context with constant cost on each links
301 # Warning: A* is not optimize for such a case
302 class ConstantPathContext
303 super PathContext
304
305 redef fun worst_cost do return 1
306 redef fun cost(l) do return 1
307 redef fun is_blocked(l) do return false
308 redef fun heuristic_cost(a, b) do return 0
309 redef fun worst_heuristic_cost do return 0
310 end
311
312 # A `PathContext` for graphs with `WeightedLink`
313 class WeightedPathContext
314 super PathContext
315
316 redef type L: WeightedLink
317
318 init
319 do
320 super
321
322 var worst_cost = 0
323 for l in graph.links do
324 var cost = l.weight
325 if cost >= worst_cost then worst_cost = cost + 1
326 end
327 self.worst_cost = worst_cost
328 end
329
330 redef var worst_cost: Int is noinit
331
332 redef fun cost(l) do
333 return l.weight
334 end
335 redef fun is_blocked(l) do return false
336 redef fun heuristic_cost(a, b) do return 0
337 redef fun worst_heuristic_cost do return 0
338 end
339
340 # A `Link` with a `weight`
341 class WeightedLink
342 super Link
343
344 # The `weight`, or cost, of this link
345 var weight: Int
346 end
347
348 # Advanced path conditions with customizable accept states
349 class TargetCondition[N: Node]
350 # Should the pathfinding accept `node` as a goal?
351 fun accept(node: N): Bool is abstract
352
353 # Approximate cost from `node` to an accept state
354 fun heuristic_cost(node: N, link: Link): Int is abstract
355 end