From a8fd5a5dd14b6ba7eecbb65b392c5d26efbed88c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Schr=C3=B6der?=
 <fschroeder@techfak.uni-bielefeld.de>
Date: Wed, 20 Dec 2023 11:18:33 +0100
Subject: [PATCH] plate dispenser without manager. plates disappear for a time
 and then appear on the plate dispenser. You can now add stuff on the plates
 on the dispenser.

New lock for gui and environment.
---
 overcooked_simulator/counters.py              | 124 ++++++++++++++----
 .../game_content/environment_config.yaml      |   5 +-
 overcooked_simulator/game_items.py            |  20 +--
 .../overcooked_environment.py                 |  62 +++++----
 overcooked_simulator/player.py                |   5 +-
 overcooked_simulator/pygame_gui/pygame_gui.py |  12 +-
 .../pygame_gui/visualization.yaml             |   2 +-
 overcooked_simulator/simulation_runner.py     |   9 +-
 8 files changed, 168 insertions(+), 71 deletions(-)

diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py
index 6c0b6585..79272eb5 100644
--- a/overcooked_simulator/counters.py
+++ b/overcooked_simulator/counters.py
@@ -1,11 +1,13 @@
 from __future__ import annotations
 
+import logging
+from collections import deque
+from datetime import datetime, timedelta
 from typing import TYPE_CHECKING, Optional
 
 if TYPE_CHECKING:
     from overcooked_simulator.overcooked_environment import (
         GameScore,
-        PlateManager,
         SinkManager,
     )
 
@@ -22,6 +24,9 @@ from overcooked_simulator.game_items import (
 )
 
 
+log = logging.getLogger(__name__)
+
+
 class Counter:
     """Simple class for a counter at a specified position (center of counter). Can hold things on top."""
 
@@ -118,9 +123,11 @@ class CuttingBoard(Counter):
 
 
 class ServingWindow(Counter):
-    def __init__(self, pos, game_score: GameScore, plate_manager: PlateManager):
+    def __init__(
+        self, pos, game_score: GameScore, plate_dispenser: PlateDispenser = None
+    ):
         self.game_score = game_score
-        self.plate_manager = plate_manager
+        self.plate_dispenser = plate_dispenser
         super().__init__(pos)
 
     def drop_off(self, item) -> Item | None:
@@ -128,8 +135,8 @@ class ServingWindow(Counter):
         print(item)
         # TODO define rewards
         self.game_score.increment_score(reward)
-        self.plate_manager.number_returned_plates += 1
-        self.plate_manager.update_plate_return()
+        if self.plate_dispenser is not None:
+            self.plate_dispenser.update_plate_out_of_kitchen()
         return None
 
     def can_score(self, item):
@@ -152,6 +159,9 @@ class ServingWindow(Counter):
     def pick_up(self, on_hands: bool = True):
         pass
 
+    def add_plate_dispenser(self, plate_dispenser):
+        self.plate_dispenser = plate_dispenser
+
 
 class Dispenser(Counter):
     def __init__(self, pos, dispensing):
@@ -177,32 +187,98 @@ class Dispenser(Counter):
         return f"{self.dispensing.name}Dispenser"
 
 
-class DirtyPlateReturn(Counter):
-    def __init__(self, pos, dispensing, plate_manager: PlateManager):
-        self.plate_manager = plate_manager
+class PlateDispenser(Counter):
+    def __init__(self, pos, dispensing, plate_config):
         self.dispensing = dispensing
         super().__init__(pos)
-        self.occupied_by = [
-            self.dispensing.create_item()
-            for _ in range(self.plate_manager.number_returned_plates)
-        ]
+        self.occupied_by = deque()
+        self.out_of_kitchen_timer = []
+        self.plate_config = {"plate_delay": [5, 10]}
+        self.plate_config.update(plate_config)
+        self.next_plate_time = datetime.max
+        self.setup_plates()
 
     def pick_up(self, on_hands: bool = True):
-        return_this = self.occupied_by.pop()
-        if self.plate_manager.number_returned_plates > 1:
-            self.plate_manager.number_returned_plates -= 1
-        else:
-            self.occupied_by = []
-        return return_this
+        if self.occupied_by:
+            return self.occupied_by.pop()
+
+    # def can_drop_off(self, item: Item) -> bool:
+    #     return False
 
     def can_drop_off(self, item: Item) -> bool:
