1 # This file is part of NIT ( http://www.nitlanguage.org ).
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
11 # The N-puzzle problem, modeled naively as a `SearchProblem`.
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.
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).
21 # The argument of the program is a initial position, the program then find
22 # the best plan to solve the puzzle.
26 # The argument "abcd.fgeh" is the grid
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
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.
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.
54 super SearchProblem[ArrayCmp[Tile], Int]
56 # The initial grid. use letters for tiles, and . for the hole.
57 var initial_grid
: String
59 # The width of the grid.
60 # Eg. 3 for a 8-puzzle grid
61 var width
: Int is noinit
63 # Construct a state form `initial_grid`
64 redef fun initial_state
do
67 var width
= len
.sqrt
.to_i
69 if width
* width
!= len
then
70 print
"Error: {g} has {len} tiles. A square number, like {width*width} is needed"
73 var res
= new ArrayCmp[Tile]
74 for i
in [0..g
.length
[ do
76 if c
== ' ' or c
== '.' then
77 var hole
= new Tile('.', -1)
80 else if c
>= '1' and c
<= '9' then
81 var t
= new Tile(c
, '1'.distance
(c
))
83 else if c
>= 'a' and c
<= 'z' then
84 var t
= new Tile(c
, 'a'.distance
(c
))
86 else if c
>= 'A' and c
<= 'Z' then
87 var t
= new Tile(c
, 'A'.distance
(c
))
90 print
"Error: illegal tile {c} in {g}"
97 # Get the four available movements, or 3 on a edge, or 2 in a corner.
98 redef fun actions
(state
)
100 var h
= get_hole
(state
)
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
)
111 # Return the state where the tile at hole+action has moved
112 redef fun apply_action
(state
, action
)
115 var res
= new ArrayCmp[Tile].with_capacity
(state
.length
)
118 # Get the hole and the tile next to it
119 var h
= get_hole
(res
)
122 # Move (by swapping the tile and the hole)
129 # The special empty tile for fast retrieval.
130 var hole
: Tile is noinit
132 # What is the position of the hole?
133 fun get_hole
(state
: Array[Tile]): Int
135 return state
.index_of
(hole
)
138 # Each tile is at its correct position
139 redef fun is_goal
(state
)
141 for i
in [0..state
.length
[ do
143 if t
!= hole
and t
.goal_idx
!= i
then return false
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
)
157 # The hole does not count
158 if t
== hole
then continue
160 var dx
= (i
% width
- t
.goal_idx
% width
).abs
161 var dy
= (i
/ width
- t
.goal_idx
/ width
).abs
163 # Add Manhattan distance
170 fun print_state
(state
: Array[Tile])
172 for i
in [0..state
.length
[ do
179 if (i
+1) % width
== 0 then print
""
183 # Print the plan with words.
184 fun print_plan
(plan
: Sequence[Int])
186 var s
= new Array[String]
192 else if i
== -width
then
194 else if i
== width
then
200 print
"Solution in {plan.length} moves: {s.join(" ")}"
203 redef fun make_memory
do
205 res
.add
new RBTreeMap[ArrayCmp[Tile], SearchNode[ArrayCmp[Tile], Int]]
206 res
.add
new BinTreeMap[ArrayCmp[Tile], SearchNode[ArrayCmp[Tile], Int]]
212 # A simple class to encapsulate the symbol and the goal position.
215 redef type OTHER: Tile is fixed
217 # The symbol written on the tile
220 # The expected position in the grid
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
231 if not args
.is_empty
then
232 if args
.first
== "--configs" then
238 if args
.is_empty
then
240 Usage: puzzle [--configs] initial...
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:
249 medium (10): eabf.cdgh
251 harder (31): hfgbedc.a
254 goal (0): abcdefghijklmno.
255 easy (30): bacdefghijlkmno.
256 medium (40): fg.jacoheldnibmk
257 hard (55): kleg.mondcafjhbi
258 harder (61): lomgkcend.afjhbi
261 goal (0): abcdefghijklmnopqrstuvwx.
262 easy (55): giabcjekmdhrtflqsownpuv.x
263 medium (75): giabcjekmdrtwulhs.vnqofpx
264 hard (79): giabcjekmdrtw.uhsvnlqofpx
265 harder (80): giabcjekmdrt.wuhsvnlqofpx
272 var pb
= new PuzzleProblem(arg
)
273 print
"Initial: {arg}"
274 pb
.print_state
(pb
.initial_state
)
277 pb
.run_configs
(1000000)
289 print
"Solved, after looking at {r.steps} positions"
290 pb
.print_plan
(r
.plan
)