You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
13 KiB
Python

7 months ago
from typing import List
from world.commons.Other_Robot import Other_Robot
from world.World import World
import numpy as np
class Radio():
'''
map limits are hardcoded:
teammates/opponents positions (x,y) in ([-16,16],[-11,11])
ball position (x,y) in ([-15,15],[-10,10])
known server limitations:
claimed: all ascii from 0x20 to 0x7E except ' ', '(', ')'
bugs:
- ' or " clip the message
- '\' at the end or near another '\'
- ';' at beginning of message
'''
# map limits are hardcoded:
# lines, columns, half lines index, half cols index, (lines-1)/x_span, (cols-1)/y_span, combinations, combinations*2states,
TP = 321,221,160,110,10, 10,70941,141882 # teammate position
OP = 201,111,100,55, 6.25,5, 22311,44622 # opponent position
BP = 301,201,150,100,10, 10,60501 # ball position
SYMB = "!#$%&*+,-./0123456789:<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~;"
SLEN = len(SYMB)
SYMB_TO_IDX = {ord(s):i for i,s in enumerate(SYMB)}
def __init__(self, world : World, commit_announcement) -> None:
self.world = world
self.commit_announcement = commit_announcement
r = world.robot
t = world.teammates
o = world.opponents
self.groups = ( # player team/unum, group has ball?, self in group?
[(t[9],t[10],o[6],o[7],o[8],o[9],o[10]), True ], # 2 teammates, 5 opponents, ball
[(t[0],t[1], t[2],t[3],t[4],t[5],t[6] ), False], # 7 teammates
[(t[7],t[8], o[0],o[1],o[2],o[3],o[4],o[5]), False] # 2 teammates, 6 opponents
)
for g in self.groups: # add 'self in group?'
g.append(any(i.is_self for i in g[0]))
def get_player_combination(self, pos, is_unknown, is_down, info):
''' Returns combination (0-based) and number of possible combinations '''
if is_unknown:
return info[7]+1, info[7]+2 # return unknown combination
x,y = pos[:2]
if x < -17 or x > 17 or y < -12 or y > 12:
return info[7], info[7]+2 # return out of bounds combination (if it exceeds 1m in any axis)
# convert to int to avoid overflow later
l = int(np.clip( round(info[4]*x+info[2]), 0, info[0]-1 )) # absorb out of bounds positions (up to 1m in each axis)
c = int(np.clip( round(info[5]*y+info[3]), 0, info[1]-1 ))
return (l*info[1]+c)+(info[6] if is_down else 0), info[7]+2 # return valid combination
def get_ball_combination(self, x, y):
''' Returns combination (0-based) and number of possible combinations '''
# if ball is out of bounds, we force it in
l = int(np.clip( round(Radio.BP[4]*x+Radio.BP[2]), 0, Radio.BP[0]-1 ))
c = int(np.clip( round(Radio.BP[5]*y+Radio.BP[3]), 0, Radio.BP[1]-1 ))
return l*Radio.BP[1]+c, Radio.BP[6] # return valid combination
def get_ball_position(self,comb):
l = comb // Radio.BP[1]
c = comb % Radio.BP[1]
return np.array([l/Radio.BP[4]-15, c/Radio.BP[5]-10, 0.042]) # assume ball is on ground
def get_player_position(self,comb, info):
if comb == info[7]: return -1 # player is out of bounds
if comb == info[7]+1: return -2 # player is in unknown location
is_down = comb >= info[6]
if is_down:
comb -= info[6]
l = comb // info[1]
c = comb % info[1]
return l/info[4]-16, c/info[5]-11, is_down
def check_broadcast_requirements(self):
'''
Check if broadcast group is valid
Returns
-------
ready : bool
True if all requirements are met
Sequence: g0,g1,g2, ig0,ig1,ig2, iig0,iig1,iig2 (whole cycle: 0.36s)
igx means 'incomplete group', where <=1 element can be MIA recently
iigx means 'very incomplete group', where <=2 elements can be MIA recently
Rationale: prevent incomplete messages from monopolizing the broadcast space
However:
- 1st round: when 0 group members are missing, that group will update 3 times every 0.36s
- 2nd round: when 1 group member is recently missing, that group will update 2 times every 0.36s
- 3rd round: when 2 group members are recently missing, that group will update 1 time every 0.36s
- when >2 group members are recently missing, that group will not be updated
Players that have never been seen or heard are not considered for the 'recently missing'.
If there is only 1 group member since the beginning, the respective group can be updated, except in the 1st round.
In this way, the 1st round cannot be monopolized by clueless agents, which is important during games with 22 players.
'''
w = self.world
r = w.robot
ago40ms = w.time_local_ms - 40
ago370ms = w.time_local_ms - 370 # maximum delay (up to 2 MIAs) is 360ms because radio has a delay of 20ms (otherwise max delay would be 340ms)
group : List[Other_Robot]
idx9 = int((w.time_server * 25)+0.1) % 9 # sequence of 9 phases
max_MIA = idx9 // 3 # maximum number of MIA players (based on server time)
group_idx = idx9 % 3 # group number (based on server time)
group, has_ball, is_self_included = self.groups[group_idx]
#============================================ 0. check if group is valid
if has_ball and w.ball_abs_pos_last_update < ago40ms: # Ball is included and not up to date
return False
if is_self_included and r.loc_last_update < ago40ms: # Oneself is included and unable to self-locate
return False
# Get players that have been previously seen or heard but not recently
MIAs = [not ot.is_self and ot.state_last_update < ago370ms and ot.state_last_update > 0 for ot in group]
self.MIAs = [ot.state_last_update == 0 or MIAs[i] for i,ot in enumerate(group)] # add players that have never been seen
if sum(MIAs) > max_MIA: # checking if number of recently missing members is not above threshold
return False
# never seen before players are always ignored except when:
# - this is the 0 MIAs round (see explanation above)
# - all are MIA
if (max_MIA == 0 and any(self.MIAs)) or all(self.MIAs):
return False
# Check for invalid members. Conditions:
# - Player is other and not MIA and:
# - last update was >40ms ago OR
# - last update did not include the head (head is important to provide state and accurate position)
if any(
(not ot.is_self and not self.MIAs[i] and
(ot.state_last_update < ago40ms or ot.state_last_update==0 or len(ot.state_abs_pos)<3)# (last update: has no head or is old)
) for i,ot in enumerate(group)
):
return False
return True
def broadcast(self):
'''
Commit messages to teammates if certain conditions are met
Messages contain: positions/states of every moving entity
'''
if not self.check_broadcast_requirements():
return
w = self.world
ot : Other_Robot
group_idx = int((w.time_server * 25)+0.1) % 3 # group number based on server time
group, has_ball, _ = self.groups[group_idx]
#============================================ 1. create combination
# add message number
combination = group_idx
no_of_combinations = 3
# add ball combination
if has_ball:
c, n = self.get_ball_combination(w.ball_abs_pos[0], w.ball_abs_pos[1])
combination += c * no_of_combinations
no_of_combinations *= n
# add group combinations
for i,ot in enumerate(group):
c, n = self.get_player_combination(ot.state_abs_pos, # player position
self.MIAs[i], ot.state_fallen, # is unknown, is down
Radio.TP if ot.is_teammate else Radio.OP) # is teammate
combination += c * no_of_combinations
no_of_combinations *= n
assert(no_of_combinations < 9.61e38) # 88*89^19 (first character cannot be ';')
#============================================ 2. create message
# 1st msg symbol: ignore ';' due to server bug
msg = Radio.SYMB[combination % (Radio.SLEN-1)]
combination //= (Radio.SLEN-1)
# following msg symbols
while combination:
msg += Radio.SYMB[combination % Radio.SLEN]
combination //= Radio.SLEN
#============================================ 3. commit message
self.commit_announcement(msg.encode()) # commit message
def receive(self, msg:bytearray):
w = self.world
r = w.robot
ago40ms = w.time_local_ms - 40
ago110ms = w.time_local_ms - 110
msg_time = w.time_local_ms - 20 # message was sent in the last step
#============================================ 1. get combination
# read first symbol, which cannot be ';' due to server bug
combination = Radio.SYMB_TO_IDX[msg[0]]
total_combinations = Radio.SLEN-1
if len(msg)>1:
for m in msg[1:]:
combination += total_combinations * Radio.SYMB_TO_IDX[m]
total_combinations *= Radio.SLEN
#============================================ 2. get msg ID
message_no = combination % 3
combination //= 3
group, has_ball, _ = self.groups[message_no]
#============================================ 3. get data
if has_ball:
ball_comb = combination % Radio.BP[6]
combination //= Radio.BP[6]
players_combs = []
ot : Other_Robot
for ot in group:
info = Radio.TP if ot.is_teammate else Radio.OP
players_combs.append( combination % (info[7]+2) )
combination //= info[7]+2
#============================================ 4. update world
if has_ball and w.ball_abs_pos_last_update < ago40ms: # update ball if it was not seen
time_diff = (msg_time - w.ball_abs_pos_last_update) / 1000
ball = self.get_ball_position(ball_comb)
w.ball_abs_vel = (ball - w.ball_abs_pos) / time_diff
w.ball_abs_speed = np.linalg.norm(w.ball_abs_vel)
w.ball_abs_pos_last_update = msg_time # (error: 0-40 ms)
w.ball_abs_pos = ball
w.is_ball_abs_pos_from_vision = False
for c, ot in zip(players_combs, group):
# handle oneself case
if ot.is_self:
# the ball's position has a fair amount of noise, whether seen by us or other players
# but our self-locatization mechanism is usually much better than how others perceive us
if r.loc_last_update < ago110ms: # so we wait until we miss 2 visual steps
data = self.get_player_position(c, Radio.TP)
if type(data)==tuple:
x,y,is_down = data
r.loc_head_position[:2] = x,y # z is kept unchanged
r.loc_head_position_last_update = msg_time
r.radio_fallen_state = is_down
r.radio_last_update = msg_time
continue
# do not update if other robot was recently seen
if ot.state_last_update >= ago40ms:
continue
info = Radio.TP if ot.is_teammate else Radio.OP
data = self.get_player_position(c, info)
if type(data)==tuple:
x,y,is_down = data
p = np.array([x,y])
if ot.state_abs_pos is not None: # update the x & y components of the velocity
time_diff = (msg_time - ot.state_last_update) / 1000
velocity = np.append( (p - ot.state_abs_pos[:2]) / time_diff, 0) # v.z = 0
vel_diff = velocity - ot.state_filtered_velocity
if np.linalg.norm(vel_diff) < 4: # otherwise assume it was beamed
ot.state_filtered_velocity /= (ot.vel_decay,ot.vel_decay,1) # neutralize decay (except in the z-axis)
ot.state_filtered_velocity += ot.vel_filter * vel_diff
ot.state_fallen = is_down
ot.state_last_update = msg_time
ot.state_body_parts_abs_pos = {"head":p}
ot.state_abs_pos = p
ot.state_horizontal_dist = np.linalg.norm(p - r.loc_head_position[:2])
ot.state_ground_area = (p, 0.3 if is_down else 0.2) # not very precise, but we cannot see the robot