-        return False
+        return (
+            not self.occupied_by
+            or isinstance(item, Plate)
+            or self.occupied_by[-1].can_combine(item)
+        )
 
-    def update(self):
-        if self.plate_manager.number_returned_plates == 0:
-            self.occupied_by = []
-        else:
-            self.occupied_by.append(self.dispensing.create_item())
+    def drop_off(self, item: Item) -> Item | None:
+        """Takes the thing dropped of by the player.
+
+        Args:
+            item: The item to be placed on the counter.
+
+        Returns: TODO Return information, whether the score is affected (Serving Window?)
+
+        """
+        if not self.occupied_by:
+            self.occupied_by.append(item)
+        elif isinstance(item, Plate):
+            self.occupied_by.append(item)
+        elif self.occupied_by[-1].can_combine(item):
+            return self.occupied_by[-1].combine(item)
+        return None
+
+    def add_dirty_plate(self):
+        self.occupied_by.appendleft(self.dispensing.create_item())
+
+    def update_plate_out_of_kitchen(self):
+        """Is called from the serving window to add a plate out of kitchen."""
+        time_plate_to_add = datetime.now() + timedelta(
+            seconds=np.random.uniform(
+                low=self.plate_config["plate_delay"][0],
+                high=self.plate_config["plate_delay"][1],
+            )
+        )
+        log.debug(f"New plate out of kitchen until {time_plate_to_add}")
+        self.out_of_kitchen_timer.append(time_plate_to_add)
+        if time_plate_to_add < self.next_plate_time:
+            self.next_plate_time = time_plate_to_add
+
+    def setup_plates(self):
+        """Create plates based on the config. Clean and dirty ones."""
+        if "dirty_plates" in self.plate_config:
+            self.occupied_by.extend(
+                [
+                    self.dispensing.create_item()
+                    for _ in range(self.plate_config["dirty_plates"])
+                ]
+            )
+        if "clean_plates" in self.plate_config:
+            self.occupied_by.extend(
+                [
+                    self.dispensing.create_item(clean_plate=True)
+                    for _ in range(self.plate_config["clean_plates"])
+                ]
+            )
+
+    def progress(self):
+        """Check if plates arrive from outside the kitchen and add a dirty plate accordingly"""
+        now = datetime.now()
+        if self.next_plate_time < now:
+            idx_delete = []
+            for i, times in enumerate(self.out_of_kitchen_timer):
+                if times < now:
+                    idx_delete.append(i)
+                    log.debug("Add dirty plate")
+                    self.add_dirty_plate()
+            for idx in reversed(idx_delete):
+                self.out_of_kitchen_timer.pop(idx)
+            self.next_plate_time = (
+                min(self.out_of_kitchen_timer)
+                if self.out_of_kitchen_timer
+                else datetime.max
+            )
 
     def __repr__(self):
         return "PlateReturn"
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
index ceaf3f69..39e273e0 100644
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ b/overcooked_simulator/game_content/environment_config.yaml
@@ -1,4 +1,7 @@
 counter_side_length: 40
 world_width: 800
 world_height: 600
-max_number_plates: 25
\ No newline at end of file
+plates:
+  clean_plates: 1
+  dirty_plates: 0
+  plate_delay: [ 5, 10 ]
\ No newline at end of file
diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py
index 621819dc..fb983d7d 100644
--- a/overcooked_simulator/game_items.py
+++ b/overcooked_simulator/game_items.py
@@ -1,8 +1,11 @@
 from __future__ import annotations
 
 import dataclasses
+import logging
 from enum import Enum
 
+log = logging.getLogger(__name__)
+
 
 class ItemType(Enum):
     Ingredient = "Ingredient"
@@ -32,7 +35,7 @@ class ItemInfo:
             return self.finished_progress_name.replace("*", self.name)
         return self.name
 
-    def create_item(self) -> Item:
+    def create_item(self, clean_plate=False) -> Item:
         match self.type:
             case ItemType.Ingredient:
                 if self.is_cuttable:
@@ -51,7 +54,7 @@ class ItemInfo:
                         steps_needed=self.steps_needed,
                         finished=False,
                         item_info=self,
-                        clean="Clean" in self.name,
+                        clean=clean_plate,
                     )
                 else:
                     return CookingEquipment(name=self.name, item_info=self)
@@ -247,14 +250,14 @@ class Plate(CookingEquipment):
     ):
         super().__init__(content, *args, **kwargs)
         self.clean = clean
