From 5055e50308d4ced3d4346e2ac5fd9b26cb61cf59 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Schr=C3=B6der?=
 <fschroeder@techfak.uni-bielefeld.de>
Date: Fri, 9 Feb 2024 12:24:11 +0100
Subject: [PATCH] Add in-game information messages feature

The updates have introduced an in-game information messages feature, allowing messages to be delivered to players during gameplay. This could be used to notify about certain events or conditions such as new fires or fire spreading. The updates also included the necessary hooks and message levels, which are 'Normal', 'Warning', and 'Success'. Furthermore, these messages can be displayed in the game's GUI.
---
 overcooked_simulator/effect_manager.py        |  4 ++-
 .../game_content/environment_config.yaml      | 16 ++++++++++-
 .../gui_2d_vis/overcooked_gui.py              | 17 +++++++++++
 overcooked_simulator/hooks.py                 |  4 +++
 overcooked_simulator/info_msg.py              | 28 +++++++++++++++++++
 .../overcooked_environment.py                 | 11 +++++++-
 overcooked_simulator/state_representation.py  | 15 ++++++++++
 7 files changed, 92 insertions(+), 3 deletions(-)
 create mode 100644 overcooked_simulator/info_msg.py

diff --git a/overcooked_simulator/effect_manager.py b/overcooked_simulator/effect_manager.py
index bb13ee97..7e18e17e 100644
--- a/overcooked_simulator/effect_manager.py
+++ b/overcooked_simulator/effect_manager.py
@@ -12,7 +12,7 @@ from overcooked_simulator.game_items import (
     Effect,
     CookingEquipment,
 )
-from overcooked_simulator.hooks import Hooks
+from overcooked_simulator.hooks import Hooks, NEW_FIRE, FIRE_SPREADING
 from overcooked_simulator.utils import get_touching_counters, find_item_on_counters
 
 if TYPE_CHECKING:
@@ -74,6 +74,7 @@ class FireEffectManager(EffectManager):
                 self.next_finished_timer = min(
                     self.next_finished_timer, self.effect_to_timer[effect.uuid]
                 )
+                self.hook(NEW_FIRE, target=target)
                 self.active_effects.append((effect, target))
             self.new_effects = []
         if self.next_finished_timer < now:
@@ -110,6 +111,7 @@ class FireEffectManager(EffectManager):
         if effect.name not in target.active_effects and target.uuid not in [
             t.uuid for _, t in self.active_effects
         ]:
+            self.hook(FIRE_SPREADING, target=target)
             if isinstance(target, CookingEquipment):
                 if target.content_list:
                     for content in target.content_list:
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
index fdbe1db2..fc792099 100644
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ b/overcooked_simulator/game_content/environment_config.yaml
@@ -129,4 +129,18 @@ extra_setup_functions:
       log_class_kwargs:
         log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
         add_hook_ref: true
-
+  info_msg:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ cutting_board_100 ]
+      log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager ''
+      log_class_kwargs:
+        msg: Glückwunsch du hast was geschnitten!
+  fire_msg:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ new_fire ]
+      log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager ''
+      log_class_kwargs:
+        msg: Feuer, Feuer, Feuer
+        level: Warning
\ No newline at end of file
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
index 8808d1db..4f20e235 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -604,6 +604,22 @@ class PyGameGUI:
 
         self.update_score_label(state)
 
