From 14a081cc975ae44ced84dfb4be0df24cbc78172f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Schr=C3=B6der?=
 <fschroeder@techfak.uni-bielefeld.de>
Date: Sun, 10 Mar 2024 13:07:14 +0100
Subject: [PATCH] Update comparisons and variable types, add tests

This commit modifies comparison operators for timers to be less than or equal in various scripts. It also changes variable types explicitly in scores.py and adds several new tests for functionalities like effects, scores, information messages, trashing function etc. Additional updates include changes in values for certain default arguments in command line parser for generating images in the script for 2D visualization.
---
 cooperative_cuisine/counters.py              |   4 +-
 cooperative_cuisine/effects.py               |   4 +-
 cooperative_cuisine/info_msg.py              |   2 +-
 cooperative_cuisine/pygame_2d_vis/drawing.py |  25 +-
 cooperative_cuisine/scores.py                |  10 +-
 tests/test_game_server.py                    |   7 +
 tests/test_pygame.py                         |  15 +-
 tests/test_start.py                          | 260 ++++++++++++++++++-
 8 files changed, 308 insertions(+), 19 deletions(-)

diff --git a/cooperative_cuisine/counters.py b/cooperative_cuisine/counters.py
index 01421b21..cd84be94 100644
--- a/cooperative_cuisine/counters.py
+++ b/cooperative_cuisine/counters.py
@@ -684,10 +684,10 @@ class PlateDispenser(Counter):
 
     def progress(self, passed_time: timedelta, now: datetime):
         """Check if plates arrive from outside the kitchen and add a dirty plate accordingly"""
-        if self.next_plate_time < now:
+        if self.next_plate_time <= now:
             idx_delete = []
             for i, times in enumerate(self.out_of_kitchen_timer):
-                if times < now:
+                if times <= now:
                     self.hook(DIRTY_PLATE_ARRIVES, counter=self, times=times, now=now)
                     idx_delete.append(i)
                     log.debug("Add dirty plate")
diff --git a/cooperative_cuisine/effects.py b/cooperative_cuisine/effects.py
index 3688ce64..53989c09 100644
--- a/cooperative_cuisine/effects.py
+++ b/cooperative_cuisine/effects.py
@@ -163,9 +163,9 @@ class FireEffectManager(EffectManager):
                 self.active_effects.append((effect, target))
             # reset new effects
             self.new_effects = []
-        if self.next_finished_timer < now:
+        if self.next_finished_timer <= now:
             for effect, target in self.active_effects:
-                if self.effect_to_timer[effect.uuid] < now:
+                if self.effect_to_timer[effect.uuid] <= now:
                     if isinstance(target, Item):
                         target = find_item_on_counters(target.uuid, self.counters)
                     if target:
diff --git a/cooperative_cuisine/info_msg.py b/cooperative_cuisine/info_msg.py
index 3d6d9525..7899e9ce 100644
--- a/cooperative_cuisine/info_msg.py
+++ b/cooperative_cuisine/info_msg.py
@@ -95,7 +95,7 @@ class InfoMsgManager(HookCallbackClass):
         for player_id, msgs in env.info_msgs_per_player.items():
             delete_msgs = []
             for idx, msg in enumerate(msgs):
-                if msg["end_time"] < env.env_time:
+                if msg["end_time"] <= env.env_time:
                     delete_msgs.append(idx)
             for idx in reversed(delete_msgs):
                 msgs.pop(idx)
diff --git a/cooperative_cuisine/pygame_2d_vis/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py
index f9a99462..0c67eaf1 100644
--- a/cooperative_cuisine/pygame_2d_vis/drawing.py
+++ b/cooperative_cuisine/pygame_2d_vis/drawing.py
@@ -2,6 +2,7 @@ import argparse
 import colorsys
 import json
 import os
+import sys
 from datetime import datetime, timedelta
 from pathlib import Path
 
@@ -1002,7 +1003,21 @@ def generate_recipe_images(config: dict, folder_path: str | Path):
         pygame.image.save(screen, f"{folder_path}/{graph_dict['meal']}.png")
 
 
