diff --git a/overcooked_simulator/__main__.py b/overcooked_simulator/__main__.py
index 436201f2ec085f299699bdbf641f6f01029270b8..f48180ac4aa23e525646e844d9e53b9f49a71595 100644
--- a/overcooked_simulator/__main__.py
+++ b/overcooked_simulator/__main__.py
@@ -50,7 +50,7 @@ def main(cli_args=None):
         print("Received Keyboard interrupt")
     finally:
         if game_server is not None and game_server.is_alive():
-            print("Terminate gparserame server")
+            print("Terminate game server")
             game_server.terminate()
         if pygame_gui is not None and pygame_gui.is_alive():
             print("Terminate pygame gui")
diff --git a/overcooked_simulator/counter_factory.py b/overcooked_simulator/counter_factory.py
index 93193edad4afa17ad4e1c40e39f60a40c880ceaf..639459a56508c627eb5015aa5b3080bfcd2a1737 100644
--- a/overcooked_simulator/counter_factory.py
+++ b/overcooked_simulator/counter_factory.py
@@ -33,6 +33,7 @@ layout_chars:
 """
 import inspect
 import sys
+from random import Random
 from typing import Any, Type, TypeVar
 
 import numpy as np
@@ -50,7 +51,15 @@ from overcooked_simulator.counters import (
     SinkAddon,
     Trashcan,
 )
-from overcooked_simulator.game_items import ItemInfo, ItemType, CookingEquipment, Plate
+from overcooked_simulator.effect_manager import EffectManager
+from overcooked_simulator.game_items import (
+    ItemInfo,
+    ItemType,
+    CookingEquipment,
+    Plate,
+    Item,
+)
+from overcooked_simulator.hooks import Hooks
 from overcooked_simulator.order import OrderAndScoreManager
 from overcooked_simulator.utils import get_closest
 
@@ -72,10 +81,10 @@ def convert_words_to_chars(layout_chars_config: dict[str, str]) -> dict[str, str
     """
     word_refs = {
         "hash": "#",
-        "space": " ",
+        # "space": " ",
         "dot": ".",
         "comma": ",",
-        "semicolon": ";",
+        # "semicolon": ";",
         "colon": ":",
         "minus": "-",
         "exclamation": "!",
@@ -89,6 +98,7 @@ def convert_words_to_chars(layout_chars_config: dict[str, str]) -> dict[str, str
         "left": "<",
         "pipe": "|",
         "at": "@",
+        "wave": "~",  # ~ is None / null in yaml
         "ocurlybracket": "{",
         "ccurlybracket": "}",
         "osquarebracket": "[",
@@ -111,6 +121,9 @@ class CounterFactory:
         serving_window_additional_kwargs: dict[str, Any],
         plate_config: PlateConfig,
         order_and_score: OrderAndScoreManager,
+        effect_manager_config: dict,
+        hook: Hooks,
+        random: Random,
     ) -> None:
         """Constructor for the `CounterFactory` class. Set up the attributes necessary to instantiate the counters.
 
@@ -140,6 +153,8 @@ class CounterFactory:
         """The plate config from the `environment_config`"""
         self.order_and_score: OrderAndScoreManager = order_and_score
         """The order and score manager to pass to `ServingWindow` and the `Tashcan` which can affect the scores."""
+        self.effect_manager_config = effect_manager_config
+        """The effect manager config to setup the effect manager based on the defined effects in the item info."""
 
         self.no_counter_chars: set[str] = set(
             c
@@ -149,8 +164,11 @@ class CounterFactory:
         """A set of characters that represent counters for agents or free spaces."""
 
         self.counter_classes: dict[str, Type] = dict(
-            inspect.getmembers(
-                sys.modules["overcooked_simulator.counters"], inspect.isclass
+            filter(
+                lambda k: issubclass(k[1], Counter),
+                inspect.getmembers(
+                    sys.modules["overcooked_simulator.counters"], inspect.isclass
+                ),
             )
         )
         """A dictionary of counter classes imported from the 'overcooked_simulator.counters' module."""
@@ -166,11 +184,19 @@ class CounterFactory:
         }
         """A dictionary mapping cooking counters to the list of equipment items associated with them."""
 
+        self.hook = hook
+        """Reference to the hook manager."""
+
+        self.random = random
+        """Random instance."""
+
     def get_counter_object(self, c: str, pos: npt.NDArray[float]) -> Counter:
         """Create and returns a counter object based on the provided character and position."""
 
         assert self.can_map(c), f"Can't map counter char {c}"
         counter_class = None
+        if c == "@":
+            print("-")
         if self.layout_chars_config[c] in self.item_info:
             item_info = self.item_info[self.layout_chars_config[c]]
             if item_info.type == ItemType.Equipment and item_info.equipment:
@@ -185,30 +211,54 @@ class CounterFactory:
                             name=item_info.name,
                             item_info=item_info,
                             transitions=self.filter_item_info(
-                                by_equipment_name=item_info.name
+                                by_equipment_name=item_info.name,
+                                add_effects=True,
                             ),
                         ),
+                        hook=self.hook,
                     )
             elif item_info.type == ItemType.Ingredient:
-                return Dispenser(pos=pos, dispensing=item_info)
+                return Dispenser(pos=pos, hook=self.hook, dispensing=item_info)
+            elif item_info.type == ItemType.Tool:
+                return Counter(
+                    pos=pos,
+                    hook=self.hook,
+                    occupied_by=Item(name=item_info.name, item_info=item_info),
+                )
 
         if counter_class is None:
-            counter_class = self.counter_classes[self.layout_chars_config[c]]
+            if self.layout_chars_config[c] in self.counter_classes:
+                counter_class = self.counter_classes[self.layout_chars_config[c]]
+            elif self.layout_chars_config[c] == "Plate":
+                return Counter(
+                    pos=pos,
+                    hook=self.hook,
+                    occupied_by=Plate(
+                        transitions=self.filter_item_info(
+                            by_item_type=ItemType.Meal, add_effects=True
+                        ),
+                        clean=True,
+                        item_info=self.item_info[Plate.__name__],
+                    ),
+                )
         kwargs = {
             "pos": pos,
+            "hook": self.hook,
         }
         if issubclass(counter_class, (CuttingBoard, Sink)):
             kwargs["transitions"] = self.filter_item_info(
-                by_equipment_name=counter_class.__name__
+                by_equipment_name=counter_class.__name__,
+                add_effects=True,
             )
         elif issubclass(counter_class, PlateDispenser):
             kwargs.update(
                 {
                     "plate_transitions": self.filter_item_info(
-                        by_item_type=ItemType.Meal
+                        by_item_type=ItemType.Meal, add_effects=True
                     ),
                     "plate_config": self.plate_config,
                     "dispensing": self.item_info[Plate.__name__],
+                    "random": self.random,
                 }
             )
         elif issubclass(counter_class, ServingWindow):
@@ -242,21 +292,32 @@ class CounterFactory:
         self,
         by_item_type: ItemType = None,
         by_equipment_name: str = None,
+        add_effects: bool = False,
     ) -> dict[str, ItemInfo]:
         """Filter the item info dict by item type or equipment name"""
+        filtered = {}
         if by_item_type is not None:
-            return {
+            filtered = {
                 name: info
                 for name, info in self.item_info.items()
                 if info.type == by_item_type
             }
         if by_equipment_name is not None:
-            return {
+            filtered = {
                 name: info
                 for name, info in self.item_info.items()
                 if info.equipment is not None
                 and info.equipment.name == by_equipment_name
             }
+        if add_effects:
+            for name, effect in self.filter_item_info(
+                by_item_type=ItemType.Effect
+            ).items():
+                for need in effect.needs:
+                    if need in filtered:
+                        filtered.update({name: effect})
+        if by_item_type or by_equipment_name:
+            return filtered
         return self.item_info
 
     def post_counter_setup(self, counters: list[Counter]):
@@ -282,11 +343,34 @@ class CounterFactory:
                     counter: Sink  # Pycharm type checker does now work for match statements?
                     assert len(sink_addons) > 0, "No SinkAddon but normal Sink"
                     closest_addon = get_closest(pos, sink_addons)
-                    assert 1 - (1 * 0.05) <= np.linalg.norm(
+                    assert 1.0 == np.linalg.norm(
                         closest_addon.pos - pos
                     ), f"No SinkAddon connected to Sink at pos {pos}"
                     counter.set_addon(closest_addon)
 
+    def setup_effect_manger(self, counters: list[Counter]) -> dict[str, EffectManager]:
+        effect_manager = {}
+        for name, effect in self.filter_item_info(by_item_type=ItemType.Effect).items():
+            assert (
+                effect.manager in self.effect_manager_config
+            ), f"Manager for effect not found: {name} -> {effect.manager} not in {list(self.effect_manager_config.keys())}"
+            if effect.manager in effect_manager:
+                manager = effect_manager[effect.manager]
+            else:
+                manager = self.effect_manager_config[effect.manager]["class"](
+                    hook=self.hook,
+                    random=self.random,
+                    **self.effect_manager_config[effect.manager]["kwargs"],
+                )
+                manager.set_counters(counters)
+                effect_manager[effect.manager] = manager
+
+            manager.add_effect(effect)
+
+            effect.manager = manager
+
+        return effect_manager
+
     @staticmethod
     def get_counter_of_type(counter_type: Type[T], counters: list[Counter]) -> list[T]:
         """Filter all counters in the environment for a counter type."""
diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py
index 7e07048f5e544318c199b5652d64c6aff03bf571..40779e5cb228c993a65b5264f684225ea1ff78bc 100644
--- a/overcooked_simulator/counters.py
+++ b/overcooked_simulator/counters.py
@@ -40,9 +40,31 @@ import uuid
 from collections import deque
 from collections.abc import Iterable
 from datetime import datetime, timedelta
+from random import Random
 from typing import TYPE_CHECKING, Optional, Callable, Set
 
+from overcooked_simulator.hooks import (
+    Hooks,
+    POST_DISPENSER_PICK_UP,
+    PRE_DISPENSER_PICK_UP,
+    CUTTING_BOARD_PROGRESS,
+    CUTTING_BOARD_100,
+    PRE_COUNTER_PICK_UP,
+    POST_COUNTER_PICK_UP,
+    PRE_SERVING,
+    POST_SERVING,
+    NO_SERVING,
+    DIRTY_PLATE_ARRIVES,
+    TRASHCAN_USAGE,
+    PLATE_CLEANED,
+    ADDED_PLATE_TO_SINK,
+    DROP_ON_SINK_ADDON,
+    PICK_UP_FROM_SINK_ADDON,
+    PLATE_OUT_OF_KITCHEN_TIME,
+)
+
 if TYPE_CHECKING:
+    from overcooked_simulator.effect_manager import Effect
     from overcooked_simulator.overcooked_environment import (
         OrderAndScoreManager,
     )
@@ -55,6 +77,7 @@ from overcooked_simulator.game_items import (
     CookingEquipment,
     Plate,
     ItemInfo,
+    EffectType,
 )
 
 
@@ -74,6 +97,7 @@ class Counter:
     def __init__(
         self,
         pos: npt.NDArray[float],
+        hook: Hooks,
         occupied_by: Optional[Item] = None,
         uid: hex = None,
         **kwargs,
@@ -90,12 +114,24 @@ class Counter:
         """The position of the counter."""
         self.occupied_by: Optional[Item] = occupied_by
         """What is on top of the counter, e.g., `Item`s."""
+        self.active_effects: list[Effect] = []
+        """The effects that currently affect the usage of the counter."""
+        self.hook = hook
+        """Reference to the hook manager."""
+        self.orientation: npt.NDArray[float] = np.array([0, 1], dtype=float)
+        """In what direction the counter is facing."""
 
     @property
     def occupied(self) -> bool:
         """Is something on top of the counter."""
         return self.occupied_by is not None
 
+    def set_orientation(self, orientation: npt.NDArray[float]) -> None:
+        if not np.isclose(np.linalg.norm(orientation), 1):
+            self.orientation = orientation / np.linalg.norm(orientation)
+        else:
+            self.orientation = orientation
+
     def pick_up(self, on_hands: bool = True) -> Item | None:
         """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.
@@ -105,16 +141,36 @@ class Counter:
 
         Returns: The item which the counter is occupied by. None if nothing is there.
         """
+        self.hook(PRE_COUNTER_PICK_UP, counter=self, on_hands=on_hands)
         if on_hands:
             if self.occupied_by:
                 occupied_by = self.occupied_by
                 self.occupied_by = None
+                self.hook(
+                    POST_COUNTER_PICK_UP,
+                    counter=self,
+                    on_hands=on_hands,
+                    return_this=occupied_by,
+                )
                 return occupied_by
             return None
         if self.occupied_by and isinstance(self.occupied_by, CookingEquipment):
-            return self.occupied_by.release()
+            return_this = self.occupied_by.release()
+            self.hook(
+                POST_COUNTER_PICK_UP,
+                counter=self,
+                on_hands=on_hands,
+                return_this=return_this,
+            )
+            return return_this
         occupied_by = self.occupied_by
         self.occupied_by = None
+        self.hook(
+            POST_COUNTER_PICK_UP,
+            counter=self,
+            on_hands=on_hands,
+            return_this=occupied_by,
+        )
         return occupied_by
 
     def can_drop_off(self, item: Item) -> bool:
@@ -144,19 +200,45 @@ class Counter:
             return self.occupied_by.combine(item)
         return None
 
-    def interact_start(self):
-        """Starts an interaction by the player. Nothing happens for the standard counter."""
-        pass
-
-    def interact_stop(self):
-        """Stops an interaction by the player. Nothing happens for the standard counter."""
-        pass
-
     def __repr__(self):
         return (
             f"{self.__class__.__name__}(pos={self.pos},occupied_by={self.occupied_by})"
         )
 
+    def do_tool_interaction(self, passed_time: timedelta, tool: Item):
+        successful = False
+        if self.occupied_by:
+            if isinstance(self.occupied_by, deque):
+                for item in self.occupied_by:
+                    successful |= self._do_single_tool_interaction(
+                        passed_time, tool, item
+                    )
+            else:
+                successful = self._do_single_tool_interaction(
+                    passed_time, tool, self.occupied_by
+                )
+        if not successful:
+            self._do_single_tool_interaction(passed_time, tool, self)
+
+    def _do_single_tool_interaction(
+        self, passed_time: timedelta, tool: Item, target: Item | Counter
+    ) -> bool:
+        suitable_effects = [
+            e for e in target.active_effects if e.name in tool.item_info.needs
+        ]
+        if suitable_effects:
+            effect = suitable_effects[0]
+            percent = passed_time.total_seconds() / tool.item_info.seconds
+            effect.progres_percentage += percent
+            if effect.progres_percentage > 1.0:
+                effect.item_info.manager.remove_active_effect(effect, target)
+                target.active_effects.remove(effect)
+            return True
+        return False
+
+    def do_hand_free_interaction(self, passed_time: timedelta, now: datetime):
+        ...
+
     def to_dict(self) -> dict:
         """For the state representation. Only the relevant attributes are put into the dict."""
         return {
@@ -164,6 +246,7 @@ class Counter:
             "category": COUNTER_CATEGORY,
             "type": self.__class__.__name__,
             "pos": self.pos.tolist(),
+            "orientation": self.orientation.tolist(),
             "occupied_by": None
             if self.occupied_by is None
             else (
@@ -171,6 +254,7 @@ class Counter:
                 if isinstance(self.occupied_by, Iterable)
                 else self.occupied_by.to_dict()
             ),
+            "active_effects": [e.to_dict() for e in self.active_effects],
         }
 
 
@@ -186,9 +270,7 @@ class CuttingBoard(Counter):
     The character `C` in the `layout` file represents the CuttingBoard.
     """
 
-    def __init__(self, pos: np.ndarray, transitions: dict[str, ItemInfo], **kwargs):
-        self.progressing: bool = False
-        """Is a player progressing/cutting on the board."""
+    def __init__(self, transitions: dict[str, ItemInfo], **kwargs):
         self.transitions: dict[str, ItemInfo] = transitions
         """The allowed transitions to a new item. Keys are the resulting items and the `ItemInfo` (value) contains 
         the needed items in the `need` attribute."""
@@ -197,9 +279,9 @@ class CuttingBoard(Counter):
         }
         """For faster accessing the needed item. Keys are the ingredients that the player can put and chop on the 
         board."""
-        super().__init__(pos=pos, **kwargs)
+        super().__init__(**kwargs)
 