+        if state["info_msg"]:
+            text_surface = self.comic_sans.render(
+                state["info_msg"][0][0],
+                antialias=True,
+                color=(0, 0, 0)
+                if state["info_msg"][0][1] == "Normal"
+                else (
+                    (255, 0, 0) if state["info_msg"][0][1] == "Warning" else (0, 255, 0)
+                ),
+                # bgcolor=(255, 255, 255),
+            )
+            self.main_window.blit(
+                text_surface,
+                (self.window_width / 4, self.window_height - self.screen_margin + 5),
+            )
+
     def set_window_size(self):
         self.game_screen = pygame.Surface(
             (
@@ -965,6 +981,7 @@ class PyGameGUI:
         log.debug(f"Starting pygame gui at {self.FPS} fps")
         pygame.init()
         pygame.font.init()
+        self.comic_sans = pygame.font.SysFont("Comic Sans MS", 30)
 
         pygame.display.set_caption("Simple Overcooked Simulator")
 
diff --git a/overcooked_simulator/hooks.py b/overcooked_simulator/hooks.py
index 285ed735..67c54629 100644
--- a/overcooked_simulator/hooks.py
+++ b/overcooked_simulator/hooks.py
@@ -96,6 +96,10 @@ ACTION_PUT = "action_put"
 
 ACTION_INTERACT_START = "action_interact_start"
 
+NEW_FIRE = "new_fire"
+
+FIRE_SPREADING = "fire_spreading"
+
 
 class Hooks:
     def __init__(self, env):
diff --git a/overcooked_simulator/info_msg.py b/overcooked_simulator/info_msg.py
new file mode 100644
index 00000000..9f88f5ff
--- /dev/null
+++ b/overcooked_simulator/info_msg.py
@@ -0,0 +1,28 @@
+from datetime import timedelta
+
+from overcooked_simulator.overcooked_environment import Environment
+
+
+class InfoMsgManager:
+    def __init__(
+        self,
+        name: str,
+        env: Environment,
+        msg: str,
+        duration: int = 5,
+        level: str = "Normal",
+    ):
+        self.msg = msg
+        self.duration = timedelta(seconds=duration)
+        self.level = level
+
+    def __call__(self, hook_ref: str, env: Environment, **kwargs):
+        for player_id in env.players:
+            env.info_msgs_per_player[player_id].append(
+                {
+                    "msg": self.msg,
+                    "start_time": env.env_time,
+                    "end_time": env.env_time + self.duration,
+                    "level": self.level,
+                }
+            )
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 5302f55a..a119d34c 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -5,6 +5,7 @@ import inspect
 import json
 import logging
 import sys
+from collections import defaultdict
 from datetime import timedelta, datetime
 from enum import Enum
 from pathlib import Path
@@ -50,7 +51,7 @@ from overcooked_simulator.order import (
     OrderConfig,
 )
 from overcooked_simulator.player import Player, PlayerConfig
-from overcooked_simulator.state_representation import StateRepresentation
+from overcooked_simulator.state_representation import StateRepresentation, InfoMsg
 from overcooked_simulator.utils import create_init_env_time, get_closest
 
 log = logging.getLogger(__name__)
@@ -279,6 +280,8 @@ class Environment:
             str, EffectManager
         ] = self.counter_factory.setup_effect_manger(self.counters)
 
+        self.info_msgs_per_player: dict[str, list[InfoMsg]] = defaultdict(list)
+
         self.hook(
             ENV_INITIALIZED,
             environment_config=env_config,
@@ -783,6 +786,12 @@ class Environment:
                 }
                 if self.player_view_restricted
                 else None,
+                "info_msg": [
+                    (msg["msg"], msg["level"])
+                    for msg in self.info_msgs_per_player[player_id]
+                    if msg["start_time"] < self.env_time
+                    and msg["end_time"] > self.env_time
+                ],
             }
             self.hook(STATE_DICT, state=state, player_id=player_id)
             json_data = json.dumps(state)
diff --git a/overcooked_simulator/state_representation.py b/overcooked_simulator/state_representation.py
index 9d4e7427..c989aecc 100644
--- a/overcooked_simulator/state_representation.py
+++ b/overcooked_simulator/state_representation.py
@@ -2,6 +2,7 @@
 Type hint classes for the representation of the json state.
 """
 from datetime import datetime
+from enum import Enum
 
 from pydantic import BaseModel
 from typing_extensions import Literal, TypedDict
@@ -73,6 +74,19 @@ class ViewRestriction(BaseModel):
     counter_mask: None | list[bool]
 
 
+class InfoMsgLevel(Enum):
+    Normal = "Normal"
+    Warning = "Warning"
+    Success = "Success"
+
+
+class InfoMsg(TypedDict):
+    msg: str
+    start_time: datetime
+    end_time: datetime
+    level: InfoMsgLevel
+
+
 class StateRepresentation(BaseModel):
     """The format of the returned state representation."""
 
@@ -85,6 +99,7 @@ class StateRepresentation(BaseModel):
     env_time: datetime  # isoformat str
     remaining_time: float
     view_restriction: None | ViewRestriction
+    info_msg: list[tuple[str, str]]
 
 
 def create_json_schema():
-- 
GitLab