-if __name__ == "__main__":
+def main(args):
+    """
+
+    Runs the Cooperative Cuisine Image Generation process.
+
+    This method takes command line arguments to specify the state file, visualization configuration file, and output file for the generated image. It then reads the visualization configuration
+    * file and state file, and calls the 'save_screenshot' and 'generate_recipe_images' methods to generate the image.
+
+    Args:
+        -s, --state: A command line argument of type `argparse.FileType("r", encoding="UTF-8")`. Specifies the state file to use for image generation. If not provided, the default value is 'ROOT_DIR / "pygame_2d_vis" / "sample_state.json"'.
+
+        -v, --visualization_config: A command line argument of type `argparse.FileType("r", encoding="UTF-8")`. Specifies the visualization configuration file to use for image generation. If not provided, the default value is 'ROOT_DIR / "pygame_2d_vis" / "visualization.yaml"'.
+
+        -o, --output_file: A command line argument of type `str`. Specifies the output file path for the generated image. If not provided, the default value is 'ROOT_DIR / "generated" / "screenshot.jpg"'.
+    """
     parser = argparse.ArgumentParser(
         prog="Cooperative Cuisine Image Generation",
         description="Generate images for a state in json.",
@@ -1024,12 +1039,16 @@ if __name__ == "__main__":
         "-o",
         "--output_file",
         type=str,
-        default=ROOT_DIR / "pygame_2d_vis" / "generated" / "screenshot.jpg",
+        default=ROOT_DIR / "generated" / "screenshot.jpg",
     )
