From fd162778cfe722b712375733f3aa30e0e82d9131 Mon Sep 17 00:00:00 2001
From: Dominik Battefeld <dbattefeld@techfak.de>
Date: Tue, 16 Jan 2024 10:23:13 +0100
Subject: [PATCH] Add previous work on config exchange

---
 overcooked_simulator/counters.py              |  30 ++-
 .../game_content/item_info_new.yaml           |  30 ++-
 overcooked_simulator/game_items.py            | 187 +++++++++---------
 .../gui_2d_vis/overcooked_gui.py              |  26 ++-
 .../overcooked_environment.py                 |  91 ++++++++-
 overcooked_simulator/simulation_runner.py     |   6 +-
 6 files changed, 235 insertions(+), 135 deletions(-)

diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py
index 616ad694..fdc94f2b 100644
--- a/overcooked_simulator/counters.py
+++ b/overcooked_simulator/counters.py
@@ -16,7 +16,6 @@ import numpy as np
 import numpy.typing as npt
 
 from overcooked_simulator.game_items import (
-    CuttableItem,
     Item,
     CookingEquipment,
     Meal,
@@ -34,6 +33,10 @@ class Counter:
         self.pos: npt.NDArray[float] = pos
         self.occupied_by: Optional[Item] = occupied_by
 
+    @property
+    def occupied(self):
+        return self.occupied_by is not None
+
     def pick_up(self, on_hands: bool = True):
         """Gets called upon a player performing the pickup action. If the counter can give something to
         the player, it does so. In the standard counter this is when an item is on the counter.
@@ -95,15 +98,30 @@ class Counter:
 
 
 class CuttingBoard(Counter):
-    def __init__(self, pos: np.ndarray):
+    def __init__(self, pos: np.ndarray, transitions: dict):
         self.progressing = False
+        self.transitions = transitions
         super().__init__(pos)
 
     def progress(self, passed_time: timedelta, now: datetime):
         """Called by environment step function for time progression"""
-        if self.progressing:
-            if isinstance(self.occupied_by, CuttableItem):
-                self.occupied_by.progress()
+        if (
+            self.occupied
+            and self.progressing
+            and self.occupied_by.name in self.transitions
+        ):
+            percent = (
+                passed_time.total_seconds()
+                / self.transitions[self.occupied_by.name]["seconds"]
+            )
+            self.occupied_by.progress(
+                equipment=self.__class__.__name__, percent=percent
+            )
+            if self.occupied_by.progress_percentage == 1.0:
+                self.occupied_by.reset()
+                self.occupied_by.name = self.transitions[self.occupied_by.name][
+                    "result"
+                ]
 
     def start_progress(self):
         """Starts the cutting process."""
@@ -305,7 +323,7 @@ class Stove(Counter):
             and isinstance(self.occupied_by, CookingEquipment)
             and self.occupied_by.can_progress()
         ):
-            self.occupied_by.progress()
+            self.occupied_by.progress(passed_time, now)
 
 
 class Sink(Counter):
diff --git a/overcooked_simulator/game_content/item_info_new.yaml b/overcooked_simulator/game_content/item_info_new.yaml
index 11f0ae05..440e8d46 100644
--- a/overcooked_simulator/game_content/item_info_new.yaml
+++ b/overcooked_simulator/game_content/item_info_new.yaml
@@ -1,12 +1,24 @@
 CuttingBoard:
   type: Equipment
 
+Sink:
+  type: Equipment
+
 Pot:
   type: Equipment
 
 Pan:
   type: Equipment
 
+DirtyPlate:
+  type: Equipment
+
+Plate:
+  type: Equipment
+  needs: [ DirtyPlate ]
+  seconds: 2.0
+  equipment: Sink
+
 # --------------------------------------------------------------------------------
 
 Tomato:
@@ -27,30 +39,30 @@ Bun:
 ChoppedTomato:
   type: Ingredient
   needs: [ Tomato ]
-  steps: 500
+  seconds: 2.0
   equipment: CuttingBoard
 
 ChoppedLettuce:
   type: Ingredient
   needs: [ Lettuce ]
-  steps: 700
+  seconds: 4.0
   equipment: CuttingBoard
 
 ChoppedOnion:
   type: Ingredient
   needs: [ Onion ]
-  steps: 500
+  seconds: 2.0
   equipment: CuttingBoard
 
 ChoppedMeat:
   type: Ingredient
   needs: [ Meat ]
-  steps: 700
+  seconds: 0.1
   equipment: CuttingBoard
 
 CookedPatty:
   type: Ingredient
-  steps: 500
+  seconds: 10.0
   needs: [ ChoppedMeat ]
   equipment: Pan
 
@@ -59,21 +71,21 @@ CookedPatty:
 Burger:
   type: Meal
   needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
-  equipment: [ ]
+  equipment: ~
 
 Salad:
   type: Meal
   needs: [ ChoppedLettuce, ChoppedTomato ]
-  equipment: [ ]
+  equipment: ~
 
 TomatoSoup:
   type: Meal
-  steps: 500
   needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
+  seconds: 3.0
   equipment: Pot
 
 OnionSoup:
   type: Meal
-  steps: 500
   needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
+  seconds: 3.0
   equipment: Pot
diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py
index 937d0617..d9ebae0e 100644
--- a/overcooked_simulator/game_items.py
+++ b/overcooked_simulator/game_items.py
@@ -1,8 +1,11 @@
 from __future__ import annotations
 
+import collections
 import dataclasses
+import datetime
 import logging
 from enum import Enum
+from typing import Optional
 
 log = logging.getLogger(__name__)
 
@@ -17,9 +20,7 @@ class ItemType(Enum):
 class ItemInfo:
     type: ItemType = dataclasses.field(compare=False)
     name: str = dataclasses.field(compare=True)
-    is_cuttable: bool = dataclasses.field(compare=False, default=False)
-    steps_needed: int = dataclasses.field(compare=False, default=0)
-    finished_progress_name: str = dataclasses.field(compare=False, default="Chopped*")
+    seconds: float = dataclasses.field(compare=False, default=0)
     needs: list[ItemInfo] = dataclasses.field(compare=False, default_factory=list)
     equipment: ItemInfo | None = dataclasses.field(compare=False, default=None)
 
@@ -30,43 +31,23 @@ class ItemInfo:
     def __post_init__(self):
         self.type = ItemType(self.type)
 
-    def get_finished_name(self):
-        if "*" in self.finished_progress_name:
-            return self.finished_progress_name.replace("*", self.name)
-        return self.name
-
     def create_item(self, clean_plate=False, parts=None) -> Item:
+        kwargs = {
+            "name": self.name,
+            "item_info": self,
+            "clean": clean_plate,
+            "parts": parts,
+        }
         match self.type:
             case ItemType.Ingredient:
-                if self.is_cuttable:
-                    return CuttableItem(
-                        name=self.name,
-                        finished=False,
-                        steps_needed=self.steps_needed,
-                        finished_name=self.get_finished_name(),
-                        item_info=self,
-                    )
-                return Item(name=self.name, item_info=self)
+                return Item(**kwargs)
             case ItemType.Equipment:
                 if "Plate" in self.name:
-                    return Plate(
-                        name=self.name,
-                        steps_needed=self.steps_needed,
-                        finished=False,
-                        item_info=self,
-                        clean=clean_plate,
-                    )
+                    return Plate(**kwargs)
                 else:
-                    return CookingEquipment(name=self.name, item_info=self)
+                    return CookingEquipment(**kwargs)
             case ItemType.Meal:
-                return Meal(
-                    name=self.name,
-                    finished=False,
-                    steps_needed=self.steps_needed,
-                    finished_name=self.get_finished_name(),
-                    item_info=self,
-                    parts=parts,
-                )
+                return Meal(**kwargs)
 
     def add_start_meal_to_equipment(self, start_item: ItemInfo):
         self._start_meals.append(start_item)
@@ -99,15 +80,14 @@ class Item:
     def __init__(self, name: str, item_info: ItemInfo, *args, **kwargs):
         self.name = self.__class__.__name__ if name is None else name
         self.item_info = item_info
-
-    def can_combine(self, other):
-        return False
-
-    def combine(self, other) -> Item | None:
-        pass
+        self.progress_equipment = None
+        self.progress_percentage = 0.0
 
     def __repr__(self):
-        return f"{self.name}({self.extra_repr})"
+        if self.progress_equipment is None:
+            return f"{self.name}({self.extra_repr})"
+        else:
+            return f"{self.name}(progress={round(self.progress_percentage * 100, 2)}%,{self.extra_repr})"
 
     def __eq__(self, other):
         return other and self.name == other.name
@@ -116,60 +96,51 @@ class Item:
     def extra_repr(self):
         return ""
 
+    def can_combine(self, other):
+        return False
 
-class ProgressibleItem(Item):
-    """Class for items which need to be processed (cut, cooked, ...)"""
-
-    def __init__(
-        self,
-        finished: bool = False,
-        steps_needed: int = 500,
-        finished_name: str = None,
-        *args,
-        **kwargs,
-    ):
-        super().__init__(*args, **kwargs)
-        self.progressed_steps = steps_needed if finished else 0
-        self.steps_needed = steps_needed
-        self.finished = finished
-        self.finished_name = (
-            f"Chopped{self.name}" if finished_name is None else finished_name
-        )
-
-    def progress(self):
-        """Progresses the item process as long as it is not finished."""
-        if self.progressed_steps >= self.steps_needed:
-            self.finished = True
-            self.finished_call()
-        if not self.finished:
-            self.progressed_steps += 1
-
-    def can_progress(self) -> bool:
-        return True
-
-    def finished_call(self):
-        self.name = self.finished_name
+    def combine(self, other) -> Item | None:
+        pass
 
-    def reset(self):
-        self.finished = False
-        self.progressed_steps = 0
+    def progress(self, equipment: str, percent: float):
+        """Progresses the item process on the given equipment as long as it is not finished."""
+        if self.progress_equipment is None:
+            self.progress_equipment = equipment
 
-    def __repr__(self):
-        if not self.steps_needed or self.finished:
-            return f"{self.name}({self.extra_repr})"
+        if self.progress_equipment == equipment:
+            self.progress_percentage += percent
+            self.progress_percentage = min(self.progress_percentage, 1.0)
         else:
-            return f"{self.name}(progress={int(self.progressed_steps / self.steps_needed * 100)}%,{self.extra_repr})"
-
+            log.warning(
+                f"{self.name} expected progress on {self.progress_equipment}, but got {percent * 100}% on {equipment}"
+            )
 
-class CuttableItem(ProgressibleItem):
-    """Class of item which can be processed by the cutting board."""
+    def reset(self):
+        self.progress_equipment = None
+        self.progress_percentage = 0.0
 
 
 class CookingEquipment(Item):
-    def __init__(self, content: Meal = None, *args, **kwargs):
+    def __init__(self, transitions: dict, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.content = content
+        self.transitions = transitions
+        self.active_transition: Optional[dict] = None
+        self.content: Item | list[Item] = []  # list if cooking, meal item when done
+        log.debug(f"Initialize {self.name}: {self.transitions}")
 
+    def can_combine(self, other):
+        # already cooking or nothing to combine
+        if self.active_transition is not None or other is None:
+            return False
+
+        # other extends ingredients for meal
+        ingredients = collections.Counter(item.name for item in self.content + [other])
+        return any(
+            ingredients <= collections.Counter(transition["needs"])
+            for transition in self.transitions.values()
+        )
+
+    """
     def can_combine(self, other):
         if other is None:
             return False
@@ -188,7 +159,25 @@ class CookingEquipment(Item):
             [other]
             + ([self.content] if self.content.progressed_steps else self.content.parts)
         )
