# first line formation simulation of swarm robots, using climbing method to form the line12# comments for research purpose:3# My previous line formation simulations in ROS utilize the relative positions of nearby4# neighbors, and linear fit a line and move to the desired position. It only works when5# there are many robots within range or robot has a large sensing range, and preferrably6# the swarm is aggregated. No communication is required though.7# In this simulation, I extended the initial situation such that robots are in random8# positions in a bounded area. They can move around and communicate with robot in range.9# If not in a bounded area, they might just run too far away without knowing it. All10# communications are single hops, each robot only gives the other robot information about11# itself, not information it gets from a third robot. That means all information is first12# hand. The idea is that, start initial wondering, each robot is trying to form the first13# line segment, or find a line it can contribute to, depends on which comes first. There14# will be more than one line being formed. To control which line should survice, each line15# is given a time of life, it decreases by time and increases when a new member join the16# line. The line disassembles when it runs out of life time, and never expire if the number17# of members exceed half of the total number of robots in the simulation, and becomes the18# dominant group. Another way a line disassembles itself is, when a line detects the exist19# of another line, the line with less members will be disassembled.2021# comments for programming purpose:22# Four statuses have been designed to serve different stages of the robot in the simulation:23# '0': free, wondering around, not in group, available to form a group or join one24# '1': forming an initial line segment, or found a line and trying to climbing to ends25# '1_0': initial an line segment, '0' found another '0' and formed a group26# '1_1': join a line, climbing to the closer end27# '2': already in a line, remain in position28# '-1': free, wondering around, no interaction with nearby robots29# Only allowed status transitions are:30# forward: '-1'->'0', '0'->'1', '1'->'2'31# backward: '1'->'-1', '2'->'-1'32# Purpose of '-1' is, when a line is disassembled, if '1' and '2' becomes '0', they will33# immediately form the old line because they are already in position. So '-1' which has no34# interaction with nearly robots, acts as a transition to status '0'.3536# line formation runs out of boundary:37# It's possible that a line is trying to continue out of the boundary. This is no good way38# to solve it, if keeping the proposed control algorithm. But reducing the occurrence can39# be done, by enabling a relatively large wall detection range. Robot will be running away40# before it really hits the wall, so it's like smaller virtual boundaries inside the window.41# But this virtual boundaries only work on robot '0', such that robot '1' or '2' wants to42# form a line out of virtual boundaries, it can.434445import pygame46import math, random47from line_formation_1_robot import LFRobot48from formation_functions import *4950pygame.init() # initialize pygame5152# for display, window origin is at left up corner53screen_size = (1200, 1000) # width and height54background_color = (0, 0, 0) # black background55robot_0_color = (0, 255, 0) # for robot status '0', green56robot_1_color = (255, 153, 153) # for robot status '1', pink57robot_2_color = (255, 51, 0) # for robot status '2', red58robot_n1_color = (0, 51, 204) # for robot status '-1', blue59robot_size = 5 # robot modeled as dot, number of pixels in radius6061# set up the simulation window and surface object62icon = pygame.image.load('icon_geometry_art.jpg')63pygame.display.set_icon(icon)64screen = pygame.display.set_mode(screen_size)65pygame.display.set_caption('Line Formation 1 Simulation - Climbing')6667# for physics, origin is at left bottom corner, starting (0, 0)68# it's more natural to do the calculation in right hand coordiante system69# the continuous, physical world for the robots70world_size = (100.0, 100.0 * screen_size[1]/screen_size[0])7172# variables to configure the simulation73robot_quantity = 3074# coefficient to resize the robot distribution, but always keep close to center75distrib_coef = 0.576const_vel = 3.0 # all moving robots are moving at a constant speed77frame_period = 100 # updating period of the simulation and graphics, in ms78comm_range = 5.0 # communication range, the radius79line_space = comm_range * 0.7 # a little more than half the communication range80space_err = line_space * 0.1 # the error to determine the space is good81climb_space = line_space * 0.5 # climbing is half the line space along the line82life_incre = 8 # each new member add these seconds to the life of the group83group_id_upper_limit = 1000 # random integer as group id from 0 to this limit84n1_life_lower = 3 # lower limit of life time of being status '-1'85n1_life_upper = 8 # upper limit of life time of being status '-1'86vbound_gap_ratio = 0.15 # for the virtual boundaries inside the window87# ratio is the gap distance to the world size on that axis8889# instantiate the robot swarm as list90robots = [] # container for all robots, index is also the identification91for i in range(robot_quantity):92# random position, away from the window edges93pos_temp = (((random.random()-0.5)*distrib_coef+0.5) * world_size[0],94((random.random()-0.5)*distrib_coef+0.5) * world_size[1])95vel_temp = const_vel96ori_temp = random.random() * 2*math.pi - math.pi # random in (-pi, pi)97object_temp = LFRobot(pos_temp, vel_temp, ori_temp)98robots.append(object_temp)99# instantiate the group variable as dictionary100groups = {}101# key is the group id, so two groups won't share same id102# value is a list103# 0.first element: the group size, including both status '2' and '1'104# 1.second element: life time remaining105# 2.third element: a list of robots on the line in adjacent order, status '2'106# 3.forth element: a list of robots off the line, not in order, status '1'107# 4.fifth element: true or false, being the dominant group108109# instantiate a distance table for every pair of robots110# will calculate once for symmetric data111dist_table = [[-1.0 for j in range(robot_quantity)] for i in range(robot_quantity)]112113# calculate the corner positions of virtual boundaries, for display114# left bottom corner115pos_lb = world_to_display([world_size[0]*vbound_gap_ratio,116world_size[1]*vbound_gap_ratio], world_size, screen_size)117# right bottom corner118pos_rb = world_to_display([world_size[0]*(1-vbound_gap_ratio),119world_size[1]*vbound_gap_ratio], world_size, screen_size)120# right top corner121pos_rt = world_to_display([world_size[0]*(1-vbound_gap_ratio),122world_size[1]*(1-vbound_gap_ratio)], world_size, screen_size)123# left top corner124pos_lt = world_to_display([world_size[0]*vbound_gap_ratio,125world_size[1]*(1-vbound_gap_ratio)], world_size, screen_size)126127# the loop128sim_exit = False # simulation exit flag129sim_pause = False # simulation pause flag, pause at begining130timer_last = pygame.time.get_ticks() # return number of milliseconds after pygame.init()131timer_now = timer_last # initialize it with timer_last132while not sim_exit:133# exit the program134for event in pygame.event.get():135if event.type == pygame.QUIT:136sim_exit = True # exit with the close window button137if event.type == pygame.KEYUP:138if event.key == pygame.K_SPACE:139sim_pause = not sim_pause # reverse the pause flag140if (event.key == pygame.K_ESCAPE) or (event.key == pygame.K_q):141sim_exit = True # exit with ESC key or Q key142143# skip the rest of the loop if paused144if sim_pause: continue145146# update the physics, control rules and graphics at the same time147timer_now = pygame.time.get_ticks()148if (timer_now - timer_last) > frame_period:149timer_last = timer_now # reset timer150# prepare the distance data for every pair of robots151for i in range(robot_quantity):152for j in range(i+1, robot_quantity):153# status of '-1' does not involve in any connection, so skip154if (robots[i].status == -1) or (robots[j].status == -1):155dist_table[i][j] = -1.0156dist_table[j][i] = -1.0157continue # skip the rest158# it only covers the upper triangle without the diagonal159vect_temp = (robots[i].pos[0]-robots[j].pos[0],160robots[i].pos[1]-robots[j].pos[1])161dist_temp = math.sqrt(vect_temp[0]*vect_temp[0] +162vect_temp[1]*vect_temp[1])163if dist_temp <= comm_range:164# only record distance smaller than communication range165dist_table[i][j] = dist_temp166dist_table[j][i] = dist_temp167else: # ignore the neighbors too far away168dist_table[i][j] = -1.0169dist_table[j][i] = -1.0170# sort the distance in another table, record the index here171index_list = [[] for i in range(robot_quantity)] # index of neighbors in range172# find all robots with non-zero distance173for i in range(robot_quantity):174for j in range(robot_quantity):175if j == i: continue # skip the self, not necessary176if dist_table[i][j] > 0: index_list[i].append(j)177# sort the index_list in the order of increasing distance178for i in range(robot_quantity):179len_temp = len(index_list[i]) # number of neighbors180if (len_temp < 2): continue # no need to sort181else:182# bubble sort183for j in range(len_temp-1):184for k in range(len_temp-1-j):185if (dist_table[i][index_list[i][k]] >186dist_table[i][index_list[i][k+1]]):187# swap the position of the two index188index_temp = index_list[i][k]189index_list[i][k] = index_list[i][k+1]190index_list[i][k+1] = index_temp191# get the status list corresponds to the sorted index_list192status_list = [[] for i in range(robot_quantity)]193for i in range(robot_quantity):194for j in index_list[i]:195status_list[i].append(robots[j].status)196197# instantiate the status transition variables, prepare for status check198# the priority to process them is in the following order199s_grab_on = {} # robot '0' grabs on robot '2', becoming '1'200# key is id of robot '0', value is id of robot '2'201s_init_form = {} # robot '0' initial forms with another '0', becoming '1'202# key is id of robot '0' that discovers other robot '0' in range203# value is a list of robot '0's that are in range, in order of increasing distance204s_form_done = {} # robot '1' finishes initial forming, becoming '2'205# key is group id206# value is a list of id of robot '1's that have finished initial forming207s_climb_done = {} # robot '1' finishes climbing, becoming '2'208# key is group id209# value is a list of id of robot '1's that have finished climbing210s_form_lost = [] # robot '1' gets lost during initial forming211# list of group id for the initial forming robots212s_climb_lost = [] # robot '1' gets lost during climbing213# list of robot id for the climbing robots214s_group_exp = [] # life time of a group naturally expires215# life of group id216s_disassemble = [] # disassemble triggerred by robot '1' or '2'217# list of lists of group id to be compared for disassembling218s_back_0 = [] # robot '-1' gets back to '0'219# list of robot id220221# check 'robots' for any status change, and schedule and process in next step222for i in range(robot_quantity):223# for the host robot having status of '0'224if robots[i].status == 0:225# check if this robot has valid neighbors at all226if len(index_list[i]) == 0: continue; # skip the neighbor check227# process neighbors with stauts '2', highest priority228if 2 in status_list[i]:229# check the group attribution of all the '2'230# get the robot id and group id of the first '2'231current_index = status_list[i].index(2)232current_robot = index_list[i][current_index]233current_group = robots[current_robot].group_id234groups_temp = {current_group:[current_robot]}235# check if there is still '2' in the list236while 2 in status_list[i][current_index+1:]:237# indexing the next '2' from current_index+1238# update the current_index, current_robot, current_group239current_index = status_list[i].index(2, current_index+1)240current_robot = index_list[i][current_index]241current_group = robots[current_robot].group_id242# update groups_temp243if current_group in groups_temp.keys():244# append this robot in the existing group245groups_temp[current_group].append(current_robot)246else:247# add new group id in groups_temp and add this robot248groups_temp[current_group] = [current_robot]249# check if there are multiple groups detected from the '2'250target_robot = -1 # the target robot to grab on251if len(groups_temp.keys()) == 1:252# there is only one group detected253dist_min = 2*comm_range # start with a large dist254robot_min = -1 # corresponding robot with min distance255# search the closest '2'256for j in groups_temp.values()[0]:257if dist_table[i][j] < dist_min:258dist_min = dist_table[i][j]259robot_min = j260target_robot = robot_min261else:262# there is more than one group detected263# no need to disassemble any group yet264# compare which group has the most members in it265member_max = 0 # start with 0 number of members266group_max = -1 # corresponding group id with most members267for j in groups_temp.keys():268if len(groups_temp[j]) > member_max:269member_max = len(groups_temp[j])270group_max = j271# search the closeat '2' inside that group272dist_min = 2*comm_range273robot_min = -1274for j in groups_temp[group_max]:275if dist_table[i][j] < dist_min:276dist_min = dist_table[i][j]277robot_min = j278target_robot = robot_min279# target_robot located, prepare to grab on it280# status transition scheduled, '0' to '1'281s_grab_on[i] = target_robot282# process neighbors with status '1', second priority283elif 1 in status_list[i]:284# find the closest '1' and get bounced away by it285# it doesn't matter if the '1's belong to different group286dist_min = 2*comm_range287robot_min = -1288for j in range(len(status_list[i])):289if status_list[i][j] != 1: continue290if dist_table[i][index_list[i][j]] < dist_min:291dist_min = dist_table[i][index_list[i][j]]292robot_min = index_list[i][j]293# target robot located, the robot_min294# get bounced away from this robot, update the moving direction295vect_temp = (robots[i].pos[0] - robots[robot_min].pos[0],296robots[i].pos[1] - robots[robot_min].pos[1])297# orientation is pointing from robot_min to host298robots[i].ori = math.atan2(vect_temp[1], vect_temp[0])299# process neighbors with status '0', least priority300else:301# establish a list of all '0', in order of increasing distance302# to be checked later if grouping is possible303# this list should be only '0's, already sorted304target_list = index_list[i][:]305# status transition scheduled, '0' forming intial group with '0'306s_init_form[i] = target_list307# for the host robot having status of '1'308elif robots[i].status == 1:309# status of '1' needs to checked and maintained constantly310# 1.check if the important group neighbors are still in range311neighbors_secured = True312for j in robots[i].key_neighbors:313if j not in index_list[i]:314neighbors_secured = False315break316if neighbors_secured == False:317# status transition scheduled, robot '1' gets lost, becoming '-1'318if robots[i].status_1_sub == 0:319# append the group id, disassemble the entire group320s_form_lost.append(robots[i].group_id)321else:322s_climb_lost.append(i) # append the robot id323else:324# all the key neighbors are in good position325# 2.dissemble check, get group attribution of all '1' and '2'326status_list_temp = status_list[i][:]327index_list_temp = index_list[i][:]328# pop out the '0' first329while 0 in status_list_temp:330index_temp = status_list_temp.index(0)331status_list_temp.pop(index_temp)332index_list_temp.pop(index_temp)333if len(index_list_temp) > 0: # ensure there are in-group robots around334# start the group attribution dictionary with first robot335groups_temp = {robots[index_list_temp[0]].group_id: [index_list_temp[0]]}336for j in index_list_temp[1:]: # then iterating from the second one337current_group = robots[j].group_id338if current_group in groups_temp.keys():339# append this robot in same group340groups_temp[current_group].append(j)341else:342# add new key in the groups_temp dictionary343groups_temp[current_group] = [j]344# check if there are multiple groups detected345if len(groups_temp.keys()) > 1:346# status transition scheduled, to disassemble groups347s_disassemble.append(groups_temp.keys())348# may produce duplicates in s_disassemble, not big problem349# 3.check if any neighbor transition needs to be done350if robots[i].status_1_sub == 0:351# host robot is in the initial forming phase352# check if the neighbor robot is in appropriate distance353if abs(dist_table[i][robots[i].key_neighbors[0]] -354line_space) < space_err:355# status transition scheduled, finish intial forming, '1' to '2'356g_it = robots[i].group_id357if g_it in s_form_done.keys():358s_form_done[g_it].append(i)359else:360s_form_done[g_it] = [i]361elif robots[i].status_1_sub == 1:362# host robot is in the climbing phase363# check if the grab-on robot is at the begining or end of the line364# if yes, no need to search new key neighbor, only check destination365if (robots[robots[i].key_neighbors[0]].status_2_sequence == 0 or366robots[robots[i].key_neighbors[0]].status_2_end == True):367# check if reaching the destination coordinates368vect_temp = (robots[i].pos[0]-robots[i].status_1_1_des[0],369robots[i].pos[1]-robots[i].status_1_1_des[1])370dist_temp = math.sqrt(vect_temp[0]*vect_temp[0] +371vect_temp[1]*vect_temp[1])372if dist_temp < space_err:373# status transition scheduled, finish climbing, '1' to '2'374g_it = robots[i].group_id375if g_it in s_climb_done.keys():376s_climb_done[g_it].append(i)377else:378s_climb_done[g_it] = [i]379else:380# grab-on robot is not at any end of the line yet, still climbing381# check if new key neighbor appears, if yes, update new key neighbor382# and update the new destination address383it0 = robots[i].key_neighbors[0] # initialize with the old key neighbor384# 'it' stands for index temp385if robots[i].status_1_1_dir == 0:386# assign the new key neighbor387it0 = groups[robots[i].group_id][2][robots[it0].status_2_sequence - 1]388else:389it0 = groups[robots[i].group_id][2][robots[it0].status_2_sequence + 1]390if it0 in index_list[i]:391# update new grab-on robot as the only key neighbor392robots[i].key_neighbors = [it0]393# calculate new destination for the climbing394if robots[it0].status_2_sequence == 0:395# if new grab-on robot is at the begining of the line396it1 = groups[robots[i].group_id][2][1] # second one in the line397# to calculate the new destination, should not just mirror the des398# from 'it1' with 'it0', it is not precise enough, error will accumulate399# robots[i].status_1_1_des = [2*robots[it0].pos[0] - robots[it1].pos[0],400# 2*robots[it0].pos[1] - robots[it1].pos[1]]401# ori_temp is pointing from 'it1' to 'it0'402ori_temp = math.atan2(robots[it0].pos[1]-robots[it1].pos[1],403robots[it0].pos[0]-robots[it1].pos[0])404robots[i].status_1_1_des = [robots[it0].pos[0] + line_space*math.cos(ori_temp),405robots[it0].pos[1] + line_space*math.sin(ori_temp)]406elif robots[it0].status_2_end == True:407# if new grab-on robot is at the end of the line408it1 = groups[robots[i].group_id][2][-2] # second inversely in the line409# robots[i].status_1_1_des = [2*robots[it0].pos[0] - robots[it1].pos[0],410# 2*robots[it0].pos[1] - robots[it1].pos[1]]411ori_temp = math.atan2(robots[it0].pos[1]-robots[it1].pos[1],412robots[it0].pos[0]-robots[it1].pos[0])413robots[i].status_1_1_des = [robots[it0].pos[0] + line_space*math.cos(ori_temp),414robots[it0].pos[1] + line_space*math.sin(ori_temp)]415else: # new grab-on robot is not at any end416it1 = 0 # index of the next promising key neighbor417if robots[i].status_1_1_dir == 0:418it1 = groups[robots[i].group_id][2][robots[it0].status_2_sequence - 1]419else:420it1 = groups[robots[i].group_id][2][robots[it0].status_2_sequence + 1]421# direction from current key neighbor(it0) to next promising key neighbor(it1)422dir_temp = math.atan2((robots[it1].pos[1]-robots[it0].pos[1]),423(robots[it1].pos[0]-robots[it0].pos[0]))424# direction from next promising key neighbor to new destination425if robots[i].status_1_1_side == 0:426# climbing at the left of the line, rotate ccw of pi/2427dir_temp = dir_temp + math.pi/2428else:429dir_temp = dir_temp - math.pi/2 # rotate cw of pi/2430# calculate new destination address431robots[i].status_1_1_des = [robots[it1].pos[0]+climb_space*math.cos(dir_temp),432robots[it1].pos[1]+climb_space*math.sin(dir_temp)]433# for the host robot having status of '2'434elif robots[i].status == 2:435# it's ok to check if key neighbors are still in range436# but key neighbors of '2' are also '2', all of them are static437# so skip this step438# dissemble check, for all the '1' and '2'439status_list_temp = status_list[i][:]440index_list_temp = index_list[i][:]441# pop out the '0' first442while 0 in status_list_temp:443index_temp = status_list_temp.index(0)444status_list_temp.pop(index_temp)445index_list_temp.pop(index_temp)446# start the group attribution dictionary with first robot447if len(index_list_temp) > 0: # ensure there are in-group robots around448groups_temp = {robots[index_list_temp[0]].group_id: [index_list_temp[0]]}449for j in index_list_temp[1:]: # then iterating from the second one450current_group = robots[j].group_id451if current_group in groups_temp:452groups_temp[current_group].append(j)453else:454groups_temp[current_group] = [j]455# check if there are multiple groups detected456if len(groups_temp.keys()) > 1:457# status transition scheduled, to disassemble groups458s_disassemble.append(groups_temp.keys())459# for the host robot having status of '-1'460elif robots[i].status == -1:461# check if life time expires, and get status back to '0'462if robots[i].status_n1_life < 0:463s_back_0.append(i)464465# check 'groups' for any status change466for g_it in groups.keys():467if groups[g_it][4]: continue # already being dominant468if groups[g_it][0] > robot_quantity/2:469# the group has more than half the total number of robots470groups[g_it][4] = True # becoming dominant471groups[g_it][1] = 100.0 # a random large number472if groups[g_it][1] < 0: # life time of a group expires473# schedule operation to disassemble this group474s_group_exp.append(g_it)475476# process the scheduled status change, in the order of the priority477# 1.s_grab_on, robot '0' grabs on robot '2', becoming '1'478for i in s_grab_on.keys():479# 1.update the 'robots' variable480robots[i].status = 1 # status becoming '1'481it0 = s_grab_on[i] # for the robot being grabed on, new key neighbor482# ie., robot 'i' grabs on robot 'it0'483robots[i].key_neighbors = [it0] # update the key neighbor484g_it = robots[it0].group_id # for the new group id, group index temp485robots[i].group_id = g_it # update group id486robots[i].status_1_sub = 1 # sub status '1' for climbing487# 2.update the 'groups' variable488groups[g_it][0] = groups[g_it][0] + 1 # increase group size by 1489groups[g_it][1] = groups[g_it][1] + life_incre # increase life time490groups[g_it][3].append(i)491# 3.deciding the climbing direction492if robots[it0].status_2_sequence > (len(groups[g_it][2])-1)/2:493robots[i].status_1_1_dir = 1494else:495robots[i].status_1_1_dir = 0496# 4.deciding which side the robot is climbing at497if robots[i].status_1_1_dir == 0: # search backward for the next robot498it1 = groups[g_it][2][robots[it0].status_2_sequence + 1]499else:500it1 = groups[g_it][2][robots[it0].status_2_sequence - 1]501# by calculating the determinant of vectors 'it1->i' and 'it1->it0'502if ((robots[it0].pos[0]-robots[it1].pos[0])*(robots[i].pos[1]-robots[it1].pos[1]) -503(robots[it0].pos[1]-robots[it1].pos[1])*(robots[i].pos[0]-robots[it1].pos[0])) >= 0:504# it's rare that the above determinant should equal to 0505robots[i].status_1_1_side = 0 # at left side506else:507robots[i].status_1_1_side = 1508# 5.calculate the initial destination509if robots[it0].status_2_sequence == 0:510it1 = groups[g_it][2][1] # adjacent robot id511robots[i].status_1_1_des = [2*robots[it0].pos[0] - robots[it1].pos[0],5122*robots[it0].pos[1] - robots[it1].pos[1]]513elif robots[it0].status_2_end == True:514it1 = groups[g_it][2][-2] # adjacent robot id515robots[i].status_1_1_des = [2*robots[it0].pos[0] - robots[it1].pos[0],5162*robots[it0].pos[1] - robots[it1].pos[1]]517else:518# robot it0 is not at the begining or end of the line519it1 = 0520if robots[i].status_1_1_dir == 0:521it1 = groups[robots[i].group_id][2][robots[it0].status_2_sequence - 1]522else:523it1 = groups[robots[i].group_id][2][robots[it0].status_2_sequence + 1]524# direction from current key neighbor(it0) to next promising key neighbor(it1)525dir_temp = math.atan2((robots[it1].pos[1]-robots[it0].pos[1]),526(robots[it1].pos[0]-robots[it0].pos[0]))527# direction from next promising key neighbor to new destination528if robots[i].status_1_1_side == 0:529# climbing at the left of the line, rotate ccw of pi/2530dir_temp = dir_temp + math.pi/2531else:532dir_temp = dir_temp - math.pi/2 # rotate cw of pi/2533# calculate new destination address534robots[i].status_1_1_des = [robots[it1].pos[0]+climb_space*math.cos(dir_temp),535robots[it1].pos[1]+climb_space*math.sin(dir_temp)]536# update the moving orientation537robots[i].ori = math.atan2(robots[i].status_1_1_des[1]-robots[i].pos[1],538robots[i].status_1_1_des[0]-robots[i].pos[0])539# 2.s_init_form, robot '0' initial forms with another '0', becoming '1'540s_pair = [] # the container for finalized initial forming pairs541while len(s_init_form.keys()) != 0: # there are still robots to be processed542for i in s_init_form.keys():543it = s_init_form[i][0] # index temp544if it in s_init_form.keys():545if s_init_form[it][0] == i: # robot 'it' also recognizes 'i' as closest546s_pair.append([i, it])547s_init_form.pop(i) # pop out both 'i' and 'it'548s_init_form.pop(it)549break550elif i not in s_init_form[it]:551# usually should not be here unless robots have different sensing range552s_init_form[i].remove(it)553if len(s_init_form[i]) == 0:554s_init_form.pop(i)555break556# have not consider the situation that 'i' in 'it' but not first one557else:558# will be here if robot 'it' chooses to be bounced away by '1', or grab on '2'559s_init_form[i].remove(it)560if len(s_init_form[i]) == 0:561s_init_form.pop(i)562break563# process the finalized pairs564for pair in s_pair:565it0 = pair[0] # get indices of the pair566it1 = pair[1]567g_it = random.randint(0, group_id_upper_limit)568while g_it in groups.keys():569g_it = random.randint(0, group_id_upper_limit) # generate again570# update the 'robots' variable571robots[it0].status = 1572robots[it1].status = 1573robots[it0].group_id = g_it574robots[it1].group_id = g_it575robots[it0].status_1_sub = 0 # sub status '0' for initial climbing576robots[it1].status_1_sub = 0577robots[it0].key_neighbors = [it1] # name the other as the key neighbor578robots[it1].key_neighbors = [it0]579# update the 'groups' variable580groups[g_it] = [2, 2*life_incre, [], [it0, it1], False] # add new entry581# deciding moving direction for the initial forming robots582vect_temp = (robots[it1].pos[0]-robots[it0].pos[0],583robots[it1].pos[1]-robots[it0].pos[1]) # pointing from it0 to it1584ori_temp = math.atan2(vect_temp[1], vect_temp[0])585if dist_table[it0][it1] > line_space: # equally dist_table[it1][it0]586# attracting each other, most likely this happens587robots[it0].ori = ori_temp588robots[it1].ori = reset_radian(ori_temp + math.pi)589else:590# repelling each other591robots[it0].ori = reset_radian(ori_temp + math.pi)592robots[it1].ori = ori_temp593# no need to check if they are already within the space error of line space594# this will be done in the next round of status change check595# 3.s_form_done, robot '1' finishes intial forming, becoming '2'596for g_it in s_form_done.keys():597if len(s_form_done[g_it]) == 2: # double check, both robots agree forming is done598it0 = s_form_done[g_it][0]599it1 = s_form_done[g_it][1]600# update 'robots' variable for 'it0' and 'it1'601robots[it0].status = 2602robots[it1].status = 2603# randomly deciding which is begining and end of the line, random choice604if random.random() > 0.5: # half chance605# swap 'ito' and 'it1', 'it0' as begining, 'it1' as end606temp = it0607it0 = it1608it1 = temp609# update the 'robots' variable610robots[it0].status_2_sequence = 0611robots[it1].status_2_sequence = 1612robots[it0].status_2_end = False613robots[it1].status_2_end = True614robots[it0].key_neighbors = [it1] # can skip this615robots[it1].key_neighbors = [it0]616# update the 'groups' variable617g_it = robots[it0].group_id618groups[g_it][2] = [it0, it1] # add the robots for the line619groups[g_it][3] = [] # empty the forming & climbing pool620# 4.s_climb_done, robot '1' finishes climbing, becoming '2'621for g_it in s_climb_done.keys():622start_pool = [] # for robots finish at the begining623end_pool = [] # for robots finish at the end624for i in s_climb_done[g_it]:625if robots[i].status_1_1_dir == 0:626start_pool.append(i)627else:628end_pool.append(i)629if len(start_pool) > 0: # start pool not empty630# most likely there is only one robot in the pool631# randomly choose one just in case, may not be the best way632it0 = random.choice(start_pool)633# no need to care the not selected, they will continue climbing634# upate the 'robots' variable635robots[it0].status = 2636robots[it0].status_2_sequence = 0 # make it begining of the line637robots[it0].status_2_end = False638robots[it0].key_neighbors = [groups[g_it][2][0]]639robots[groups[g_it][2][0]].key_neighbors.append(it0) # add a new key neighbor640# update the 'groups' variable641groups[g_it][3].remove(it0) # remove from the climbing pool642groups[g_it][2].insert(0, it0) # insert new index at the begining643# increase the status_2_sequence of other robots on the line by 1644for i in groups[g_it][2][1:]:645robots[i].status_2_sequence = robots[i].status_2_sequence + 1646if len(end_pool) > 0: # end pool not empty647it0 = random.choice(end_pool)648# update the 'robots' variable649robots[it0].status = 2650robots[it0].status_2_sequence = len(groups[g_it][2])651robots[it0].status_2_end = True652it1 = groups[g_it][2][-1] # the old end of the line653robots[it1].status_2_end = False654robots[it0].key_neighbors = [it1]655robots[it1].key_neighbors.append(it0)656# update the 'groups' variable657groups[g_it][3].remove(it0) # remove from the climbing pool658groups[g_it][2].append(it0) # append 'it0' at the end of the line659# 5.s_form_lost, robot '1' gets lost during initial forming660for g_it in s_form_lost:661# can disassemble the group here, but may as well do it together in s_disassemble662s_disassemble.append([g_it])663# 6.s_climb_lost, robot '1' gets lost during climbing, becoming '-1'664for i in s_climb_lost:665# update the 'robots' variable666robots[i].status = -1667# a new random moving orientation668robots[i].ori = random.random() * 2*math.pi - math.pi669# a new random life time670robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)671# update the 'groups' variable672g_it = robots[i].group_id673groups[g_it][0] = groups[g_it][0] - 1 # reduce group size by 1674# life time of a group doesn't decrease due to member lost675groups[g_it][3].remove(i) # remove the robot from the pool list676# 7.s_group_exp, natural life expiration of the groups677for g_it in s_group_exp:678s_disassemble.append([g_it]) # leave it to s_disassemble679# 8.s_disassemble, triggered by robot '1' or '2'680s_dis_list = [] # list of group id that has been finalized for disassembling681# compare number of members to decide which groups to disassemble682for gs_it in s_disassemble:683if len(gs_it) == 1:684# from the s_form_lost685if gs_it[0] not in s_dis_list:686s_dis_list.append(gs_it[0])687else:688# compare which group has the most members, and disassemble the rest689g_temp = gs_it[:]690member_max = 0 # number of members in the group691group_max = -1 # corresponding group id with most members692for g_it in g_temp:693if groups[g_it][0] > member_max:694member_max = groups[g_it][0]695group_max = g_it696g_temp.remove(group_max) # remove the group with the most members697for g_it in g_temp:698if g_it not in s_dis_list: # avoid multiple occurrence699s_dis_list.append(g_it)700# start disassembling701for g_it in s_dis_list:702# update the 'robots' variable703for i in groups[g_it][3]: # for robots off the line704robots[i].status = -1705# give a random (integer) life time in designed range706robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)707robots[i].ori = random.random() * 2*math.pi - math.pi708# give a random moving orientation is still not good enough709# a new way for the robots on the line is 'explode' the first half to one side710# and 'explode' the second half to the other side.711num_online = len(groups[g_it][2]) # number of robots on the line712num_1_half = num_online/2 # number of robots for the first half713num_2_half = num_online - num_1_half # number of robots for the second half714for i in groups[g_it][2]: # for robots on the line715robots[i].status = -1716robots[i].status_n1_life = random.randint(n1_life_lower, n1_life_upper)717# robots[i].ori = random.random() * 2*math.pi - math.pi718# first deciding the direction of the line, always pointing to the closer end719ori_temp = 0720seq_temp = robots[i].status_2_sequence # sequence on the line721if robots[i].status_2_sequence == 0:722# then get second one on the line723it0 = groups[robots[i].group_id][2][1]724# orientation points to the small index end725ori_temp = math.atan2(robots[i].pos[1]-robots[it0].pos[1],726robots[i].pos[0]-robots[it0].pos[0])727elif robots[i].status_2_end == True:728# then get inverse second one on the line729it0 = groups[robots[i].group_id][2][-2]730# orientation points to the large index end731ori_temp = math.atan2(robots[i].pos[1]-robots[it0].pos[1],732robots[i].pos[0]-robots[it0].pos[0])733else:734# get both neighbors735it0 = groups[g_it][2][seq_temp - 1]736it1 = groups[g_it][2][seq_temp + 1]737if seq_temp < num_1_half: # robot in the first half738# orientation points to the small index end739ori_temp = math.atan2(robots[it0].pos[1]-robots[it1].pos[1],740robots[it0].pos[0]-robots[it1].pos[0])741else:742# orientation points to the large index end743ori_temp = math.atan2(robots[it1].pos[1]-robots[it0].pos[1],744robots[it1].pos[0]-robots[it0].pos[0])745# then calculate the 'explosion' direction746if seq_temp < num_1_half:747robots[i].ori = reset_radian(ori_temp + math.pi*(seq_temp+1)/(num_1_half+1))748# always 'explode' to the left side749else:750robots[i].ori = reset_radian(ori_temp + math.pi*(num_online-seq_temp)/(num_2_half+1))751# pop out this group in 'groups'752groups.pop(g_it)753# 9.s_back_0, life time of robot '-1' expires, becoming '0'754for i in s_back_0:755# still maintain the old moving direction756robots[i].status = 0757758# update the physics(pos and ori), and wall bouncing, life decrease of '-1'759for i in range(robot_quantity):760if robots[i].status == 2:761continue # every robot moves except '2'762# check if out of boundaries, algorithms revised from 'experiment_3_automaton'763# change only direction of velocity764if robots[i].status != 0:765# for the robots '-1', '1' and '2', bounded by the real boundaries766if robots[i].pos[0] >= world_size[0]: # out of right boundary767if math.cos(robots[i].ori) > 0: # velocity on x is pointing right768robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)769elif robots[i].pos[0] <= 0: # out of left boundary770if math.cos(robots[i].ori) < 0: # velocity on x is pointing left771robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)772if robots[i].pos[1] >= world_size[1]: # out of top boundary773if math.sin(robots[i].ori) > 0: # velocity on y is pointing up774robots[i].ori = reset_radian(2*(0) - robots[i].ori)775elif robots[i].pos[1] <= 0: # out of bottom boundary776if math.sin(robots[i].ori) < 0: # velocity on y is pointing down777robots[i].ori = reset_radian(2*(0) - robots[i].ori)778else:779# for robot '0' only, bounded in a smaller area780# so more lines will be formed in the center area, and line won't run out781if robots[i].pos[0] >= world_size[0]*(1-vbound_gap_ratio):782if math.cos(robots[i].ori) > 0:783robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)784elif robots[i].pos[0] <= world_size[0]*vbound_gap_ratio:785if math.cos(robots[i].ori) < 0:786robots[i].ori = reset_radian(2*(math.pi/2) - robots[i].ori)787if robots[i].pos[1] >= world_size[1]*(1-vbound_gap_ratio):788if math.sin(robots[i].ori) > 0:789robots[i].ori = reset_radian(2*(0) - robots[i].ori)790elif robots[i].pos[1] <= world_size[1]*vbound_gap_ratio:791if math.sin(robots[i].ori) < 0:792robots[i].ori = reset_radian(2*(0) - robots[i].ori)793# update one step of distance794travel_dist = robots[i].vel * frame_period/1000.0795robots[i].pos[0] = robots[i].pos[0] + travel_dist*math.cos(robots[i].ori)796robots[i].pos[1] = robots[i].pos[1] + travel_dist*math.sin(robots[i].ori)797# update direction of velocity for robot '1', conflict with boundary check?798if robots[i].status == 1:799if robots[i].status_1_sub == 0:800it0 = robots[i].key_neighbors[0]801vect_temp = (robots[it0].pos[0]-robots[i].pos[0],802robots[it0].pos[1]-robots[i].pos[1]) # pointing from i to it0803ori_temp = math.atan2(vect_temp[1], vect_temp[0])804if dist_table[i][it0] > line_space: # it's ok to use the out dated dist_table805# pointing toward the neighbor806robots[i].ori = ori_temp807else:808# pointing opposite the neighbor809robots[i].ori = reset_radian(ori_temp + math.pi)810else:811robots[i].ori = math.atan2(robots[i].status_1_1_des[1]-robots[i].pos[1],812robots[i].status_1_1_des[0]-robots[i].pos[0])813# decrease life time of robot with status '-1'814if robots[i].status == -1:815robots[i].status_n1_life = robots[i].status_n1_life - frame_period/1000.0816# life time decrease of the groups817for g_it in groups.keys():818if groups[g_it][4]: continue # not decrease life of the dominant819groups[g_it][1] = groups[g_it][1] - frame_period/1000.0820821# graphics update822screen.fill(background_color)823# draw the virtual boundaries824pygame.draw.lines(screen, (255, 255, 255), True, [pos_lb, pos_rb, pos_rt, pos_lt], 1)825# draw the robots826for i in range(robot_quantity):827display_pos = world_to_display(robots[i].pos, world_size, screen_size)828# get color of the robot829color_temp = ()830if robots[i].status == 0:831color_temp = robot_0_color832elif robots[i].status == 1:833color_temp = robot_1_color834elif robots[i].status == 2:835color_temp = robot_2_color836elif robots[i].status == -1:837color_temp = robot_n1_color838# draw the robot as a small solid circle839pygame.draw.circle(screen, color_temp, display_pos, robot_size, 0) # fill the circle840pygame.display.update()841842pygame.quit()843844845846847