-    args = parser.parse_args()
+    args = parser.parse_args(args)
     with open(args.visualization_config, "r") as f:
         viz_config = yaml.safe_load(f)
     with open(args.state, "r") as f:
         state = json.load(f)
     save_screenshot(state, viz_config, args.output_file)
     generate_recipe_images(viz_config, args.output_file.parent)
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/cooperative_cuisine/scores.py b/cooperative_cuisine/scores.py
index 5ae2c6ce..948d28f6 100644
--- a/cooperative_cuisine/scores.py
+++ b/cooperative_cuisine/scores.py
@@ -94,13 +94,13 @@ class ScoreViaHooks(HookCallbackClass):
             **kwargs: Additional keyword arguments to be passed to the parent class.
         """
         super().__init__(name, env, **kwargs)
-        self.score_map = score_map
+        self.score_map: dict[str, float] = score_map
         """Mapping of hook references to scores."""
-        self.static_score = static_score
+        self.static_score: float = static_score
         """The static score to be added if no other conditions are met."""
-        self.kwarg_filter = kwarg_filter
+        self.kwarg_filter: dict[str, Any] = kwarg_filter
         """Filtering condition for keyword arguments."""
-        self.score_on_specific_kwarg = score_on_specific_kwarg
+        self.score_on_specific_kwarg: str = score_on_specific_kwarg
         """The specific keyword argument to score on."""
 
     def __call__(self, hook_ref: str, env: Environment, **kwargs):
@@ -114,7 +114,7 @@ class ScoreViaHooks(HookCallbackClass):
                 self.env.increment_score(self.static_score, info=hook_ref)
         elif self.score_map and hook_ref in self.score_map:
             if self.kwarg_filter:
-                if kwargs.items() <= self.kwarg_filter.items():
+                if self.kwarg_filter.items() <= kwargs.items():
                     self.env.increment_score(
                         self.score_map[hook_ref],
                         info=f"{hook_ref} - {self.kwarg_filter}",
diff --git a/tests/test_game_server.py b/tests/test_game_server.py
index f072fed2..84271ad7 100644
--- a/tests/test_game_server.py
+++ b/tests/test_game_server.py
@@ -282,3 +282,10 @@ def test_websocket_wrong_inputs(create_env_config):
         finally:
             task.cancel()
             loop.close()
+
+
+def test_root():
+    with TestClient(app) as client:
+        res = client.get("/")
+        assert res.status_code == status.HTTP_200_OK
+        assert res.json() == {"Cooperative": "Cuisine"}
diff --git a/tests/test_pygame.py b/tests/test_pygame.py
index c4456951..9df43e08 100644
--- a/tests/test_pygame.py
+++ b/tests/test_pygame.py
@@ -1,7 +1,10 @@
+import json
+import os
+
 import yaml
 
 from cooperative_cuisine import ROOT_DIR
-from cooperative_cuisine.pygame_2d_vis.drawing import calc_angle, Visualizer
+from cooperative_cuisine.pygame_2d_vis.drawing import calc_angle, Visualizer, main
 from cooperative_cuisine.pygame_2d_vis.game_colors import RGB, WHITE, colors
 
 
@@ -16,6 +19,11 @@ def test_calc_angle():
     assert calc_angle([1.0, 1.0], [1.0, -1.0]) == -90.0
 
 
+def test_drawing():
+    main([])
+    assert os.path.exists(os.path.join(ROOT_DIR, "generated", "screenshot.jpg"))
+
+
 def test_visualizer():
     with open(ROOT_DIR / "pygame_2d_vis" / "visualization.yaml", "r") as file:
         visualization_config = yaml.safe_load(file)
@@ -25,3 +33,8 @@ def test_visualizer():
     vis.create_player_colors(10)
     assert len(vis.player_colors) >= 10
     assert len(set(vis.player_colors)) >= 10
+
+    with open(ROOT_DIR / "pygame_2d_vis" / "sample_state.json", "r") as file:
+        state = json.load(file)
+    image = vis.get_state_image(40, state)
+    assert image.shape == (480, 360, 3)
diff --git a/tests/test_start.py b/tests/test_start.py
index fec31349..744bf686 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -2,16 +2,32 @@ from datetime import timedelta
 
 import numpy as np
 import pytest
+import yaml
 
 from cooperative_cuisine import ROOT_DIR
 from cooperative_cuisine.action import ActionType, InterActionData, Action
-from cooperative_cuisine.counters import Counter, CuttingBoard
+from cooperative_cuisine.counters import (
+    Counter,
+    CuttingBoard,
+    CookingCounter,
+    ServingWindow,
+    Trashcan,
+)
+from cooperative_cuisine.effects import FireEffectManager
 from cooperative_cuisine.environment import (
     Environment,
 )
 from cooperative_cuisine.game_server import PlayerRequestType
-from cooperative_cuisine.hooks import Hooks
-from cooperative_cuisine.items import Item, ItemInfo, ItemType
+from cooperative_cuisine.hooks import (
+    Hooks,
+    SERVE_NOT_ORDERED_MEAL,
+    hooks_via_callback_class,
+    PLAYER_ADDED,
+    POST_STEP,
+)
+from cooperative_cuisine.info_msg import InfoMsgManager
+from cooperative_cuisine.items import Item, ItemInfo, ItemType, Plate, CookingEquipment
+from cooperative_cuisine.scores import ScoreViaHooks
 from cooperative_cuisine.server_results import (
     PlayerInfo,
     CreateEnvResult,
@@ -21,7 +37,7 @@ from cooperative_cuisine.state_representation import (
     StateRepresentation,
     create_json_schema,
 )
-from cooperative_cuisine.utils import create_init_env_time
+from cooperative_cuisine.utils import create_init_env_time, get_touching_counters
 
 layouts_folder = ROOT_DIR / "configs" / "layouts"
 environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml"
@@ -54,7 +70,6 @@ def layout_config():
     with open(layout_path, "r") as file:
         layout = file.read()
     return layout
-    env.add_player("0")
 
 
 @pytest.fixture
@@ -218,6 +233,7 @@ def test_processing(env_config, layout_config, item_info):
         },
     )
     env.counters.append(counter)
+    env.overwrite_counters(env.counters)
 
     tomato = Item(name="Tomato", item_info=None)
     env.add_player("1", np.array([2, 3]))
@@ -308,3 +324,237 @@ def test_server_result_definition():
         msg="123",
         player_hash="1234324",
     )
+
+
+def test_fire(env_config, layout_config, item_info):
+    env = Environment(env_config, layout_config, item_info, as_files=False)
+    env.add_player("0")
+    oven = None
+    for c in env.counters:
+        if (
+            isinstance(c, CookingCounter)
+            and c.name == "Stove"
+            and c.occupied_by.name == "Pan"
+        ):
+            oven = c
+            break
+    assert oven is not None
+
+    raw_patty = Item(name="RawPatty", item_info=env.item_info["RawPatty"])
+
+    assert oven.can_drop_off(raw_patty)
+
+    oven.drop_off(raw_patty, "0")
+    assert isinstance(oven.occupied_by, CookingEquipment)
+    assert oven.occupied_by.content_list == [raw_patty]
+
+    env.step(timedelta(seconds=env.item_info["CookedPatty"].seconds))
+    assert oven.occupied_by.content_list[0].name == "CookedPatty"
+    env.step(timedelta(seconds=env.item_info["BurntCookedPatty"].seconds))
+    assert oven.occupied_by.content_list[0].name == "BurntCookedPatty"
+    env.step(timedelta(seconds=env.item_info["Fire"].seconds))
+    assert len(oven.occupied_by.active_effects) != 0
+    assert oven.occupied_by.active_effects[0].name == "Fire"
+
+    fire_manager = env.effect_manager["FireManager"]
+    assert isinstance(fire_manager, FireEffectManager)
+    env.step(fire_manager.next_finished_timer - env.env_time)
+
+    touching_counters = get_touching_counters(oven, env.counters)
+    next_empty = None
+    connect_counter = None
+    for c in touching_counters:
+        if c.occupied_by:
+            assert len(c.occupied_by.active_effects) == 1
+        else:
+            assert len(c.active_effects) == 1
+            next_touching = get_touching_counters(c, env.counters)
+            for a in next_touching:
+                if a not in touching_counters and a.__class__.__name__ == "Counter":
+                    a.occupied_by = None
+                    next_empty = a
+                    connect_counter = c
+    env.step(timedelta(seconds=0.01))
+    assert next_empty is not None
+    next_empty.occupied_by = Item(name="Tomato", item_info=env.item_info["Tomato"])
+    env.step(
+        fire_manager.effect_to_timer[connect_counter.active_effects[0].uuid]
+        - env.env_time
+    )
+    assert len(next_empty.occupied_by.active_effects) == 1
+
+    fire_extinguisher = None
+    for c in env.counters:
+        if c.occupied_by and c.occupied_by.name == "Extinguisher":
+            fire_extinguisher = c.occupied_by
+            c.occupied_by = None
+            break
+
+    assert fire_extinguisher is not None
+    env.players["0"].holding = fire_extinguisher
+    env.players["0"].pos = oven.pos
+    env.players["0"].pos[1] -= 1.0
+    env.perform_action(
+        Action(
+            player="0",
+            action_type=ActionType.MOVEMENT,
+            action_data=np.array([0.0, -1.0]),
+            duration=0.1,
+        )
+    )
+    env.step(timedelta(seconds=0.1))
+
+    env.perform_action(
+        Action(
+            player="0",
+            action_type=ActionType.INTERACT,
+            action_data=InterActionData.START,
+        )
+    )
+    env.step(timedelta(seconds=env.item_info["Extinguisher"].seconds))
+    env.step(timedelta(seconds=env.item_info["Extinguisher"].seconds))
+
+    assert len(oven.occupied_by.active_effects) == 0
+
+
+def test_score(env_config, layout_config, item_info):
+    def incr_score_callback(hook_ref, env: Environment, meal, meal_name, **kwargs):
+        assert isinstance(meal, Item)
+        assert isinstance(meal_name, str)
+        assert meal_name == "TomatoSoup"
+        env.increment_score(1_000, "Soup Soup")
+
+    env = Environment(env_config, layout_config, item_info, as_files=False)
+    assert env.score == 0.0
+    env.add_player("0")
+    env.register_callback_for_hook(SERVE_NOT_ORDERED_MEAL, incr_score_callback)
+    env.register_callback_for_hook(
+        SERVE_NOT_ORDERED_MEAL,
+        ScoreViaHooks(
+            name="123",
+            env=env,
+            score_on_specific_kwarg="meal_name",
+            score_map={"TomatoSoup": 2_000},
+        ),
+    )
+    env.register_callback_for_hook(
+        SERVE_NOT_ORDERED_MEAL,
+        ScoreViaHooks(name="124", env=env, score_map={SERVE_NOT_ORDERED_MEAL: 4_000}),
+    )
+    env.register_callback_for_hook(
+        SERVE_NOT_ORDERED_MEAL,
+        ScoreViaHooks(
+            name="124",
+            env=env,
+            score_map={SERVE_NOT_ORDERED_MEAL: 8_000},
+            kwarg_filter={"meal_name": "TomatoSoup"},
+        ),
+    )
+    env.register_callback_for_hook(
+        SERVE_NOT_ORDERED_MEAL,
+        ScoreViaHooks(
+            name="123",
+            env=env,
+            score_on_specific_kwarg="meal_name",
+            static_score=16_000,
+            score_map={},
+        ),
+    )
+    serving_window = None
+    for c in env.counters:
+        if isinstance(c, ServingWindow):
+            serving_window = c
+            break
+    assert serving_window is not None
+    env.order_manager.serving_not_ordered_meals = True
+    env.order_manager.open_orders = []
+    plate = Plate(
+        transitions=env.counter_factory.filter_item_info(
+            by_item_type=ItemType.Meal, add_effects=True
+        ),
+        clean=True,
+        item_info=env.item_info["Plate"],
+        hook=env.hook,
+    )
+    plate.content_list = [
+        Item(name="TomatoSoup", item_info=env.item_info["TomatoSoup"])
+    ]
+    assert serving_window.can_drop_off(plate)
+
+    returned = serving_window.drop_off(plate, "0")
+    assert returned is None
+
+    plates_prev = len(serving_window.plate_dispenser.occupied_by)
+    assert env.score >= 31_000
+    assert len(serving_window.plate_dispenser.out_of_kitchen_timer) == 1
+    env.step(serving_window.plate_dispenser.out_of_kitchen_timer[0] - env.env_time)
+    assert len(serving_window.plate_dispenser.out_of_kitchen_timer) == 0
+    assert len(serving_window.plate_dispenser.occupied_by) == plates_prev + 1
+    assert (
+        serving_window.plate_dispenser.occupied_by[0].clean
+        != serving_window.plate_dispenser.plate_config.return_dirty
+    )
+
+
+def test_info_msgs(env_config, layout_config, item_info):
+    env_config_dict = yaml.load(env_config, Loader=yaml.Loader)
+    # TODO change after merge with 115
+    env_config_dict["extra_setup_functions"]["dummy_msg"] = {
+        "func": hooks_via_callback_class,
+        "kwargs": {
+            "hooks": [PLAYER_ADDED],
+            "callback_class": InfoMsgManager,
+            "callback_class_kwargs": {"msg": "hello there"},
+        },
+    }
+
+    env_config_dict["extra_setup_functions"]["dummy_msg_2"] = {
+        "func": hooks_via_callback_class,
+        "kwargs": {
+            "hooks": [POST_STEP],
+            "callback_class": InfoMsgManager,
+            "callback_class_kwargs": {"msg": "step step"},
+        },
+    }
+
+    env_config = yaml.dump(env_config_dict)
+
+    env = Environment(env_config, layout_config, item_info, as_files=False)
+    env.add_player("0")
+    assert env.info_msgs_per_player["0"][0]["msg"] == "hello there"
+    env.step(timedelta(seconds=0.1))
+    assert env.info_msgs_per_player["0"][1]["msg"] == "step step"
+    env.step(env.info_msgs_per_player["0"][0]["end_time"] - env.env_time)
+    assert len(env.info_msgs_per_player["0"]) == 2
+    assert env.info_msgs_per_player["0"][0]["msg"] == "step step"
+
+
+def test_trashcan(env_config, layout_config, item_info):
+    env = Environment(env_config, layout_config, item_info, as_files=False)
+    env.add_player("0")
+    trash = None
+    for c in env.counters:
+        if isinstance(c, Trashcan):
+            trash = c
+            break
+    assert trash is not None
+
+    item = Item(name="Tomato", item_info=env.item_info["Tomato"])
+    assert trash.can_drop_off(item)
+    assert trash.drop_off(item, "0") is None
+
+    plate = Plate(
+        transitions=env.counter_factory.filter_item_info(
+            by_item_type=ItemType.Meal, add_effects=True
+        ),
+        clean=True,
+        item_info=env.item_info["Plate"],
+        hook=env.hook,
+    )
+    plate.content_list = [
+        Item(name="TomatoSoup", item_info=env.item_info["TomatoSoup"])
+    ]
+
+    assert trash.can_drop_off(plate)
+    assert trash.drop_off(plate, "0") == plate
+    assert plate.content_list == []
-- 
GitLab