-    def progress(self, passed_time: timedelta, now: datetime):
+    def do_hand_free_interaction(self, passed_time: timedelta, now: datetime):
         """Called by environment step function for time progression.
 
         Args:
@@ -212,8 +294,15 @@ class CuttingBoard(Counter):
         """
         if (
             self.occupied
-            and self.progressing
             and self.occupied_by.name in self.inverted_transition_dict
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.occupied_by.active_effects
+            )
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.active_effects
+            )
         ):
             percent = (
                 passed_time.total_seconds()
@@ -222,32 +311,18 @@ class CuttingBoard(Counter):
             self.occupied_by.progress(
                 equipment=self.__class__.__name__, percent=percent
             )
+            self.hook(
+                CUTTING_BOARD_PROGRESS,
+                counter=self,
+                percent=percent,
+                passed_time=passed_time,
+            )
             if self.occupied_by.progress_percentage == 1.0:
                 self.occupied_by.reset()
                 self.occupied_by.name = self.inverted_transition_dict[
                     self.occupied_by.name
                 ].name
-
-    def start_progress(self):
-        """Starts the cutting process."""
-        self.progressing = True
-
-    def pause_progress(self):
-        """Pauses the cutting process"""
-        self.progressing = False
-
-    def interact_start(self):
-        """Handles player interaction, starting to hold key down."""
-        self.start_progress()
-
-    def interact_stop(self):
-        """Handles player interaction, stopping to hold key down."""
-        self.pause_progress()
-
-    def to_dict(self) -> dict:
-        d = super().to_dict()
-        d.update((("progressing", self.progressing),))
-        return d
+                self.hook(CUTTING_BOARD_100, counter=self)
 
 
 class ServingWindow(Counter):
@@ -264,7 +339,6 @@ class ServingWindow(Counter):
 
     def __init__(
         self,
-        pos: npt.NDArray[float],
         order_and_score: OrderAndScoreManager,
         meals: set[str],
         env_time_func: Callable[[], datetime],
@@ -281,17 +355,28 @@ class ServingWindow(Counter):
         """All allowed meals by the `environment_config.yml`."""
         self.env_time_func: Callable[[], datetime] = env_time_func
         """Reference to get the current env time by calling the `env_time_func`."""
-        super().__init__(pos=pos, **kwargs)
+        super().__init__(**kwargs)
 
     def drop_off(self, item) -> Item | None:
         env_time = self.env_time_func()
+        self.hook(PRE_SERVING, counter=self, item=item, env_time=env_time)
         if self.order_and_score.serve_meal(item=item, env_time=env_time):
             if self.plate_dispenser is not None:
                 self.plate_dispenser.update_plate_out_of_kitchen(env_time=env_time)
+                self.hook(POST_SERVING, counter=self, item=item, env_time=env_time)
             return None
+        self.hook(NO_SERVING, counter=self, item=item, env_time=env_time)
         return item
 
     def can_drop_off(self, item: Item) -> bool:
+        if any(
+            e.item_info.effect_type == EffectType.Unusable for e in self.active_effects
+        ):
+            return False
+        if any(
+            e.item_info.effect_type == EffectType.Unusable for e in item.active_effects
+        ):
+            return False
         return isinstance(item, CookingEquipment) and (
             (item.content_ready is not None and item.content_ready.name in self.meals)
             or (len(item.content_list) == 1 and item.content_list[0].name in self.meals)
@@ -322,18 +407,24 @@ class Dispenser(Counter):
     Which also is easier for the visualization of the dispenser.
     """
 
-    def __init__(self, pos: npt.NDArray[float], dispensing: ItemInfo, **kwargs):
+    def __init__(self, dispensing: ItemInfo, **kwargs):
         self.dispensing: ItemInfo = dispensing
         """`ItemInfo` what the the Dispenser is dispensing. One ready object always is on top of the counter."""
         super().__init__(
-            pos=pos,
             occupied_by=self.create_item(),
             **kwargs,
         )
 
     def pick_up(self, on_hands: bool = True) -> Item | None:
+        self.hook(PRE_DISPENSER_PICK_UP, counter=self, on_hands=on_hands)
         return_this = self.occupied_by
         self.occupied_by = self.create_item()
+        self.hook(
+            POST_DISPENSER_PICK_UP,
+            counter=self,
+            on_hands=on_hands,
+            return_this=return_this,
+        )
         return return_this
 
     def drop_off(self, item: Item) -> Item | None:
@@ -390,13 +481,13 @@ class PlateDispenser(Counter):
 
     def __init__(
         self,
-        pos: npt.NDArray[float],
         dispensing: ItemInfo,
         plate_config: PlateConfig,
         plate_transitions: dict[str, ItemInfo],
+        random: Random,
         **kwargs,
     ) -> None:
-        super().__init__(pos=pos, **kwargs)
+        super().__init__(**kwargs)
         self.dispensing: ItemInfo = dispensing
         """Plate ItemInfo."""
         self.occupied_by: deque = deque()
@@ -411,6 +502,8 @@ class PlateDispenser(Counter):
         `out_of_kitchen_timer` list every frame."""
         self.plate_transitions: dict[str, ItemInfo] = plate_transitions
         """Transitions for the plates. Relevant for the sink, because a plate can become a clean one there."""
+        self.random = random
+        """Random instance."""
         self.setup_plates()
 
     def pick_up(self, on_hands: bool = True) -> Item | None:
@@ -435,15 +528,16 @@ class PlateDispenser(Counter):
         """Is called from the serving window to add a plate out of kitchen."""
         # not perfect identical to datetime.now but based on framerate enough.
         time_plate_to_add = env_time + timedelta(
-            seconds=np.random.uniform(
-                low=self.plate_config.plate_delay[0],
-                high=self.plate_config.plate_delay[1],
+            seconds=self.random.uniform(
+                a=self.plate_config.plate_delay[0],
+                b=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
+        self.hook(PLATE_OUT_OF_KITCHEN_TIME, time_plate_to_add=time_plate_to_add)
 
     def setup_plates(self):
         """Create plates based on the config. Clean and dirty ones."""
@@ -467,6 +561,7 @@ class PlateDispenser(Counter):
             idx_delete = []
             for i, times in enumerate(self.out_of_kitchen_timer):
                 if times < now:
+                    self.hook(DIRTY_PLATE_ARRIVES, counter=self, times=times, now=now)
                     idx_delete.append(i)
                     log.debug("Add dirty plate")
                     self.add_dirty_plate()
@@ -501,10 +596,8 @@ class Trashcan(Counter):
     The character `X` in the `layout` file represents the Trashcan.
     """
 
-    def __init__(
-        self, order_and_score: OrderAndScoreManager, pos: npt.NDArray[float], **kwargs
-    ):
-        super().__init__(pos, **kwargs)
+    def __init__(self, order_and_score: OrderAndScoreManager, **kwargs):
+        super().__init__(**kwargs)
         self.order_and_score: OrderAndScoreManager = order_and_score
         """Reference to the `OrderAndScoreManager`, because unnecessary removed items can affect the score."""
 
@@ -512,16 +605,25 @@ class Trashcan(Counter):
         pass
 
     def drop_off(self, item: Item) -> Item | None:
+        if any(
+            e.item_info.effect_type == EffectType.Unusable for e in item.active_effects
+        ) or any(
+            e.item_info.effect_type == EffectType.Unusable for e in self.active_effects
+        ):
+            return item
         if isinstance(item, CookingEquipment):
-            self.order_and_score.apply_penalty_for_using_trash(item.content_list)
+            penalty = self.order_and_score.apply_penalty_for_using_trash(
+                item.content_list
+            )
             item.reset_content()
             return item
         else:
-            self.order_and_score.apply_penalty_for_using_trash(item)
+            penalty = self.order_and_score.apply_penalty_for_using_trash(item)
+        self.hook(TRASHCAN_USAGE, counter=self, item=item, penalty=penalty)
         return None
 
     def can_drop_off(self, item: Item) -> bool:
-        return True
+        return item.name != "Extinguisher"
 
 
 class CookingCounter(Counter):
@@ -561,6 +663,10 @@ class CookingCounter(Counter):
             and isinstance(self.occupied_by, CookingEquipment)
             and self.occupied_by.name in self.equipments
             and self.occupied_by.can_progress()
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.active_effects
+            )
         ):
             self.occupied_by.progress(passed_time, now)
 
@@ -588,14 +694,11 @@ class Sink(Counter):
 
     def __init__(
         self,
-        pos: npt.NDArray[float],
         transitions: dict[str, ItemInfo],
         sink_addon: SinkAddon = None,
         **kwargs,
     ):
-        super().__init__(pos=pos, **kwargs)
-        self.progressing: bool = False
-        """If a player currently cleans a plate."""
+        super().__init__(**kwargs)
         self.sink_addon: SinkAddon = sink_addon
         """The connected sink addon which will receive the clean plates"""
         self.occupied_by: deque[Plate] = deque()
@@ -616,12 +719,30 @@ class Sink(Counter):
         """If there is a plate in the sink."""
         return len(self.occupied_by) != 0
 
-    def progress(self, passed_time: timedelta, now: datetime):
+    def do_hand_free_interaction(self, passed_time: timedelta, now: datetime):
         """Called by environment step function for time progression"""
         if (
             self.occupied
-            and self.progressing
             and self.occupied_by[-1].name in self.transition_needs
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.active_effects
+            )
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.sink_addon.active_effects
+            )
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.occupied_by[-1].active_effects
+            )
+            and (
+                not self.sink_addon.occupied_by
+                or not any(
+                    e.item_info.effect_type == EffectType.Unusable
+                    for e in self.sink_addon.occupied_by[-1].active_effects
+                )
+            )
         ):
             for name, info in self.transitions.items():
                 if info.needs[0] == self.occupied_by[-1].name:
@@ -630,6 +751,7 @@ class Sink(Counter):
                         equipment=self.__class__.__name__, percent=percent
                     )
                     if self.occupied_by[-1].progress_percentage == 1.0:
+                        self.hook(PLATE_CLEANED, counter=self)
                         self.occupied_by[-1].reset()
                         self.occupied_by[-1].name = name
                         plate = self.occupied_by.pop()
@@ -637,27 +759,12 @@ class Sink(Counter):
                         self.sink_addon.add_clean_plate(plate)
                     break
 
-    def start_progress(self):
-        """Starts the cutting process."""
-        self.progressing = True
-
-    def pause_progress(self):
-        """Pauses the cutting process"""
-        self.progressing = False
-
-    def interact_start(self):
-        """Handles player interaction, starting to hold key down."""
-        self.start_progress()
-
-    def interact_stop(self):
-        """Handles player interaction, stopping to hold key down."""
-        self.pause_progress()
-
     def can_drop_off(self, item: Item) -> bool:
         return isinstance(item, Plate) and not item.clean
 
     def drop_off(self, item: Plate) -> Item | None:
         self.occupied_by.appendleft(item)
+        self.hook(ADDED_PLATE_TO_SINK, counter=self, item=item)
         return None
 
     def pick_up(self, on_hands: bool = True) -> Item | None:
@@ -667,11 +774,6 @@ class Sink(Counter):
         """Set the closest addon in post_setup."""
         self.sink_addon = sink_addon
 
-    def to_dict(self) -> dict:
-        d = super().to_dict()
-        d.update((("progressing", self.progressing),))
-        return d
-
 
 class SinkAddon(Counter):
     """The counter on which the clean plates appear after cleaning them in the `Sink`
@@ -681,8 +783,8 @@ class SinkAddon(Counter):
     The character `+` in the `layout` file represents the SinkAddon.
     """
 
-    def __init__(self, pos: npt.NDArray[float], occupied_by=None):
-        super().__init__(pos=pos)
+    def __init__(self, occupied_by=None, **kwargs):
+        super().__init__(**kwargs)
         # maybe check if occupied by is already a list or deque?
         self.occupied_by: deque = deque([occupied_by]) if occupied_by else deque()
         """The stack of clean plates."""
@@ -691,6 +793,7 @@ class SinkAddon(Counter):
         return self.occupied_by and self.occupied_by[-1].can_combine(item)
 
     def drop_off(self, item: Item) -> Item | None:
+        self.hook(DROP_ON_SINK_ADDON, counter=self, item=item)
         return self.occupied_by[-1].combine(item)
 
     def add_clean_plate(self, plate: Plate):
@@ -699,4 +802,5 @@ class SinkAddon(Counter):
 
     def pick_up(self, on_hands: bool = True) -> Item | None:
         if self.occupied_by:
+            self.hook(PICK_UP_FROM_SINK_ADDON)
             return self.occupied_by.pop()
diff --git a/overcooked_simulator/effect_manager.py b/overcooked_simulator/effect_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..7e18e17ef757425d933360cc658306224efcd2fa
--- /dev/null
+++ b/overcooked_simulator/effect_manager.py
@@ -0,0 +1,136 @@
+from __future__ import annotations
+
+from collections import deque
+from datetime import timedelta, datetime
+from random import Random
+from typing import TYPE_CHECKING, Tuple
+
+from overcooked_simulator.game_items import (
+    ItemInfo,
+    Item,
+    ItemType,
+    Effect,
+    CookingEquipment,
+)
+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:
+    from overcooked_simulator.counters import Counter
+
+
+class EffectManager:
+    def __init__(self, hook: Hooks, random: Random) -> None:
+        self.effects = []
+        self.counters = []
+        self.hook = hook
+        self.new_effects: list[Tuple[Effect, Item | Counter]] = []
+        self.random = random
+
+    def add_effect(self, effect: ItemInfo):
+        self.effects.append(effect)
+
+    def set_counters(self, counters: list[Counter]):
+        self.counters.extend(counters)
+
+    def register_active_effect(self, effect: Effect, target: Item | Counter):
+        target.active_effects.append(effect)
+        self.new_effects.append((effect, target))
+
+    def progress(self, passed_time: timedelta, now: datetime):
+        ...
+
+    def can_start_effect_transition(
+        self, effect: ItemInfo, target: Item | Counter
+    ) -> bool:
+        return effect.name not in [e.name for e in target.active_effects]
+
+    def remove_active_effect(self, effect: Effect, target: Item | Counter):
+        ...
+
+
+class FireEffectManager(EffectManager):
+    # TODO add Random object
+
+    def __init__(
+        self,
+        spreading_duration: list[float],
+        fire_burns_ingredients_and_meals: bool,
+        **kwargs,
+    ):
+        super().__init__(**kwargs)
+        self.spreading_duration = spreading_duration
+        self.fire_burns_ingredients_and_meals = fire_burns_ingredients_and_meals
+        self.effect_to_timer: dict[str:datetime] = {}
+        self.next_finished_timer = datetime.max
+        self.active_effects: list[Tuple[Effect, Item | Counter]] = []
+
+    def progress(self, passed_time: timedelta, now: datetime):
+        if self.new_effects:
+            for effect, target in self.new_effects:
+                self.effect_to_timer[effect.uuid] = now + timedelta(
+                    seconds=self.random.uniform(*self.spreading_duration)
+                )
+                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:
+            for effect, target in self.active_effects:
+                if self.effect_to_timer[effect.uuid] < now:
+                    if isinstance(target, Item):
+                        target = find_item_on_counters(target.uuid, self.counters)
+                    if target:
+                        touching = get_touching_counters(target, self.counters)
+                        for counter in touching:
+                            if counter.occupied_by:
+                                if isinstance(counter.occupied_by, deque):
+                                    self.apply_effect(effect, counter.occupied_by[-1])
+                                else:
+                                    self.apply_effect(effect, counter.occupied_by)
+                            else:
+                                self.apply_effect(effect, counter)
+                    self.effect_to_timer[effect.uuid] = now + timedelta(
+                        seconds=self.random.uniform(*self.spreading_duration)
+                    )
+            if self.effect_to_timer:
+                self.next_finished_timer = min(self.effect_to_timer.values())
+            else:
+                self.next_finished_timer = datetime.max
+
+    def apply_effect(self, effect: Effect, target: Item | Counter):
+        if (
+            isinstance(target, Item)
+            and target.item_info.type == ItemType.Tool
+            and effect.name in target.item_info.needs
+        ):
+            # Tools that reduce fire can not burn
+            return
+        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:
+                        self.burn_content(content)
+                    if self.fire_burns_ingredients_and_meals:
+                        self.burn_content(target.content_ready)
+            elif isinstance(target, Item):
+                self.burn_content(target)
+            self.register_active_effect(
+                Effect(effect.name, item_info=effect.item_info), target
+            )
+
+    def burn_content(self, content: Item):
+        if self.fire_burns_ingredients_and_meals and content:
+            if not content.name.startswith("Burnt"):
+                content.name = "Burnt" + content.name
+
+    def remove_active_effect(self, effect: Effect, target: Item | Counter):
+        if (effect, target) in self.active_effects:
+            self.active_effects.remove((effect, target))
+        if effect.uuid in self.effect_to_timer:
+            del self.effect_to_timer[effect.uuid]
diff --git a/overcooked_simulator/example_study_server.py b/overcooked_simulator/example_study_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..778135f47054e0cdc4a948f7f3a1098b95542426
--- /dev/null
+++ b/overcooked_simulator/example_study_server.py
@@ -0,0 +1,136 @@
+"""
+# Usage
+- Set `CONNECT_WITH_STUDY_SERVER` in overcooked_gui.py to True.
+- Run this script. Copy the manager id that is printed
+- Run the game_server.py script with the manager id copied from the terminal
+```
+python game_server.py --manager_ids COPIED_UUID
+```
+- Run 2 overcooked_gui.py scripts in different terminals. For more players change `NUMBER_PLAYER_PER_ENV` and start more guis.
+
+The environment starts when all players connected.
+"""
+
+import argparse
+import asyncio
+import logging
+from typing import Tuple
+
+import requests
+import uvicorn
+from fastapi import FastAPI
+
+from overcooked_simulator import ROOT_DIR
+from overcooked_simulator.game_server import CreateEnvironmentConfig
+from overcooked_simulator.server_results import PlayerInfo
+from overcooked_simulator.utils import (
+    url_and_port_arguments,
+    add_list_of_manager_ids_arguments,
+)
+
+NUMBER_PLAYER_PER_ENV = 2
+
+log = logging.getLogger(__name__)
+
+
+app = FastAPI()
+
+game_server_url = "localhost:8000"
+server_manager_id = None
+
+
+# @app.get("/")
+# async def root(response_class=HTMLResponse):
+#     return """
+#     <html>
+#         <head>
+#             <title>Overcooked Game</title>
+#         </head>
+#         <body>
+#             <h1>Start Game!</h1>
+#             <button type="button">Click Me!</button>
+#         </body>
+#     </html>
+#     """
+
+running_envs: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {}
+current_free_envs = []
+
+
+@app.post("/connect_to_game/{request_id}")
+async def want_to_play(request_id: str):
+    global current_free_envs
+    # TODO based on study desing / internal state of request id current state (which level to play)
+    if current_free_envs:
+        current_free_env = current_free_envs.pop()
+
+        running_envs[current_free_env][2].append(request_id)
+        new_running_env = (
+            running_envs[current_free_env][0] + 1,
+            running_envs[current_free_env][1],
+            running_envs[current_free_env][2],
+        )
+        player_info = running_envs[current_free_env][1][str(new_running_env[0])]
+        running_envs[current_free_env] = new_running_env
+        if new_running_env[0] < NUMBER_PLAYER_PER_ENV - 1:
+            current_free_env.append(current_free_env)
+        return player_info
+    else:
+        environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
+        layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
+        item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
+        with open(item_info_path, "r") as file:
+            item_info = file.read()
+        with open(layout_path, "r") as file:
+            layout = file.read()
+        with open(environment_config_path, "r") as file:
+            environment_config = file.read()
+        creation_json = CreateEnvironmentConfig(
+            manager_id=server_manager_id,
+            number_players=NUMBER_PLAYER_PER_ENV,
+            environment_settings={"all_player_can_pause_game": False},
+            item_info_config=item_info,
+            environment_config=environment_config,
+            layout_config=layout,
+            seed=1234567890,
+        ).model_dump(mode="json")
+        # todo async
+        env_info = requests.post(
+            game_server_url + "/manage/create_env/", json=creation_json
+        )
+
+        if env_info.status_code == 403:
+            raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
+        env_info = env_info.json()
+        print(env_info)
+        running_envs[env_info["env_id"]] = (0, env_info["player_info"], [request_id])
+        current_free_envs.append(env_info["env_id"])
+        return env_info["player_info"]["0"]
+
+
+def main(host, port, game_server_url_, manager_id):
+    global game_server_url, server_manager_id
+    game_server_url = "http://" + game_server_url_
+    server_manager_id = manager_id[0]
+    print(f"Use {server_manager_id=} for {game_server_url=}")
+    loop = asyncio.new_event_loop()
+    config = uvicorn.Config(app, host=host, port=port, loop=loop)
+    server = uvicorn.Server(config)
+    loop.run_until_complete(server.serve())
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Overcooked Simulator Study Server",
+        description="Study Server: Match Making, client pre and post managing.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+    url_and_port_arguments(parser=parser, server_name="Study Server", default_port=8080)
+    add_list_of_manager_ids_arguments(parser=parser)
+    args = parser.parse_args()
+    main(
+        args.url,
+        args.port,
+        game_server_url_="localhost:8000",
+        manager_id=args.manager_ids,
+    )
diff --git a/overcooked_simulator/game_content/agents/arch_config.yml b/overcooked_simulator/game_content/agents/arch_config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e7108df0e6c1b618eae57b948a6ed139e4fae445
--- /dev/null
+++ b/overcooked_simulator/game_content/agents/arch_config.yml
@@ -0,0 +1,24 @@
+concurrency: MultiProcessing
+
+communication:
+  communication_prefs:
+    - !name:ipaacar_com_service.communications.ipaacar_com.IPAACARInfo
+
+modules:
+  connection:
+    module_info: !name:cocosy_agent.modules.connection_module.ConnectionModule
+    mean_frequency_step: 2  # 2: every 0.5 seconds
+  working_memory:
+    module_info: !name:cocosy_agent.modules.working_memory_module.WorkingMemoryModule
+  subtask_selection:
+    module_info: !name:cocosy_agent.modules.random_subtask_module.RandomSubtaskModule
+  action_execution:
+    module_info: !name:cocosy_agent.modules.action_execution_module.ActionExecutionModule
+    mean_frequency_step: 10  # 2: every 0.5 seconds
+  #  gui:
+  #    module_info: !name:aaambos.std.guis.pysimplegui.pysimplegui_window.PySimpleGUIWindowModule
+  #    window_title: Counting GUI
+  #    topics_to_show: [["SubtaskDecision", "cocosy_agent.conventions.communication.SubtaskDecision", ["task_type"]], ["ActionControl", "cocosy_agent.conventions.communication.ActionControl", ["action_type"]]]
+  status_manager:
+    module_info: !name:aaambos.std.modules.module_status_manager.ModuleStatusManager
+    gui: false
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/agents/random_agent.py b/overcooked_simulator/game_content/agents/random_agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b666bb8c2e4e06d8b250bf8d743eb2965b65e26
--- /dev/null
+++ b/overcooked_simulator/game_content/agents/random_agent.py
@@ -0,0 +1,221 @@
+import argparse
+import asyncio
+import dataclasses
+import json
+import random
+import time
+from collections import defaultdict
+from datetime import datetime, timedelta
+
+import numpy as np
+from websockets import connect
+
+from overcooked_simulator.overcooked_environment import (
+    ActionType,
+    Action,
+    InterActionData,
+)
+from overcooked_simulator.utils import custom_asdict_factory
+
+
+async def agent():
+    parser = argparse.ArgumentParser("Random agent")
+    parser.add_argument("--uri", type=str)
+    parser.add_argument("--player_id", type=str)
+    parser.add_argument("--player_hash", type=str)
+    parser.add_argument("--step_time", type=float, default=0.5)
+
+    args = parser.parse_args()
+
+    async with connect(args.uri) as websocket:
+        await websocket.send(
+            json.dumps({"type": "ready", "player_hash": args.player_hash})
+        )
+        await websocket.recv()
+
+        ended = False
+
+        counters = None
+
+        player_info = {}
+        current_agent_pos = None
+        interaction_counter = None
+
+        last_interacting = False
+        last_interact_progress = None
+
+        threshold = datetime.max
+
+        task_type = None
+        task_args = None
+
+        started_interaction = False
+        still_interacting = False
+        current_nearest_counter_id = None
+
+        while not ended:
+            time.sleep(args.step_time)
+            await websocket.send(
+                json.dumps({"type": "get_state", "player_hash": args.player_hash})
+            )
+            state = json.loads(await websocket.recv())
+
+            if counters is None:
+                counters = defaultdict(list)
+                for counter in state["counters"]:
+                    counters[counter["type"]].append(counter)
+
+            for player in state["players"]:
+                if player["id"] == args.player_id:
+                    player_info = player
+                    current_agent_pos = player["pos"]
+                    if player["current_nearest_counter_id"]:
+                        if (
+                            current_nearest_counter_id
+                            != player["current_nearest_counter_id"]
+                        ):
+                            for counter in state["counters"]:
+                                if (
+                                    counter["id"]
+                                    == player["current_nearest_counter_id"]
+                                ):
+                                    interaction_counter = counter
+                                    current_nearest_counter_id = player[
+                                        "current_nearest_counter_id"
+                                    ]
+                                    break
+                    if last_interacting:
+                        if (
+                            not interaction_counter
+                            or not interaction_counter["occupied_by"]
+                            or isinstance(interaction_counter["occupied_by"], list)
+                            or (
+                                interaction_counter["occupied_by"][
+                                    "progress_percentage"
+                                ]
+                                == 1.0
+                            )
+                        ):
+                            last_interacting = False
+                            last_interact_progress = None
+                    else:
+                        if (
+                            interaction_counter
+                            and interaction_counter["occupied_by"]
+                            and not isinstance(interaction_counter["occupied_by"], list)
+                        ):
+                            if (
+                                last_interact_progress
+                                != interaction_counter["occupied_by"][
+                                    "progress_percentage"
+                                ]
+                            ):
+                                last_interact_progress = interaction_counter[
+                                    "occupied_by"
+                                ]["progress_percentage"]
+                                last_interacting = True
+
+                    break
+
+            if task_type:
+                if threshold < datetime.now():
+                    print(
+                        args.player_hash, args.player_id, "---Threshold---Too long---"
+                    )
+                    task_type = None
+                match task_type:
+                    case "GOTO":
+                        diff = np.array(task_args) - np.array(current_agent_pos)
+                        dist = np.linalg.norm(diff)
+                        if dist > 1.2:
+                            if dist != 0:
+                                await websocket.send(
+                                    json.dumps(
+                                        {
+                                            "type": "action",
+                                            "action": dataclasses.asdict(
+                                                Action(
+                                                    args.player_id,
+                                                    ActionType.MOVEMENT,
+                                                    (diff / dist).tolist(),
+                                                    args.step_time + 0.01,
+                                                ),
+                                                dict_factory=custom_asdict_factory,
+                                            ),
+                                            "player_hash": args.player_hash,
+                                        }
+                                    )
+                                )
+                                await websocket.recv()
+                        else:
+                            task_type = None
+                            task_args = None
+                    case "INTERACT":
+                        if not started_interaction or (
+                            still_interacting and interaction_counter
+                        ):
+                            if not started_interaction:
+                                started_interaction = True
+
+                            still_interacting = True
+                            await websocket.send(
+                                json.dumps(
+                                    {
+                                        "type": "action",
+                                        "action": dataclasses.asdict(
+                                            Action(
+                                                args.player_id,
+                                                ActionType.INTERACT,
+                                                InterActionData.START,
+                                            ),
+                                            dict_factory=custom_asdict_factory,
+                                        ),
+                                        "player_hash": args.player_hash,
+                                    }
+                                )
+                            )
+                            await websocket.recv()
+                        else:
+                            still_interacting = False
+                            started_interaction = False
+                            task_type = None
+                            task_args = None
+                    case "PUT":
+                        await websocket.send(
+                            json.dumps(
+                                {
+                                    "type": "action",
+                                    "action": dataclasses.asdict(
+                                        Action(
+                                            args.player_id,
+                                            ActionType.PUT,
+                                            "pickup",
+                                        ),
+                                        dict_factory=custom_asdict_factory,
+                                    ),
+                                    "player_hash": args.player_hash,
+                                }
+                            )
+                        )
+                        await websocket.recv()
+                        task_type = None
+                        task_args = None
+                    case None:
+                        ...
+
+            if not task_type:
+                task_type = random.choice(["GOTO", "PUT", "INTERACT"])
+                threshold = datetime.now() + timedelta(seconds=15.0)
+                if task_type == "GOTO":
+                    counter_type = random.choice(list(counters.keys()))
+                    task_args = random.choice(counters[counter_type])["pos"]
+                    print(args.player_hash, args.player_id, task_type, counter_type)
+                else:
+                    print(args.player_hash, args.player_id, task_type)
+                    task_args = None
+
+            ended = state["ended"]
+
+
+if __name__ == "__main__":
+    asyncio.run(agent())
diff --git a/overcooked_simulator/game_content/agents/run_config.yml b/overcooked_simulator/game_content/agents/run_config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f9c6cdf64e2133ae910dd13d90a8ec734368cd00
--- /dev/null
+++ b/overcooked_simulator/game_content/agents/run_config.yml
@@ -0,0 +1,15 @@
+general:
+  agent_name: cocosy_agent
+  instance: _dev
+  local_agent_directories: ~/aaambos_agents
+  plus:
+    agent_websocket: ws://localhost:8000:/ws/player/MY_CLIENT_ID
+    player_hash: abcdefghijklmnopqrstuvwxyz
+    agent_id: 1
+
+logging:
+    log_level_command_line: INFO
+
+supervisor:
+  run_time_manager_class: !name:aaambos.std.supervision.instruction_run_time_manager.instruction_run_time_manager.InstructionRunTimeManager
+
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
index c0de86d2afc35fad41a5c206b448a0f1eecdad6e..00716059a6b64f9aba3a7ebb2fc00b541582d04f 100644
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ b/overcooked_simulator/game_content/environment_config.yaml
@@ -18,14 +18,16 @@ meals:
 
 layout_chars:
   _: Free
-  hash: Counter
+  hash: Counter  # #
   A: Agent
+  pipe: Extinguisher
   P: PlateDispenser
   C: CuttingBoard
   X: Trashcan
-  W: ServingWindow
+  $: ServingWindow
   S: Sink
   +: SinkAddon
+  at: Plate  # @ just a clean plate on a counter
   U: Pot  # with Stove
   Q: Pan  # with Stove
   O: Peel  # with Oven
@@ -40,6 +42,16 @@ layout_chars:
   G: Sausage  # sausaGe
   B: Bun
   M: Meat
+  question: Counter  # ? mushroom
+  ↓: Counter
+  ^: Counter
+  right: Counter
+  left: Counter
+  wave: Free  # ~ Water
+  minus: Free  # - Ice
+  dquote: Counter  # " wall/truck
+  p: Counter # second plate return ??
+
 
 orders:
   order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
@@ -83,5 +95,63 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 8
+  player_speed_units_per_seconds: 6
   interaction_range: 1.6
+  restricted_view: False
+  view_angle: 70
+  view_range: 5.5  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:overcooked_simulator.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  #  json_states:
+  #    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+  #      log_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      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/game_content/item_info.yaml b/overcooked_simulator/game_content/item_info.yaml
index a6458c6329cbf0323e6fb59338f34e0739c9786e..1266f61ebd611cd5c2a9097b1be9dd7eff65b7f7 100644
--- a/overcooked_simulator/game_content/item_info.yaml
+++ b/overcooked_simulator/game_content/item_info.yaml
@@ -176,3 +176,57 @@ Pizza:
   needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
   seconds: 7.0
   equipment: Peel
+
+# --------------------------------------------------------------------------------
+
+BurntCookedPatty:
+  type: Waste
+  seconds: 5.0
+  needs: [ CookedPatty ]
+  equipment: Pan
+
+BurntChips:
+  type: Waste
+  seconds: 5.0
+  needs: [ Chips ]
+  equipment: Basket
+
+BurntFriedFish:
+  type: Waste
+  seconds: 5.0
+  needs: [ FriedFish ]
+  equipment: Basket
+
+BurntTomatoSoup:
+  type: Waste
+  needs: [ TomatoSoup ]
+  seconds: 6.0
+  equipment: Pot
+
+BurntOnionSoup:
+  type: Waste
+  needs: [ OnionSoup ]
+  seconds: 6.0
+  equipment: Pot
+
+BurntPizza:
+  type: Waste
+  needs: [ Pizza ]
+  seconds: 7.0
+  equipment: Peel
+
+# --------------------------------------------------------------------------------
+
+Fire:
+  type: Effect
+  seconds: 5.0
+  needs: [ BurntCookedPatty, BurntChips, BurntFriedFish, BurntTomatoSoup, BurntOnionSoup, BurntPizza ]
+  manager: FireManager
+  effect_type: Unusable
+
+# --------------------------------------------------------------------------------
+
+Extinguisher:
+  type: Tool
+  seconds: 1.0
+  needs: [ Fire ]
diff --git a/overcooked_simulator/game_content/item_info_debug.yaml b/overcooked_simulator/game_content/item_info_debug.yaml
index c2282253e9539c9686cd6746f536e0316c3b21ec..fd871d1d5685df542317bf772b67f3a3fd8ed7d8 100644
--- a/overcooked_simulator/game_content/item_info_debug.yaml
+++ b/overcooked_simulator/game_content/item_info_debug.yaml
@@ -177,3 +177,57 @@ Pizza:
   needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
   seconds: 0.1
   equipment: Peel
+
+# --------------------------------------------------------------------------------
+
+BurntCookedPatty:
+  type: Waste
+  seconds: 5.0
+  needs: [ CookedPatty ]
+  equipment: Pan
+
+BurntChips:
+  type: Waste
+  seconds: 1.0
+  needs: [ Chips ]
+  equipment: Basket
+
+BurntFriedFish:
+  type: Waste
+  seconds: 5.0
+  needs: [ FriedFish ]
+  equipment: Basket
+
+BurntTomatoSoup:
+  type: Waste
+  needs: [ TomatoSoup ]
+  seconds: 6.0
+  equipment: Pot
+
+BurntOnionSoup:
+  type: Waste
+  needs: [ OnionSoup ]
+  seconds: 6.0
+  equipment: Pot
+
+BurntPizza:
+  type: Waste
+  needs: [ Pizza ]
+  seconds: 7.0
+  equipment: Peel
+
+# --------------------------------------------------------------------------------
+
+Fire:
+  type: Effect
+  seconds: 1.0
+  needs: [ BurntCookedPatty, BurntChips, BurntFriedFish, BurntTomatoSoup, BurntOnionSoup, BurntPizza ]
+  manager: FireManager
+  effect_type: Unusable
+
+# --------------------------------------------------------------------------------
+
+Extinguisher:
+  type: Tool
+  seconds: 0.1
+  needs: [ Fire ]
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/basic.layout b/overcooked_simulator/game_content/layouts/basic.layout
index ccc4076303e985a8b60c9f2dd091f323b5d6e7a6..5238c21f86c487ed90ff8efc6a373c6213d1f6c9 100644
--- a/overcooked_simulator/game_content/layouts/basic.layout
+++ b/overcooked_simulator/game_content/layouts/basic.layout
@@ -1,7 +1,7 @@
 #QU#FO#TNLB#
 #__________M
-#__________K
-W__________I
+|__________K
+$__________I
 #__A_____A_D
 C__________E
 C__________G
diff --git a/overcooked_simulator/game_content/layouts/empty.layout b/overcooked_simulator/game_content/layouts/empty.layout
index 2fa1dd8c29c076e9d94e9a305843ff87bebb29f1..1160842744d30ff014b5e02ac7b5bea7d2421e3d 100644
--- a/overcooked_simulator/game_content/layouts/empty.layout
+++ b/overcooked_simulator/game_content/layouts/empty.layout
@@ -1,7 +1,8 @@
-______
-______
-______
-______
-______
-______
-_____P
\ No newline at end of file
+_______
+_______
+_______
+_______
+__A____
+_______
+_______
+______P
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/large.layout b/overcooked_simulator/game_content/layouts/large.layout
new file mode 100644
index 0000000000000000000000000000000000000000..460244ca3fd685c71cdab3cc2bc6d5d4ae1c9092
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/large.layout
@@ -0,0 +1,23 @@
+#QU#F###O#T#################N###L###B#
+#____________________________________#
+#____________________________________M
+#____________________________________#
+#____________________________________#
+#____________________________________K
+$____________________________________I
+#____________________________________#
+#____________________________________#
+#__A_____A___________________________D
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+C____________________________________E
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+C____________________________________G
+#____________________________________#
+#P#####S+####X#####S+#################
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/large_t.layout b/overcooked_simulator/game_content/layouts/large_t.layout
new file mode 100644
index 0000000000000000000000000000000000000000..304e6f7746f4e0c7510f69395b55c7f691de84f6
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/large_t.layout
@@ -0,0 +1,45 @@
+#QU#F###O#T#################N###L###B#
+#____________________________________#
+#____________________________________M
+#____________________________________#
+#____________________________________#
+#____________________________________K
+W____________________________________I
+#____________________________________#
+#____________________________________#
+#__A_____A___________________________D
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+C____________________________________E
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+#____________________________________#
+C____________________________________G
+#____________________________________#
+#P#####S+####X#####S+#################
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/1-1-far-apart.layout b/overcooked_simulator/game_content/layouts/overcooked-1/1-1-far-apart.layout
new file mode 100644
index 0000000000000000000000000000000000000000..0fa1d52d170632fb5996b5baf907f502d0b1cc06
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/1-1-far-apart.layout
@@ -0,0 +1,14 @@
+###N####U####
+#___________|
+#___A___A___#
+#___________S
+##########__+
+P___________#
+$___________#
+$___________X
+###C#C###@@##
+
+; seconds=150
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/1-1_(Overcooked!)
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/1-2-pedestrians.layout b/overcooked_simulator/game_content/layouts/overcooked-1/1-2-pedestrians.layout
new file mode 100644
index 0000000000000000000000000000000000000000..e9ac2fb08cd9d6437eac0d30da17d6a6114f037e
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/1-2-pedestrians.layout
@@ -0,0 +1,12 @@
+_##U#U#__###|X_#
+______#____A___$
++_____@__@_____$
+S________#_____P
+____A____#______
+_##C#C#__#T#N##_
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/1-2_(Overcooked!)
+; pedestrians: down the middle road
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/1-3-moving-counters.layout b/overcooked_simulator/game_content/layouts/overcooked-1/1-3-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..3dfb4177c4332c3263818e86c6390d562f5bc1bb
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/1-3-moving-counters.layout
@@ -0,0 +1,15 @@
+_____________
+___U#U##$$P|_
+_#____#______
+_@__A_#___A__
+_@____#______
+_@____#______
+_X____#______
+_#C#C##NT?___
+_____________
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=false
+; link: https://overcooked.fandom.com/wiki/1-3_(Overcooked!)
+; moving counters
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/1-4-bottleneck.layout b/overcooked_simulator/game_content/layouts/overcooked-1/1-4-bottleneck.layout
new file mode 100644
index 0000000000000000000000000000000000000000..1eac27c1c4852d05ecec30d6b9ddf1942fceecde
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/1-4-bottleneck.layout
@@ -0,0 +1,14 @@
+##S+####QQQQ#
+T____###____|
+M_A__###__A_#
+B___________#
+L____###____$
+#____###____$
+#____###____P
+X____###____@
+##C#C###@@@@#
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/1-4_(Overcooked!)
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/1-5-circle.layout b/overcooked_simulator/game_content/layouts/overcooked-1/1-5-circle.layout
new file mode 100644
index 0000000000000000000000000000000000000000..a278d9ab1c5d4214d8194d0b2ef82822dff2ae32
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/1-5-circle.layout
@@ -0,0 +1,14 @@
+#####P$$|#####
+#?NT#A_A_#S+##
+#____________X
+#_##########_#
+#_##########_#
+#_##########_#
+#_#######@@@_#
+#____________#
+#C#C####U#U#U#
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/1-5_(Overcooked!)
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/1-6-raising-platforms.layout b/overcooked_simulator/game_content/layouts/overcooked-1/1-6-raising-platforms.layout
new file mode 100644
index 0000000000000000000000000000000000000000..5af323f1c86c874cb126fb25170fe46e67cf01e3
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/1-6-raising-platforms.layout
@@ -0,0 +1,14 @@
+##S+###@Q@Q@#
+M___________#
+T___________|
+L___________$
+#___________$
+#___________P
+X___________#
+##C#C##Q#Q#B#
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/1-6_(Overcooked!)
+; raising platforms based on earthquakes
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/2-1-moving-trucks.layout b/overcooked_simulator/game_content/layouts/overcooked-1/2-1-moving-trucks.layout
new file mode 100644
index 0000000000000000000000000000000000000000..b7a4745ab23db07ddaa344b83e36ffa2840a2a07
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/2-1-moving-trucks.layout
@@ -0,0 +1,17 @@
+_______________
+__#QQQ#@@@#____
+__#_______$____
+__B_______$____
+__#_______P____
+_______________
+__M__A____X____
+__L_______#____
+__T__A____C____
+__#|###C###____
+_______________
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=false
+; link: https://overcooked.fandom.com/wiki/2-1_(Overcooked!)
+; moving trucks: counters and ground are moving
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/2-2-rats.layout b/overcooked_simulator/game_content/layouts/overcooked-1/2-2-rats.layout
new file mode 100644
index 0000000000000000000000000000000000000000..4543c03b09012c4d8f86a88d3eb688f7230ba0cf
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/2-2-rats.layout
@@ -0,0 +1,17 @@
+#####P$$|#####
+#####____#####
+##S+#____#S+##
+X____________X
+#____________#
+U___@__A_@___#
+#___@____@___#
+#___#_A__#___#
+U___#____#___#
+#___#____#___#
+#?N##C##C##NT#
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/2-2_(Overcooked!)
+; rats: steal ingredients + meals
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/2-3-separated-conveyors.layout b/overcooked_simulator/game_content/layouts/overcooked-1/2-3-separated-conveyors.layout
new file mode 100644
index 0000000000000000000000000000000000000000..6980844cd618acaf578975d98b261d0b0c6d0147
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/2-3-separated-conveyors.layout
@@ -0,0 +1,15 @@
+>>>>>>>>>>>>>>>↓
+^#_____##@____#↓
+^+A____|#@_A__#↓
+^S_____Q#C____$↓
+^M_____###____$↓
+^L_____Q#C____P↓
+^B_____###____#↓
+^T_____X#@____X↓
+^<<<<<<<<<<<<<<<
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/2-3_(Overcooked!)
+; conveyors
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/2-4-separated-2.layout b/overcooked_simulator/game_content/layouts/overcooked-1/2-4-separated-2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..d0416de526c511526c42cf430b7ae40ead2ff46b
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/2-4-separated-2.layout
@@ -0,0 +1,16 @@
+#@@@##C#C######
+#_____________$
+#___A_________$
+#_____________P
+####____###___#
+X<<<<<<X>>>>>>X
+#___###____####
+Q_____________#
+#________A____#
+Q_____________#
+##Q#+S#|##BTLM#
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/2-4_(Overcooked!)
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/3-1-ice.layout b/overcooked_simulator/game_content/layouts/overcooked-1/3-1-ice.layout
new file mode 100644
index 0000000000000000000000000000000000000000..4d47b65b2cd83bf38a0bdf9c08c6d235c4074cc9
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/3-1-ice.layout
@@ -0,0 +1,19 @@
+~~~~~~~~~~~~~~~~
+~~~~~~P$$~~~~~~~
+~~~---------~~~~
+~~~-----------~~
+~~--#C#C|##----~
+~---S#####IA---~
+~---+#####K---~~
+~---#FFF##F---~~
+~~-A----@@@--~~~
+~~-----------~~~
+~~~~--------~~~~
+~~~~~~~----~~~~~
+~~~~~~~~~~~~~~~~
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/3-1_(Overcooked!)
+; ice: accelerating
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/3-2-separated-moving-counters.layout b/overcooked_simulator/game_content/layouts/overcooked-1/3-2-separated-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..dc8aefbc3a7c3e149bbff07a600bd452453edee9
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/3-2-separated-moving-counters.layout
@@ -0,0 +1,16 @@
+##$$####$$###
+####P##______
+______?______
+______N______
+U_____T_____U
+X#####X______
+U_____#_____U
+______#______
+__A___#__A___
+@@@C#C#______
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=false
+; link: https://overcooked.fandom.com/wiki/3-2_(Overcooked!)
+; moving counters
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/3-3-moving-trucks-2.layout b/overcooked_simulator/game_content/layouts/overcooked-1/3-3-moving-trucks-2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..f5385f60d38a706c609fc6f199a323f1bfeb36d1
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/3-3-moving-trucks-2.layout
@@ -0,0 +1,15 @@
+__________""#NIK?TX##
+__________""#__A____$
+__________""#_______$
+__________""#__A____P
+__________""_________
+C_________""________C
+#_________""________#
+C_________""________C
+#|U#U#U@@####F@F@F|##
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=false
+; link: https://overcooked.fandom.com/wiki/3-3_(Overcooked!)
+; moving trucks: counters and ground are moving
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/3-4-ice-moving-platforms.layout b/overcooked_simulator/game_content/layouts/overcooked-1/3-4-ice-moving-platforms.layout
new file mode 100644
index 0000000000000000000000000000000000000000..389caab9fc91d253aeba25b3c9129e416139b326
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/3-4-ice-moving-platforms.layout
@@ -0,0 +1,14 @@
+##F#F#~~~~@@F##
+X-----~~~-----#
+#-A---~~~-----#
+I-----~~~-----$
+#-----~~~-----$
+K-----~~~-----P
+|--A-----------
+#+S##~---#C#C##
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/3-4_(Overcooked!)
+; ice, moving platforms, water
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/4-1-moving-counters.layout b/overcooked_simulator/game_content/layouts/overcooked-1/4-1-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..4b6fcaec5c611c09428f82fd8d116c69bee5d47c
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/4-1-moving-counters.layout
@@ -0,0 +1,17 @@
+"""""#|#O#O#O#X#"""
+_____#_________#"""
+__#____________#"""
+__####____##@@@####
+__+_______#_______$
+__S___A___#___A___$
+__#_______#_______P
+__#####C#C#____####
+__#____________#"""
+____"#_________#"""
+"""""###DTE?G###"""
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/4-1_(Overcooked!)
+; moving counters
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/4-2-dark.layout b/overcooked_simulator/game_content/layouts/overcooked-1/4-2-dark.layout
new file mode 100644
index 0000000000000000000000000000000000000000..a591aa97c088a0edaa01287038770ff4df30d579
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/4-2-dark.layout
@@ -0,0 +1,19 @@
+#########|U#@@@#
+##S+##C##______$
+#______________$
+C______________P
+#______________#
+#______###X#####
+U______#######T#
+#__A___________#
+#___________A__#
+N______________#
+#########______?
+################
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/4-2_(Overcooked!)
+; link: https://www.trueachievements.com/game/Overcooked/walkthrough/6
+; dark: only flashlight fov
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/4-3-moving-counters.layout b/overcooked_simulator/game_content/layouts/overcooked-1/4-3-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..eb704056ac8135e4e8cc11ede3dbd135f4794340
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/4-3-moving-counters.layout
@@ -0,0 +1,18 @@
+###S+#####P$$##X#
+#_______|_______#
+C_______________Q
+#_______#_______#
+#_______#_______#
+###_######@@@_###
+B_______#_______#
+T__A____#___A___#
+M_______________Q
+L_______#_______#
+#_______#_______#
+#################
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/4-3_(Overcooked!)
+; moving counters
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/overcooked-1/4-4-moving-counters-separated.layout b/overcooked_simulator/game_content/layouts/overcooked-1/4-4-moving-counters-separated.layout
new file mode 100644
index 0000000000000000000000000000000000000000..88bc660ac2bc1ddcbd2600b715a38613dc4d16b7
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/overcooked-1/4-4-moving-counters-separated.layout
@@ -0,0 +1,16 @@
+#________↓C#O##O###
++________↓________S
+S________↓________+
+#________↓________#
+#________↓#MBLT#@@#
+#|#Q##Q#C↓________p
+P________↓________$
+$_____A__↓________$
+$________↓________#
+#@@#G?DE#X_________
+
+; seconds=240
+; plates={c:0, d:0}
+; dirty_plates=true
+; link: https://overcooked.fandom.com/wiki/4-4_(Overcooked!)
+; moving counters
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/rot_test.layout b/overcooked_simulator/game_content/layouts/rot_test.layout
new file mode 100644
index 0000000000000000000000000000000000000000..d3bd23d0a2dbe378bbc7a128995e8240ebb916fb
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/rot_test.layout
@@ -0,0 +1,5 @@
+##S+#
+S___#
++___S
+#___+
+#+SP#
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/split.layout b/overcooked_simulator/game_content/layouts/split.layout
index 39bace3e0a94b0594d8ef9f294daeb40aae4492f..3f29e313f63f566ad9deb846f465412949d82e0c 100644
--- a/overcooked_simulator/game_content/layouts/split.layout
+++ b/overcooked_simulator/game_content/layouts/split.layout
@@ -1,7 +1,7 @@
 #QU#T###NLB#
 #__________M
 #____A_____#
-W__________#
+$__________#
 ############
 C__________#
 C_____A____#
diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py
index 9a406367ff71e802b5ba31e6d7bf2cd95304f062..94da1512d82c1f538a292359df63cfa3eccd0c73 100644
--- a/overcooked_simulator/game_items.py
+++ b/overcooked_simulator/game_items.py
@@ -26,7 +26,10 @@ import datetime
 import logging
 import uuid
 from enum import Enum
-from typing import Optional, TypedDict
+from typing import Optional, TypedDict, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from overcooked_simulator.effect_manager import EffectManager
 
 log = logging.getLogger(__name__)
 """The logger for this module."""
@@ -38,6 +41,10 @@ COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment"
 """The string for the `category` value in the json state representation for all cooking equipments."""
 
 
+class EffectType(Enum):
+    Unusable = "Unusable"
+
+
 class ItemType(Enum):
     Ingredient = "Ingredient"
     """All ingredients and process ingredients."""
@@ -45,6 +52,12 @@ class ItemType(Enum):
     """All combined ingredients that can be served."""
     Equipment = "Equipment"
     """All counters and cooking equipments."""
+    Waste = "Waste"
+    """Burnt ingredients and meals."""
+    Effect = "Effect"
+    """Does not change the item but the object attributes, like adding fire."""
+    Tool = "Tool"
+    """Item that remains in hands in extends the interactive abilities of the player."""
 
 
 @dataclasses.dataclass
@@ -87,16 +100,24 @@ class ItemInfo:
     """The name of the item, is set automatically by the "group" name of the item."""
     seconds: float = dataclasses.field(compare=False, default=0)
     """If progress is needed this argument defines how long it takes to complete the process in seconds."""
+
+    # TODO maybe as a lambda/based on Prefix?
     needs: list[str] = dataclasses.field(compare=False, default_factory=list)
     """The ingredients/items which are needed to create the item/start the progress."""
     equipment: ItemInfo | None = dataclasses.field(compare=False, default=None)
     """On which the item can be created. `null`, `~` (None) converts to Plate."""
+    manager: str | None | EffectManager = None
+    """The manager for the effect."""
+    effect_type: None | EffectType = None
+    """How does the effect effect interaction, combine actions etc."""
 
     recipe: collections.Counter | None = None
     """Internally set in CookingEquipment"""
 
     def __post_init__(self):
         self.type = ItemType(self.type)
+        if self.effect_type:
+            self.effect_type = EffectType(self.effect_type)
 
 
 class ActiveTransitionTypedDict(TypedDict):
@@ -104,7 +125,7 @@ class ActiveTransitionTypedDict(TypedDict):
 
     seconds: int | float
     """The needed seconds to progress for the transition."""
-    result: str
+    result: str | Item | Effect
     """The new name of the item after the transition."""
 
 
@@ -125,8 +146,12 @@ class Item:
         """The equipment with which the item was last progressed."""
         self.progress_percentage: float = 0.0
         """The current progress percentage of the item if it is progress-able."""
+        self.inverse_progress: bool = False
+        """Whether the progress will produce waste."""
         self.uuid: str = uuid.uuid4().hex if uid is None else uid
         """A unique identifier for the item. Useful for GUIs that handles specific asset instances."""
+        self.active_effects: list[Effect] = []
+        """The effects that affect the item."""
 
     def __repr__(self):
         if self.progress_equipment is None:
@@ -167,6 +192,7 @@ class Item:
         """Reset the progress."""
         self.progress_equipment = None
         self.progress_percentage = 0.0
+        self.inverse_progress = False
 
     def to_dict(self) -> dict:
         """For the state representation. Only the relevant attributes are put into the dict."""
@@ -175,6 +201,8 @@ class Item:
             "category": self.item_category,
             "type": self.name,
             "progress_percentage": self.progress_percentage,
+            "inverse_progress": self.inverse_progress,
+            "active_effects": [e.to_dict() for e in self.active_effects],
         }
 
 
@@ -206,9 +234,17 @@ class CookingEquipment(Item):
 
     def can_combine(self, other) -> bool:
         # already cooking or nothing to combine
-        if other is None:
+        if other is None or (
+            isinstance(other, CookingEquipment) and not other.content_list
+        ):
             return False
 
+        if any(
+            e.item_info.effect_type == EffectType.Unusable for e in other.active_effects
+        ) or any(
+            e.item_info.effect_type == EffectType.Unusable for e in self.active_effects
+        ):
+            return False
         if isinstance(other, CookingEquipment):
             other = other.content_list
         else:
@@ -226,39 +262,72 @@ class CookingEquipment(Item):
             self.content_list.extend(other.content_list)
             return_value = other
             other.reset_content()
+            other.reset()
         elif isinstance(other, list):
             self.content_list.extend(other)
         else:
             self.content_list.append(other)
 
-        ingredients = collections.Counter(item.name for item in self.content_list)
-        for result, transition in self.transitions.items():
-            if ingredients == transition.recipe:
-                if transition.seconds == 0:
-                    self.content_ready = Item(name=result, item_info=transition)
-                else:
-                    self.active_transition = {
-                        "seconds": transition.seconds,
-                        "result": Item(name=result, item_info=transition),
-                    }
-                break
-        else:
-            self.content_ready = None
+        self.check_active_transition()
         return return_value
 
     def can_progress(self) -> bool:
         """Check if the cooking equipment can progress items at all."""
-        return self.active_transition is not None
+        return self.active_transition is not None and not any(
+            e.item_info.effect_type == EffectType.Unusable for e in self.active_effects
+        )
 
     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_list = [self.active_transition["result"]]
+            if isinstance(self.active_transition["result"], Effect):
+                self.active_transition[
+                    "result"
+                ].item_info.manager.register_active_effect(
+                    self.active_transition["result"], self
+                )
+            else:
+                self.content_list = [self.active_transition["result"]]
             self.reset()
+            self.check_active_transition()
 
         # todo set active transition for fire/burnt?
 
+    def check_active_transition(self):
+        ingredients = collections.Counter(item.name for item in self.content_list)
+        for result, transition in self.transitions.items():
+            if transition.type == ItemType.Effect:
+                if set(ingredients.keys()).issubset(
+                    transition.needs
+                ) and transition.manager.can_start_effect_transition(transition, self):
+                    if transition.seconds == 0:
+                        transition.manager.register_active_effect(
+                            Effect(name=transition.name, item_info=transition), self
+                        )
+                    else:
+                        self.active_transition = {
+                            "seconds": transition.seconds,
+                            "result": Effect(
+                                name=transition.name, item_info=transition
+                            ),
+                        }
+                        self.inverse_progress = True
+                    break  # ?
+            else:
+                if ingredients == transition.recipe:
+                    if transition.seconds == 0:
+                        self.content_ready = Item(name=result, item_info=transition)
+                    else:
+                        self.active_transition = {
+                            "seconds": transition.seconds,
+                            "result": Item(name=result, item_info=transition),
+                        }
+                        self.inverse_progress = transition.type == ItemType.Waste
+                    break
+        else:
+            self.content_ready = None
+
     def reset_content(self):
         """Reset the content attributes after the content was picked up from the equipment."""
         self.content_list = []
@@ -268,6 +337,7 @@ class CookingEquipment(Item):
         """Release the content when the player "picks up" the equipment with a plate in the hands"""
         content = self.content_list
         self.reset_content()
+        self.reset()
         return content
 
     @property
@@ -340,3 +410,19 @@ class Plate(CookingEquipment):
         elif self.clean:
             return True
         return False
+
+
+class Effect:
+    def __init__(self, name: str, item_info: ItemInfo, uid: str = None):
+        self.uuid: str = uuid.uuid4().hex if uid is None else uid
+        self.name = name
+        self.item_info = item_info
+        self.progres_percentage = 0.0
+
+    def to_dict(self) -> dict:
+        return {
+            "id": self.uuid,
+            "type": self.name,
+            "progress_percentage": self.progres_percentage,
+            "inverse_progress": True,
+        }
diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py
index 1848a5329552dc8e0f733fcc3b330fd78ada83dd..84879915dce1ff120d6f7a99e405466aa9f7adce 100644
--- a/overcooked_simulator/game_server.py
+++ b/overcooked_simulator/game_server.py
@@ -125,6 +125,7 @@ class EnvironmentHandler:
             layout_config=environment_config.layout_config,
             item_info=environment_config.item_info_config,
             as_files=False,
+            env_name=env_id,
         )
         player_info = {}
         for player_id in range(environment_config.number_players):
@@ -219,7 +220,9 @@ class EnvironmentHandler:
             self.envs[env_id].last_step_time = time.time_ns()
             self.envs[env_id].environment.reset_env_time()
 
-    def get_state(self, player_hash: str) -> str:  # -> StateRepresentation as json
+    def get_state(
+        self, player_hash: str
+    ) -> str | int:  # -> StateRepresentation as json
         """Get the current state representation of the environment for a player.
 
         Args:
@@ -235,7 +238,11 @@ class EnvironmentHandler:
         ):
             return self.envs[
                 self.player_data[player_hash].env_id
-            ].environment.get_json_state()
+            ].environment.get_json_state(self.player_data[player_hash].player_id)
+        if player_hash not in self.player_data:
+            return 1
+        if self.player_data[player_hash].env_id not in self.envs:
+            return 2
 
     def pause_env(self, manager_id: str, env_id: str, reason: str):
         """Pause the specified environment.
@@ -586,6 +593,18 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul
             "player_hash" in message_dict
         ), "'player_hash' key not in message dictionary'"
         match request_type:
+            case PlayerRequestType.GET_STATE:
+                state = environment_handler.get_state(message_dict["player_hash"])
+                if isinstance(state, int):
+                    return {
+                        "request_type": message_dict["type"],
+                        "status": 400,
+                        "msg": "env id of player not in running envs"
+                        if state == 2
+                        else "player hash unknown",
+                        "player_hash": None,
+                    }
+                return state
             case PlayerRequestType.READY:
                 accepted = environment_handler.set_player_ready(
                     message_dict["player_hash"]
@@ -596,10 +615,6 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul
                     "status": 200 if accepted else 400,
                     "player_hash": message_dict["player_hash"],
                 }
-
-            case PlayerRequestType.GET_STATE:
-                return environment_handler.get_state(message_dict["player_hash"])
-
             case PlayerRequestType.ACTION:
                 assert (
                     "action" in message_dict
@@ -655,6 +670,13 @@ class CreateEnvironmentConfig(BaseModel):
     item_info_config: str  # file content
     environment_config: str  # file content
     layout_config: str  # file content
+    seed: int
+
+
+class ManageEnv(BaseModel):
+    manager_id: str
+    env_id: str
+    reason: str
 
 
 class AdditionalPlayer(BaseModel):
@@ -679,8 +701,10 @@ async def additional_player(creation: AdditionalPlayer) -> dict[str, PlayerInfo]
 
 
 @app.post("/manage/stop_env/")
-async def stop_env(manager_id: str, env_id: str, reason: str) -> str:
-    accept = environment_handler.stop_env(manager_id, env_id, reason)
+async def stop_env(manage_env: ManageEnv) -> str:
+    accept = environment_handler.stop_env(
+        manage_env.manager_id, manage_env.env_id, manage_env.reason
+    )
     if accept:
         raise HTTPException(
             status_code=403 if accept == 1 else 409,
@@ -724,7 +748,10 @@ async def websocket_player_endpoint(websocket: WebSocket, client_id: str):
         log.debug(f"Client #{client_id} disconnected")
 
 
-def main(host: str, port: int, manager_ids: list[str]):
+def main(
+    host: str, port: int, manager_ids: list[str], enable_websocket_logging: bool = False
+):
+    setup_logging(enable_websocket_logging)
     loop = asyncio.new_event_loop()
     asyncio.set_event_loop(loop)
     environment_handler.extend_allowed_manager(manager_ids)
@@ -745,8 +772,7 @@ if __name__ == "__main__":
     disable_websocket_logging_arguments(parser)
     add_list_of_manager_ids_arguments(parser)
     args = parser.parse_args()
-    setup_logging(args.enable_websocket_logging)
-    main(args.url, args.port, args.manager_ids)
+    main(args.url, args.port, args.manager_ids, args.enable_websocket_logging)
     """
     Or in console: 
     uvicorn overcooked_simulator.fastapi_game_server:app --reload
diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py
index c7484b081e94dab1150acd6fc6a2709edef8b490..fce7041227f1d95c9b03fb45e7d1f880cf8d0cd9 100644
--- a/overcooked_simulator/gui_2d_vis/drawing.py
+++ b/overcooked_simulator/gui_2d_vis/drawing.py
@@ -1,7 +1,6 @@
 import argparse
 import colorsys
 import json
-import math
 from datetime import datetime, timedelta
 from pathlib import Path
 
@@ -17,6 +16,7 @@ from overcooked_simulator.state_representation import (
     PlayerState,
     CookingEquipmentState,
     ItemState,
+    EffectState,
 )
 
 USE_PLAYER_COOK_SPRITES = True
@@ -24,11 +24,33 @@ SHOW_INTERACTION_RANGE = False
 SHOW_COUNTER_CENTERS = False
 
 
-def create_polygon(n, length):
+def calc_angle(vec_a: list[float], vec_b: list[float]) -> float:
+    a = pygame.math.Vector2(vec_a)
+    b = pygame.math.Vector2(vec_b)
+    return a.angle_to(b)
+
+
+def grayscale(img):
+    arr = pygame.surfarray.pixels3d(img)
+    mean_arr = np.dot(arr[:, :, :], [0.216, 0.587, 0.144])
+    mean_arr3d = mean_arr[..., np.newaxis]
+    new_arr = np.repeat(mean_arr3d[:, :, :], 3, axis=2)
+    new_arr = new_arr.astype(np.int8)
+    surface = pygame.Surface(new_arr.shape[0:2], pygame.SRCALPHA, 32)
+
+    # Copy the rgb part of array to the new surface.
+    pygame.pixelcopy.array_to_surface(surface, new_arr)
+    surface_alpha = np.array(surface.get_view("A"), copy=False)
+    surface_alpha[:, :] = pygame.surfarray.pixels_alpha(img)
+    return surface
+
+
+def create_polygon(n, start_vec):
     if n == 1:
         return np.array([0, 0])
 
-    vector = np.array([length, 0])
+    vector = start_vec.copy()
+
     angle = (2 * np.pi) / n
 
     rot_matrix = np.array(
@@ -44,12 +66,29 @@ def create_polygon(n, length):
 
 
 class Visualizer:
+    """Class for visualizing the game state retrieved from the gameserver.
+    2D game screen is drawn with pygame shapes and images.
+
+    Args:
+        config: Visualization configuration (loaded from yaml file) given as a dict.
+
+    """
+
     def __init__(self, config):
         self.image_cache_dict = {}
         self.player_colors = []
         self.config = config
 
+        self.fire_state = 0
+        self.fire_time_steps = 8
+
     def create_player_colors(self, n) -> None:
+        """Create different colors for the players. The color hues are sampled uniformly in HSV-Space,
+        then the corresponding colors from the defined colors list are looked up.
+
+        Args:
+            n: Number of players to create colors for.
+        """
         hue_values = np.linspace(0, 1, n + 1)
 
         colors_vec = np.array([col for col in colors.values()])
@@ -67,10 +106,19 @@ class Visualizer:
 
     def draw_gamescreen(
         self,
-        screen,
-        state,
-        grid_size,
+        screen: pygame.Surface,
+        state: dict,
+        grid_size: int,
+        controlled_player_idxs: list[int],
     ):
+        """Draws the game state on the given surface.
+
+        Args:
+            screen: The pygame surface to draw the game on.
+            state: The gamestate retrieved from the environment.
+            grid_size: The gridsize to base every object size in the game on.
+        """
+
         width = int(np.ceil(state["kitchen"]["width"] * grid_size))
         height = int(np.ceil(state["kitchen"]["height"] * grid_size))
         self.draw_background(
@@ -85,14 +133,99 @@ class Visualizer:
             grid_size,
         )
 
+        for idx, col in zip(controlled_player_idxs, [colors["blue"], colors["red"]]):
+            pygame.draw.circle(
+                screen,
+                col,
+                np.array(state["players"][idx]["pos"]) * grid_size + (grid_size // 2),
+                (grid_size / 2),
+            )
+
         self.draw_players(
             screen,
             state["players"],
             grid_size,
         )
 
-    def draw_background(self, surface, width, height, grid_size):
-        """Visualizes a game background."""
+        if "view_restriction" in state and state["view_restriction"]:
+            # rotate direction vector in both direction with the angel
+            # draw 2 large rect which are rotated so that one edge is the viewing border
+
+            direction = pygame.math.Vector2(state["view_restriction"]["direction"])
+            pos = pygame.math.Vector2(state["view_restriction"]["position"])
+            angle = state["view_restriction"]["angle"] / 2
+            range = state["view_restriction"]["range"]
+
+            angle = min(angle, 180)
+
+            pos = pos * grid_size + pygame.math.Vector2([grid_size / 2, grid_size / 2])
+
+            rect_scale = max(width, height) * 2
+            # rect_scale = 2 * grid_size
+
+            left_beam = pos + (direction.rotate(angle) * rect_scale * 2)
+            right_beam = pos + (direction.rotate(-angle) * rect_scale * 2)
+
+            offset_front = direction * grid_size * 0.7
+            if angle != 180:
+                pygame.draw.polygon(
+                    screen,
+                    colors["black"],
+                    (
+                        pos - offset_front,
+                        left_beam - offset_front,
+                        left_beam + (direction.rotate(90) * rect_scale),
+                        pos
+                        - (direction * rect_scale * 2)
+                        + (direction.rotate(90) * rect_scale),
+                        pos
+                        - (direction * rect_scale * 2)
+                        + (direction.rotate(-90) * rect_scale),
+                        right_beam + (direction.rotate(-90) * rect_scale),
+                        right_beam - offset_front,
+                    ),
+                )
+            if range:
+                n_circle_points = 40
+
+                start_vec = np.array(-direction * range)
+                points = (
+                    np.array(create_polygon(n_circle_points, start_vec)) * grid_size
+                ) + pos
+
+                circle_closed = np.concatenate([points, points[0:1]], axis=0)
+
+                corners = [
+                    pos - (direction * rect_scale),
+                    *circle_closed,
+                    pos - (direction * rect_scale),
+                    pos
+                    - (direction * rect_scale)
+                    + (direction.rotate(90) * rect_scale),
+                    pos
+                    + (direction * rect_scale)
+                    + (direction.rotate(90) * rect_scale),
+                    pos
+                    + (direction * rect_scale)
+                    + (direction.rotate(-90) * rect_scale),
+                    pos
+                    - (direction * rect_scale)
+                    + (direction.rotate(-90) * rect_scale),
+                ]
+
+                pygame.draw.polygon(screen, colors["black"], [*corners])
+
+    def draw_background(
+        self, surface: pygame.Surface, width: int, height: int, grid_size: int
+    ):
+        """Visualizes a game background.
+
+        Args:
+            surface: The pygame surface to draw the background on.
+            width: The kitchen width.
+            height: The kitchen height.
+            grid_size: The gridsize to base the background shapes on.
+        """
         block_size = grid_size // 2  # Set the size of the grid block
         surface.fill(colors[self.config["Kitchen"]["ground_tiles_color"]])
         for x in range(0, width, block_size):
@@ -112,16 +245,36 @@ class Visualizer:
         size: float,
         pos: npt.NDArray,
         rot_angle=0,
+        burnt: bool = False,
     ):
+        """Draws an image on the given screen.
+
+        Args:
+            screen: The pygame surface to draw the image on.
+            img_path: The path to the image file, given relative to the gui_2d_vis directory.
+            size: The size of the image, given in pixels.
+            pos: The position of the center of the image, given in pixels.
+            rot_angle: Optional angle to rotate the image around.
+        """
         cache_entry = f"{img_path}"
-        if cache_entry in self.image_cache_dict.keys():
-            image = self.image_cache_dict[cache_entry]
+        if cache_entry + ("-burnt" if burnt else "") in self.image_cache_dict:
+            image = self.image_cache_dict[cache_entry + ("-burnt" if burnt else "")]
         else:
-            image = pygame.image.load(
-                ROOT_DIR / "gui_2d_vis" / img_path
-            ).convert_alpha()
-            self.image_cache_dict[cache_entry] = image
-
+            if burnt:
+                if cache_entry in self.image_cache_dict:
+                    normal_image = self.image_cache_dict[cache_entry]
+                else:
+                    normal_image = pygame.image.load(
+                        ROOT_DIR / "gui_2d_vis" / img_path
+                    ).convert_alpha()
+                    self.image_cache_dict[cache_entry] = normal_image
+                image = grayscale(normal_image)
+                self.image_cache_dict[cache_entry + "-burnt"] = image
+            else:
+                image = pygame.image.load(
+                    ROOT_DIR / "gui_2d_vis" / img_path
+                ).convert_alpha()
+                self.image_cache_dict[cache_entry] = image
         image = pygame.transform.scale(image, (size, size))
         if rot_angle != 0:
             image = pygame.transform.rotate(image, rot_angle)
@@ -138,26 +291,29 @@ class Visualizer:
     ):
         """Visualizes the players as circles with a triangle for the facing direction.
         If the player holds something in their hands, it is displayed
-        Args:            state: The game state returned by the environment.
+
+        Args:
+            screen: The pygame surface to draw the players on.
+            players: The state of the players returned by the environment.
+            grid_size: The gridsize to rescale the drawn players to.
         """
         for p_idx, player_dict in enumerate(players):
             player_dict: PlayerState
             pos = np.array(player_dict["pos"]) * grid_size
             pos += grid_size / 2  # correct for grid offset
 
-            facing = np.array(player_dict["facing_direction"])
+            facing = np.array(player_dict["facing_direction"], dtype=float)
 
             if USE_PLAYER_COOK_SPRITES:
                 pygame.draw.circle(
                     screen,
-                    self.player_colors[p_idx],
+                    colors[self.player_colors[p_idx]],
                     pos - facing * grid_size * 0.25,
                     grid_size * 0.2,
                 )
 
                 img_path = self.config["Cook"]["parts"][0]["path"]
-                rel_x, rel_y = facing
-                angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90
+                angle = calc_angle(facing.tolist(), [0, 1])
                 size = self.config["Cook"]["parts"][0]["size"] * grid_size
                 self.draw_image(screen, img_path, size, pos, angle)
 
@@ -199,7 +355,7 @@ class Visualizer:
                 )
 
             if player_dict["holding"] is not None:
-                holding_item_pos = pos + (20 * facing)
+                holding_item_pos = pos + (grid_size * 0.5 * facing)
                 self.draw_item(
                     pos=holding_item_pos,
                     grid_size=grid_size,
@@ -230,6 +386,8 @@ class Visualizer:
         grid_size: float,
         parts: list[dict[str]],
         scale: float = 1.0,
+        burnt: bool = False,
+        orientation: list[float] | None = None,
     ):
         """Draws an item, based on its visual parts specified in the visualization config.
 
@@ -239,23 +397,45 @@ class Visualizer:
             pos: Where to draw the item parts.
             parts: The visual parts to draw.
             scale: Rescale the item by this factor.
+            orientation: Rotate the item to face this direction.
         """
         for part in parts:
             part_type = part["type"]
+            angle, angle_offset = 0, 0
 
             draw_pos = pos.copy()
-            if "center_offset" in part:
-                draw_pos += np.array(part["center_offset"]) * grid_size
+
+            if orientation is not None:
+                angle_offset = calc_angle(orientation, [0, 1])
+                if "rotate_image" in part.keys():
+                    if part["rotate_image"]:
+                        angle = calc_angle(orientation, [0, 1])
+                else:
+                    angle = angle_offset
+            # if "rotate_offset" in part.keys():
+            #     angle_offset = 0
 
             match part_type:
                 case "image":
+                    if "center_offset" in part:
+                        d = pygame.math.Vector2(part["center_offset"]) * grid_size
+                        d.rotate_ip(angle_offset)
+                        d[0] = -d[0]
+                        draw_pos += np.array(d)
                     self.draw_image(
                         screen,
                         part["path"],
                         part["size"] * scale * grid_size,
                         draw_pos,
+                        burnt=burnt,
+                        rot_angle=angle,
                     )
+
                 case "rect":
+                    if "center_offset" in part:
+                        d = pygame.math.Vector2(part["center_offset"]) * grid_size
+                        d.rotate_ip(angle_offset)
+                        draw_pos += np.array(d)
                     height = part["height"] * grid_size
                     width = part["width"] * grid_size
                     color = part["color"]
@@ -266,16 +446,22 @@ class Visualizer:
                         width,
                     )
                     pygame.draw.rect(screen, color, rect)
+
                 case "circle":
+                    if "center_offset" in part:
+                        d = pygame.math.Vector2(part["center_offset"]) * grid_size
+                        d.rotate_ip(-angle_offset)
+                        draw_pos += np.array(d)
                     radius = part["radius"] * grid_size
                     color = colors[part["color"]]
+
                     pygame.draw.circle(screen, color, draw_pos, radius)
 
     def draw_item(
         self,
         pos: npt.NDArray[float] | list[float],
         grid_size: float,
-        item: ItemState | CookingEquipmentState,
+        item: ItemState | CookingEquipmentState | EffectState,
         scale: float = 1.0,
         plate=False,
         screen=None,
@@ -293,37 +479,69 @@ class Visualizer:
             plate: item is on a plate (soup are is different on a plate and pot)
         """
 
-        if not isinstance(item, list):  # can we remove this check?
-            if item["type"] in self.config:
+        if not isinstance(item, list):  # can we remove this check?w
+            if item["type"] in self.config or (
+                item["type"].startswith("Burnt")
+                and item["type"].replace("Burnt", "") in self.config
+            ):
                 item_key = item["type"]
                 if "Soup" in item_key and plate:
                     item_key += "Plate"
+                if item_key.startswith("Burnt"):
+                    item_key = item_key.replace("Burnt", "")
+
+                if item_key == "Fire":
+                    item_key = (
+                        f"{item_key}{int(self.fire_state/self.fire_time_steps)+1}"
+                    )
+
                 self.draw_thing(
                     pos=pos,
                     parts=self.config[item_key]["parts"],
                     scale=scale,
                     screen=screen,
                     grid_size=grid_size,
+                    burnt=item["type"].startswith("Burnt"),
                 )
-                #
+
         if "progress_percentage" in item and item["progress_percentage"] > 0.0:
+            if item["inverse_progress"]:
+                percentage = 1 - item["progress_percentage"]
+            else:
+                percentage = item["progress_percentage"]
             self.draw_progress_bar(
-                screen, pos, item["progress_percentage"], grid_size=grid_size
+                screen,
+                pos,
+                percentage,
+                grid_size=grid_size,
+                attention=item["inverse_progress"],
             )
 
         if (
             "content_ready" in item
             and item["content_ready"]
-            and item["content_ready"]["type"] in self.config
+            and (
+                item["content_ready"]["type"] in self.config
+                or (
+                    item["content_ready"]["type"].startswith("Burnt")
+                    and item["content_ready"]["type"].replace("Burnt", "")
+                    in self.config
+                )
+            )
         ):
             self.draw_thing(
                 pos=pos,
-                parts=self.config[item["content_ready"]["type"]]["parts"],
+                parts=self.config[item["content_ready"]["type"].replace("Burnt", "")][
+                    "parts"
+                ],
                 screen=screen,
                 grid_size=grid_size,
+                burnt=item["type"].startswith("Burnt"),
             )
         elif "content_list" in item and item["content_list"]:
-            triangle_offsets = create_polygon(len(item["content_list"]), length=10)
+            triangle_offsets = create_polygon(
+                len(item["content_list"]), np.array([0, 10])
+            )
             scale = 1 if len(item["content_list"]) == 1 else 0.6
             for idx, o in enumerate(item["content_list"]):
                 self.draw_item(
@@ -334,6 +552,9 @@ class Visualizer:
                     screen=screen,
                     grid_size=grid_size,
                 )
+        if "active_effects" in item and item["active_effects"]:
+            for effect in item["active_effects"]:
+                self.draw_item(pos=pos, item=effect, screen=screen, grid_size=grid_size)
 
     @staticmethod
     def draw_progress_bar(
@@ -341,19 +562,27 @@ class Visualizer:
         pos: npt.NDArray[float],
         percent: float,
         grid_size: float,
+        attention: bool = False,
     ):
-        """Visualize progress of progressing item as a green bar under the item."""
-        pos -= grid_size / 2
+        """Visualize progress of progressing item as a green bar under the item.
+
+        Args:
+            screen: The pygame surface to draw the progress bar on.
+            pos: The center position of a tile to draw the progress bar under.
+            percent: Progressed percent of the progress bar.
+            grid_size: Scaling of the progress bar given in pixels.
+        """
+        bar_pos = pos - (grid_size / 2)
 
         bar_height = grid_size * 0.2
         progress_width = percent * grid_size
         progress_bar = pygame.Rect(
-            pos[0],
-            pos[1] + grid_size - bar_height,
+            bar_pos[0],
+            bar_pos[1] + grid_size - bar_height,
             progress_width,
             bar_height,
         )
-        pygame.draw.rect(screen, colors["green1"], progress_bar)
+        pygame.draw.rect(screen, colors["red" if attention else "green1"], progress_bar)
 
     def draw_counter(
         self, screen: pygame.Surface, counter_dict: dict, grid_size: float
@@ -361,16 +590,31 @@ class Visualizer:
         """Visualization of a counter at its position. If it is occupied by an item, it is also shown.
         The visual composition of the counter is read in from visualization.yaml file, where it is specified as
         different parts to be drawn.
-        Args:            counter: The counter to visualize.
+        Args:
+            screen: The pygame surface to draw the counter on.
+            counter_dict: The counter to visualize, given as a dict from the game state.
+            grid_size: Scaling of the counter given in pixels.
         """
         pos = np.array(counter_dict["pos"], dtype=float) * grid_size
         counter_type = counter_dict["type"]
 
         pos += grid_size // 2  # correct for grid offset
 
-        self.draw_thing(screen, pos, grid_size, self.config["Counter"]["parts"])
+        self.draw_thing(
+            screen,
+            pos,
+            grid_size,
+            self.config["Counter"]["parts"],
+            orientation=counter_dict["orientation"],
+        )
         if counter_type in self.config:
-            self.draw_thing(screen, pos, grid_size, self.config[counter_type]["parts"])
+            self.draw_thing(
+                screen,
+                pos,
+                grid_size,
+                self.config[counter_type]["parts"],
+                orientation=counter_dict["orientation"],
+            )
         else:
             if counter_type in self.config:
                 parts = self.config[counter_type]["parts"]
@@ -383,6 +627,7 @@ class Visualizer:
                 pos=pos,
                 parts=parts,
                 grid_size=grid_size,
+                orientation=counter_dict["orientation"],
             )
 
     def draw_counter_occupier(
@@ -391,7 +636,16 @@ class Visualizer:
         occupied_by: dict | list,
         grid_size,
         pos: npt.NDArray[float],
+        item_scale: float,
     ):
+        """Visualization of a thing lying on a counter.
+        Args:
+            screen: The pygame surface to draw the item on the counter on.
+            occupied_by: The thing that occupies the counter.
+            grid_size: Scaling of the object given in pixels.
+            pos: The position of the counter which the thing lies on.
+            item_scale: Relative scaling of the item.
+        """
         # Multiple plates on plate return:
         if isinstance(occupied_by, list):
             for i, o in enumerate(occupied_by):
@@ -400,6 +654,7 @@ class Visualizer:
                     pos=np.abs([pos[0], pos[1] - (i * 3)]),
                     grid_size=grid_size,
                     item=o,
+                    scale=item_scale,
                 )
         # All other items:
         else:
@@ -408,35 +663,105 @@ class Visualizer:
                 grid_size=grid_size,
                 item=occupied_by,
                 screen=screen,
+                scale=item_scale,
             )
 
-    def draw_counters(self, screen: pygame, counters, grid_size):
+    def draw_counters(self, screen: pygame, counters: dict, grid_size: int):
         """Visualizes the counters in the environment.
 
-        Args:            state: The game state returned by the environment.
+        Args:
+            screen: The pygame surface to draw the counters on.
+            counters: The counter state returned by the environment.
+            grid_size: Scaling of the object given in pixels.
         """
+        global FIRE_STATE
+
         for counter in counters:
             self.draw_counter(screen, counter, grid_size)
 
         for counter in counters:
             if counter["occupied_by"]:
+                item_pos = np.array(counter["pos"])
+                item_scale = 1.0
+
+                counter_type = counter["type"]
+
+                if counter_type.endswith("Dispenser") and "Plate" not in counter_type:
+                    if "item_offset" in self.config["Dispenser"].keys():
+                        offset_vec = pygame.math.Vector2(
+                            self.config["Dispenser"]["item_offset"]
+                        )
+                        offset_vec.rotate_ip(
+                            offset_vec.angle_to(
+                                pygame.math.Vector2(counter["orientation"])
+                            )
+                            + 180
+                        )
+                        item_pos += offset_vec
+                    if "item_scale" in self.config["Dispenser"].keys():
+                        item_scale = self.config["Dispenser"]["item_scale"]
+
                 self.draw_counter_occupier(
-                    screen,
-                    counter["occupied_by"],
-                    grid_size,
-                    np.array(counter["pos"]) * grid_size + (grid_size / 2),
+                    screen=screen,
+                    occupied_by=counter["occupied_by"],
+                    grid_size=grid_size,
+                    pos=item_pos * grid_size + (grid_size / 2),
+                    item_scale=item_scale,
                 )
+            if counter["active_effects"]:
+                for effect in counter["active_effects"]:
+                    self.draw_item(
+                        pos=np.array(counter["pos"]) * grid_size + (grid_size / 2),
+                        grid_size=grid_size,
+                        screen=screen,
+                        item=effect,
+                    )
+
             if SHOW_COUNTER_CENTERS:
-                pygame.draw.circle(
+                pos = np.array(counter["pos"]) * grid_size
+                pygame.draw.circle(screen, colors["green1"], pos, 3)
+                pygame.draw.circle(screen, colors["green1"], pos, 3)
+                facing = np.array(counter["orientation"])
+                pygame.draw.polygon(
                     screen,
-                    colors["green1"],
-                    np.array(counter["pos"]) * grid_size + (grid_size / 2),
-                    3,
+                    colors["red"],
+                    (
+                        (
+                            pos[0] + (facing[1] * 0.1 * grid_size),
+                            pos[1] - (facing[0] * 0.1 * grid_size),
+                        ),
+                        (
+                            pos[0] - (facing[1] * 0.1 * grid_size),
+                            pos[1] + (facing[0] * 0.1 * grid_size),
+                        ),
+                        pos + (facing * 0.5 * grid_size),
+                    ),
                 )
 
+        self.fire_state = (self.fire_state + 1) % (3 * self.fire_time_steps)
+
     def draw_orders(
-        self, screen, state, grid_size, width, height, screen_margin, config
+        self,
+        screen: pygame.surface,
+        state: dict,
+        grid_size: int,
+        width: int,
+        height: int,
+        screen_margin: int,
+        config: dict,
     ):
+        """Visualization of the current orders.
+
+        Args:
+            screen: pygame surface to draw the orders on, probably not the game screen itself.
+            state: The game state returned by the environment.
+            grid_size: Scaling of the drawn orders, given in pixels.
+            width: Width of the pygame window
+            height: Height of the pygame window.
+            screen_margin: Size of the space around the game screen, for buttons, ... .
+            config: Visualization configuration (loaded from yaml file) given as a dict.
+
+        """
         orders_width = width - 100
         orders_height = screen_margin
         order_screen = pygame.Surface(
@@ -491,6 +816,7 @@ class Visualizer:
                 percent=percentage,
                 screen=order_screen,
                 grid_size=grid_size,
+                attention=percentage < 0.25,
             )
 
         orders_rect = order_screen.get_rect()
@@ -503,6 +829,14 @@ class Visualizer:
     def save_state_image(
         self, grid_size: int, state: dict, filename: str | Path
     ) -> None:
+        """Saves a screenshot of the visualization of the given state.
+
+        Args:
+            grid_size: Scaling of the world elements given in pixels.
+            state: Game state returned by the environment.
+            filename: Filename to save the screenshot to.
+
+        """
         width = int(np.ceil(state["kitchen"]["width"] * grid_size))
         height = int(np.ceil(state["kitchen"]["height"] * grid_size))
 
@@ -514,6 +848,15 @@ class Visualizer:
 
 
 def save_screenshot(state: dict, config: dict, filename: str | Path) -> None:
+    """Standalone function to save a screenshot. Creates a visualizer from the config and visualizes
+    the game state, saves it to the given filename.
+
+    Args:
+        state: The gamestate to visualize.
+        config: Visualization config for the visualizer.
+        filename: Filename to save the image to.
+
+    """
     viz = Visualizer(config)
     viz.create_player_colors(len(state["players"]))
     pygame.init()
diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json
index cabbe0368805a697727c738673b1783ac1e56e7e..8e7e819a989131890f69df5df701b99f810ecb50 100644
--- a/overcooked_simulator/gui_2d_vis/gui_theme.json
+++ b/overcooked_simulator/gui_2d_vis/gui_theme.json
@@ -6,7 +6,7 @@
       "disabled_bg": "#25292e",
       "selected_bg": "#193754",
       "dark_bg": "#15191e",
-      "normal_text": "#c5cbd8",
+      "normal_text": "#000000",
       "hovered_text": "#FFFFFF",
       "selected_text": "#FFFFFF",
       "disabled_text": "#6d736f",
@@ -92,5 +92,70 @@
       "normal_border": "#000000",
       "normal_text": "#000000"
     }
+  },
+  "#players": {
+    "colours": {
+      "dark_bg": "#fffacd",
+      "normal_border": "#fffacd"
+    }
+  },
+  "#players_players": {
+    "colours": {
+      "dark_bg": "#fffacd"
+    }
+  },
+  "#players_bots": {
+    "colours": {
+      "dark_bg": "#fffacd"
+    }
+  },
+  "#number_players_label": {
+    "colours": {
+      "dark_bg": "#fffacd",
+      "normal_text": "#000000"
+    },
+    "font": {
+      "size": 14,
+      "bold": 1
+    }
+  },
+  "#number_bots_label": {
+    "colours": {
+      "dark_bg": "#fffacd",
+      "normal_text": "#000000"
+    },
+    "font": {
+      "size": 14,
+      "bold": 1,
+      "colour": "#000000"
+    }
+  },
+  "#multiple_keysets_button": {
+    "font": {
+      "size": 12,
+      "bold": 1,
+      "colour": "#000000"
+    }
+  },
+  "#split_players_button": {
+    "font": {
+      "size": 12,
+      "bold": 1,
+      "colour": "#000000"
+    }
+  },
+  "#controller_button": {
+    "font": {
+      "size": 12,
+      "bold": 1,
+      "colour": "#000000"
+    }
+  },
+  "#quantity_button": {
+    "font": {
+      "size": 24,
+      "bold": 1,
+      "colour": "#000000"
+    }
   }
 }
\ No newline at end of file
diff --git a/overcooked_simulator/gui_2d_vis/images/arrow_right.png b/overcooked_simulator/gui_2d_vis/images/arrow_right.png
index 522ec051e8f1ad938c8e53cd0e8b563f1e383cb1..a1ea0946b67e89bed858a0312ed6ee70ea68c1c7 100644
Binary files a/overcooked_simulator/gui_2d_vis/images/arrow_right.png and b/overcooked_simulator/gui_2d_vis/images/arrow_right.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/counter2.png b/overcooked_simulator/gui_2d_vis/images/counter2.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e88163e958c39f2186412e4838a3f9c08660ede
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/counter2.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/counter4.png b/overcooked_simulator/gui_2d_vis/images/counter4.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad21220dfcb320f3cb5cadafbfb36afc30337064
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/counter4.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/counter5.png b/overcooked_simulator/gui_2d_vis/images/counter5.png
new file mode 100644
index 0000000000000000000000000000000000000000..646589514d340a8a02a1ab6c8e6143d139c93ffb
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/counter5.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fire.png b/overcooked_simulator/gui_2d_vis/images/fire.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d2e5237cbd6125eaeaa7781075a7dc382ab904e
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fire2.png b/overcooked_simulator/gui_2d_vis/images/fire2.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f28ad6e87d7e5985a1dbf9d72d13643a7936b00
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire2.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fire3.png b/overcooked_simulator/gui_2d_vis/images/fire3.png
new file mode 100644
index 0000000000000000000000000000000000000000..65b883b7d5663f7c8f99032c9939e3f4479e03f0
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire3.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fire_extinguisher.png b/overcooked_simulator/gui_2d_vis/images/fire_extinguisher.png
new file mode 100644
index 0000000000000000000000000000000000000000..a03d2d39285c6e286df992826af27c4f8ffc8d16
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire_extinguisher.png differ
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
index aa499ea24689acdc49b6166677e8b0e39aaf4e57..62e5ca6335f20c1c0d6ec49b1e815cde06e4e6d5 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -4,7 +4,10 @@ import json
 import logging
 import random
 import sys
+import uuid
 from enum import Enum
+from pathlib import Path
+from subprocess import Popen
 
 import numpy as np
 import pygame
@@ -24,12 +27,14 @@ from overcooked_simulator.overcooked_environment import (
 )
 from overcooked_simulator.utils import (
     custom_asdict_factory,
-    setup_logging,
     url_and_port_arguments,
     disable_websocket_logging_arguments,
     add_list_of_manager_ids_arguments,
+    setup_logging,
 )
 
+CONNECT_WITH_STUDY_SERVER = False
+
 
 class MenuStates(Enum):
     Start = "Start"
@@ -45,52 +50,77 @@ class PlayerKeySet:
     First four keys are for movement. Order: Down, Up, Left, Right.    5th key is for interacting with counters.    6th key ist for picking up things or dropping them.
     """
 
-    def __init__(self, player_name: str | int, keys: list[pygame.key], joystick: int):
+    def __init__(
+            self,
+            move_keys: list[pygame.key],
+            interact_key: pygame.key,
+            pickup_key: pygame.key,
+            switch_key: pygame.key,
+            players: list[int],
+            joystick: int
+    ):
         """Creates a player key set which contains information about which keyboard keys control the player.
 
         Movement keys in the following order: Down, Up, Left, Right
 
         Args:
-            player_name: The name of the player to control.
-            keys: The keys which control this player in the following order: Down, Up, Left, Right, Interact, Pickup.
+            move_keys: The keys which control this players movement in the following order: Down, Up, Left, Right.
+            interact_key: The key to interact with objects in the game.
+            pickup_key: The key to pick items up or put them down.
+            switch_key: The key for switching through controllable players.
+            players: The player indices which this keyset can control.
             # FIXME: not needed when player name is an index
             joystick: number of joystick (later check if available)
         """
-        self.name = player_name
-        self.player_keys = keys
-        self.move_vectors = [[-1, 0], [1, 0], [0, -1], [0, 1]]
-        self.key_to_movement = {
-            key: vec for (key, vec) in zip(self.player_keys[:-2], self.move_vectors)
+        self.move_vectors: list[list[int]] = [[-1, 0], [1, 0], [0, -1], [0, 1]]
+        self.key_to_movement: dict[pygame.key, list[int]] = {
+            key: vec for (key, vec) in zip(move_keys, self.move_vectors)
         }
-        self.interact_key = self.player_keys[-2]
-        self.pickup_key = self.player_keys[-1]
+        self.move_keys: list[pygame.key] = move_keys
+        self.interact_key: pygame.key = interact_key
+        self.pickup_key: pygame.key = pickup_key
+        self.switch_key: pygame.key = switch_key
+        self.controlled_players: list[int] = players
+        self.current_player: int = players[0] if players else 0
+        self.current_idx = 0
+        self.other_keyset: list[PlayerKeySet] = []
         self.joystick = joystick
 
+    def set_controlled_players(self, controlled_players: list[int]) -> None:
+        self.controlled_players = controlled_players
+        self.current_player = self.controlled_players[0]
+        self.current_idx = 0
+
+    def next_player(self) -> None:
+        self.current_idx = (self.current_idx + 1) % len(self.controlled_players)
+        if self.other_keyset:
+            for ok in self.other_keyset:
+                if ok.current_idx == self.current_idx:
+                    self.next_player()
+                    return
+        self.current_player = self.controlled_players[self.current_idx]
+
 
 class PyGameGUI:
     """Visualisation of the overcooked environment and reading keyboard inputs using pygame."""
 
     def __init__(
             self,
-            player_names: list[str | int],
-            player_keys: list[pygame.key],
             url: str,
             port: int,
             manager_ids: list[str],
     ):
+        pygame.init()
+        pygame.display.set_icon(
+            pygame.image.load(ROOT_DIR / "gui_2d_vis" / "images" / "fish3.png")
+        )
+
         self.game_screen: pygame.Surface = None
         self.FPS = 60
         self.running = True
 
-        self.player_names = player_names
-        self.player_keys = player_keys
-
-        self.player_key_sets: list[PlayerKeySet] = [
-            PlayerKeySet(player_name, keys, joystick=index)
-            for index, (player_name, keys) in enumerate(zip(
-                self.player_names, self.player_keys[: len(self.player_names)]
-            ))
-        ]
+        self.reset_gui_values()
+        self.key_sets: list[PlayerKeySet] = []
 
         self.websocket_url = f"ws://{url}:{port}/ws/player/"
         self.websockets = {}
@@ -104,82 +134,125 @@ class PyGameGUI:
         self.screen_margin = self.visualization_config["GameWindow"]["screen_margin"]
         self.min_width = self.visualization_config["GameWindow"]["min_width"]
         self.min_height = self.visualization_config["GameWindow"]["min_height"]
-
         self.buttons_width = self.visualization_config["GameWindow"]["buttons_width"]
         self.buttons_height = self.visualization_config["GameWindow"]["buttons_height"]
-
         self.order_bar_height = self.visualization_config["GameWindow"][
             "order_bar_height"
         ]
-
-        self.window_width = self.min_width
-        self.window_height = self.min_height
-
-        self.main_window = pygame.display.set_mode(
-            (self.window_width, self.window_height)
-        )
-
-        # self.game_width, self.game_height = 0, 0
-
+        (
+            self.window_width_fullscreen,
+            self.window_height_fullscreen,
+        ) = pygame.display.get_desktop_sizes()[0]
+        self.window_width_windowed = self.min_width
+        self.window_height_windowed = self.min_height
+        self.kitchen_width = 1
+        self.kitchen_height = 1
+        self.kitchen_aspect_ratio = 1
         self.images_path = ROOT_DIR / "pygame_gui" / "images"
+        self.vis = Visualizer(self.visualization_config)
+
+        self.fullscreen = False
 
         self.menu_state = MenuStates.Start
         self.manager: pygame_gui.UIManager
 
-        self.vis = Visualizer(self.visualization_config)
-        self.vis.create_player_colors(len(self.player_names))
-
-    def get_window_sizes(self, state: dict):
-        kitchen_width = state["kitchen"]["width"]
-        kitchen_height = state["kitchen"]["height"]
-        if self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_width":
-            game_width = self.visualization_config["GameWindow"]["size"]
-            kitchen_aspect_ratio = kitchen_height / kitchen_width
-            game_height = int(game_width * kitchen_aspect_ratio)
-            grid_size = int(game_width / (kitchen_width - 0.1))
-
-        elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_height":
-            game_height = self.visualization_config["GameWindow"]["size"]
-            kitchen_aspect_ratio = kitchen_width / kitchen_height
-            game_width = int(game_height * kitchen_aspect_ratio)
-            grid_size = int(game_width / (kitchen_width - 0.1))
-
-        elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "grid":
-            grid_size = self.visualization_config["GameWindow"]["size"]
-            game_width, game_height = (
-                kitchen_width * grid_size,
-                kitchen_height * grid_size,
-            )
+        self.sub_processes = []
 
+    def get_window_sizes_from_state(self, state: dict):
+        self.kitchen_width = state["kitchen"]["width"]
+        self.kitchen_height = state["kitchen"]["height"]
+        self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width
+        game_width = self.visualization_config["GameWindow"]["min_width"] - (
+                2 * self.screen_margin
+        )
+        game_height = self.visualization_config["GameWindow"]["min_height"] - (
+                2 * self.screen_margin
+        )
+
+        if self.kitchen_width > game_width:
+            self.game_height = game_width * self.kitchen_aspect_ratio
+            self.grid_size = game_width / self.kitchen_width
         else:
-            game_width, game_height = 0, 0
-            grid_size = 0
+            self.game_width = game_height / self.kitchen_aspect_ratio
+            self.grid_size = game_width / self.kitchen_width
 
-        window_width, window_height = (
-            game_width + (2 * self.screen_margin),
-            game_height + (2 * self.screen_margin),  # bar with orders
-        )
+        self.window_width_windowed = self.min_width
+        self.window_height_windowed = self.min_height
 
-        window_width = max(window_width, self.min_width)
-        window_height = max(window_height, self.min_height)
-        return (
-            int(window_width),
-            int(window_height),
-            int(game_width),
-            int(game_height),
-            grid_size,
-        )
+    def recalc_game_size(self):
+        log.debug("Resizing game screen")
+        max_width = self.window_width - (2 * self.screen_margin)
+        max_height = self.window_height - (2 * self.screen_margin)
+        if max_width < max_height:
+            self.game_width = max_width
+            self.game_height = max_width * self.kitchen_aspect_ratio
+            self.grid_size = int(self.game_height / self.kitchen_height)
+
+        else:
+            self.game_height = max_height
+            self.game_width = max_height / self.kitchen_aspect_ratio
+            self.grid_size = int(self.game_width / self.kitchen_width)
+
+        self.game_width = max(self.game_width, 100)
+        self.game_height = max(self.game_height, 100)
+        self.grid_size = max(self.grid_size, 1)
+
+        residual_x = self.game_width - (self.kitchen_width * self.grid_size)
+        residual_y = self.game_height - (self.kitchen_height * self.grid_size)
+        self.game_width -= residual_x
+        self.game_height -= residual_y
+
+    def setup_player_keys(self, n=1, disjunct=False):
+        # TODO maybe read the player names and keyboard keys from config file?
+        # First four keys are for movement. Order: Down, Up, Left, Right.
+        # 5th key is for interacting with counters.
+        # 6th key ist for picking up things or dropping them.
+        # FIXME: add buttons of joystick here
+        if n:
+            players = list(range(self.number_humans_to_be_added))
+            key_set1 = PlayerKeySet(
+                move_keys=[pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s],
+                interact_key=pygame.K_f,
+                pickup_key=pygame.K_e,
+                switch_key=pygame.K_SPACE,
+                players=players,
+                joystick=0
+            )
+            key_set2 = PlayerKeySet(
+                move_keys=[pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN],
+                interact_key=pygame.K_i,
+                pickup_key=pygame.K_o,
+                switch_key=pygame.K_p,
+                players=players,
+                joystick=1
+            )
+            key_sets = [key_set1, key_set2]
+
+            if disjunct:
+                key_set1.set_controlled_players(players[::2])
+                key_set2.set_controlled_players(players[1::2])
+            elif n > 1:
+                key_set1.set_controlled_players(players)
+                key_set2.set_controlled_players(players)
+                key_set1.other_keyset = [key_set2]
+                key_set2.other_keyset = [key_set1]
+                key_set2.next_player()
+            return key_sets[:n]
+        else:
+            return []
 
     def handle_keys(self):
         """Handles keyboard inputs. Sends action for the respective players. When a key is held down, every frame
         an action is sent in this function.
         """
+
         keys = pygame.key.get_pressed()
-        for player_idx, key_set in enumerate(self.player_key_sets):
-            relevant_keys = [keys[k] for k in key_set.player_keys]
-            if any(relevant_keys[:-2]):
+        for key_set in self.key_sets:
+            current_player_name = str(key_set.current_player)
+            relevant_keys = [keys[k] for k in key_set.move_keys]
+            if any(relevant_keys):
                 move_vec = np.zeros(2)
-                for idx, pressed in enumerate(relevant_keys[:-2]):
+                for idx, pressed in enumerate(relevant_keys):
                     if pressed:
                         move_vec += key_set.move_vectors[idx]
                 if np.linalg.norm(move_vec) != 0:
@@ -187,7 +260,10 @@ class PyGameGUI:
 
                 print("move vector keys", move_vec)
                 action = Action(
-                    key_set.name, ActionType.MOVEMENT, move_vec, duration=1 / self.FPS
+                    current_player_name,
+                    ActionType.MOVEMENT,
+                    move_vec,
+                    duration=self.time_delta,
                 )
                 self.send_action(action)
 
@@ -235,22 +311,27 @@ class PyGameGUI:
         Args:
             event: Pygame event for extracting the key action.
         """
-        for key_set in self.player_key_sets:
+
+        for key_set in self.key_sets:
+            current_player_name = str(key_set.current_player)
             if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN:
-                action = Action(key_set.name, ActionType.PUT, "pickup")
+                action = Action(current_player_name, ActionType.PUT, "pickup")
                 self.send_action(action)
 
             if event.key == key_set.interact_key:
                 if event.type == pygame.KEYDOWN:
                     action = Action(
-                        key_set.name, ActionType.INTERACT, InterActionData.START
+                        current_player_name, ActionType.INTERACT, InterActionData.START
                     )
                     self.send_action(action)
                 elif event.type == pygame.KEYUP:
                     action = Action(
-                        key_set.name, ActionType.INTERACT, InterActionData.STOP
+                        current_player_name, ActionType.INTERACT, InterActionData.STOP
                     )
                     self.send_action(action)
+            if event.key == key_set.switch_key and not CONNECT_WITH_STUDY_SERVER:
+                if event.type == pygame.KEYDOWN:
+                    key_set.next_player()
 
     def handle_joy_stick_event(self, event, joysticks):
         """Handles joy stick events for the pickup and interaction keys. Pickup is a single action,
@@ -292,42 +373,52 @@ class PyGameGUI:
 
         self.start_button = pygame_gui.elements.UIButton(
             relative_rect=pygame.Rect(
-                (
-                    (self.window_width // 2) - self.buttons_width // 2,
-                    (self.window_height / 2) - self.buttons_height // 2,
-                ),
-                (self.buttons_width, self.buttons_height),
+                (0, 0), (self.buttons_width, self.buttons_height)
             ),
             text="Start Game",
             manager=self.manager,
+            anchors={"center": "center"},
         )
         self.start_button.can_hover()
 
-        self.quit_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    (self.window_width - self.buttons_width),
-                    0,
-                ),
-                (self.buttons_width, self.buttons_height),
+        quit_rect = pygame.Rect(
+            (
+                0,
+                0,
             ),
+            (self.buttons_width, self.buttons_height),
+        )
+        quit_rect.topright = (0, 0)
+        self.quit_button = pygame_gui.elements.UIButton(
+            relative_rect=quit_rect,
             text="Quit Game",
             manager=self.manager,
             object_id="#quit_button",
+            anchors={"right": "right", "top": "top"},
         )
         self.quit_button.can_hover()
 
+        fullscreen_button_rect = pygame.Rect(
+            (0, 0), (self.buttons_width * 0.7, self.buttons_height)
+        )
+        fullscreen_button_rect.topright = (-self.buttons_width, 0)
+        self.fullscreen_button = pygame_gui.elements.UIButton(
+            relative_rect=fullscreen_button_rect,
+            text="Fullscreen",
+            manager=self.manager,
+            object_id="#fullscreen_button",
+            anchors={"right": "right", "top": "top"},
+        )
+        self.fullscreen_button.can_hover()
+
+        reset_button_rect = pygame.Rect((0, 0), (self.screen_margin * 0.75, 50))
+        reset_button_rect.topright = (0, 2 * self.buttons_height)
         self.reset_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    self.window_width - (self.screen_margin * 3 // 4),
-                    self.screen_margin,
-                ),
-                (self.screen_margin - (self.screen_margin // 4), 50),
-            ),
+            relative_rect=reset_button_rect,
             text="RESET",
             manager=self.manager,
             object_id="#reset_button",
+            anchors={"right": "right", "top": "top"},
         )
         self.reset_button.can_hover()
 
@@ -372,8 +463,9 @@ class PyGameGUI:
 
         self.layout_file_paths = {
             str(p.name): p
-            for p in (ROOT_DIR / "game_content" / "layouts").glob("*.layout")
+            for p in [Path(f) for f in sorted((ROOT_DIR / "game_content" / "layouts").rglob("*.layout"))]
         }
+
         assert len(self.layout_file_paths) != 0, "No layout files."
         dropdown_width, dropdown_height = 200, 40
         self.layout_selection = pygame_gui.elements.UIDropDownMenu(
@@ -414,6 +506,171 @@ class PyGameGUI:
             object_id="#score_label",
         )
 
+        #######################
+
+        player_selection_rect = pygame.Rect(
+            (0, 0),
+            (
+                self.window_width * 0.9,
+                (self.window_height // 3),
+            ),
+        )
+        player_selection_rect.bottom = -10
+        self.player_selection_container = pygame_gui.elements.UIPanel(
+            player_selection_rect,
+            manager=self.manager,
+            object_id="#players",
+            anchors={"bottom": "bottom", "centerx": "centerx"},
+        )
+
+        multiple_keysets_button_rect = pygame.Rect((0, 0), (190, 50))
+        self.multiple_keysets_button = pygame_gui.elements.UIButton(
+            relative_rect=multiple_keysets_button_rect,
+            manager=self.manager,
+            container=self.player_selection_container,
+            text="not set",
+            anchors={"left": "left", "centery": "centery"},
+            object_id="#multiple_keysets_button",
+        )
+
+        split_players_button_rect = pygame.Rect((0, 0), (190, 50))
+        self.split_players_button = pygame_gui.elements.UIButton(
+            relative_rect=split_players_button_rect,
+            manager=self.manager,
+            container=self.player_selection_container,
+            text="not set",
+            anchors={"centerx": "centerx", "centery": "centery"},
+            object_id="#split_players_button",
+        )
+        if self.multiple_keysets:
+            self.split_players_button.show()
+        else:
+            self.split_players_button.hide()
+
+        xbox_controller_button_rect = pygame.Rect((0, 0), (190, 50))
+        xbox_controller_button_rect.right = 0
+        self.xbox_controller_button = pygame_gui.elements.UIButton(
+            relative_rect=xbox_controller_button_rect,
+            manager=self.manager,
+            container=self.player_selection_container,
+            text="Controller?",
+            anchors={"right": "right", "centery": "centery"},
+            object_id="#controller_button",
+        )
+
+        ########
+        #
+        # panel = pygame_gui.elements.UIPanel(
+        #     pygame.Rect((50, 50), (700, 500)),
+        #     manager=manager,
+        #     anchors={
+        #         "left": "left",
+        #         "right": "right",
+        #         "top": "top",
+        #         "bottom": "bottom",
+        #     },
+        # )
+
+        players_container_rect = pygame.Rect(
+            (0, 0),
+            (
+                self.window_width * 0.6,
+                self.player_selection_container.get_abs_rect().height // 3,
+            ),
+        )
+        self.player_number_container = pygame_gui.elements.UIPanel(
+            relative_rect=players_container_rect,
+            manager=self.manager,
+            object_id="#players_players",
+            container=self.player_selection_container,
+            anchors={"top": "top", "centerx": "centerx"},
+        )
+
+        bot_container_rect = pygame.Rect(
+            (0, 0),
+            (
+                self.window_width * 0.6,
+                self.player_selection_container.get_abs_rect().height // 3,
+            ),
+        )
+        bot_container_rect.bottom = 0
+        self.bot_number_container = pygame_gui.elements.UIPanel(
+            relative_rect=bot_container_rect,
+            manager=self.manager,
+            object_id="#players_bots",
+            container=self.player_selection_container,
+            anchors={"bottom": "bottom", "centerx": "centerx"},
+        )
+
+        number_players_rect = pygame.Rect((0, 0), (200, 200))
+        self.added_players_label = pygame_gui.elements.UILabel(
+            number_players_rect,
+            manager=self.manager,
+            object_id="#number_players_label",
+            container=self.player_number_container,
+            text=f"Humans to be added: {self.number_humans_to_be_added}",
+            anchors={"center": "center"},
+        )
+
+        number_bots_rect = pygame.Rect((0, 0), (200, 200))
+        self.added_bots_label = pygame_gui.elements.UILabel(
+            number_bots_rect,
+            manager=self.manager,
+            object_id="#number_bots_label",
+            container=self.bot_number_container,
+            text=f"Bots to be added: {self.number_bots_to_be_added}",
+            anchors={"center": "center"},
+        )
+
+        size = 50
+        add_player_button_rect = pygame.Rect((0, 0), (size, size))
+        add_player_button_rect.right = 0
+        self.add_human_player_button = pygame_gui.elements.UIButton(
+            relative_rect=add_player_button_rect,
+            text="+",
+            manager=self.manager,
+            object_id="#quantity_button",
+            container=self.player_number_container,
+            anchors={"right": "right", "centery": "centery"},
+        )
+        self.add_human_player_button.can_hover()
+
+        remove_player_button_rect = pygame.Rect((0, 0), (size, size))
+        remove_player_button_rect.left = 0
+        self.remove_human_button = pygame_gui.elements.UIButton(
+            relative_rect=remove_player_button_rect,
+            text="-",
+            manager=self.manager,
+            object_id="#quantity_button",
+            container=self.player_number_container,
+            anchors={"left": "left", "centery": "centery"},
+        )
+        self.remove_human_button.can_hover()
+
+        add_bot_button_rect = pygame.Rect((0, 0), (size, size))
+        add_bot_button_rect.right = 0
+        self.add_bot_button = pygame_gui.elements.UIButton(
+            relative_rect=add_bot_button_rect,
+            text="+",
+            manager=self.manager,
+            object_id="#quantity_button",
+            container=self.bot_number_container,
+            anchors={"right": "right", "centery": "centery"},
+        )
+        self.add_bot_button.can_hover()
+
+        remove_bot_button_rect = pygame.Rect((0, 0), (size, size))
+        remove_bot_button_rect.left = 0
+        self.remove_bot_button = pygame_gui.elements.UIButton(
+            relative_rect=remove_bot_button_rect,
+            text="-",
+            manager=self.manager,
+            object_id="#quantity_button",
+            container=self.bot_number_container,
+            anchors={"left": "left", "centery": "centery"},
+        )
+        self.remove_bot_button.can_hover()
+
     def draw(self, state):
         """Main visualization function.
 
@@ -422,6 +679,7 @@ class PyGameGUI:
             self.game_screen,
             state,
             self.grid_size,
+            [k.current_player for k in self.key_sets],
         )
 
         # self.manager.draw_ui(self.main_window)
