The N-puzzle problem, modeled naively as a SearchProblem.

A square grid, made of tiles represented with a letter, is scrambled. A missing tile, the hole, represented with a dot, is used to move them.

The goal is to found a plan, made of the four basic movements up, down, left, and right, that move each letter to its correct position: the solved grid list letters alphabetically from left to right then top to bottom. The hole must be in the last position (bottom-right).

The argument of the program is a initial position, the program then find the best plan to solve the puzzle.

Example:

The argument "abcd.fgeh" is the grid

abc
d.f
geh

The goal is:

abc
def
gh.

The shortest plan, in two steps, is to move up the tile under the hole (e), then to move left the tile after the hole (h).

Introduced classes

class PuzzleProblem

ai :: PuzzleProblem

The state (S) is a square grid, modeled as a one-dimensional array of Tile.
class Tile

ai :: Tile

A movable tile

Redefined classes

redef class Sys

ai :: puzzle $ Sys

The main class of the program.

All class definitions

class PuzzleProblem

ai $ PuzzleProblem

The state (S) is a square grid, modeled as a one-dimensional array of Tile.
redef class Sys

ai :: puzzle $ Sys

The main class of the program.
class Tile

ai $ Tile

A movable tile
package_diagram ai::puzzle puzzle ai::search search ai::puzzle->ai::search realtime realtime ai::search->realtime trees trees ai::search->trees ...realtime ... ...realtime->realtime ...trees ... ...trees->trees a_star-m a_star-m a_star-m->ai::puzzle

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module abstract_tree

trees :: abstract_tree

Introduce tree structures abstraction
module array

core :: array

This module introduces the standard array structure.
module bintree

trees :: bintree

Binary Tree data-structure
module bitset

core :: bitset

Services to handle BitSet
module bytes

core :: bytes

Services for byte streams and arrays
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module kernel

core :: kernel

Most basic classes and methods.
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module native

core :: native

Native structures for text and bytes
module numeric

core :: numeric

Advanced services for Numeric types
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module rbtree

trees :: rbtree

A red–black tree is a data structure which is a type of self-balancing binary search tree.
module re

core :: re

Regular expression support for all services based on Pattern
module realtime

realtime :: realtime

Services to keep time of the wall clock time
module ropes

core :: ropes

Tree-based representation of a String.
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module time

core :: time

Management of time and dates
module trees

trees :: trees

General module for tree data structures
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O

Parents

module search

ai :: search

Basic framework for search problems and solver.

Children

module a_star-m

a_star-m

# The N-puzzle problem, modeled naively as a `SearchProblem`.
#
# A square grid, made of tiles represented with a letter, is scrambled.
# A missing tile, the hole, represented with a dot, is used to move them.
#
# The goal is to found a plan, made of the four basic movements up, down,
# left, and right, that move each letter to its correct position: the solved
# grid list letters alphabetically from left to right then top to bottom.
# The hole must be in the last position (bottom-right).
#
# The argument of the program is a initial position, the program then find
# the best plan to solve the puzzle.
#
# ## Example:
#
# The argument "abcd.fgeh" is the grid
#
# ~~~raw
# abc
# d.f
# geh
# ~~~
#
# The goal is:
#
# ~~~raw
# abc
# def
# gh.
# ~~~
#
# The shortest plan, in two steps, is to move *up* the tile under the hole (e),
# then to move *left* the tile after the hole (h).
module puzzle is example

import ai::search