-        self.name = self.__repr__()
+        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.name = "CleanPlate"
         self.clean = True
+        self.name = self.create_name()
 
     def progress(self):
         """Progresses the item process as long as it is not finished."""
@@ -268,13 +271,10 @@ class Plate(CookingEquipment):
         return not self.clean
 
     def can_combine(self, other):
-        return super().can_combine(other) and self.clean
+        return self.clean and super().can_combine(other)
 
     def combine(self, other):
         return super().combine(other)
 
-    def __repr__(self):
-        if self.clean:
-            return "CleanPlate"
-        else:
-            return "DirtyPlate"
+    def create_name(self):
+        return "CleanPlate" if self.clean else "DirtyPlate"
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 7fb7f3fc..5c9e2419 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 import logging
 import random
 from pathlib import Path
+from threading import Lock
 from typing import Optional
 
 import numpy as np
@@ -18,7 +19,7 @@ from overcooked_simulator.counters import (
     ServingWindow,
     Stove,
     Sink,
-    DirtyPlateReturn,
+    PlateDispenser,
     SinkAddon,
 )
 from overcooked_simulator.game_items import ItemInfo
@@ -57,24 +58,6 @@ class GameScore:
         return self.score
 
 
-class PlateManager:
-    def __init__(self, max_number_plates):
-        self.max_number_plates = max_number_plates
-        self.number_returned_plates = max_number_plates
-        self.plates_out_of_kitchen = 0
-
-        self.dirty_plate_return = None
-
-    def return_plate(self):
-        self.number_returned_plates += 1
-
-    def register_plate_return(self, plate_return: DirtyPlateReturn):
-        self.dirty_plate_return = plate_return
-
-    def update_plate_return(self):
-        self.dirty_plate_return.update()
-
-
 class SinkManager:
     def __init__(self):
         self.sink: Optional[Sink] = None
@@ -95,6 +78,7 @@ class Environment:
     """
 
     def __init__(self, env_config_path: Path, layout_path, item_info_path: Path):
+        self.lock = Lock()
         self.players: dict[str, Player] = {}
 
         with open(env_config_path, "r") as file:
@@ -106,18 +90,19 @@ class Environment:
         self.item_info = self.load_item_info()
 
         self.game_score = GameScore()
-        self.plate_manager = PlateManager(environment_config["max_number_plates"])
         self.sink_manager = SinkManager()
 
         self.SYMBOL_TO_CHARACTER_MAP = {
             "#": Counter,  # because # looks a bit like a counter
             "B": CuttingBoard,
             "X": Trash,
-            "W": lambda pos: ServingWindow(pos, self.game_score, self.plate_manager),
+            "W": lambda pos: ServingWindow(pos, self.game_score),
             "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]),
             "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]),
-            "P": lambda pos: DirtyPlateReturn(
-                pos, self.item_info["Plate"], self.plate_manager
+            "P": lambda pos: PlateDispenser(
+                pos,
+                self.item_info["Plate"],
+                environment_config["plates"] if "plates" in environment_config else {},
             ),
             "N": lambda pos: Dispenser(pos, self.item_info["Onion"]),  # N for oNioN
             "_": "Free",
@@ -136,6 +121,8 @@ class Environment:
             self.free_positions,
         ) = self.parse_layout_file(self.layout_path)
 
+        self.init_counters()
+
         self.score: int = 0
 
         self.world_width: int = environment_config["world_width"]
@@ -180,8 +167,6 @@ class Environment:
                 counter_class = self.SYMBOL_TO_CHARACTER_MAP[character]
                 if not isinstance(counter_class, str):
                     counter = counter_class(pos)
-                    if isinstance(counter, DirtyPlateReturn):
-                        self.plate_manager.register_plate_return(counter)
                     counters.append(counter)
                     if isinstance(counter, SinkAddon):
                         self.sink_manager.register_sink_addon(counter)
@@ -442,9 +427,10 @@ class Environment:
         """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders
         and time limits.
         """
-        for counter in self.counters:
-            if isinstance(counter, (CuttingBoard, Stove, Sink)):
-                counter.progress()
+        with self.lock:
+            for counter in self.counters:
+                if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)):
+                    counter.progress()
 
     def get_state(self):
         """Get the current state of the game environment. The state here is accessible by the current python objects.
