From a03b1511ac741e88938087ba0cf1a50a846a298a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Schr=C3=B6der?=
 <fschroeder@techfak.uni-bielefeld.de>
Date: Thu, 21 Dec 2023 14:08:47 +0100
Subject: [PATCH] use an env time instead of datetime now. Pass the passed time
 via the step function. Add a test

---
 overcooked_simulator/counters.py              | 16 +++++----
 .../overcooked_environment.py                 | 13 ++++++--
 overcooked_simulator/simulation_runner.py     |  9 ++---
 overcooked_simulator/utils.py                 |  7 ++++
 tests/test_start.py                           | 33 ++++++++++++++++---
 5 files changed, 62 insertions(+), 16 deletions(-)
 create mode 100644 overcooked_simulator/utils.py

diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py
index 9694c824..616ad694 100644
--- a/overcooked_simulator/counters.py
+++ b/overcooked_simulator/counters.py
@@ -5,6 +5,8 @@ from collections import deque
 from datetime import datetime, timedelta
 from typing import TYPE_CHECKING, Optional
 
+from overcooked_simulator.utils import create_init_env_time
+
 if TYPE_CHECKING:
     from overcooked_simulator.overcooked_environment import (
         GameScore,
@@ -97,7 +99,7 @@ class CuttingBoard(Counter):
         self.progressing = False
         super().__init__(pos)
 
-    def progress(self):
+    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):
@@ -194,6 +196,7 @@ class PlateDispenser(Counter):
         self.plate_config = {"plate_delay": [5, 10]}
         self.plate_config.update(plate_config)
         self.next_plate_time = datetime.max
+        self.env_time = create_init_env_time()  # is overwritten in progress anyway
         self.setup_plates()
 
     def pick_up(self, on_hands: bool = True):
@@ -223,7 +226,8 @@ class PlateDispenser(Counter):
 
     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(
+        # not perfect identical to datetime.now but based on framerate enough.
+        time_plate_to_add = self.env_time + timedelta(
             seconds=np.random.uniform(
                 low=self.plate_config["plate_delay"][0],
                 high=self.plate_config["plate_delay"][1],
@@ -251,9 +255,9 @@ class PlateDispenser(Counter):
                 ]
             )
 
-    def progress(self):
+    def progress(self, passed_time: timedelta, now: datetime):
         """Check if plates arrive from outside the kitchen and add a dirty plate accordingly"""
-        now = datetime.now()
+        self.env_time = now
         if self.next_plate_time < now:
             idx_delete = []
             for i, times in enumerate(self.out_of_kitchen_timer):
@@ -294,7 +298,7 @@ class Stove(Counter):
         else:
             return self.occupied_by.can_combine(item)
 
-    def progress(self):
+    def progress(self, passed_time: timedelta, now: datetime):
         """Called by environment step function for time progression"""
         if (
             self.occupied_by
@@ -311,7 +315,7 @@ class Sink(Counter):
         self.sink_addon: SinkAddon = sink_addon
         self.occupied_by = deque()
 
-    def progress(self):
+    def progress(self, passed_time: timedelta, now: datetime):
         """Called by environment step function for time progression"""
         if self.progressing:
             if self.occupied_by:
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 8a51ecf4..d049f913 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import logging
 import random
+from datetime import timedelta
 from pathlib import Path
 from threading import Lock
 
@@ -23,6 +24,7 @@ from overcooked_simulator.counters import (
 )
 from overcooked_simulator.game_items import ItemInfo, ItemType
 from overcooked_simulator.player import Player
+from overcooked_simulator.utils import create_init_env_time
 
 log = logging.getLogger(__name__)
 
@@ -131,6 +133,8 @@ class Environment:
         self.world_width: int = environment_config["world_width"]
         self.world_height: int = environment_config["world_height"]
 
+        self.env_time = create_init_env_time()
+
     def load_item_info(self) -> dict[str, ItemInfo]:
         with open(self.item_info_path, "r") as file:
             item_lookup = yaml.safe_load(file)
@@ -429,14 +433,15 @@ class Environment:
         )
         return collisions_lower or collisions_upper
 
-    def step(self):
+    def step(self, passed_time: timedelta):
         """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders
         and time limits.
         """
+        self.env_time += passed_time
         with self.lock:
             for counter in self.counters:
                 if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)):
