diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py index b733be005bbcc76e9ca7ffb13f1caf5027178800..23cdcc74fc0069ae27ab291259b5337cbcf8bb7d 100644 --- a/cooperative_cuisine/__init__.py +++ b/cooperative_cuisine/__init__.py @@ -371,6 +371,7 @@ websockets, - the **orders**, how to sample incoming orders and their attributes, - the **player**/agent, that interacts in the environment, - the **pygame 2d visualization**, GUI, drawing, and video generation, +- the **recipe** validation and graph generation, - the **recording**, via hooks, actions, environment configs, states, etc. can be recorded in files, - the **scores**, via hooks, events can affect the scores, - type hints are defined in **state representation** for the json state and **server results** for the data returned by diff --git a/cooperative_cuisine/__main__.py b/cooperative_cuisine/__main__.py index 6c067da2020b3b758b03c83f9abfdbf8da1d4eae..257ff656ae58f81e6ec88650132d910e091e8296 100644 --- a/cooperative_cuisine/__main__.py +++ b/cooperative_cuisine/__main__.py @@ -6,6 +6,7 @@ from cooperative_cuisine.utils import ( url_and_port_arguments, disable_websocket_logging_arguments, add_list_of_manager_ids_arguments, + add_study_arguments, ) USE_STUDY_SERVER = True @@ -26,6 +27,7 @@ def start_study_server(cli_args): game_host=cli_args.game_url, game_port=cli_args.game_port, manager_ids=cli_args.manager_ids, + study_config_path=cli_args.study_config, ) @@ -53,6 +55,7 @@ def main(cli_args=None): url_and_port_arguments(parser) disable_websocket_logging_arguments(parser) add_list_of_manager_ids_arguments(parser) + add_study_arguments(parser) cli_args = parser.parse_args() diff --git a/cooperative_cuisine/configs/study/level1/level1_config.yaml b/cooperative_cuisine/configs/study/level1/level1_config.yaml index 1751bfa019ad9a00922c628424b9a1486f748e70..6732fa298663a9e6c70094abef84dff2d3d5cbc4 100644 --- a/cooperative_cuisine/configs/study/level1/level1_config.yaml +++ b/cooperative_cuisine/configs/study/level1/level1_config.yaml @@ -9,7 +9,7 @@ game: undo_dispenser_pickup: true meals: - all: true + all: false # if all: false -> only orders for these meals are generated # TODO: what if this list is empty? list: diff --git a/cooperative_cuisine/configs/study/study_config.yaml b/cooperative_cuisine/configs/study/study_config.yaml index 0b6a0bb654070178ce958e217de765adffca8869..eb241e93381f6a63973e765a56aea0b4d8336712 100644 --- a/cooperative_cuisine/configs/study/study_config.yaml +++ b/cooperative_cuisine/configs/study/study_config.yaml @@ -1,21 +1,17 @@ -# Config paths are relative to configs folder. -# Layout files are relative to layouts folder. - - levels: - - config_path: study/level1/level1_config.yaml - layout_path: basic.layout - item_info_path: study/level1/level1_item_info.yaml + - config_path: STUDY_DIR/level1/level1_config.yaml + layout_path: LAYOUTS_DIR/overcooked-1/1-1-far-apart.layout + item_info_path: STUDY_DIR/level1/level1_item_info.yaml name: "Level 1-1: Far Apart" - - config_path: environment_config.yaml - layout_path: basic.layout - item_info_path: item_info.yaml + - config_path: CONFIGS_DIR/environment_config.yaml + layout_path: LAYOUTS_DIR/basic.layout + item_info_path: CONFIGS_DIR/item_info.yaml name: "Basic" - - config_path: study/level2/level2_config.yaml - layout_path: overcooked-1/1-4-bottleneck.layout - item_info_path: study/level2/level2_item_info.yaml + - config_path: STUDY_DIR/level2/level2_config.yaml + layout_path: LAYOUTS_DIR/overcooked-1/1-4-bottleneck.layout + item_info_path: STUDY_DIR/level2/level2_item_info.yaml name: "Level 1-4: Bottleneck" diff --git a/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py index 909db81e47a75035a7a5ccb95f5b7927c390c640..169565e065ecb7b1319df62ffb3d7a4cf49a07d9 100644 --- a/cooperative_cuisine/environment.py +++ b/cooperative_cuisine/environment.py @@ -167,6 +167,8 @@ class Environment: env_config = file.read() with open(layout_config, "r") as layout_file: layout_config = layout_file.read() + with open(item_info, "r") as file: + item_info = file.read() self.environment_config: EnvironmentConfig = yaml.load( env_config, Loader=yaml.Loader @@ -192,7 +194,7 @@ class Environment: self.item_info: dict[str, ItemInfo] = self.load_item_info(item_info) """The loaded item info dict. Keys are the item names.""" - self.hook(ITEM_INFO_LOADED, item_info=item_info, as_files=as_files) + self.hook(ITEM_INFO_LOADED, item_info=item_info) # self.validate_item_info() if self.environment_config["meals"]["all"]: @@ -350,13 +352,10 @@ class Environment: Utility method to pass a reference to the serving window.""" return self.env_time - def load_item_info(self, data) -> dict[str, ItemInfo]: + def load_item_info(self, item_info) -> dict[str, ItemInfo]: """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos.""" - if self.as_files: - with open(data, "r") as file: - data = file.read() - self.hook(ITEM_INFO_CONFIG, item_info_config=data) - item_lookup = yaml.safe_load(data) + self.hook(ITEM_INFO_CONFIG, item_info_config=item_info) + item_lookup = yaml.safe_load(item_info) for item_name in item_lookup: item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name]) @@ -408,7 +407,8 @@ class Environment: player_name: The id/name of the player to reference actions and in the state. pos: The optional init position of the player. """ - # TODO check if the player name already exists in the environment and do not overwrite player. + if player_name in self.players: + raise ValueError(f"Player {player_name} already exists.") log.debug(f"Add player {player_name} to the game") player = Player( player_name, diff --git a/cooperative_cuisine/recording.py b/cooperative_cuisine/recording.py index 79f98fb9d845edf4457eb9fcd17b413286ff433e..01f0de86833e7f81290fb70438e3c98ec36f9775 100644 --- a/cooperative_cuisine/recording.py +++ b/cooperative_cuisine/recording.py @@ -46,12 +46,9 @@ import os import traceback from pathlib import Path -import platformdirs - -from cooperative_cuisine import ROOT_DIR from cooperative_cuisine.environment import Environment from cooperative_cuisine.hooks import HookCallbackClass -from cooperative_cuisine.utils import NumpyAndDataclassEncoder +from cooperative_cuisine.utils import NumpyAndDataclassEncoder, expand_path log = logging.getLogger(__name__) @@ -80,18 +77,8 @@ class FileRecorder(HookCallbackClass): ): super().__init__(name, env, **kwargs) self.add_hook_ref = add_hook_ref - log_path = log_path.replace("ENV_NAME", env.env_name).replace( - "LOG_RECORD_NAME", name - ) - if log_path.startswith("USER_LOG_DIR/"): - log_path = ( - Path(platformdirs.user_log_dir("cooperative_cuisine")) - / log_path[len("USER_LOG_DIR/") :] - ) - elif log_path.startswith("ROOT_DIR/"): - log_path = ROOT_DIR / log_path[len("ROOT_DIR/") :] - else: - log_path = Path(log_path) + log_path = log_path.replace("LOG_RECORD_NAME", name) + log_path = Path(expand_path(log_path, env_name=env.env_name)) self.log_path = log_path log.info(f"Recorder record for {name} in file://{log_path}") os.makedirs(log_path.parent, exist_ok=True) diff --git a/cooperative_cuisine/study_server.py b/cooperative_cuisine/study_server.py index 3319cdde14eb4748399c12b5b01cf821e703d879..68a3fee4cf169df3e7570e2f1c3f06125fb0c51f 100644 --- a/cooperative_cuisine/study_server.py +++ b/cooperative_cuisine/study_server.py @@ -34,6 +34,8 @@ from cooperative_cuisine.server_results import PlayerInfo from cooperative_cuisine.utils import ( url_and_port_arguments, add_list_of_manager_ids_arguments, + expand_path, + add_study_arguments, ) NUMBER_PLAYER_PER_ENV = 2 @@ -87,12 +89,15 @@ class StudyState: self.next_level_env = None self.players_done = {} - self.USE_AAAMBOS_AGENT = False + self.use_aaambos_agent = False self.websocket_url = f"ws://{game_url}:{game_port}/ws/player/" print("WS:", self.websocket_url) self.sub_processes = [] + self.current_item_info = None + self.current_config = None + @property def study_done(self): return self.current_level_idx >= len(self.levels) @@ -115,14 +120,18 @@ class StudyState: return filled and not self.is_full def create_env(self, level): - with open(ROOT_DIR / "configs" / level["item_info_path"], "r") as file: + item_info_path = expand_path(level["item_info_path"]) + layout_path = expand_path(level["layout_path"]) + config_path = expand_path(level["config_path"]) + + with open(item_info_path, "r") as file: item_info = file.read() self.current_item_info: EnvironmentConfig = yaml.load( item_info, Loader=yaml.Loader ) - with open(ROOT_DIR / "configs" / "layouts" / level["layout_path"], "r") as file: + with open(layout_path, "r") as file: layout = file.read() - with open(ROOT_DIR / "configs" / level["config_path"], "r") as file: + with open(config_path, "r") as file: environment_config = file.read() self.current_config: EnvironmentConfig = yaml.load( environment_config, Loader=yaml.Loader @@ -154,7 +163,7 @@ class StudyState: self.create_and_connect_bot(player_id, player_info) return env_info - def start(self): + def start_level(self): level = self.levels[self.current_level_idx] self.current_running_env = self.create_env(level) @@ -170,8 +179,7 @@ class StudyState: self.current_level_idx += 1 if not self.study_done: - level = self.levels[self.current_level_idx] - self.current_running_env = self.create_env(level) + self.start_level() for ( participant_id, player_info, @@ -199,8 +207,7 @@ class StudyState: def player_finished_level(self, participant_id): self.players_done[participant_id] = True - level_done = all(self.players_done.values()) - if level_done: + if all(self.players_done.values()): self.next_level() def get_connection(self, participant_id: str): @@ -223,7 +230,7 @@ class StudyState: print( f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"' ) - if self.USE_AAAMBOS_AGENT: + if self.use_aaambos_agent: sub = Popen( " ".join( [ @@ -258,7 +265,7 @@ class StudyState: def kill_bots(self): for sub in self.sub_processes: try: - if self.USE_AAAMBOS_AGENT: + if self.use_aaambos_agent: pgrp = os.getpgid(sub.pid) os.killpg(pgrp, signal.SIGINT) subprocess.run( @@ -271,8 +278,6 @@ class StudyState: pass self.sub_processes = [] - for websocket in self.websockets.values(): - websocket.close() def __repr__(self): return f"Study({self.current_running_env['env_id']})" @@ -295,13 +300,15 @@ class StudyManager: str, Tuple[int, dict[str, PlayerInfo], list[str]] ] = {} + self.study_config_path = ROOT_DIR / "configs" / "study" / "study_config.yml" + def create_study(self): study = StudyState( - ROOT_DIR / "configs" / "study" / "study_config.yaml", + self.study_config_path, self.game_host, self.game_port, ) - study.start() + study.start_level() self.running_studies.append(study) def add_participant(self, participant_id: str, number_players: int): @@ -334,6 +341,10 @@ class StudyManager: def set_manager_id(self, manager_id: str): self.server_manager_id = manager_id + def set_study_config(self, study_config_path: str): + # TODO validate study_config? + self.study_config_path = study_config_path + study_manager = StudyManager() @@ -404,9 +415,10 @@ async def want_to_play_tutorial(participant_id: str): ) -def main(study_host, study_port, game_host, game_port, manager_ids): +def main(study_host, study_port, game_host, game_port, manager_ids, study_config_path): study_manager.set_game_server_url(game_host=game_host, game_port=game_port) study_manager.set_manager_id(manager_id=manager_ids[0]) + study_manager.set_study_config(study_config_path=study_config_path) print( f"Use {study_manager.server_manager_id=} for game_server_url=http://{game_host}:{game_port}" @@ -430,6 +442,7 @@ if __name__ == "__main__": default_game_port=8000, ) add_list_of_manager_ids_arguments(parser=parser) + add_study_arguments(parser=parser) args = parser.parse_args() game_server_url = f"https://{args.game_url}:{args.game_port}" @@ -439,4 +452,5 @@ if __name__ == "__main__": game_host=args.game_url, game_port=args.game_port, manager_ids=args.manager_ids, + study_config_path=args.study_config, ) diff --git a/cooperative_cuisine/utils.py b/cooperative_cuisine/utils.py index 2d0a7dc38472667f22e9673cddbbe4d8f29e7fe8..7d57d5c6156ee247749d5f4ac29edecc6749a53e 100644 --- a/cooperative_cuisine/utils.py +++ b/cooperative_cuisine/utils.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING import numpy as np import numpy.typing as npt +import platformdirs from scipy.spatial import distance_matrix from cooperative_cuisine import ROOT_DIR @@ -27,6 +28,17 @@ from cooperative_cuisine.player import Player DEFAULT_SERVER_URL = "localhost" +def expand_path(path: str, env_name: str = "") -> str: + return os.path.expanduser( + path.replace("ROOT_DIR", str(ROOT_DIR)) + .replace("ENV_NAME", env_name) + .replace("USER_LOG_DIR", platformdirs.user_log_dir("cooperative_cuisine")) + .replace("LAYOUTS_DIR", str(ROOT_DIR / "configs" / "layouts")) + .replace("STUDY_DIR", str(ROOT_DIR / "configs" / "study")) + .replace("CONFIGS_DIR", str(ROOT_DIR / "configs")) + ) + + @dataclasses.dataclass class VectorStateGenerationData: grid_base_array: npt.NDArray[npt.NDArray[float]] @@ -179,8 +191,9 @@ def setup_logging(enable_websocket_logging=False): def url_and_port_arguments( parser, server_name="game server", default_study_port=8080, default_game_port=8000 ): + # TODO follow cli args standards: https://askubuntu.com/questions/711702/when-are-command-options-prefixed-with-two-hyphens parser.add_argument( - "-study-url", + "-study", "--study-url", "--study-host", type=str, @@ -195,7 +208,7 @@ def url_and_port_arguments( help=f"Port number for the {server_name}", ) parser.add_argument( - "-game-url", + "-game", "--game-url", "--game-host", type=str, @@ -228,6 +241,15 @@ def add_list_of_manager_ids_arguments(parser): ) +def add_study_arguments(parser): + parser.add_argument( + "--study-config", + type=str, + default=ROOT_DIR / "configs" / "study" / "study_config.yaml", + help="The config of the study.", + ) + + class NumpyAndDataclassEncoder(json.JSONEncoder): """Special json encoder for numpy types"""