|
| 1 | +''' |
| 2 | +8 PUZZLE PROBLEM SOLVING USING A* ALGORITHM |
| 3 | +
|
| 4 | +An instance of the n-puzzle game consists of a board holding n^{2}-1 |
| 5 | +distinct movable tiles, plus an empty space. The tiles are numbers from |
| 6 | +the set 1,..,n^{2}-1. For any such board, the empty space may be legally |
| 7 | +swapped with any tile horizontally or vertically adjacent to it. In this |
| 8 | +assignment, the blank space is going to be represented with the number 0. |
| 9 | +Given an initial state of the board, the combinatorial search problem is |
| 10 | +to find a sequence of moves that transitions this state to the goal state; |
| 11 | +that is, the configuration with all tiles arranged in ascending order |
| 12 | +0,1,..,n^{2}-1. |
| 13 | +
|
| 14 | +So, this is the goal state that we want to reach: |
| 15 | +[1, 2, 3] |
| 16 | +[8, 0, 4] |
| 17 | +[7, 6, 5] |
| 18 | +
|
| 19 | +The search space is the set of all possible states reachable from the |
| 20 | +initial state. The blank space may be swapped with a component in one of |
| 21 | +the four directions {‘Up’, ‘Down’, ‘Left’, ‘Right’}, one move at a time. |
| 22 | +
|
| 23 | +A* algorithm has 3 parameters: |
| 24 | +g: the cost of moving from the initial cell to the current cell. |
| 25 | +h: also known as the heuristic value, it is the estimated cost of moving from |
| 26 | + the current cell to the final cell. The actual cost cannot be calculated until |
| 27 | + the final cell is reached. Hence, h is the estimated cost. We must make sure |
| 28 | + that there is never an over estimation of the cost. |
| 29 | +f: it is the sum of g and h. So, f = g + h |
| 30 | +We always go to the state that has minimum 'f' value. |
| 31 | +
|
| 32 | +''' |
| 33 | + |
| 34 | +# importing the necessary libraries |
| 35 | +from time import time |
| 36 | +from queue import PriorityQueue |
| 37 | + |
| 38 | +# creating a class Puzzle |
| 39 | +class Puzzle: |
| 40 | + # setting the goal state of 8-puzzle |
| 41 | + goal_state=[1,2,3,8,0,4,7,6,5] |
| 42 | + # setting up the members of a class |
| 43 | + heuristic=None |
| 44 | + evaluation_function=None |
| 45 | + needs_hueristic=False |
| 46 | + num_of_instances=0 |
| 47 | + |
| 48 | + # constructor to initialize the class members |
| 49 | + def __init__(self,state,parent,action,path_cost,needs_hueristic=False): |
| 50 | + self.parent=parent |
| 51 | + self.state=state |
| 52 | + self.action=action |
| 53 | + # calculating the path_cost as the sum of its parent cost and path_cost |
| 54 | + if parent: |
| 55 | + self.path_cost = path_cost + parent.path_cost |
| 56 | + else: |
| 57 | + self.path_cost = path_cost |
| 58 | + if needs_hueristic: |
| 59 | + self.needs_hueristic=True |
| 60 | + self.generate_heuristic() |
| 61 | + # calculating the expression as f = g + h |
| 62 | + self.evaluation_function= path_cost + needs_hueristic |
| 63 | + # incrementing the number of instance by 1 |
| 64 | + Puzzle.num_of_instances+=1 |
| 65 | + |
| 66 | + # method used to display a state of 8-puzzle |
| 67 | + def __str__(self): |
| 68 | + return str(self.state[0:3])+'\n'+str(self.state[3:6])+'\n'+str(self.state[6:9]) |
| 69 | + |
| 70 | + # method used to generate a heuristic value |
| 71 | + def generate_heuristic(self): |
| 72 | + self.heuristic=0 |
| 73 | + for num in range(1,9): |
| 74 | + # calculating the heuristic value as manhattan distance which is the absolute |
| 75 | + # difference between current state and goal state. |
| 76 | + # using index() method to get the index of num in state |
| 77 | + distance= abs(self.state.index(num) - self.goal_state.index(num)) |
| 78 | + i=int(distance/3) |
| 79 | + j=int(distance%3) |
| 80 | + self.heuristic=self.heuristic+i+j |
| 81 | + |
| 82 | + def goal_test(self): |
| 83 | + # including a condition to compare the current state with the goal state |
| 84 | + if self.state == self.goal_state: |
| 85 | + return True |
| 86 | + return False |
| 87 | + |
| 88 | + @staticmethod |
| 89 | + def find_legal_actions(i,j): |
| 90 | + # find the legal actions as Up, Down, Left, Right based on each cell of state |
| 91 | + legal_action = ['U', 'D', 'L', 'R'] |
| 92 | + if i == 0: # up is disable |
| 93 | + # if row is 0 in board then up is disable |
| 94 | + legal_action.remove('U') |
| 95 | + elif i == 2: |
| 96 | + legal_action.remove('D') |
| 97 | + if j == 0: |
| 98 | + legal_action.remove('L') |
| 99 | + elif j == 2: |
| 100 | + legal_action.remove('R') |
| 101 | + # returnig legal_action |
| 102 | + return legal_action |
| 103 | + |
| 104 | + # method to generate the child of the current state of the board |
| 105 | + def generate_child(self): |
| 106 | + children=[] |
| 107 | + x = self.state.index(0) |
| 108 | + # generating the row (i) & col (j) position based on the current index of 0 on the board |
| 109 | + i = int(x/3) |
| 110 | + j = int(x%3) |
| 111 | + # calling the method to find the legal actions based on i and j values |
| 112 | + legal_actions=self.find_legal_actions(i,j) |
| 113 | + |
| 114 | + for action in legal_actions: |
| 115 | + new_state = self.state.copy() |
| 116 | + # if the legal action is UP |
| 117 | + if action is 'U': |
| 118 | + # swapping between current index of 0 with its up element on the board |
| 119 | + new_state[x], new_state[x-3] = new_state[x-3], new_state[x] |
| 120 | + elif action is 'D': |
| 121 | + # swapping between current index of 0 with its down element on the board |
| 122 | + new_state[x], new_state[x+3] = new_state[x+3], new_state[x] |
| 123 | + elif action is 'L': |
| 124 | + # swapping between the current index of 0 with its left element on the board |
| 125 | + new_state[x], new_state[x-1] = new_state[x-1], new_state[x] |
| 126 | + elif action is 'R': |
| 127 | + # swapping between the current index of 0 with its right element on the board |
| 128 | + new_state[x], new_state[x+1] = new_state[x+1], new_state[x] |
| 129 | + # appending the new_state of Puzzle object with parent, action,path_cost is 1, its needs_hueristic flag |
| 130 | + children.append(Puzzle(new_state, self, action, 1, self.needs_hueristic)) |
| 131 | + # returning the children |
| 132 | + return children |
| 133 | + |
| 134 | + # method to find the solution |
| 135 | + def find_solution(self): |
| 136 | + solution = [] |
| 137 | + all_states = [] |
| 138 | + solution.append(self.action) |
| 139 | + all_states.append(self) |
| 140 | + path = self |
| 141 | + while path.parent != None: |
| 142 | + path = path.parent |
| 143 | + solution.append(path.action) |
| 144 | + all_states.append(path) |
| 145 | + solution = solution[:-1] |
| 146 | + solution.reverse |
| 147 | + |
| 148 | + print('\nAll states from goal to initial: ') |
| 149 | + for i in all_states: |
| 150 | + print(i, '\n') |
| 151 | + return solution |
| 152 | + |
| 153 | +# method for A-star search |
| 154 | +# passing the initial_state as parameter to the breadth_first_search method |
| 155 | +def Astar_search(initial_state): |
| 156 | + count=0 |
| 157 | + # creating an empty list of explored nodes |
| 158 | + explored=[] |
| 159 | + # creating a instance of Puzzle as initial_state, None, None, 0, True |
| 160 | + start_node=Puzzle(initial_state, None, None, 0, True) |
| 161 | + q = PriorityQueue() |
| 162 | + # putting a tuple with start_node.evaluation_function, count, start_node into PriorityQueue |
| 163 | + q.put((start_node.evaluation_function, count, start_node)) |
| 164 | + |
| 165 | + while not q.empty(): |
| 166 | + # getting the current node of a queue. Use the get() method of Queue |
| 167 | + node=q.get() |
| 168 | + # extracting the current node of a PriorityQueue based on the index of a tuple. |
| 169 | + # referring a tuple format put in PriorityQueue |
| 170 | + node=node[2] |
| 171 | + # appending the state of node in the explored list as node.state |
| 172 | + explored.append(node.state) |
| 173 | + if node.goal_test(): |
| 174 | + return node.find_solution() |
| 175 | + # calling the generate_child method to generate the child node of current node |
| 176 | + children=node.generate_child() |
| 177 | + for child in children: |
| 178 | + if child.state not in explored: |
| 179 | + count += 1 |
| 180 | + # putting a tuple with child.evaluation_function, count, child into PriorityQueue |
| 181 | + q.put((child.evaluation_function, count, child)) |
| 182 | + return |
| 183 | + |
| 184 | +# starting executing the 8-puzzle with setting up the initial state |
| 185 | +# here we have considered 3 initial state intitalized using state variable |
| 186 | +state= [1, 3, 4, |
| 187 | + 8, 6, 2, |
| 188 | + 7, 0, 5] |
| 189 | + |
| 190 | +# initializing the num_of_instances to zero |
| 191 | +Puzzle.num_of_instances = 0 |
| 192 | +# set t0 to current time |
| 193 | +t0 = time() |
| 194 | +astar = Astar_search(state) |
| 195 | +# getting the time t1 after executing the breadth_first_search method |
| 196 | +t1 = time() - t0 |
| 197 | +print('A*:',astar) |
| 198 | +print('space:', Puzzle.num_of_instances) |
| 199 | +print('time:', t1) |
| 200 | +print() |
| 201 | +print('------------------------------------------') |
| 202 | + |
| 203 | +''' |
| 204 | +Sample working: |
| 205 | +
|
| 206 | +All states from goal to initial: |
| 207 | +[1, 2, 3] |
| 208 | +[8, 0, 4] |
| 209 | +[7, 6, 5] |
| 210 | +
|
| 211 | +[1, 0, 3] |
| 212 | +[8, 2, 4] |
| 213 | +[7, 6, 5] |
| 214 | +
|
| 215 | +[1, 3, 0] |
| 216 | +[8, 2, 4] |
| 217 | +[7, 6, 5] |
| 218 | +
|
| 219 | +[1, 3, 4] |
| 220 | +[8, 2, 0] |
| 221 | +[7, 6, 5] |
| 222 | +
|
| 223 | +[1, 3, 4] |
| 224 | +[8, 0, 2] |
| 225 | +[7, 6, 5] |
| 226 | +
|
| 227 | +[1, 3, 4] |
| 228 | +[8, 6, 2] |
| 229 | +[7, 0, 5] |
| 230 | +
|
| 231 | +A*: ['D', 'L', 'U', 'R', 'U'] |
| 232 | +space: 117 |
| 233 | +time: 0.0029821395874023438 |
| 234 | +
|
| 235 | +''' |
0 commit comments