#!/usr/bin/env python # coding: utf-8 # # The 8 Puzzle # Play the 8 puzzle on-line [here](http://www.tilepuzzles.com/default.asp?p=12). # 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 # 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 # This way you index into a cell using # In[7]: state[1, 2] # 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](http://www.neotitans.com/resources/python-generators-tutorial.html]) # 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 # In[19]: acts = actions_f(state) acts # 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 # In[22]: next(acts) # In[23]: next(acts) # In[24]: next(acts) # In[25]: next(acts) # In[26]: next(acts) # 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) # 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](http://pythonconquerstheuniverse.wordpress.com/category/python-debugger/) 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](http://pages.physics.cornell.edu/~myers/teaching/ComputationalMethods/python/ipython.html). # # `jupyter` startup settings are discussed [here](https://jupyter-notebook.readthedocs.io/en/stable/config_overview.html)