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