|
| 1 | +# eightpuzzle.py |
| 2 | +# -------------- |
| 3 | +# Licensing Information: You are free to use or extend these projects for |
| 4 | +# educational purposes provided that (1) you do not distribute or publish |
| 5 | +# solutions, (2) you retain this notice, and (3) you provide clear |
| 6 | +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. |
| 7 | +# |
| 8 | +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. |
| 9 | +# The core projects and autograders were primarily created by John DeNero |
| 10 | + |
| 11 | +# Student side autograding was added by Brad Miller, Nick Hay, and |
| 12 | +# Pieter Abbeel ([email protected]). |
| 13 | + |
| 14 | + |
| 15 | +import search |
| 16 | +import random |
| 17 | + |
| 18 | +# Module Classes |
| 19 | + |
| 20 | +class EightPuzzleState: |
| 21 | + """ |
| 22 | + The Eight Puzzle is described in the course textbook on |
| 23 | + page 64. |
| 24 | +
|
| 25 | + This class defines the mechanics of the puzzle itself. The |
| 26 | + task of recasting this puzzle as a search problem is left to |
| 27 | + the EightPuzzleSearchProblem class. |
| 28 | + """ |
| 29 | + |
| 30 | + def __init__( self, numbers ): |
| 31 | + """ |
| 32 | + Constructs a new eight puzzle from an ordering of numbers. |
| 33 | +
|
| 34 | + numbers: a list of integers from 0 to 8 representing an |
| 35 | + instance of the eight puzzle. 0 represents the blank |
| 36 | + space. Thus, the list |
| 37 | +
|
| 38 | + [1, 0, 2, 3, 4, 5, 6, 7, 8] |
| 39 | +
|
| 40 | + represents the eight puzzle: |
| 41 | + ------------- |
| 42 | + | 1 | | 2 | |
| 43 | + ------------- |
| 44 | + | 3 | 4 | 5 | |
| 45 | + ------------- |
| 46 | + | 6 | 7 | 8 | |
| 47 | + ------------ |
| 48 | +
|
| 49 | + The configuration of the puzzle is stored in a 2-dimensional |
| 50 | + list (a list of lists) 'cells'. |
| 51 | + """ |
| 52 | + self.cells = [] |
| 53 | + numbers = numbers[:] # Make a copy so as not to cause side-effects. |
| 54 | + numbers.reverse() |
| 55 | + for row in range( 3 ): |
| 56 | + self.cells.append( [] ) |
| 57 | + for col in range( 3 ): |
| 58 | + self.cells[row].append( numbers.pop() ) |
| 59 | + if self.cells[row][col] == 0: |
| 60 | + self.blankLocation = row, col |
| 61 | + |
| 62 | + def isGoal( self ): |
| 63 | + """ |
| 64 | + Checks to see if the puzzle is in its goal state. |
| 65 | +
|
| 66 | + ------------- |
| 67 | + | | 1 | 2 | |
| 68 | + ------------- |
| 69 | + | 3 | 4 | 5 | |
| 70 | + ------------- |
| 71 | + | 6 | 7 | 8 | |
| 72 | + ------------- |
| 73 | +
|
| 74 | + >>> EightPuzzleState([0, 1, 2, 3, 4, 5, 6, 7, 8]).isGoal() |
| 75 | + True |
| 76 | +
|
| 77 | + >>> EightPuzzleState([1, 0, 2, 3, 4, 5, 6, 7, 8]).isGoal() |
| 78 | + False |
| 79 | + """ |
| 80 | + current = 0 |
| 81 | + for row in range( 3 ): |
| 82 | + for col in range( 3 ): |
| 83 | + if current != self.cells[row][col]: |
| 84 | + return False |
| 85 | + current += 1 |
| 86 | + return True |
| 87 | + |
| 88 | + def legalMoves( self ): |
| 89 | + """ |
| 90 | + Returns a list of legal moves from the current state. |
| 91 | +
|
| 92 | + Moves consist of moving the blank space up, down, left or right. |
| 93 | + These are encoded as 'up', 'down', 'left' and 'right' respectively. |
| 94 | +
|
| 95 | + >>> EightPuzzleState([0, 1, 2, 3, 4, 5, 6, 7, 8]).legalMoves() |
| 96 | + ['down', 'right'] |
| 97 | + """ |
| 98 | + moves = [] |
| 99 | + row, col = self.blankLocation |
| 100 | + if(row != 0): |
| 101 | + moves.append('up') |
| 102 | + if(row != 2): |
| 103 | + moves.append('down') |
| 104 | + if(col != 0): |
| 105 | + moves.append('left') |
| 106 | + if(col != 2): |
| 107 | + moves.append('right') |
| 108 | + return moves |
| 109 | + |
| 110 | + def result(self, move): |
| 111 | + """ |
| 112 | + Returns a new eightPuzzle with the current state and blankLocation |
| 113 | + updated based on the provided move. |
| 114 | +
|
| 115 | + The move should be a string drawn from a list returned by legalMoves. |
| 116 | + Illegal moves will raise an exception, which may be an array bounds |
| 117 | + exception. |
| 118 | +
|
| 119 | + NOTE: This function *does not* change the current object. Instead, |
| 120 | + it returns a new object. |
| 121 | + """ |
| 122 | + row, col = self.blankLocation |
| 123 | + if(move == 'up'): |
| 124 | + newrow = row - 1 |
| 125 | + newcol = col |
| 126 | + elif(move == 'down'): |
| 127 | + newrow = row + 1 |
| 128 | + newcol = col |
| 129 | + elif(move == 'left'): |
| 130 | + newrow = row |
| 131 | + newcol = col - 1 |
| 132 | + elif(move == 'right'): |
| 133 | + newrow = row |
| 134 | + newcol = col + 1 |
| 135 | + else: |
| 136 | + raise "Illegal Move" |
| 137 | + |
| 138 | + # Create a copy of the current eightPuzzle |
| 139 | + newPuzzle = EightPuzzleState([0, 0, 0, 0, 0, 0, 0, 0, 0]) |
| 140 | + newPuzzle.cells = [values[:] for values in self.cells] |
| 141 | + # And update it to reflect the move |
| 142 | + newPuzzle.cells[row][col] = self.cells[newrow][newcol] |
| 143 | + newPuzzle.cells[newrow][newcol] = self.cells[row][col] |
| 144 | + newPuzzle.blankLocation = newrow, newcol |
| 145 | + |
| 146 | + return newPuzzle |
| 147 | + |
| 148 | + # Utilities for comparison and display |
| 149 | + def __eq__(self, other): |
| 150 | + """ |
| 151 | + Overloads '==' such that two eightPuzzles with the same configuration |
| 152 | + are equal. |
| 153 | +
|
| 154 | + >>> EightPuzzleState([0, 1, 2, 3, 4, 5, 6, 7, 8]) == \ |
| 155 | + EightPuzzleState([1, 0, 2, 3, 4, 5, 6, 7, 8]).result('left') |
| 156 | + True |
| 157 | + """ |
| 158 | + for row in range( 3 ): |
| 159 | + if self.cells[row] != other.cells[row]: |
| 160 | + return False |
| 161 | + return True |
| 162 | + |
| 163 | + def __hash__(self): |
| 164 | + return hash(str(self.cells)) |
| 165 | + |
| 166 | + def __getAsciiString(self): |
| 167 | + """ |
| 168 | + Returns a display string for the maze |
| 169 | + """ |
| 170 | + lines = [] |
| 171 | + horizontalLine = ('-' * (13)) |
| 172 | + lines.append(horizontalLine) |
| 173 | + for row in self.cells: |
| 174 | + rowLine = '|' |
| 175 | + for col in row: |
| 176 | + if col == 0: |
| 177 | + col = ' ' |
| 178 | + rowLine = rowLine + ' ' + col.__str__() + ' |' |
| 179 | + lines.append(rowLine) |
| 180 | + lines.append(horizontalLine) |
| 181 | + return '\n'.join(lines) |
| 182 | + |
| 183 | + def __str__(self): |
| 184 | + return self.__getAsciiString() |
| 185 | + |
| 186 | +# TODO: Implement The methods in this class |
| 187 | + |
| 188 | +class EightPuzzleSearchProblem(search.SearchProblem): |
| 189 | + """ |
| 190 | + Implementation of a SearchProblem for the Eight Puzzle domain |
| 191 | +
|
| 192 | + Each state is represented by an instance of an eightPuzzle. |
| 193 | + """ |
| 194 | + def __init__(self,puzzle): |
| 195 | + "Creates a new EightPuzzleSearchProblem which stores search information." |
| 196 | + self.puzzle = puzzle |
| 197 | + |
| 198 | + def getStartState(self): |
| 199 | + return puzzle |
| 200 | + |
| 201 | + def isGoalState(self,state): |
| 202 | + return state.isGoal() |
| 203 | + |
| 204 | + def getSuccessors(self,state): |
| 205 | + """ |
| 206 | + Returns list of (successor, action, stepCost) pairs where |
| 207 | + each succesor is either left, right, up, or down |
| 208 | + from the original state and the cost is 1.0 for each |
| 209 | + """ |
| 210 | + succ = [] |
| 211 | + for a in state.legalMoves(): |
| 212 | + succ.append((state.result(a), a, 1)) |
| 213 | + return succ |
| 214 | + |
| 215 | + def getCostOfActions(self, actions): |
| 216 | + """ |
| 217 | + actions: A list of actions to take |
| 218 | +
|
| 219 | + This method returns the total cost of a particular sequence of actions. The sequence must |
| 220 | + be composed of legal moves |
| 221 | + """ |
| 222 | + return len(actions) |
| 223 | + |
| 224 | +EIGHT_PUZZLE_DATA = [[1, 0, 2, 3, 4, 5, 6, 7, 8], |
| 225 | + [1, 7, 8, 2, 3, 4, 5, 6, 0], |
| 226 | + [4, 3, 2, 7, 0, 5, 1, 6, 8], |
| 227 | + [5, 1, 3, 4, 0, 2, 6, 7, 8], |
| 228 | + [1, 2, 5, 7, 6, 8, 0, 4, 3], |
| 229 | + [0, 3, 1, 6, 8, 2, 7, 5, 4]] |
| 230 | + |
| 231 | +def loadEightPuzzle(puzzleNumber): |
| 232 | + """ |
| 233 | + puzzleNumber: The number of the eight puzzle to load. |
| 234 | +
|
| 235 | + Returns an eight puzzle object generated from one of the |
| 236 | + provided puzzles in EIGHT_PUZZLE_DATA. |
| 237 | +
|
| 238 | + puzzleNumber can range from 0 to 5. |
| 239 | +
|
| 240 | + >>> print loadEightPuzzle(0) |
| 241 | + ------------- |
| 242 | + | 1 | | 2 | |
| 243 | + ------------- |
| 244 | + | 3 | 4 | 5 | |
| 245 | + ------------- |
| 246 | + | 6 | 7 | 8 | |
| 247 | + ------------- |
| 248 | + """ |
| 249 | + return EightPuzzleState(EIGHT_PUZZLE_DATA[puzzleNumber]) |
| 250 | + |
| 251 | +def createRandomEightPuzzle(moves=100): |
| 252 | + """ |
| 253 | + moves: number of random moves to apply |
| 254 | +
|
| 255 | + Creates a random eight puzzle by applying |
| 256 | + a series of 'moves' random moves to a solved |
| 257 | + puzzle. |
| 258 | + """ |
| 259 | + puzzle = EightPuzzleState([0,1,2,3,4,5,6,7,8]) |
| 260 | + for i in range(moves): |
| 261 | + # Execute a random legal move |
| 262 | + puzzle = puzzle.result(random.sample(puzzle.legalMoves(), 1)[0]) |
| 263 | + return puzzle |
| 264 | + |
| 265 | +if __name__ == '__main__': |
| 266 | + puzzle = createRandomEightPuzzle(25) |
| 267 | + print('A random puzzle:') |
| 268 | + print(puzzle) |
| 269 | + |
| 270 | + problem = EightPuzzleSearchProblem(puzzle) |
| 271 | + path = search.breadthFirstSearch(problem) |
| 272 | + print('BFS found a path of %d moves: %s' % (len(path), str(path))) |
| 273 | + curr = puzzle |
| 274 | + i = 1 |
| 275 | + for a in path: |
| 276 | + curr = curr.result(a) |
| 277 | + print('After %d move%s: %s' % (i, ("", "s")[i>1], a)) |
| 278 | + print(curr) |
| 279 | + |
| 280 | + raw_input("Press return for the next state...") # wait for key stroke |
| 281 | + i += 1 |
0 commit comments