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.

300 lines
12 KiB
Python

7 months ago
from os import path, listdir, getcwd, cpu_count
from os.path import join, realpath, dirname, isfile, isdir, getmtime
from scripts.commons.UI import UI
import __main__
import argparse,json,sys
import pickle
import subprocess
class Script():
ROOT_DIR = path.dirname(path.dirname(realpath( join(getcwd(), dirname(__file__))) )) # project root directory
def __init__(self, cpp_builder_unum=0) -> None:
'''
Arguments specification
-----------------------
- To add new arguments, edit the information below
- After changing information below, the config.json file must be manually deleted
- In other modules, these arguments can be accessed by their 1-letter ID
'''
# list of arguments: 1-letter ID, Description, Hardcoded default
self.options = {'i': ('Server Hostname/IP', 'localhost'),
'p': ('Agent Port', '3100'),
'm': ('Monitor Port', '3200'),
't': ('Team Name', 'FCPortugal'),
'u': ('Uniform Number', '1'),
'r': ('Robot Type', '1'),
'P': ('Penalty Shootout', '0'),
'F': ('magmaFatProxy', '0'),
'D': ('Debug Mode', '1')}
# list of arguments: 1-letter ID, data type, choices
self.op_types = {'i': (str, None),
'p': (int, None),
'm': (int, None),
't': (str, None),
'u': (int, range(1,12)),
'r': (int, [0,1,2,3,4]),
'P': (int, [0,1]),
'F': (int, [0,1]),
'D': (int, [0,1])}
'''
End of arguments specification
'''
self.read_or_create_config()
#advance help text position
formatter = lambda prog: argparse.HelpFormatter(prog,max_help_position=52)
parser = argparse.ArgumentParser(formatter_class=formatter)
o = self.options
t = self.op_types
for id in self.options: # shorter metavar for aesthetic reasons
parser.add_argument(f"-{id}", help=f"{o[id][0]:30}[{o[id][1]:20}]", type=t[id][0], nargs='?', default=o[id][1], metavar='X', choices=t[id][1])
self.args = parser.parse_args()
if getattr(sys, 'frozen', False): # disable debug mode when running from binary
self.args.D = 0
self.players = [] # list of created players
Script.build_cpp_modules(exit_on_build = (cpp_builder_unum != 0 and cpp_builder_unum != self.args.u))
if self.args.D:
try:
print(f"\nNOTE: for help run \"python {__main__.__file__} -h\"")
except:
pass
columns = [[],[],[]]
for key, value in vars(self.args).items():
columns[0].append(o[key][0])
columns[1].append(o[key][1])
columns[2].append(value)
UI.print_table(columns, ["Argument","Default at /config.json","Active"], alignment=["<","^","^"])
def read_or_create_config(self) -> None:
if not path.isfile('config.json'): # Save hardcoded default values if file does not exist
with open("config.json", "w") as f:
json.dump(self.options, f, indent=4)
else: # Load user-defined values (that can be overwritten in the terminal)
with open("config.json", "r") as f:
self.options = json.loads(f.read())
@staticmethod
def build_cpp_modules(special_environment_prefix=[], exit_on_build=False):
'''
Build C++ modules in folder /cpp using Pybind11
Parameters
----------
special_environment_prefix : `list`
command prefix to run a given command in the desired environment
useful to compile C++ modules for different python interpreter versions (other than default version)
Conda Env. example: ['conda', 'run', '-n', 'myEnv']
If [] the default python interpreter is used as compilation target
exit_on_build : bool
exit if there is something to build (so that only 1 player per team builds c++ modules)
'''
cpp_path = Script.ROOT_DIR + "/cpp/"
exclusions = ["__pycache__"]
cpp_modules = [d for d in listdir(cpp_path) if isdir(join(cpp_path, d)) and d not in exclusions]
if not cpp_modules: return #no modules to build
python_cmd = f"python{sys.version_info.major}.{sys.version_info.minor}" # "python3" can select the wrong version, this prevents that
def init():
print("--------------------------\nC++ modules:",cpp_modules)
try:
process = subprocess.Popen(special_environment_prefix+[python_cmd, "-m", "pybind11", "--includes"], stdout=subprocess.PIPE)
(includes, err) = process.communicate()
process.wait()
except:
print(f"Error while executing child program: '{python_cmd} -m pybind11 --includes'")
exit()
includes = includes.decode().rstrip() # strip trailing newlines (and other whitespace chars)
print("Using Pybind11 includes: '",includes,"'",sep="")
return includes
nproc = str(cpu_count())
zero_modules = True
for module in cpp_modules:
module_path = join(cpp_path, module)
# skip module if there is no Makefile (typical distribution case)
if not isfile(join(module_path, "Makefile")):
continue
# skip module in certain conditions
if isfile(join(module_path, module+".so")) and isfile(join(module_path, module+".c_info")):
with open(join(module_path, module+".c_info"), 'rb') as f:
info = pickle.load(f)
if info == python_cmd:
code_mod_time = max(getmtime(join(module_path, f)) for f in listdir(module_path) if f.endswith(".cpp") or f.endswith(".h"))
bin_mod_time = getmtime(join(module_path, module+".so"))
if bin_mod_time + 30 > code_mod_time: # favor not building with a margin of 30s (scenario: we unzip the fcpy project, including the binaries, the modification times are all similar)
continue
# init: print stuff & get Pybind11 includes
if zero_modules:
if exit_on_build:
print("There are C++ modules to build. This player is not allowed to build. Aborting.")
exit()
zero_modules = False
includes = init()
# build module
print(f'{f"Building: {module}... ":40}',end='',flush=True)
process = subprocess.Popen(['make', '-j'+nproc, 'PYBIND_INCLUDES='+includes], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=module_path)
(output, err) = process.communicate()
exit_code = process.wait()
if exit_code == 0:
print("success!")
with open(join(module_path, module+".c_info"),"wb") as f: # save python version
pickle.dump(python_cmd, f, protocol=4) # protocol 4 is backward compatible with Python 3.4
else:
print("Aborting! Building errors:")
print(output.decode(), err.decode())
exit()
if not zero_modules:
print("All modules were built successfully!\n--------------------------")
def batch_create(self, agent_cls, args_per_player):
''' Creates batch of agents '''
for a in args_per_player:
self.players.append( agent_cls(*a) )
def batch_execute_agent(self, index : slice = slice(None)):
'''
Executes agent normally (including commit & send)
Parameters
----------
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
'''
for p in self.players[index]:
p.think_and_send()
def batch_execute_behavior(self, behavior, index : slice = slice(None)):
'''
Executes behavior
Parameters
----------
behavior : str
name of behavior to execute
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
'''
for p in self.players[index]:
p.behavior.execute(behavior)
def batch_commit_and_send(self, index : slice = slice(None)):
'''
Commits & sends data to server
Parameters
----------
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
'''
for p in self.players[index]:
p.scom.commit_and_send( p.world.robot.get_command() )
def batch_receive(self, index : slice = slice(None), update=True):
'''
Waits for server messages
Parameters
----------
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
update : bool
update world state based on information received from server
if False, the agent becomes unaware of itself and its surroundings
which is useful for reducing cpu resources for dummy agents in demonstrations
'''
for p in self.players[index]:
p.scom.receive(update)
def batch_commit_beam(self, pos2d_and_rotation, index : slice = slice(None)):
'''
Beam all player to 2D position with a given rotation
Parameters
----------
pos2d_and_rotation : `list`
iterable of 2D positions and rotations e.g. [(0,0,45),(-5,0,90)]
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
'''
for p, pos_rot in zip(self.players[index], pos2d_and_rotation):
p.scom.commit_beam(pos_rot[0:2],pos_rot[2])
def batch_unofficial_beam(self, pos3d_and_rotation, index : slice = slice(None)):
'''
Beam all player to 3D position with a given rotation
Parameters
----------
pos3d_and_rotation : `list`
iterable of 3D positions and rotations e.g. [(0,0,0.5,45),(-5,0,0.5,90)]
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
'''
for p, pos_rot in zip(self.players[index], pos3d_and_rotation):
p.scom.unofficial_beam(pos_rot[0:3],pos_rot[3])
def batch_terminate(self, index : slice = slice(None)):
'''
Close all sockets connected to the agent port
For scripts where the agent lives until the application ends, this is not needed
Parameters
----------
index : slice
subset of agents
(e.g. index=slice(1,2) will select the second agent)
(e.g. index=slice(1,3) will select the second and third agents)
by default, all agents are selected
'''
for p in self.players[index]:
p.terminate()
del self.players[index] # delete selection