@@ -461,3 +447,23 @@ class Environment:
 
         """
         pass
+
+    def init_counters(self):
+        plate_dispenser = self.get_counter_of_type(PlateDispenser)
+        assert len(plate_dispenser) > 0, "No Plate Return in the environment"
+
+        for counter in self.counters:
+            match counter:
+                case ServingWindow():
+                    counter.add_plate_dispenser(plate_dispenser[0])
+                case Sink():
+                    ...
+                case SinkAddon():
+                    ...
+
+        pass
+
+    def get_counter_of_type(self, counter_type) -> list[Counter]:
+        return list(
+            filter(lambda counter: isinstance(counter, counter_type), self.counters)
+        )
diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py
index 49317e40..00f90bce 100644
--- a/overcooked_simulator/player.py
+++ b/overcooked_simulator/player.py
@@ -1,4 +1,5 @@
 import logging
+from collections import deque
 from pathlib import Path
 from typing import Optional
 
@@ -107,7 +108,9 @@ class Player:
         elif counter.can_drop_off(self.holding):
             self.holding = counter.drop_off(self.holding)
 
-        elif not isinstance(counter.occupied_by, list) and self.holding.can_combine(counter.occupied_by):
+        elif not isinstance(
+            counter.occupied_by, (list, deque)
+        ) and self.holding.can_combine(counter.occupied_by):
             returned_by_counter = counter.pick_up(on_hands=False)
             self.holding.combine(returned_by_counter)
 
diff --git a/overcooked_simulator/pygame_gui/pygame_gui.py b/overcooked_simulator/pygame_gui/pygame_gui.py
index 5b0070c2..e4ec3b06 100644
--- a/overcooked_simulator/pygame_gui/pygame_gui.py
+++ b/overcooked_simulator/pygame_gui/pygame_gui.py
@@ -2,6 +2,7 @@ import colorsys
 import logging
 import math
 import sys
+from collections import deque
 
 import numpy as np
 import numpy.typing as npt
@@ -375,11 +376,12 @@ class PyGameGUI:
 
         if counter.occupied_by is not None:
             # Multiple plates on plate return:
-            if isinstance(counter.occupied_by, list):
-                for i, o in enumerate(counter.occupied_by):
-                    self.draw_item(
-                        np.abs([counter.pos[0], counter.pos[1] - (i * 3)]), o
-                    )
+            if isinstance(counter.occupied_by, (list, deque)):
+                with self.simulator.env.lock:
+                    for i, o in enumerate(counter.occupied_by):
+                        self.draw_item(
+                            np.abs([counter.pos[0], counter.pos[1] - (i * 3)]), o
+                        )
             # All other items:
             else:
                 self.draw_item(counter.pos, counter.occupied_by)
diff --git a/overcooked_simulator/pygame_gui/visualization.yaml b/overcooked_simulator/pygame_gui/visualization.yaml
index 05d58ec7..62a327a4 100644
--- a/overcooked_simulator/pygame_gui/visualization.yaml
+++ b/overcooked_simulator/pygame_gui/visualization.yaml
@@ -21,7 +21,7 @@ CuttingBoard:
       center_offset: [ +0.15, -0.2 ]
       color: silver
 
-DirtyPlateReturn:
+PlateDispenser:
   parts:
     - type: rect
       height: 0.95
diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py
index 4d38ac34..795b61e6 100644
--- a/overcooked_simulator/simulation_runner.py
+++ b/overcooked_simulator/simulation_runner.py
@@ -2,6 +2,8 @@ import logging
 import time
 from threading import Thread
 
+import numpy as np
+
 from overcooked_simulator import ROOT_DIR
 from overcooked_simulator.overcooked_environment import Environment, Action
 
@@ -28,12 +30,17 @@ class Simulator(Thread):
         layout_path,
         frequency: int,
         item_info_path=ROOT_DIR / "game_content" / "item_info.yaml",
+        seed: int = 8654321,
     ):
+        # TODO look at https://builtin.com/data-science/numpy-random-seed to change to other random
+        np.random.seed(seed)
         self.finished: bool = False
 
         self.step_frequency: int = frequency
         self.preferred_sleep_time_ns: float = 1e9 / self.step_frequency
-        self.env: Environment = Environment(env_layout_path, layout_path, item_info_path)
+        self.env: Environment = Environment(
+            env_layout_path, layout_path, item_info_path
+        )
 
         super().__init__()
 
-- 
GitLab