@@ -430,8 +688,8 @@ class PyGameGUI:
         self.vis.draw_orders(
             screen=self.main_window,
             state=state,
-            grid_size=self.grid_size,
-            width=self.game_width,
+            grid_size=self.buttons_height,
+            width=self.window_width - self.buttons_width - (self.buttons_width * 0.7),
             height=self.game_height,
             screen_margin=self.screen_margin,
             config=self.visualization_config,
@@ -453,23 +711,51 @@ class PyGameGUI:
 
         self.update_score_label(state)
 
+        if state["info_msg"]:
+            for idx, msg in enumerate(reversed(state["info_msg"])):
+                text_surface = self.comic_sans.render(
+                    msg[0],
+                    antialias=True,
+                    color=(0, 0, 0)
+                    if msg[1] == "Normal"
+                    else ((255, 0, 0) if msg[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 + (20 * idx),
+                    ),
+                )
+
     def set_window_size(self):
         self.game_screen = pygame.Surface(
             (
                 self.game_width,
                 self.game_height,
-            ),
+            )
         )
+
+        if self.fullscreen:
+            flags = pygame.FULLSCREEN
+            self.window_width = self.window_width_fullscreen
+            self.window_height = self.window_height_fullscreen
+        else:
+            flags = 0
+            self.window_width = self.window_width_windowed
+            self.window_height = self.window_height_windowed
+
         self.main_window = pygame.display.set_mode(
             (
                 self.window_width,
                 self.window_height,
-            )
+            ),
+            flags=flags,
+            display=0,
         )
 
     def reset_window_size(self):
