Exercise 2.1 - 8-Puzzle

Strategy for a 8-Puzzle Solver.

Problem Representation

Search Algorithm

Agent Design Components

State Space

Action Space

{swap up,swap down,swap left,swap right}{U,D,L,R}\{swap\ up, swap\ down,swap\ left, swap\ right\} \rightarrow \{U, D, L, R\}

World Dynamics

Utility Function

State Class

A constructor (__init__)

class EightPuzzle:
	def __init__(self, squares):
		if type(squares) is str:
			self.squares == list(squares)
		else:
			# Convert the list to string
			self.squares = [str(i) for i in squares]

			# Find position of _
			idx = -1
			for i in range(len(self.squares)):
				if self.squares[i] == '_':
					idx = i;
			self.idx = idx

Equality (__eq__)

def __eq__(self, obj):;
		if obj is None:
				return False
		# optional test for type:
		"""
		if type(obj) != EightPuzzle:
				return False
		"""
		return tuple(self.squares) ==
				tuple(obj.squares)

Hash (__hash__)

def __hash__(self):
	return hash(tuple(self.squares))

Finding Successors - get_successors(...)

def get_successors(self):
  successors = []

  if self.idx % 3 > 0:
    successors.append(self.move_left())
  else:
    successors.append(None)

  if self.idx % 3 < 2:
    successors.append(self.move_right())
  else:
    successors.append(None)

  if self.idx // 3 > 0:
    successors.append(self.move_up())
  else:
    successors.append(None)

  if self.idx // 3 < 2:
    successors.append(self.move_down())
  else:
    successors.append(None)

  return successors

Helper Methods

def move_left(self):
	  new_squares = copy.deepcopy(self.squares)
	  new_squares[self.idx] = self.squares[self.idx-1]
	  new_squares[self.idx-1] = self.squares[self.idx]
	  return EightPuzzle(new_squares)

def move_right(self):
	  new_squares = copy.deepcopy(self.squares)
	  new_squares[self.idx] = self.squares[self.idx+1]
	  new_squares[self.idx+1] = self.squares[self.idx]
	  return EightPuzzle(new_squares)

def move_up(self):
	  new_squares = copy.deepcopy(self.squares)
	  new_squares[self.idx] = self.squares[self.idx-3]
	  new_squares[self.idx-3] = self.squares[self.idx]
	  return EightPuzzle(new_squares)

def move_down(self):
	  new_squares = copy.deepcopy(self.squares)
	  new_squares[self.idx] = self.squares[self.idx+3]
	  new_squares[self.idx+3] = self.squares[self.idx]
	  return EightPuzzle(new_squares)

Search Node Class

class ContainerEntry:
	def __init__(self, puzzle, actions):
	  self.puzzle = puzzle
	  self.actions = actions

	def get_successors(self):
	  s = []
	  suc = self.puzzle.get_successors()

	  if suc[0] is not None:
      s.append(ContainerEntry(suc[0],
				self.actions + [LEFT]))
	  if suc[1] is not None:
      s.append(ContainerEntry(suc[1],
				self.actions + [RIGHT]))
	  if suc[2] is not None:
      s.append(ContainerEntry(suc[2],
				self.actions + [UP]))
	  if suc[3] is not None:
      s.append(ContainerEntry(suc[3],
				self.actions + [DOWN]))

	  return s

	def __eq__(self, obj):
	  return self.puzzle == obj.puzzle

Search Algorithm

  1. Container = [SearchNode(init_state)]
  2. While Container is not Empty:
  3. Current_Node ← Choose Node from Container (and remove it)
  4. If Current_Node.state is the goal state:
  5. Return Current_Node.actions
    
  6. Successors ← Current_Node.get_successors()
  7. For s in Successors:
  8. If s not visited (or s visited at higher cost than current cost)
    
  9.   Add SearchNode(s) to the Container
    
  10.   Add s to the visited set.
    
# BFS Algorithm
def bfs(initial, goal):
    container = [ContainerEntry(initial, [])]
    visited = set([])

    i = 0
    while len(container) > 0:
        # expand node
        node = container.pop(0)
        if node.puzzle == goal:
            return node.actions

        # add successors
        suc = node.get_successors()
        for s in suc:
            if s.puzzle not in visited:
                container.append(s)
                visited.add(s.puzzle)
        i += 1

    return None
# DFS Algorithm
def dfs(initial, goal):
    container = [ContainerEntry(initial, [])]
    visited = set([])

    i = 0
    while len(container) > 0:
        # expand node
        node = container.pop(-1)
        if node.puzzle == goal:
            return node.actions

        # add successors
        suc = node.get_successors()
        for s in suc:
            if s.puzzle not in visited:
                container.append(s)
                visited.add(s.puzzle)
        i += 1

    return None

Exercise 2.2: Performance Comparison

Exercise 2.3: Solvability and Parity

Parity

Number of Inversions

Figure 1 - Example for Number of Inversions in an 8-Puzzle

Implementing Parity

def num_inversions(self):
  total = 0
  for i in range(len(self.squares)):
    if self.squares[i] == '_':
      continue
    si = int(self.squares[i])
    for j in range(i, len(self.squares)):
      if self.squares[j] == '_':
        continue
      sj = int(self.squares[j])
      if si > sj:
        total += 1
  return total

def get_parity(self):
  return self.num_inversions() % 2

# In the main solution code, we write
if p1.get_parity() != p2.get_parity():
		print('No solution')
		return

Exercise 2.4: Actions with Costs

cost = {'up':1, 'down':2, 'left':3, 'right':4}