+    """
 
+    def combine(self, other):
+        if isinstance(other, CookingEquipment):
+            ...
+        else:
+            self.content.append(other)
+
+        ingredients = collections.Counter(item.name for item in self.content)
+        for result, transition in self.transitions.items():
+            recipe = collections.Counter(transition["needs"])
+            if ingredients == recipe:
+                self.active_transition = {
+                    "seconds": transition["seconds"],
+                    "result": Item(name=result, item_info=transition["info"])
+                }
+                break
+
+    """
     def combine(self, other):
         if self.content is None:
             if isinstance(other, CookingEquipment):
@@ -223,16 +212,17 @@ class CookingEquipment(Item):
                 )
             return
         self.content.combine(other)
+    """
 
-    def can_progress(self, counter_type="Stove") -> bool:
-        return (
-            self.content
-            and isinstance(self.content, ProgressibleItem)
-            and self.content.can_progress()
-        )
+    def can_progress(self) -> bool:
+        return self.active_transition is not None
 
-    def progress(self):
-        self.content.progress()
+    def progress(self, passed_time: datetime.timedelta, now: datetime.datetime):
+        percent = passed_time.total_seconds() / self.active_transition["seconds"]
+        super().progress(equipment=self.name, percent=percent)
+        if self.progress_percentage == 1.0:
+            self.content = self.active_transition["result"]
+            self.reset()
 
     def can_release_content(self) -> bool:
         return (
@@ -250,8 +240,12 @@ class CookingEquipment(Item):
     def extra_repr(self):
         return self.content
 
+    def reset(self):
+        super().reset()
+        self.active_transition = None
+
 
-class Meal(ProgressibleItem):
+class Meal(Item):
     def __init__(self, parts=None, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.parts = [] if parts is None else parts
@@ -287,15 +281,10 @@ class Meal(ProgressibleItem):
 
 
 class Plate(CookingEquipment):
-    def __init__(
-        self, clean, steps_needed, finished, content: Meal = None, *args, **kwargs
-    ):
+    def __init__(self, clean, content: Meal = None, *args, **kwargs):
         super().__init__(content, *args, **kwargs)
         self.clean = clean
         self.name = self.create_name()
-        self.steps_needed = steps_needed
-        self.finished = finished
-        self.progressed_steps = steps_needed if finished else 0
 
     def finished_call(self):
         self.clean = True
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
index 7c910c07..c2728c76 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -15,7 +15,6 @@ from scipy.spatial import KDTree
 from overcooked_simulator import ROOT_DIR
 from overcooked_simulator.counters import Counter
 from overcooked_simulator.game_items import (
-    ProgressibleItem,
     Item,
     CookingEquipment,
     Meal,
@@ -408,8 +407,8 @@ class PyGameGUI:
                     pos, self.visualization_config[item.name]["parts"], scale=scale
                 )
 
-        if isinstance(item, (ProgressibleItem, Plate)) and not item.finished:
-            self.draw_progress_bar(pos, item.progressed_steps, item.steps_needed)
+        if isinstance(item, (Item, Plate)) and item.progress_percentage > 0.0:
+            self.draw_progress_bar(pos, item.progress_percentage)
 
         if isinstance(item, CookingEquipment) and item.content:
             self.draw_item(pos, item.content)
@@ -423,18 +422,17 @@ class PyGameGUI:
                     triangle_offsets = create_polygon(len(item.parts), length=10)
                     self.draw_item(pos + triangle_offsets[idx], o, scale=0.6)
 
-    def draw_progress_bar(self, pos, current, needed):
+    def draw_progress_bar(self, pos, percent):
         """Visualize progress of progressing item as a green bar under the item."""
-        if current != 0:
-            bar_height = self.grid_size * 0.2
-            progress_width = (current / needed) * self.grid_size
-            progress_bar = pygame.Rect(
-                pos[0] - (self.grid_size / 2),
-                pos[1] - (self.grid_size / 2) + self.grid_size - bar_height,
-                progress_width,
-                bar_height,
-            )
-            pygame.draw.rect(self.game_screen, colors["green1"], progress_bar)
+        bar_height = self.grid_size * 0.2
+        progress_width = percent * self.grid_size
+        progress_bar = pygame.Rect(
+            pos[0] - (self.grid_size / 2),
+            pos[1] - (self.grid_size / 2) + self.grid_size - bar_height,
+            progress_width,
+            bar_height,
+        )
+        pygame.draw.rect(self.game_screen, colors["green1"], progress_bar)
 
     def draw_counter(self, counter):
         """Visualization of a counter at its position. If it is occupied by an item, it is also shown.
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 8f5adf96..367e59bf 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -22,7 +22,7 @@ from overcooked_simulator.counters import (
     PlateDispenser,
     SinkAddon,
 )