-        self.window_width = self.min_width
-        self.window_height = self.min_height
         self.game_width = 0
         self.game_height = 0
         self.set_window_size()
@@ -488,6 +774,8 @@ class PyGameGUI:
                 self.timer_label.hide()
                 self.orders_label.hide()
                 self.conclusion_label.hide()
+
+                self.player_selection_container.show()
             case MenuStates.Game:
                 self.start_button.hide()
                 self.back_button.hide()
@@ -499,6 +787,9 @@ class PyGameGUI:
                 self.timer_label.show()
                 self.orders_label.show()
                 self.conclusion_label.hide()
+
+                self.player_selection_container.hide()
+
             case MenuStates.End:
                 self.start_button.hide()
                 self.back_button.show()
@@ -510,6 +801,8 @@ class PyGameGUI:
                 self.orders_label.hide()
                 self.conclusion_label.show()
 
+                self.player_selection_container.hide()
+
     def update_score_label(self, state):
         score = state["score"]
         self.score_label.set_text(f"Score {score}")
@@ -525,70 +818,160 @@ class PyGameGUI:
         self.timer_label.set_text(f"Time remaining: {display_time}")
 
     def setup_environment(self):
-        environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
-        layout_path = self.layout_file_paths[self.layout_selection.selected_option]
-        item_info_path = ROOT_DIR / "game_content" / "item_info_debug.yaml"
-        with open(item_info_path, "r") as file:
-            item_info = file.read()
-        with open(layout_path, "r") as file:
-            layout = file.read()
-        with open(environment_config_path, "r") as file:
-            environment_config = file.read()
-        creation_json = CreateEnvironmentConfig(
-            manager_id=self.manager_id,
-            number_players=2,
-            environment_settings={"all_player_can_pause_game": False},
-            item_info_config=item_info,
-            environment_config=environment_config,
-            layout_config=layout,
-        ).model_dump(mode="json")
-        # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
-        env_info = requests.post(
-            f"{self.request_url}/manage/create_env/",
-            json=creation_json,
-        )
-        if env_info.status_code == 403:
-            raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
-        env_info = env_info.json()
-        assert isinstance(env_info, dict), "Env info must be a dictionary"
-        self.current_env_id = env_info["env_id"]
-        self.player_info = env_info["player_info"]
-        for player_id, player_info in env_info["player_info"].items():
+        if CONNECT_WITH_STUDY_SERVER:
+            self.player_info = requests.post(
+                f"http://localhost:8080/connect_to_game/{uuid.uuid4().hex}"
+            ).json()
+            self.key_sets[0].current_player = int(self.player_info["player_id"])
+            self.player_info = {self.player_info["player_id"]: self.player_info}
+        else:
+            environment_config_path = (
+                    ROOT_DIR / "game_content" / "environment_config.yaml"
+            )
+            layout_path = self.layout_file_paths[self.layout_selection.selected_option]
+            item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
+            with open(item_info_path, "r") as file:
+                item_info = file.read()
+            with open(layout_path, "r") as file:
+                layout = file.read()
+            with open(environment_config_path, "r") as file:
+                environment_config = file.read()
+
+            seed = 161616161616
+            creation_json = CreateEnvironmentConfig(
+                manager_id=self.manager_id,
+                number_players=self.number_players,
+                environment_settings={"all_player_can_pause_game": False},
+                item_info_config=item_info,
+                environment_config=environment_config,
+                layout_config=layout,
+                seed=seed,
+            ).model_dump(mode="json")
+
+            # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
+            env_info = requests.post(
+                f"{self.request_url}/manage/create_env/",
+                json=creation_json,
+            )
+            if env_info.status_code == 403:
+                raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
+            env_info = env_info.json()
+            assert isinstance(env_info, dict), "Env info must be a dictionary"
+            self.current_env_id = env_info["env_id"]
+            self.player_info = env_info["player_info"]
+
+        state = None
+        for p, (player_id, player_info) in enumerate(self.player_info.items()):
+            if p < self.number_humans_to_be_added:
+                websocket = connect(self.websocket_url + player_info["client_id"])
+                websocket.send(
+                    json.dumps(
+                        {"type": "ready", "player_hash": player_info["player_hash"]}
+                    )
+                )
+                assert (
+                        json.loads(websocket.recv())["status"] == 200
+                ), "not accepted player"
+                self.websockets[player_id] = websocket
+            else:
+                player_hash = player_info["player_hash"]
+                print(
+                    f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"'
+                )
+                # sub = Popen(
+                #     " ".join(
+                #         [
+                #             "exec",
+                #             "aaambos",
+                #             "run",
+                #             "--arch_config",
+                #             str(
+                #                 ROOT_DIR / "game_content" / "agents" / "arch_config.yml"
+                #             ),
+                #             "--run_config",
+                #             str(
+                #                 ROOT_DIR / "game_content" / "agents" / "run_config.yml"
+                #             ),
+                #             f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"',
+                #             f"--instance={player_hash}",
+                #         ]
+                #     ),
+                #     shell=True,
+                # )
+                sub = Popen(
+                    " ".join(
+                        [
+                            "python",
+                            str(
+                                ROOT_DIR / "game_content" / "agents" / "random_agent.py"
+                            ),
+                            f'--uri {self.websocket_url + player_info["client_id"]}',
+                            f"--player_hash {player_hash}",
+                            f"--player_id {player_id}",
+                        ]
+                    ),
+                    shell=True,
+                )
+                self.sub_processes.append(sub)
+
+            if p + 1 == self.number_humans_to_be_added:
+                self.state_player_id = player_id
+                websocket.send(
+                    json.dumps(
+                        {"type": "get_state", "player_hash": player_info["player_hash"]}
+                    )
+                )
+                state = json.loads(websocket.recv())
+
+        if not self.number_humans_to_be_added:
+            player_id = "0"
+            player_info = self.player_info[player_id]
             websocket = connect(self.websocket_url + player_info["client_id"])
             websocket.send(
                 json.dumps({"type": "ready", "player_hash": player_info["player_hash"]})
             )
             assert json.loads(websocket.recv())["status"] == 200, "not accepted player"
             self.websockets[player_id] = websocket
