diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py index a0577c01a74d76250c0152a73dcd51e1ab5093a9..4ec76825b50da6103fa25529f88754827d16013a 100644 --- a/cooperative_cuisine/__init__.py +++ b/cooperative_cuisine/__init__.py @@ -323,7 +323,7 @@ player_config: effect_manager: # fire effect ... -extra_setup_functions: # scores, recording, msgs, etc. +hook_callbacks: # scores, recording, msgs, etc. ... ``` diff --git a/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml index 69fefccee00483d1049d929548385a5fd6f17e89..e63620d0843c21a7f77e0dbf92538a5acc0c1779 100644 --- a/cooperative_cuisine/configs/environment_config.yaml +++ b/cooperative_cuisine/configs/environment_config.yaml @@ -98,114 +98,96 @@ effect_manager: fire_burns_ingredients_and_meals: true -extra_setup_functions: +hook_callbacks: # # --------------- Scoring --------------- orders: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ completed_order ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 20 - score_on_specific_kwarg: meal_name - score_map: - Burger: 15 - OnionSoup: 10 - Salad: 5 - TomatoSoup: 10 + hooks: [ completed_order ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 20 + score_on_specific_kwarg: meal_name + score_map: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 not_ordered_meals: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ serve_not_ordered_meal ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 2 + hooks: [ serve_not_ordered_meal ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 2 trashcan_usages: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ trashcan_usage ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: -5 + hooks: [ trashcan_usage ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: -5 expired_orders: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ order_expired ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: -10 - # # --------------- Recording --------------- + hooks: [ order_expired ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: -10 + # --------------- Recording --------------- # json_states: - # func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - # kwargs: - # hooks: [ json_state ] - # callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - # callback_class_kwargs: - # log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + # hooks: [ json_state ] + # callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + # callback_class_kwargs: + # record_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl actions: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ pre_perform_action ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + hooks: [ pre_perform_action ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl random_env_events: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ order_duration_sample, plate_out_of_kitchen_time ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl - add_hook_ref: true + hooks: [ order_duration_sample, plate_out_of_kitchen_time ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true env_configs: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ env_initialized, item_info_config ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl - add_hook_ref: true + hooks: [ env_initialized, item_info_config ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true # Game event recording game_events: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: - - post_counter_pick_up - - post_counter_drop_off - - post_dispenser_pick_up - - cutting_board_100 - - player_start_interaction - - player_end_interact - - post_serving - - no_serving - - dirty_plate_arrives - - trashcan_usage - - plate_cleaned - - added_plate_to_sink - - drop_on_sink_addon - - pick_up_from_sink_addon - - serve_not_ordered_meal - - serve_without_plate - - completed_order - - new_orders - - order_expired - - action_on_not_reachable_counter - - new_fire - - fire_spreading - - drop_off_on_cooking_equipment - - players_collide - - post_plate_dispenser_pick_up - - post_plate_dispenser_drop_off - - on_item_transition - - progress_started - - progress_finished - - content_ready + hooks: + - post_counter_pick_up + - post_counter_drop_off + - post_dispenser_pick_up + - cutting_board_100 + - player_start_interaction + - player_end_interact + - post_serving + - no_serving + - dirty_plate_arrives + - trashcan_usage + - plate_cleaned + - added_plate_to_sink + - drop_on_sink_addon + - pick_up_from_sink_addon + - serve_not_ordered_meal + - serve_without_plate + - completed_order + - new_orders + - order_expired + - action_on_not_reachable_counter + - new_fire + - fire_spreading + - drop_off_on_cooking_equipment + - players_collide + - post_plate_dispenser_pick_up + - post_plate_dispenser_drop_off + - on_item_transition + - progress_started + - progress_finished + - content_ready - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl - add_hook_ref: true + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true # info_msg: diff --git a/cooperative_cuisine/configs/study/level1/level1_config.yaml b/cooperative_cuisine/configs/study/level1/level1_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/cooperative_cuisine/configs/study/level2/level2_config.yaml b/cooperative_cuisine/configs/study/level2/level2_config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/cooperative_cuisine/configs/tutorial_env_config.yaml b/cooperative_cuisine/configs/tutorial_env_config.yaml index 86a1d0341679a6a64c32bf2ec9f9cc5c30adea15..15a9fc6dde4672cb7b0c40b9a875c8f20195ba50 100644 --- a/cooperative_cuisine/configs/tutorial_env_config.yaml +++ b/cooperative_cuisine/configs/tutorial_env_config.yaml @@ -94,4 +94,4 @@ effect_manager: fire_burns_ingredients_and_meals: true -extra_setup_functions: \ No newline at end of file +hook_callbacks: \ No newline at end of file diff --git a/cooperative_cuisine/counters.py b/cooperative_cuisine/counters.py index 01421b2137f238b8413eee3194a227c2c87e8576..cd84be94e982e3c6e49ecefd501dbe075be84237 100644 --- a/cooperative_cuisine/counters.py +++ b/cooperative_cuisine/counters.py @@ -684,10 +684,10 @@ class PlateDispenser(Counter): def progress(self, passed_time: timedelta, now: datetime): """Check if plates arrive from outside the kitchen and add a dirty plate accordingly""" - if self.next_plate_time < now: + if self.next_plate_time <= now: idx_delete = [] for i, times in enumerate(self.out_of_kitchen_timer): - if times < now: + if times <= now: self.hook(DIRTY_PLATE_ARRIVES, counter=self, times=times, now=now) idx_delete.append(i) log.debug("Add dirty plate") diff --git a/cooperative_cuisine/effects.py b/cooperative_cuisine/effects.py index 3688ce649ef10ddbddb1671709a75db41f5a1fe6..53989c0916dd9d8f2fe82b44ef2be4f971e1da29 100644 --- a/cooperative_cuisine/effects.py +++ b/cooperative_cuisine/effects.py @@ -163,9 +163,9 @@ class FireEffectManager(EffectManager): self.active_effects.append((effect, target)) # reset new effects self.new_effects = [] - if self.next_finished_timer < now: + if self.next_finished_timer <= now: for effect, target in self.active_effects: - if self.effect_to_timer[effect.uuid] < now: + if self.effect_to_timer[effect.uuid] <= now: if isinstance(target, Item): target = find_item_on_counters(target.uuid, self.counters) if target: diff --git a/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py index e898be1862752bfaa80ceb429256134dc710c621..25eff9e6734879d45dfad4ad6359c2b75fc1826f 100644 --- a/cooperative_cuisine/environment.py +++ b/cooperative_cuisine/environment.py @@ -51,6 +51,7 @@ from cooperative_cuisine.hooks import ( ACTION_INTERACT_START, ITEM_INFO_CONFIG, POST_STEP, + hooks_via_callback_class, ) from cooperative_cuisine.items import ( ItemInfo, @@ -90,8 +91,8 @@ class EnvironmentConfig(TypedDict): """Configuration about the player characters.""" layout_chars: dict[str, str] """Definition of which characters in the layout file correspond to which kitchen counter.""" - extra_setup_functions: dict[str, dict] - """Configuration of extra setup functions, for example hook behavior.""" + hook_callbacks: dict[str, dict] + """Configuration of callbacks via HookCallbackClass.""" effect_manager: dict """Config of different effects in the environment, which control for example fire behavior.""" @@ -184,7 +185,7 @@ class Environment: "player_config" ].view_range - self.extra_setup_functions() + self.hook_callbacks() self.layout_config: str = layout_config """The layout config for the environment""" @@ -570,16 +571,14 @@ class Environment: """ self.hook.register_callback(hook_ref, callback) - def extra_setup_functions(self): + def hook_callbacks(self): """Executes extra setup functions specified in the environment configuration.""" - if self.environment_config["extra_setup_functions"]: - for function_name, function_def in self.environment_config[ - "extra_setup_functions" + if self.environment_config["hook_callbacks"]: + for callback_name, setup_kwargs in self.environment_config[ + "hook_callbacks" ].items(): - log.info(f"Setup function {function_name}") - function_def["func"]( - name=function_name, env=self, **function_def["kwargs"] - ) + log.info(f"Setup hook callback {callback_name}") + hooks_via_callback_class(name=callback_name, env=self, **setup_kwargs) def increment_score(self, score: int | float, info: str = ""): """Add a value to the current score and log it.""" diff --git a/cooperative_cuisine/hooks.py b/cooperative_cuisine/hooks.py index 8c9cb70bc2a0c2dddfe22ecd096994bb170bd1f6..f16b10afe108fd2572ba801a01d0dca679882e21 100644 --- a/cooperative_cuisine/hooks.py +++ b/cooperative_cuisine/hooks.py @@ -2,7 +2,7 @@ You can add callbacks at specific points in the environment. This "hook" mechanism is defined here. -You can add hooks via the `environment_config` under `extra_setup_functions` and the here defined +You can add hooks via the `environment_config` under `hook_callbacks` and the here defined `hooks_via_callback_class` function. Each hook get different kwargs. But `env` with the environment object and `hook_ref` with the name of the hook are @@ -229,7 +229,7 @@ def hooks_via_callback_class( callback_class: Type[HookCallbackClass], callback_class_kwargs: dict[str, Any], ): - """Function to reference in the `environment_config.yml` to add functionality via hooks and a configured callback class. + """Setup hook callback class. Args: name: A string representing the name of the callback class instance. diff --git a/cooperative_cuisine/info_msg.py b/cooperative_cuisine/info_msg.py index 3d6d95257a0e138282e32b8666a72627f12cb974..d8077ed06d116579c0265644e30a29dd1a2fdf5d 100644 --- a/cooperative_cuisine/info_msg.py +++ b/cooperative_cuisine/info_msg.py @@ -1,22 +1,18 @@ """Based on hooks, text-based info msgs can be displayed. ```yaml - extra_setup_functions: + hook_callbacks: info_msg: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ cutting_board_100 ] - callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager '' - callback_class_kwargs: - msg: Glückwunsch du hast was geschnitten! - fire_msg: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ new_fire ] - callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager '' - callback_class_kwargs: - msg: Feuer, Feuer, Feuer - level: Warning + hooks: [ cutting_board_100 ] + callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager '' + callback_class_kwargs: + msg: Glückwunsch du hast was geschnitten! + fire_msg: + hooks: [ new_fire ] + callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager '' + callback_class_kwargs: + msg: Feuer, Feuer, Feuer + level: Warning ``` """ @@ -95,7 +91,7 @@ class InfoMsgManager(HookCallbackClass): for player_id, msgs in env.info_msgs_per_player.items(): delete_msgs = [] for idx, msg in enumerate(msgs): - if msg["end_time"] < env.env_time: + if msg["end_time"] <= env.env_time: delete_msgs.append(idx) for idx in reversed(delete_msgs): msgs.pop(idx) diff --git a/cooperative_cuisine/pygame_2d_vis/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py index f9a994625856c4ee3a2bee92e208c0df7b6a6d3c..0c67eaf1549e8e45510832cc5c9296591883715c 100644 --- a/cooperative_cuisine/pygame_2d_vis/drawing.py +++ b/cooperative_cuisine/pygame_2d_vis/drawing.py @@ -2,6 +2,7 @@ import argparse import colorsys import json import os +import sys from datetime import datetime, timedelta from pathlib import Path @@ -1002,7 +1003,21 @@ def generate_recipe_images(config: dict, folder_path: str | Path): pygame.image.save(screen, f"{folder_path}/{graph_dict['meal']}.png") -if __name__ == "__main__": +def main(args): + """ + + Runs the Cooperative Cuisine Image Generation process. + + This method takes command line arguments to specify the state file, visualization configuration file, and output file for the generated image. It then reads the visualization configuration + * file and state file, and calls the 'save_screenshot' and 'generate_recipe_images' methods to generate the image. + + Args: + -s, --state: A command line argument of type `argparse.FileType("r", encoding="UTF-8")`. Specifies the state file to use for image generation. If not provided, the default value is 'ROOT_DIR / "pygame_2d_vis" / "sample_state.json"'. + + -v, --visualization_config: A command line argument of type `argparse.FileType("r", encoding="UTF-8")`. Specifies the visualization configuration file to use for image generation. If not provided, the default value is 'ROOT_DIR / "pygame_2d_vis" / "visualization.yaml"'. + + -o, --output_file: A command line argument of type `str`. Specifies the output file path for the generated image. If not provided, the default value is 'ROOT_DIR / "generated" / "screenshot.jpg"'. + """ parser = argparse.ArgumentParser( prog="Cooperative Cuisine Image Generation", description="Generate images for a state in json.", @@ -1024,12 +1039,16 @@ if __name__ == "__main__": "-o", "--output_file", type=str, - default=ROOT_DIR / "pygame_2d_vis" / "generated" / "screenshot.jpg", + default=ROOT_DIR / "generated" / "screenshot.jpg", ) - args = parser.parse_args() + args = parser.parse_args(args) with open(args.visualization_config, "r") as f: viz_config = yaml.safe_load(f) with open(args.state, "r") as f: state = json.load(f) save_screenshot(state, viz_config, args.output_file) generate_recipe_images(viz_config, args.output_file.parent) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/cooperative_cuisine/pygame_2d_vis/video_replay.py b/cooperative_cuisine/pygame_2d_vis/video_replay.py index acb08cbaefafc6ad20e855f28eb6f7d274318956..46521e12524e1c0c2b2074cc5b1f68486d354364 100644 --- a/cooperative_cuisine/pygame_2d_vis/video_replay.py +++ b/cooperative_cuisine/pygame_2d_vis/video_replay.py @@ -72,14 +72,14 @@ def simulate( You can record the relevant files via hooks in the environment_config: ```yaml - extra_setup_functions + hook_callbacks env_configs: func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' kwargs: hooks: [ env_initialized, item_info_config ] callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl add_hook_ref: true actions: func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' @@ -87,7 +87,7 @@ def simulate( hooks: [ pre_perform_action ] callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl ``` You can call simulation function via the command line. For example by replacing the ENVIRONMENT_ID (Linux system) or the complete path: @@ -219,7 +219,7 @@ def from_json_states( hooks: [ json_state ] callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + record_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl ``` You can call this function via the command line: diff --git a/cooperative_cuisine/recording.py b/cooperative_cuisine/recording.py index b124a7f2a6ec5a149b5960b34dd46a38e355c907..a71cc0b3654b67f238798726e19c756a2f1f8111 100644 --- a/cooperative_cuisine/recording.py +++ b/cooperative_cuisine/recording.py @@ -7,42 +7,35 @@ https://pypi.org/project/platformdirs/) -> `user_log_dir`. - `ENV_NAME` to the name of the environment. ```yaml -extra_setup_functions: +hook_callbacks: json_states: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ json_state ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + hooks: [ json_state ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl actions: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ pre_perform_action ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + hooks: [ pre_perform_action ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl random_env_events: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ order_duration_sample, plate_out_of_kitchen_time ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl - add_hook_ref: true + hooks: [ order_duration_sample, plate_out_of_kitchen_time ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true env_configs: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ env_initialized, item_info_config ] - callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' - callback_class_kwargs: - log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl - add_hook_ref: true + hooks: [ env_initialized, item_info_config ] + callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' + callback_class_kwargs: + record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl + add_hook_ref: true ``` """ import json import logging import os +import sys import traceback from pathlib import Path from string import Template @@ -68,7 +61,7 @@ class FileRecorder(HookCallbackClass): self, name: str, env: Environment, - log_path: str = "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl", + record_path: str = "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl", add_hook_ref: bool = False, **kwargs, ): @@ -77,19 +70,19 @@ class FileRecorder(HookCallbackClass): Args: name (str): The name of the recorder. This name is used to replace the placeholder "LOG_RECORD_NAME" in the default log file path. env (Environment): The environment in which the recorder is being used. - log_path (str, optional): The path to the log file. Defaults to "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl". + record_path (str, optional): The path to the log file. Defaults to "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl". add_hook_ref (bool, optional): Indicates whether to add a hook reference to the recorded data. Defaults to False. **kwargs: Additional keyword arguments. """ super().__init__(name, env, **kwargs) self.add_hook_ref: bool = add_hook_ref """Indicates whether to add a hook reference to the recorded data. Default value is False.""" - log_path = log_path.replace("LOG_RECORD_NAME", name) - log_path = Path(expand_path(log_path, env_name=env.env_name)) - self.log_path: Path = log_path + record_path = record_path.replace("LOG_RECORD_NAME", name) + record_path = Path(expand_path(record_path, env_name=env.env_name)) + self.record_path: Path = record_path """The path to the log file. Default value is "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl".""" - log.info(f"Recorder record for {name} in file://{log_path}") - os.makedirs(log_path.parent, exist_ok=True) + log.info(f"Recorder record for {name} in file://{record_path}") + os.makedirs(record_path.parent, exist_ok=True) def __call__(self, hook_ref: str, env: Environment, **kwargs): for key, item in kwargs.items(): @@ -134,8 +127,8 @@ class FileRecorder(HookCallbackClass): ) + "\n" ) - with open(self.log_path, "a") as log_file: - log_file.write(record) + with open(self.record_path, "a") as record_file: + record_file.write(record) except TypeError as e: traceback.print_exception(e) log.info( @@ -151,7 +144,7 @@ def print_recorded_events_human_readable(jsonl_path: Path): """ - def stringify_item(item_): + def stringify_item(item_) -> str | None: if isinstance(item_, float): return str(item_) if isinstance(item_, str): @@ -226,7 +219,5 @@ def print_recorded_events_human_readable(jsonl_path: Path): if __name__ == "__main__": - json_lines_path: Path = Path( - "/home/fabian/.local/state/cooperative_cuisine/log/fcb095915c454446b9ee2905ff534610/game_events.jsonl" - ) + json_lines_path: Path = Path(sys.argv[0]) print_recorded_events_human_readable(json_lines_path) diff --git a/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml b/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml index 2eb50382df3b4a5ddae99e632707cae76bdcf7dc..1167ad76732ba1dcdde9506f73d94ee1b42cccf6 100644 --- a/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml +++ b/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml @@ -98,86 +98,64 @@ effect_manager: { } # fire_burns_ingredients_and_meals: true -extra_setup_functions: +hook_callbacks: # # --------------- Scoring --------------- orders: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ completed_order ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 0.95 + hooks: [ completed_order ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 0.95 serve_not_ordered_meals: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ serve_not_ordered_meal ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 0.95 + hooks: [ serve_not_ordered_meal ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 0.95 trashcan_usages: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ trashcan_usage ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: -0.2 + hooks: [ trashcan_usage ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: -0.2 item_cut: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ cutting_board_100 ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 0.1 + hooks: [ cutting_board_100 ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 0.1 stepped: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ post_step ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: -0.01 + hooks: [ post_step ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: -0.01 combine: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ drop_off_on_cooking_equipment ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 0.01 + hooks: [ drop_off_on_cooking_equipment ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 0.01 start_interact: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ player_start_interaction ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 0.01 - # json_states: - # func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - # kwargs: - # hooks: [ json_state ] - # log_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' - # log_class_kwargs: - # log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + hooks: [ player_start_interaction ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 0.01 +# json_states: +# hooks: [ json_state ] +# record_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' +# record_class_kwargs: +# record_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl # actions: -# func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' -# kwargs: -# hooks: [ pre_perform_action ] -# log_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' -# log_class_kwargs: -# log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl +# hooks: [ pre_perform_action ] +# record_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' +# record_class_kwargs: +# record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl # random_env_events: -# func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' -# kwargs: -# hooks: [ order_duration_sample, plate_out_of_kitchen_time ] -# log_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' -# log_class_kwargs: -# log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl -# add_hook_ref: true +# hooks: [ order_duration_sample, plate_out_of_kitchen_time ] +# record_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' +# record_class_kwargs: +# record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl +# add_hook_ref: true # env_configs: -# func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' -# kwargs: -# hooks: [ env_initialized, item_info_config ] -# log_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' -# log_class_kwargs: -# log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl -# add_hook_ref: true +# hooks: [ env_initialized, item_info_config ] +# record_class: !!python/name:cooperative_cuisine.recording.LogRecorder '' +# record_class_kwargs: +# record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl +# add_hook_ref: true diff --git a/cooperative_cuisine/scores.py b/cooperative_cuisine/scores.py index 5ae2c6ce83146b3d190f67e3e8236e3637a1b244..3bab87e92f29caec0903c3865c747757f3b0c188 100644 --- a/cooperative_cuisine/scores.py +++ b/cooperative_cuisine/scores.py @@ -1,5 +1,5 @@ """ -Scores are managed via hooks. You can add them in the `environment_config` under `extra_setup_functions`. +Scores are managed via hooks. You can add them in the `environment_config` under `hook_callbacks`. The here defined `ScoreViaHooks` is a `HookCallbackClass`. It allows you to define how the score is effected by specific hook events. @@ -12,41 +12,33 @@ You can: You can filter the events via `kwarg_filter`. ```yaml -extra_setup_functions: +hook_callbacks: orders: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ completed_order ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 20 - score_on_specific_kwarg: meal_name - score_map: - Burger: 15 - OnionSoup: 10 - Salad: 5 - TomatoSoup: 10 + hooks: [ completed_order ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 20 + score_on_specific_kwarg: meal_name + score_map: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 not_ordered_meals: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ serve_not_ordered_meal ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: 2 + hooks: [ serve_not_ordered_meal ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: 2 trashcan_usages: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ trashcan_usage ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: -5 + hooks: [ trashcan_usage ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: -5 expired_orders: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ order_expired ] - callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' - callback_class_kwargs: - static_score: -10 + hooks: [ order_expired ] + callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks '' + callback_class_kwargs: + static_score: -10 ``` @@ -94,13 +86,13 @@ class ScoreViaHooks(HookCallbackClass): **kwargs: Additional keyword arguments to be passed to the parent class. """ super().__init__(name, env, **kwargs) - self.score_map = score_map + self.score_map: dict[str, float] = score_map """Mapping of hook references to scores.""" - self.static_score = static_score + self.static_score: float = static_score """The static score to be added if no other conditions are met.""" - self.kwarg_filter = kwarg_filter + self.kwarg_filter: dict[str, Any] = kwarg_filter """Filtering condition for keyword arguments.""" - self.score_on_specific_kwarg = score_on_specific_kwarg + self.score_on_specific_kwarg: str = score_on_specific_kwarg """The specific keyword argument to score on.""" def __call__(self, hook_ref: str, env: Environment, **kwargs): @@ -114,7 +106,7 @@ class ScoreViaHooks(HookCallbackClass): self.env.increment_score(self.static_score, info=hook_ref) elif self.score_map and hook_ref in self.score_map: if self.kwarg_filter: - if kwargs.items() <= self.kwarg_filter.items(): + if self.kwarg_filter.items() <= kwargs.items(): self.env.increment_score( self.score_map[hook_ref], info=f"{hook_ref} - {self.kwarg_filter}", diff --git a/tests/test_game_server.py b/tests/test_game_server.py index f072fed2dd5413ad016e64406146262c93723e53..84271ad77196e85aef512ce9994e0d8926130a0e 100644 --- a/tests/test_game_server.py +++ b/tests/test_game_server.py @@ -282,3 +282,10 @@ def test_websocket_wrong_inputs(create_env_config): finally: task.cancel() loop.close() + + +def test_root(): + with TestClient(app) as client: + res = client.get("/") + assert res.status_code == status.HTTP_200_OK + assert res.json() == {"Cooperative": "Cuisine"} diff --git a/tests/test_pygame.py b/tests/test_pygame.py new file mode 100644 index 0000000000000000000000000000000000000000..9df43e0813184f20a159001c91684f149bc17fbb --- /dev/null +++ b/tests/test_pygame.py @@ -0,0 +1,40 @@ +import json +import os + +import yaml + +from cooperative_cuisine import ROOT_DIR +from cooperative_cuisine.pygame_2d_vis.drawing import calc_angle, Visualizer, main +from cooperative_cuisine.pygame_2d_vis.game_colors import RGB, WHITE, colors + + +def test_colors(): + assert RGB(red=255, green=255, blue=255).hex_format() == "#FFFFFF" + assert WHITE.hex_format() == "#FFFFFF" + assert len(colors) >= 552 + + +def test_calc_angle(): + assert calc_angle([0.0, 1.0], [1.0, 0.0]) == -90.0 + assert calc_angle([1.0, 1.0], [1.0, -1.0]) == -90.0 + + +def test_drawing(): + main([]) + assert os.path.exists(os.path.join(ROOT_DIR, "generated", "screenshot.jpg")) + + +def test_visualizer(): + with open(ROOT_DIR / "pygame_2d_vis" / "visualization.yaml", "r") as file: + visualization_config = yaml.safe_load(file) + + vis = Visualizer(visualization_config) + + vis.create_player_colors(10) + assert len(vis.player_colors) >= 10 + assert len(set(vis.player_colors)) >= 10 + + with open(ROOT_DIR / "pygame_2d_vis" / "sample_state.json", "r") as file: + state = json.load(file) + image = vis.get_state_image(40, state) + assert image.shape == (480, 360, 3) diff --git a/tests/test_start.py b/tests/test_start.py index fec31349ac50c39f3d02019c66f1a7f802389a33..784709530f86f1f44c89d9dfd83c4836265b66dd 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -1,17 +1,35 @@ +from collections import deque from datetime import timedelta import numpy as np import pytest +import yaml from cooperative_cuisine import ROOT_DIR from cooperative_cuisine.action import ActionType, InterActionData, Action -from cooperative_cuisine.counters import Counter, CuttingBoard +from cooperative_cuisine.counters import ( + Counter, + CuttingBoard, + CookingCounter, + ServingWindow, + Trashcan, + PlateDispenser, + Sink, +) +from cooperative_cuisine.effects import FireEffectManager from cooperative_cuisine.environment import ( Environment, ) from cooperative_cuisine.game_server import PlayerRequestType -from cooperative_cuisine.hooks import Hooks -from cooperative_cuisine.items import Item, ItemInfo, ItemType +from cooperative_cuisine.hooks import ( + Hooks, + SERVE_NOT_ORDERED_MEAL, + PLAYER_ADDED, + POST_STEP, +) +from cooperative_cuisine.info_msg import InfoMsgManager +from cooperative_cuisine.items import Item, ItemInfo, ItemType, Plate, CookingEquipment +from cooperative_cuisine.scores import ScoreViaHooks from cooperative_cuisine.server_results import ( PlayerInfo, CreateEnvResult, @@ -21,7 +39,7 @@ from cooperative_cuisine.state_representation import ( StateRepresentation, create_json_schema, ) -from cooperative_cuisine.utils import create_init_env_time +from cooperative_cuisine.utils import create_init_env_time, get_touching_counters layouts_folder = ROOT_DIR / "configs" / "layouts" environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml" @@ -54,7 +72,6 @@ def layout_config(): with open(layout_path, "r") as file: layout = file.read() return layout - env.add_player("0") @pytest.fixture @@ -218,6 +235,7 @@ def test_processing(env_config, layout_config, item_info): }, ) env.counters.append(counter) + env.overwrite_counters(env.counters) tomato = Item(name="Tomato", item_info=None) env.add_player("1", np.array([2, 3])) @@ -308,3 +326,354 @@ def test_server_result_definition(): msg="123", player_hash="1234324", ) + + +def test_fire(env_config, layout_config, item_info): + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + oven = None + for c in env.counters: + if ( + isinstance(c, CookingCounter) + and c.name == "Stove" + and c.occupied_by.name == "Pan" + ): + oven = c + break + assert oven is not None + + raw_patty = Item(name="RawPatty", item_info=env.item_info["RawPatty"]) + + assert oven.can_drop_off(raw_patty) + + oven.drop_off(raw_patty, "0") + assert isinstance(oven.occupied_by, CookingEquipment) + assert oven.occupied_by.content_list == [raw_patty] + + env.step(timedelta(seconds=env.item_info["CookedPatty"].seconds)) + assert oven.occupied_by.content_list[0].name == "CookedPatty" + env.step(timedelta(seconds=env.item_info["BurntCookedPatty"].seconds)) + assert oven.occupied_by.content_list[0].name == "BurntCookedPatty" + env.step(timedelta(seconds=env.item_info["Fire"].seconds)) + assert len(oven.occupied_by.active_effects) != 0 + assert oven.occupied_by.active_effects[0].name == "Fire" + + fire_manager = env.effect_manager["FireManager"] + assert isinstance(fire_manager, FireEffectManager) + env.step(fire_manager.next_finished_timer - env.env_time) + + touching_counters = get_touching_counters(oven, env.counters) + next_empty = None + connect_counter = None + for c in touching_counters: + if c.occupied_by: + assert len(c.occupied_by.active_effects) == 1 + else: + assert len(c.active_effects) == 1 + next_touching = get_touching_counters(c, env.counters) + for a in next_touching: + if a not in touching_counters and a.__class__.__name__ == "Counter": + a.occupied_by = None + next_empty = a + connect_counter = c + env.step(timedelta(seconds=0.01)) + assert next_empty is not None + next_empty.occupied_by = Item(name="Tomato", item_info=env.item_info["Tomato"]) + env.step( + fire_manager.effect_to_timer[connect_counter.active_effects[0].uuid] + - env.env_time + ) + assert len(next_empty.occupied_by.active_effects) == 1 + + fire_extinguisher = None + for c in env.counters: + if c.occupied_by and c.occupied_by.name == "Extinguisher": + fire_extinguisher = c.occupied_by + c.occupied_by = None + break + + assert fire_extinguisher is not None + env.players["0"].holding = fire_extinguisher + env.players["0"].pos = oven.pos + env.players["0"].pos[1] += 1.0 + env.perform_action( + Action( + player="0", + action_type=ActionType.MOVEMENT, + action_data=np.array([0.0, -1.0]), + duration=0.1, + ) + ) + env.step(timedelta(seconds=0.1)) + + env.perform_action( + Action( + player="0", + action_type=ActionType.INTERACT, + action_data=InterActionData.START, + ) + ) + env.step(timedelta(seconds=env.item_info["Extinguisher"].seconds)) + env.step(timedelta(seconds=env.item_info["Extinguisher"].seconds)) + + assert len(oven.occupied_by.active_effects) == 0 + + +def test_score(env_config, layout_config, item_info): + def incr_score_callback(hook_ref, env: Environment, meal, meal_name, **kwargs): + assert isinstance(meal, Item) + assert isinstance(meal_name, str) + assert meal_name == "TomatoSoup" + env.increment_score(1_000, "Soup Soup") + + env = Environment(env_config, layout_config, item_info, as_files=False) + assert env.score == 0.0 + env.add_player("0") + env.register_callback_for_hook(SERVE_NOT_ORDERED_MEAL, incr_score_callback) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks( + name="123", + env=env, + score_on_specific_kwarg="meal_name", + score_map={"TomatoSoup": 2_000}, + ), + ) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks(name="124", env=env, score_map={SERVE_NOT_ORDERED_MEAL: 4_000}), + ) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks( + name="124", + env=env, + score_map={SERVE_NOT_ORDERED_MEAL: 8_000}, + kwarg_filter={"meal_name": "TomatoSoup"}, + ), + ) + env.register_callback_for_hook( + SERVE_NOT_ORDERED_MEAL, + ScoreViaHooks( + name="123", + env=env, + score_on_specific_kwarg="meal_name", + static_score=16_000, + score_map={}, + ), + ) + serving_window = None + for c in env.counters: + if isinstance(c, ServingWindow): + serving_window = c + break + assert serving_window is not None + env.order_manager.serving_not_ordered_meals = True + env.order_manager.open_orders = [] + plate = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=True, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + plate.content_list = [ + Item(name="TomatoSoup", item_info=env.item_info["TomatoSoup"]) + ] + assert serving_window.can_drop_off(plate) + + returned = serving_window.drop_off(plate, "0") + assert returned is None + + plates_prev = len(serving_window.plate_dispenser.occupied_by) + assert env.score >= 31_000 + assert len(serving_window.plate_dispenser.out_of_kitchen_timer) == 1 + env.step(serving_window.plate_dispenser.out_of_kitchen_timer[0] - env.env_time) + assert len(serving_window.plate_dispenser.out_of_kitchen_timer) == 0 + assert len(serving_window.plate_dispenser.occupied_by) == plates_prev + 1 + assert ( + serving_window.plate_dispenser.occupied_by[0].clean + != serving_window.plate_dispenser.plate_config.return_dirty + ) + + +def test_info_msgs(env_config, layout_config, item_info): + env_config_dict = yaml.load(env_config, Loader=yaml.Loader) + # TODO change after merge with 115 + env_config_dict["hook_callbacks"]["dummy_msg"] = { + "hooks": [PLAYER_ADDED], + "callback_class": InfoMsgManager, + "callback_class_kwargs": {"msg": "hello there"}, + } + + env_config_dict["hook_callbacks"]["dummy_msg_2"] = { + "hooks": [POST_STEP], + "callback_class": InfoMsgManager, + "callback_class_kwargs": {"msg": "step step"}, + } + + env_config = yaml.dump(env_config_dict) + + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + assert env.info_msgs_per_player["0"][0]["msg"] == "hello there" + env.step(timedelta(seconds=0.1)) + assert env.info_msgs_per_player["0"][1]["msg"] == "step step" + env.step(env.info_msgs_per_player["0"][0]["end_time"] - env.env_time) + assert len(env.info_msgs_per_player["0"]) == 2 + assert env.info_msgs_per_player["0"][0]["msg"] == "step step" + + +def test_trashcan(env_config, layout_config, item_info): + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + trash = None + for c in env.counters: + if isinstance(c, Trashcan): + trash = c + break + assert trash is not None + + item = Item(name="Tomato", item_info=env.item_info["Tomato"]) + assert trash.can_drop_off(item) + assert trash.drop_off(item, "0") is None + + plate = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=True, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + plate.content_list = [ + Item(name="TomatoSoup", item_info=env.item_info["TomatoSoup"]) + ] + + assert trash.can_drop_off(plate) + assert trash.drop_off(plate, "0") == plate + assert plate.content_list == [] + + assert trash.pick_up(True, "0") is None + + +def test_plate_dispenser(env_config, layout_config, item_info): + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + plate_dis = None + for c in env.counters: + if isinstance(c, PlateDispenser): + plate_dis = c + break + + assert plate_dis is not None + assert ( + len(plate_dis.occupied_by) > 0 + ) # Otherwise adapt env_config above before passing to Environment + n_plates = len(plate_dis.occupied_by) + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + item = Item(name="ChoppedTomato", item_info=env.item_info["ChoppedTomato"]) + assert plate_dis.can_drop_off(item) + returned = plate_dis.drop_off(item, "0") + assert returned is None + + assert len(plate_dis.occupied_by) == n_plates + + first_plate = plate_dis.pick_up(True, "0") + assert isinstance(first_plate, Plate) + assert ( + first_plate.content_list and first_plate.content_list[0].name == "ChoppedTomato" + ) + + assert plate_dis.can_drop_off(first_plate) + returned = plate_dis.drop_off(first_plate, "0") + assert isinstance(returned, Plate) and len(returned.content_list) == 0 + assert plate_dis.occupied_by[-1].content_list[0].name == "ChoppedTomato" + + plate_dis.occupied_by = deque() + assert plate_dis.can_drop_off(item) + returned = plate_dis.drop_off(item, "0") + assert returned is None + assert plate_dis.occupied_by[0].name == "ChoppedTomato" + + +def test_sink(env_config, layout_config, item_info): + env = Environment(env_config, layout_config, item_info, as_files=False) + env.add_player("0") + sink = None + for c in env.counters: + if isinstance(c, Sink): + sink = c + break + assert sink is not None + env.players["0"].pos = sink.pos + env.players["0"].pos[1] -= 1.0 + env.perform_action( + Action( + player="0", + action_type=ActionType.MOVEMENT, + action_data=np.array([0.0, 1.0]), + duration=0.1, + ) + ) + env.step(timedelta(seconds=0.1)) + + plate = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=False, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + plate_2 = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=False, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + clean_plate = Plate( + transitions=env.counter_factory.filter_item_info( + by_item_type=ItemType.Meal, add_effects=True + ), + clean=True, + item_info=env.item_info["Plate"], + hook=env.hook, + ) + + assert sink.can_drop_off(plate) + assert sink.drop_off(plate, "0") is None + assert sink.can_drop_off(plate_2) + assert sink.drop_off(plate_2, "0") is None + assert not sink.can_drop_off(clean_plate) + + assert len(sink.occupied_by) == 2 + + env.perform_action( + Action( + player="0", + action_type=ActionType.INTERACT, + action_data=InterActionData.START, + ) + ) + env.step(timedelta(seconds=env.item_info["Plate"].seconds)) + assert len(sink.occupied_by) == 1 + assert len(sink.sink_addon.occupied_by) == 1 + assert sink.sink_addon.occupied_by[0].clean + assert not sink.occupied_by[0].clean + assert sink.pick_up(True, "0") is None + env.step(timedelta(seconds=env.item_info["Plate"].seconds)) + assert len(sink.occupied_by) == 0 + assert len(sink.sink_addon.occupied_by) == 2 + assert sink.sink_addon.occupied_by[0].clean + assert sink.sink_addon.occupied_by[1].clean + + item = Item(name="ChoppedTomato", item_info=env.item_info["ChoppedTomato"]) + assert sink.sink_addon.can_drop_off(item) + assert sink.sink_addon.drop_off(item, "0") is None + assert sink.sink_addon.pick_up(True, "0").content_list[0].name == "ChoppedTomato" + assert len(sink.sink_addon.occupied_by) == 1 diff --git a/tests/test_study_server.py b/tests/test_study_server.py index 0d979633495d970274dad50e005dfa61f4513fcc..20776e60b190ee93d4bceec0c89e38c35deb531e 100644 --- a/tests/test_study_server.py +++ b/tests/test_study_server.py @@ -92,4 +92,40 @@ def test_game_server_crashed(): assert res.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR +def test_tutorial(): + test_response = Response() + test_response.status_code = status.HTTP_200_OK + test_response.encoding = "utf8" + test_response._content = json.dumps( + { + "player_info": { + "0": { + "player_id": "0", + "client_id": "ksjdhfkjsdfn", + "player_hash": "shdfbmsndfb", + } + }, + "env_id": "123456789", + "recipe_graphs": [], + } + ).encode() + with mock.patch.object( + study_server_module, "request_game_server", return_value=test_response + ) as mock_call: + with TestClient(app) as client: + res = client.post("/connect_to_tutorial/124") + + assert res.status_code == status.HTTP_200_OK + + mock_call.assert_called_once() + + with mock.patch.object( + study_server_module, "request_game_server", return_value=test_response + ) as mock_call: + with TestClient(app) as client: + res = client.post("/disconnect_from_tutorial/124") + + assert res.status_code == status.HTTP_200_OK + + # TOOD test bots