-from overcooked_simulator.game_items import ItemInfo, ItemType
+from overcooked_simulator.game_items import ItemInfo, ItemType, CookingEquipment
 from overcooked_simulator.player import Player
 from overcooked_simulator.utils import create_init_env_time
 
@@ -88,12 +88,21 @@ class Environment:
 
         self.item_info_path: Path = item_info_path
         self.item_info = self.load_item_info()
+        self.validate_item_info()
 
         self.game_score = GameScore()
 
         self.SYMBOL_TO_CHARACTER_MAP = {
             "#": Counter,
-            "C": CuttingBoard,
+            "C": lambda pos: CuttingBoard(
+                pos,
+                {
+                    info.needs[0]: {"seconds": info.seconds, "result": item}
+                    for item, info in self.item_info.items()
+                    if info.equipment is not None
+                    and info.equipment.name == "CuttingBoard"
+                },
+            ),
             "X": Trash,
             "W": lambda pos: ServingWindow(pos, self.game_score),
             "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]),
@@ -108,12 +117,28 @@ class Environment:
             "A": "Agent",
             "U": lambda pos: Stove(
                 pos,
-                self.item_info["Pot"].create_item(),
+                occupied_by=CookingEquipment(
+                    name="Pot",
+                    item_info=self.item_info["Pot"],
+                    transitions={
+                        item: {"seconds": info.seconds, "needs": info.needs, "info": info}
+                        for item, info in self.item_info.items()
+                        if info.equipment is not None and info.equipment.name == "Pot"
+                    },
+                ),
             ),  # Stove with pot: U because it looks like a pot
             "Q": lambda pos: Stove(
                 pos,
-                self.item_info["Pan"].create_item(),
-            ),  # Stove with pot: U because it looks like a pot
+                occupied_by=CookingEquipment(
+                    name="Pan",
+                    item_info=self.item_info["Pan"],
+                    transitions={
+                        item: {"seconds": info.seconds, "needs": info.needs, "info": info}
+                        for item, info in self.item_info.items()
+                        if info.equipment is not None and info.equipment.name == "Pan"
+                    },
+                ),
+            ),  # Stove with pan: Q because it looks like a pan
             "B": lambda pos: Dispenser(pos, self.item_info["Bun"]),
             "M": lambda pos: Dispenser(pos, self.item_info["Meat"]),
             "S": lambda pos: Sink(pos),