-        self.state_player_id = player_id
-        websocket.send(
-            json.dumps({"type": "get_state", "player_hash": player_info["player_hash"]})
-        )
-        state = json.loads(websocket.recv())
+            self.state_player_id = player_id
+            websocket.send(
+                json.dumps(
+                    {"type": "get_state", "player_hash": player_info["player_hash"]}
+                )
+            )
+            state = json.loads(websocket.recv())
 
-        (
-            self.window_width,
-            self.window_height,
-            self.game_width,
-            self.game_height,
-            self.grid_size,
-        ) = self.get_window_sizes(state)
+        self.get_window_sizes_from_state(state)
 
     def start_button_press(self):
         self.menu_state = MenuStates.Game
 
+        self.number_players = (
+                self.number_humans_to_be_added + self.number_bots_to_be_added
+        )
+        self.vis.create_player_colors(self.number_players)
+
+        if self.split_players:
+            assert (
+                    self.number_humans_to_be_added > 1
+            ), "Not enough players for key configuration."
+        num_key_set = 2 if self.multiple_keysets else 1
+        self.key_sets = self.setup_player_keys(
+            min(self.number_humans_to_be_added, num_key_set), self.split_players
+        )
+
         self.setup_environment()
 
+        self.recalc_game_size()
         self.set_window_size()
-
         self.init_ui_elements()
         log.debug("Pressed start button")
 
-        # self.api.set_sim(self.simulator)
-
     def back_button_press(self):
         self.menu_state = MenuStates.Start
         self.reset_window_size()
+
+        self.update_selection_elements()
+
         log.debug("Pressed back button")
 
     def quit_button_press(self):
@@ -597,31 +980,69 @@ class PyGameGUI:
         log.debug("Pressed quit button")
 
     def reset_button_press(self):
-        requests.post(
-            f"{self.request_url}/manage/stop_env",
-            json={
-                "manager_id": self.manager_id,
-                "env_id": self.current_env_id,
-                "reason": "reset button pressed",
-            },
-        )
+        # self.reset_gui_values()
+        if not CONNECT_WITH_STUDY_SERVER:
+            requests.post(
+                f"{self.request_url}/manage/stop_env",
+                json={
+                    "manager_id": self.manager_id,
+                    "env_id": self.current_env_id,
+                    "reason": "reset button pressed",
+                },
+            )
 
         # self.websocket.send(json.dumps("reset_game"))
         # answer = self.websocket.recv()        log.debug("Pressed reset button")
 
     def finished_button_press(self):
-        requests.post(
-            f"{self.request_url}/manage/stop_env",
-            json={
-                "manager_id": self.manager_id,
-                "env_id": self.current_env_id,
-                "reason": "finish button pressed",
-            },
-        )
+        if not CONNECT_WITH_STUDY_SERVER:
+            requests.post(
+                f"{self.request_url}/manage/stop_env/",
+                json={
+                    "manager_id": self.manager_id,
+                    "env_id": self.current_env_id,
+                    "reason": "finish button pressed",
+                },
+            )
         self.menu_state = MenuStates.End
         self.reset_window_size()
         log.debug("Pressed finished button")
 
