examples: annotate examples
[nit.git] / lib / ai / examples / puzzle.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # This file is free software, which comes along with NIT. This software is
4 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
5 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
6 # PARTICULAR PURPOSE. You can modify it is you want, provided this header
7 # is kept unaltered, and a notification of the changes is added.
8 # You are allowed to redistribute it and sell it, alone or is a part of
9 # another product.
10
11 # The N-puzzle problem, modeled naively as a `SearchProblem`.
12 #
13 # A square grid, made of tiles represented with a letter, is scrambled.
14 # A missing tile, the hole, represented with a dot, is used to move them.
15 #
16 # The goal is to found a plan, made of the four basic movements up, down,
17 # left, and right, that move each letter to its correct position: the solved
18 # grid list letters alphabetically from left to right then top to bottom.
19 # The hole must be in the last position (bottom-right).
20 #
21 # The argument of the program is a initial position, the program then find
22 # the best plan to solve the puzzle.
23 #
24 # ## Example:
25 #
26 # The argument "abcd.fgeh" is the grid
27 #
28 # ~~~
29 # abc
30 # d.f
31 # geh
32 # ~~~
33 #
34 # The goal is:
35 #
36 # ~~~
37 # abc
38 # def
39 # gh.
40 # ~~~
41 #
42 # The shortest plan, in two steps, is to move *up* the tile under the hole (e),
43 # then to move *left* the tile after the hole (h).
44 module puzzle is example
45
46 import ai::search
47
48 # The state (`S`) is a square grid, modeled as a one-dimensional array of Tile.
49 # read from left to right then top to bottom.
50 #
51 # An action (`A`) is the relative position of the tile to swap with the hole.
52 # Therefore, `-1` for left, `1` for right, `-width` for up and `width` for down.
53 class PuzzleProblem
54 super SearchProblem[ArrayCmp[Tile], Int]
55
56 # The initial grid. use letters for tiles, and . for the hole.
57 var initial_grid: String
58
59 # The width of the grid.
60 # Eg. 3 for a 8-puzzle grid
61 var width: Int is noinit
62
63 # Construct a state form `initial_grid`
64 redef fun initial_state do
65 var g = initial_grid
66 var len = g.length
67 var width = len.sqrt.to_i
68 self.width = width
69 if width * width != len then
70 print "Error: {g} has {len} tiles. A square number, like {width*width} is needed"
71 exit 1
72 end
73 var res = new ArrayCmp[Tile]
74 for i in [0..g.length[ do
75 var c = g.chars[i]
76 if c == ' ' or c == '.' then
77 var hole = new Tile('.', -1)
78 self.hole = hole
79 res.add hole
80 else if c >= '1' and c <= '9' then
81 var t = new Tile(c, '1'.distance(c))
82 res.add t
83 else if c >= 'a' and c <= 'z' then
84 var t = new Tile(c, 'a'.distance(c))
85 res.add t
86 else if c >= 'A' and c <= 'Z' then
87 var t = new Tile(c, 'A'.distance(c))
88 res.add t
89 else
90 print "Error: illegal tile {c} in {g}"
91 exit 1
92 end
93 end
94 return res
95 end
96
97 # Get the four available movements, or 3 on a edge, or 2 in a corner.
98 redef fun actions(state)
99 do
100 var h = get_hole(state)
101 var x = h % width
102 var y = h / width
103 var res = new Array[Int]
104 if x >= 1 then res.add(-1)
105 if x < width-1 then res.add(1)
106 if y >= 1 then res.add(-width)
107 if y < width-1 then res.add(width)
108 return res
109 end
110
111 # Return the state where the tile at hole+action has moved
112 redef fun apply_action(state, action)
113 do
114 # Copy the state
115 var res = new ArrayCmp[Tile].with_capacity(state.length)
116 res.add_all(state)
117
118 # Get the hole and the tile next to it
119 var h = get_hole(res)
120 var t = h + action
121
122 # Move (by swapping the tile and the hole)
123 res[h] = res[t]
124 res[t] = hole
125
126 return res
127 end
128
129 # The special empty tile for fast retrieval.
130 var hole: Tile is noinit
131
132 # What is the position of the hole?
133 fun get_hole(state: Array[Tile]): Int
134 do
135 return state.index_of(hole)
136 end
137
138 # Each tile is at its correct position
139 redef fun is_goal(state)
140 do
141 for i in [0..state.length[ do
142 var t = state[i]
143 if t != hole and t.goal_idx != i then return false
144 end
145 return true
146 end
147
148 # The sum of the Manhattan distances of each tile to its goal
149 # Not the best heuristic but the simplest to implement among the good ones.
150 redef fun heuristic(state)
151 do
152 var p = 0
153 var i = -1
154 for t in state do
155 i += 1
156
157 # The hole does not count
158 if t == hole then continue
159
160 var dx = (i % width - t.goal_idx % width).abs
161 var dy = (i / width - t.goal_idx / width).abs
162
163 # Add Manhattan distance
164 p += dx + dy
165 end
166 return p.to_f
167 end
168
169 # Print the grid
170 fun print_state(state: Array[Tile])
171 do
172 for i in [0..state.length[ do
173 var t = state[i]
174 if t == hole then
175 printn "."
176 else
177 printn t.symbol
178 end
179 if (i+1) % width == 0 then print ""
180 end
181 end
182
183 # Print the plan with words.
184 fun print_plan(plan: Sequence[Int])
185 do
186 var s = new Array[String]
187 for i in plan do
188 if i == -1 then
189 s.add "right(>)"
190 else if i == 1 then
191 s.add "left(<)"
192 else if i == -width then
193 s.add "down(v)"
194 else if i == width then
195 s.add "up(^)"
196 else
197 abort
198 end
199 end
200 print "Solution in {plan.length} moves: {s.join(" ")}"
201 end
202
203 redef fun make_memory do
204 var res = super
205 res.add new RBTreeMap[ArrayCmp[Tile], SearchNode[ArrayCmp[Tile], Int]]
206 res.add new BinTreeMap[ArrayCmp[Tile], SearchNode[ArrayCmp[Tile], Int]]
207 return res
208 end
209 end
210
211 # A movable tile
212 # A simple class to encapsulate the symbol and the goal position.
213 class Tile
214 super Comparable
215 redef type OTHER: Tile is fixed
216
217 # The symbol written on the tile
218 var symbol: Char
219
220 # The expected position in the grid
221 var goal_idx: Int
222
223 redef fun to_s do return symbol.to_s
224 redef fun ==(o) do return o isa Tile and goal_idx == o.goal_idx
225 redef fun <(o) do return goal_idx < o.goal_idx
226 redef fun <=>(o) do return goal_idx <=> o.goal_idx
227 end
228
229 var configs = false
230
231 if not args.is_empty then
232 if args.first == "--configs" then
233 configs = true
234 args.shift
235 end
236 end
237
238 if args.is_empty then
239 print """
240 Usage: puzzle [--configs] initial...
241
242 --configs: search and time solutions with various configurations of solvers.
243 initial: an initial configuration (letters for the tiles, and dot for the hole). eg:
244
245 8-puzzle:
246
247 goal (0): abcdefgh.
248 easy (4): abce.fdgh
249 medium (10): eabf.cdgh
250 hard (20): feacbh.dg
251 harder (31): hfgbedc.a
252
253 15-puzzle:
254 goal (0): abcdefghijklmno.
255 easy (30): bacdefghijlkmno.
256 medium (40): fg.jacoheldnibmk
257 hard (55): kleg.mondcafjhbi
258 harder (61): lomgkcend.afjhbi
259
260 24-puzzle:
261 goal (0): abcdefghijklmnopqrstuvwx.
262 easy (55): giabcjekmdhrtflqsownpuv.x
263 medium (75): giabcjekmdrtwulhs.vnqofpx
264 hard (79): giabcjekmdrtw.uhsvnlqofpx
265 harder (80): giabcjekmdrt.wuhsvnlqofpx
266 """
267 exit 0
268 end
269
270
271 for arg in args do
272 var pb = new PuzzleProblem(arg)
273 print "Initial: {arg}"
274 pb.print_state(pb.initial_state)
275
276 if configs then
277 pb.run_configs(1000000)
278 continue
279 end
280
281 var s = pb.astar
282 s.memorize = true
283 var r = s.run
284 if r == null then
285 print "No solution."
286 break
287 end
288
289 print "Solved, after looking at {r.steps} positions"
290 pb.print_plan(r.plan)
291 end