/school/intro_to_ai/larset2-hw1/larset2-hw1.py

https://github.com/Ahumm/Zawumbo-General · Python · 273 lines · 172 code · 21 blank · 80 comment · 47 complexity · 280f340c832c67607c3d2f452c8edd23 MD5 · raw file

  1. ##################################################################
  2. ## ##
  3. ## Tate Larsen ##
  4. ## 9/23/2012 ##
  5. ## ##
  6. ## ------------------------------------------------------------ ##
  7. ## ##
  8. ## Intro to AI - Homework 1 ##
  9. ## 8 Tile Puzzle Solver ##
  10. ## ##
  11. ## ------------------------------------------------------------ ##
  12. ## ##
  13. ## Tested with Python 2.7.3 ##
  14. ## Previous versions ought to work ##
  15. ## Will not work with Python 3.X+ ##
  16. ## ##
  17. ## ------------------------------------------------------------ ##
  18. ## ##
  19. ## Usage: ##
  20. ## Manual puzzle entry : python larset2-hw1.py ##
  21. ## Read puzzle from file : python larset2-hw1.py <filename> ##
  22. ## ##
  23. ## ------------------------------------------------------------ ##
  24. ## ##
  25. ## Included Test Files: ##
  26. ## initstate1 - Invalid Input ##
  27. ## initstate2 - Valid Input, Solvable ##
  28. ## initstate3 - Valid Input, Unsolvable ##
  29. ## ##
  30. ## ------------------------------------------------------------ ##
  31. ## ##
  32. ## Note: ##
  33. ## Stops after checking MAX_RUN reachable permutations ##
  34. ## Can appear to hang if MAX_RUN is too large but ##
  35. ## it is still running ##
  36. ## ##
  37. ##################################################################
  38. import sys
  39. # Specify maximum number of permutations to check
  40. MAX_RUN = 4000
  41. class board:
  42. state = [0,1,2,3,4,5,6,7,8]
  43. move_count = 0
  44. f_score = 0
  45. last_board = None
  46. def __init__(self):
  47. return
  48. # Redefine == to only compare the state list
  49. def __eq__(self, other):
  50. return self.state == other.state
  51. # set fields of a board with one call
  52. def set_board(self, new_state, new_move_count, new_last_board):
  53. self.state = new_state
  54. self.move_count = new_move_count
  55. self.last_board = new_last_board
  56. # Find the 2-4 moves possible with the current board configuration
  57. def get_possible_moves(self):
  58. possible_states = []
  59. # Find the empty tile
  60. e_i = self.state.index(0)
  61. e_i_x = e_i % 3
  62. e_i_y = e_i / 3
  63. # Generate possible states
  64. if(e_i_x > 0):
  65. pos = board()
  66. n_s = list(self.state)
  67. # Swap tiles
  68. a, b = e_i, (e_i - 1)
  69. n_s[a], n_s[b] = n_s[b], n_s[a]
  70. pos.set_board(n_s, self.move_count + 1, self)
  71. # Ignore previous state
  72. if pos != self.last_board:
  73. possible_states.append(pos)
  74. if(e_i_x < 2):
  75. pos = board()
  76. n_s = list(self.state)
  77. # Swap tiles
  78. a, b = e_i, (e_i + 1)
  79. n_s[a], n_s[b] = n_s[b], n_s[a]
  80. pos.set_board(n_s, self.move_count + 1, self)
  81. # Ignore previous state
  82. if pos != self.last_board:
  83. possible_states.append(pos)
  84. if(e_i_y > 0):
  85. pos = board()
  86. n_s = list(self.state)
  87. # Swap tiles
  88. a, b = e_i, (e_i - 3)
  89. n_s[a], n_s[b] = n_s[b], n_s[a]
  90. pos.set_board(n_s, self.move_count + 1, self)
  91. # Ignore previous state
  92. if pos != self.last_board:
  93. possible_states.append(pos)
  94. if(e_i_y < 2):
  95. pos = board()
  96. n_s = list(self.state)
  97. # Swap tiles
  98. a, b = e_i, (e_i + 3)
  99. n_s[a], n_s[b] = n_s[b], n_s[a]
  100. pos.set_board(n_s, self.move_count + 1, self)
  101. # Ignore previous state
  102. if pos != self.last_board:
  103. possible_states.append(pos)
  104. return possible_states
  105. # Calculate the Manhattan Distance between two board states
  106. def manhattan_distance(self, other):
  107. total_dist = 0
  108. for n in range(9):
  109. i_c = self.state.index(n)
  110. i_x_c = i_c % 3
  111. i_y_c = i_c / 3
  112. i_g = other.state.index(n)
  113. i_x_g = i_g % 3
  114. i_y_g = i_g / 3
  115. tile_dist = abs(i_x_c - i_x_g) + abs(i_y_c - i_y_g)
  116. total_dist += tile_dist
  117. return total_dist
  118. # Rebuild the Path taken to the solution by following the chain of last_boards up to the starting board and print it
  119. def print_path(self):
  120. if self.last_board:
  121. self.last_board.print_path()
  122. print "Move number: %d \n%d %d %d \n%d %d %d \n%d %d %d \n" % tuple([self.move_count] + self.state)
  123. def a_star(start_board, goal_board):
  124. # Setup
  125. closed_set = []
  126. open_set = [start_board]
  127. start_board.f_score = start_board.move_count + start_board.manhattan_distance(goal_board)
  128. while open_set:
  129. cur_board = open_set.pop(0) # pop the first board
  130. # End after a reasonable number of board permutations are explored
  131. if len(closed_set) > MAX_RUN:
  132. return -1
  133. # Return if we have found a solution
  134. if cur_board == goal_board:
  135. print "\nSolution found in %d moves\n" % (cur_board.move_count)
  136. return cur_board
  137. # Add the current board to the closed_set
  138. closed_set.append(cur_board)
  139. # Get all unvisited neighbor nodes
  140. # Ignore duplicates and boards in the closed_set
  141. neighbors = cur_board.get_possible_moves()
  142. neighbors_remove = []
  143. for n in neighbors:
  144. for c in closed_set:
  145. if n == c:
  146. neighbors_remove.append(n)
  147. for n_r in neighbors_remove:
  148. if n_r in neighbors:
  149. neighbors.remove(n_r)
  150. for n in neighbors:
  151. # Check if the current board is in the open_set already
  152. i = -1
  153. for o_b in open_set:
  154. if o_b == n:
  155. i = open_set.index(o_b)
  156. if i > -1:
  157. # check if we have found a shorter path to the board if found in the open_set
  158. if n.move_count < open_set[i].move_count:
  159. continue
  160. else:
  161. n.f_score = n.move_count + n.manhattan_distance(goal_board)
  162. open_set[i] = n
  163. continue
  164. # Board not found in the open_set, add it
  165. n.f_score = n.move_count + n.manhattan_distance(goal_board)
  166. open_set.append(n)
  167. # Sort the open_set by predicted score
  168. open_set = sorted(open_set, key=lambda x: x.f_score)
  169. # No solution found for the starting board
  170. return None
  171. #Attempt to solve a puzzle with the given starting state
  172. def solve(input_list):
  173. # Generate the goal board
  174. goal_state = board()
  175. # Generate the starting board
  176. start_state = board()
  177. start_state.state = input_list
  178. # Attempt to solve
  179. solution = a_star(start_state, goal_state)
  180. # Handle various solution states
  181. if solution != -1:
  182. if solution:
  183. solution.print_path()
  184. else:
  185. print "No solution found."
  186. else:
  187. # Couldn't find a solution in few enough tries
  188. print "No solution found in %d reachable board permutations." % (MAX_RUN)
  189. # Read starting state from a file
  190. def file_input(filename):
  191. f_input = []
  192. try:
  193. f = open(filename, "r")
  194. s = f.read()
  195. f.close()
  196. s_0 = s.split()
  197. f_input = [int(float(i)) if '.' in i else int(i) for i in s_0]
  198. except ValueError as e:
  199. # User entered invalid character (i.e. not an int or float)
  200. print "File contents invalid, must contain all numbers 0-8 and only numbers 0-8"
  201. return
  202. except IOError as e:
  203. # Problem Opening the file
  204. print "Error opening the file: ", e
  205. return
  206. except Exception as e:
  207. # Handle unexpected errors
  208. print "Unexpected error: ", e
  209. print "Please try again"
  210. return
  211. # Check if file input was valid
  212. if sorted(f_input) != [0,1,2,3,4,5,6,7,8]:
  213. print "File contents invalid, must contain all numbers 0-8 and only numbers 0-8"
  214. return
  215. print "Input is valid"
  216. solve(f_input)
  217. # Request user input of starting state
  218. def loop_input():
  219. while True:
  220. # Get and parse user input, handling exceptions
  221. u_input = []
  222. try:
  223. s = raw_input("line 1: ")
  224. s_0 = s.split()
  225. s = raw_input("line 2: ")
  226. s_0 += s.split()
  227. s = raw_input("line 3: ")
  228. s_0 += s.split()
  229. u_input = [int(float(i)) if '.' in i else int(i) for i in s_0]
  230. except ValueError as e:
  231. # User entered invalid character (i.e. not an int or float)
  232. print "Invalid input, please enter all numbers 0-8 and only numbers 0-8a"
  233. continue
  234. except Exception as e:
  235. # Handle unexpected errors
  236. print "Unexpected error: ", e
  237. print "Please try again"
  238. continue
  239. # Check if input was valid
  240. if sorted(u_input) != [0,1,2,3,4,5,6,7,8]:
  241. print "Invalid input, please enter all numbers 0-8 and only numbers 0-8"
  242. continue
  243. print "Input is valid"
  244. solve(u_input)
  245. return
  246. def main(argv):
  247. if len(argv) > 0:
  248. file_input(argv[0])
  249. else:
  250. loop_input()
  251. if __name__ == "__main__":
  252. main(sys.argv[1:])