+    def reset_gui_values(self):
+        self.currently_controlled_player_idx = 0
+        self.number_humans_to_be_added = 1
+        self.number_bots_to_be_added = 0
+        self.split_players = False
+        self.multiple_keysets = False
+        self.player_minimum = 1
+
+    def update_selection_elements(self):
+        if self.number_humans_to_be_added <= self.player_minimum:
+            self.remove_human_button.disable()
+            self.number_humans_to_be_added = self.player_minimum
+        else:
+            self.remove_human_button.enable()
+        self.number_humans_to_be_added = max(
+            self.player_minimum, self.number_humans_to_be_added
+        )
+
+        text = "WASD+ARROW" if self.multiple_keysets else "WASD"
+        self.multiple_keysets_button.set_text(text)
+        # self.split_players_button
+        self.added_players_label.set_text(
+            f"Humans to be added: {self.number_humans_to_be_added}"
+        )
+        self.added_bots_label.set_text(
+            f"Bots to be added: {self.number_bots_to_be_added}"
+        )
+        text = "Yes" if self.split_players else "No"
+        self.split_players_button.set_text(f"Split players: {text}")
+
+        if self.multiple_keysets:
+            self.split_players_button.show()
+        else:
+            self.split_players_button.hide()
+
     def send_action(self, action: Action):
         """Sends an action to the game environment.
 
@@ -651,35 +1072,48 @@ class PyGameGUI:
             json.dumps(
                 {
                     "type": "get_state",
-                    "player_hash": self.player_info[self.state_player_id][
-                        "player_hash"
-                    ],
+                    "player_hash": self.player_info[
+                        str(self.key_sets[0].current_player)
+                    ]["player_hash"],
                 }
             )
         )
-        # self.websocket.send(json.dumps("get_state"))
-        # state_dict = json.loads(self.websocket.recv())
         state = json.loads(self.websockets[self.state_player_id].recv())
         return state
 
     def disconnect_websockets(self):
+        for sub in self.sub_processes:
+            try:
+                sub.kill()
+                # pgrp = os.getpgid(sub.pid)
+                # os.killpg(pgrp, signal.SIGINT)
+                # subprocess.run(
+                #     "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True
+                # )
+            except ProcessLookupError:
+                pass
+
+        self.sub_processes = []
         for websocket in self.websockets.values():
             websocket.close()
 
     def start_pygame(self):
         """Starts pygame and the gui loop. Each frame the game state is visualized and keyboard inputs are read."""
         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")
 
         clock = pygame.time.Clock()
 
         self.reset_window_size()
+
         self.init_ui_elements()
         self.manage_button_visibility()
 
+        self.update_selection_elements()
+
         # Game loop
         self.running = True
         # This dict can be left as-is, since pygame will generate a
@@ -689,8 +1123,9 @@ class PyGameGUI:
 
         while self.running:
             try:
-                time_delta = clock.tick(self.FPS) / 1000.0
+                self.time_delta = clock.tick(self.FPS) / 1000
 
+                # print(clock.get_time())
                 for event in pygame.event.get():
                     if event.type == pygame.QUIT:
                         self.running = False
@@ -703,11 +1138,25 @@ class PyGameGUI:
                         joysticks[joy.get_instance_id()] = joy
                         print(f"Joystick {joy.get_instance_id()} connected")
 
-                        # UI Buttons:
+                    # elif event.type == pygame.VIDEORESIZE:
+                    #     # scrsize = event.size
+                    #     self.window_width_windowed = event.w
+                    #     self.window_height_windowed = event.h
+                    #     self.recalc_game_size()
+                    #     self.set_window_size()
+                    #     self.init_ui_elements()
+                    #     self.manage_button_visibility()
+
                     if event.type == pygame_gui.UI_BUTTON_PRESSED:
                         match event.ui_element:
                             case self.start_button:
+                                if not (
+                                        self.number_humans_to_be_added
+                                        + self.number_bots_to_be_added
+                                ):
+                                    continue
                                 self.start_button_press()
+
                             case self.back_button:
                                 self.back_button_press()
                                 self.disconnect_websockets()
@@ -715,14 +1164,55 @@ class PyGameGUI:
                             case self.finished_button:
                                 self.finished_button_press()
                                 self.disconnect_websockets()
+
                             case self.quit_button:
                                 self.quit_button_press()
                                 self.disconnect_websockets()
+
                             case self.reset_button:
                                 self.reset_button_press()
                                 self.disconnect_websockets()
                                 self.start_button_press()
 
+                            case self.add_human_player_button:
+                                self.number_humans_to_be_added += 1
+                            case self.remove_human_button:
+                                self.number_humans_to_be_added = max(
+                                    0, self.number_humans_to_be_added - 1
+                                )
+                            case self.add_bot_button:
+                                self.number_bots_to_be_added += 1
+                            case self.remove_bot_button:
+                                self.number_bots_to_be_added = max(
+                                    0, self.number_bots_to_be_added - 1
+                                )
+                            case self.multiple_keysets_button:
+                                self.multiple_keysets = not self.multiple_keysets
+                                self.split_players = False
+                            case self.split_players_button:
+                                self.split_players = not self.split_players
+                                if self.split_players:
+                                    self.player_minimum = 2
+                                else:
+                                    self.player_minimum = 1
+
+                            case self.xbox_controller_button:
+                                print("xbox_controller_button pressed.")
+
+                            case self.fullscreen_button:
+                                self.fullscreen = not self.fullscreen
+                                if self.fullscreen:
+                                    self.window_width = self.window_width_fullscreen
+                                    self.window_height = self.window_height_fullscreen
+                                else:
+                                    self.window_width = self.window_width_windowed
+                                    self.window_height = self.window_height_windowed
+                                self.recalc_game_size()
+                                self.set_window_size()
+                                self.init_ui_elements()
+
+                        self.update_selection_elements()
+
                         self.manage_button_visibility()
 
                     if (
@@ -771,7 +1261,7 @@ class PyGameGUI:
                     case MenuStates.End:
                         self.update_conclusion_label(state)
 
-                self.manager.update(time_delta)
+                self.manager.update(self.time_delta)
                 pygame.display.flip()
 
             except (KeyboardInterrupt, SystemExit):
@@ -783,25 +1273,8 @@ class PyGameGUI:
 
 
 def main(url: str, port: int, manager_ids: list[str]):
-    # TODO maybe read the player names and keyboard keys from config file?
-    # First four keys are for movement. Order: Down, Up, Left, Right.
-    # 5th key is for interacting with counters.
-    # 6th key ist for picking up things or dropping them.
-    keys1 = [
-        pygame.K_LEFT,
-        pygame.K_RIGHT,
-        pygame.K_UP,
-        pygame.K_DOWN,
-        pygame.K_SPACE,
-        pygame.K_i,
-    ]
-    keys2 = [pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s, pygame.K_f, pygame.K_e]
-    # FIXME: add buttons of joystick here
-
-    number_players = 2
+    setup_logging()
     gui = PyGameGUI(
-        list(map(str, range(number_players))),
-        [keys1, keys2],
         url=url,
         port=port,
         manager_ids=manager_ids,
@@ -820,5 +1293,4 @@ if __name__ == "__main__":
     disable_websocket_logging_arguments(parser)
     add_list_of_manager_ids_arguments(parser)
     args = parser.parse_args()
-    setup_logging(enable_websocket_logging=args.enable_websocket_logging)
     main(args.url, args.port, args.manager_ids)
diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml
index 87415a0ed41fa01c46c5fb8e830d370e4acd776b..a0d7989c1c1bf9c2a96fcc28e1baf09bcd96e55e 100644
--- a/overcooked_simulator/gui_2d_vis/visualization.yaml
+++ b/overcooked_simulator/gui_2d_vis/visualization.yaml
@@ -1,10 +1,8 @@
 # colors: https://www.webucator.com/article/python-color-constants-module/
 
 GameWindow:
-  WhatIsFixed: grid  # grid or window_width or window_height
-  size: 50
   screen_margin: 100
-  min_width: 700
+  min_width: 900
   min_height: 600
   buttons_width: 180
   buttons_height: 60
@@ -22,84 +20,97 @@ Kitchen:
 
 Counter:
   parts:
-    - type: rect
-      height: 1
-      width: 1
-      color: whitesmoke
+    #    - type: rect
+    #      height: 1
+    #      width: 1
+    #      color: whitesmoke
+    - type: image
+      path: images/counter5.png
+      size: 1
 
 CuttingBoard:
   parts:
     - type: image
       path: images/cutting_board_large.png
-      size: 0.9
+      size: 0.75
+      center_offset: [ 0, -0.05 ]
 
 
 PlateDispenser:
-  parts:
-    - type: rect
-      height: 0.95
-      width: 0.95
-      color: cadetblue1
+  parts: [ ]
+#    - type: rect
+#      height: 0.95
+#      width: 0.95
+#      color: cadetblue1
 
 Trashcan:
   parts:
     - type: image
       path: images/trash3.png
-      size: 0.9
-      center_offset: [ 0, 0 ]
-
-TomatoDispenser:
-  parts:
-    - color: orangered1
-      type: rect
-      height: 0.8
-      width: 0.8
-
-LettuceDispenser:
-  parts:
-    - color: palegreen3
-      type: rect
-      height: 0.8
-      width: 0.8
+      size: 0.88
+      center_offset: [ 0, -0.05 ]
 
-OnionDispenser:
-  parts:
-    - color: deeppink3
-      type: rect
-      height: 0.8
-      width: 0.8
+#TomatoDispenser:
+#  parts:
+#    - color: orangered1
+#      type: rect
+#      height: 0.8
+#      width: 0.8
+#
+#LettuceDispenser:
+#  parts:
+#    - color: palegreen3
+#      type: rect
+#      height: 0.8
+#      width: 0.8
+#
+#OnionDispenser:
+#  parts:
+#    - color: deeppink3
+#      type: rect
+#      height: 0.8
+#      width: 0.8
+#
+#MeatDispenser:
+#  parts:
+#    - color: indianred1
+#      type: rect
+#      height: 0.8
+#      width: 0.8
+#
+#BunDispenser:
+#  parts:
+#    - color: sandybrown
+#      type: rect
+#      height: 0.8
+#      width: 0.8
 
-MeatDispenser:
+Dispenser:
   parts:
-    - color: indianred1
-      type: rect
-      height: 0.8
-      width: 0.8
+    - type: circle
+      color: black
+      radius: 0.35
+      center_offset: [ 0, -0.05 ]
+    - type: circle
+      color: gray83
+      radius: 0.33
+      center_offset: [ 0, -0.05 ]
 
-BunDispenser:
-  parts:
-    - color: sandybrown
-      type: rect
-      height: 0.8
-      width: 0.8
 
-Dispenser:
-  parts:
-    - color: gray83
-      type: rect
-      height: 0.8
-      width: 0.8
+  item_offset: [ 0, -0.05 ]
+  item_scale: 0.9
 
 ServingWindow:
   parts:
-    - type: image
-      path: images/arrow_right.png
-      size: 1
-      center_offset: [ 0, 0 ]
+    #    - type: image
+    #      path: images/arrow_right.png
+    #      size: 1
+    #      center_offset: [ 0, 0 ]
     - type: image
       path: images/bell_gold.png
       size: 0.5
-      center_offset: [ 0.1, -0.4 ]
+      center_offset: [ -0.4, 0.1 ]
+      rotate_image: False
 
 Stove:
   parts:
@@ -115,15 +126,49 @@ Sink:
   parts:
     - type: image
       path: images/sink1.png
-      size: 1
-      center_offset: [ 0, -0.05 ]
+      size: 0.85
+      center_offset: [ 0, -0.12 ]
 
 SinkAddon:
   parts:
     - type: image
       path: images/drip2.png
+      size: 0.75
+      center_offset: [ 0, -0.05 ]
+
+# Tools
+Extinguisher:
+  parts:
+    - type: image
+      path: images/fire_extinguisher.png
       size: 0.85
-      center_offset: [ 0, 0.03 ]
+      center_offset: [ 0, -0.05 ]
+
+# Effects
+Fire:
+  parts:
+    - type: image
+      path: images/fire.png
+      size: 1
+
+Fire1:
+  parts:
+    - type: image
+      path: images/fire.png
+      size: 1.0
+
+Fire2:
+  parts:
+    - type: image
+      path: images/fire2.png
+      size: 1.0
+
+Fire3:
+  parts:
+    - type: image
+      path: images/fire3.png
+      size: 1.0
+
 
 # Items
 Tomato:
@@ -275,6 +320,7 @@ Oven:
       color: black
       height: 0.8
       width: 0.3
+      center_offset: [ 0, -0.1 ]
 
 Basket:
   parts:
diff --git a/overcooked_simulator/hooks.py b/overcooked_simulator/hooks.py
new file mode 100644
index 0000000000000000000000000000000000000000..67c5462948a6bfc5271010f58574918f9920e97a
--- /dev/null
+++ b/overcooked_simulator/hooks.py
@@ -0,0 +1,153 @@
+from collections import defaultdict
+from functools import partial
+from typing import Callable
+
+# TODO add player_id as kwarg to all hooks -> pass player id to all methods
+
+ITEM_INFO_LOADED = "item_info_load"
+"""Called after the item info is loaded and stored in the env attribute `item_info`. The kwargs are the passed config 
+(`item_info`) to the environment from which it was loaded and if it is a file path or the config string (`as_files`)"""
+
+LAYOUT_FILE_PARSED = "layout_file_parsed"
+"""After the layout file was parsed. No additional kwargs. Everything is stored in the env."""
+
+ITEM_INFO_CONFIG = "item_info_config"
+
+ENV_INITIALIZED = "env_initialized"
+"""At the end of the __init__ method. No additional kwargs. Everything is stored in the env."""
+
+PRE_PERFORM_ACTION = "pre_perform_action"
+"""Before an action is performed / entered into the environment. `action` kwarg with the entered action."""
+
+POST_PERFORM_ACTION = "post_perform_action"
+"""After an action is performed / entered into the environment. `action` kwarg with the entered action."""
+
+# TODO Pre and Post Perform Movement
+
+PLAYER_ADDED = "player_added"
+"""Called after a player has been added. Kwargs: `player_name` and `pos`."""
+
+GAME_ENDED_STEP = "game_ended_step"
+
+PRE_STATE = "pre_state"
+
+STATE_DICT = "state_dict"
+
+JSON_STATE = "json_state"
+
+PRE_RESET_ENV_TIME = "pre_reset_env_time"
+
+POST_RESET_ENV_TIME = "post_reset_env_time"
+
+PRE_COUNTER_PICK_UP = "pre_counter_pick_up"
+POST_COUNTER_PICK_UP = "post_counter_pick_up"
+
+PRE_COUNTER_DROP_OFF = "pre_counter_drop_off"
+POST_COUNTER_DROP_OFF = "post_counter_drop_off"
+
+PRE_DISPENSER_PICK_UP = "dispenser_pick_up"
+POST_DISPENSER_PICK_UP = "dispenser_pick_up"
+
+CUTTING_BOARD_PROGRESS = "cutting_board_progress"
+CUTTING_BOARD_100 = "cutting_board_100"
+
+CUTTING_BOARD_START_INTERACT = "cutting_board_start_interaction"
+CUTTING_BOARD_END_INTERACT = "cutting_board_end_interact"
+
+PRE_SERVING = "pre_serving"
+POST_SERVING = "post_serving"
+NO_SERVING = "no_serving"
+
+# TODO drop off
+
+PLATE_OUT_OF_KITCHEN_TIME = "plate_out_of_kitchen_time"
+
+DIRTY_PLATE_ARRIVES = "dirty_plate_arrives"
+
+TRASHCAN_USAGE = "trashcan_usage"
+
+PLATE_CLEANED = "plate_cleaned"
+
+SINK_START_INTERACT = "sink_start_interact"
+
+SINK_END_INTERACT = "sink_end_interact"
+
+ADDED_PLATE_TO_SINK = "added_plate_to_sink"
+
+DROP_ON_SINK_ADDON = "drop_on_sink_addon"
+
+PICK_UP_FROM_SINK_ADDON = "pick_up_from_sink_addon"
+
+SERVE_NOT_ORDERED_MEAL = "serve_not_ordered_meal"
+
+SERVE_WITHOUT_PLATE = "serve_without_plate"
+
+ORDER_DURATION_SAMPLE = "order_duration_sample"
+
+COMPLETED_ORDER = "completed_order"
+
+INIT_ORDERS = "init_orders"
+
+NEW_ORDERS = "new_orders"
+
+ACTION_ON_NOT_REACHABLE_COUNTER = "action_on_not_reachable_counter"
+
+ACTION_PUT = "action_put"
+
+ACTION_INTERACT_START = "action_interact_start"
+
+NEW_FIRE = "new_fire"
+
+FIRE_SPREADING = "fire_spreading"
+
+
+class Hooks:
+    def __init__(self, env):
+        self.hooks = defaultdict(list)
+        self.env = env
+
+    def __call__(self, hook_ref, **kwargs):
+        for callback in self.hooks[hook_ref]:
+            callback(hook_ref=hook_ref, env=self.env, **kwargs)
+
+    def register_callback(self, hook_ref: str | list[str], callback: Callable):
+        if isinstance(hook_ref, (tuple, list, set)):  # TODO check for iterable
+            for ref in hook_ref:
+                self.hooks[ref].append(callback)
+        else:
+            self.hooks[hook_ref].append(callback)
+
+
+def print_hook_callback(text, env, **kwargs):
+    print(env.env_time, text)
+
+
+def add_dummy_callbacks(env):
+    env.register_callback_for_hook(
+        SERVE_NOT_ORDERED_MEAL,
+        partial(
+            print_hook_callback,
+            text="You tried to served a meal that was not ordered!",
+        ),
+    )
+    env.register_callback_for_hook(
+        SINK_START_INTERACT,
+        partial(
+            print_hook_callback,
+            text="You started to use the Sink!",
+        ),
+    )
+    env.register_callback_for_hook(
+        COMPLETED_ORDER,
+        partial(
+            print_hook_callback,
+            text="You completed an order!",
+        ),
+    )
+    env.register_callback_for_hook(
+        TRASHCAN_USAGE,
+        partial(
+            print_hook_callback,
+            text="You used the trashcan!",
+        ),
+    )
diff --git a/overcooked_simulator/info_msg.py b/overcooked_simulator/info_msg.py
new file mode 100644
index 0000000000000000000000000000000000000000..f65385b8aeb765c6b02102d4052dc43f2ca71ecb
--- /dev/null
+++ b/overcooked_simulator/info_msg.py
@@ -0,0 +1,39 @@
+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,
+                }
+            )
+        self.remove_old_msgs(env)
+
+    @staticmethod
+    def remove_old_msgs(env: Environment):
+        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:
+                    delete_msgs.append(idx)
+            for idx in reversed(delete_msgs):
+                msgs.pop(idx)
diff --git a/overcooked_simulator/joystick_example.py b/overcooked_simulator/joystick_example.py
new file mode 100644
index 0000000000000000000000000000000000000000..9d941ff3f946ffa59a3b9f2104b492d26e7af7a6
--- /dev/null
+++ b/overcooked_simulator/joystick_example.py
@@ -0,0 +1,180 @@
+import pygame
+
+pygame.init()
+
+#####
+# Button A: 0
+# Button B: 1
+# Button X: 2
+# Button Y: 3
+#
+# Button left up: 4
+# Button right up: 5
+# Button two window: 6
+# Button 3 stripes: 7
+# Button x-box: 8
+# joy stick left: 9
+# joy stick right: 10
+#
+# Cross -> Hat
+# Cross up: (0,1)
+# Cross right: (1,0)
+# Cross down: (0,-1)
+# Cross left: (-1,0)
+#
+# Axis 0: joy stick left: -1 = left, ~0 = center, 1 = right
+# Axis 1: joy stick left: -1 = up, ~0 = center, 1 = down
+# Axis 2: buttom left up at the back: -1 = off, 1 = on
+# Axis 3: joy stick right: -1 = left, ~0 = center, 1 = right
+# Axis 4: joy stick right: -1 = up, ~0 = center, 1 = down
+# Axis 5: buttom right up at the back: -1 = off, 1 = on
+#####
+
+
+# This is a simple class that will help us print to the screen.
+# It has nothing to do with the joysticks, just outputting the
+# information.
+class TextPrint:
+    def __init__(self):
+        self.reset()
+        self.font = pygame.font.Font(None, 25)
+
+    def tprint(self, screen, text):
+        text_bitmap = self.font.render(text, True, (0, 0, 0))
+        screen.blit(text_bitmap, (self.x, self.y))
+        self.y += self.line_height
+
+    def reset(self):
+        self.x = 10
+        self.y = 10
+        self.line_height = 15
+
+    def indent(self):
+        self.x += 10
+
+    def unindent(self):
+        self.x -= 10
+
+
+def main():
+    # Set the width and height of the screen (width, height), and name the window.
+    screen = pygame.display.set_mode((500, 700))
+    pygame.display.set_caption("Joystick example")
+
+    # Used to manage how fast the screen updates.
+    clock = pygame.time.Clock()
+
+    # Get ready to print.
+    text_print = TextPrint()
+
+    # This dict can be left as-is, since pygame will generate a
+    # pygame.JOYDEVICEADDED event for every joystick connected
+    # at the start of the program.
+    joysticks = {}
+
+    done = False
+    while not done:
+        # Event processing step.
+        # Possible joystick events: JOYAXISMOTION, JOYBALLMOTION, JOYBUTTONDOWN,
+        # JOYBUTTONUP, JOYHATMOTION, JOYDEVICEADDED, JOYDEVICEREMOVED
+        for event in pygame.event.get():
+            print("event", event)
+            if event.type == pygame.QUIT:
+                done = True  # Flag that we are done so we exit this loop.
+
+            if event.type == pygame.JOYBUTTONDOWN:
+                print("Joystick button pressed.")
+                if event.button == 0:
+                    joystick = joysticks[event.instance_id]
+                    if joystick.rumble(0, 0.7, 500):
+                        print(f"Rumble effect played on joystick {event.instance_id}")
+
+            if event.type == pygame.JOYBUTTONUP:
+                print("Joystick button released.")
+
+            # Handle hotplugging
+            if event.type == pygame.JOYDEVICEADDED:
+                # This event will be generated when the program starts for every
+                # joystick, filling up the list without needing to create them manually.
+                joy = pygame.joystick.Joystick(event.device_index)
+                joysticks[joy.get_instance_id()] = joy
+                print(f"Joystick {joy.get_instance_id()} connencted")
+
+            if event.type == pygame.JOYDEVICEREMOVED:
+                del joysticks[event.instance_id]
+                print(f"Joystick {event.instance_id} disconnected")
+
+        # Drawing step
+        # First, clear the screen to white. Don't put other drawing commands
+        # above this, or they will be erased with this command.
+        screen.fill((255, 255, 255))
+        text_print.reset()
+
+        # Get count of joysticks.
+        joystick_count = pygame.joystick.get_count()
+
+        text_print.tprint(screen, f"Number of joysticks: {joystick_count}")
+        text_print.indent()
+
+        # For each joystick:
+        for joystick in joysticks.values():
+            jid = joystick.get_instance_id()
+
+            text_print.tprint(screen, f"Joystick {jid}")
+            text_print.indent()
+
+            # Get the name from the OS for the controller/joystick.
+            name = joystick.get_name()
+            text_print.tprint(screen, f"Joystick name: {name}")
+
+            guid = joystick.get_guid()
+            text_print.tprint(screen, f"GUID: {guid}")
+
+            power_level = joystick.get_power_level()
+            text_print.tprint(screen, f"Joystick's power level: {power_level}")
+
+            # Usually axis run in pairs, up/down for one, and left/right for
+            # the other. Triggers count as axes.
+            axes = joystick.get_numaxes()
+            text_print.tprint(screen, f"Number of axes: {axes}")
+            text_print.indent()
+
+            for i in range(axes):
+                axis = joystick.get_axis(i)
+                text_print.tprint(screen, f"Axis {i} value: {axis:>6.3f}")
+            text_print.unindent()
+
+            buttons = joystick.get_numbuttons()
+            text_print.tprint(screen, f"Number of buttons: {buttons}")
+            text_print.indent()
+
+            for i in range(buttons):
+                button = joystick.get_button(i)
+                text_print.tprint(screen, f"Button {i:>2} value: {button}")
+            text_print.unindent()
+
+            hats = joystick.get_numhats()
+            text_print.tprint(screen, f"Number of hats: {hats}")
+            text_print.indent()
+
+            # Hat position. All or nothing for direction, not a float like
+            # get_axis(). Position is a tuple of int values (x, y).
+            for i in range(hats):
+                hat = joystick.get_hat(i)
+                text_print.tprint(screen, f"Hat {i} value: {str(hat)}")
+            text_print.unindent()
+
+            text_print.unindent()
+
+        # Go ahead and update the screen with what we've drawn.
+        pygame.display.flip()
+
+        # Limit to 30 frames per second.
+        clock.tick(30)
+
+
+if __name__ == "__main__":
+    main()
+    # If you forget this line, the program will 'hang'
+    # on exit if running from IDLE.
+    pygame.quit()
\ No newline at end of file
diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py
index b61c9218f7adce6b059933eaf27d66c9bb9e8ee1..6cedaaab7b1165a60c48c39a3ae54c408839d305 100644
--- a/overcooked_simulator/order.py
+++ b/overcooked_simulator/order.py
@@ -46,14 +46,23 @@ from __future__ import annotations
 
 import dataclasses
 import logging
-import random
 import uuid
 from abc import abstractmethod
 from collections import deque
 from datetime import datetime, timedelta
-from typing import Callable, Tuple, Any, Deque, Protocol, TypedDict
+from random import Random
+from typing import Callable, Tuple, Any, Deque, Protocol, TypedDict, Type
 
 from overcooked_simulator.game_items import Item, Plate, ItemInfo
+from overcooked_simulator.hooks import (
+    Hooks,
+    SERVE_NOT_ORDERED_MEAL,
+    SERVE_WITHOUT_PLATE,
+    COMPLETED_ORDER,
+    INIT_ORDERS,
+    NEW_ORDERS,
+    ORDER_DURATION_SAMPLE,
+)
 
 log = logging.getLogger(__name__)
 """The logger for this module."""
@@ -62,6 +71,17 @@ ORDER_CATEGORY = "Order"
 """The string for the `category` value in the json state representation for all orders."""
 
 
+class OrderConfig(TypedDict):
+    """The configuration of the order in the `environment_config`under the `order` key."""
+
+    order_gen_class: Type[OrderGeneration]
+    """The class that should handle the order generation."""
+    order_gen_kwargs: dict[str, Any]
+    """The additional kwargs for the order gen class."""
+    serving_not_ordered_meals: Callable[[Item], Tuple[bool, float]]
+    """"""
+
+
 @dataclasses.dataclass
 class Order:
     """Datawrapper for Orders"""
@@ -118,9 +138,13 @@ class OrderGeneration:
         ```
     """
 
-    def __init__(self, available_meals: dict[str, ItemInfo], **kwargs):
+    def __init__(self, available_meals: dict[str, ItemInfo], hook: Hooks, random: Random, **kwargs):
         self.available_meals: list[ItemInfo] = list(available_meals.values())
         """Available meals restricted through the `environment_config.yml`."""
+        self.hook = hook
+        """Reference to the hook manager."""
+        self.random = random
+        """Random instance."""
 
     @abstractmethod
     def init_orders(self, now) -> list[Order]:
@@ -142,11 +166,22 @@ class OrderGeneration:
 class OrderAndScoreManager:
     """The Order and Score Manager that is called from the serving window."""
 
