def __init__(self, maze_dim): """ Used to set up attributes that the robot will use to learn and navigate the maze. """ # Position-related attributes self.robot_pos = {'location': [0, 0], 'heading': 'up'} # Current pos self.steps_first_round = 0 self.steps_final_round = 0 self.maze_dim = maze_dim self.maze_representation = None # Goal-related attributes center = maze_dim/2 self.center_locations = [ [center, center], [center - 1, center], [center, center - 1], [center - 1, center - 1]] self.reached_destination = False # For exploring state self.exploring = False self.steps_exploring = 0 self.consecutive_explored_cells = 0 # Initialize terrain self.terrain = Terrain(maze_dim) # Algorithm to use: self.algorithm = None if str(sys.argv[2]).lower() == 'ff': self.algorithm = FloodFill() elif str(sys.argv[2]).lower() == 'ar': self.algorithm = AlwaysRight() elif str(sys.argv[2]).lower() == 'mr': self.algorithm = ModifiedRight() else: raise ValueError( "Incorrect algorithm name. Options are: " "\n- 'ff': flood-fill" "\n- 'ar': always-right" "\n- 'mr': modified-right (prefers unvisited cells)" ) # Explore after reaching center of the maze: if str(sys.argv[3]).lower() == 'true': self.explore_after_center = True elif str(sys.argv[3]).lower() == 'false': self.explore_after_center = False else: raise ValueError( "Incorrect explore value: Options are: " "\n- 'true': to keep exploring after reaching the center" "\n- 'false': to end run immediately after reaching the center" )
class Robot(object): def __init__(self, maze_dim): """ Used to set up attributes that the robot will use to learn and navigate the maze. """ # Position-related attributes self.robot_pos = {'location': [0, 0], 'heading': 'up'} # Current pos self.steps_first_round = 0 self.steps_final_round = 0 self.maze_dim = maze_dim self.maze_representation = None # Goal-related attributes center = maze_dim/2 self.center_locations = [ [center, center], [center - 1, center], [center, center - 1], [center - 1, center - 1]] self.reached_destination = False # For exploring state self.exploring = False self.steps_exploring = 0 self.consecutive_explored_cells = 0 # Initialize terrain self.terrain = Terrain(maze_dim) # Algorithm to use: self.algorithm = None if str(sys.argv[2]).lower() == 'ff': self.algorithm = FloodFill() elif str(sys.argv[2]).lower() == 'ar': self.algorithm = AlwaysRight() elif str(sys.argv[2]).lower() == 'mr': self.algorithm = ModifiedRight() else: raise ValueError( "Incorrect algorithm name. Options are: " "\n- 'ff': flood-fill" "\n- 'ar': always-right" "\n- 'mr': modified-right (prefers unvisited cells)" ) # Explore after reaching center of the maze: if str(sys.argv[3]).lower() == 'true': self.explore_after_center = True elif str(sys.argv[3]).lower() == 'false': self.explore_after_center = False else: raise ValueError( "Incorrect explore value: Options are: " "\n- 'true': to keep exploring after reaching the center" "\n- 'false': to end run immediately after reaching the center" ) def next_move(self, sensors): """ Use this function to determine the next move the robot should make, based on the input from the sensors after its previous move. Sensor inputs are a list of three distances from the robot's left, front, and right-facing sensors, in that order. Outputs should be a tuple of two values. The first value indicates robot rotation (if any), as a number: 0 for no rotation, +90 for a 90-degree rotation clockwise, and -90 for a 90-degree rotation counterclockwise. Other values will result in no rotation. The second value indicates robot movement, and the robot will attempt to move the number of indicated squares: a positive number indicates forwards movement, while a negative number indicates backwards movement. The robot may move a maximum of three units per turn. Any excess movement is ignored. If the robot wants to end a run (e.g. during the first training run in the maze) then returning the tuple ('Reset', 'Reset') will indicate to the tester to end the run and return the robot to the start. """ # Store current location and direction x, y, heading = self.get_current_position() # Get walls for current location walls = self.get_walls_for_current_location(x, y, heading, sensors) # If we have reached the center of the maze if self.is_at_center_of_the_maze(x, y): # Move backwards rotation = 0 movement = -1 # Update terrain (visual representation) self.terrain.update(x, y, heading, walls, self.exploring) # State that we have reached destination self.reached_destination = True # Set flags to exploring self.exploring = True # Else, first update distances, then get next move else: # 1) Push current location to stack self.terrain.cells_to_check.append([x, y]) # 2) Add current cell to stack of visited destinations if [x, y] not in self.terrain.visited_before_reaching_destination: self.terrain.visited_before_reaching_destination.append([x, y]) # 4) Update terrain and distances self.terrain.update(x, y, heading, walls, self.exploring) # 4) Get next move rotation, movement = self.get_next_move(x, y, heading, sensors) self.update_location(rotation, movement) # If we have reached destination, reset values if rotation == 'Reset' and movement == 'Reset': self.reset_values() # If we are about to hit the goal in the second round if self.robot_pos['location'] in self.center_locations \ and self.steps_final_round != 0: self.report_results() return rotation, movement # -------------------------------------------- # LOCATION-RELATED # -------------------------------------------- def reset_values(self): self.robot_pos = {'location': [0, 0], 'heading': 'up'} def get_current_position(self): heading = self.robot_pos['heading'] location = self.robot_pos['location'] x = location[0] y = location[1] return x, y, heading def is_at_starting_position(self, x, y): return x == 0 and y == 0 def is_at_center_of_the_maze(self, x, y): return [x, y] in self.center_locations def is_at_a_dead_end(self, sensors): x, y, heading = self.get_current_position() adj_distances, adj_visited = \ self.terrain.get_adj_info(x, y, heading, sensors, False) return sensors == [0, 0, 0] or adj_distances == list(MAX_DISTANCES) def get_walls_for_current_location(self, x, y, heading, sensors): if self.is_at_starting_position(x, y): walls = [1, 0, 1, 1] # If it had been visited before, just get those values elif self.terrain.grid[x][y].visited != '': walls = self.terrain.grid[x][y].get_total_walls() # Else, get current walls. Note that it can only have real walls # since the location has never been visited, and imaginary walls # are the result of dead ends that force the robot to the prev location else: # Placeholder walls = [0, 0, 0, 0] # Change sensor info to wall info walls_sensors = [1 if x == 0 else 0 for x in sensors] # Map walls to correct x and y coordinates for i in range(len(walls_sensors)): dir_sensor = dir_sensors[heading][i] index = wall_index[dir_sensor] walls[index] = walls_sensors[i] # Update missing wall index (the cell right behind the robot) index = wall_index[dir_reverse[heading]] walls[index] = 0 return walls def get_new_direction(self, rotation): if rotation == -90: return dir_sensors[self.robot_pos['heading']][0] elif rotation == 90: return dir_sensors[self.robot_pos['heading']][2] else: return self.robot_pos['heading'] def update_location(self, rotation, movement): if movement == 'Reset' or rotation == 'Reset': return else: movement = int(movement) # Perform rotation if rotation == -90: self.robot_pos['heading'] = \ dir_sensors[self.robot_pos['heading']][0] elif rotation == 90: self.robot_pos['heading'] = \ dir_sensors[self.robot_pos['heading']][2] # Advance if movement == -1: self.robot_pos['location'][0] -= \ dir_move[self.robot_pos['heading']][0] self.robot_pos['location'][1] -= \ dir_move[self.robot_pos['heading']][1] else: while movement > 0: self.robot_pos['location'][0] += \ dir_move[self.robot_pos['heading']][0] self.robot_pos['location'][1] += \ dir_move[self.robot_pos['heading']][1] movement -= 1 def number_of_walls(self, sensors): number_of_walls = 0 for sensor in sensors: if sensor == 0: number_of_walls += 1 return number_of_walls # -------------------------------------------- # MOVEMENT-RELATED # -------------------------------------------- def get_next_move(self, x, y, heading, sensors): if self.reached_destination and self.exploring: rotation, movement = self.explore(x, y, heading, sensors) self.steps_exploring += 1 elif not self.reached_destination and not self.exploring: if self.algorithm.name == 'flood-fill' and self.is_at_a_dead_end(sensors): rotation, movement = self.deal_with_dead_end(x, y, heading) else: adj_distances, adj_visited = self.terrain.get_adj_info( x, y, heading, sensors) valid_index = self.algorithm.get_valid_index(adj_distances, adj_visited) rotation, movement = self.convert_from_index(valid_index) self.steps_first_round += 1 else: # Final round (optimized movements) if self.steps_final_round == 0: print('******* FINAL REPORT *******') self.terrain.draw() rotation, movement = self.final_round(x, y, heading, sensors) self.steps_final_round += 1 return rotation, movement def get_valid_index(self, x, y, heading, sensors, exploring): if not exploring: # 1) Get adjacent distances from sensors adj_distances, adj_visited = self.terrain.get_adj_info( x, y, heading, sensors) # Get min index (guaranteed to not be a wall) valid_index = adj_distances.index(min(adj_distances)) # Prefer unvisited cells possible_distance = WALL_VALUE best_index = WALL_VALUE for i, dist in enumerate(adj_distances): if dist != WALL_VALUE and adj_visited[i] is '': if dist <= possible_distance: best_index = i if best_index == 1: break if best_index != WALL_VALUE: valid_index = best_index else: # 1) Get adjacent distances from sensors adj_distances, adj_visited = self.terrain.get_adj_info( x, y, heading, sensors) # Convert WALL_VALUES to -1 (robot will follow max distance) adj_distances = [-1 if dist == WALL_VALUE else dist for dist in adj_distances] # Get max index (guaranteed to not be a wall) valid_index = None # Prefer cells that have not been visited for i, dist in enumerate(adj_distances): if dist != -1 and adj_visited[i] is '': self.consecutive_explored_cells = 0 valid_index = i break if valid_index is None: self.consecutive_explored_cells += 1 possible_candidate = None for i, dist in enumerate(adj_distances): if dist != -1 and adj_visited[i] is '*': if possible_candidate is None: possible_candidate = i else: a = adj_distances[possible_candidate] b = adj_distances[i] if b > a: possible_candidate = i valid_index = possible_candidate if valid_index is None: possible_candidate = None for i, dist in enumerate(adj_distances): if dist != -1 and adj_visited[i] is 'e': if possible_candidate is None: possible_candidate = i else: a = adj_distances[possible_candidate] b = adj_distances[i] if b > a: possible_candidate = i valid_index = possible_candidate return valid_index def explore(self, x, y, heading, sensors): if self.should_end_exploring(x, y) or not self.explore_after_center: rotation = 'Reset' movement = 'Reset' self.exploring = False self.terrain.set_imaginary_walls_for_unvisited_cells() self.terrain.update_distances(last_update=True) else: # If we reach a dead end: if self.is_at_a_dead_end(sensors): rotation, movement = self.deal_with_dead_end(x, y, heading) else: valid_index = self.get_valid_index(x, y, heading, sensors, True) if valid_index is not None: rotation, movement = self.convert_from_index(valid_index) else: rotation, movement = 'Reset', 'Reset' return rotation, movement def convert_from_index(self, index): # Move Left if index == 0: rotation = -90 movement = 1 # Move Up elif index == 1: rotation = 0 movement = 1 # Move Right elif index == 2: rotation = 90 movement = 1 # Minimum distance is behind, so just rotate clockwise else: rotation = 90 movement = 0 return rotation, movement def deal_with_dead_end(self, x, y, heading): # 1) Move back one step rotation = 0 movement = -1 # 2) Get reference to cell cell = self.terrain.grid[x][y] # 3) Place imaginary wall behind the robot before exiting location reverse_direction = dir_reverse[heading] index = self.terrain.get_index_of_wall(reverse_direction) cell.imaginary_walls[index] = 1 # 4) Change the value of visited to signify dead end cell.visited = 'x' cell.distance = WALL_VALUE # 5) Update imaginary walls and distances self.terrain.update_imaginary_walls(x, y, cell.imaginary_walls) return rotation, movement def should_end_exploring(self, x, y): """ The robot should end exploring in all these cases: - It has already explored more than 90% of the cells - It has already taken 30 steps (in the exploration phase) - It has reached the center of the maze (again) - It has reached the starting location (again) """ # Check for % of cells covered if self.terrain.get_percentage_of_maze_explored() > 80: return True if self.consecutive_explored_cells >= 3: return True # Check for number of steps if self.steps_exploring > 15: return True # Check for center of the maze: if self.is_at_center_of_the_maze(x, y): return True if self.is_at_starting_position(x, y): return True return False def final_round(self, x, y, heading, sensors): """ Returns the correct rotation and maximum numbers of steps that the robot can take to optimize the score while staying on track """ rotation = None movement = None current_distance = self.terrain.grid[x][y].distance adj_distances, adj_visited = \ self.terrain.get_adj_info( x, y, heading, sensors, False) # Change sensor info to max allowed moves, when it applies sensors = [3 if step > 3 else step for step in sensors] for i, steps in enumerate(sensors): # If we found a movement, exit and apply it if movement is not None: break # Otherwise, iterate through steps to see if one matches with the # next correct and logical distance for that number of steps elif self.is_a_possible_move(adj_distances, adj_visited, i): for idx in range(steps): step = steps - idx rotation = rotations[str(i)] new_direction = self.get_new_direction(rotation) furthest_distance = self.terrain.get_distance( x, y, new_direction, step) if furthest_distance == current_distance - step: movement = step break return rotation, movement def is_a_possible_move(self, adj_distances, adj_visited, i): """ Distances are valid if they are not walls, unvisited, or dead ends. """ return (adj_visited[i] is not '' and adj_visited[i] is not 'x' and adj_distances[i] is not WALL_VALUE) def report_results(self): distance = self.terrain.grid[0][0].distance percentage = self.terrain.get_percentage_of_maze_explored() first_round = self.steps_first_round + self.steps_exploring final_round = self.steps_final_round print('ALGORITHM USED: {}'.format(self.algorithm.name.upper())) print('EXPLORING AFTER CENTER: {}'.format(self.explore_after_center)) print('NUMBER OF MOVES FIRST ROUND: {}'.format(first_round)) print('PERCENTAGE OF MAZE EXPLORED: {}%'.format(percentage)) print('DISTANCE TO CENTER: {}'.format(distance)) print('NUMBER OF MOVES FINAL ROUND: {}'.format(final_round)) print('********************************')