-                    counter.progress()
+                    counter.progress(passed_time=passed_time, now=self.env_time)
 
     def get_state(self):
         """Get the current state of the game environment. The state here is accessible by the current python objects.
@@ -484,3 +489,7 @@ class Environment:
         return list(
             filter(lambda counter: isinstance(counter, counter_type), self.counters)
         )
+
+    def reset_env_time(self):
+        self.env_time = create_init_env_time()
+        log.debug(f"Reset env time to {self.env_time}")
diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py
index 795b61e6..c1f32c80 100644
--- a/overcooked_simulator/simulation_runner.py
+++ b/overcooked_simulator/simulation_runner.py
@@ -1,5 +1,6 @@
 import logging
 import time
+from datetime import timedelta
 from threading import Thread
 
 import numpy as np
@@ -44,9 +45,9 @@ class Simulator(Thread):
 
         super().__init__()
 
-    def step(self):
+    def step(self, passed_time: timedelta):
         """One simulation step of the environment."""
-        self.env.step()
+        self.env.step(passed_time)
 
     def enter_action(self, action: Action):
         """Takes an action and executes it in the environment.
@@ -96,10 +97,10 @@ class Simulator(Thread):
         """Starts the simulator thread. Runs in a loop until stopped."""
 
         overslept_in_ns = 0
-
+        self.env.reset_env_time()
         while not self.finished:
             step_start = time.time_ns()
-            self.step()
+            self.step(timedelta(seconds=overslept_in_ns / 1_000_000_000))
             step_duration = time.time_ns() - step_start
 
             time_to_sleep_ns = self.preferred_sleep_time_ns - (
diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py
new file mode 100644
index 00000000..4e4ab7ad
--- /dev/null
+++ b/overcooked_simulator/utils.py
@@ -0,0 +1,7 @@
+from datetime import datetime
+
+
+def create_init_env_time():
+    return datetime(
+        year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
+    )
diff --git a/tests/test_start.py b/tests/test_start.py
index a8fc103e..ac5a63ba 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -1,4 +1,5 @@
 import time
+from datetime import timedelta
 
 import numpy as np
 import pytest
@@ -6,8 +7,9 @@ import pytest
 from overcooked_simulator import ROOT_DIR
 from overcooked_simulator.counters import Counter, CuttingBoard
 from overcooked_simulator.game_items import CuttableItem
-from overcooked_simulator.overcooked_environment import Action
+from overcooked_simulator.overcooked_environment import Action, Environment
 from overcooked_simulator.simulation_runner import Simulator
+from overcooked_simulator.utils import create_init_env_time
 
 layouts_folder = ROOT_DIR / "game_content" / "layouts"
 
@@ -51,9 +53,12 @@ def test_simulator_frequency():
         def __init__(self):
             self.c = 0
 
-        def step(self):
+        def step(self, passed_time):
             self.c += 1
 
+        def reset_env_time(self):
+            pass
+
     frequency = 2000
     running_time_seconds = 2
 
@@ -68,7 +73,6 @@ def test_simulator_frequency():
     time.sleep(running_time_seconds)
     sim.stop()
 
-    print(sim.env.c)
     accepted_tolerance = 0.02
     lower = frequency * running_time_seconds * (1 - accepted_tolerance)
     upper = frequency * running_time_seconds * (1 + accepted_tolerance)
@@ -216,7 +220,7 @@ def test_processing():
 
     counter_pos = np.array([100, 100])
     counter = CuttingBoard(counter_pos)
-    sim.env.counters = [counter]
+    sim.env.counters.append(counter)
 
     tomato = CuttableItem(name="Tomato", item_info=None)
     sim.register_player("p1", np.array([100, 150]))
@@ -243,3 +247,24 @@ def test_processing():
     sim.enter_action(button_up)
 
     sim.stop()
+
+
+def test_time_passed():
+    np.random.seed(42)
+    env = Environment(
+        ROOT_DIR / "game_content" / "environment_config.yaml",
+        layouts_folder / "empty.layout",
+        ROOT_DIR / "game_content" / "item_info.yaml",
+    )
+    env.reset_env_time()
+    passed_time = timedelta(seconds=10)
+    env.step(passed_time)
+    assert (
+        env.env_time == create_init_env_time() + passed_time
+    ), "Env time needs to be updated via the step function"
+
+    passed_time_2 = timedelta(seconds=12)
+    env.step(passed_time_2)
+    assert (
+        env.env_time == create_init_env_time() + passed_time + passed_time_2
+    ), "Env time needs to be updated via the step function"
-- 
GitLab