-    def __init__(self, order_config, available_meals: dict[str, ItemInfo]):
+    def __init__(
+        self,
+        order_config,
+        available_meals: dict[str, ItemInfo],
+        hook: Hooks,
+        random: Random,
+    ):
+        self.random = random
+        """Random instance."""
         self.score: float = 0.0
         """The current score of the environment."""
         self.order_gen: OrderGeneration = order_config["order_gen_class"](
-            available_meals=available_meals, kwargs=order_config["order_gen_kwargs"]
+            available_meals=available_meals,
+            hook=hook,
+            random=random,
+            kwargs=order_config["order_gen_kwargs"],
         )
         """The order generation."""
         self.serving_not_ordered_meals: Callable[
@@ -176,6 +211,9 @@ class OrderAndScoreManager:
         self.last_expired: list[Order] = []
         """Cache last expired orders for `OrderGeneration.get_orders` call."""
 
+        self.hook = hook
+        """Reference to the hook manager."""
+
     def update_next_relevant_time(self):
         """For more efficient checking when to do something in the progress call."""
         next_relevant_time = datetime.max
@@ -198,6 +236,12 @@ class OrderAndScoreManager:
                     if order is None:
                         if self.serving_not_ordered_meals:
                             accept, score = self.serving_not_ordered_meals(meal)
+                            self.hook(
+                                SERVE_NOT_ORDERED_MEAL,
+                                accept=accept,
+                                score=score,
+                                meal=meal,
+                            )
                             if accept:
                                 log.info(
                                     f"Serving meal without order {meal.name!r} with score {score}"
@@ -225,7 +269,10 @@ class OrderAndScoreManager:
                     self.last_finished.append(order)
                     del self.open_orders[index]
                     self.served_meals.append((meal, env_time))
+                    self.hook(COMPLETED_ORDER, score=score, order=order, meal=meal)
                     return True
+        else:
+            self.hook(SERVE_WITHOUT_PLATE, item=item)
         log.info(f"Do not serve item {item}")
         return False
 
@@ -237,6 +284,7 @@ class OrderAndScoreManager:
     def create_init_orders(self, env_time):
         """Create the initial orders in an environment."""
         init_orders = self.order_gen.init_orders(env_time)
+        self.hook(INIT_ORDERS)
         self.setup_penalties(new_orders=init_orders, env_time=env_time)
         self.open_orders.extend(init_orders)
 
@@ -248,6 +296,8 @@ class OrderAndScoreManager:
             new_finished_orders=self.last_finished,
             expired_orders=self.last_expired,
         )
+        if new_orders:
+            self.hook(NEW_ORDERS, new_orders=new_orders)
         self.setup_penalties(new_orders=new_orders, env_time=now)
         self.open_orders.extend(new_orders)
         self.last_finished = []
@@ -266,6 +316,7 @@ class OrderAndScoreManager:
                 for i, (penalty_time, penalty) in enumerate(order.timed_penalties):
                     # check penalties
                     if penalty_time < now:
+                        # TODO add hook
                         self.score -= penalty
                         remove_penalties.append(i)
 
@@ -307,9 +358,11 @@ class OrderAndScoreManager:
             for order in self.open_orders
         ]
 
-    def apply_penalty_for_using_trash(self, remove: Item | list[Item]):
+    def apply_penalty_for_using_trash(self, remove: Item | list[Item]) -> float:
         """Is called if a item is put into the trashcan."""
-        self.increment_score(self.penalty_for_trash(remove))
+        penalty = self.penalty_for_trash(remove)
+        self.increment_score(penalty)
+        return penalty
 
 
 class ScoreCalcFuncType(Protocol):