@@ -151,6 +176,62 @@ class Environment:
                 item_info.sort_start_meals()
         return item_lookup
 
+    def validate_item_info(self):
+        pass
+        # infos = {t: [] for t in ItemType}
+        # graph = nx.DiGraph()
+        # for info in self.item_info.values():
+        #     infos[info.type].append(info)
+        #     graph.add_node(info.name)
+        #     match info.type:
+        #         case ItemType.Ingredient:
+        #             if info.is_cuttable:
+        #                 graph.add_edge(
+        #                     info.name, info.finished_progress_name[:-1] + info.name
+        #                 )
+        #         case ItemType.Equipment:
+        #             ...
+        #         case ItemType.Meal:
+        #             if info.equipment is not None:
+        #                 graph.add_edge(info.equipment.name, info.name)
+        #             for ingredient in info.needs:
+        #                 graph.add_edge(ingredient, info.name)
+
+        # graph = nx.DiGraph()
+        # for item_name, item_info in self.item_info.items():
+        #     graph.add_node(item_name, type=item_info.type.name)
+        #     if len(item_info.equipment) == 0:
+        #         for item in item_info.needs:
+        #             graph.add_edge(item, item_name)
+        #     else:
+        #         for item in item_info.needs:
+        #             for equipment in item_info.equipment:
+        #                 graph.add_edge(item, equipment)
+        #                 graph.add_edge(equipment, item_name)
+
+        # plt.figure(figsize=(10, 10))
+        # pos = nx.nx_agraph.graphviz_layout(graph, prog="twopi", args="")
+        # nx.draw(graph, pos=pos, with_labels=True, node_color="white", node_size=500)
+        # print(nx.multipartite_layout(graph, subset_key="type", align="vertical"))
+
+        # pos = {
+        #     node: (
+        #         len(nx.ancestors(graph, node)) - len(nx.descendants(graph, node)),
+        #         y,
+        #     )
+        #     for y, node in enumerate(graph)
+        # }
+        # nx.draw(
+        #     graph,
+        #     pos=pos,
+        #     with_labels=True,
+        #     node_shape="s",
+        #     node_size=500,
+        #     node_color="white",
+        # )
+        # TODO add colors for ingredients, equipment and meals
+        # plt.show()
+
     def parse_layout_file(self, layout_file: Path):
         """Creates layout of kitchen counters in the environment based on layout file.
         Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at
diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py
index 8ad41e28..38fe465d 100644
--- a/overcooked_simulator/simulation_runner.py
+++ b/overcooked_simulator/simulation_runner.py
@@ -30,7 +30,7 @@ class Simulator(Thread):
         env_config_path,
         layout_path,
         frequency: int,
-        item_info_path=ROOT_DIR / "game_content" / "item_info.yaml",
+        item_info_path=ROOT_DIR / "game_content" / "item_info_new.yaml",
         seed: int = 8654321,
     ):
         # TODO look at https://builtin.com/data-science/numpy-random-seed to change to other random
@@ -98,9 +98,11 @@ class Simulator(Thread):
 
         overslept_in_ns = 0
         self.env.reset_env_time()
+        last_step_start = time.time_ns()
         while not self.finished:
             step_start = time.time_ns()
-            self.step(timedelta(seconds=overslept_in_ns / 1_000_000_000))
+            self.step(timedelta(seconds=(step_start - last_step_start) / 1_000_000_000))
+            last_step_start = step_start
             step_duration = time.time_ns() - step_start
 
             time_to_sleep_ns = self.preferred_sleep_time_ns - (
-- 
GitLab