From 9c732065d6c34ff31ee28972ba2cc0e7ae0aeff4 Mon Sep 17 00:00:00 2001 From: fheinrich <fheinrich@techfak.uni-bielefeld.de> Date: Fri, 8 Mar 2024 13:04:50 +0100 Subject: [PATCH] Added missing hooks, human readable print of recording --- .../configs/environment_config.yaml | 31 ++++--- .../configs/item_info_debug.yaml | 14 ++-- .../configs/study/study_config.yaml | 26 +++--- cooperative_cuisine/counter_factory.py | 2 + cooperative_cuisine/counters.py | 60 ++++++++++++-- cooperative_cuisine/environment.py | 1 + cooperative_cuisine/hooks.py | 46 +++++----- cooperative_cuisine/items.py | 32 ++++++- cooperative_cuisine/movement.py | 3 +- cooperative_cuisine/player.py | 17 +++- cooperative_cuisine/pygame_2d_vis/drawing.py | 4 +- cooperative_cuisine/recording.py | 83 ++++++++++++++++++- 12 files changed, 242 insertions(+), 77 deletions(-) diff --git a/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml index 608eb972..46517a09 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,31 +173,35 @@ 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 - serve_not_ordered_meal - serve_without_plate - completed_order - - new_orders - - order_expired + # - 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 diff --git a/cooperative_cuisine/configs/item_info_debug.yaml b/cooperative_cuisine/configs/item_info_debug.yaml index fd871d1d..f7783818 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/study_config.yaml b/cooperative_cuisine/configs/study/study_config.yaml index 38158fdc..3afc47b2 100644 --- a/cooperative_cuisine/configs/study/study_config.yaml +++ b/cooperative_cuisine/configs/study/study_config.yaml @@ -1,18 +1,18 @@ levels: - - 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: 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: CONFIGS_DIR/environment_config.yaml - # 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 - name: "Level 1-4: Bottleneck" + - config_path: CONFIGS_DIR/environment_config.yaml + layout_path: LAYOUTS_DIR/basic.layout + item_info_path: CONFIGS_DIR/item_info_debug.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 +# name: "Level 1-4: Bottleneck" num_players: 1 diff --git a/cooperative_cuisine/counter_factory.py b/cooperative_cuisine/counter_factory.py index 02f6d5d7..033d8bed 100644 --- a/cooperative_cuisine/counter_factory.py +++ b/cooperative_cuisine/counter_factory.py @@ -218,6 +218,7 @@ class CounterFactory: by_equipment_name=item_info.name, add_effects=True, ), + hook=self.hook, ), hook=self.hook, ) @@ -248,6 +249,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 a4ea2bba..a0a495a2 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( @@ -587,18 +590,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 +696,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 +706,7 @@ class PlateDispenser(Counter): "clean": clean, "transitions": self.plate_transitions, "item_info": self.dispensing, + "hook": self.hook } return Plate(**kwargs) @@ -693,6 +731,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 @@ -880,5 +919,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 c166aa2b..cadcac0a 100644 --- a/cooperative_cuisine/environment.py +++ b/cooperative_cuisine/environment.py @@ -407,6 +407,7 @@ class Environment: else {} ) ), + hook=self.hook, pos=pos, ) self.players[player.name] = player diff --git a/cooperative_cuisine/hooks.py b/cooperative_cuisine/hooks.py index 1e44e08e..601a2b20 100644 --- a/cooperative_cuisine/hooks.py +++ b/cooperative_cuisine/hooks.py @@ -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 1b563a15..fa76cde7 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, @@ -224,7 +231,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): super().__init__(*args, **kwargs) self.transitions: dict[str, ItemInfo] = transitions """What is needed to cook a meal / create another ingredient.""" @@ -239,6 +246,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(): @@ -295,9 +304,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( @@ -305,6 +322,7 @@ class CookingEquipment(Item): ) else: self.content_list = [self.active_transition["result"]] + self.hook(PROGRESS_FINISHED, item=self) self.reset() self.check_active_transition() @@ -337,7 +355,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 = { @@ -389,7 +409,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, + ): self.clean: bool = clean """If the plate is clean or dirty.""" self.meals: Set[str] = set(transitions.keys()) @@ -397,6 +424,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 56956686..e315edb6 100644 --- a/cooperative_cuisine/movement.py +++ b/cooperative_cuisine/movement.py @@ -233,4 +233,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 6dc25caa..86ed01c1 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, ): self.name: str = name @@ -87,12 +93,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. @@ -171,6 +180,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 @@ -180,6 +190,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/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py index e3d5c0d3..f9a99462 100644 --- a/cooperative_cuisine/pygame_2d_vis/drawing.py +++ b/cooperative_cuisine/pygame_2d_vis/drawing.py @@ -881,9 +881,7 @@ class Visualizer: self.draw_gamescreen(screen, state, grid_size, [0 for _ in state["players"]]) pygame.image.save(screen, filename) - def get_state_image( - self, grid_size: int, save_folder: dict - ) -> npt.NDArray[np.uint8]: + def get_state_image(self, grid_size: int, state: dict) -> npt.NDArray[np.uint8]: width = int(np.ceil(state["kitchen"]["width"] * grid_size)) height = int(np.ceil(state["kitchen"]["height"] * grid_size)) diff --git a/cooperative_cuisine/recording.py b/cooperative_cuisine/recording.py index 490377ba..ba7a669c 100644 --- a/cooperative_cuisine/recording.py +++ b/cooperative_cuisine/recording.py @@ -80,10 +80,12 @@ 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)): + if isinstance(item, (Counter, Item, Effect)): + kwargs[key] = item.to_dict() + elif isinstance(item, Order): kwargs[key] = str(item) elif isinstance(item, list): - kwargs[key] = [str(i) for i in item] + kwargs[key] = [i.to_dict() for i in item] if hook_ref == "new_orders": new_orders = kwargs["new_orders"] kwargs["new_orders"] = [str(order) for order in new_orders] @@ -92,8 +94,8 @@ class FileRecorder(HookCallbackClass): json.dumps( { "env_time": env.env_time.isoformat(), - **kwargs, **({"hook_ref": hook_ref} if self.add_hook_ref else {}), + **kwargs, }, cls=NumpyAndDataclassEncoder, ) @@ -106,3 +108,78 @@ class FileRecorder(HookCallbackClass): log.info( f"Not JSON serializable Record, hook: {hook_ref}, kwargs: {kwargs}" ) + + +def print_recorded_events_human_readable(jsonl_path: Path): + seen_keys = [] + print() + column_size = 20 + with open(jsonl_path, "r") as jsonl_file: + for line in jsonl_file: + record = json.loads(line) + + def stringify_item(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 + + print(f"{record['hook_ref']}") + for dict_key in record.keys(): + # print(dict_key, record[dict_key]) + match dict_key: + case "hook_ref" | "env_time" | "on_hands": + pass + + case "counter": + if not record[dict_key]: + print(f" {'Counter:'.ljust(column_size)}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) + print( + f" {'Counter:'.ljust(column_size)}{record[dict_key]['type']}({occupied})", + ) + # case "return_this" | "item" | "equipment" | "content_ready" | "before" | "returned_item" | "result" | "meal" | "target" | "meal" | "meal_name" | "Occupied_by": + + case other: + try: + print( + f" {(str(dict_key).capitalize()+':').ljust(column_size)}{stringify_item(record[dict_key])}" + ) + except Exception as e: + print( + f" {(str(dict_key).capitalize()+':').ljust(column_size)}{record[dict_key]}" + ) + + print() + + +if __name__ == "__main__": + jsonl_path: Path = Path( + "/Users/fheinrich/Library/Logs/cooperative_cuisine/1c3011b16bcc426ab1019ef73bd2c1c9/game_events.jsonl" + ) + print_recorded_events_human_readable(jsonl_path) -- GitLab