@@ -474,8 +527,8 @@ class RandomOrderGeneration(OrderGeneration):
     ```
     """
 
-    def __init__(self, available_meals: dict[str, ItemInfo], **kwargs):
-        super().__init__(available_meals, **kwargs)
+    def __init__(self, available_meals: dict[str, ItemInfo], hook: Hooks, random: Random, **kwargs):
+        super().__init__(available_meals, hook, random, **kwargs)
         self.kwargs: RandomOrderKwarg = RandomOrderKwarg(**kwargs["kwargs"])
         self.next_order_time: datetime | None = datetime.max
         self.number_cur_orders: int = 0
@@ -487,7 +540,7 @@ class RandomOrderGeneration(OrderGeneration):
         if not self.kwargs.sample_on_serving:
             self.create_random_next_time_delta(now)
         return self.create_orders_for_meals(
-            random.choices(self.available_meals, k=self.kwargs.num_start_meals),
+            self.random.choices(self.available_meals, k=self.kwargs.num_start_meals),
             now,
             self.kwargs.sample_on_serving,
         )
@@ -510,7 +563,7 @@ class RandomOrderGeneration(OrderGeneration):
             self.needed_orders = max(self.needed_orders, 0)
             self.number_cur_orders += len(new_finished_orders)
             return self.create_orders_for_meals(
-                random.choices(self.available_meals, k=len(new_finished_orders)),
+                self.random.choices(self.available_meals, k=len(new_finished_orders)),
                 now,
             )
         if self.next_order_time <= now:
@@ -523,7 +576,7 @@ class RandomOrderGeneration(OrderGeneration):
                     self.next_order_time = datetime.max
                 self.number_cur_orders += 1
                 return self.create_orders_for_meals(
-                    [random.choice(self.available_meals)],
+                    [self.random.choice(self.available_meals)],
                     now,
                 )
         return []
@@ -538,9 +591,13 @@ class RandomOrderGeneration(OrderGeneration):
             else:
                 duration = timedelta(
                     seconds=getattr(
-                        random, self.kwargs.order_duration_random_func["func"]
+                        self.random, self.kwargs.order_duration_random_func["func"]
                     )(**self.kwargs.order_duration_random_func["kwargs"])
                 )
+            self.hook(
+                ORDER_DURATION_SAMPLE,
+                duration=duration,
+            )
             log.info(f"Create order for meal {meal} with duration {duration}")
             orders.append(
                 Order(
@@ -564,7 +621,7 @@ class RandomOrderGeneration(OrderGeneration):
 
     def create_random_next_time_delta(self, now: datetime):
         self.next_order_time = now + timedelta(
-            seconds=getattr(random, self.kwargs.sample_on_dur_random_func["func"])(
+            seconds=getattr(self.random, self.kwargs.sample_on_dur_random_func["func"])(
                 **self.kwargs.sample_on_dur_random_func["kwargs"]
             )
         )
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 84d6312821e9cb74b1e488d850e2e13f73429c99..d157ce604b478f28afe1c827f4a7e71b30aa3a49 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -4,33 +4,63 @@ import dataclasses
 import inspect
 import json
 import logging
-import random
 import sys
+from collections import defaultdict
 from datetime import timedelta, datetime
 from enum import Enum
 from pathlib import Path
-from typing import Literal
+from random import Random
+from typing import Literal, TypedDict, Callable, Tuple
 
 import numpy as np
 import numpy.typing as npt
 import yaml
+from scipy.spatial import distance_matrix
 
 from overcooked_simulator.counter_factory import CounterFactory
 from overcooked_simulator.counters import (
     Counter,
     PlateConfig,
 )
+from overcooked_simulator.effect_manager import EffectManager
 from overcooked_simulator.game_items import (
     ItemInfo,
     ItemType,
 )
-from overcooked_simulator.order import OrderAndScoreManager
+from overcooked_simulator.hooks import (
+    ITEM_INFO_LOADED,
+    LAYOUT_FILE_PARSED,
+    ENV_INITIALIZED,
+    PRE_PERFORM_ACTION,
+    POST_PERFORM_ACTION,
+    PLAYER_ADDED,
+    GAME_ENDED_STEP,
+    PRE_STATE,
+    STATE_DICT,
+    JSON_STATE,
+    PRE_RESET_ENV_TIME,
+    POST_RESET_ENV_TIME,
+    Hooks,
+    ACTION_ON_NOT_REACHABLE_COUNTER,
+    ACTION_PUT,
+    ACTION_INTERACT_START,
+    ITEM_INFO_CONFIG,
+)
+from overcooked_simulator.order import (
+    OrderAndScoreManager,
+    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__)
 
+FOG_OF_WAR = True
+
+
+PREVENT_SQUEEZING_INTO_OTHER_PLAYERS = False
+
 
 class ActionType(Enum):
     """The 3 different types of valid actions. They can be extended via the `Action.action_data` attribute."""
@@ -79,6 +109,17 @@ class Action:
 # TODO Abstract base class for different environments
 
 
+class EnvironmentConfig(TypedDict):
+    plates: PlateConfig
+    game: dict[Literal["time_limit_seconds"], int]
+    meals: dict[Literal["all"] | Literal["list"], bool | list[str]]
+    orders: OrderConfig
+    player_config: PlayerConfig
+    layout_chars: dict[str, str]
+    extra_setup_functions: dict[str, dict]
+    effect_manager: dict
+
+
 class Environment:
     """Environment class which handles the game logic for the overcooked-inspired environment.
 
@@ -93,7 +134,20 @@ class Environment:
         layout_config: Path | str,
         item_info: Path | str,
         as_files: bool = True,
+        env_name: str = "overcooked_sim",
+        seed: int = 56789223842348,
     ):
+        self.env_name = env_name
+        """Reference to the run. E.g, the env id."""
+        self.env_time: datetime = create_init_env_time()
+        """the internal time of the environment. An environment starts always with the time from 
+        `create_init_env_time`."""
+
+        self.random: Random = Random(seed)
+        """Random instance."""
+        self.hook: Hooks = Hooks(self)
+        """Hook manager. Register callbacks and create hook points with additional kwargs."""
+
         self.players: dict[str, Player] = {}
         """the player, keyed by their id/name."""
 
@@ -101,15 +155,33 @@ class Environment:
         """Are the configs just the path to the files."""
         if self.as_files:
             with open(env_config, "r") as file:
-                self.environment_config = yaml.load(file, Loader=yaml.Loader)
-        else:
-            self.environment_config = yaml.load(env_config, Loader=yaml.Loader)
+                env_config = file.read()
+        self.environment_config: EnvironmentConfig = yaml.load(
+            env_config, Loader=yaml.Loader
+        )
+        """The config of the environment. All environment specific attributes is configured here."""
+
+        self.player_view_restricted = self.environment_config["player_config"][
+            "restricted_view"
+        ]
+        if self.player_view_restricted:
+            self.player_view_angle = self.environment_config["player_config"][
+                "view_angle"
+            ]
+            self.player_view_range = self.environment_config["player_config"][
+                "view_range"
+            ]
+
+        self.extra_setup_functions()
+
         self.layout_config = layout_config
         """The layout config for the environment"""
         # self.counter_side_length = 1  # -> this changed! is 1 now
 
         self.item_info: dict[str, ItemInfo] = self.load_item_info(item_info)
         """The loaded item info dict. Keys are the item names."""
+        self.hook(ITEM_INFO_LOADED, item_info=item_info, as_files=as_files)
+
         # self.validate_item_info()
         if self.environment_config["meals"]["all"]:
             self.allowed_meal_names = set(
@@ -131,6 +203,8 @@ class Environment:
                 for item, info in self.item_info.items()
                 if info.type == ItemType.Meal and item in self.allowed_meal_names
             },
+            hook=self.hook,
+            random=self.random,
         )
         """The manager for the orders and score update."""
 
@@ -154,6 +228,9 @@ class Environment:
                 )
             ),
             order_and_score=self.order_and_score,
+            effect_manager_config=self.environment_config["effect_manager"],
+            hook=self.hook,
+            random=self.random,
         )
 
         (
@@ -161,9 +238,20 @@ class Environment:
             self.designated_player_positions,
             self.free_positions,
         ) = self.parse_layout_file()
+        self.hook(LAYOUT_FILE_PARSED)
 
-        self.world_borders_x = [-0.5, self.kitchen_width - 0.5]
-        self.world_borders_y = [-0.5, self.kitchen_height - 0.5]
+        self.world_borders = np.array(
+            [[-0.5, self.kitchen_width - 0.5], [-0.5, self.kitchen_height - 0.5]],
+            dtype=float,
+        )
+
+        self.player_movement_speed = self.environment_config["player_config"][
+            "player_speed_units_per_seconds"
+        ]
+        self.player_radius = self.environment_config["player_config"]["radius"]
+        self.player_interaction_range = self.environment_config["player_config"][
+            "interaction_range"
+        ]
 
         progress_counter_classes = list(
             filter(
@@ -183,9 +271,8 @@ class Environment:
         )
         """Counters that needs to be called in the step function via the `progress` method."""
 
-        self.env_time: datetime = create_init_env_time()
-        """the internal time of the environment. An environment starts always with the time from 
-        `create_init_env_time`."""
+        self.counter_positions = np.array([c.pos for c in self.counters])
+
         self.order_and_score.create_init_orders(self.env_time)
         self.start_time = self.env_time
         """The relative env time when it started."""
@@ -195,11 +282,55 @@ class Environment:
         """The relative env time when it will stop/end"""
         log.debug(f"End time: {self.env_time_end}")
 
+        self.effect_manager: dict[
+            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,
+            layout_config=self.layout_config,
+            seed=seed,
+            env_start_time_worldtime=datetime.now(),
+        )
+
+    def overwrite_counters(self, counters):
+        self.counters = counters
+        self.counter_positions = np.array([c.pos for c in self.counters])
+
+        progress_counter_classes = list(
+            filter(
+                lambda cl: hasattr(cl, "progress"),
+                dict(
+                    inspect.getmembers(
+                        sys.modules["overcooked_simulator.counters"], inspect.isclass
+                    )
+                ).values(),
+            )
+        )
+        self.progressing_counters = list(
+            filter(
+                lambda c: c.__class__ in progress_counter_classes,
+                self.counters,
+            )
+        )
+
     @property
     def game_ended(self) -> bool:
         """Whether the game is over or not based on the calculated `Environment.env_time_end`"""
         return self.env_time >= self.env_time_end
 
+    def set_collision_arrays(self):
+        number_players = len(self.players)
+        self.world_borders_lower = self.world_borders[np.newaxis, :, 0].repeat(
+            number_players, axis=0
+        )
+        self.world_borders_upper = self.world_borders[np.newaxis, :, 1].repeat(
+            number_players, axis=0
+        )
+
     def get_env_time(self):
         """the internal time of the environment. An environment starts always with the time from `create_init_env_time`.
 
@@ -210,9 +341,9 @@ class Environment:
         """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos."""
         if self.as_files:
             with open(data, "r") as file:
-                item_lookup = yaml.safe_load(file)
-        else:
-            item_lookup = yaml.safe_load(data)
+                data = file.read()
+        self.hook(ITEM_INFO_CONFIG, item_info_config=data)
+        item_lookup = yaml.safe_load(data)
         for item_name in item_lookup:
             item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name])
 
@@ -278,7 +409,9 @@ class Environment:
         # TODO add colors for ingredients, equipment and meals
         # plt.show()
 
-    def parse_layout_file(self):
+    def parse_layout_file(
+        self,
+    ) -> Tuple[list[Counter], list[npt.NDArray], list[npt.NDArray]]:
         """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
         [counter_size/2, counter_size/2], counters are directly next to each other (of no empty space is specified
@@ -293,16 +426,22 @@ class Environment:
 
         if self.as_files:
             with open(self.layout_config, "r") as layout_file:
-                lines = layout_file.readlines()
-        else:
-            lines = self.layout_config.split("\n")
+                self.layout_config = layout_file.read()
+        lines = self.layout_config.split("\n")
 
+        grid = []
+
+        lines = list(filter(lambda l: l != "", lines))
         for line in lines:
-            line = line.replace("\n", "").replace(" ", "")  # remove newline char
+            line = line.replace(" ", "")
+            if not line or line.startswith(";"):
+                break
             current_x: float = starting_at
+            grid_line = []
             for character in line:
-                character = character.capitalize()
+                # character = character.capitalize()
                 pos = np.array([current_x, current_y])
+
                 assert self.counter_factory.can_map(
                     character
                 ), f"{character=} in layout file can not be mapped"
@@ -310,7 +449,9 @@ class Environment:
                     counters.append(
                         self.counter_factory.get_counter_object(character, pos)
                     )
+                    grid_line.append(1)
                 else:
+                    grid_line.append(0)
                     match self.counter_factory.map_not_counter(character):
                         case "Agent":
                             designated_player_positions.append(pos)
@@ -318,14 +459,76 @@ class Environment:
                             free_positions.append(np.array([current_x, current_y]))
 
                 current_x += 1
+
+            grid.append(grid_line)
             current_y += 1
 
         self.kitchen_width: float = len(lines[0]) + starting_at
-        self.kitchen_height = len(lines) + starting_at
+        self.kitchen_height = current_y
+
+        self.determine_counter_orientations(
+            counters, grid, np.array([self.kitchen_width / 2, self.kitchen_height / 2])
+        )
+
         self.counter_factory.post_counter_setup(counters)
 
         return counters, designated_player_positions, free_positions
 
+    def determine_counter_orientations(self, counters, grid, kitchen_center):
+        grid = np.array(grid).T
+
+        grid_width = grid.shape[0]
+        grid_height = grid.shape[1]
+
+        last_counter = None
+        fst_counter_in_row = None
+        for c in counters:
+            grid_idx = np.floor(c.pos).astype(int)
+            neighbour_offsets = np.array([[0, 1], [0, -1], [1, 0], [-1, 0]], dtype=int)
+
+            neighbours_free = []
+            for offset in neighbour_offsets:
+                neighbour_pos = grid_idx + offset
+                if (
+                    neighbour_pos[0] > (grid_width - 1)
+                    or neighbour_pos[0] < 0
+                    or neighbour_pos[1] > (grid_height - 1)
+                    or neighbour_pos[1] < 0
+                ):
+                    pass
+                else:
+                    if grid[neighbour_pos[0]][neighbour_pos[1]] == 0:
+                        neighbours_free.append(offset)
+            if len(neighbours_free) > 0:
+                vector_to_center = c.pos - kitchen_center
+                vector_to_center /= np.linalg.norm(vector_to_center)
+                n_idx = np.argmin(
+                    np.linalg.norm(vector_to_center - n) for n in neighbours_free
+                )
+                nearest_vec = neighbours_free[n_idx]
+                # print(nearest_vec, type(nearest_vec))
+                c.set_orientation(nearest_vec)
+
+            elif grid_idx[0] == 0:
+                if grid_idx[1] == 0 or fst_counter_in_row is None:
+                    # counter top left
+                    c.set_orientation(np.array([1, 0]))
+                else:
+                    c.set_orientation(fst_counter_in_row.orientation)
+                fst_counter_in_row = c
+            else:
+                c.set_orientation(last_counter.orientation)
+
+            last_counter = c
+
+        # for c in counters:
+        #     near_counters = [
+        #         other
+        #         for other in counters
+        #         if np.isclose(np.linalg.norm(c.pos - other.pos), 1)
+        #     ]
+        #     # print(c.pos, len(near_counters))
+
     def perform_action(self, action: Action):
         """Performs an action of a player in the environment. Maps different types of action inputs to the
         correct execution of the players.
@@ -335,6 +538,7 @@ class Environment:
             action: The action to be performed
         """
         assert action.player in self.players.keys(), "Unknown player."
+        self.hook(PRE_PERFORM_ACTION, action=action)
         player = self.players[action.player]
 
         if action.action_type == ActionType.MOVEMENT:
@@ -346,15 +550,20 @@ class Environment:
             counter = self.get_facing_counter(player)
             if player.can_reach(counter):
                 if action.action_type == ActionType.PUT:
-                    player.pick_action(counter)
-
+                    player.put_action(counter)
+                    self.hook(ACTION_PUT, action=action, counter=counter)
                 elif action.action_type == ActionType.INTERACT:
                     if action.action_data == InterActionData.START:
-                        player.perform_interact_hold_start(counter)
-                        player.last_interacted_counter = counter
+                        player.perform_interact_start(counter)
+                        self.hook(ACTION_INTERACT_START, action=action, counter=counter)
+            else:
+                self.hook(
+                    ACTION_ON_NOT_REACHABLE_COUNTER, action=action, counter=counter
+                )
             if action.action_data == InterActionData.STOP:
-                if player.last_interacted_counter:
-                    player.perform_interact_hold_stop(player.last_interacted_counter)
+                player.perform_interact_stop()
+
+        self.hook(POST_PERFORM_ACTION, action=action)
 
     def get_facing_counter(self, player: Player):
         """Determines the counter which the player is looking at.
@@ -370,7 +579,7 @@ class Environment:
         facing_counter = get_closest(player.facing_point, self.counters)
         return facing_counter
 
-    def perform_movement(self, player: Player, duration: timedelta):
+    def perform_movement(self, duration: timedelta):
         """Moves a player in the direction specified in the action.action. If the player collides with a
         counter or other player through this movement, then they are not moved.
         (The extended code with the two ifs is for sliding movement at the counters, which feels a bit smoother.
@@ -382,145 +591,124 @@ class Environment:
         Detects collisions with other players and pushes them out of the way.
 
         Args:
-            player: The player to move.
             duration: The duration for how long the movement to perform.
         """
-        old_pos = player.pos.copy()
-
-        move_vector = player.current_movement
-
         d_time = duration.total_seconds()
-        step = move_vector * (player.player_speed_units_per_seconds * d_time)
-
-        player.move(step)
-        if self.detect_collision(player):
-            collided_players = self.get_collided_players(player)
-            for collided_player in collided_players:
-                pushing_vector = collided_player.pos - player.pos
-                if np.linalg.norm(pushing_vector) != 0:
-                    pushing_vector = pushing_vector / np.linalg.norm(pushing_vector)
-
-                old_pos_other = collided_player.pos.copy()
-                collided_player.current_movement = pushing_vector
-                self.perform_movement(collided_player, duration)
-                if self.detect_collision_counters(
-                    collided_player
-                ) or self.detect_collision_world_bounds(collided_player):
-                    collided_player.move_abs(old_pos_other)
-            player.move_abs(old_pos)
-
-            old_pos = player.pos.copy()
-
-            step_sliding = step.copy()
-            step_sliding[0] = 0
-            player.move(step_sliding * 0.5)
-            player.turn(step)
-
-            if self.detect_collision(player):
-                player.move_abs(old_pos)
-
-                old_pos = player.pos.copy()
-
-                step_sliding = step.copy()
-                step_sliding[1] = 0
-                player.move(step_sliding * 0.5)
-                player.turn(step)
-
-                if self.detect_collision(player):
-                    player.move_abs(old_pos)
-
-        if self.counters:
-            closest_counter = self.get_facing_counter(player)
-            player.current_nearest_counter = (
-                closest_counter if player.can_reach(closest_counter) else None
-            )
-
-    def detect_collision(self, player: Player):
-        """Detect collisions between the player and other players or counters.
-
-        Args:
-            player: The player for which to check collisions.
 
-        Returns: True if the player is intersecting with any object in the environment.
-        """
-        return (
-            len(self.get_collided_players(player)) != 0
-            or self.detect_collision_counters(player)
-            or self.detect_collision_world_bounds(player)
+        player_positions = np.array([p.pos for p in self.players.values()], dtype=float)
+        player_movement_vectors = np.array(
+            [
+                p.current_movement if self.env_time <= p.movement_until else [0, 0]
+                for p in self.players.values()
+            ],
+            dtype=float,
         )
+        number_players = len(player_positions)
 
-    def get_collided_players(self, player: Player) -> list[Player]:
-        """Detects collisions between the queried player and other players. Returns the list of the collided players.
-        A player is modelled as a circle. Collision is detected if the distance between the players is smaller
-        than the sum of the radius's.
-
-        Args:
-            player: The player to check collisions with other players for.
-
-        Returns: The list of other players the player collides with.
-
-        """
-        other_players = filter(lambda p: p.name != player.name, self.players.values())
-
-        def collide(p):
-            return np.linalg.norm(player.pos - p.pos) <= player.radius + p.radius
-
-        return list(filter(collide, other_players))
-
-    def detect_player_collision(self, player: Player):
-        """Detects collisions between the queried player and other players.
-        A player is modelled as a circle. Collision is detected if the distance between the players is smaller
-        than the sum of the radius's.
-
-        Args:
-            player: The player to check collisions with other players for.
-
-        Returns: True if the player collides with other players, False if not.
+        targeted_positions = player_positions + (
+            player_movement_vectors * (self.player_movement_speed * d_time)
+        )
 
-        """
-        other_players = filter(lambda p: p.name != player.name, self.players.values())
+        # Collisions player between player
+        distances_players_after_scipy = distance_matrix(
+            targeted_positions, targeted_positions
+        )
 
-        def collide(p):
-            return np.linalg.norm(player.pos - p.pos) <= (player.radius + p.radius)
+        player_diff_vecs = -(
+            player_positions[:, np.newaxis, :] - player_positions[np.newaxis, :, :]
+        )
+        collision_idxs = distances_players_after_scipy < (2 * self.player_radius)
+        eye_idxs = np.eye(number_players, number_players, dtype=bool)
+        collision_idxs[eye_idxs] = False
 
-        return any(map(collide, other_players))
+        # Player push players around
+        player_diff_vecs[collision_idxs == False] = 0
+        push_vectors = np.sum(player_diff_vecs, axis=0)
 
-    def detect_collision_counters(self, player: Player):
-        """Checks for collisions of the queried player with each counter.
+        updated_movement = push_vectors + player_movement_vectors
+        new_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
 
-        Args:
-            player:  The player to check collisions with counters for.
+        # Collisions players counters
+        counter_diff_vecs = (
+            new_positions[:, np.newaxis, :] - self.counter_positions[np.newaxis, :, :]
+        )
+        counter_distances = np.max((np.abs(counter_diff_vecs)), axis=2)
+        # counter_distances = np.linalg.norm(counter_diff_vecs, axis=2)
+        closest_counter_positions = self.counter_positions[
+            np.argmin(counter_distances, axis=1)
+        ]
+
+        nearest_counter_to_player = closest_counter_positions - new_positions
+
+        collided = np.min(counter_distances, axis=1) < self.player_radius + 0.5
+        relevant_axes = np.abs(nearest_counter_to_player).argmax(axis=1)
+
+        for idx, player in enumerate(player_positions):
+            axis = relevant_axes[idx]
+
+            if collided[idx]:
+                # collide with counter left or top
+                if nearest_counter_to_player[idx][axis] < 0:
+                    updated_movement[idx, axis] = max(updated_movement[idx, axis], 0)
+                # collide with counter right or bottom
+                if nearest_counter_to_player[idx][axis] > 0:
+                    updated_movement[idx, axis] = min(updated_movement[idx, axis], 0)
+
+        new_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
 
-        Returns: True if the player collides with any counter, False if not.
+        # Check if pushed players collide with counters or second closest is to close
+        counter_diff_vecs = (
+            new_positions[:, np.newaxis, :] - self.counter_positions[np.newaxis, :, :]
+        )
+        counter_distances = np.max((np.abs(counter_diff_vecs)), axis=2)
+        collided2 = np.min(counter_distances, axis=1) < self.player_radius + 0.5
+        # player do not move if they collide after pushing/sliding
+        new_positions[collided2] = player_positions[collided2]
+        # Players that pushed the player that can not be pushed do also no movement
+        # in the future these players could slide around the player?
+        for idx, collides in enumerate(collided2):
+            if collides:
+                new_positions[collision_idxs[idx]] = player_positions[
+                    collision_idxs[idx]
+                ]
 
-        """
-        return any(
-            map(
-                lambda counter: self.detect_collision_player_counter(player, counter),
-                self.counters,
+        # Check if two moving players collide into each other: No movement (Future: slide?)
+        if PREVENT_SQUEEZING_INTO_OTHER_PLAYERS:
+            distances_players_after_scipy = distance_matrix(
+                new_positions, new_positions
             )
+            collision_idxs = distances_players_after_scipy < (2 * self.player_radius)
+            collision_idxs[eye_idxs] = False
+            collision_idxs = np.any(collision_idxs, axis=1)
+            new_positions[collision_idxs] = player_positions[collision_idxs]
+
+        # Collisions player world borders
+        new_positions = np.clip(
+            new_positions,
+            self.world_borders_lower + self.player_radius,
+            self.world_borders_upper - self.player_radius,
         )
 
-    @staticmethod
-    def detect_collision_player_counter(player: Player, counter: Counter):
-        """Checks if the player and counter collide (overlap).
-        A counter is modelled as a rectangle (square actually), a player is modelled as a circle.
-        The distance of the player position (circle center) and the counter rectangle is calculated, if it is
-        smaller than the player radius, a collision is detected.
+        for idx, p in enumerate(self.players.values()):
+            if not (new_positions[idx] == player_positions[idx]).all():
+                p.pos = new_positions[idx]
+                p.perform_interact_stop()
 
-        Args:
-            player: The player to check the collision for.
-            counter: The counter to check the collision for.
-
-        Returns: True if player and counter overlap, False if not.
+            p.turn(player_movement_vectors[idx])
 
-        """
-        cx, cy = player.pos
-        dx = max(np.abs(cx - counter.pos[0]) - 1 / 2, 0)
-        dy = max(np.abs(cy - counter.pos[1]) - 1 / 2, 0)
-        distance = np.linalg.norm([dx, dy])
-        # TODO: Efficiency improvement by checking only nearest counters? Quadtree...?
-        return distance < player.radius
+            facing_distances = np.linalg.norm(
+                p.facing_point - self.counter_positions, axis=1
+            )
+            closest_counter = self.counters[facing_distances.argmin()]
+            p.current_nearest_counter = (
+                closest_counter
+                if facing_distances.min() <= self.player_interaction_range
+                else None
+            )
 
     def add_player(self, player_name: str, pos: npt.NDArray = None):
         """Add a player to the environment.
@@ -545,17 +733,22 @@ class Environment:
         self.players[player.name] = player
         if player.pos is None:
             if len(self.designated_player_positions) > 0:
-                free_idx = random.randint(0, len(self.designated_player_positions) - 1)
+                free_idx = self.random.randint(
+                    0, len(self.designated_player_positions) - 1
+                )
                 player.move_abs(self.designated_player_positions[free_idx])
                 del self.designated_player_positions[free_idx]
             elif len(self.free_positions) > 0:
-                free_idx = random.randint(0, len(self.free_positions) - 1)
+                free_idx = self.random.randint(0, len(self.free_positions) - 1)
                 player.move_abs(self.free_positions[free_idx])
                 del self.free_positions[free_idx]
             else:
                 log.debug("No free positions left in kitchens")
             player.update_facing_point()
 
+        self.set_collision_arrays()
+        self.hook(PLAYER_ADDED, player_name=player_name, pos=pos)
+
     def detect_collision_world_bounds(self, player: Player):
         """Checks for detections of the player and the world bounds.
 
@@ -578,16 +771,23 @@ class Environment:
         """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders
         and time limits.
         """
+        # self.hook(PRE_STEP, passed_time=passed_time)
         self.env_time += passed_time
 
-        if not self.game_ended:
+        if self.game_ended:
+            self.hook(GAME_ENDED_STEP)
+        else:
             for player in self.players.values():
-                if self.env_time <= player.movement_until:
-                    self.perform_movement(player, passed_time)
+                player.progress(passed_time, self.env_time)
+
+            self.perform_movement(passed_time)
 
             for counter in self.progressing_counters:
                 counter.progress(passed_time=passed_time, now=self.env_time)
             self.order_and_score.progress(passed_time=passed_time, now=self.env_time)
+            for effect_manager in self.effect_manager.values():
+                effect_manager.progress(passed_time=passed_time, now=self.env_time)
+        # self.hook(POST_STEP, passed_time=passed_time)
 
     def get_state(self):
         """Get the current state of the game environment. The state here is accessible by the current python objects.
@@ -605,24 +805,59 @@ class Environment:
             "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)),
         }
 
-    def get_json_state(self, player_id: str = None):
-        state = {
-            "players": [p.to_dict() for p in self.players.values()],
-            "counters": [c.to_dict() for c in self.counters],
-            "kitchen": {"width": self.kitchen_width, "height": self.kitchen_height},
-            "score": self.order_and_score.score,
-            "orders": self.order_and_score.order_state(),
-            "ended": self.game_ended,
-            "env_time": self.env_time.isoformat(),
-            "remaining_time": max(
-                self.env_time_end - self.env_time, timedelta(0)
-            ).total_seconds(),
-        }
-        json_data = json.dumps(state)
-        assert StateRepresentation.model_validate_json(json_data=json_data)
-        return json_data
+    def get_json_state(self, player_id: str = None) -> str:
+        if player_id in self.players:
+            self.hook(PRE_STATE, player_id=player_id)
+            state = {
+                "players": [p.to_dict() for p in self.players.values()],
+                "counters": [c.to_dict() for c in self.counters],
+                "kitchen": {"width": self.kitchen_width, "height": self.kitchen_height},
+                "score": self.order_and_score.score,
+                "orders": self.order_and_score.order_state(),
+                "ended": self.game_ended,
+                "env_time": self.env_time.isoformat(),
+                "remaining_time": max(
+                    self.env_time_end - self.env_time, timedelta(0)
+                ).total_seconds(),
+                "view_restriction": {
+                    "direction": self.players[player_id].facing_direction.tolist(),
+                    "position": self.players[player_id].pos.tolist(),
+                    "angle": self.player_view_angle,
+                    "counter_mask": None,
+                    "range": self.player_view_range,
+                }
+                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)
+            self.hook(JSON_STATE, json_data=json_data, player_id=player_id)
+            assert StateRepresentation.model_validate_json(json_data=json_data)
+            return json_data
+        raise ValueError(f"No valid {player_id=}")
 
     def reset_env_time(self):
         """Reset the env time to the initial time, defined by `create_init_env_time`."""
+        self.hook(PRE_RESET_ENV_TIME)
         self.env_time = create_init_env_time()
+        self.hook(POST_RESET_ENV_TIME)
         log.debug(f"Reset env time to {self.env_time}")
+
+    def register_callback_for_hook(self, hook_ref: str | list[str], callback: Callable):
+        self.hook.register_callback(hook_ref, callback)
+
+    def extra_setup_functions(self):
+        if self.environment_config["extra_setup_functions"]:
+            for function_name, function_def in self.environment_config[
+                "extra_setup_functions"
+            ].items():
+                log.info(f"Setup function {function_name}")
+                function_def["func"](
+                    name=function_name, env=self, **function_def["kwargs"]
+                )
diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py
index 5f87b87f7032e54f12cace9fecda1d882aac5c54..638bc4cd5c0d537aa5823af96543a008b43c3300 100644
--- a/overcooked_simulator/player.py
+++ b/overcooked_simulator/player.py
@@ -7,16 +7,16 @@ holding object**. If so, it picks up the content and combines it on its hands.
 """
 
 import dataclasses
-import datetime
 import logging
 from collections import deque
+from datetime import datetime, timedelta
 from typing import Optional
 
 import numpy as np
 import numpy.typing as npt
 
 from overcooked_simulator.counters import Counter
-from overcooked_simulator.game_items import Item, Plate
+from overcooked_simulator.game_items import Item, ItemType
 from overcooked_simulator.state_representation import PlayerState
 
 log = logging.getLogger(__name__)
@@ -33,6 +33,12 @@ class PlayerConfig:
     """The move distance/speed of the player per action call."""
     interaction_range: float = 1.6
     """How far player can interact with counters."""
+    restricted_view: bool = False
+    """Whether or not the player can see the entire map at once or just a view frustrum."""
+    view_angle: int | None = None
+    """Angle of the players view if restricted."""
+    view_range: float | None = None
+    """Range of the players view if restricted. In grid units."""
 
 
 class Player:
@@ -57,15 +63,9 @@ class Player:
 
         self.holding: Optional[Item] = None
         """What item the player is holding."""
+        self.player_config = player_config
+        """See `PlayerConfig`."""
 
-        self.radius: float = player_config.radius
-        """See `PlayerConfig.radius`."""
-        self.player_speed_units_per_seconds: float | int = (
-            player_config.player_speed_units_per_seconds
-        )
-        """See `PlayerConfig.move_dist`."""
-        self.interaction_range: float = player_config.interaction_range
-        """See `PlayerConfig.interaction_range`."""
         self.facing_direction: npt.NDArray[float] = np.array([0, 1])
         """Current direction the player looks."""
         self.last_interacted_counter: Optional[
@@ -81,25 +81,17 @@ class Player:
 
         self.current_movement: npt.NDArray[float] = np.zeros(2, float)
         """The movement vector that will be used to calculate the movement in the next step call."""
-        self.movement_until: datetime.datetime = datetime.datetime.min
+        self.movement_until: datetime = datetime.min
         """The env time until the player wants to move."""
 
+        self.interacting: bool = False
+
     def set_movement(self, move_vector, move_until):
         """Called by the `perform_action` method. Movements will be performed (pos will be updated) in the `step`
         function of the environment"""
         self.current_movement = move_vector
         self.movement_until = move_until
-
-    def move(self, movement: npt.NDArray[float]):
-        """Moves the player position by the given movement vector.
-        A unit direction vector multiplied by move_dist is added to the player position.
-
-        Args:
-            movement: 2D-Vector of length 1
-        """
-        self.pos += movement
-        if np.linalg.norm(movement) != 0:
-            self.turn(movement)
+        self.perform_interact_stop()
 
     def move_abs(self, new_pos: npt.NDArray[float]):
         """Overwrites the player location by the new_pos 2d-vector. Absolute movement.
@@ -123,7 +115,9 @@ class Player:
 
     def update_facing_point(self):
         """Update facing point on the player border circle based on the radius."""
-        self.facing_point = self.pos + (self.facing_direction * self.radius * 0.5)
+        self.facing_point = self.pos + (
+            self.facing_direction * self.player_config.radius * 0.5
+        )
 
     def can_reach(self, counter: Counter):
         """Checks whether the player can reach the counter in question. Simple check if the distance is not larger
@@ -136,9 +130,12 @@ class Player:
             True if the counter is in range of the player, False if not.
 
         """
-        return np.linalg.norm(counter.pos - self.facing_point) <= self.interaction_range
+        return (
+            np.linalg.norm(counter.pos - self.facing_point)
+            <= self.player_config.interaction_range
+        )
 
-    def pick_action(self, counter: Counter):
+    def put_action(self, counter: Counter):
         """Performs the pickup-action with the counter. Handles the logic of what the player is currently holding,
         what is currently on the counter and what can be picked up or combined in hand.
 
@@ -159,30 +156,41 @@ class Player:
             self.holding.combine(returned_by_counter)
 
         log.debug(
-            f"Self: {self.holding}, {counter.__class__.__name__}: {counter.occupied_by}"
+            f"Self: {self.holding.__class__.__name__}: {self.holding}, {counter.__class__.__name__}: {counter.occupied_by}"
         )
-        if isinstance(self.holding, Plate):
-            log.debug(self.holding.clean)
+        # if isinstance(self.holding, Plate):
+        #     log.debug(self.holding.clean)
 
-    @staticmethod
-    def perform_interact_hold_start(counter: Counter):
+    def perform_interact_start(self, counter: Counter):
         """Starts an interaction with the counter. Should be called for a
         keydown event, for holding down a key on the keyboard.
 
         Args:
             counter: The counter to start the interaction with.
         """
-        counter.interact_start()
+        self.interacting = True
+        self.last_interacted_counter = counter
 
-    @staticmethod
-    def perform_interact_hold_stop(counter: Counter):
+    def perform_interact_stop(self):
         """Stops an interaction with the counter. Should be called for a
         keyup event, for letting go of a keyboard key.
 
         Args:
             counter: The counter to stop the interaction with.
         """
-        counter.interact_stop()
+        self.interacting = False
+        self.last_interacted_counter = None
+
+    def progress(self, passed_time: timedelta, now: datetime):
+        if self.interacting and self.last_interacted_counter:
+            # TODO only interact on counter (Sink/CuttingBoard) if hands are free configure in config?
+            if self.holding:
+                if self.holding.item_info.type == ItemType.Tool:
+                    self.last_interacted_counter.do_tool_interaction(
+                        passed_time, self.holding
+                    )
+            else:
+                self.last_interacted_counter.do_hand_free_interaction(passed_time, now)
 
     def __repr__(self):
         return f"Player(name:{self.name},pos:{str(self.pos)},holds:{self.holding})"
diff --git a/overcooked_simulator/recording.py b/overcooked_simulator/recording.py
new file mode 100644
index 0000000000000000000000000000000000000000..17a65ce0fa233a4013b6872bde7afbc33ccb584c
--- /dev/null
+++ b/overcooked_simulator/recording.py
@@ -0,0 +1,71 @@
+import json
+import logging
+import os
+import traceback
+from pathlib import Path
+from typing import Any
+
+import platformdirs
+
+from overcooked_simulator import ROOT_DIR
+from overcooked_simulator.overcooked_environment import Environment
+from overcooked_simulator.utils import NumpyAndDataclassEncoder
+
+log = logging.getLogger(__name__)
+
+
+def class_recording_with_hooks(
+    name: str,
+    env: Environment,
+    hooks: list[str],
+    log_class,
+    log_class_kwargs: dict[str, Any],
+):
+    recorder = log_class(name=name, env=env, **log_class_kwargs)
+    for hook in hooks:
+        env.register_callback_for_hook(hook, recorder)
+
+
+class LogRecorder:
+    def __init__(
+        self,
+        name: str,
+        env: Environment,
+        log_path: str = "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl",
+        add_hook_ref: bool = False,
+    ):
+        self.add_hook_ref = add_hook_ref
+        log_path = log_path.replace("ENV_NAME", env.env_name).replace(
+            "LOG_RECORD_NAME", name
+        )
+        if log_path.startswith("USER_LOG_DIR/"):
+            log_path = (
+                Path(platformdirs.user_log_dir("overcooked_simulator"))
+                / log_path[len("USER_LOG_DIR/") :]
+            )
+        elif log_path.startswith("ROOT_DIR/"):
+            log_path = ROOT_DIR / log_path[len("ROOT_DIR/") :]
+        else:
+            log_path = Path(log_path)
+        self.log_path = log_path
+        log.info(f"Recorder record for {name} in file://{log_path}")
+        os.makedirs(log_path.parent, exist_ok=True)
+
+    def __call__(self, hook_ref: str, env: Environment, **kwargs):
+        try:
+            record = (
+                json.dumps(
+                    {
+                        "env_time": env.env_time.isoformat(),
+                        **kwargs,
+                        **({"hook_ref": hook_ref} if self.add_hook_ref else {}),
+                    },
+                    cls=NumpyAndDataclassEncoder,
+                )
+                + "\n"
+            )
+            with open(self.log_path, "a") as log_file:
+                log_file.write(record)
+        except TypeError as e:
+            traceback.print_exception(e)
+            log.info(f"Not JSON serializable Record {kwargs}")
diff --git a/overcooked_simulator/state_representation.py b/overcooked_simulator/state_representation.py
index 47ddf2878977b394b35855732bcdd52a4bc0d0da..fe587ca6d41c40514d0fe3d8e862c09575a6c09b 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
@@ -15,11 +16,20 @@ class OrderState(TypedDict):
     max_duration: float
 
 
+class EffectState(TypedDict):
+    id: str
+    type: str
+    progress_percentage: float | int
+    inverse_progress: bool
+
+
 class ItemState(TypedDict):
     id: str
     category: Literal["Item"] | Literal["ItemCookingEquipment"]
     type: str
     progress_percentage: float | int
+    inverse_progress: bool
+    active_effects: list[EffectState]
     # add ItemType Meal ?
 
 
@@ -33,17 +43,14 @@ class CounterState(TypedDict):
     category: Literal["Counter"]
     type: str
     pos: list[float]
+    orientation: list[float]
     occupied_by: None | list[
         ItemState | CookingEquipmentState
     ] | ItemState | CookingEquipmentState
+    active_effects: list[EffectState]
     # list[ItemState] -> type in ["Sink", "PlateDispenser"]
 
 
-class CuttingBoardAndSinkState(TypedDict):
-    type: Literal["CuttingBoard"] | Literal["Sink"]
-    progressing: bool
-
-
 class PlayerState(TypedDict):
     id: str
     pos: list[float]
@@ -60,6 +67,27 @@ class KitchenInfo(BaseModel):
     height: float
 
 
+class ViewRestriction(BaseModel):
+    direction: list[float]
+    position: list[float]
+    angle: int  # degrees
+    counter_mask: None | list[bool]
+    range: float | None
+
+
+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."""
 
@@ -71,6 +99,8 @@ class StateRepresentation(BaseModel):
     ended: bool
     env_time: datetime  # isoformat str
     remaining_time: float
+    view_restriction: None | ViewRestriction
+    info_msg: list[tuple[str, str]]
 
 
 def create_json_schema():
diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py
index 2754f08912d31ff513ed891646d29571800e8960..aa91a0a99190c177f28dec0fc036e2f001b83b8a 100644
--- a/overcooked_simulator/utils.py
+++ b/overcooked_simulator/utils.py
@@ -1,20 +1,28 @@
 """
 Some utility functions.
 """
+from __future__ import annotations
 
+import dataclasses
+import json
 import logging
 import os
 import sys
 import uuid
-from datetime import datetime
+from collections import deque
+from datetime import datetime, timedelta
 from enum import Enum
+from typing import TYPE_CHECKING
 
 import numpy as np
 import numpy.typing as npt
 from scipy.spatial import distance_matrix
 
 from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.counters import Counter
+
+if TYPE_CHECKING:
+    from overcooked_simulator.counters import Counter
+from overcooked_simulator.player import Player
 
 
 def create_init_env_time():
@@ -39,6 +47,38 @@ def get_closest(point: npt.NDArray[float], counters: list[Counter]):
     ]
 
 
+def get_collided_players(
+    player_idx, players: list[Player], player_radius: float
+) -> list[Player]:
+    player_positions = np.array([p.pos for p in players], dtype=float)
+    distances = distance_matrix(player_positions, player_positions)[player_idx]
+    player_radiuses = np.array([player_radius for p in players], dtype=float)
+    collisions = distances <= player_radiuses + player_radius
+    collisions[player_idx] = False
+
+    return [players[idx] for idx, val in enumerate(collisions) if val]
+
+
+def get_touching_counters(target: Counter, counters: list[Counter]) -> list[Counter]:
+    return list(
+        filter(
+            lambda counter: np.linalg.norm(counter.pos - target.pos) == 1.0, counters
+        )
+    )
+
+
+def find_item_on_counters(item_uuid: str, counters: list[Counter]) -> Counter | None:
+    for counter in counters:
+        if counter.occupied_by:
+            if isinstance(counter.occupied_by, deque):
+                for item in counter.occupied_by:
+                    if item.uuid == item_uuid:
+                        return counter
+            else:
+                if item_uuid == counter.occupied_by.uuid:
+                    return counter
+
+
 def custom_asdict_factory(data):
     """Convert enums to their value."""
 
@@ -73,21 +113,21 @@ def setup_logging(enable_websocket_logging=False):
         logging.getLogger("websockets.client").setLevel(logging.ERROR)
 
 
-def url_and_port_arguments(parser):
+def url_and_port_arguments(parser, server_name="game server", default_port=8000):
     parser.add_argument(
         "-url",
         "--url",
         "--host",
         type=str,
         default="localhost",
-        help="Overcooked game server host url.",
+        help=f"Overcooked {server_name} host url.",
     )
     parser.add_argument(
         "-p",
         "--port",
         type=int,
-        default=8000,
-        help="Port number for the game engine server",
+        default=default_port,
+        help=f"Port number for the {server_name}",
     )
 
 
@@ -106,3 +146,35 @@ def add_list_of_manager_ids_arguments(parser):
         default=[uuid.uuid4().hex],
         help="List of manager IDs that can create environments.",
     )
+
+
+class NumpyAndDataclassEncoder(json.JSONEncoder):
+    """Special json encoder for numpy types"""
+
+    def default(self, obj):
+        if isinstance(obj, np.integer):
+            return int(obj)
+        elif isinstance(obj, np.floating):
+            return float(obj)
+        elif isinstance(obj, np.ndarray):
+            return obj.tolist()
+        elif isinstance(obj, timedelta):
+            return obj.total_seconds()
+        elif isinstance(obj, datetime):
+            return obj.isoformat()
+        elif dataclasses.is_dataclass(obj):
+            return dataclasses.asdict(obj, dict_factory=custom_asdict_factory)
+        # elif callable(obj):
+        #     return getattr(obj, "__name__", "Unknown")
+
+        return json.JSONEncoder.default(self, obj)
+
+
+def create_layout(w, h):
+    for y in range(h):
+        for x in range(w):
+            if x == 0 or y == 0 or x == w - 1 or y == h - 1:
+                print("#", end="")
+            else:
+                print("_", end="")
+        print("")
diff --git a/setup.py b/setup.py
index d57d47c30a8eefd7e427ff97be2c4884bd1a4cfb..a4b8f900e5aa215bba1ef9b2d66078e5ce9096ab 100644
--- a/setup.py
+++ b/setup.py
@@ -11,16 +11,18 @@ with open("CHANGELOG.md") as history_file:
     history = history_file.read()
 
 requirements = [
-    "numpy",
-    "pygame",
-    "scipy",
+    "numpy>=1.26.2",
+    "pygame>=2.5.2",
+    "scipy>=1.11.4",
     "pytest>=3",
-    "pyyaml",
-    "pygame-gui",
-    "fastapi",
-    "uvicorn",
-    "websockets",
-    "requests",
+    "pyyaml>=6.0.1",
+    "pygame-gui>=0.6.9",
+    "pydantic>=2.5.3",
+    "fastapi>=0.109.2",
+    "uvicorn>=0.27.0",
+    "websockets>=12.0",
+    "requests>=2.31.0",
+    "platformdirs>=4.1.0",
 ]
 
 test_requirements = [
@@ -55,4 +57,12 @@ setup(
     url="https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator",
     version="0.1.0",
     zip_safe=False,
+    extras_requires={
+        "rl": [
+            "gymnasium>=0.28.1",
+            "stable-baselines3[extra]>=2.2.1",
+            "opencv-python>=4.9",
+            "wandb>=0.16.3",
+        ]
+    },
 )
diff --git a/tests/test_start.py b/tests/test_start.py
index 1dbdafdafd48a79f29969cc686bc87665562799e..8531f9d81dc52cb4327c98e641406ad2d45bdec2 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -6,6 +6,7 @@ import pytest
 from overcooked_simulator import ROOT_DIR
 from overcooked_simulator.counters import Counter, CuttingBoard
 from overcooked_simulator.game_items import Item, ItemInfo, ItemType
+from overcooked_simulator.hooks import Hooks
 from overcooked_simulator.overcooked_environment import (
     Action,
     Environment,
@@ -45,6 +46,7 @@ def layout_config():
     with open(layout_path, "r") as file:
         layout = file.read()
     return layout
+    env.add_player("0")
 
 
 @pytest.fixture
@@ -79,7 +81,7 @@ def test_movement(env_config, layout_empty_config, item_info):
     player_name = "1"
     start_pos = np.array([3, 4])
     env.add_player(player_name, start_pos)
-    env.players[player_name].player_speed_units_per_seconds = 1
+    env.player_movement_speed = 1
     move_direction = np.array([1, 0])
     move_action = Action(player_name, ActionType.MOVEMENT, move_direction, duration=0.1)
     do_moves_number = 3
@@ -88,22 +90,19 @@ def test_movement(env_config, layout_empty_config, item_info):
         env.step(timedelta(seconds=0.1))
 
     expected = start_pos + do_moves_number * (
-        move_direction
-        * env.players[player_name].player_speed_units_per_seconds
-        * move_action.duration
+        move_direction * env.player_movement_speed * move_action.duration
     )
-
     assert np.isclose(
         np.linalg.norm(expected - env.players[player_name].pos), 0
     ), "Performed movement do not move the player as expected."
 
 
-def test_player_speed_units_per_seconds(env_config, layout_empty_config, item_info):
+def test_player_movement_speed(env_config, layout_empty_config, item_info):
     env = Environment(env_config, layout_empty_config, item_info, as_files=False)
     player_name = "1"
     start_pos = np.array([3, 4])
     env.add_player(player_name, start_pos)
-    env.players[player_name].player_speed_units_per_seconds = 2
+    env.player_movement_speed = 2
     move_direction = np.array([1, 0])
     move_action = Action(player_name, ActionType.MOVEMENT, move_direction, duration=0.1)
     do_moves_number = 3
@@ -112,9 +111,7 @@ def test_player_speed_units_per_seconds(env_config, layout_empty_config, item_in
         env.step(timedelta(seconds=0.1))
 
     expected = start_pos + do_moves_number * (
-        move_direction
-        * env.players[player_name].player_speed_units_per_seconds
-        * move_action.duration
+        move_direction * env.player_movement_speed * move_action.duration
     )
 
     assert np.isclose(
@@ -122,44 +119,14 @@ def test_player_speed_units_per_seconds(env_config, layout_empty_config, item_in
     ), "Performed movement do not move the player as expected."
 
 
-def test_collision_detection(env_config, layout_config, item_info):
-    env = Environment(env_config, layout_config, item_info, as_files=False)
-
-    counter_pos = np.array([1, 2])
-    counter = Counter(counter_pos)
-    env.counters = [counter]
-    env.add_player("1", np.array([1, 1]))
-    env.add_player("2", np.array([1, 4]))
-
-    player1 = env.players["1"]
-    player2 = env.players["2"]
-
-    assert not env.detect_collision_counters(player1), "Should not collide"
-    assert not env.detect_player_collision(player1), "Should not collide yet."
-
-    assert not env.detect_collision(player1), "Does not collide yet."
-
-    player1.move_abs(counter_pos)
-    assert env.detect_collision_counters(
-        player1
-    ), "Player and counter at same pos. Not detected."
-    player2.move_abs(counter_pos)
-    assert env.detect_player_collision(player1), "Players at same pos. Not detected."
-
-    player1.move_abs(np.array([-1, -1]))
-    assert env.detect_collision_world_bounds(
-        player1
-    ), "Player collides with world bounds."
-
-
 def test_player_reach(env_config, layout_empty_config, item_info):
     env = Environment(env_config, layout_empty_config, item_info, as_files=False)
 
     counter_pos = np.array([2, 2])
-    counter = Counter(counter_pos)
-    env.counters = [counter]
+    counter = Counter(pos=counter_pos, hook=Hooks(env))
+    env.overwrite_counters([counter])
     env.add_player("1", np.array([2, 4]))
-    env.players["1"].player_speed_units_per_seconds = 1
+    env.player_movement_speed = 1
     player = env.players["1"]
     assert not player.can_reach(counter), "Player is too far away."
 
@@ -175,13 +142,13 @@ def test_pickup(env_config, layout_config, item_info):
     env = Environment(env_config, layout_config, item_info, as_files=False)
 
     counter_pos = np.array([2, 2])
-    counter = Counter(counter_pos)
+    counter = Counter(pos=counter_pos, hook=Hooks(env))
     counter.occupied_by = Item(name="Tomato", item_info=None)
-    env.counters = [counter]
+    env.overwrite_counters([counter])
 
     env.add_player("1", np.array([2, 3]))
     player = env.players["1"]
-    player.player_speed_units_per_seconds = 1
+    env.player_movement_speed = 1
 
     move_down = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)
     move_up = Action("1", ActionType.MOVEMENT, np.array([0, 1]), duration=1)
@@ -226,7 +193,8 @@ def test_processing(env_config, layout_config, item_info):
     env = Environment(env_config, layout_config, item_info, as_files=False)
     counter_pos = np.array([2, 2])
     counter = CuttingBoard(
-        counter_pos,
+        pos=counter_pos,
+        hook=Hooks(env),
         transitions={
             "ChoppedTomato": ItemInfo(
                 name="ChoppedTomato",
@@ -238,12 +206,11 @@ def test_processing(env_config, layout_config, item_info):
         },
     )
     env.counters.append(counter)
-    env.progressing_counters.append(counter)
 
     tomato = Item(name="Tomato", item_info=None)
     env.add_player("1", np.array([2, 3]))
     player = env.players["1"]
-    player.player_speed_units_per_seconds = 1
+    env.player_movement_speed = 1
     player.holding = tomato
 
     move = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)
@@ -275,6 +242,7 @@ def test_time_passed():
         layouts_folder / "empty.layout",
         ROOT_DIR / "game_content" / "item_info.yaml",
     )
+    env.add_player("0")
     env.reset_env_time()
     passed_time = timedelta(seconds=10)
     env.step(passed_time)
@@ -296,6 +264,8 @@ def test_time_limit():
         layouts_folder / "empty.layout",
         ROOT_DIR / "game_content" / "item_info.yaml",
     )
+    env.add_player("0")
+
     env.reset_env_time()
 
     assert not env.game_ended, "Game has not ended yet"