# -*- coding: utf-8 -*- """ Created on Sat Oct 24 14:54:47 2020 @author: hbartos whogans1 """ #our program solves the maze using a right wall follow. #The robots goal is to spot a purple ball. Once the ball is found, #the robot will print a map representing the path it has taken through the maze, and celebrate import sys, os import numpy as np from ursim import RoboSimApp, ctrl import maze from matplotlib import colors import matplotlib.pyplot as plt DEG = np.pi/180 def solve_maze(m, x0, y0, dir0, x1, y1): maze_commands= 'wallcheck1' return maze_commands ###################################################################### class MazeController(ctrl.Controller): def __init__(self, commands): self.commands = commands ################################################## def initialize(self, time, odom_pose): self.command_index = 0 if self.commands: init_state = self.commands else: init_state = 'done' #self.movenum stores the current move that the robot is on, ex: 1st 2nd 3rd etc self.movenum=1 #self.map is a 2d array, where each square represents a square of the maze. If a square is zero, it has not been visited. #if a square stores a number, that number is the move number at which the robot most recently visited that square. self.map= np.full((20,20 ), 0) self.map[5][5]=1 self.reset_state(time, odom_pose, init_state, True,False) ################################################## def reset_state(self, time, odom_pose, state, reset_time,turncompleted): #Each wallcheck state includes a turn which may or may not be called. #to keep track of whether that robot has completed its turn, we use a variable called self.turny self.turny=turncompleted self.state = state self.start_pose = odom_pose if reset_time: self.start_time = time self.labeled_state = state ################################################## def points_at_angles(self, scan, desired_angles, cutoff=3*DEG): if scan is None or scan.angles is None or scan.ranges is None: return None # get indices of valid ranges ok_idx = np.nonzero(~np.isnan(scan.ranges))[0] if not len(ok_idx): return None valid_angles = scan.angles[ok_idx] valid_ranges = scan.ranges[ok_idx] points = [] for angle in desired_angles: angle_err = np.abs(angle - valid_angles) min_idx = angle_err.argmin() min_err = angle_err[min_idx] if min_err > cutoff: return None true_angle = valid_angles[min_idx] point = np.array([ np.cos(true_angle), np.sin(true_angle) ]) * valid_ranges[min_idx] points.append(point) return np.array(points) ################################################## def turn(self, turn_angle): command = ctrl.ControllerOutput(forward_vel=0, angular_vel=0.75*turn_angle+0.05*np.sign(turn_angle)) is_finished = abs(turn_angle) < 0.002 return command, is_finished ################################################## def drive(self, dist): command = ctrl.ControllerOutput(forward_vel=dist + 0.005*np.sign(dist), angular_vel=0) is_finished = abs(dist) < 0.01 return command, is_finished ################################################## def straighten(self, scan): points = self.points_at_angles(scan, [2*DEG, -2*DEG]) if points is None: return None, False else: delta = points[1] - points[0] turn_angle = np.arctan2(delta[0], -delta[1]) return self.turn(turn_angle) ################################################## def nudge(self, scan): points = self.points_at_angles(scan, [0]) if points is None: return None, False else: dist_to_wall = points[0][0] rounded_dist = np.floor(dist_to_wall) + 0.5 dist_to_drive = dist_to_wall - rounded_dist return self.drive(dist_to_drive) #calculates distance to wall def walle(self, scan): points = self.points_at_angles(scan, [0]) if points is None: return -1 else: dist_to_wall = points[0][0] return dist_to_wall #this method checks to see the balls location in the robot frame, if the ball is visible def ball(self,camera_data): dball = camera_data.detections['purple_ball'] if len(dball): blob = dball[0] return blob return -1 #this method displays the squares which the robot has travelled. # Visited squares appear in various shades of blue.After the first 14 visited squares, all visited squares are in thistle #display prints the array which shows the number of moves, as well as two graphs to show the path the robot took #one path draws a line to show the path of the robot, the other shades squares to show where the robot was def display(self): print('here is your map! Each number represents the most recent move number at which a square was visited') #matrix plot print(np.matrix(self.map)) #first plot is color coded from blue to black, with blue being the earliest move and black being the latest fig, ax = plt.subplots(1,1, tight_layout=True) county=0 gradient=[(1,1,1)] for i in range(self.movenum): county=county+(1/self.movenum) gradient.append((0.0,0.0,1-county)) cmap=colors.ListedColormap(gradient) count=1 bounds=[-1,.9] for i in range(self.movenum): count=count+1 bounds.append(.9+count) norm=colors.BoundaryNorm(bounds,cmap.N) ax.imshow(self.map,cmap=cmap,norm=norm) #second plot is a line which shows the path the robot traveled xy = np.full((self.movenum, 2), 0) for i in range(19): for j in range (19): if self.map[i][j] != 0: order = self.map[i][j] xy[order-1][0] = i xy[order-1][1] = j fig, ax = plt.subplots() for start, stop in zip(xy[:-1], xy[1:]): x, y = zip(start, stop) ax.plot(x, y, color=plt.cm.gist_ncar(np.random.random())) plt.show() return True ################################################## #the update method looks for a right-side wall and follows that wall using several states ################################################## def update(self, time, dt, robot_state, camera_data): #first, if the ball is seen, the robot locates the ball # and orients itself in front of it. Once it reaches the ball, it displays the traversal map of the maze, and celebrates scan = camera_data.scan cmd_vel=ctrl.ControllerOutput(0,0) if self.state == 'done': return None dist_to_ball=-1 blob=self.ball(camera_data) if(blob!=-1): dist_to_ball= ((blob.xyz_mean[1])**2+ (blob.xyz_mean[0])**2)**.5 #print(self.walle(scan)) if (dist_to_ball<.5 and dist_to_ball >0): if (self.state != 'display_stat'): if (self.state != 'celebration'): self.reset_state(time,robot_state.odom_pose, 'display_stat',False,False) else: self.reset_state(time,robot_state.odom_pose, 'celebration',False,False) if(self.state=='forward'): if(dist_to_ball>=.5 and (dist_to_ball<1)): self.reset_state(time,robot_state.odom_pose, 'findball',False,False) if((1=1) and (self.turnstarted==False)): self.reset_state(time, robot_state.odom_pose, 'forward', False, False) elif (self.walle(scan)<1): self.turnstarted=True #if there is a wall, turn left to face the direction which was forward during wallcheck1 if(self.turnstarted==True): desired_angle = np.pi / 2 turn_angle = desired_angle - T_init_from_cur.angle cmd_vel, done = self.turn(turn_angle) if abs(turn_angle) <= 2*DEG: self.reset_state(time, robot_state.odom_pose, 'wallcheck2', False, True) #when done turning, enter wallcheck3 if(self.turny==True): cmd_vel, done1 = self.straighten(scan) if(done1): self.turnstarted=False self.reset_state(time, robot_state.odom_pose, 'wallcheck3', False, False) #In the wallcheck3 state, the robot has turned so that is facing its original forward direction # if there is no wall in front of it, the robot should go forwards. Otherwise, the robot must turn left, and re-enter wallcheck1 if self.state== 'wallcheck3': #print('wallcheck3') #if there is no wall, go forward if(self.turny==False): #if(self.walle(scan)== -1): #self.reset_state(time, robot_state.odom_pose, 'forward', False, False) if ((self.walle(scan)>1) and (self.turnstarted==False)): self.reset_state(time, robot_state.odom_pose, 'forward', False, False) #otherwise, turn left elif (self.walle(scan)<1): self.turnstarted=True if(self.turnstarted==True): desired_angle = np.pi / 2 turn_angle = desired_angle - T_init_from_cur.angle cmd_vel, done = self.turn(turn_angle) if abs(turn_angle) <= 2*DEG: self.reset_state(time, robot_state.odom_pose, 'wallcheck3', False, True) #when done turning left, enter wallcheck1 if(self.turny==True): cmd_vel, done1 = self.straighten(scan) if(done1): self.reset_state(time, robot_state.odom_pose, 'wallcheck1', False,False) #correctly aligns the robot to the center of a square elif self.state == 'nudge': cmd_vel, done = self.nudge(scan) if (done): self.reset_state(time, robot_state.odom_pose, 'wallcheck1', False,False) #forward goes forward, saves its location, and transitions to nudge elif self.state == 'forward': dist_to_drive = 0.9 - np.sqrt(T_init_from_cur.position[0]**2 + T_init_from_cur.position[1]**2) cmd_vel, done = self.drive(dist_to_drive) #every time the robot finishes a move forward, it records its updated location in self.map if done: T=robot_state.odom_pose posy=T.position self.movenum=self.movenum + 1 x=int(np.round(posy[0]))+5 y=int(np.round(posy[1]))+5 self.map[x][y]=self.movenum self.reset_state(time, robot_state.odom_pose, 'nudge', False,False) #when the robot has successfully completed the maze and found the ball, it celebrates by spinning in circles elif self.state=='celebration': cmd_vel=ctrl.ControllerOutput(0,10) done=False #displays the map of the robots visited locations while solving the maze elif self.state=='display_stat': if self.display(): done=True self.reset_state(time, robot_state.odom_pose, 'display_stat', False,False) """elif self.state=='display_stat2': if self.other_plot(): done=True self.reset_state(time, robot_state.odom_pose, 'display_stat2', False,False) """ return cmd_vel ###################################################################### def parse_command_line(): try: command_type = sys.argv[1] mazefile = sys.argv[2] x0 = int(sys.argv[3]) y0 = int(sys.argv[4]) dir0 = maze.DIR_STRING_LOOKUP[sys.argv[5]] if not os.path.exists(mazefile): mazefile = os.path.join(os.path.dirname(__file__), 'maps', mazefile) print('mazefile:', mazefile) assert command_type in ('practice', 'solve') if command_type == 'practice': commands = sys.argv[6:] else: x1 = int(sys.argv[6]) y1 = int(sys.argv[7]) except: name = os.path.basename(sys.argv[0]) print(('usage: python {} practice MAZEFILE X0 Y0 DIRECTION CMD1 [CMD2 ...]\n' ' or: python {} solve MAZEFILE X0 Y0 DIRECTION X1 Y1').format(name, name)) print() raise m = maze.Maze(mazefile) assert m.is_solvable() assert x0 >= 0 and x0 < m.width() assert y0 >= 0 and y0 < m.height() if command_type == 'solve': assert x1 >= 0 and x1 < m.width() assert y1 >= 0 and y1 < m.height() commands = solve_maze(m, x0, y0, dir0, x1, y1) else: x1 = None y1 = None return m, x0, y0, dir0, x1, y1, commands ###################################################################### def make_wall(sim, x0, y0, x1, y1): p0 = np.array([x0, y0], dtype=float) p1 = np.array([x1, y1], dtype=float) pmid = 0.5*(p0 + p1) diff = p1 - p0 dist = np.linalg.norm(diff) offset = 0.5*diff - 0.01*diff/dist angle = (np.random.random()*2-1)*1*DEG c = np.cos(angle) s = np.sin(angle) R = np.array([ [c, -s], [s, c]]) offset = np.dot(R, offset) pmid += 1 p0 = pmid - offset p1 = pmid + offset sim.add_wall(p0, p1) ###################################################################### def setup_maze(sim, m, x0, y0, dir0, x1, y1): w = m.width() h = m.height() sim.set_dims(w+2, h+2) # do outer walls for x in range(w): make_wall(sim, x, 0, x+1, 0) make_wall(sim, x, h, x+1, h) for y in range(h): make_wall(sim, 0, y, 0, y+1) make_wall(sim, w, y, w, y+1) for x in range(w): for y in range(h): if x+1 < w and not m.can_move(x, y, maze.DIR_RIGHT): make_wall(sim, x+1, y, x+1, y+1) if y+1 < h and not m.can_move(x, y, maze.DIR_UP): make_wall(sim, x, y+1, x+1, y+1) init_pos = np.array([x0 + 1.5, y0 + 1.5]) init_angle = maze.DIR_HEADINGS[dir0] angle_offset = (np.random.random())*5*DEG + 5*DEG if np.random.randint(2): angle_offset = -angle_offset init_pos += (np.random.random(2)*2-1)*0.10 init_angle += angle_offset if x1 is not None and y1 is not None: theta = np.linspace(90, (360+90), 11)*DEG ct = np.cos(theta) st = np.sin(theta) r = np.where(np.arange(len(theta)) % 2, 0.125, 0.3) points = np.array([ r*ct, r*st ]).transpose() points += [x1 + 1.5, y1 + 1.5] sim.add_ball([x1+1.5,y1+1.5]) for i in range(len(points)-1): sim.add_tape_strip(points[i:i+2], 'blue') sim.initialize_robot(init_pos, init_angle) ###################################################################### def main(): m, x0, y0, dir0, x1, y1, commands = parse_command_line() controller = MazeController(commands) app = RoboSimApp(controller) setup_maze(app.sim, m, x0, y0, dir0, x1, y1) app.run() if __name__ == '__main__': main()