diff --git a/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml index a6c7c752d93e7e593c1a913ce32291f7495ac0fe..eec7ad500072538f9b44c0fa2b470500410182a5 100644 --- a/cooperative_cuisine/configs/environment_config.yaml +++ b/cooperative_cuisine/configs/environment_config.yaml @@ -1,6 +1,6 @@ plates: clean_plates: 2 - dirty_plates: 0 + dirty_plates: 1 plate_delay: [ 5, 10 ] # range of seconds until the dirty plate arrives. @@ -53,9 +53,10 @@ orders: # if all: false -> only orders for these meals are generated # TODO: what if this list is empty? list: - - TomatoSoup - - OnionSoup - - Salad + # - TomatoSoup + # - OnionSoup + # - Salad + - FriedFish order_gen_class: !!python/name:cooperative_cuisine.orders.RandomOrderGeneration '' # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py order_gen_kwargs: @@ -83,7 +84,7 @@ orders: player_config: radius: 0.4 - speed_units_per_seconds: 6 + speed_units_per_seconds: 10 interaction_range: 1.6 restricted_view: False view_angle: 70 @@ -172,18 +173,15 @@ extra_setup_functions: hooks: - post_counter_pick_up - post_counter_drop_off - - dispenser_pick_up + - post_dispenser_pick_up - cutting_board_100 - - cutting_board_start_interaction - - cutting_board_end_interact - - pre_serving + - player_start_interaction + - player_end_interact - post_serving - no_serving - dirty_plate_arrives - trashcan_usage - plate_cleaned - - sink_start_interact - - sink_end_interact - added_plate_to_sink - drop_on_sink_addon - pick_up_from_sink_addon @@ -197,18 +195,19 @@ extra_setup_functions: - 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 - empty_info_msg: - func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' - kwargs: - hooks: [ action_put ] - callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager '' - callback_class_kwargs: - msg: "" + # info_msg: # func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' # kwargs: diff --git a/cooperative_cuisine/configs/human_readable_print_templates.yaml b/cooperative_cuisine/configs/human_readable_print_templates.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b190f6bccf1f0a3a3c580a8e37dd4c5f05e56365 --- /dev/null +++ b/cooperative_cuisine/configs/human_readable_print_templates.yaml @@ -0,0 +1,27 @@ +post_dispenser_pick_up: "Player $player picked up $return_this from the $counter." +post_counter_pick_up: "Player $player picked $return_this up from $counter." +post_counter_drop_off: "Player $player dropped $item off on $counter." +cutting_board_100: "Player $player finished chopping at $counter." +player_start_interaction: "Player $player started interacting with $counter." +player_end_interact: "Player $player stopped interacting with $counter." +post_serving: "Item $item was served at $counter." +dirty_plate_arrives: "A plate returned to $counter." +trashcan_usage: "Player $player threw $item in $counter." +plate_cleaned: "Player $player cleaned a plate at $counter." +added_plate_to_sink: "Player $player put $item in $counter." +drop_on_sink_addon: "Player $player put $item on $counter." +pick_up_from_sink_addon: "Player $player picked up $occupied_by from $counter." +serve_not_ordered_meal: "Meal $meal was served but it was not ordered." +completed_order: "Order $order was completed." +new_orders: "Orders $new_orders were ordered." +order_expired: "Order $order expired." +action_on_not_reachable_counter: "Action $action was performed yet nearest counter $counter was not reachable." +new_fire: "A fire broke out at $target." +fire_spreading: "A fire spread to target." +drop_off_on_cooking_equipment: "Player $player put $item in/on $equipment at $counter." +post_plate_dispenser_pick_up: "Player $player picked up $returned_item from $counter." +post_plate_dispenser_drop_off: "Player $player dropped $item on $counter." +on_item_transition: "$item became $result." +progress_started: "Item $item started progressing." +progress_finished: "Item $item finished its progress." +content_ready: "Meal $result was created on $before." diff --git a/cooperative_cuisine/configs/item_info_debug.yaml b/cooperative_cuisine/configs/item_info_debug.yaml index fd871d1d5685df542317bf772b67f3a3fd8ed7d8..f7783818b44059e9a004f627c60b433b6ac2e01a 100644 --- a/cooperative_cuisine/configs/item_info_debug.yaml +++ b/cooperative_cuisine/configs/item_info_debug.yaml @@ -182,45 +182,45 @@ Pizza: BurntCookedPatty: type: Waste - seconds: 5.0 + seconds: 15.0 needs: [ CookedPatty ] equipment: Pan BurntChips: type: Waste - seconds: 1.0 + seconds: 15.0 needs: [ Chips ] equipment: Basket BurntFriedFish: type: Waste - seconds: 5.0 + seconds: 15.0 needs: [ FriedFish ] equipment: Basket BurntTomatoSoup: type: Waste needs: [ TomatoSoup ] - seconds: 6.0 + seconds: 15.0 equipment: Pot BurntOnionSoup: type: Waste needs: [ OnionSoup ] - seconds: 6.0 + seconds: 15.0 equipment: Pot BurntPizza: type: Waste needs: [ Pizza ] - seconds: 7.0 + seconds: 15.0 equipment: Peel # -------------------------------------------------------------------------------- Fire: type: Effect - seconds: 1.0 + seconds: 20.0 needs: [ BurntCookedPatty, BurntChips, BurntFriedFish, BurntTomatoSoup, BurntOnionSoup, BurntPizza ] manager: FireManager effect_type: Unusable diff --git a/cooperative_cuisine/configs/study/level1/level1_config.yaml b/cooperative_cuisine/configs/study/level1/level1_config.yaml index a0d0fe8b701b1efb88436089eee643806876c918..94641a0a3e971a8b54115fdd473b7be45f2eb0ef 100644 --- a/cooperative_cuisine/configs/study/level1/level1_config.yaml +++ b/cooperative_cuisine/configs/study/level1/level1_config.yaml @@ -142,6 +142,48 @@ extra_setup_functions: # callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder '' # callback_class_kwargs: # log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl + + # 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 + + 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 + actions: func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' kwargs: diff --git a/cooperative_cuisine/configs/study/level2/level2_config.yaml b/cooperative_cuisine/configs/study/level2/level2_config.yaml index 6ebb7fb3cd2d767dec79ab32a6d214ad95d08ee9..e47528aa0dc770891e930474be3da5d14ad63dd6 100644 --- a/cooperative_cuisine/configs/study/level2/level2_config.yaml +++ b/cooperative_cuisine/configs/study/level2/level2_config.yaml @@ -164,6 +164,48 @@ extra_setup_functions: callback_class_kwargs: log_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 + + 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 + # info_msg: # func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class '' # kwargs: diff --git a/cooperative_cuisine/configs/study/study_config.yaml b/cooperative_cuisine/configs/study/study_config.yaml index 38158fdc1c43cd23e4b4b8b2fb548a691903eb96..28c420aa32e0b7b2228b7e16815dd77f3cf35633 100644 --- a/cooperative_cuisine/configs/study/study_config.yaml +++ b/cooperative_cuisine/configs/study/study_config.yaml @@ -8,7 +8,7 @@ levels: # layout_path: LAYOUTS_DIR/basic.layout # item_info_path: CONFIGS_DIR/item_info.yaml # name: "Basic" - # + - 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 diff --git a/cooperative_cuisine/counter_factory.py b/cooperative_cuisine/counter_factory.py index d991daecc6149c8cc32ff714c25fce31a57832a6..ba8ce4b9870c43c9a9a67c0de15b176a56c4ea4a 100644 --- a/cooperative_cuisine/counter_factory.py +++ b/cooperative_cuisine/counter_factory.py @@ -216,6 +216,7 @@ class CounterFactory: by_equipment_name=item_info.name, add_effects=True, ), + hook=self.hook, ), hook=self.hook, ) @@ -246,6 +247,7 @@ class CounterFactory: ), clean=True, item_info=self.item_info[Plate.__name__], + hook=self.hook, ), ) kwargs = { diff --git a/cooperative_cuisine/counters.py b/cooperative_cuisine/counters.py index a4ea2bba8d4681c349a9575af2e69338dffa71c4..991074a33ca8b97c432d6be23cfcbb0db48b1644 100644 --- a/cooperative_cuisine/counters.py +++ b/cooperative_cuisine/counters.py @@ -64,6 +64,10 @@ from cooperative_cuisine.hooks import ( DROP_OFF_ON_COOKING_EQUIPMENT, POST_COUNTER_DROP_OFF, PRE_COUNTER_DROP_OFF, + POST_PLATE_DISPENSER_DROP_OFF, + PRE_PLATE_DISPENSER_DROP_OFF, + PRE_PLATE_DISPENSER_PICK_UP, + POST_PLATE_DISPENSER_PICK_UP, ) from cooperative_cuisine.state_representation import CounterState @@ -224,10 +228,9 @@ class Counter: self.occupied_by = item self.hook( POST_COUNTER_DROP_OFF, - item=item, - equipment=self.occupied_by, counter=self, player=player, + item=item, ) elif self.occupied_by.can_combine(item): self.hook( @@ -295,13 +298,14 @@ class Counter: return False def do_hand_free_interaction( - self, passed_time: timedelta, now: datetime, player_name: str + self, passed_time: timedelta, now: datetime, player: str ): """Called by environment step function for time progression. Args: passed_time: the time passed since the last progress call now: the current env time. **Not the same as `datetime.now`**. + player: Name of the player doing the interaction. """ ... @@ -348,13 +352,14 @@ class CuttingBoard(Counter): super().__init__(**kwargs) def do_hand_free_interaction( - self, passed_time: timedelta, now: datetime, player_name: str + self, passed_time: timedelta, now: datetime, player: str ): """Called by environment step function for time progression. Args: passed_time: the time passed since the last progress call now: the current env time. **Not the same as `datetime.now`**. + player: Name of the player doing the interaction. Checks if the item on the board is in the allowed transitions via a Cutting board. Pass the progress call to the item on the board. If the progress on the item reaches 100% it changes the name of the item based on the @@ -390,7 +395,7 @@ class CuttingBoard(Counter): self.occupied_by.name = self.inverted_transition_dict[ self.occupied_by.name ].name - self.hook(CUTTING_BOARD_100, counter=self, player_name=player_name) + self.hook(CUTTING_BOARD_100, counter=self, player=player) class ServingWindow(Counter): @@ -548,7 +553,8 @@ class PlateDispenser(Counter): """At the moment, one and only one plate dispenser must exist in an environment, because only at one place the dirty plates should arrive. - How many plates should exist at the start of the level on the plate dispenser is defined in the `environment_config.yml`: + How many plates should exist at the start of the level on the + plate dispenser is defined in the `environment_config.yml`: ```yaml plates: clean_plates: 1 @@ -587,18 +593,55 @@ class PlateDispenser(Counter): """Random instance.""" self.setup_plates() + """ + PRE_PLATE_DISPENSER_PICK_UP = "pre_plate_dispenser_pick_up" + POST_PLATE_DISPENSER_PICK_UP = "post_plate_dispenser_pick_up" + + """ + def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None: + self.hook( + PRE_PLATE_DISPENSER_PICK_UP, + counter=self, + player=player, + ) if self.occupied_by: - return self.occupied_by.pop() + returned_item = self.occupied_by.pop() + self.hook( + POST_PLATE_DISPENSER_PICK_UP, + counter=self, + player=player, + returned_item=returned_item, + ) + return returned_item def can_drop_off(self, item: Item) -> bool: return not self.occupied_by or self.occupied_by[-1].can_combine(item) def drop_off(self, item: Item, player: str = "0") -> Item | None: + self.hook( + PRE_PLATE_DISPENSER_DROP_OFF, + counter=self, + player=player, + item=item, + ) if not self.occupied_by: self.occupied_by.append(item) + self.hook( + POST_PLATE_DISPENSER_DROP_OFF, + counter=self, + player=player, + item=item, + ) elif self.occupied_by[-1].can_combine(item): - return self.occupied_by[-1].combine(item) + returned_item = self.occupied_by[-1].combine(item) + self.hook( + POST_PLATE_DISPENSER_DROP_OFF, + counter=self, + player=player, + item=returned_item, + ) + return returned_item return None def add_dirty_plate(self): @@ -656,9 +699,6 @@ class PlateDispenser(Counter): else datetime.max ) - def __repr__(self): - return "PlateReturn" - def create_item(self, clean: bool = False) -> Plate: """Create a plate. @@ -669,6 +709,7 @@ class PlateDispenser(Counter): "clean": clean, "transitions": self.plate_transitions, "item_info": self.dispensing, + "hook": self.hook } return Plate(**kwargs) @@ -693,6 +734,7 @@ class Trashcan(Counter): ): return item if isinstance(item, CookingEquipment): + self.hook(TRASHCAN_USAGE, counter=self, item=item, player=player) item.reset_content() item.reset() return item @@ -797,7 +839,7 @@ class Sink(Counter): return len(self.occupied_by) != 0 def do_hand_free_interaction( - self, passed_time: timedelta, now: datetime, player_name: str + self, passed_time: timedelta, now: datetime, player: str ): if ( self.occupied @@ -829,7 +871,7 @@ class Sink(Counter): equipment=self.__class__.__name__, percent=percent ) if self.occupied_by[-1].progress_percentage == 1.0: - self.hook(PLATE_CLEANED, counter=self, player_name=player_name) + self.hook(PLATE_CLEANED, counter=self, player=player) self.occupied_by[-1].reset() self.occupied_by[-1].name = name plate = self.occupied_by.pop() @@ -880,5 +922,10 @@ class SinkAddon(Counter): def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None: if self.occupied_by: - self.hook(PICK_UP_FROM_SINK_ADDON, player=player) + self.hook( + PICK_UP_FROM_SINK_ADDON, + player=player, + occupied_by=self.occupied_by[-1], + counter=self, + ) return self.occupied_by.pop() diff --git a/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py index 3b28261852ba9ad268963413cdbe160e47224ad9..57c957ae8b154a59594c3b06a43b1a89e3670988 100644 --- a/cooperative_cuisine/environment.py +++ b/cooperative_cuisine/environment.py @@ -418,6 +418,7 @@ class Environment: else {} ) ), + hook=self.hook, pos=pos, ) self.players[player.name] = player @@ -437,7 +438,7 @@ class Environment: player.update_facing_point() self.movement.set_collision_arrays(len(self.players)) - self.hook(PLAYER_ADDED, player_name=player_name, pos=pos) + self.hook(PLAYER_ADDED, player=player, pos=pos) def step(self, passed_time: timedelta): """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders @@ -543,8 +544,10 @@ class Environment: """Registers a callback function for a given hook reference. Args: - hook_ref (str | list[str]): The reference to the hook or hooks for which the callback should be registered. It can be a single string or a list of strings. - callback (Callable): The callback function to be registered for the specified hook(s). The function should accept the necessary parameters and perform the desired actions. + hook_ref (str | list[str]): The reference to the hook or hooks for which the callback should be registered. + It can be a single string or a list of strings. + callback (Callable): The callback function to be registered for the specified hook(s). + The function should accept the necessary parameters and perform the desired actions. """ self.hook.register_callback(hook_ref, callback) diff --git a/cooperative_cuisine/game_server.py b/cooperative_cuisine/game_server.py index 9f4271d6f083a6c9170aa9a44386552be16b5ddf..093f30761eff6754c549a311af809e9dbc56f1a2 100644 --- a/cooperative_cuisine/game_server.py +++ b/cooperative_cuisine/game_server.py @@ -93,7 +93,7 @@ class EnvironmentStatus(Enum): RUNNING = "running" """The environment is running.""" STOPPED = "stopped" - """The environement is stopped.""" + """The environment is stopped.""" @dataclasses.dataclass @@ -322,7 +322,7 @@ class EnvironmentHandler: Args: manager_id (str): The ID of the manager that manages the environment. env_id (str): The ID of the environment. - reason (str): The reason for unpausing the environment. + reason (str): The reason for un-pausing the environment. """ if ( manager_id in self.manager_envs @@ -521,7 +521,8 @@ class EnvironmentHandler: def is_known_client_id(self, client_id: str) -> bool: """Check if a client ID is known. - Client IDs are generated by the server for players to connect to a websocket. Therefore, unknown IDs are ignored. + Client IDs are generated by the server for players to connect to a websocket. + Therefore, unknown IDs are ignored. Args: client_id (str): The client ID to be checked. @@ -535,7 +536,8 @@ class EnvironmentHandler: """Pass an action of a player to the environment. Args: - player_hash (str): The hash that allows access to the player data (should only know the player client and not other players). + player_hash (str): The hash that allows access to the player data + (should only know the player client and not other players). action (Action): The action to be performed. Returns: @@ -783,7 +785,7 @@ async def stop_env(manage_env: ManageEnv) -> str: @app.websocket("/ws/player/{client_id}") async def websocket_player_endpoint(websocket: WebSocket, client_id: str): - """The method that recives messages from the websocket of a player and sends the results back to the client. + """The method that receives messages from the websocket of a player and sends the results back to the client. Args: websocket (WebSocket): The WebSocket connection object. @@ -826,7 +828,8 @@ if __name__ == "__main__": parser = argparse.ArgumentParser( prog="Cooperative Cuisine Game Server", description="Game Engine Server: Starts overcooked game engine server.", - epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/cooperative_cuisine.html", + epilog="For further information, see " + "https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/cooperative_cuisine.html", ) url_and_port_arguments(parser) diff --git a/cooperative_cuisine/hooks.py b/cooperative_cuisine/hooks.py index 1e44e08e1abee3e21d38229ab78afdfd9d19efea..2c9f4900b25c67b40b94453291e2589a0c99da34 100644 --- a/cooperative_cuisine/hooks.py +++ b/cooperative_cuisine/hooks.py @@ -43,7 +43,7 @@ POST_PERFORM_ACTION = "post_perform_action" # TODO Pre and Post Perform Movement PLAYER_ADDED = "player_added" -"""Called after a player has been added. Kwargs: `player_name` and `pos`.""" +"""Called after a player has been added. Kwargs: `player` and `pos`.""" GAME_ENDED_STEP = "game_ended_step" @@ -64,71 +64,65 @@ POST_RESET_ENV_TIME = "post_reset_env_time" PRE_COUNTER_PICK_UP = "pre_counter_pick_up" POST_COUNTER_PICK_UP = "post_counter_pick_up" - PRE_COUNTER_DROP_OFF = "pre_counter_drop_off" POST_COUNTER_DROP_OFF = "post_counter_drop_off" -PRE_DISPENSER_PICK_UP = "dispenser_pick_up" -POST_DISPENSER_PICK_UP = "dispenser_pick_up" +PRE_DISPENSER_PICK_UP = "pre_dispenser_pick_up" +POST_DISPENSER_PICK_UP = "post_dispenser_pick_up" + +DROP_OFF_ON_COOKING_EQUIPMENT = "drop_off_on_cooking_equipment" + +PRE_PLATE_DISPENSER_PICK_UP = "pre_plate_dispenser_pick_up" +POST_PLATE_DISPENSER_PICK_UP = "post_plate_dispenser_pick_up" +PRE_PLATE_DISPENSER_DROP_OFF = "pre_plate_dispenser_drop_off" +POST_PLATE_DISPENSER_DROP_OFF = "post_plate_dispenser_drop_off" CUTTING_BOARD_PROGRESS = "cutting_board_progress" CUTTING_BOARD_100 = "cutting_board_100" -CUTTING_BOARD_START_INTERACT = "cutting_board_start_interaction" -CUTTING_BOARD_END_INTERACT = "cutting_board_end_interact" +PROGRESS_STARTED = "progress_started" +PROGRESS_FINISHED = "progress_finished" PRE_SERVING = "pre_serving" POST_SERVING = "post_serving" NO_SERVING = "no_serving" -# TODO drop off - PLATE_OUT_OF_KITCHEN_TIME = "plate_out_of_kitchen_time" - DIRTY_PLATE_ARRIVES = "dirty_plate_arrives" TRASHCAN_USAGE = "trashcan_usage" -PLATE_CLEANED = "plate_cleaned" - -SINK_START_INTERACT = "sink_start_interact" - -SINK_END_INTERACT = "sink_end_interact" - ADDED_PLATE_TO_SINK = "added_plate_to_sink" - DROP_ON_SINK_ADDON = "drop_on_sink_addon" - PICK_UP_FROM_SINK_ADDON = "pick_up_from_sink_addon" +PLATE_CLEANED = "plate_cleaned" SERVE_NOT_ORDERED_MEAL = "serve_not_ordered_meal" - SERVE_WITHOUT_PLATE = "serve_without_plate" ORDER_DURATION_SAMPLE = "order_duration_sample" - COMPLETED_ORDER = "completed_order" - INIT_ORDERS = "init_orders" - NEW_ORDERS = "new_orders" - ORDER_EXPIRED = "order_expired" ACTION_ON_NOT_REACHABLE_COUNTER = "action_on_not_reachable_counter" - ACTION_PUT = "action_put" - ACTION_INTERACT_START = "action_interact_start" +ITEM_BURNED = "item_burned" # MISSING NEW_FIRE = "new_fire" - FIRE_SPREADING = "fire_spreading" -DROP_OFF_ON_COOKING_EQUIPMENT = "drop_off_on_cooking_equipment" - PLAYERS_COLLIDE = "players_collide" +PLAYER_START_INTERACT = "player_start_interaction" +PLAYER_END_INTERACT = "player_end_interact" + +ON_ITEM_TRANSITION = "on_item_transition" +CONTENT_READY = "content_ready" + + class Hooks: """ Class Hooks diff --git a/cooperative_cuisine/items.py b/cooperative_cuisine/items.py index fbfa2dab675b0fddea6b48ba4f5def73f4a349c4..0679d34747e20d9ecd270b952ab688516cc72f76 100644 --- a/cooperative_cuisine/items.py +++ b/cooperative_cuisine/items.py @@ -30,6 +30,13 @@ import uuid from enum import Enum from typing import Optional, TypedDict, TYPE_CHECKING, Literal, Set +from cooperative_cuisine.hooks import ( + Hooks, + ON_ITEM_TRANSITION, + PROGRESS_FINISHED, + PROGRESS_STARTED, + CONTENT_READY, +) from cooperative_cuisine.state_representation import ( ItemState, CookingEquipmentState, @@ -233,7 +240,7 @@ class CookingEquipment(Item): item_category = COOKING_EQUIPMENT_ITEM_CATEGORY - def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs): + def __init__(self, transitions: dict[str, ItemInfo], hook: Hooks, *args, **kwargs): """Constructor for CookingEquipment. Args: @@ -255,6 +262,8 @@ class CookingEquipment(Item): self.content_list: list[Item] = [] """The items that the equipment holds.""" + self.hook: Hooks = hook + log.debug(f"Initialize {self.name}: {self.transitions}") for transition in self.transitions.values(): @@ -311,9 +320,17 @@ class CookingEquipment(Item): def progress(self, passed_time: datetime.timedelta, now: datetime.datetime): percent = passed_time.total_seconds() / self.active_transition["seconds"] + if self.progress_percentage == 0.0: + self.hook(PROGRESS_STARTED, item=self) super().progress(equipment=self.name, percent=percent) if self.progress_percentage == 1.0: if isinstance(self.active_transition["result"], Effect): + self.hook( + ON_ITEM_TRANSITION, + item=self, + result=self.active_transition["result"], + seconds=self.active_transition["seconds"], + ) self.active_transition[ "result" ].item_info.manager.register_active_effect( @@ -321,6 +338,7 @@ class CookingEquipment(Item): ) else: self.content_list = [self.active_transition["result"]] + self.hook(PROGRESS_FINISHED, item=self) self.reset() self.check_active_transition() @@ -353,7 +371,9 @@ class CookingEquipment(Item): break # ? else: if ingredients == transition.recipe: + # TODO here hook? if transition.seconds == 0: + self.hook(CONTENT_READY, result=result, before=self) self.content_ready = Item(name=result, item_info=transition) else: self.active_transition = { @@ -405,7 +425,14 @@ class CookingEquipment(Item): class Plate(CookingEquipment): """The plate can have to states: clean and dirty. In the clean state it can hold content/other items.""" - def __init__(self, transitions: dict[str, ItemInfo], clean: bool, *args, **kwargs): + def __init__( + self, + transitions: dict[str, ItemInfo], + clean: bool, + hook: Hooks, + *args, + **kwargs, + ): """Constructor for Plate. Args: @@ -421,6 +448,7 @@ class Plate(CookingEquipment): super().__init__( name=self.create_name(), transitions={k: v for k, v in transitions.items() if not v.equipment}, + hook=hook, *args, **kwargs, ) diff --git a/cooperative_cuisine/movement.py b/cooperative_cuisine/movement.py index 320ce0869d76e6dccd506ba03f472a17e888d715..0105065c0d01a6eb35169d45366fe6bd14cb2bb9 100644 --- a/cooperative_cuisine/movement.py +++ b/cooperative_cuisine/movement.py @@ -243,4 +243,5 @@ class Movement: else None ) if p.last_interacted_counter != p.current_nearest_counter: - p.perform_interact_stop() + if p.interacting: + p.perform_interact_stop() diff --git a/cooperative_cuisine/player.py b/cooperative_cuisine/player.py index d6242a48809bdd13d2e0c8a2d3a5e7dd80be363e..82133a94af53b206a8b8a404f3c7252323c067f9 100644 --- a/cooperative_cuisine/player.py +++ b/cooperative_cuisine/player.py @@ -16,6 +16,11 @@ import numpy as np import numpy.typing as npt from cooperative_cuisine.counters import Counter +from cooperative_cuisine.hooks import ( + Hooks, + PLAYER_START_INTERACT, + PLAYER_END_INTERACT, +) from cooperative_cuisine.items import Item, ItemType from cooperative_cuisine.state_representation import PlayerState @@ -52,6 +57,7 @@ class Player: self, name: str, player_config: PlayerConfig, + hook: Hooks, pos: Optional[npt.NDArray[float]] = None, ): """Constructor of Player. @@ -94,12 +100,15 @@ class Player: self.interacting: bool = False """Is the player currently interacting with a counter.""" + self.hook: Hooks = hook + def set_movement(self, move_vector, move_until): """Called by the `perform_action` method. Movements will be performed (pos will be updated) in the `step` function of the environment""" self.current_movement = move_vector self.movement_until = move_until - self.perform_interact_stop() + if self.interacting: + self.perform_interact_stop() def move_abs(self, new_pos: npt.NDArray[float]): """Overwrites the player location by the new_pos 2d-vector. Absolute movement. @@ -178,6 +187,7 @@ class Player: """ self.interacting = True self.last_interacted_counter = counter + self.hook(PLAYER_START_INTERACT, player=self.name, counter=counter) def perform_interact_stop(self): """Stops an interaction with the counter. Should be called for a @@ -187,6 +197,11 @@ class Player: counter: The counter to stop the interaction with. """ self.interacting = False + self.hook( + PLAYER_END_INTERACT, + player=self.name, + counter=self.last_interacted_counter, + ) self.last_interacted_counter = None def progress(self, passed_time: timedelta, now: datetime): diff --git a/cooperative_cuisine/pygame_2d_vis/gui.py b/cooperative_cuisine/pygame_2d_vis/gui.py index 63f570b3d6b7a4d8de46fd07218a9416f1688f84..ccc28ce9d96b2e485c643fa4f041917a60b6cc85 100644 --- a/cooperative_cuisine/pygame_2d_vis/gui.py +++ b/cooperative_cuisine/pygame_2d_vis/gui.py @@ -1929,10 +1929,15 @@ class PyGameGUI: self.disconnect_websockets() if not self.CONNECT_WITH_STUDY_SERVER: self.stop_game_on_server("Program exited.") + if self.fullscreen_button: + self.fullscreen_button_press() self.disconnect_websockets() if not self.CONNECT_WITH_STUDY_SERVER: self.stop_game_on_server("Program exited.") + + if self.fullscreen: + self.fullscreen_button_press() pygame.quit() sys.exit() diff --git a/cooperative_cuisine/recording.py b/cooperative_cuisine/recording.py index a30881994a3fe108006f7cdaf8b501fbda508141..596060ad7de9823010b65f6bb78e07b07f99b73c 100644 --- a/cooperative_cuisine/recording.py +++ b/cooperative_cuisine/recording.py @@ -45,7 +45,11 @@ import logging import os import traceback from pathlib import Path +from string import Template +import yaml + +from cooperative_cuisine import ROOT_DIR from cooperative_cuisine.counters import Counter from cooperative_cuisine.environment import Environment from cooperative_cuisine.hooks import HookCallbackClass @@ -89,20 +93,42 @@ class FileRecorder(HookCallbackClass): def __call__(self, hook_ref: str, env: Environment, **kwargs): for key, item in kwargs.items(): - if isinstance(item, (Counter, Item, Effect, Order)): - kwargs[key] = str(item) + if isinstance(item, (Counter, Item, Effect)): + kwargs[key] = item.to_dict() + elif isinstance(item, Order): + order_state = { + "id": item.uuid, + "category": "Order", + "meal": item.meal.name, + "start_time": item.start_time.isoformat(), + "max_duration": item.max_duration.total_seconds(), + } + kwargs[key] = order_state + elif isinstance(item, list): - kwargs[key] = [str(i) for i in item] - if hook_ref == "new_orders": - new_orders = kwargs["new_orders"] - kwargs["new_orders"] = [str(order) for order in new_orders] + new_list = [] + for i in item: + if isinstance(i, Order): + new_list.append( + { + "id": i.uuid, + "category": "Order", + "meal": i.meal.name, + "start_time": i.start_time.isoformat(), + "max_duration": i.max_duration.total_seconds(), + } + ) + else: + new_list.append(i.to_dict()) + + kwargs[key] = new_list try: record = ( json.dumps( { "env_time": env.env_time.isoformat(), - **kwargs, **({"hook_ref": hook_ref} if self.add_hook_ref else {}), + **kwargs, }, cls=NumpyAndDataclassEncoder, ) @@ -115,3 +141,90 @@ class FileRecorder(HookCallbackClass): log.info( f"Not JSON serializable Record, hook: {hook_ref}, kwargs: {kwargs}" ) + + +def print_recorded_events_human_readable(jsonl_path: Path): + """This function prints a game_event recording in human-readable form. + + Args: + jsonl_path: Path to the file with recorded game events. + + """ + + def stringify_item(item): + if isinstance(item, float): + return str(item) + if isinstance(item, str): + return item + if isinstance(item, list) and len(item) == 1: + item = item[0] + if item: + content = None + if item and item["type"] in [ + "Plate", + "Pot", + "Basket", + "Peel", + "Pan", + ]: + if item != "Plate" and item["content_ready"]: + content_ready = stringify_item(item["content_ready"]) + return f"{item['type']}{f'({content_ready})'}" + + content = [stringify_item(i) for i in item["content_list"]] + if not content: + content = "None" + return f"{item['type']}{f'({content})' if content else ''}" + else: + return None + + with open(ROOT_DIR / "configs" / "human_readable_print_templates.yaml", "r") as f: + string_templates = yaml.safe_load(f) + + column_size = 20 + with open(jsonl_path, "r") as jsonl_file: + for line in jsonl_file: + record = json.loads(line) + + hook = record["hook_ref"] + + for dict_key in record.keys(): + match dict_key: + case "hook_ref" | "env_time" | "on_hands": + pass + case "counter": + if not record[dict_key]: + record[dict_key] = "None" + else: + occupied = record[dict_key]["occupied_by"] + if occupied: + if isinstance(occupied, list): + occupied = [stringify_item(i) for i in occupied] + else: + occupied = stringify_item(occupied) + record[dict_key] = f"{record[dict_key]['type']}({occupied})" + + case "order": + order = f"Order({record[dict_key]['meal']})" + record[dict_key] = order + case "new_orders": + orders = [f"Order({o['meal']})" for o in record[dict_key]] + record[dict_key] = orders + case _: + try: + record[dict_key] = stringify_item(record[dict_key]) + except (KeyError, TypeError) as e: + print(hook, dict_key, record[dict_key], type(e), e) + + if hook in string_templates.keys(): + string_template = Template(string_templates[hook]) + print(string_template.substitute(**dict(record.items()))) + else: + print(hook) + for key, item in record.items(): + print(f" - {(key+':').ljust(column_size)}{item}") + + +if __name__ == "__main__": + json_lines_path: Path = Path("/home/fabian/.local/state/cooperative_cuisine/log/fcb095915c454446b9ee2905ff534610/game_events.jsonl") + print_recorded_events_human_readable(json_lines_path) diff --git a/tests/test_cooking_equipment.py b/tests/test_cooking_equipment.py index 03f936f1a1625b318e46905668009ed8fce659db..60377d836cff58accd9ca9d22cebea3c51d8f332 100644 --- a/tests/test_cooking_equipment.py +++ b/tests/test_cooking_equipment.py @@ -1,5 +1,6 @@ import pytest +from cooperative_cuisine.hooks import Hooks from cooperative_cuisine.items import ItemInfo, CookingEquipment, Item, ItemType @@ -8,7 +9,7 @@ def test_can_combine_single_other_item(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) other_item = Item("Onion", ItemInfo(type=ItemType.Ingredient, name="Onion")) @@ -20,12 +21,13 @@ def test_can_combine_list_of_other_items(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) other_item = CookingEquipment( name="Pan", transitions={}, item_info=ItemInfo(type=ItemType.Equipment, name="Pan"), + hook=Hooks(None) ) assert cooking_equipment.can_combine(other_item) == False @@ -36,7 +38,7 @@ def test_can_combine_without_other_item(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) assert cooking_equipment.can_combine(None) == False @@ -47,7 +49,7 @@ def test_combine(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) other_item = Item("Onion", ItemInfo(type=ItemType.Ingredient, name="Onion")) @@ -59,7 +61,7 @@ def test_progress(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) result = Item(name="TestResult", item_info=None) cooking_equipment.active_transition = { @@ -78,7 +80,7 @@ def test_reset_content(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) cooking_equipment.content_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] cooking_equipment.reset_content() @@ -91,7 +93,7 @@ def test_release(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) cooking_equipment.content_list = ["Content1", "Content2"] @@ -105,7 +107,7 @@ def test_extra_repr_without_content(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) assert cooking_equipment.extra_repr == "[], None" @@ -116,7 +118,7 @@ def test_extra_repr_with_content(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) item_1 = Item( @@ -135,7 +137,7 @@ def test_get_potential_meal_without_content(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) assert cooking_equipment.get_potential_meal() is None @@ -146,7 +148,7 @@ def test_get_potential_meal_with_content(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) cooking_equipment = CookingEquipment( - transitions={}, name="Pot", item_info=item_info + transitions={}, name="Pot", item_info=item_info, hook=Hooks(None) ) item_1 = Item( @@ -166,7 +168,7 @@ def test_get_potential_meal_with_content(): @pytest.fixture def cooking_equipment(): item_info = ItemInfo(type=ItemType.Meal, name="Soup", seconds=5.0) - return CookingEquipment(transitions={}, name="Pot", item_info=item_info) + return CookingEquipment(transitions={}, name="Pot", item_info=item_info, hook=Hooks(None)) def test_reset(cooking_equipment): diff --git a/tests/test_counter.py b/tests/test_counter.py index d6860acb3e00eb02a6cfc8703bf3bb9fd8e10f7b..058a9e17ad95f1365e0dc5be6aec76ecdfa80a3e 100644 --- a/tests/test_counter.py +++ b/tests/test_counter.py @@ -35,7 +35,7 @@ def test_serving_window(): plate_dispenser.plate_received ), "ServingWindow needs to update plate out of kitchen for ordered meal." plate_dispenser.plate_received = False - plate = Plate(transitions={}, clean=True, item_info=None) + plate = Plate(transitions={}, clean=True, item_info=None, hook=Hooks(None)) plate.content_list = [Item(name="TestMeal", item_info=None)] assert serving_window.can_drop_off( item=plate