class Solver: def __init__(self, tile_config): self.start_state = tile_config[:] self.board = Board(tile_config) self.solutions = {} self.recursed = 0 self.all_paths = [] self.solution_path = [] def solve(self): moves = self.find_shortest_path() self.solution_path = moves if (moves != None): for move in moves: self.board.move(move) else: print("CRUD. Could not find a solution for {}.".format(self.start_state)) return self.board.solved() def find_all_paths_from_point(self, board_state, completed_moves, previous_states, paths_so_far): all_paths = paths_so_far[:] available_moves = Board(board_state).lookahead() for move in available_moves: new_board_state = available_moves[move] path = completed_moves[:] if (new_board_state == self.board.solution): # we're done path.append(move) all_paths.append(path) elif (len(completed_moves) > 0) and (move == completed_moves[-1]): # ignore attempts to undo pass elif (len(path) > 2): path.append(move) path.append("R") all_paths.append(path) elif (new_board_state in previous_states): path.append(move) path.append("X") all_paths.append(path) else: path.append(move) previous_states.append(board_state) all_paths.extend(self.find_all_paths_from_point(new_board_state, path, previous_states, all_paths)) return all_paths def find_shortest_path(self): self.all_paths = self.find_all_paths_from_point(self.start_state, [], [], []) shortest_path = None for path in self.all_paths: if (not isinstance(path[-1], int)): # skip the ones marked as not viable pass elif (shortest_path == None) or (len(shortest_path) > len(path)): shortest_path = path return shortest_path
def test_board_can_move_piece(): puzzle = Board([2, 3, 1, None]) puzzle.move(3) assert puzzle.tiles == [2, None, 1, 3]
def test_board_cannot_move_if_tile_surrounded(): puzzle = Board([1, 2, 3, 4, 5, 6, 7, None, 8]) puzzle.move(2) assert puzzle.tiles == [1, 2, 3, 4, 5, 6, 7, None, 8]
def test_board_cannot_make_diagonal_move(): puzzle = Board([2, 3, 1, None]) puzzle.move(2) assert puzzle.tiles == [2, 3, 1, None]
class Solver: def __init__(self, tile_config): self.start_state = tile_config[:] self.board = Board(tile_config) self.solutions = {} self.recursed = 0 self.all_paths = [] self.solution_path = [] self.shortest_solution = None def solve(self): # moves = self.find_shortest_path() # self.solution_path = moves self.find_all_paths_from_point(self.start_state, [], [], []) if (self.shortest_solution != None): for move in self.shortest_solution: self.board.move(move) else: print("Could not find a solution for {}.".format(self.start_state)) return self.board.solved() def find_all_paths_from_point(self, board_state, completed_moves, previous_states, paths_so_far): all_paths = paths_so_far[:] available_moves = self.lookahead(board_state) print( "\nGoing into path search loop.\nBoard state: {} \nAvailable Moves: {}" .format(board_state, available_moves)) move_scores = self.prioritize(available_moves) print("Move Scores: {}".format(move_scores)) move_priorities = sorted(move_scores, key=move_scores.get) for move in move_priorities: new_board_state = available_moves[move] path = completed_moves[:] if (new_board_state == self.board.solution): # we're done path.append(move) all_paths.append(path) if (self.shortest_solution == None): self.shortest_solution = path elif (len(path) < len(self.shortest_solution)): self.shortest_solution = path elif (len(completed_moves) > 0) and (move == completed_moves[-1]): # ignore attempts to undo pass elif (self.shortest_solution != None) and (len(all_paths) > 9000): all_paths.append(["Z"]) break # you have a solution, just give up elif ((len(path) >= 30) or ((self.shortest_solution != None) and (len(path) + 1) >= len(self.shortest_solution))): # give up path.append(move) path.append("L") all_paths.append(path) elif (new_board_state in previous_states): path.append(move) path.append("X") all_paths.append(path) else: path.append(move) previous_states.append(board_state) all_paths.extend( self.find_all_paths_from_point(new_board_state, path, previous_states, all_paths)) print("Returning from find_all_paths_from_point\nExplored {} paths". format(len(all_paths))) if (self.shortest_solution != None): print("...shortest path has {} moves\n > {}".format( len(self.shortest_solution), self.shortest_solution)) else: print("...no solution yet.\n") return all_paths def prioritize(self, possibilities): scores = {} for move in possibilities: # print("\nWorking on move {}".format(move)) board_state = possibilities[move] move_score = self.sum_scores(board_state) # lookahead_score = 0 # lookahead = self.lookahead(board_state) # for board in lookahead: # lookahead_score += self.sum_scores(lookahead[board]) # scores[move] = move_score + lookahead_score scores[move] = move_score # gc.collect() # tracemalloc.start(10) # # snapshot = tracemalloc.take_snapshot() # top_stats = snapshot.statistics('lineno') # print("[ Top 10 ]") # for stat in top_stats[:10]: # print(stat) # stats = snapshots[-1].filter_traces().compare_to(self.snapshots[-2], 'filename') # for stat in stats[:10]: # print("{} new KiB {} total KiB {} new {} total memory blocks: ".format(stat.size_diff / 1024, stat.size / 1024, # stat.count_diff, stat.count)) return scores def sum_scores(self, tiles): score = 0 print("Scoring board state: {}".format(tiles)) inversions = self.count_inversions(tiles) * 10 deltapos = self.count_delta_from_position(tiles) * 10 deltarowcol = self.count_delta_from_rowcol(tiles) * 10 countneighbor = self.count_delta_from_neighbor(tiles) print( "inversion {} | deltapos {} | deltarowcol {} | neighbor {}".format( inversions, deltapos, deltarowcol, countneighbor)) return inversions + deltarowcol + deltapos + countneighbor def count_inversions(self, tiles): numbers = [item for item in tiles if isinstance(item, int)] num_inversions = 0 for num, tile in enumerate(numbers, start=0): for i in range(num + 1, len(numbers)): if (numbers[i] < tile): num_inversions += 1 # print("...inversions is {}".format(num_inversions)) return num_inversions def count_delta_from_position(self, tiles): delta = 0 for num, tile in enumerate(tiles, start=1): if (tile != None): delta += abs(tile - num) # print("...position delta is {}".format(delta)) return delta def count_delta_from_rowcol(self, tiles): delta = 0 for num, tile in enumerate(tiles, start=1): if (tile == None): tile = (self.board.dimension)**2 tilerow = (num // self.board.dimension) + 1 tilecol = ((num + 1) % self.board.dimension) + 1 tilerow_solution = (tile // self.board.dimension) + 1 tilecol_solution = ((tile + 1) % self.board.dimension) + 1 delta = (abs(tilerow_solution - tilerow)) + (abs(tilecol_solution - tilecol)) # print("...rowcol delta is {}".format(delta)) return delta def count_delta_from_neighbor(self, tiles): count = 0 for num, tile in enumerate(tiles, start=0): neighbor_positions = self.neighbor_indices(tiles, num) for neighbor in neighbor_positions: if (tile != None and tiles[neighbor] != None): count += abs(tile - tiles[neighbor]) count += 8 - tiles.index(None) return count def lookahead(self, board): available_moves = self.get_available_moves_from(board) none_position = board.index(None) whatif = {} for tile in available_moves: whatif_board = board[:] tile_position = board.index(tile) whatif_board[tile_position] = None whatif_board[none_position] = tile whatif[tile] = whatif_board return whatif def get_available_moves_from(self, board): none_index = board.index(None) neighbor_indices = self.neighbor_indices(board, none_index) moves = [] for position in neighbor_indices: moves.append(board[position]) return sorted(moves) def neighbor_indices(self, board, position): neighbor_indices = [] upper = position - self.board.dimension if (upper >= 0): neighbor_indices.append(upper) right = position % self.board.dimension if (right != self.board.dimension - 1): neighbor_indices.append(position + 1) lower = position + self.board.dimension if (lower < len(board)): neighbor_indices.append(lower) left = position % self.board.dimension if (left != 0): neighbor_indices.append(position - 1) return neighbor_indices
class Solver: def __init__(self, tile_config): self.board = Board(tile_config) self.moves = [] self.visited_states = [] self.status = "Solving..." self.locked_tiles = [] def solve(self): if self.board.is_solvable(): self.visited_states.append(self.get_board_state()) while (not self.board.solved()) and (self.status != "stuck"): self.find_best_move() else: self.status = "Unsolvable" def get_available_moves(self): return sorted([tile for tile in self.board.tile_neighbors(None) if isinstance(tile, int)]) def get_board_state(self): board_state = self.board.tiles[:] return board_state def check_locks(self): # this is a really really dumb implementation - TEMPORARY # ONLY WORKS for 3x3s, not 4x4s self.locked_tiles = [] # if 1 is in the 1 place, lock it if (self.board.tiles[0] == 1): self.locked_tiles.append(1) # and if 2 AND 3 are in place, lock them if (self.board.tiles[1] == 2 and self.board.tiles[2] == 3): self.locked_tiles.append(2) self.locked_tiles.append(3) if ((self.board.dimension == 3) and (1 in self.locked_tiles) and (self.board.tiles[3] == 4) and (self.board.tiles[6] == 7) ): self.locked_tiles.append(4) self.locked_tiles.append(7) def find_best_move(self): possible_moves = self.board.lookahead() move_scores = {} for move in possible_moves: move_scores[move] = self.board.solution_diff_score_from(possible_moves[move]) # only consider moves that don't get us back to a known state if (move in self.locked_tiles): move_scores[move] += 9999 if len(self.visited_states) > 0 and (possible_moves[move] == self.visited_states[-1]): move_scores[move] += 99999 if (possible_moves[move] in self.visited_states): move_scores[move] += 99 best_move = min(move_scores, key=move_scores.get) # print("BEST MOVE: {}".format(best_move)) self.board.move(best_move) self.moves.append(best_move) self.visited_states.append(self.get_board_state()) # if (best_move in self.locked_tiles): # self.locked_tiles.remove(best_move) self.check_locks() if (len(self.moves) > 500): self.status = "stuck"