The 8 Puzzle

Play the 8 puzzle on-line here.

Let's discuss how to implement the 8 puzzle in python.

How do you want to represent the state of the 8 puzzle? Say the state is

-------------    
| 1 | 2 | 3 |
------------
| 4 |   | 5 |
------------
| 6 | 7 | 8 |
-------------

You could use a list

In [4]:
state = [1, 2, 3, 4, 0, 5, 6, 7, 8]
state
Out[4]:
[1, 2, 3, 4, 0, 5, 6, 7, 8]

with 0 representing the empty cell. You could represent it as a numpy array.

In [5]:
import numpy as np
In [6]:
state = np.array([[1, 2, 3], [4, 0, 5], [6, 7, 8]])
state
Out[6]:
array([[1, 2, 3],
       [4, 0, 5],
       [6, 7, 8]])

This way you index into a cell using

In [7]:
state[1, 2]
Out[7]:
5

for the second row and third column.

I found the simple list a little easier to work with. Then you can write a print_state_8p function to show it.

In [9]: print_state_8p(state)
1 2 3
4 - 5
6 7 8

Another useful function is one that finds the blank in a given state.

In [18]: find_blank_8p(state)
Out[18]: (1, 1)

In [19]: find_blank_8p([1,2,3, 4,7,5, 6,0,8])
Out[19]: (2, 1)

Other useful functions include ones that convert between an index into the list state and a row and column pair.

One bit of trickiness in the iterative deepening algorithm, repeated here from last time, is that sometimes a list of states is returned as the solution path, and other times the string 'cutoff' or 'failure' is returned.

In [ ]:
def depth_limited_search(state, goal_state, actions_f, take_action_f, depth_limit):
    
    # If we have reached the goal, exit, returning an empty solution path.
    If state == goal_state, then
        return []
    
    # If we have reached the depth limit, return the string 'cutoff'.
    If depth_limit is 0, then
        Return the string 'cutoff' to signal that the depth limit was reached
        
    cutoff_occurred = False
    
    # For each possible action from state ...
    For each action in actions_f(state):
        
        # Apply the action to the current state to get a next state, named child_state
        child_state = take_action_f(state, action)
        
        # Recursively call this function to continue the search starting from the child_state.
        # Decrease by one the depth_limit for this search.
        result = depth_limited_search(child_state, goal_state, actions_f, take_action_f, depth_limit - 1)
        
        # If result was 'cufoff', just note that this happened.
        If result is 'cutoff', then
            cutoff_occurred = True
            
        # If result was not 'failure', search succeeded so add childState to front of solution path and
        # return that path.
        else if result is not 'failure' then
            Add child_state to front of partial solution path, in result, returned by depth_limited_search
            return result
        
    # We reach here only if cutoff or failure occurred.  Return whichever occurred.
    If cutoff_occurred, then
        return 'cutoff'
    else
        return 'failure'
In [ ]:
def iterative_deepening_search(start_state, goal_state, actions_f, take_action_f, max_depth):
    
    # Conduct multiple searches, starting with smallest depth, then increasing it by 1 each time.
    for depth in range(max_depth):
        
        # Conduct search from startState
        result = depth_limited_search(start_state, goal_state, actions_f, take_action_f, depth)
        
        # If result was failure, return 'failure'.
        if result is 'failure':
            return 'failure'
        
        # Otherwise, if result was not cutoff, it succeeded, so add start_state to solution path and return it.
        if result is not 'cutoff', then
            Add start_state to front of solution path, in result, returned by depth_limited_search       
            return result
        
    # If we reach here, no solution found within the max_depth limit.
    return 'cutoff'

Remember, for the 8 puzzle all actions are not available from all states. The state

-------------    
|   | 2 | 3 |
 ------------
| 1 | 4 | 5 |
------------
| 6 | 7 | 8 |
-------------

only has two possible actions, 'down' and 'right'. It makes the most sense to implement this restriction in the actions_f function, so take_action_f can assume only valid actions are given to it.

As implemented for this assignment, our depth-limited search generates a list of all valid actions from a state, stores them, then starts a for loop to try each one. At any point in the depth-first search, all siblings of states being explored are stored in the local variables of each recursive call.

Python Generators

Remember that the "backtracking" version of depth-first search is one in which all sibling actions are not stored, but generated as needed.

Sounds like a complicated implementation. Python generators to the rescue! This is a bit advanced and the solution to Assignment 2 does not need generators, but, be curious!

Here is a simplified version of actions_f, without the checks for valid actions.

In [18]:
def actions_f(state):
  actions = []
  actions.append('left')
  actions.append('right')
  actions.append('up')
  actions.append('down')
  return actions

It just returns the actions.

In [31]: actions_f(state)
Out[31]: ['left', 'right', 'up', 'down']
In [10]:
state = np.array([[1, 2, 3], [4, 0, 5], [6, 7, 8]])
state
Out[10]:
array([[1, 2, 3],
       [4, 0, 5],
       [6, 7, 8]])
In [19]:
acts = actions_f(state)
acts
Out[19]:
['left', 'right', 'up', 'down']

The function actions_f can be converted to one that returns a generator by using the yield statement.

In [28]:
def actions_f(state):
  yield 'left'
  yield 'right'
  yield 'up'
  yield 'down'

Sheesh. That's even simpler than the original. It's use must be more complicated. And it is, but just a bit.

In [29]:
acts = actions_f(state)
acts
Out[29]:
<generator object actions_f at 0x7fa9643e26d0>
In [22]:
next(acts)
Out[22]:
'left'
In [23]:
next(acts)
Out[23]:
'right'
In [24]:
next(acts)
Out[24]:
'up'
In [25]:
next(acts)
Out[25]:
'down'
In [26]:
next(acts)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-26-588cd4d23904> in <module>
----> 1 next(acts)

StopIteration: 

That last one raised a StopIteration exception. The generator is often used in a for loop that stops correctly.

In [27]:
for a in actions_f(state):
    print(a)
left
right
up
down

This looks exactly like the for loop when actions_f actually returns the whole list!

Debugging with pdb

See the site Python Conquers the Universe for a brief introduction to using the pdb module.

And don't forget good old print statements.

debug = True
  .
  .
  .
if debug:
    print('Just loaded data into list named nums whose length is', len(nums))

ipython and jupyter startup settings

ipython can be set to automatically start pdb when an error is encountered. Many other settings are available. See IPython Tip Sheet.

jupyter startup settings are discussed here