# The state (`S`) is a square grid, modeled as a one-dimensional array of Tile.
# read from left to right then top to bottom.
#
# An action (`A`) is the relative position of the tile to swap with the hole.
# Therefore, `-1` for left, `1` for right, `-width` for up and `width` for down.
class PuzzleProblem
	super SearchProblem[ArrayCmp[Tile], Int]

	# The initial grid. use letters for tiles, and . for the hole.
	var initial_grid: String

	# The width of the grid.
	# Eg. 3 for a 8-puzzle grid
	var width: Int is noinit

	# Construct a state form `initial_grid`
	redef fun initial_state do
		var g = initial_grid
		var len = g.length
		var width = len.sqrt.to_i
		self.width = width
		if width * width != len then
			print "Error: {g} has {len} tiles. A square number, like {width*width} is needed"
			exit 1
		end
		var res = new ArrayCmp[Tile]
		for i in [0..g.length[ do
			var c = g.chars[i]
			if c == ' ' or c == '.' then
				var hole = new Tile('.', -1)
				self.hole = hole
				res.add hole
			else if c >= '1' and c <= '9' then
				var t = new Tile(c, '1'.distance(c))
				res.add t
			else if c >= 'a' and c <= 'z' then
				var t = new Tile(c, 'a'.distance(c))
				res.add t
			else if c >= 'A' and c <= 'Z' then
				var t = new Tile(c, 'A'.distance(c))
				res.add t
			else
				print "Error: illegal tile {c} in {g}"
				exit 1
			end
		end
		return res
	end

	# Get the four available movements, or 3 on a edge, or 2 in a corner.
	redef fun actions(state)
	do
		var h = get_hole(state)
		var x = h % width
		var y = h / width
		var res = new Array[Int]
		if x >= 1 then res.add(-1)
		if x < width-1 then res.add(1)
		if y >= 1 then res.add(-width)
		if y < width-1 then res.add(width)
		return res
	end

	# Return the state where the tile at hole+action has moved
	redef fun apply_action(state, action)
	do
		# Copy the state
		var res = new ArrayCmp[Tile].with_capacity(state.length)
		res.add_all(state)

		# Get the hole and the tile next to it
		var h = get_hole(res)
		var t = h + action

		# Move (by swapping the tile and the hole)
		res[h] = res[t]
		res[t] = hole

		return res
	end

	# The special empty tile for fast retrieval.
	var hole: Tile is noinit

	# What is the position of the hole?
	fun get_hole(state: Array[Tile]): Int
	do
		return state.index_of(hole)
	end

	# Each tile is at its correct position
	redef fun is_goal(state)
	do
		for i in [0..state.length[ do
			var t = state[i]
			if t != hole and t.goal_idx != i then return false
		end
		return true
	end

	# The sum of the Manhattan distances of each tile to its goal
	# Not the best heuristic but the simplest to implement among the good ones.
	redef fun heuristic(state)
	do
		var p = 0
		var i = -1
		for t in state do
			i += 1

			# The hole does not count
			if t == hole then continue

			var dx = (i % width - t.goal_idx % width).abs
			var dy = (i / width - t.goal_idx / width).abs

			# Add Manhattan distance
			p += dx + dy
		end
		return p.to_f
	end

	# Print the grid
	fun print_state(state: Array[Tile])
	do
		for i in [0..state.length[ do
			var t = state[i]
			if t == hole then
				printn "."
			else
				printn t.symbol
			end
			if (i+1) % width == 0 then print ""
		end
	end

	# Print the plan with words.
	fun print_plan(plan: Sequence[Int])
	do
		var s = new Array[String]
		for i in plan do
			if i == -1 then
				s.add "right(>)"
			else if i == 1 then
				s.add "left(<)"
			else if i == -width then
				s.add "down(v)"
			else if i == width then
				s.add "up(^)"
			else
				abort
			end
		end
		print "Solution in {plan.length} moves: {s.join(" ")}"
	end

	redef fun make_memory do
		var res = super
		res.add new RBTreeMap[ArrayCmp[Tile], SearchNode[ArrayCmp[Tile], Int]]
		res.add new BinTreeMap[ArrayCmp[Tile], SearchNode[ArrayCmp[Tile], Int]]
		return res
	end
end

# A movable tile
# A simple class to encapsulate the symbol and the goal position.
class Tile
	super Comparable
	redef type OTHER: Tile is fixed

	# The symbol written on the tile
	var symbol: Char

	# The expected position in the grid
	var goal_idx: Int

	redef fun to_s do return symbol.to_s
	redef fun ==(o) do return o isa Tile and goal_idx == o.goal_idx
	redef fun <(o) do return goal_idx < o.goal_idx
	redef fun <=>(o) do return goal_idx <=> o.goal_idx
end

var configs = false

if not args.is_empty then
	if args.first == "--configs" then
		configs = true
		args.shift
	end
end

if args.is_empty then
	print """
Usage: puzzle [--configs] initial...

--configs: search and time solutions with various configurations of solvers.
initial:   an initial configuration (letters for the tiles, and dot for the hole). eg:

 8-puzzle:

 goal (0):    abcdefgh.
 easy (4):    abce.fdgh
 medium (10): eabf.cdgh
 hard (20):   feacbh.dg
 harder (31): hfgbedc.a

 15-puzzle:
 goal (0):    abcdefghijklmno.
 easy (30):   bacdefghijlkmno.
 medium (40): fg.jacoheldnibmk
 hard (55):   kleg.mondcafjhbi
 harder (61): lomgkcend.afjhbi

 24-puzzle:
 goal (0):    abcdefghijklmnopqrstuvwx.
 easy (55):   giabcjekmdhrtflqsownpuv.x
 medium (75): giabcjekmdrtwulhs.vnqofpx
 hard (79):   giabcjekmdrtw.uhsvnlqofpx
 harder (80): giabcjekmdrt.wuhsvnlqofpx
"""
	exit 0
end


for arg in args do
	var pb = new PuzzleProblem(arg)
	print "Initial: {arg}"
	pb.print_state(pb.initial_state)

	if configs then
		pb.run_configs(1000000)
		continue
	end

	var s = pb.astar
	s.memorize = true
	var r = s.run
	if r == null then
		print "No solution."
		break
	end

	print "Solved, after looking at {r.steps} positions"
	pb.print_plan(r.plan)
end
lib/ai/examples/puzzle.nit:11,1--291,3