diff --git a/.gitignore b/.gitignore
index 3c384f425d962516c4f4bcc3b86663cfb0ad0f81..eb6bba84b50a43973ce3844834114045c6c25b17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
 # Created by https://www.toptal.com/developers/gitignore/api/python,intellij,visualstudiocode,pycharm,git,flask,django,docusaurus,ros,ros2,linux,macos,windows
 # Edit at https://www.toptal.com/developers/gitignore?templates=python,intellij,visualstudiocode,pycharm,git,flask,django,docusaurus,ros,ros2,linux,macos,windows
 
+playground
+
 ### Django ###
 *.log
 *.pot
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d8e1afad0b9c7a5006b9e6b12a2c0402eade3121..20c922cf496d3f850f66a511a0f29e4c35b6d89c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,7 @@ pytest:
   stage: test
   script:
     - apt-get update -qy
-    - apt-get install -y python3-dev python3-pip
+    - apt-get install -y python3-dev python3-pip graphviz graphviz-dev
     - pip install pytest
     - pip install .
     - pytest --junitxml=report.xml
@@ -14,10 +14,10 @@ pytest:
 pages:
   script:
     - apt-get update -qy
-    - apt-get install -y python3-dev python3-pip
+    - apt-get install -y python3-dev python3-pip graphviz graphviz-dev
     - pip install pdoc
-    - pip install .
-    - pdoc --output-dir public overcooked_simulator  --logo https://gitlab.ub.uni-bielefeld.de/uploads/-/system/project/avatar/6780/Cooking-Vector-Illustration-Icon-Graphics-4267218-1-580x435.jpg --docformat google
+    - pip install ".[rl]"
+    - pdoc --output-dir public cooperative_cuisine !cooperative_cuisine.reinforcement_learning --logo https://gitlab.ub.uni-bielefeld.de/uploads/-/system/project/avatar/6780/Cooking-Vector-Illustration-Icon-Graphics-4267218-1-580x435.jpg --docformat google
   artifacts:
     paths:
       - public
diff --git a/README.md b/README.md
index 135e869b284165af0d9eebe765a90478cc7f1f31..2798a4f0bf10be8d1b5f63dccb2ce0f29106497d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# Overcooked Simulator
+# Cooperative Cuisine Environment
 
 [Documentation](https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator)
 
-The real-time overcooked simulation for a cognitive cooperative system.
+The overcooked-like cooperative cuisine environment for real-time human cooperative interactions and artificial agents.
 
 **The name ist still work in progress and we will probably change it.**
 
@@ -10,7 +10,7 @@ The real-time overcooked simulation for a cognitive cooperative system.
 
 You have two options to install the environment. Either clone it and install it locally or install it in your
 site-packages.
-You need a Python 3.10 or higher environment. Either conda or PyEnv.
+You need a Python 3.10 or newer environment. Either conda or PyEnv.
 
 ### Local Editable Installation
 
@@ -18,7 +18,7 @@ In your `repo`, `PyCharmProjects` or similar directory with the correct environm
 
 ```bash
 git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
-cd overcooked_simulator
+cd overcooked-simulator
 pip install -e .
 ```
 
@@ -27,35 +27,36 @@ pip install -e .
 Run it via the command line (in your pyenv/conda environment):
 
 ```bash
-overcooked-sim  --url "localhost" --port 8000
+cooperative_cuisine  -s localhost -sp 8080 -g localhost -gp 8000
 ```
 
-_The arguments are the defaults. Therefore, they are optional._
+*The arguments shown are the defaults.*
 
-You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
+You can also start the **Game Server**m **Study Server** (Matchmaking),and the **PyGame GUI** individually in different
+terminals.
 
 ```bash
-python3 overcooked_simulator/game_server.py --url "localhost" --port 8000
+python3 cooperative_cuisine/game_server.py -g localhost -gp 8000 --manager_ids SECRETKEY1 SECRETKEY2
 
-python3 overcooked_simulator/gui_2d_vis/overcooked_simulator.py --url "localhost" --port 8000
-```
+python3 cooperative_cuisine/study_server.py -s localhost -sp 8080 --manager_ids SECRETKEY1
 
-You can start also several GUIs.
+python3 cooperative_cuisine/pygame_2d_vis/gui.py -s localhost -sp 8080 -g localhost -gp 8000
+```
 
-You can replace the GUI with your own GUI (+ study server/matchmaking server).
+You can start also several GUIs. The study server does the matchmaking.
 
 ### Library Installation
 
 The correct environment needs to be active:
 
 ```bash
-pip install overcooked-environment@git+https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator@main
+pip install cooperative_cuisine@git+https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator@main
 ```
 
 #### Run
 
 You can now use the environment and/or simulator in your python code. Just by importing
-it `import overcooked_environment`
+it `import cooperative_cuisine`
 
 ## Configuration
 
@@ -69,7 +70,8 @@ can be cooked/created.
 ### Layout Config
 
 You can define the layout of the kitchen via a layout file. The position of counters are based on a grid system, even
-when the players do not move grid steps but continuous steps. Each character defines a different type of counter.
+when the players do not move grid steps but continuous steps. Each character defines a different type of counter. Which
+character is mapped to which counter is defined in the Environment config.
 
 ### Environment Config
 
diff --git a/conda.recipe/meta.yml b/conda.recipe/meta.yml
index b654f9f3fa208a7d5a9c6ca5b73735639111a201..40752589ae557189d727cf3cb7e7c48864a9de6f 100644
--- a/conda.recipe/meta.yml
+++ b/conda.recipe/meta.yml
@@ -1,8 +1,8 @@
-{% set data = load_setup_py_data() %}
+{ % set data = load_setup_py_data() % }
 
 package:
-  name: overcooked-simulator
-  version: {{ data['version'] }}
+  name: cooperative_cuisine
+  version: { { data[ 'version' ] } }
 
 source:
   path: ..
@@ -12,10 +12,10 @@ build:
   # separate bld.bat and build.sh files instead of this key.  Add the line
   # "skip: True  # [py<35]" (for example) to limit to Python 3.5 and newer, or
   # "skip: True  # [not win]" to limit to Windows.
-  script: {{ PYTHON }} -m pip install --no-deps --ignore-installed -vv .
+  script: { { PYTHON } } -m pip install --no-deps --ignore-installed -vv .
   noarch: python
-  
-  
+
+
 
 requirements:
   # if you need compilers, uncomment these
@@ -28,9 +28,9 @@ requirements:
   run:
     - python
     # dependencies are defined in setup.py
-    {% for dep in data['install_requires'] %}
-    - {{ dep.lower() }}
-    {% endfor %}
+    { % for dep in data[ 'install_requires' ] % }
+    - { { dep.lower() } }
+    { % endfor % }
 
 test:
   source_files:
@@ -43,6 +43,6 @@ test:
 
 about:
   home: https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator
-  summary: The real-time overcooked simulation for a cognitive cooperative system
-  license: {{ data.get('license') }}
+  summary: A overcooked-like environment for a cognitive cooperative system (also in real-time)
+  license: { { data.get('license') } }
   license_file: LICENSE
\ No newline at end of file
diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..c32875fb9546c2096f130f0f95c37af06046c29e
--- /dev/null
+++ b/cooperative_cuisine/__init__.py
@@ -0,0 +1,391 @@
+"""
+
+This is the documentation of CooperativeCuisine.
+
+# About the package
+
+The package contains an environment for cooperation between players/agents. A PyGameGUI visualizes the game to
+human or virtual agents in 2D. A 3D web-enabled version (for example for online studies, currently under development)
+can be found [here](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/godot-overcooked-3d-visualization).
+
+# Background / Literature
+The overcooked/cooking domain is a well established cooperation domain/task. There exists
+environments designed for reinforcement learning agents as well as the game and adaptations of the game for human
+players in a more "real-time"-like environment. They all mostly differ in the visual and graphics dimension. 2D versions
+like overcooked-ai, ... are most well-known in the community. But more visually appealing 3D versions for cooperation with
+humans are getting developed more frequently (cite,...). Besides, the general adaptations of the original overcooked
+game.
+CooperativeCuisine, we want to bring both worlds together: the reinforcement learning and real-time playable
+environment with an appealing visualisation. Enable the potential of developing artificial agents that play with humans
+like a "real", cooperative, human partner.
+
+# Installation
+
+You need a Python **3.10** or newer environment.
+```bash
+pip install cooperative_cuisine@git+https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator@main
+```
+Or clone it and install it as an editable library which allows you to use all the scripts directly.
+```bash
+git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
+cd overcooked-simulator
+pip install -e .
+```
+
+# Usage / Examples
+ Our Cooperative Cuisine is designed for real time interaction but also for reinforcement
+learning (gymnasium environment) with time independent step function calls. It focuses on configurability, extensibility and appealing visualization
+options.
+
+## Human Player
+Run it via the command line (in your pyenv/conda environment):
+
+```bash
+cooperative_cuisine  -s localhost -sp 8080 -g localhost -gp 8000
+```
+
+*The arguments shown are the defaults.*
+
+You can also start the **Game Server**m **Study Server** (Matchmaking),and the **PyGame GUI** individually in different terminals.
+
+```bash
+python3 cooperative_cuisine/game_server.py -g localhost -gp 8000 --manager_ids SECRETKEY1 SECRETKEY2
+
+python3 cooperative_cuisine/study_server.py -s localhost -sp 8080 --manager_ids SECRETKEY1
+
+python3 cooperative_cuisine/pygame_2d_vis/gui.py -s localhost -sp 8080 -g localhost -gp 8000
+```
+
+## Connect with agent and receive game state
+Or you start a game server, create an environment and connect each player/agent via a websocket connection.
+
+To start a game server see above. Your own manager needs to create an environment.
+```python
+import requests
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.game_server import CreateEnvironmentConfig
+from cooperative_cuisine.server_results import CreateEnvResult
+
+
+with open(ROOT_DIR / "configs" / "item_info.yaml", "r") as file:
+    item_info = file.read()
+with open(ROOT_DIR / "configs" / "layouts" / "basic.layout", "r") as file:
+    layout = file.read()
+with open(ROOT_DIR / "configs" / "environment_config.yaml", "r") as file:
+    environment_config = file.read()
+
+create_env = CreateEnvironmentConfig(
+    manager_id="SECRETKEY1",
+    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")
+
+env_info = requests.post("http://localhost:8000/manage/create_env", json=create_env)
+if env_info.status_code == 403:
+    raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
+env_info: CreateEnvResult = env_info.json()
+```
+
+Connect each player via a websocket (threaded or async).
+```python
+import json
+import dataclasses
+from websockets import connect
+
+from cooperative_cuisine.environment import Action, ActionType, InterActionData
+from cooperative_cuisine.utils import custom_asdict_factory
+
+
+p1_websocket = connect("ws://localhost:8000/ws/player/" + env_info["player_info"]["0"]["client_id"])
+
+# set player "0" as ready
+p1_websocket.send(json.dumps({"type": "ready", "player_hash": env_info["player_info"]["0"]["player_hash"]}))
+assert json.loads(websocket.recv())["status"] == 200, "not accepted player"
+
+
+# get the state for player "0", call it on every frame/step
+p1_websocket.send(json.dumps({"type": "get_state", "player_hash": env_info["player_info"]["0"]["player_hash"]}))
+state = json.loads(websocket.recv())
+
+# send an action for player "0"
+# --- movement ---
+action = Action(
+    player="0",
+    action_type=ActionType.MOVEMENT,
+    action_data=[0.0, 1.0],  # direction (here straight up)
+    duration=0.5  # seconds
+)
+# --- pickup/drop off ---
+action = Action(
+    player="0",
+    action_type=ActionType.PUT,
+    action_data="pickup",
+)
+# --- interact ---
+action = Action(
+    player="0",
+    action_type=ActionType.INTERACT,
+    action_data=InterActionData.START  # InterActionData.STOP when to stop the interaction
+)
+
+p1_websocket.send(json.dumps({
+    "type": "action",
+    "player_hash": env_info["player_info"]["0"]["player_hash"],
+    "action": dataclasses.asdict(
+        action, dict_factory=custom_asdict_factory
+    ),
+}))
+websocket.recv()
+
+```
+
+Stop the environment if you want the game to end before the time is up.
+```python
+requests.post(
+    "http://localhost:8000/manage/stop_env",
+    json={
+        "manager_id": "SECRETKEY1",
+        "env_id": env_info["env_id"],
+        "reason": "closed environment",
+    },
+)
+```
+
+## Direct integration into your code
+You can use the `cooperative_cuisine.environment.Environment` class
+and call the `step`, `get_json_state`, and `perform_action` methods directly.
+
+```python
+from datetime import timedelta
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.environment import Action, Environment
+
+env = Environment(
+    env_config=ROOT_DIR / "configs" / "environment_config.yaml",
+    layout_config=ROOT_DIR / "configs" / "layouts" / "basic.layout",
+    item_info=ROOT_DIR / "configs" / "item_info.yaml"
+)
+
+env.add_player("0")
+env.add_player("1")
+
+while True:
+    # adapt this to real time if needed with time.sleep etc.
+    env.step(timedelta(seconds=0.1))
+
+    player_0_state = json.loads(env.get_json_state("0"))
+    if player_0_state["ended"]:
+        break
+
+    action = ...  # Please refer to the above code but remember to use np.array instead of list for the movement direction vector
+    env.perform_action(action)
+```
+
+# JSON State
+The JSON schema for the state of the environment for a player can be generated by running the `state_representation.py`
+```bash
+python state_representation.py
+```
+Should look like
+```
+{'$defs': {'CookingEquipmentState': {'properties': {'content_list': {'items': {'$ref': '#/$defs/ItemState'}, 'title': 'Content List', 'type': 'array'}, 'content_ready': {'anyOf': [{'$ref': '#/$defs/ItemState'}, {'type': 'null'}]}}, 'required': ['content_list', 'content_ready'], 'title': 'CookingEquipmentState', 'type': 'object'}, 'CounterState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'category': {'const': 'Counter', 'title': 'Category'}, 'type': {'title': 'Type', 'type': 'string'}, 'pos': {'items': {'type': 'number'}, 'title': 'Pos', 'type': 'array'}, 'orientation': {'items': {'type': 'number'}, 'title': 'Orientation', 'type': 'array'}, 'occupied_by': {'anyOf': [{'items': {'anyOf': [{'$ref': '#/$defs/ItemState'}, {'$ref': '#/$defs/CookingEquipmentState'}]}, 'type': 'array'}, {'$ref': '#/$defs/ItemState'}, {'$ref': '#/$defs/CookingEquipmentState'}, {'type': 'null'}], 'title': 'Occupied By'}, 'active_effects': {'items': {'$ref': '#/$defs/EffectState'}, 'title': 'Active Effects', 'type': 'array'}}, 'required': ['id', 'category', 'type', 'pos', 'orientation', 'occupied_by', 'active_effects'], 'title': 'CounterState', 'type': 'object'}, 'EffectState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'type': {'title': 'Type', 'type': 'string'}, 'progress_percentage': {'anyOf': [{'type': 'number'}, {'type': 'integer'}], 'title': 'Progress Percentage'}, 'inverse_progress': {'title': 'Inverse Progress', 'type': 'boolean'}}, 'required': ['id', 'type', 'progress_percentage', 'inverse_progress'], 'title': 'EffectState', 'type': 'object'}, 'ItemState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'category': {'anyOf': [{'const': 'Item'}, {'const': 'ItemCookingEquipment'}], 'title': 'Category'}, 'type': {'title': 'Type', 'type': 'string'}, 'progress_percentage': {'anyOf': [{'type': 'number'}, {'type': 'integer'}], 'title': 'Progress Percentage'}, 'inverse_progress': {'title': 'Inverse Progress', 'type': 'boolean'}, 'active_effects': {'items': {'$ref': '#/$defs/EffectState'}, 'title': 'Active Effects', 'type': 'array'}}, 'required': ['id', 'category', 'type', 'progress_percentage', 'inverse_progress', 'active_effects'], 'title': 'ItemState', 'type': 'object'}, 'KitchenInfo': {'description': 'Basic information of the kitchen.', 'properties': {'width': {'title': 'Width', 'type': 'number'}, 'height': {'title': 'Height', 'type': 'number'}}, 'required': ['width', 'height'], 'title': 'KitchenInfo', 'type': 'object'}, 'OrderState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'category': {'const': 'Order', 'title': 'Category'}, 'meal': {'title': 'Meal', 'type': 'string'}, 'start_time': {'format': 'date-time', 'title': 'Start Time', 'type': 'string'}, 'max_duration': {'title': 'Max Duration', 'type': 'number'}}, 'required': ['id', 'category', 'meal', 'start_time', 'max_duration'], 'title': 'OrderState', 'type': 'object'}, 'PlayerState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'pos': {'items': {'type': 'number'}, 'title': 'Pos', 'type': 'array'}, 'facing_direction': {'items': {'type': 'number'}, 'title': 'Facing Direction', 'type': 'array'}, 'holding': {'anyOf': [{'$ref': '#/$defs/ItemState'}, {'$ref': '#/$defs/CookingEquipmentState'}, {'type': 'null'}], 'title': 'Holding'}, 'current_nearest_counter_pos': {'anyOf': [{'items': {'type': 'number'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Current Nearest Counter Pos'}, 'current_nearest_counter_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Current Nearest Counter Id'}}, 'required': ['id', 'pos', 'facing_direction', 'holding', 'current_nearest_counter_pos', 'current_nearest_counter_id'], 'title': 'PlayerState', 'type': 'object'}, 'ViewRestriction': {'properties': {'direction': {'items': {'type': 'number'}, 'title': 'Direction', 'type': 'array'}, 'position': {'items': {'type': 'number'}, 'title': 'Position', 'type': 'array'}, 'angle': {'title': 'Angle', 'type': 'integer'}, 'counter_mask': {'anyOf': [{'items': {'type': 'boolean'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Counter Mask'}, 'range': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'title': 'Range'}}, 'required': ['direction', 'position', 'angle', 'counter_mask', 'range'], 'title': 'ViewRestriction', 'type': 'object'}}, 'description': 'The format of the returned state representation.', 'properties': {'players': {'items': {'$ref': '#/$defs/PlayerState'}, 'title': 'Players', 'type': 'array'}, 'counters': {'items': {'$ref': '#/$defs/CounterState'}, 'title': 'Counters', 'type': 'array'}, 'kitchen': {'$ref': '#/$defs/KitchenInfo'}, 'score': {'anyOf': [{'type': 'number'}, {'type': 'integer'}], 'title': 'Score'}, 'orders': {'items': {'$ref': '#/$defs/OrderState'}, 'title': 'Orders', 'type': 'array'}, 'all_players_ready': {'title': 'All Players Ready', 'type': 'boolean'}, 'ended': {'title': 'Ended', 'type': 'boolean'}, 'env_time': {'format': 'date-time', 'title': 'Env Time', 'type': 'string'}, 'remaining_time': {'title': 'Remaining Time', 'type': 'number'}, 'view_restrictions': {'anyOf': [{'items': {'$ref': '#/$defs/ViewRestriction'}, 'type': 'array'}, {'type': 'null'}], 'title': 'View Restrictions'}, 'served_meals': {'items': {'maxItems': 2, 'minItems': 2, 'prefixItems': [{'type': 'string'}, {'type': 'string'}], 'type': 'array'}, 'title': 'Served Meals', 'type': 'array'}, 'info_msg': {'items': {'maxItems': 2, 'minItems': 2, 'prefixItems': [{'type': 'string'}, {'type': 'string'}], 'type': 'array'}, 'title': 'Info Msg', 'type': 'array'}}, 'required': ['players', 'counters', 'kitchen', 'score', 'orders', 'all_players_ready', 'ended', 'env_time', 'remaining_time', 'view_restrictions', 'served_meals', 'info_msg'], 'title': 'StateRepresentation', 'type': 'object'}
+```
+
+The BaseModel and TypedDicts can be found in `cooperative_cuisine.state_representation`. The
+`cooperative_cuisine.state_representation.StateRepresentation` represents the json state that the `get_json_state`
+returns.
+
+## Generate images from JSON states
+You might have stored some json states and now you want to visualize them via the
+pygame-2d visualization. You can do that by running the `drawing.py` script and referencing a json file.
+```bash
+python3 cooperative_cuisine/pygame_2d_vis/drawing.py --state my_state.json
+```
+- You can specify a different visualization config with `-v` or `--visualization_config`.
+- You can specify the name of the output file with `-o` or `--output_file`. The default is `screenshot.jpg`.
+
+## Generate images/videos from recordings
+You can record json states or only the actions and the environment config via hooks and the recording class.
+
+If you want to generate images or complete videos, see `cooperative_cuisine.pygame_2d_vis.video_replay`.
+
+# Configuration
+
+The environment configuration is currently done with 3 config files + GUI configuration.
+
+## Item Config
+
+The item config defines which ingredients, cooking equipment and meals can exist and how meals and processed ingredients
+can be cooked/created.
+
+For example
+
+    CuttingBoard:
+      type: Equipment
+
+    Stove:
+      type: Equipment
+
+    Pot:
+      type: Equipment
+      equipment: Stove
+
+    Tomato:
+      type: Ingredient
+
+    ChoppedTomato:
+      type: Ingredient
+      needs: [ Tomato ]
+      seconds: 4.0
+      equipment: CuttingBoard
+
+    TomatoSoup:
+      type: Meal
+      needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
+      seconds: 6.0
+      equipment: Pot
+
+
+## Layout Config
+
+You can define the layout of the kitchen via a layout file. The position of counters are based on a grid system, even
+when the players do not move grid steps but continuous steps. Each character defines a different type of counter. Which
+character is mapped to which counter is defined in the Environment config.
+
+For example
+
+```
+#QU#FO#TNLB#
+#__________M
+#__________K
+W__________I
+#__A_____A_D
+C__________E
+C__________G
+#__________#
+#P#S+#X##S+#
+```
+
+## Environment Config
+
+The environment config details how a level/environment is defined. Here, the available plates, meals, order and player
+configuration is done.
+
+For example
+
+```yaml
+plates:
+  clean_plates: 1
+  dirty_plates: 2
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 300
+
+meals:
+  all: true
+
+layout_chars:
+  _: Free
+  hash: Counter
+  A: Agent
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  W: ServingWindow
+  S: Sink
+  +: SinkAddon
+  U: Pot  # with Stove
+  T: Tomato
+
+orders:
+  ...
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 8
+  interaction_range: 1.6
+```
+
+## PyGame Visualization Config
+
+Here the visualisation for all objects is defined. Reference the images or define a list of base shapes that represent
+the counters, ingredients, meals and players.
+
+## Study Config
+
+You can setup a study with a study config.
+It defines which levels the player will play after they connect to the study server.
+Further, you define how many players play together within on environment.
+
+An example study config is:
+```yaml
+# Config paths are relative to configs folder.
+# Layout files are relative to layouts folder.
+
+
+levels:
+  - config_path: study/level1/level1_config.yaml
+    layout_path: overcooked-1/1-1-far-apart.layout
+    item_info_path: study/level1/level1_item_info.yaml
+    name: "Level 1-1: Far Apart"
+
+  - config_path: environment_config.yaml
+    layout_path: basic.layout
+    item_info_path: item_info.yaml
+    name: "Basic"
+
+  - config_path: study/level2/level2_config.yaml
+    layout_path: overcooked-1/1-4-bottleneck.layout
+    item_info_path: study/level2/level2_item_info.yaml
+    name: "Level 1-4: Bottleneck"
+
+
+num_players: 1
+num_bots: 0
+```
+
+# Citation
+
+# Structure of the Documentation
+The API documentation follows the file and content structure in the repo.
+On the left you can find the navigation panel that brings you to the implementation of
+- the **counter factory** converts the characters in the layout file to counter instances,
+- the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven),
+  sink, etc.,
+- the **effect manager**, how the fire effect is defined,
+- the **environment**, handles the incoming actions and provides the state,
+- the **study server**, the match making server,
+- the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
+- the **game server**, which can manage several running environments and can communicates via FastAPI post requests and
+websockets,
+- the **hooks**, you can adapt and extend recording, scoring, msgs to players, etc. via hooks,
+- the **info msgs** to players, based on hook events, short text msgs can be generated,
+- the **orders**, how to sample incoming orders and their attributes,
+- the **player**/agent, that interacts in the environment,
+- the **pygame 2d visualization**, GUI, drawing, and video generation,
+- the **recording**, via hooks, actions, environment configs, states, etc. can be recorded in files,
+- the **scores**, via hooks, events can affect the scores,
+- type hints are defined in **state representation** for the json state and **server results** for the data returned by
+the game server in post requests.
+- **util**ity code.
+
+
+"""
+import os
+from pathlib import Path
+
+ROOT_DIR = Path(os.path.dirname(os.path.abspath(__file__)))  # This is your Project Root
+"""A path variable to get access to the layouts coming with the package. For example,
+```python 
+from cooperative_cuisine import ROOT_DIR
+
+environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml"
+```
+"""
diff --git a/cooperative_cuisine/__main__.py b/cooperative_cuisine/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c067da2020b3b758b03c83f9abfdbf8da1d4eae
--- /dev/null
+++ b/cooperative_cuisine/__main__.py
@@ -0,0 +1,116 @@
+import argparse
+import time
+from multiprocessing import Process
+
+from cooperative_cuisine.utils import (
+    url_and_port_arguments,
+    disable_websocket_logging_arguments,
+    add_list_of_manager_ids_arguments,
+)
+
+USE_STUDY_SERVER = True
+
+
+def start_game_server(cli_args):
+    from cooperative_cuisine.game_server import main
+
+    main(cli_args.game_url, cli_args.game_port, cli_args.manager_ids)
+
+
+def start_study_server(cli_args):
+    from cooperative_cuisine.study_server import main
+
+    main(
+        cli_args.study_url,
+        cli_args.study_port,
+        game_host=cli_args.game_url,
+        game_port=cli_args.game_port,
+        manager_ids=cli_args.manager_ids,
+    )
+
+
+def start_pygame_gui(cli_args):
+    from cooperative_cuisine.pygame_2d_vis.gui import main
+
+    main(
+        cli_args.study_url,
+        cli_args.study_port,
+        cli_args.game_url,
+        cli_args.game_port,
+        cli_args.manager_ids,
+        CONNECT_WITH_STUDY_SERVER=USE_STUDY_SERVER,
+    )
+
+
+def main(cli_args=None):
+    study_server = None
+
+    parser = argparse.ArgumentParser(
+        prog="Cooperative Cuisine",
+        description="Game Engine Server + PyGameGUI: Starts overcooked game engine server and a PyGame 2D Visualization window in two processes.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+    url_and_port_arguments(parser)
+    disable_websocket_logging_arguments(parser)
+    add_list_of_manager_ids_arguments(parser)
+
+    cli_args = parser.parse_args()
+
+    game_server = None
+    pygame_gui = None
+    try:
+        if USE_STUDY_SERVER:
+            print("Start study server:")
+            study_server = Process(target=start_study_server, args=(cli_args,))
+            study_server.start()
+            time.sleep(0.5)
+
+        print("Start game engine:")
+        game_server = Process(target=start_game_server, args=(cli_args,))
+        game_server.start()
+        time.sleep(0.5)
+
+        print("Start PyGame GUI 1:")
+        pygame_gui = Process(target=start_pygame_gui, args=(cli_args,))
+        pygame_gui.start()
+
+        if USE_STUDY_SERVER:
+            pass
+            # print("Start PyGame GUI 2:")
+            # pygame_gui_2 = Process(target=start_pygame_gui, args=(cli_args,))
+            # pygame_gui_2.start()
+            # # #
+            # print("Start PyGame GUI 3:")
+            # pygame_gui_3 = Process(target=start_pygame_gui, args=(cli_args,))
+            # pygame_gui_3.start()
+            #
+            # print("Start PyGame GUI 4:")
+            # pygame_gui_4 = Process(target=start_pygame_gui, args=(cli_args,))
+            # pygame_gui_4.start()
+            # while (
+            #     pygame_gui.is_alive()
+            #     and pygame_gui_2.is_alive()
+            #     and pygame_gui_3.is_alive()
+            # ):
+            #     time.sleep(1)
+
+        while pygame_gui.is_alive():
+            time.sleep(1)
+
+    except KeyboardInterrupt:
+        print("Received Keyboard interrupt")
+    finally:
+        if USE_STUDY_SERVER and study_server is not None and study_server.is_alive():
+            print("Terminate study server")
+            study_server.terminate()
+        if game_server is not None and game_server.is_alive():
+            print("Terminate game server")
+            game_server.terminate()
+        if pygame_gui is not None and pygame_gui.is_alive():
+            print("Terminate pygame gui")
+            pygame_gui.terminate()
+        time.sleep(0.1)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/cooperative_cuisine/configs/agents/arch_config.yml b/cooperative_cuisine/configs/agents/arch_config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e7108df0e6c1b618eae57b948a6ed139e4fae445
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/agents/random_agent.py b/cooperative_cuisine/configs/agents/random_agent.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c36a303bdfd238d4ef0c6c41a06aed8c3c21436
--- /dev/null
+++ b/cooperative_cuisine/configs/agents/random_agent.py
@@ -0,0 +1,223 @@
+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 cooperative_cuisine.environment import (
+    ActionType,
+    Action,
+    InterActionData,
+)
+from cooperative_cuisine.utils import custom_asdict_factory
+
+TIME_TO_STOP_ACTION = 3.0
+
+
+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=TIME_TO_STOP_ACTION)
+                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/cooperative_cuisine/configs/agents/run_config.yml b/cooperative_cuisine/configs/agents/run_config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f9c6cdf64e2133ae910dd13d90a8ec734368cd00
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d589789dcb43f573f2cc9fa212845b880399873c
--- /dev/null
+++ b/cooperative_cuisine/configs/environment_config.yaml
@@ -0,0 +1,180 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 0
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 300
+  undo_dispenser_pickup: true
+
+meals:
+  all: true
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - TomatoSoup
+    - OnionSoup
+    - Salad
+
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: 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
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  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:cooperative_cuisine.orders.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 6
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+  serving_not_ordered_meals: true
+  # can meals that are not ordered be served / dropped on the serving window
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 6
+  interaction_range: 1.6
+  restricted_view: False
+  view_angle: 70
+  view_range: 4  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  # # ---------------  Scoring  ---------------
+  orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ completed_order ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 20
+        score_on_specific_kwarg: meal_name
+        score_map:
+          Burger: 15
+          OnionSoup: 10
+          Salad: 5
+          TomatoSoup: 10
+  not_ordered_meals:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ serve_not_ordered_meal ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 2
+  trashcan_usages:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ trashcan_usage ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -5
+  expired_orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_expired ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -10
+  # # --------------- Recording ---------------
+  #  json_states:
+  #    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+  #      callback_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+#  info_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ cutting_board_100 ]
+#      callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      callback_class_kwargs:
+#        msg: Glückwunsch du hast was geschnitten!
+#  fire_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ new_fire ]
+#      callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      callback_class_kwargs:
+#        msg: Feuer, Feuer, Feuer
+#        level: Warning
diff --git a/cooperative_cuisine/configs/item_info.yaml b/cooperative_cuisine/configs/item_info.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..96ddf78c9ddb9800f1948f9e159cc2215784648c
--- /dev/null
+++ b/cooperative_cuisine/configs/item_info.yaml
@@ -0,0 +1,233 @@
+CuttingBoard:
+  type: Equipment
+
+Sink:
+  type: Equipment
+
+Stove:
+  type: Equipment
+
+DeepFryer:
+  type: Equipment
+
+Oven:
+  type: Equipment
+
+Pot:
+  type: Equipment
+  equipment: Stove
+
+Pan:
+  type: Equipment
+  equipment: Stove
+
+Basket:
+  type: Equipment
+  equipment: DeepFryer
+
+Peel:
+  type: Equipment
+  equipment: Oven
+
+DirtyPlate:
+  type: Equipment
+
+Plate:
+  type: Equipment
+  needs: [ DirtyPlate ]
+  seconds: 2.0
+  equipment: Sink
+
+# --------------------------------------------------------------------------------
+
+Tomato:
+  type: Ingredient
+
+Lettuce:
+  type: Ingredient
+
+Onion:
+  type: Ingredient
+
+Meat:
+  type: Ingredient
+
+Bun:
+  type: Ingredient
+
+Potato:
+  type: Ingredient
+
+Fish:
+  type: Ingredient
+
+Dough:
+  type: Ingredient
+
+Cheese:
+  type: Ingredient
+
+Sausage:
+  type: Ingredient
+
+# Chopped things
+ChoppedTomato:
+  type: Ingredient
+  needs: [ Tomato ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedLettuce:
+  type: Ingredient
+  needs: [ Lettuce ]
+  seconds: 3.0
+  equipment: CuttingBoard
+
+ChoppedOnion:
+  type: Ingredient
+  needs: [ Onion ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+RawPatty:
+  type: Ingredient
+  needs: [ Meat ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+RawChips:
+  type: Ingredient
+  needs: [ Potato ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedFish:
+  type: Ingredient
+  needs: [ Fish ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+PizzaBase:
+  type: Ingredient
+  needs: [ Dough ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+GratedCheese:
+  type: Ingredient
+  needs: [ Cheese ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedSausage:
+  type: Ingredient
+  needs: [ Sausage ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+CookedPatty:
+  type: Ingredient
+  seconds: 5.0
+  needs: [ RawPatty ]
+  equipment: Pan
+
+# --------------------------------------------------------------------------------
+
+Chips:
+  type: Meal
+  seconds: 5.0
+  needs: [ RawChips ]
+  equipment: Basket
+
+FriedFish:
+  type: Meal
+  seconds: 5.0
+  needs: [ ChoppedFish ]
+  equipment: Basket
+
+Burger:
+  type: Meal
+  needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
+  equipment: ~
+
+Salad:
+  type: Meal
+  needs: [ ChoppedLettuce, ChoppedTomato ]
+  equipment: ~
+
+TomatoSoup:
+  type: Meal
+  needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
+  seconds: 6.0
+  equipment: Pot
+
+OnionSoup:
+  type: Meal
+  needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
+  seconds: 6.0
+  equipment: Pot
+
+FishAndChips:
+  type: Meal
+  needs: [ FriedFish, Chips ]
+  equipment: ~
+
+Pizza:
+  type: Meal
+  needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
+  seconds: 7.0
+  equipment: Peel
+
+# --------------------------------------------------------------------------------
+
+BurntCookedPatty:
+  type: Waste
+  seconds: 10.0
+  needs: [ CookedPatty ]
+  equipment: Pan
+
+BurntChips:
+  type: Waste
+  seconds: 10.0
+  needs: [ Chips ]
+  equipment: Basket
+
+BurntFriedFish:
+  type: Waste
+  seconds: 10.0
+  needs: [ FriedFish ]
+  equipment: Basket
+
+BurntTomatoSoup:
+  type: Waste
+  needs: [ TomatoSoup ]
+  seconds: 20.0
+  equipment: Pot
+
+BurntOnionSoup:
+  type: Waste
+  needs: [ OnionSoup ]
+  seconds: 20.0
+  equipment: Pot
+
+BurntPizza:
+  type: Waste
+  needs: [ Pizza ]
+  seconds: 10.0
+  equipment: Peel
+
+# --------------------------------------------------------------------------------
+
+Fire:
+  type: Effect
+  seconds: 20.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/cooperative_cuisine/configs/item_info_debug.yaml
similarity index 71%
rename from overcooked_simulator/game_content/item_info_debug.yaml
rename to cooperative_cuisine/configs/item_info_debug.yaml
index c2282253e9539c9686cd6746f536e0316c3b21ec..fd871d1d5685df542317bf772b67f3a3fd8ed7d8 100644
--- a/overcooked_simulator/game_content/item_info_debug.yaml
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/basic.layout b/cooperative_cuisine/configs/layouts/basic.layout
new file mode 100644
index 0000000000000000000000000000000000000000..f3d0c2ec5af7857c4af42841d154a677fd31b21b
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/basic.layout
@@ -0,0 +1,9 @@
+#QU#FO#TNLB#
+#__________M
+|__________K
+$__________I
+#__A_____A_D
+C__________E
+#__________G
+C__________#
+##PS+#X##S+#
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/empty.layout b/cooperative_cuisine/configs/layouts/empty.layout
new file mode 100644
index 0000000000000000000000000000000000000000..1160842744d30ff014b5e02ac7b5bea7d2421e3d
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/empty.layout
@@ -0,0 +1,8 @@
+_______
+_______
+_______
+_______
+__A____
+_______
+_______
+______P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/overcooked-1/1-1-far-apart.layout b/cooperative_cuisine/configs/layouts/overcooked-1/1-1-far-apart.layout
new file mode 100644
index 0000000000000000000000000000000000000000..1b0b160ca73b18a62526ab9d020b9a4394fd2c63
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-1/1-1-far-apart.layout
@@ -0,0 +1,14 @@
+###N#T##U####
+#___________|
+L___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/cooperative_cuisine/configs/layouts/overcooked-1/1-2-pedestrians.layout b/cooperative_cuisine/configs/layouts/overcooked-1/1-2-pedestrians.layout
new file mode 100644
index 0000000000000000000000000000000000000000..e9ac2fb08cd9d6437eac0d30da17d6a6114f037e
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/1-3-moving-counters.layout b/cooperative_cuisine/configs/layouts/overcooked-1/1-3-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..3dfb4177c4332c3263818e86c6390d562f5bc1bb
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/1-4-bottleneck.layout b/cooperative_cuisine/configs/layouts/overcooked-1/1-4-bottleneck.layout
new file mode 100644
index 0000000000000000000000000000000000000000..5ef0ecad7bbdaffe2900f311052057db17d27f23
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-1/1-4-bottleneck.layout
@@ -0,0 +1,14 @@
+##S+####QQQU#
+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/cooperative_cuisine/configs/layouts/overcooked-1/1-5-circle.layout b/cooperative_cuisine/configs/layouts/overcooked-1/1-5-circle.layout
new file mode 100644
index 0000000000000000000000000000000000000000..a278d9ab1c5d4214d8194d0b2ef82822dff2ae32
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/1-6-raising-platforms.layout b/cooperative_cuisine/configs/layouts/overcooked-1/1-6-raising-platforms.layout
new file mode 100644
index 0000000000000000000000000000000000000000..5af323f1c86c874cb126fb25170fe46e67cf01e3
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/2-1-moving-trucks.layout b/cooperative_cuisine/configs/layouts/overcooked-1/2-1-moving-trucks.layout
new file mode 100644
index 0000000000000000000000000000000000000000..b7a4745ab23db07ddaa344b83e36ffa2840a2a07
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/2-2-rats.layout b/cooperative_cuisine/configs/layouts/overcooked-1/2-2-rats.layout
new file mode 100644
index 0000000000000000000000000000000000000000..4543c03b09012c4d8f86a88d3eb688f7230ba0cf
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/2-3-separated-conveyors.layout b/cooperative_cuisine/configs/layouts/overcooked-1/2-3-separated-conveyors.layout
new file mode 100644
index 0000000000000000000000000000000000000000..6980844cd618acaf578975d98b261d0b0c6d0147
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/2-4-separated-2.layout b/cooperative_cuisine/configs/layouts/overcooked-1/2-4-separated-2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..d0416de526c511526c42cf430b7ae40ead2ff46b
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/3-1-ice.layout b/cooperative_cuisine/configs/layouts/overcooked-1/3-1-ice.layout
new file mode 100644
index 0000000000000000000000000000000000000000..4d47b65b2cd83bf38a0bdf9c08c6d235c4074cc9
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/3-2-separated-moving-counters.layout b/cooperative_cuisine/configs/layouts/overcooked-1/3-2-separated-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..dc8aefbc3a7c3e149bbff07a600bd452453edee9
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/3-3-moving-trucks-2.layout b/cooperative_cuisine/configs/layouts/overcooked-1/3-3-moving-trucks-2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..f5385f60d38a706c609fc6f199a323f1bfeb36d1
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/3-4-ice-moving-platforms.layout b/cooperative_cuisine/configs/layouts/overcooked-1/3-4-ice-moving-platforms.layout
new file mode 100644
index 0000000000000000000000000000000000000000..389caab9fc91d253aeba25b3c9129e416139b326
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/4-1-moving-counters.layout b/cooperative_cuisine/configs/layouts/overcooked-1/4-1-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..6ae7c2113efad94e1b27caf688e6af19d2059f6f
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-1/4-1-moving-counters.layout
@@ -0,0 +1,17 @@
+"""""#|#O#O#O#X#"""
+_____#_________#"""
+__#____________#"""
+__####____##@@@####
+__+_______#_______$
+__S___A___#___A___$
+__#_______#_______P
+__#####C#C#____####
+__#____________#"""
+____"#_________#"""
+"""""#L#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/cooperative_cuisine/configs/layouts/overcooked-1/4-2-dark.layout b/cooperative_cuisine/configs/layouts/overcooked-1/4-2-dark.layout
new file mode 100644
index 0000000000000000000000000000000000000000..a591aa97c088a0edaa01287038770ff4df30d579
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/4-3-moving-counters.layout b/cooperative_cuisine/configs/layouts/overcooked-1/4-3-moving-counters.layout
new file mode 100644
index 0000000000000000000000000000000000000000..eb704056ac8135e4e8cc11ede3dbd135f4794340
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/overcooked-1/4-4-moving-counters-separated.layout b/cooperative_cuisine/configs/layouts/overcooked-1/4-4-moving-counters-separated.layout
new file mode 100644
index 0000000000000000000000000000000000000000..88bc660ac2bc1ddcbd2600b715a38613dc4d16b7
--- /dev/null
+++ b/cooperative_cuisine/configs/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/cooperative_cuisine/configs/layouts/rl_small.layout b/cooperative_cuisine/configs/layouts/rl_small.layout
new file mode 100644
index 0000000000000000000000000000000000000000..c3e66a2cc76fa913c6b24b04c92f0fa969ff2caa
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/rl_small.layout
@@ -0,0 +1,4 @@
+#X##
+T__P
+U__#
+#C$#
diff --git a/cooperative_cuisine/configs/layouts/tutorial.layout b/cooperative_cuisine/configs/layouts/tutorial.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8f4cf0f8ffa67a79836f4f0e69bbb0f2ec60410e
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/tutorial.layout
@@ -0,0 +1,5 @@
+#C##
+#__X
+T__#
+L__$
+#P##
diff --git a/overcooked_simulator/game_content/layouts/godot_test_layout.layout b/cooperative_cuisine/configs/layouts/zztest_layouts/godot_test_layout.layout
similarity index 100%
rename from overcooked_simulator/game_content/layouts/godot_test_layout.layout
rename to cooperative_cuisine/configs/layouts/zztest_layouts/godot_test_layout.layout
diff --git a/cooperative_cuisine/configs/layouts/zztest_layouts/test1.layout b/cooperative_cuisine/configs/layouts/zztest_layouts/test1.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8ccd0a1152692fd1c9a0a911b0bb677bab98cb95
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/zztest_layouts/test1.layout
@@ -0,0 +1 @@
+____A___P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/zztest_layouts/test2.layout b/cooperative_cuisine/configs/layouts/zztest_layouts/test2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8cfae98af3e7748e2df2dcdaae2ffffbe8c4a074
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/zztest_layouts/test2.layout
@@ -0,0 +1,9 @@
+_
+_
+_
+A
+_
+_
+_
+_
+P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/zztest_layouts/test3.layout b/cooperative_cuisine/configs/layouts/zztest_layouts/test3.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8847c681b18500df23abe068dac76fb4dfbdd6d2
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/zztest_layouts/test3.layout
@@ -0,0 +1,4 @@
+___
+_A_
+___
+__P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/zztest_layouts/test4.layout b/cooperative_cuisine/configs/layouts/zztest_layouts/test4.layout
new file mode 100644
index 0000000000000000000000000000000000000000..09d7551fa64358eb61a97d4c562617dca82b9655
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/zztest_layouts/test4.layout
@@ -0,0 +1,3 @@
+____
+_A__
+___P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/godot_test_layout.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/godot_test_layout.layout
new file mode 100644
index 0000000000000000000000000000000000000000..06db451a2f644732cc693835a9a5aaee6a38d612
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/godot_test_layout.layout
@@ -0,0 +1,9 @@
+##########
+#________#
+#________#
+#________#
+#________#
+#________#
+#________#
+#________#
+#########P
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/large.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/large.layout
new file mode 100644
index 0000000000000000000000000000000000000000..460244ca3fd685c71cdab3cc2bc6d5d4ae1c9092
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_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/cooperative_cuisine/configs/layouts_archive/test_layouts/large_t.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/large_t.layout
new file mode 100644
index 0000000000000000000000000000000000000000..de56d63203bcd0740d8663c1a945025704afecf9
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/large_t.layout
@@ -0,0 +1,45 @@
+#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/cooperative_cuisine/configs/layouts_archive/test_layouts/rot_test.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/rot_test.layout
new file mode 100644
index 0000000000000000000000000000000000000000..d3bd23d0a2dbe378bbc7a128995e8240ebb916fb
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_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/cooperative_cuisine/configs/layouts_archive/test_layouts/split.layout
similarity index 88%
rename from overcooked_simulator/game_content/layouts/split.layout
rename to cooperative_cuisine/configs/layouts_archive/test_layouts/split.layout
index 39bace3e0a94b0594d8ef9f294daeb40aae4492f..3f29e313f63f566ad9deb846f465412949d82e0c 100644
--- a/overcooked_simulator/game_content/layouts/split.layout
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/split.layout
@@ -1,7 +1,7 @@
 #QU#T###NLB#
 #__________M
 #____A_____#
-W__________#
+$__________#
 ############
 C__________#
 C_____A____#
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/test1.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/test1.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8ccd0a1152692fd1c9a0a911b0bb677bab98cb95
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/test1.layout
@@ -0,0 +1 @@
+____A___P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/test2.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/test2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8cfae98af3e7748e2df2dcdaae2ffffbe8c4a074
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/test2.layout
@@ -0,0 +1,9 @@
+_
+_
+_
+A
+_
+_
+_
+_
+P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/test3.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/test3.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8847c681b18500df23abe068dac76fb4dfbdd6d2
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/test3.layout
@@ -0,0 +1,4 @@
+___
+_A_
+___
+__P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/test4.layout b/cooperative_cuisine/configs/layouts_archive/test_layouts/test4.layout
new file mode 100644
index 0000000000000000000000000000000000000000..09d7551fa64358eb61a97d4c562617dca82b9655
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts_archive/test_layouts/test4.layout
@@ -0,0 +1,3 @@
+____
+_A__
+___P
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/study/level1/level1_config.yaml b/cooperative_cuisine/configs/study/level1/level1_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7dad55acb1a62c84628b0b4b29411c4483a5fc95
--- /dev/null
+++ b/cooperative_cuisine/configs/study/level1/level1_config.yaml
@@ -0,0 +1,179 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 0
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 300
+  undo_dispenser_pickup: true
+
+meals:
+  all: false
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - Salad
+    - TomatoSoup
+
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: 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
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  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:cooperative_cuisine.orders.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 6
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+  serving_not_ordered_meals: true
+  # can meals that are not ordered be served / dropped on the serving window
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 6
+  interaction_range: 1.6
+  restricted_view: False
+  view_angle: 70
+  view_range: 4  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  # # ---------------  Scoring  ---------------
+  orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ completed_order ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 20
+        score_on_specific_kwarg: meal_name
+        score_map:
+          Burger: 15
+          OnionSoup: 10
+          Salad: 5
+          TomatoSoup: 10
+  not_ordered_meals:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ serve_not_ordered_meal ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 2
+  trashcan_usages:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ trashcan_usage ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -5
+  expired_orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_expired ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -10
+  # # --------------- Recording ---------------
+  #  json_states:
+  #    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+  #      callback_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+#  info_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ cutting_board_100 ]
+#      log_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Glückwunsch du hast was geschnitten!
+#  fire_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ new_fire ]
+#      log_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Feuer, Feuer, Feuer
+#        level: Warning
diff --git a/overcooked_simulator/game_content/item_info.yaml b/cooperative_cuisine/configs/study/level1/level1_item_info.yaml
similarity index 71%
rename from overcooked_simulator/game_content/item_info.yaml
rename to cooperative_cuisine/configs/study/level1/level1_item_info.yaml
index a6458c6329cbf0323e6fb59338f34e0739c9786e..1266f61ebd611cd5c2a9097b1be9dd7eff65b7f7 100644
--- a/overcooked_simulator/game_content/item_info.yaml
+++ b/cooperative_cuisine/configs/study/level1/level1_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/cooperative_cuisine/configs/study/level2/level2_config.yaml b/cooperative_cuisine/configs/study/level2/level2_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..918a653361e4e3ef1d3c1eef99319dcd44de4288
--- /dev/null
+++ b/cooperative_cuisine/configs/study/level2/level2_config.yaml
@@ -0,0 +1,179 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 0
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 300
+  undo_dispenser_pickup: true
+
+
+meals:
+  all: false
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - Pizza
+
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: 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
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  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:cooperative_cuisine.orders.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 6
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+  serving_not_ordered_meals: true
+  # can meals that are not ordered be served / dropped on the serving window
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 6
+  interaction_range: 1.6
+  restricted_view: True
+  view_angle: 100
+  view_range: 3.2  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  # # ---------------  Scoring  ---------------
+  orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ completed_order ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 20
+        score_on_specific_kwarg: meal_name
+        score_map:
+          Burger: 15
+          OnionSoup: 10
+          Salad: 5
+          TomatoSoup: 10
+  not_ordered_meals:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ serve_not_ordered_meal ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 2
+  trashcan_usages:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ trashcan_usage ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -5
+  expired_orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_expired ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -10
+  # # --------------- Recording ---------------
+  #  json_states:
+  #    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+  #      callback_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+#  info_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ cutting_board_100 ]
+#      log_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Glückwunsch du hast was geschnitten!
+#  fire_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ new_fire ]
+#      log_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Feuer, Feuer, Feuer
+#        level: Warning
diff --git a/cooperative_cuisine/configs/study/level2/level2_item_info.yaml b/cooperative_cuisine/configs/study/level2/level2_item_info.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c924241dcd60f6fb1d7ef79ebaf3462719eae491
--- /dev/null
+++ b/cooperative_cuisine/configs/study/level2/level2_item_info.yaml
@@ -0,0 +1,232 @@
+CuttingBoard:
+  type: Equipment
+
+Sink:
+  type: Equipment
+
+Stove:
+  type: Equipment
+
+DeepFryer:
+  type: Equipment
+
+Oven:
+  type: Equipment
+
+Pot:
+  type: Equipment
+  equipment: Stove
+
+Pan:
+  type: Equipment
+  equipment: Stove
+
+Basket:
+  type: Equipment
+  equipment: DeepFryer
+
+Peel:
+  type: Equipment
+  equipment: Oven
+
+DirtyPlate:
+  type: Equipment
+
+Plate:
+  type: Equipment
+  needs: [ DirtyPlate ]
+  seconds: 2.0
+  equipment: Sink
+
+# --------------------------------------------------------------------------------
+
+Tomato:
+  type: Ingredient
+
+Lettuce:
+  type: Ingredient
+
+Onion:
+  type: Ingredient
+
+Meat:
+  type: Ingredient
+
+Bun:
+  type: Ingredient
+
+Potato:
+  type: Ingredient
+
+Fish:
+  type: Ingredient
+
+Dough:
+  type: Ingredient
+
+Cheese:
+  type: Ingredient
+
+Sausage:
+  type: Ingredient
+
+ChoppedTomato:
+  type: Ingredient
+  needs: [ Tomato ]
+  seconds: 3.0
+  equipment: CuttingBoard
+
+ChoppedLettuce:
+  type: Ingredient
+  needs: [ Lettuce ]
+  seconds: 3.0
+  equipment: CuttingBoard
+
+ChoppedOnion:
+  type: Ingredient
+  needs: [ Onion ]
+  seconds: 5.0
+  equipment: CuttingBoard
+
+RawPatty:
+  type: Ingredient
+  needs: [ Meat ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+RawChips:
+  type: Ingredient
+  needs: [ Potato ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedFish:
+  type: Ingredient
+  needs: [ Fish ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+PizzaBase:
+  type: Ingredient
+  needs: [ Dough ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+GratedCheese:
+  type: Ingredient
+  needs: [ Cheese ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedSausage:
+  type: Ingredient
+  needs: [ Sausage ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+CookedPatty:
+  type: Ingredient
+  seconds: 5.0
+  needs: [ RawPatty ]
+  equipment: Pan
+
+# --------------------------------------------------------------------------------
+
+Chips:
+  type: Meal
+  seconds: 5.0
+  needs: [ RawChips ]
+  equipment: Basket
+
+FriedFish:
+  type: Meal
+  seconds: 5.0
+  needs: [ ChoppedFish ]
+  equipment: Basket
+
+Burger:
+  type: Meal
+  needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
+  equipment: ~
+
+Salad:
+  type: Meal
+  needs: [ ChoppedLettuce, ChoppedTomato ]
+  equipment: ~
+
+TomatoSoup:
+  type: Meal
+  needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
+  seconds: 6.0
+  equipment: Pot
+
+OnionSoup:
+  type: Meal
+  needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
+  seconds: 6.0
+  equipment: Pot
+
+FishAndChips:
+  type: Meal
+  needs: [ FriedFish, Chips ]
+  equipment: ~
+
+Pizza:
+  type: Meal
+  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/cooperative_cuisine/configs/study/study_config.yaml b/cooperative_cuisine/configs/study/study_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e20ee61ff83613bd758ea6d6b5406ed2b640067b
--- /dev/null
+++ b/cooperative_cuisine/configs/study/study_config.yaml
@@ -0,0 +1,23 @@
+# Config paths are relative to configs folder.
+# Layout files are relative to layouts folder.
+
+
+levels:
+  - config_path: study/level1/level1_config.yaml
+    layout_path: overcooked-1/1-1-far-apart.layout
+    item_info_path: study/level1/level1_item_info.yaml
+    name: "Level 1-1: Far Apart"
+
+  - config_path: environment_config.yaml
+    layout_path: basic.layout
+    item_info_path: item_info.yaml
+    name: "Basic"
+
+  - config_path: study/level2/level2_config.yaml
+    layout_path: overcooked-1/1-4-bottleneck.layout
+    item_info_path: study/level2/level2_item_info.yaml
+    name: "Level 1-4: Bottleneck"
+
+
+num_players: 1
+num_bots: 0
diff --git a/cooperative_cuisine/configs/tutorial_env_config.yaml b/cooperative_cuisine/configs/tutorial_env_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1a41ba318239c52e435ae32ad9bdd34d34de6c6e
--- /dev/null
+++ b/cooperative_cuisine/configs/tutorial_env_config.yaml
@@ -0,0 +1,177 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 0
+  plate_delay: [ 5, 10 ]
+  return_dirty: false
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 90000
+
+meals:
+  all: false
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - Salad
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: 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
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  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:cooperative_cuisine.orders.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 0
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+  serving_not_ordered_meals: true
+  # can meals that are not ordered be served / dropped on the serving window
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 6
+  interaction_range: 1.6
+  restricted_view: False
+  view_angle: 70
+  view_range: 4  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  # # ---------------  Scoring  ---------------
+  orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ completed_order ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 20
+        score_on_specific_kwarg: meal_name
+        score_map:
+          Burger: 15
+          OnionSoup: 10
+          Salad: 5
+          TomatoSoup: 10
+  not_ordered_meals:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ serve_not_ordered_meal ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 2
+  trashcan_usages:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ trashcan_usage ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -5
+  expired_orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_expired ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -10
+  # # --------------- Recording ---------------
+  #  json_states:
+  #    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+  #      callback_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+#  info_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ cutting_board_100 ]
+#      log_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Glückwunsch du hast was geschnitten!
+#  fire_msg:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ new_fire ]
+#      log_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Feuer, Feuer, Feuer
+#        level: Warning
diff --git a/cooperative_cuisine/counter_factory.py b/cooperative_cuisine/counter_factory.py
new file mode 100644
index 0000000000000000000000000000000000000000..2dc90d33f9ef70c837f042b1e991304f732bfe6a
--- /dev/null
+++ b/cooperative_cuisine/counter_factory.py
@@ -0,0 +1,386 @@
+"""
+The `CounterFactory` initializes the `Counter` classes from the characters in the layout config.
+The mapping depends on the definition in the `environment_config.yml` in the `layout_chars` section.
+
+```yaml
+layout_chars:
+  _: Free
+  hash: Counter
+  A: Agent
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  W: ServingWindow
+  S: Sink
+  +: SinkAddon
+  U: Pot  # with Stove
+  Q: Pan  # with Stove
+  O: Peel  # with Oven
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  G: Sausage  # sausaGe
+  B: Bun
+  M: Meat
+```
+
+# Code Documentation
+"""
+import inspect
+import sys
+from random import Random
+from typing import Any, Type, TypeVar
+
+import numpy as np
+import numpy.typing as npt
+
+from cooperative_cuisine.counters import (
+    Counter,
+    CookingCounter,
+    Dispenser,
+    ServingWindow,
+    CuttingBoard,
+    PlateDispenser,
+    Sink,
+    PlateConfig,
+    SinkAddon,
+    Trashcan,
+)
+from cooperative_cuisine.effect_manager import EffectManager
+from cooperative_cuisine.game_items import (
+    ItemInfo,
+    ItemType,
+    CookingEquipment,
+    Plate,
+    Item,
+)
+from cooperative_cuisine.hooks import Hooks
+from cooperative_cuisine.orders import OrderManager
+from cooperative_cuisine.utils import get_closest
+
+T = TypeVar("T")
+
+
+def convert_words_to_chars(layout_chars_config: dict[str, str]) -> dict[str, str]:
+    """Converts words in a given layout chars configuration dictionary to their corresponding characters.
+
+    This is useful for characters that can not be keys in a yaml file. For example, `#` produces a comment.
+    Therefore, you can specify `hash` as a key (`hash: Counter`). `word_refs` defines the conversions. *Click on `▶ View Source`.*
+
+    Args:
+        layout_chars_config: A dictionary containing layout character configurations, where the keys are words
+            representing layout characters and the values are the corresponding character representations.
+
+    Returns:
+        A dictionary where the keys are the layout characters and the values are their corresponding words.
+    """
+    word_refs = {
+        "hash": "#",
+        # "space": " ",
+        "dot": ".",
+        "comma": ",",
+        # "semicolon": ";",
+        "colon": ":",
+        "minus": "-",
+        "exclamation": "!",
+        "question": "?",
+        "dquote": '"',
+        "squote": "'",
+        "star": "*",
+        "ampersand": "&",
+        "equal": "=",
+        "right": ">",
+        "left": "<",
+        "pipe": "|",
+        "at": "@",
+        "wave": "~",  # ~ is None / null in yaml
+        "ocurlybracket": "{",
+        "ccurlybracket": "}",
+        "osquarebracket": "[",
+        "csquarebracket": "]",
+    }
+    return {word_refs.get(c, c): name for c, name in layout_chars_config.items()}
+
+
+class CounterFactory:
+    """The `CounterFactory` class is responsible for creating counter objects based on the layout configuration and
+    item information provided. It also provides methods for mapping and filtering the item information.
+    """
+
+    additional_counter_names = {"Counter"}
+
+    def __init__(
+        self,
+        layout_chars_config: dict[str, str],
+        item_info: dict[str, ItemInfo],
+        serving_window_additional_kwargs: dict[str, Any],
+        plate_config: PlateConfig,
+        order_manager: OrderManager,
+        effect_manager_config: dict,
+        undo_dispenser_pickup: bool,
+        hook: Hooks,
+        random: Random,
+    ) -> None:
+        """Constructor for the `CounterFactory` class. Set up the attributes necessary to instantiate the counters.
+
+        Initializes the object with the provided parameters. It performs the following tasks:
+        - Converts the layout character configuration from words to characters.
+        - Sets the item information dictionary.
+        - Sets the additional keyword arguments for serving window configuration.
+        - Sets the plate configuration.
+
+        Args:
+            layout_chars_config: A dictionary mapping layout characters to their corresponding names.
+            item_info: A dictionary containing information about different items.
+            serving_window_additional_kwargs: Additional keyword arguments for serving window configuration.
+            plate_config: The configuration for plate usage.
+        """
+        self.layout_chars_config: dict[str, str] = convert_words_to_chars(
+            layout_chars_config
+        )
+        """Layout chars to the counter names."""
+        self.item_info: dict[str, ItemInfo] = item_info
+        """All item infos from the `item_info` config."""
+        self.serving_window_additional_kwargs: dict[
+            str, Any
+        ] = serving_window_additional_kwargs
+        """The additional keyword arguments for the serving window."""
+        self.plate_config: PlateConfig = plate_config
+        """The plate config from the `environment_config`"""
+        self.order_manager: OrderManager = order_manager
+        """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
+            for c, name in self.layout_chars_config.items()
+            if name in ["Agent", "Free"]
+        )
+        """A set of characters that represent counters for agents or free spaces."""
+
+        self.counter_classes: dict[str, Type] = dict(
+            filter(
+                lambda k: issubclass(k[1], Counter),
+                inspect.getmembers(
+                    sys.modules["cooperative_cuisine.counters"], inspect.isclass
+                ),
+            )
+        )
+        """A dictionary of counter classes imported from the 'cooperative_cuisine.counters' module."""
+
+        self.cooking_counter_equipments: dict[str, set[str]] = {
+            cooking_counter: {
+                equipment
+                for equipment, e_info in self.item_info.items()
+                if e_info.equipment and e_info.equipment.name == cooking_counter
+            }
+            for cooking_counter, info in self.item_info.items()
+            if info.type == ItemType.Equipment and info.equipment is None
+        }
+        """A dictionary mapping cooking counters to the list of equipment items associated with them."""
+
+        self.undo_dispenser_pickup = undo_dispenser_pickup
+        """Put back ingredients of the same type on a dispenser."""
+
+        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:
+                if item_info.equipment.name not in self.counter_classes:
+                    return CookingCounter(
+                        name=item_info.equipment.name,
+                        equipments=self.cooking_counter_equipments[
+                            item_info.equipment.name
+                        ],
+                        pos=pos,
+                        occupied_by=CookingEquipment(
+                            name=item_info.name,
+                            item_info=item_info,
+                            transitions=self.filter_item_info(
+                                by_equipment_name=item_info.name,
+                                add_effects=True,
+                            ),
+                        ),
+                        hook=self.hook,
+                    )
+            elif item_info.type == ItemType.Ingredient:
+                return Dispenser(
+                    pos=pos,
+                    hook=self.hook,
+                    dispensing=item_info,
+                    undo_dispenser_pickup=self.undo_dispenser_pickup,
+                )
+            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:
+            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__,
+                add_effects=True,
+            )
+        elif issubclass(counter_class, PlateDispenser):
+            kwargs.update(
+                {
+                    "plate_transitions": self.filter_item_info(
+                        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):
+            kwargs.update(self.serving_window_additional_kwargs)
+        if issubclass(counter_class, (ServingWindow, Trashcan)):
+            kwargs[
+                "order_manager"
+            ] = self.order_manager  # individual because for the later trash scorer
+        return counter_class(**kwargs)
+
+    def can_map(self, char) -> bool:
+        """Check if the provided character can be mapped to a counter object."""
+        return char in self.layout_chars_config and (
+            not self.is_counter(char)
+            or self.layout_chars_config[char] in self.item_info
+            or self.layout_chars_config[char] in self.counter_classes
+        )
+
+    def is_counter(self, c: str) -> bool:
+        """Checks if the provided character represents a counter."""
+        return c in self.layout_chars_config and c not in self.no_counter_chars
+
+    def map_not_counter(self, c: str) -> str:
+        """Maps the provided character to a non-counter word based on the layout configuration."""
+        assert self.can_map(c) and not self.is_counter(
+            c
+        ), "Cannot map char {c} as a 'not counter'"
+        return self.layout_chars_config[c]
+
+    def filter_item_info(
+        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:
+            filtered = {
+                name: info
+                for name, info in self.item_info.items()
+                if info.type == by_item_type
+            }
+        if by_equipment_name is not None:
+            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]):
+        """Initialize the counters in the environment.
+
+        Connect the `ServingWindow`(s) with the `PlateDispenser`.
+        Find and connect the `SinkAddon`s with the `Sink`s
+
+        Args:
+            counters: list of counters to perform the post setup on.
+        """
+        plate_dispenser = self.get_counter_of_type(PlateDispenser, counters)
+        assert len(plate_dispenser) > 0, "No Plate Dispenser in the environment"
+
+        sink_addons = self.get_counter_of_type(SinkAddon, counters)
+
+        for counter in counters:
+            match counter:
+                case ServingWindow():
+                    counter: ServingWindow  # Pycharm type checker does now work for match statements?
+                    counter.add_plate_dispenser(plate_dispenser[0])
+                case Sink(pos=pos):
+                    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.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."""
+        return list(filter(lambda counter: isinstance(counter, counter_type), counters))
diff --git a/overcooked_simulator/counters.py b/cooperative_cuisine/counters.py
similarity index 50%
rename from overcooked_simulator/counters.py
rename to cooperative_cuisine/counters.py
index f63d77aac4d9a2ae6a146b164051c23a4ca3e90f..ff0c420dc192ae7b4bb448f5b80fd5dc0a52748a 100644
--- a/overcooked_simulator/counters.py
+++ b/cooperative_cuisine/counters.py
@@ -3,8 +3,8 @@ what should happen when the agent wants to pick something up from the counter. O
 the `Counter.drop_off` method receives the item what should be put on the counter. Before that the
 `Counter.can_drop_off` method checked if the item can be put on the counter. The progress on Counters or on objects
 on the counters are handled via the Counters. They have the task to delegate the progress call via the `progress`
-method, e.g., the `CuttingBoard.progress`. On which type of counter the progress method is called is currently
-defined in the environment class.
+method, e.g., the `CuttingBoard.progress`. The environment class detects which classes in this module have the
+`progress` method defined and on instances of these classes the progress will be delegated.
 
 Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the
 interaction/requirements for the interaction.
@@ -18,6 +18,7 @@ interaction/requirements for the interaction.
     Stove:
       type: Equipment
 
+
 The defined counter classes are:
 - `Counter`
 - `CuttingBoard`
@@ -39,47 +40,53 @@ import uuid
 from collections import deque
 from collections.abc import Iterable
 from datetime import datetime, timedelta
-from typing import TYPE_CHECKING, Optional, Callable, TypedDict
+from random import Random
+from typing import TYPE_CHECKING, Optional, Callable, Set
+
+from cooperative_cuisine.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,
+    DROP_OFF_ON_COOKING_EQUIPMENT,
+)
 
 if TYPE_CHECKING:
-    from overcooked_simulator.overcooked_environment import (
-        OrderAndScoreManager,
+    from cooperative_cuisine.effect_manager import Effect
+    from cooperative_cuisine.environment import (
+        OrderManager,
     )
 
 import numpy as np
 import numpy.typing as npt
 
-from overcooked_simulator.game_items import (
+from cooperative_cuisine.game_items import (
     Item,
     CookingEquipment,
     Plate,
     ItemInfo,
+    EffectType,
 )
 
 
 log = logging.getLogger(__name__)
+"""The logger for this module."""
 
 COUNTER_CATEGORY = "Counter"
-
-
-class TransitionsValueDict(TypedDict):
-    """The values in the transitions dicts of the `CookingEquipment`."""
-
-    seconds: int | float
-    """The needed seconds to progress for the transition."""
-    needs: list[str]
-    """The names of the needed items for the transition."""
-    info: ItemInfo | str
-    """The ItemInfo of the resulting item."""
-
-
-class TransitionsValueByNameDict(TypedDict):
-    """The values in the transitions dicts of the `CuttingBoard` and the `Sink`."""
-
-    seconds: int | float
-    """The needed seconds to progress for the transition."""
-    result: str
-    """The new name of the item after the transition."""
+"""The string for the `category` value in the json state representation for all counters."""
 
 
 class Counter:
@@ -91,8 +98,10 @@ class Counter:
     def __init__(
         self,
         pos: npt.NDArray[float],
+        hook: Hooks,
         occupied_by: Optional[Item] = None,
         uid: hex = None,
+        **kwargs,
     ):
         """Constructor setting the arguments as attributes.
 
@@ -100,33 +109,70 @@ class Counter:
             pos: Position of the counter in the environment. 2-element vector.
             occupied_by: The item on top of the counter.
         """
-        self.uuid = uuid.uuid4().hex if uid is None else None
+        self.uuid: str = uuid.uuid4().hex if uid is None else None
+        """A unique id for better tracking in GUIs with assets which instance moved or changed."""
         self.pos: npt.NDArray[float] = pos
+        """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):
+    def occupied(self) -> bool:
+        """Is something on top of the counter."""
         return self.occupied_by is not None
 
-    def pick_up(self, on_hands: bool = True) -> Item | 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, player: str = "0") -> 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.
 
         Args:
             on_hands: Will the item be put on empty hands or on a cooking equipment.
+            player: The player name that tries to pick up from the 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, player=player)
         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:
@@ -135,46 +181,84 @@ class Counter:
 
         Args:
             item: The item for which to check, if it can be placed on the counter.
+            player: The player name that tries to drop something on the counter.
+
 
         Returns: True if the item can be placed on the counter, False if not.
 
         """
         return self.occupied_by is None or self.occupied_by.can_combine(item)
 
-    def drop_off(self, item: Item) -> Item | None:
+    def drop_off(self, item: Item, player: str = "0") -> Item | None:
         """Takes the thing dropped of by the player.
 
         Args:
             item: The item to be placed on the counter.
 
+
         Returns:
             Item or None what should be put back on the players hand, e.g., the cooking equipment.
         """
         if self.occupied_by is None:
             self.occupied_by = item
         elif self.occupied_by.can_combine(item):
+            self.hook(
+                DROP_OFF_ON_COOKING_EQUIPMENT,
+                item=item,
+                equipment=self.occupied_by,
+                counter=self,
+                player=player,
+            )
             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 {
             "id": self.uuid,
             "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 (
@@ -182,30 +266,34 @@ 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],
         }
 
 
 class CuttingBoard(Counter):
-    """Cutting ingredients on. The requirement in a new object could look like
+    """Cutting ingredients on. The requirement in a new object could look like.
+
+        ChoppedTomato:
+          type: Ingredient
+          needs: [ Tomato ]
+          seconds: 4.0
+          equipment: CuttingBoard
 
-    ```yaml
-    ChoppedTomato:
-      type: Ingredient
-      needs: [ Tomato ]
-      seconds: 4.0
-      equipment: CuttingBoard
-    ```
     The character `C` in the `layout` file represents the CuttingBoard.
     """
 
-    def __init__(
-        self, pos: np.ndarray, transitions: dict[str, TransitionsValueByNameDict]
-    ):
-        self.progressing = False
-        self.transitions = transitions
-        super().__init__(pos=pos)
+    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."""
+        self.inverted_transition_dict: dict[str, ItemInfo] = {
+            info.needs[0]: info for name, info in self.transitions.items()
+        }
+        """For faster accessing the needed item. Keys are the ingredients that the player can put and chop on the 
+        board."""
+        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:
@@ -218,47 +306,40 @@ class CuttingBoard(Counter):
         """
         if (
             self.occupied
-            and self.progressing
-            and self.occupied_by.name in self.transitions
+            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()
-                / self.transitions[self.occupied_by.name]["seconds"]
+                / self.inverted_transition_dict[self.occupied_by.name].seconds
             )
             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.transitions[self.occupied_by.name][
-                    "result"
-                ]
-
-    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.occupied_by.name = self.inverted_transition_dict[
+                    self.occupied_by.name
+                ].name
+                self.hook(CUTTING_BOARD_100, counter=self)
 
 
 class ServingWindow(Counter):
     """The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for
-    the info about a plate outside of the kitchen.
+    the info about a plate outside the kitchen.
 
     All items in the `item_info.yml` with the type meal are considered to be servable, if they are ordered. Not
     ordered meals can also be served, if a `serving_not_ordered_meals` function is set in the `environment_config.yml`.
@@ -270,33 +351,56 @@ class ServingWindow(Counter):
 
     def __init__(
         self,
-        pos: npt.NDArray[float],
-        order_and_score: OrderAndScoreManager,
+        order_manager: OrderManager,
         meals: set[str],
         env_time_func: Callable[[], datetime],
         plate_dispenser: PlateDispenser = None,
+        **kwargs,
     ):
-        self.order_and_score = order_and_score
-        self.plate_dispenser = plate_dispenser
-        self.meals = meals
-        self.env_time_func = env_time_func
-        super().__init__(pos=pos)
+        self.order_manager: OrderManager = order_manager
+        """Reference to the OrderAndScoreManager class. It determines which meals can be served and it manages the 
+        score."""
+        self.plate_dispenser: PlateDispenser = plate_dispenser
+        """Served meals are mentioned on the plate dispenser. So that the plate dispenser can add a dirty plate after 
+        some time."""
+        self.meals: set[str] = meals
+        """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__(**kwargs)
 
-    def drop_off(self, item) -> Item | None:
+    def drop_off(self, item, player: str = "0") -> Item | None:
         env_time = self.env_time_func()
-        if self.order_and_score.serve_meal(item=item, env_time=env_time):
+        self.hook(
+            PRE_SERVING,
+            counter=self,
+            item=item,
+            env_time=env_time,
+            player=player,
+        )
+        if self.order_manager.serve_meal(item=item, env_time=env_time, player=player):
             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, player=player)
         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)
         )
 
-    def pick_up(self, on_hands: bool = True) -> Item | None:
+    def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None:
         pass
 
     def add_plate_dispenser(self, plate_dispenser):
@@ -308,7 +412,7 @@ class Dispenser(Counter):
 
     At the moment all ingredients have an unlimited stock.
 
-    The character for each dispenser in the `layout` file is currently hard coded in the environment class:
+    The character for each dispenser in the `layout` file is defined in the `environment_config.yml`:
     ```yaml
     T: Tomato
     L: Lettuce
@@ -316,35 +420,48 @@ class Dispenser(Counter):
     B: Bun
     M: Meat
     ```
-    The plan is to put the info also in the config.
 
     In the implementation, an instance of the item to dispense is always on top of the dispenser.
     Which also is easier for the visualization of the dispenser.
     """
 
-    def __init__(self, pos: npt.NDArray[float], dispensing: ItemInfo):
-        self.dispensing = dispensing
+    def __init__(self, dispensing: ItemInfo, undo_dispenser_pickup: bool, **kwargs):
+        self.dispensing: ItemInfo = dispensing
+        """`ItemInfo` what the the Dispenser is dispensing. One ready object always is on top of the counter."""
+        self.undo_dispenser_pickup: bool = undo_dispenser_pickup
+        """Put back ingredients of the same type on a dispenser."""
         super().__init__(
-            pos=pos,
             occupied_by=self.create_item(),
+            **kwargs,
         )
 
-    def pick_up(self, on_hands: bool = True) -> Item | None:
+    def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None:
+        self.hook(PRE_DISPENSER_PICK_UP, counter=self, on_hands=on_hands, player=player)
         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,
+            player=player,
+        )
         return return_this
 
-    def drop_off(self, item: Item) -> Item | None:
+    def drop_off(self, item: Item, player: str = "0") -> Item | None:
         if self.occupied_by.can_combine(item):
             return self.occupied_by.combine(item)
 
     def can_drop_off(self, item: Item) -> bool:
-        return self.occupied_by.can_combine(item)
+        return self.occupied_by.can_combine(item) or (
+            self.undo_dispenser_pickup and item.name == self.dispensing.name
+        )
 
     def __repr__(self):
         return f"{self.dispensing.name}Dispenser"
 
     def create_item(self) -> Item:
+        """Create a new item to put on the dispenser after the previous one was picked up."""
         kwargs = {
             "name": self.dispensing.name,
             "item_info": self.dispensing,
@@ -367,6 +484,8 @@ class PlateConfig:
     """dirty plates at the start."""
     plate_delay: list[int, int] = dataclasses.field(default_factory=lambda: [5, 10])
     """The uniform sampling range for the plate delay between serving and return in seconds."""
+    return_dirty: bool = True
+    """Specifies if plates are returned dirty or clean to the plate dispenser."""
 
 
 class PlateDispenser(Counter):
@@ -387,31 +506,39 @@ class PlateDispenser(Counter):
 
     def __init__(
         self,
-        pos: npt.NDArray[float],
         dispensing: ItemInfo,
         plate_config: PlateConfig,
-        plate_transitions: dict,
+        plate_transitions: dict[str, ItemInfo],
+        random: Random,
         **kwargs,
     ) -> None:
-        super().__init__(pos=pos, **kwargs)
-        self.dispensing = dispensing
-        self.occupied_by = deque()
-        self.out_of_kitchen_timer = []
-        self.plate_config = plate_config
-        self.next_plate_time = datetime.max
-        self.plate_transitions: dict[str, TransitionsValueDict] = plate_transitions
+        super().__init__(**kwargs)
+        self.dispensing: ItemInfo = dispensing
+        """Plate ItemInfo."""
+        self.occupied_by: deque = deque()
+        """The queue of plates. New dirty ones are put at the end and therefore under the current plates."""
+        self.out_of_kitchen_timer: list[datetime] = []
+        """Internal timer for how many plates are out of kitchen and how long."""
+        self.plate_config: PlateConfig = plate_config
+        """The config how many plates are present in the kitchen at the beginning (and in total) and the config for 
+        the random "out of kitchen" timer."""
+        self.next_plate_time: datetime = datetime.max
+        """For efficient checking if dirty plates should be created, instead of looping through the 
+        `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:
+    def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None:
         if self.occupied_by:
             return self.occupied_by.pop()
 
     def can_drop_off(self, item: Item) -> bool:
         return not self.occupied_by or self.occupied_by[-1].can_combine(item)
 
-    def drop_off(self, item: Item) -> Item | None:
-        """At the moment items can be put on the top of the plate dispenser or the top plate if it is clean and can
-        be put on a plate."""
+    def drop_off(self, item: Item, player: str = "0") -> Item | None:
         if not self.occupied_by:
             self.occupied_by.append(item)
         elif self.occupied_by[-1].can_combine(item):
@@ -419,21 +546,25 @@ class PlateDispenser(Counter):
         return None
 
     def add_dirty_plate(self):
-        self.occupied_by.appendleft(self.create_item())
+        """Add a dirty plate after a timer is completed."""
+        self.occupied_by.appendleft(
+            self.create_item(clean=not self.plate_config.return_dirty)
+        )
 
     def update_plate_out_of_kitchen(self, env_time: datetime):
         """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."""
@@ -457,6 +588,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()
@@ -472,6 +604,11 @@ class PlateDispenser(Counter):
         return "PlateReturn"
 
     def create_item(self, clean: bool = False) -> Plate:
+        """Create a plate.
+
+        Args:
+            clean: Whether the plate is clean or dirty.
+        """
         kwargs = {
             "clean": clean,
             "transitions": self.plate_transitions,
@@ -486,17 +623,28 @@ class Trashcan(Counter):
     The character `X` in the `layout` file represents the Trashcan.
     """
 
-    def pick_up(self, on_hands: bool = True) -> Item | None:
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+    def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None:
         pass
 
-    def drop_off(self, item: Item) -> Item | None:
+    def drop_off(self, item: Item, player: str = "0") -> 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):
             item.reset_content()
+            item.reset()
             return item
+        self.hook(TRASHCAN_USAGE, counter=self, item=item, player=player)
         return None
 
     def can_drop_off(self, item: Item) -> bool:
-        return True
+        return item.name != "Extinguisher"
 
 
 class CookingCounter(Counter):
@@ -514,19 +662,18 @@ class CookingCounter(Counter):
     def __init__(
         self,
         name: str,
-        cooking_counter_equipments: dict[str, list[str]],
+        equipments: set[str],
         **kwargs,
     ):
-        self.name = name
-        self.cooking_counter_equipments = cooking_counter_equipments
+        self.name: str = name
+        """The type/name of the cooking counter, e.g., Stove, DeepFryer, Oven."""
+        self.equipments: set[str] = equipments
+        """The valid equipment for the cooking counter, e.g., for a Stove {'Pot','Pan'}."""
         super().__init__(**kwargs)
 
     def can_drop_off(self, item: Item) -> bool:
         if self.occupied_by is None:
-            return (
-                isinstance(item, CookingEquipment)
-                and item.name in self.cooking_counter_equipments[self.name]
-            )
+            return isinstance(item, CookingEquipment) and item.name in self.equipments
         else:
             return self.occupied_by.can_combine(item)
 
@@ -535,8 +682,12 @@ class CookingCounter(Counter):
         if (
             self.occupied_by
             and isinstance(self.occupied_by, CookingEquipment)
-            and self.occupied_by.name in self.cooking_counter_equipments[self.name]
+            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)
 
@@ -564,80 +715,86 @@ class Sink(Counter):
 
     def __init__(
         self,
-        pos: npt.NDArray[float],
-        transitions: dict[str, TransitionsValueByNameDict],
+        transitions: dict[str, ItemInfo],
         sink_addon: SinkAddon = None,
+        **kwargs,
     ):
-        super().__init__(pos=pos)
-        self.progressing = False
+        super().__init__(**kwargs)
         self.sink_addon: SinkAddon = sink_addon
         """The connected sink addon which will receive the clean plates"""
-        self.occupied_by = deque()
+        self.occupied_by: deque[Plate] = deque()
         """The queue of dirty plates. Only the one on the top is progressed."""
         self.transitions = transitions
         """The allowed transitions for the items in the sink. Here only clean plates transfer from dirty plates."""
+        self.transition_needs: Set[str] = set()
+        """Set of all first needs of the transition item info."""
+
+        for name, info in transitions.items():
+            assert (
+                len(info.needs) >= 1
+            ), "transitions in a Sink need at least one item need."
+            self.transition_needs.update([info.needs[0]])
 
     @property
-    def occupied(self):
+    def occupied(self) -> bool:
+        """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.transitions
-        ):
-            percent = (
-                passed_time.total_seconds()
-                / self.transitions[self.occupied_by[-1].name]["seconds"]
+            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
             )
-            self.occupied_by[-1].progress(
-                equipment=self.__class__.__name__, percent=percent
+            and not any(
+                e.item_info.effect_type == EffectType.Unusable
+                for e in self.sink_addon.active_effects
             )
-            if self.occupied_by[-1].progress_percentage == 1.0:
-                self.occupied_by[-1].reset()
-                self.occupied_by[-1].name = self.transitions[self.occupied_by[-1].name][
-                    "result"
-                ]
-                plate = self.occupied_by.pop()
-                plate.clean = True
-                self.sink_addon.add_clean_plate(plate)
-
-    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()
+            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:
+                    percent = passed_time.total_seconds() / info.seconds
+                    self.occupied_by[-1].progress(
+                        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()
+                        plate.clean = True
+                        self.sink_addon.add_clean_plate(plate)
+                    break
 
     def can_drop_off(self, item: Item) -> bool:
         return isinstance(item, Plate) and not item.clean
 
-    def drop_off(self, item: Item) -> Item | None:
+    def drop_off(self, item: Plate, player: str = "0") -> Item | None:
         self.occupied_by.appendleft(item)
+        self.hook(ADDED_PLATE_TO_SINK, counter=self, item=item, player=player)
         return None
 
-    def pick_up(self, on_hands: bool = True) -> Item | None:
+    def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None:
         return None
 
     def set_addon(self, sink_addon: SinkAddon):
+        """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`
@@ -647,20 +804,24 @@ 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([occupied_by]) if occupied_by else deque()
+        self.occupied_by: deque = deque([occupied_by]) if occupied_by else deque()
+        """The stack of clean plates."""
 
     def can_drop_off(self, item: Item) -> bool:
         return self.occupied_by and self.occupied_by[-1].can_combine(item)
 
-    def drop_off(self, item: Item) -> Item | None:
+    def drop_off(self, item: Item, player: str = "0") -> Item | None:
+        self.hook(DROP_ON_SINK_ADDON, counter=self, item=item, player=player)
         return self.occupied_by[-1].combine(item)
 
     def add_clean_plate(self, plate: Plate):
+        """Called from the `Sink` after a plate is cleaned / the progress is complete."""
         self.occupied_by.appendleft(plate)
 
-    def pick_up(self, on_hands: bool = True) -> Item | None:
+    def pick_up(self, on_hands: bool = True, player: str = "0") -> Item | None:
         if self.occupied_by:
+            self.hook(PICK_UP_FROM_SINK_ADDON, player=player)
             return self.occupied_by.pop()
diff --git a/cooperative_cuisine/effect_manager.py b/cooperative_cuisine/effect_manager.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fb36740e748f9662574955c5d2190f4f9cb9e45
--- /dev/null
+++ b/cooperative_cuisine/effect_manager.py
@@ -0,0 +1,175 @@
+"""
+Until now, only the fire effect is managed here.
+
+Fire spreads after some time to other counter.
+
+"""
+
+from __future__ import annotations
+
+from collections import deque
+from datetime import timedelta, datetime
+from random import Random
+from typing import TYPE_CHECKING, Tuple
+
+from cooperative_cuisine.game_items import (
+    ItemInfo,
+    Item,
+    ItemType,
+    Effect,
+    CookingEquipment,
+)
+from cooperative_cuisine.hooks import Hooks, NEW_FIRE, FIRE_SPREADING
+from cooperative_cuisine.utils import get_touching_counters, find_item_on_counters
+
+if TYPE_CHECKING:
+    from cooperative_cuisine.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):
+    """A class representing a manager for fire effects."""
+
+    def __init__(
+        self,
+        spreading_duration: list[float],
+        fire_burns_ingredients_and_meals: bool,
+        **kwargs,
+    ):
+        super().__init__(**kwargs)
+        self.spreading_duration: list[float] = spreading_duration
+        """A list of two floats representing the minimum and maximum duration for the fire effect to spread."""
+        self.fire_burns_ingredients_and_meals: bool = fire_burns_ingredients_and_meals
+        """A boolean indicating whether the fire burns ingredients and meals."""
+        self.effect_to_timer: dict[str:datetime] = {}
+        """A dictionary mapping effect uuids to their corresponding timers."""
+        self.next_finished_timer = datetime.max
+        """A datetime representing the time when the next effect will finish."""
+        self.active_effects: list[Tuple[Effect, Item | Counter]] = []
+        """A list of tuples representing the active effects and their target items or counters."""
+
+    def progress(self, passed_time: timedelta, now: datetime):
+        """Check if new effects occurred and apply them and check if fire spread and then create new fire effects.
+
+        Args:
+            passed_time: A timedelta representing the amount of time that has passed since the last progress update.
+            now: A datetime representing the current time.
+
+        """
+        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):
+        """
+        Apply an effect to a target item or counter.
+
+        Args:
+            effect: The effect to apply.
+            target: The target item or counter to apply the effect to.
+
+        """
+        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):
+        """Add the prefix "Burnt" to the content name.
+
+        Args:
+            content (Item): The content to be burned.
+        """
+        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):
+        """Remove the fire effect from a target item or counter.
+
+        Args:
+            effect: The effect to be removed.
+            target: The item or counter associated with the effect.
+
+        """
+        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/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b289e15d3ad9723d593deeab3be66f8173cb057
--- /dev/null
+++ b/cooperative_cuisine/environment.py
@@ -0,0 +1,967 @@
+from __future__ import annotations
+
+import dataclasses
+import inspect
+import json
+import logging
+import os
+import sys
+from collections import defaultdict
+from concurrent.futures import ThreadPoolExecutor
+from datetime import timedelta, datetime
+from enum import Enum
+from pathlib import Path
+from random import Random
+from typing import Literal, TypedDict, Callable, Tuple
+
+import networkx
+import numpy as np
+import numpy.typing as npt
+import yaml
+from networkx import DiGraph
+from scipy.spatial import distance_matrix
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.counter_factory import CounterFactory
+from cooperative_cuisine.counters import (
+    Counter,
+    PlateConfig,
+)
+from cooperative_cuisine.effect_manager import EffectManager
+from cooperative_cuisine.game_items import (
+    ItemInfo,
+    ItemType,
+)
+from cooperative_cuisine.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,
+    POST_STEP,
+)
+from cooperative_cuisine.orders import (
+    OrderManager,
+    OrderConfig,
+)
+from cooperative_cuisine.player import Player, PlayerConfig
+from cooperative_cuisine.state_representation import InfoMsg
+from cooperative_cuisine.utils import create_init_env_time, get_closest
+
+log = logging.getLogger(__name__)
+
+
+PREVENT_SQUEEZING_INTO_OTHER_PLAYERS = True
+
+
+class ActionType(Enum):
+    """The 3 different types of valid actions. They can be extended via the `Action.action_data` attribute."""
+
+    MOVEMENT = "movement"
+    """move the agent."""
+    PUT = "pickup"
+    """interaction type 1, e.g., for pickup or drop off. Maybe other words: transplace?"""
+    # TODO change value to put
+    INTERACT = "interact"
+    """interaction type 2, e.g., for progressing. Start and stop interaction via `keydown` and `keyup` actions."""
+
+
+class InterActionData(Enum):
+    """The data for the interaction action: `ActionType.MOVEMENT`."""
+
+    START = "keydown"
+    "start an interaction."
+    STOP = "keyup"
+    "stop an interaction without moving away."
+
+
+@dataclasses.dataclass
+class Action:
+    """Action class, specifies player, action type and action itself."""
+
+    player: str
+    """Id of the player."""
+    action_type: ActionType
+    """Type of the action to perform. Defines what action data is valid."""
+    action_data: npt.NDArray[float] | InterActionData | Literal["pickup"]
+    """Data for the action, e.g., movement vector or start and stop interaction."""
+    duration: float | int = 0
+    """Duration of the action (relevant for movement)"""
+
+    def __repr__(self):
+        return f"Action({self.player},{self.action_type.value},{self.action_data},{self.duration})"
+
+    def __post_init__(self):
+        if isinstance(self.action_type, str):
+            self.action_type = ActionType(self.action_type)
+        if isinstance(self.action_data, str) and self.action_data != "pickup":
+            self.action_data = InterActionData(self.action_data)
+
+
+# TODO Abstract base class for different environments
+
+
+class EnvironmentConfig(TypedDict):
+    plates: PlateConfig
+    game: dict[
+        Literal["time_limit_seconds"] | Literal["undo_dispenser_pickup"], int | bool
+    ]
+    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.
+
+    Handles player movement, collision-detection, counters, cooking processes, recipes, incoming orders, time.
+    """
+
+    PAUSED = None
+
+    def __init__(
+        self,
+        env_config: Path | str,
+        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.score: float = 0.0
+        """The current score of the environment."""
+
+        self.players: dict[str, Player] = {}
+        """the player, keyed by their id/name."""
+
+        self.as_files = as_files
+        """Are the configs just the path to the files."""
+        if self.as_files:
+            with open(env_config, "r") as file:
+                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(
+                [
+                    item
+                    for item, info in self.item_info.items()
+                    if info.type == ItemType.Meal
+                ]
+            )
+        else:
+            self.allowed_meal_names = set(self.environment_config["meals"]["list"])
+            """The allowed meals depend on the `environment_config.yml` configured behaviour. Either all meals that 
+            are possible or only a limited subset."""
+
+        self.order_manager = OrderManager(
+            order_config=self.environment_config["orders"],
+            available_meals={
+                item: info
+                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."""
+
+        self.kitchen_height: int = 0
+        """The height of the kitchen, is set by the `Environment.parse_layout_file` method"""
+        self.kitchen_width: int = 0
+        """The width of the kitchen, is set by the `Environment.parse_layout_file` method"""
+
+        self.counter_factory = CounterFactory(
+            layout_chars_config=self.environment_config["layout_chars"],
+            item_info=self.item_info,
+            serving_window_additional_kwargs={
+                "meals": self.allowed_meal_names,
+                "env_time_func": self.get_env_time,
+            },
+            plate_config=PlateConfig(
+                **(
+                    self.environment_config["plates"]
+                    if "plates" in self.environment_config
+                    else {}
+                )
+            ),
+            order_manager=self.order_manager,
+            effect_manager_config=self.environment_config["effect_manager"],
+            undo_dispenser_pickup=self.environment_config["game"][
+                "undo_dispenser_pickup"
+            ]
+            if "game" in self.environment_config
+            and "undo_dispenser_pickup" in self.environment_config["game"]
+            else False,
+            hook=self.hook,
+            random=self.random,
+        )
+
+        (
+            self.counters,
+            self.designated_player_positions,
+            self.free_positions,
+        ) = self.parse_layout_file()
+        self.hook(LAYOUT_FILE_PARSED)
+
+        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(
+                lambda cl: hasattr(cl, "progress"),
+                dict(
+                    inspect.getmembers(
+                        sys.modules["cooperative_cuisine.counters"], inspect.isclass
+                    )
+                ).values(),
+            )
+        )
+        self.progressing_counters = list(
+            filter(
+                lambda c: c.__class__ in progress_counter_classes,
+                self.counters,
+            )
+        )
+        """Counters that needs to be called in the step function via the `progress` method."""
+
+        self.counter_positions = np.array([c.pos for c in self.counters])
+
+        self.order_manager.create_init_orders(self.env_time)
+        self.start_time = self.env_time
+        """The relative env time when it started."""
+        self.env_time_end = self.env_time + timedelta(
+            seconds=self.environment_config["game"]["time_limit_seconds"]
+        )
+        """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(),
+        )
+
+        self.all_players_ready = False
+
+    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["cooperative_cuisine.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`.
+
+        Utility method to pass a reference to the serving window."""
+        return self.env_time
+
+    def load_item_info(self, data) -> dict[str, ItemInfo]:
+        """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos."""
+        if self.as_files:
+            with open(data, "r") as file:
+                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])
+
+        for item_name, item_info in item_lookup.items():
+            if item_info.equipment:
+                item_info.equipment = item_lookup[item_info.equipment]
+        return item_lookup
+
+    def get_meal_graph(self, meal: ItemInfo) -> dict:
+        graph = DiGraph(
+            directed=True, rankdir="LR", graph_attr={"nslimit": "0", "nslimit1": "2"}
+        )
+
+        root = meal.name + "_0"
+
+        graph.add_node(root)
+        add_queue = [root]
+
+        start = True
+        while add_queue:
+            current = add_queue.pop()
+
+            current_info = self.item_info[current.split("_")[0]]
+            current_index = current.split("_")[-1]
+
+            if start:
+                graph.add_edge("Plate_0", current)
+                current = "Plate_0"
+                start = False
+
+            if current_info.needs:
+                if len(current_info.needs) == 1:
+                    need = current_info.needs[0] + f"_{current_index}"
+                    add_queue.append(need)
+
+                    if current_info.equipment:
+                        equip_id = current_info.equipment.name + f"_{current_index}"
+                        if current_info.equipment.equipment:
+                            equip_equip_id = (
+                                current_info.equipment.equipment.name
+                                + f"_{current_index}"
+                            )
+                            graph.add_edge(equip_equip_id, current)
+                            graph.add_edge(equip_id, equip_equip_id)
+                            graph.add_edge(need, equip_id)
+                        else:
+                            graph.add_edge(equip_id, current)
+                            graph.add_edge(need, equip_id)
+                    else:
+                        graph.add_edge(need, current)
+
+                elif len(current_info.needs) > 1:
+                    for idx, item_name in enumerate(current_info.needs):
+                        add_queue.append(item_name + f"_{idx}")
+
+                        if current_info.equipment and current_info.equipment.equipment:
+                            equip_id = current_info.equipment.name + f"_{current_index}"
+                            equip_equip_id = (
+                                current_info.equipment.equipment.name
+                                + f"_{current_index}"
+                            )
+                            graph.add_edge(equip_equip_id, current)
+                            graph.add_edge(equip_id, equip_equip_id)
+                            graph.add_edge(item_name + f"_{idx}", equip_id)
+                        else:
+                            graph.add_edge(
+                                item_name + f"_{idx}",
+                                current,
+                            )
+
+        layout = networkx.nx_agraph.graphviz_layout(graph, prog="dot")
+
+        edges = [(start, end) for start, end in graph.edges]
+
+        return {"meal": meal.name, "edges": edges, "layout": layout}
+
+    def validate_item_info(self):
+        """TODO"""
+        raise NotImplementedError
+
+    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
+        in layout).
+        """
+
+        starting_at: float = 0.0
+        current_y: float = starting_at
+        counters: list[Counter] = []
+        designated_player_positions: list[npt.NDArray] = []
+        free_positions: list[npt.NDArray] = []
+
+        if self.as_files:
+            with open(self.layout_config, "r") as layout_file:
+                self.layout_config = layout_file.read()
+        lines = self.layout_config.split("\n")
+
+        grid = []
+
+        max_width = 0
+
+        lines = list(filter(lambda l: l != "", lines))
+        for line in lines:
+            line = line.replace(" ", "")
+            if not line or line.startswith(";"):
+                break
+            current_x: float = starting_at
+            grid_line = []
+
+            for character in line:
+                # 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"
+                if self.counter_factory.is_counter(character):
+                    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)
+                        case "Free":
+                            free_positions.append(np.array([current_x, current_y]))
+
+                current_x += 1
+
+            if len(line) >= max_width:
+                max_width = len(line)
+
+            grid.append(grid_line)
+            current_y += 1
+
+        grid = [line + ([0] * (max_width - len(line))) for line in grid]
+
+        self.kitchen_width: float = max_width + 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.
+        Possible action types are movement, pickup and interact actions.
+
+        Args:
+            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:
+            player.set_movement(
+                action.action_data,
+                self.env_time + timedelta(seconds=action.duration),
+            )
+        else:
+            counter = self.get_facing_counter(player)
+            if player.can_reach(counter):
+                if action.action_type == ActionType.PUT:
+                    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_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:
+                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.
+        Adds a multiple of the player facing direction onto the player position and finds the closest
+        counter for that point.
+
+        Args:
+            player: The player for which to find the facing counter.
+
+        Returns:
+
+        """
+        facing_counter = get_closest(player.facing_point, self.counters)
+        return facing_counter
+
+    def get_counter_collisions(self, player_positions):
+        counter_diff_vecs = (
+            player_positions[:, np.newaxis, :]
+            - self.counter_positions[np.newaxis, :, :]
+        )
+        counter_distances = np.max((np.abs(counter_diff_vecs)), axis=2)
+        closest_counter_positions = self.counter_positions[
+            np.argmin(counter_distances, axis=1)
+        ]
+        nearest_counter_to_player = player_positions - closest_counter_positions
+        relevant_axes = np.abs(nearest_counter_to_player).argmax(axis=1)
+
+        distances = np.linalg.norm(
+            np.max(
+                [
+                    np.abs(counter_diff_vecs) - 0.5,
+                    np.zeros(counter_diff_vecs.shape),
+                ],
+                axis=0,
+            ),
+            axis=2,
+        )
+
+        collided = np.any(distances < self.player_radius, axis=1)
+
+        return collided, relevant_axes, nearest_counter_to_player
+
+    def get_player_push(self, player_positions):
+        distances_players_after_scipy = distance_matrix(
+            player_positions, player_positions
+        )
+
+        player_diff_vecs = -(
+            player_positions[:, np.newaxis, :] - player_positions[np.newaxis, :, :]
+        )
+        collisions = distances_players_after_scipy < (2 * self.player_radius)
+        eye_idxs = np.eye(len(player_positions), len(player_positions), dtype=bool)
+        collisions[eye_idxs] = False
+        player_diff_vecs[collisions == False] = 0
+        push_vectors = np.sum(player_diff_vecs, axis=0)
+        collisions = np.any(collisions, axis=1)
+        return collisions, push_vectors
+
+    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.
+        This happens, when the player moves diagonally against the counters or world boundary.
+        This just checks if the single axis party of the movement could move the player and does so at a lower rate.)
+
+        The movement action is a unit 2d vector.
+
+        Detects collisions with other players and pushes them out of the way.
+
+        Args:
+            duration: The duration for how long the movement to perform.
+        """
+        d_time = duration.total_seconds()
+
+        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,
+        )
+
+        targeted_positions = player_positions + (
+            player_movement_vectors * (self.player_movement_speed * d_time)
+        )
+
+        # Collisions player between player
+        force_factor = 1.2
+        _, push_vectors = self.get_player_push(targeted_positions)
+        updated_movement = (force_factor * push_vectors) + player_movement_vectors
+        new_targeted_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
+        # same again to prevent squeezing into other players
+        _, push_vectors2 = self.get_player_push(new_targeted_positions)
+        updated_movement = (force_factor * push_vectors2) + updated_movement
+        new_targeted_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
+
+        # Check collisions with counters
+        (
+            collided,
+            relevant_axes,
+            nearest_counter_to_player,
+        ) = self.get_counter_collisions(new_targeted_positions)
+
+        # Check if sliding against counters is possible
+        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] = np.max(
+                        [updated_movement[idx, axis], 0]
+                    )
+                # collide with counter right or bottom
+                if nearest_counter_to_player[idx][axis] < 0:
+                    updated_movement[idx, axis] = np.min(
+                        [updated_movement[idx, axis], 0]
+                    )
+        new_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
+
+        # Check collisions with counters again, now absolute with no sliding possible
+        (
+            collided,
+            relevant_axes,
+            nearest_counter_to_player,
+        ) = self.get_counter_collisions(new_positions)
+        new_positions[collided] = player_positions[collided]
+
+        # Check player collisions a final time
+        # collided, _ = self.get_player_push(new_positions)
+        # if np.any(collided):
+        #     print(".", end="")
+
+        # Collisions player world borders
+        new_positions = np.clip(
+            new_positions,
+            self.world_borders_lower + self.player_radius,
+            self.world_borders_upper - self.player_radius,
+        )
+
+        for idx, p in enumerate(self.players.values()):
+            if not (new_positions[idx] == player_positions[idx]).all():
+                p.pos = new_positions[idx]
+
+            p.turn(player_movement_vectors[idx])
+
+            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
+            )
+            if p.last_interacted_counter != p.current_nearest_counter:
+                p.perform_interact_stop()
+
+    def add_player(self, player_name: str, pos: npt.NDArray = None):
+        """Add a player to the environment.
+
+        Args:
+            player_name: The id/name of the player to reference actions and in the state.
+            pos: The optional init position of the player.
+        """
+        # TODO check if the player name already exists in the environment and do not overwrite player.
+        log.debug(f"Add player {player_name} to the game")
+        player = Player(
+            player_name,
+            player_config=PlayerConfig(
+                **(
+                    self.environment_config["player_config"]
+                    if "player_config" in self.environment_config
+                    else {}
+                )
+            ),
+            pos=pos,
+        )
+        self.players[player.name] = player
+        if player.pos is None:
+            if len(self.designated_player_positions) > 0:
+                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 = 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.
+
+        Args:
+            player: The player which to not let escape the world.
+
+        Returns: True if the player touches the world bounds, False if not.
+        """
+        collisions_lower = any(
+            (player.pos - (player.radius))
+            < [self.world_borders_x[0], self.world_borders_y[0]]
+        )
+        collisions_upper = any(
+            (player.pos + (player.radius))
+            > [self.world_borders_x[1], self.world_borders_y[1]]
+        )
+        return collisions_lower or collisions_upper
+
+    def step(self, passed_time: timedelta):
+        """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders
+        and time limits.
+        """
+        # self.hook(PRE_STEP, passed_time=passed_time)
+        self.env_time += passed_time
+
+        if self.game_ended:
+            self.hook(GAME_ENDED_STEP)
+        else:
+            for player in self.players.values():
+                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_manager.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.
+
+        Returns: Dict of lists of the current relevant game objects.
+
+        """
+        return {
+            "players": self.players,
+            "counters": self.counters,
+            "score": self.score,
+            "orders": self.order_manager.open_orders,
+            "ended": self.game_ended,
+            "env_time": self.env_time,
+            "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)),
+        }
+
+    def get_json_state(
+        self,
+        player_id: str = None,
+    ) -> str:
+        """Return the current state of the game formatted in json dict.
+
+        Args:
+            player_id: The player for which to get the state.
+            play_beep: Signal the GUI to play a beep when all connected players are ready to play the game.
+
+        Returns: The state of the game formatted as a json-string
+
+        """
+        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.score,
+                "orders": self.order_manager.order_state(),
+                "all_players_ready": self.all_players_ready,
+                "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_restrictions": [
+                    {
+                        "direction": player.facing_direction.tolist(),
+                        "position": player.pos.tolist(),
+                        "angle": self.player_view_angle,
+                        "counter_mask": None,
+                        "range": self.player_view_range,
+                    }
+                    for player in self.players.values()
+                ]
+                if self.player_view_restricted
+                else None,
+                "served_meals": [
+                    (player, str(meal))
+                    for (meal, time, player) in self.order_manager.served_meals
+                ],
+                "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 get_recipe_graphs(self) -> list:
+        os.makedirs(ROOT_DIR / "generated", exist_ok=True)
+
+        if self.environment_config["meals"]["all"]:
+            meals = [m for m in self.item_info.values() if m.type == ItemType.Meal]
+        else:
+            meals = [
+                self.item_info[m]
+                for m in self.environment_config["meals"]["list"]
+                if self.item_info[m].type == ItemType.Meal
+            ]
+
+        # print(list(m.name for m in meals))
+        # time_start = time.time()
+        # graph_dicts = list(map(self.get_meal_graph, meals))
+        with ThreadPoolExecutor(max_workers=len(meals)) as executor:
+            graph_dicts = list(executor.map(self.get_meal_graph, meals))
+        # print("DURATION", time.time() - time_start)
+        return graph_dicts
+
+    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"]
+                )
+
+    def increment_score(self, score: int | float, info: str = ""):
+        """Add a value to the current score and log it."""
+        self.score += score
+        log.debug(f"Score: {self.score} ({score}) - {info}")
diff --git a/overcooked_simulator/game_items.py b/cooperative_cuisine/game_items.py
similarity index 54%
rename from overcooked_simulator/game_items.py
rename to cooperative_cuisine/game_items.py
index 8f1b39bd52c35fdc923ce714a88b4c7f815fe471..6ed9f4d25daa7e0eb3b7e844df4c340e29f4e997 100644
--- a/overcooked_simulator/game_items.py
+++ b/cooperative_cuisine/game_items.py
@@ -26,18 +26,38 @@ import datetime
 import logging
 import uuid
 from enum import Enum
-from typing import Optional
+from typing import Optional, TypedDict, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from cooperative_cuisine.effect_manager import EffectManager
 
 log = logging.getLogger(__name__)
+"""The logger for this module."""
 
 ITEM_CATEGORY = "Item"
+"""The string for the `category` value in the json state representation for all normal items."""
+
 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."""
     Meal = "Meal"
+    """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
@@ -80,28 +100,58 @@ 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):
+    """The values in the active transitions dicts of `CookingEquipment`."""
+
+    seconds: int | float
+    """The needed seconds to progress for the transition."""
+    result: str | Item | Effect
+    """The new name of the item after the transition."""
 
 
 class Item:
     """Base class for game items which can be held by a player."""
 
     item_category = ITEM_CATEGORY
+    """Class dependent category (is changed for the `CookingEquipment` class). """
 
     def __init__(
         self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs
     ):
-        self.name = self.__class__.__name__ if name is None else name
-        self.item_info = item_info
-        self.progress_equipment = None
-        self.progress_percentage = 0.0
-        self.uuid = uuid.uuid4().hex if uid is None else uid
+        self.name: str = self.__class__.__name__ if name is None else name
+        """The name of the item, e.g., `Tomato` or `ChoppedTomato`"""
+        self.item_info: ItemInfo = item_info
+        """The information about the item from the `item_info.yml` config."""
+        self.progress_equipment: str | None = None
+        """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:
@@ -114,12 +164,15 @@ class Item:
 
     @property
     def extra_repr(self):
+        """Additional string to add to the representation of the item (in __repr__)."""
         return ""
 
     def can_combine(self, other) -> bool:
+        """Check if the item can be combined with the other. After it returned True the `combine` method is called."""
         return False
 
     def combine(self, other) -> Item | None:
+        """Combine the item with another item based on possible transitions/needs."""
         pass
 
     def progress(self, equipment: str, percent: float):
@@ -136,15 +189,20 @@ class Item:
             )
 
     def reset(self):
+        """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."""
         return {
             "id": self.uuid,
             "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],
         }
 
 
@@ -152,12 +210,13 @@ class CookingEquipment(Item):
     """Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself (
     progress_percentage) and not in the items in the content list."""
 
-    item_category = "Cooking Equipment"
+    item_category = COOKING_EQUIPMENT_ITEM_CATEGORY
 
-    def __init__(self, transitions: dict, *args, **kwargs):
+    def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.transitions = transitions
-        self.active_transition: Optional[dict] = None
+        self.transitions: dict[str, ItemInfo] = transitions
+        """What is needed to cook a meal / create another ingredient."""
+        self.active_transition: Optional[ActiveTransitionTypedDict] = None
         """The info how and when to convert the content_list to a new item."""
 
         # TODO change content ready just to str (name of the item)?
@@ -171,13 +230,21 @@ class CookingEquipment(Item):
         log.debug(f"Initialize {self.name}: {self.transitions}")
 
         for transition in self.transitions.values():
-            transition["recipe"] = collections.Counter(transition["needs"])
+            transition.recipe = collections.Counter(transition.needs)
 
     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:
@@ -187,9 +254,7 @@ class CookingEquipment(Item):
         ingredients = collections.Counter(
             item.name for item in self.content_list + other
         )
-        return any(
-            ingredients <= recipe["recipe"] for recipe in self.transitions.values()
-        )
+        return any(ingredients <= recipe.recipe for recipe in self.transitions.values())
 
     def combine(self, other) -> Item | None:
         return_value = None
@@ -197,46 +262,82 @@ 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():
-            recipe = transition["recipe"]
-            if ingredients == recipe:
-                if transition["seconds"] == 0:
-                    self.content_ready = Item(name=result, item_info=transition["info"])
-                else:
-                    self.active_transition = {
-                        "seconds": transition["seconds"],
-                        "result": Item(name=result, item_info=transition["info"]),
-                    }
-                break
-        else:
-            self.content_ready = None
+        self.check_active_transition()
         return return_value
 
     def can_progress(self) -> bool:
-        return self.active_transition is not None
+        """Check if the cooking equipment can progress items at all."""
+        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 = []
         self.content_ready = None
 
     def release(self):
+        """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
@@ -248,6 +349,7 @@ class CookingEquipment(Item):
         self.active_transition = None
 
     def get_potential_meal(self) -> Item | None:
+        """The meal that could be served depends on the attributes `content_ready` and `content_list`"""
         if self.content_ready:
             return self.content_ready
         if len(self.content_list) == 1:
@@ -273,27 +375,24 @@ class CookingEquipment(Item):
 class Plate(CookingEquipment):
     """The plate can have to states: clean and dirty. In the clean state it can hold content/other items."""
 
-    def __init__(self, transitions, clean, *args, **kwargs):
-        self.clean = clean
+    def __init__(self, transitions: dict[str, ItemInfo], clean: bool, *args, **kwargs):
+        self.clean: bool = clean
+        """If the plate is clean or dirty."""
         self.meals = set(transitions.keys())
         """All meals can be hold by a clean plate"""
         super().__init__(
             name=self.create_name(),
-            transitions={
-                k: v for k, v in transitions.items() if not v["info"].equipment
-            },
+            transitions={k: v for k, v in transitions.items() if not v.equipment},
             *args,
             **kwargs,
         )
 
-    def finished_call(self):
-        self.clean = True
-        self.name = self.create_name()
-
     def progress(self, equipment: str, percent: float):
         Item.progress(self, equipment, percent)
 
     def create_name(self):
+        """The name depends on the clean or dirty state of the plate. Clean plates are `Plate`, otherwise
+        `DirtyPlate`."""
         return "Plate" if self.clean else "DirtyPlate"
 
     def can_combine(self, other):
@@ -311,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/cooperative_cuisine/game_server.py
similarity index 87%
rename from overcooked_simulator/game_server.py
rename to cooperative_cuisine/game_server.py
index 35db3885b24d70c21ee3c37db3a95ddad423613c..e30c4e1216c7778b4f2955149d8501598cdd0f1d 100644
--- a/overcooked_simulator/game_server.py
+++ b/cooperative_cuisine/game_server.py
@@ -29,13 +29,17 @@ from pydantic import BaseModel
 from starlette.websockets import WebSocketDisconnect
 from typing_extensions import TypedDict
 
-from overcooked_simulator.overcooked_environment import Action, Environment
-from overcooked_simulator.server_results import (
+from cooperative_cuisine.environment import Action, Environment
+from cooperative_cuisine.server_results import (
     CreateEnvResult,
     PlayerInfo,
     PlayerRequestResult,
 )
-from overcooked_simulator.utils import setup_logging, url_and_port_arguments
+from cooperative_cuisine.utils import (
+    url_and_port_arguments,
+    add_list_of_manager_ids_arguments,
+    disable_websocket_logging_arguments,
+)
 
 log = logging.getLogger(__name__)
 
@@ -97,10 +101,11 @@ class EnvironmentHandler:
         """The preferred sleep time between environment steps in nanoseconds based on the `env_step_frequency`."""
         self.client_ids_to_player_hashes = {}
         """A dictionary mapping client IDs to player hashes."""
+        self.allowed_manager: list[str] = []
 
     def create_env(
         self, environment_config: CreateEnvironmentConfig
-    ) -> CreateEnvResult:
+    ) -> CreateEnvResult | int:
         """Create a new environment.
 
         Args:
@@ -110,13 +115,19 @@ class EnvironmentHandler:
             A dictionary containing the created environment ID and player information.
 
         """
+        if environment_config.manager_id not in self.allowed_manager:
+            return 1
         env_id = uuid.uuid4().hex
 
+        print("GAME SERVER ALLOWED IDS:", self.allowed_manager)
+
         env = Environment(
             env_config=environment_config.environment_config,
             layout_config=environment_config.layout_config,
             item_info=environment_config.item_info_config,
             as_files=False,
+            env_name=env_id,
+            seed=environment_config.seed,
         )
         player_info = {}
         for player_id in range(environment_config.number_players):
@@ -130,7 +141,13 @@ class EnvironmentHandler:
 
         self.manager_envs[environment_config.manager_id].update([env_id])
 
-        return {"env_id": env_id, "player_info": player_info}
+        graphs = env.get_recipe_graphs()
+        print(graphs)
+
+        res = CreateEnvResult(
+            env_id=env_id, player_info=player_info, recipe_graphs=graphs
+        )
+        return res
 
     def create_player(
         self, env: Environment, env_id: str, player_id: str
@@ -195,6 +212,7 @@ class EnvironmentHandler:
                     env_id=config.env_id,
                     player_id=player_id,
                 )
+                log.debug(f"Added player {player_id} to env {config.env_id}")
         return new_player_info
 
     def start_env(self, env_id: str):
@@ -210,24 +228,34 @@ class EnvironmentHandler:
             self.envs[env_id].start_time = start_time
             self.envs[env_id].last_step_time = time.time_ns()
             self.envs[env_id].environment.reset_env_time()
+            self.envs[env_id].environment.all_players_ready = True
 
-    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:
             player_hash (str): The unique identifier of the player.
 
         Returns: str: The state representation of the environment for a player. Is
-        `overcooked_simulator.state_representation.StateRepresentation` as a json.
+        `cooperative_cuisine.state_representation.StateRepresentation` as a json.
 
         """
         if (
             player_hash in self.player_data
             and self.player_data[player_hash].env_id in self.envs
         ):
-            return self.envs[
+            state = self.envs[
                 self.player_data[player_hash].env_id
-            ].environment.get_json_state()
+            ].environment.get_json_state(
+                self.player_data[player_hash].player_id,
+            )
+            return state
+        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.
@@ -242,7 +270,7 @@ class EnvironmentHandler:
             manager_id in self.manager_envs
             and env_id in self.manager_envs[manager_id]
             and self.envs[env_id].status
-            not in [EnvironmentStatus.STOPPED, Environment.PAUSED]
+            not in [EnvironmentStatus.STOPPED, EnvironmentStatus.PAUSED]
         ):
             self.envs[env_id].status = EnvironmentStatus.PAUSED
 
@@ -258,7 +286,7 @@ class EnvironmentHandler:
             manager_id in self.manager_envs
             and env_id in self.manager_envs[manager_id]
             and self.envs[env_id].status
-            not in [EnvironmentStatus.STOPPED, Environment.PAUSED]
+            not in [EnvironmentStatus.STOPPED, EnvironmentStatus.PAUSED]
         ):
             self.envs[env_id].status = EnvironmentStatus.PAUSED
             self.envs[env_id].last_step_time = time.time_ns()
@@ -281,8 +309,11 @@ class EnvironmentHandler:
             if self.envs[env_id].status != EnvironmentStatus.STOPPED:
                 self.envs[env_id].status = EnvironmentStatus.STOPPED
                 self.envs[env_id].stop_reason = reason
+                log.debug(f"Stopped environment: id={env_id}, reason={reason}")
                 return 0
+            log.debug(f"Could not stop environment: id={env_id}, env is not running")
             return 2
+        log.debug(f"Could not stop environment: id={env_id}, no env with this id")
         return 1
 
     def set_player_ready(self, player_hash) -> bool:
@@ -482,6 +513,9 @@ class EnvironmentHandler:
             return True
         return False
 
+    def extend_allowed_manager(self, manager: list[str]):
+        self.allowed_manager.extend(manager)
+
 
 class PlayerConnectionManager:
     """
@@ -540,8 +574,11 @@ class PlayerConnectionManager:
             await connection.send_text(message)
 
 
-manager = PlayerConnectionManager()
-environment_handler: EnvironmentHandler = EnvironmentHandler()
+connection_manager = PlayerConnectionManager()
+frequency = 200
+environment_handler: EnvironmentHandler = EnvironmentHandler(
+    env_step_frequency=frequency
+)
 
 
 class PlayerRequestType(Enum):
@@ -575,6 +612,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"]
@@ -585,10 +634,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
@@ -644,6 +689,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):
@@ -656,6 +708,8 @@ class AdditionalPlayer(BaseModel):
 @app.post("/manage/create_env/")
 async def create_env(creation: CreateEnvironmentConfig) -> CreateEnvResult:
     result = environment_handler.create_env(creation)
+    if result == 1:
+        raise HTTPException(status_code=403, detail="Manager ID not known/registered.")
     return result
 
 
@@ -666,8 +720,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,
@@ -694,7 +750,7 @@ async def websocket_player_endpoint(websocket: WebSocket, client_id: str):
     if not environment_handler.is_known_client_id(client_id):
         log.warning(f"wrong websocket connection with {client_id=}")
         return
-    await manager.connect_player(websocket, client_id)
+    await connection_manager.connect_player(websocket, client_id)
     log.debug(f"Client #{client_id} connected")
     environment_handler.set_player_connected(client_id)
     try:
@@ -703,17 +759,21 @@ async def websocket_player_endpoint(websocket: WebSocket, client_id: str):
             answer = manage_websocket_message(message, client_id)
             if isinstance(answer, dict):
                 answer = json.dumps(answer)
-            await manager.send_personal_message(answer, websocket)
+            await connection_manager.send_personal_message(answer, websocket)
 
     except WebSocketDisconnect:
-        manager.disconnect(client_id)
+        connection_manager.disconnect(client_id)
         environment_handler.set_player_disconnected(client_id)
         log.debug(f"Client #{client_id} disconnected")
 
 
-def main(host: str, port: int):
+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)
     loop.create_task(environment_handler.environment_steps())
     config = uvicorn.Config(app, host=host, port=port, loop=loop)
     server = uvicorn.Server(config)
@@ -722,16 +782,17 @@ def main(host: str, port: int):
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(
-        prog="Overcooked Simulator Game Server",
+        prog="Cooperative Cuisine Game Server",
         description="Game Engine Server: Starts overcooked game engine server.",
-        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/cooperative_cuisine.html",
     )
 
     url_and_port_arguments(parser)
+    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)
+    main(args.url, args.port, args.manager_ids, args.enable_websocket_logging)
     """
     Or in console: 
-    uvicorn overcooked_simulator.fastapi_game_server:app --reload
+    uvicorn cooperative_cuisine.fastapi_game_server:app --reload
     """
diff --git a/cooperative_cuisine/hooks.py b/cooperative_cuisine/hooks.py
new file mode 100644
index 0000000000000000000000000000000000000000..959103602194ad4ba2b5aa423eb5dc1895f7fb07
--- /dev/null
+++ b/cooperative_cuisine/hooks.py
@@ -0,0 +1,201 @@
+"""
+You can add callbacks at specific points in the environment.
+This "hook" mechanism is defined here.
+
+You can add hooks via the `environment_config` under `extra_setup_functions` and the here defined
+`hooks_via_callback_class` function.
+
+Each hook get different kwargs. But `env` with the environment object and `hook_ref` with the name of the hook are
+always present.
+
+# Code Documentation
+"""
+
+from __future__ import annotations
+
+from abc import abstractmethod
+from collections import defaultdict
+from functools import partial
+from typing import Callable, Any, TYPE_CHECKING, Type
+
+if TYPE_CHECKING:
+    from cooperative_cuisine.environment import Environment
+
+# 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"
+
+PRE_STEP = "pre_step"
+POST_STEP = "post_step"
+
+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"
+
+ORDER_EXPIRED = "order_expired"
+
+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"
+
+DROP_OFF_ON_COOKING_EQUIPMENT = "drop_off_on_cooking_equipment"
+
+
+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)
+
+
+class HookCallbackClass:
+    def __init__(self, name: str, env: Environment, **kwargs):
+        self.name = name
+        self.env = env
+
+    @abstractmethod
+    def __call__(self, hook_ref: str, env: Environment, **kwargs):
+        ...
+
+
+def hooks_via_callback_class(
+    name: str,
+    env: Environment,
+    hooks: list[str],
+    callback_class: Type[HookCallbackClass],
+    callback_class_kwargs: dict[str, Any],
+):
+    recorder = callback_class(name=name, env=env, **callback_class_kwargs)
+    for hook in hooks:
+        env.register_callback_for_hook(hook, recorder)
+
+
+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/cooperative_cuisine/info_msg.py b/cooperative_cuisine/info_msg.py
new file mode 100644
index 0000000000000000000000000000000000000000..d28ebfb5a3170ce873c00600a6ae258ad89418f2
--- /dev/null
+++ b/cooperative_cuisine/info_msg.py
@@ -0,0 +1,65 @@
+"""Based on hooks, text-based info msgs can be displayed.
+
+ ```yaml
+ extra_setup_functions:
+   info_msg:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ cutting_board_100 ]
+      callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+      callback_class_kwargs:
+        msg: Glückwunsch du hast was geschnitten!
+  fire_msg:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ new_fire ]
+      callback_class: !!python/name:cooperative_cuisine.info_msg.InfoMsgManager ''
+      callback_class_kwargs:
+        msg: Feuer, Feuer, Feuer
+        level: Warning
+ ```
+
+ """
+
+from datetime import timedelta
+
+from cooperative_cuisine.environment import Environment
+from cooperative_cuisine.hooks import HookCallbackClass
+
+
+class InfoMsgManager(HookCallbackClass):
+    def __init__(
+        self,
+        name: str,
+        env: Environment,
+        msg: str,
+        duration: int = 5,
+        level: str = "Normal",
+        **kwargs
+    ):
+        super().__init__(name, env, **kwargs)
+        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/order.py b/cooperative_cuisine/orders.py
similarity index 52%
rename from overcooked_simulator/order.py
rename to cooperative_cuisine/orders.py
index 68e5d311cf9cca0f30795486787a5597b5d82f93..e1e082e15536bcb3996b2469f1e3e82c222e0b09 100644
--- a/overcooked_simulator/order.py
+++ b/cooperative_cuisine/orders.py
@@ -6,7 +6,7 @@ It is very configurable by letting you reference own Python classes and function
 ```yaml
 orders:
   serving_not_ordered_meals: null
-  order_gen_class:  !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
+  order_gen_class:  !!python/name:cooperative_cuisine.orders.RandomOrderGeneration ''
   order_gen_kwargs:
     ...
 ```
@@ -25,16 +25,10 @@ This file defines the following classes:
 
 Further, it defines same implementations for the basic order generation based on random sampling:
 - `RandomOrderGeneration`
-- `simple_score_calc_gen_func`
-- `simple_expired_penalty`
-- `zero`
 
 For an easier usage of the random orders, also some classes for type hints and dataclasses are defined:
 - `RandomOrderKwarg`
 - `RandomFuncConfig`
-- `ScoreCalcFuncType`
-- `ScoreCalcGenFuncType`
-- `ExpiredPenaltyFuncType`
 
 
 ## Code Documentation
@@ -43,18 +37,41 @@ 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 overcooked_simulator.game_items import Item, Plate, ItemInfo
+from random import Random
+from typing import Callable, Tuple, Any, Deque, TypedDict, Type
+
+from cooperative_cuisine.game_items import Item, Plate, ItemInfo
+from cooperative_cuisine.hooks import (
+    Hooks,
+    SERVE_NOT_ORDERED_MEAL,
+    SERVE_WITHOUT_PLATE,
+    COMPLETED_ORDER,
+    INIT_ORDERS,
+    NEW_ORDERS,
+    ORDER_DURATION_SAMPLE,
+    ORDER_EXPIRED,
+)
 
 log = logging.getLogger(__name__)
+"""The logger for this module."""
 
 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
@@ -67,38 +84,10 @@ class Order:
     """The start time relative to the env_time. On which the order is returned from the get_orders func."""
     max_duration: timedelta
     """The duration after which the order expires."""
-    score_calc: ScoreCalcFuncType
-    """The function that calculates the score of the served meal/fulfilled order."""
-    timed_penalties: list[
-        Tuple[timedelta, float] | Tuple[timedelta, float, int, timedelta]
-    ]
-    """List of timed penalties when the order is not fulfilled."""
-    expired_penalty: float
-    """The penalty to the score if the order expires"""
     uuid: str = dataclasses.field(default_factory=lambda: uuid.uuid4().hex)
-
+    """The unique identifier for the order."""
     finished_info: dict[str, Any] = dataclasses.field(default_factory=dict)
     """Is set after the order is completed."""
-    _timed_penalties: list[Tuple[datetime, float]] = dataclasses.field(
-        default_factory=list
-    )
-    """Converted penalties the env is working with from the `timed_penalties`"""
-
-    def order_time(self, env_time: datetime) -> timedelta:
-        return self.start_time - env_time
-
-    def create_penalties(self, env_time: datetime):
-        for penalty_info in self.timed_penalties:
-            match penalty_info:
-                case (offset, penalty):
-                    self._timed_penalties.append((env_time + offset, penalty))
-                case (duration, penalty, number_repeat, offset):
-                    self._timed_penalties.extend(
-                        [
-                            (env_time + offset + (duration * i), penalty)
-                            for i in range(number_repeat)
-                        ]
-                    )
 
 
 class OrderGeneration:
@@ -108,14 +97,25 @@ class OrderGeneration:
     Example:
         ```yaml
         orders:
-          order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
+          order_gen_class: !!python/name:cooperative_cuisine.orders.RandomOrderGeneration ''
           kwargs:
             ...
         ```
     """
 
-    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]:
@@ -134,44 +134,60 @@ class OrderGeneration:
         ...
 
 
-class OrderAndScoreManager:
+class OrderManager:
     """The Order and Score Manager that is called from the serving window."""
 
-    def __init__(self, order_config, available_meals: dict[str, ItemInfo]):
-        self.score = 0
+    def __init__(
+        self,
+        order_config,
+        available_meals: dict[str, ItemInfo],
+        hook: Hooks,
+        random: Random,
+    ):
+        self.random = random
+        """Random instance."""
         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[
             [Item], Tuple[bool, float]
         ] = order_config["serving_not_ordered_meals"]
         """Function that decides if not ordered meals can be served and what score it gives"""
+
         self.available_meals = available_meals
         """The meals for that orders can be sampled from."""
         self.open_orders: Deque[Order] = deque()
         """Current open orders. This attribute is used for the environment state."""
 
         # TODO log who / which player served which meal -> for split scores
-        self.served_meals: list[Tuple[Item, datetime]] = []
+        self.served_meals: list[Tuple[Item, datetime, str]] = []
         """List of served meals. Maybe for the end screen."""
         self.last_finished: list[Order] = []
         """Cache last finished orders for `OrderGeneration.get_orders` call. From the served meals."""
         self.next_relevant_time: datetime = datetime.max
-        """For reduced order checking. Store the next time when to create an order or check for penalties."""
+        """For reduced order checking. Store the next time when to create an order."""
         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
         for order in self.open_orders:
             next_relevant_time = min(
                 next_relevant_time, order.start_time + order.max_duration
             )
-            for penalty in order._timed_penalties:
-                next_relevant_time = min(next_relevant_time, penalty[0])
         self.next_relevant_time = next_relevant_time
 
-    def serve_meal(self, item: Item, env_time: datetime) -> bool:
+    def serve_meal(self, item: Item, env_time: datetime, player: str) -> bool:
+        """Is called by the ServingWindow to serve a meal. Returns True if the meal can be served and should be
+        "deleted" from the hands of the player."""
         if isinstance(item, Plate):
             meal = item.get_potential_meal()
             if meal is not None:
@@ -179,46 +195,40 @@ class OrderAndScoreManager:
                     order = self.find_order_for_meal(meal)
                     if order is None:
                         if self.serving_not_ordered_meals:
-                            accept, score = self.serving_not_ordered_meals(meal)
-                            if accept:
-                                log.info(
-                                    f"Serving meal without order {meal.name!r} with score {score}"
-                                )
-                                self.increment_score(score)
-                                self.served_meals.append((meal, env_time))
-                            return accept
+                            self.hook(
+                                SERVE_NOT_ORDERED_MEAL,
+                                meal=meal,
+                                meal_name=meal.name,
+                            )
+                            log.info(f"Serving meal without order {meal.name!r}")
+                            self.served_meals.append((meal, env_time, player))
+                            return True
                         log.info(
                             f"Do not serve meal {meal.name!r} because it is not ordered"
                         )
                         return False
                     order, index = order
-                    score = order.score_calc(
-                        relative_order_time=env_time - order.start_time,
-                        order=order,
-                    )
-                    self.increment_score(score)
-                    order.finished_info = {
-                        "end_time": env_time,
-                        "score": score,
-                    }
-                    log.info(
-                        f"Serving meal {meal.name!r} with order with score {score}"
-                    )
+                    log.info(f"Serving meal {meal.name!r} with order")
                     self.last_finished.append(order)
                     del self.open_orders[index]
-                    self.served_meals.append((meal, env_time))
+                    self.served_meals.append((meal, env_time, player))
+                    self.hook(
+                        COMPLETED_ORDER,
+                        order=order,
+                        meal=meal,
+                        relative_order_time=env_time - order.start_time,
+                        meal_name=meal.name,
+                    )
                     return True
+        else:
+            self.hook(SERVE_WITHOUT_PLATE, item=item)
         log.info(f"Do not serve item {item}")
         return False
 
-    def increment_score(self, score: int | float):
-        self.score += score
-        log.debug(f"Score: {self.score}")
-
     def create_init_orders(self, env_time):
         """Create the initial orders in an environment."""
         init_orders = self.order_gen.init_orders(env_time)
-        self.setup_penalties(new_orders=init_orders, env_time=env_time)
+        self.hook(INIT_ORDERS)
         self.open_orders.extend(init_orders)
 
     def progress(self, passed_time: timedelta, now: datetime):
@@ -229,7 +239,8 @@ class OrderAndScoreManager:
             new_finished_orders=self.last_finished,
             expired_orders=self.last_expired,
         )
-        self.setup_penalties(new_orders=new_orders, env_time=now)
+        if new_orders:
+            self.hook(NEW_ORDERS, new_orders=new_orders)
         self.open_orders.extend(new_orders)
         self.last_finished = []
         self.last_expired = []
@@ -240,19 +251,9 @@ class OrderAndScoreManager:
             for index, order in enumerate(self.open_orders):
                 if now >= order.start_time + order.max_duration:
                     # orders expired
-                    self.increment_score(order.expired_penalty)
+                    self.hook(ORDER_EXPIRED, order=order)
                     remove_orders.append(index)
-                    continue  # no penalties for expired orders
-                remove_penalties = []
-                for i, (penalty_time, penalty) in enumerate(order.timed_penalties):
-                    # check penalties
-                    if penalty_time < now:
-                        self.score -= penalty
-                        remove_penalties.append(i)
-
-                for i in reversed(remove_penalties):
-                    # or del order.timed_penalties[index]
-                    order.timed_penalties.pop(i)
+                    continue
 
             expired_orders: list[Order] = []
             for remove_order in reversed(remove_orders):
@@ -263,17 +264,14 @@ class OrderAndScoreManager:
             self.update_next_relevant_time()
 
     def find_order_for_meal(self, meal) -> Tuple[Order, int] | None:
+        """Get the order that will be fulfilled for a meal. At the moment the oldest order in the list that has the
+        same meal (name)."""
         for index, order in enumerate(self.open_orders):
             if order.meal.name == meal.name:
                 return order, index
 
-    @staticmethod
-    def setup_penalties(new_orders: list[Order], env_time: datetime):
-        """Call the `Order.create_penalties` method for new orders."""
-        for order in new_orders:
-            order.create_penalties(env_time)
-
     def order_state(self) -> list[dict]:
+        """Similar to the `to_dict` in `Item` and `Counter`. Relevant for the state of the environment"""
         return [
             {
                 "id": order.uuid,
@@ -286,74 +284,6 @@ class OrderAndScoreManager:
         ]
 
 
-class ScoreCalcFuncType(Protocol):
-    """Typed kwargs of the expected `Order.score_calc` function. Which is also returned by the
-    `RandomOrderKwarg.score_calc_gen_func`.
-
-    The function should calculate the score for the completed orders.
-
-    Args:
-        relative_order_time: `timedelta`  the duration how long the order was active.
-        order: `Order` the order that was completed.
-
-    Returns:
-        `float`: the score for a completed order and duration of the order.
-    """
-
-    def __call__(self, relative_order_time: timedelta, order: Order) -> float:
-        ...
-
-
-class ScoreCalcGenFuncType(Protocol):
-    """Typed kwargs of the expected function for the `RandomOrderKwarg.score_calc_gen_func`.
-
-    Generate the ScoreCalcFunc for an order based on its meal, duration etc.
-
-    Args:
-        meal: `ItemInfo` the type of meal the order orders.
-        duration: `timedelta` the duration after the order expires.
-        now: `datetime` the environment time the order is created.
-        kwargs: `dict` the static kwargs defined in the `environment_config.yml`
-
-    Returns:
-        `ScoreCalcFuncType` a reference to a function that calculates the score for a completed meal.
-    """
-
-    def __call__(
-        self,
-        meal: ItemInfo,
-        duration: timedelta,
-        now: datetime,
-        kwargs: dict,
-        **other_kwargs,
-    ) -> ScoreCalcFuncType:
-        ...
-
-
-class ExpiredPenaltyFuncType(Protocol):
-    """Typed kwargs of the expected function for the `RandomOrderKwarg.expired_penalty_func`.
-
-    An example is the `zero` function.
-
-    Args:
-        item: `ItemInfo` the meal of the order that expired. It is calculated before the order is active.
-    """
-
-    def __call__(self, item: ItemInfo, **kwargs) -> float:
-        ...
-
-
-def zero(item: ItemInfo, **kwargs) -> float:
-    """Example and default for the `RandomOrderKwarg.expired_penalty_func` function.
-
-    Just no penalty for expired orders.
-
-    Returns:
-        zero / 0.0
-    """
-    return 0.0
-
-
 class RandomFuncConfig(TypedDict):
     """Types of the dict for sampling with different random functions from the [`random` library](https://docs.python.org/3/library/random.html).
 
@@ -391,14 +321,6 @@ class RandomOrderKwarg:
     """How many orders can maximally be active at the same time."""
     order_duration_random_func: RandomFuncConfig
     """How long the order is alive until it expires. If `sample_on_serving` is `true` all orders have no expire time."""
-    score_calc_gen_func: ScoreCalcGenFuncType
-    """The function that generates the `Order.score_calc` for each order."""
-    score_calc_gen_kwargs: dict
-    """The additional static kwargs for `score_calc_gen_func`."""
-    expired_penalty_func: Callable[[ItemInfo], float] = zero
-    """The function that calculates the penalty for a meal that was not served."""
-    expired_penalty_kwargs: dict = dataclasses.field(default_factory=dict)
-    """The additional static kwargs for the `expired_penalty_func`."""
 
 
 class RandomOrderGeneration(OrderGeneration):
@@ -412,47 +334,44 @@ class RandomOrderGeneration(OrderGeneration):
     You can set this order generation in your `environment_config.yml` with
     ```yaml
     orders:
-      order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
-        kwargs:
-            order_duration_random_func:
-              # how long should the orders be alive
-              # 'random' library call with getattr, kwargs are passed to the function
-              func: uniform
-              kwargs:
-                a: 40
-                b: 60
-            max_orders: 6
-            # maximum number of active orders at the same time
-            num_start_meals: 3
-            # number of orders generated at the start of the environment
-            sample_on_dur_random_func:
-              # 'random' library call with getattr, kwargs are passed to the function
-              func: uniform
-              kwargs:
-                a: 10
-                b: 20
-            sample_on_serving: false
-            # Sample the delay for the next order only after a meal was served.
-            score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func ''
-            score_calc_gen_kwargs:
-              # the kwargs for the score_calc_gen_func
-              other: 0
-              scores:
-                Burger: 15
-                OnionSoup: 10
-                Salad: 5
-                TomatoSoup: 10
-            expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
-            expired_penalty_kwargs:
-              default: -5
+      order_gen_class: !!python/name:cooperative_cuisine.orders.RandomOrderGeneration ''
+      # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py
+      order_gen_kwargs:
+        order_duration_random_func:
+          # how long should the orders be alive
+          # 'random' library call with getattr, kwargs are passed to the function
+          func: uniform
+          kwargs:
+            a: 40
+            b: 60
+        max_orders: 6
+        # maximum number of active orders at the same time
+        num_start_meals: 2
+        # number of orders generated at the start of the environment
+        sample_on_dur_random_func:
+          # 'random' library call with getattr, kwargs are passed to the function
+          func: uniform
+          kwargs:
+            a: 10
+            b: 20
+        sample_on_serving: false
+        # Sample the delay for the next order only after a meal was served.
+      serving_not_ordered_meals: true
+      # can meals that are not ordered be served / dropped on the serving window
     ```
     """
 
-    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 = 0
+        self.number_cur_orders: int = 0
         self.needed_orders: int = 0
         """For the sample on dur but when it was restricted due to max order number."""
 
@@ -461,7 +380,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,
         )
@@ -484,7 +403,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:
@@ -497,7 +416,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 []
@@ -512,25 +431,19 @@ 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(
                     meal=meal,
                     start_time=now,
                     max_duration=duration,
-                    score_calc=self.kwargs.score_calc_gen_func(
-                        meal=meal,
-                        duration=duration,
-                        now=now,
-                        kwargs=self.kwargs.score_calc_gen_kwargs,
-                    ),
-                    timed_penalties=[],
-                    expired_penalty=self.kwargs.expired_penalty_func(
-                        meal, **self.kwargs.expired_penalty_kwargs
-                    ),
                 )
             )
 
@@ -538,57 +451,8 @@ 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"]
             )
         )
         log.info(f"Next order in {self.next_order_time}")
-
-
-def simple_score_calc_gen_func(
-    meal: Item, duration: timedelta, now: datetime, kwargs: dict, **other_kwargs
-) -> Callable:
-    """An example for the `RandomOrderKwarg.score_calc_gen_func` that selects the score for an order based on its meal from a list.
-
-    Example:
-        ```yaml
-        score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func ''
-        score_calc_gen_kwargs:
-          # the kwargs for the score_calc_gen_func
-          other: 0
-          scores:
-            Burger: 15
-            OnionSoup: 10
-            Salad: 5
-            TomatoSoup: 10
-        ```
-    """
-    scores = kwargs["scores"]
-    other = kwargs["other"]
-
-    def score_calc(relative_order_time: timedelta, order: Order) -> float:
-        if order.meal.name in scores:
-            return scores[order.meal.name]
-        return other
-
-    return score_calc
-
-
-def simple_expired_penalty(item: ItemInfo, default: float, **kwargs) -> float:
-    """Example for the `RandomOrderKwarg.expired_penalty_func` function.
-
-    A static default.
-
-    Example:
-        ```yaml
-        expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
-        expired_penalty_kwargs:
-          default: -5
-        ```
-    """
-    return default
-
-
-def serving_not_ordered_meals_with_zero_score(meal: Item) -> Tuple[bool, float | int]:
-    """Not ordered meals are accepted but do not affect the score."""
-    return True, 0
diff --git a/overcooked_simulator/player.py b/cooperative_cuisine/player.py
similarity index 66%
rename from overcooked_simulator/player.py
rename to cooperative_cuisine/player.py
index 0f5f25424210d595d8290c61231d641b16d934a7..b85c76661433e2f899fba4ce93bde25df1883d16 100644
--- a/overcooked_simulator/player.py
+++ b/cooperative_cuisine/player.py
@@ -7,19 +7,20 @@ 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.state_representation import PlayerState
+from cooperative_cuisine.counters import Counter
+from cooperative_cuisine.game_items import Item, ItemType
+from cooperative_cuisine.state_representation import PlayerState
 
 log = logging.getLogger(__name__)
+"""The logger for this module."""
 
 
 @dataclasses.dataclass
@@ -32,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:
@@ -56,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[
@@ -78,23 +79,19 @@ class Player:
         """A point on the "circle" of the players border in the `facing_direction` with which the closest counter is 
         calculated with."""
 
-        self.current_movement: npt.NDArray[2] = np.zeros(2, float)
-        self.movement_until: datetime.datetime = datetime.datetime.min
+        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.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.
@@ -118,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
@@ -127,12 +126,16 @@ class Player:
         Args:
             counter: The counter, can the player reach it?
 
-        Returns: True if the counter is in range of the player, False if not.
+        Returns:
+            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.
 
@@ -141,47 +144,59 @@ class Player:
         """
 
         if self.holding is None:
-            self.holding = counter.pick_up()
+            self.holding = counter.pick_up(player=self.name)
 
         elif counter.can_drop_off(self.holding):
-            self.holding = counter.drop_off(self.holding)
+            self.holding = counter.drop_off(self.holding, player=self.name)
 
         elif not isinstance(
             counter.occupied_by, (list, deque)
         ) and self.holding.can_combine(counter.occupied_by):
-            returned_by_counter = counter.pick_up(on_hands=False)
+            returned_by_counter = counter.pick_up(on_hands=False, player=self.name)
             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})"
 
     def to_dict(self) -> PlayerState:
+        """For the state representation. Only the relevant attributes are put into the dict."""
         # TODO add color to player class for vis independent player color
         return {
             "id": self.name,
diff --git a/overcooked_simulator/gui_2d_vis/__init__.py b/cooperative_cuisine/pygame_2d_vis/__init__.py
similarity index 52%
rename from overcooked_simulator/gui_2d_vis/__init__.py
rename to cooperative_cuisine/pygame_2d_vis/__init__.py
index 9c531caa6003ae868d4eeef943dfa92d5c349fda..f76fc15e4010460e9cb062aadf330b36249c7935 100644
--- a/overcooked_simulator/gui_2d_vis/__init__.py
+++ b/cooperative_cuisine/pygame_2d_vis/__init__.py
@@ -1,10 +1,10 @@
 """
-2D visualization of the overcooked simulator.
+2D visualization of the CooperativeCuisine.
 
 You can select the layout and start an environment:
-- You can play the overcooked simulator. You can quit the application in the top right or end the level in the bottom right: [Screenshot](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator/-/raw/main/overcooked_simulator/gui_2d_vis/images/overcooked-start-screen.png?ref_type=heads)
-- The orders are pictured in the top, the current score in the bottom left and the remaining time in the bottom: [Screenshot](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator/-/raw/main/overcooked_simulator/gui_2d_vis/images/overcooked-level-screen.png?ref_type=heads)
-- The final screen after ending a level shows the score: [Screenshot](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator/-/raw/main/overcooked_simulator/gui_2d_vis/images/overcooked-end-screen.png?ref_type=heads)
+- You can play the CooperativeCuisine. You can quit the application in the top right or end the level in the bottom right: [Screenshot](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator/-/raw/main/overcooked_simulator/pygame_2d_vis/images/overcooked-start-screen.png?ref_type=heads)
+- The orders are pictured in the top, the current score in the bottom left and the remaining time in the bottom: [Screenshot](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator/-/raw/main/overcooked_simulator/pygame_2d_vis/images/overcooked-level-screen.png?ref_type=heads)
+- The final screen after ending a level shows the score: [Screenshot](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator/-/raw/main/overcooked_simulator/pygame_2d_vis/images/overcooked-end-screen.png?ref_type=heads)
 
 The keys for the control of the players are:
 
diff --git a/cooperative_cuisine/pygame_2d_vis/continue.drawio.png b/cooperative_cuisine/pygame_2d_vis/continue.drawio.png
new file mode 100644
index 0000000000000000000000000000000000000000..8a249e36fbab58f15550227268228b84d0e58099
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/continue.drawio.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py
new file mode 100644
index 0000000000000000000000000000000000000000..164a20d86c73af7433d68c958bc0b55eb2da71b4
--- /dev/null
+++ b/cooperative_cuisine/pygame_2d_vis/drawing.py
@@ -0,0 +1,1030 @@
+import argparse
+import colorsys
+import json
+import os
+from datetime import datetime, timedelta
+from pathlib import Path
+
+import numpy as np
+import numpy.typing as npt
+import pygame
+import yaml
+from scipy.spatial import KDTree
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.environment import Environment
+from cooperative_cuisine.pygame_2d_vis.game_colors import colors
+from cooperative_cuisine.state_representation import (
+    PlayerState,
+    CookingEquipmentState,
+    ItemState,
+    EffectState,
+)
+
+USE_PLAYER_COOK_SPRITES = True
+SHOW_INTERACTION_RANGE = False
+SHOW_COUNTER_CENTERS = False
+
+
+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 = start_vec.copy()
+
+    angle = (2 * np.pi) / n
+
+    rot_matrix = np.array(
+        [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]
+    )
+
+    vecs = [vector]
+    for i in range(n - 1):
+        vector = np.dot(rot_matrix, vector)
+        vecs.append(vector)
+
+    return vecs
+
+
+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
+        self.observation_screen = None
+
+    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()])
+
+        tree = KDTree(colors_vec)
+
+        color_names = list(colors.keys())
+
+        self.player_colors = []
+        for hue in hue_values:
+            rgb = colorsys.hsv_to_rgb(hue, 1, 1)
+            query_color = np.array([int(c * 255) for c in rgb])
+            _, index = tree.query(query_color, k=1)
+            self.player_colors.append(color_names[index])
+
+    def draw_gamescreen(
+        self,
+        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(
+            surface=screen,
+            width=width,
+            height=height,
+            grid_size=grid_size,
+        )
+        self.draw_counters(
+            screen,
+            state["counters"],
+            grid_size,
+        )
+
+        for idx, col in zip(controlled_player_idxs, [colors["blue"], colors["red"]]):
+            pygame.draw.circle(
+                screen,
+                col,
+                np.array(state["players"][int(idx)]["pos"]) * grid_size
+                + (grid_size // 2),
+                (grid_size / 2),
+            )
+
+        self.draw_players(
+            screen,
+            state["players"],
+            grid_size,
+        )
+
+        if "view_restrictions" in state and state["view_restrictions"]:
+            self.draw_lightcones(screen, grid_size, width, height, state)
+
+    def draw_lightcones(self, screen, grid_size, width, height, state):
+        view_restrictions = state["view_restrictions"]
+        mask = pygame.Surface(screen.get_size(), pygame.SRCALPHA)
+        mask.fill((0, 0, 0, 0))
+        mask_color = (0, 0, 0, 0)
+        for idx, restriction in enumerate(view_restrictions):
+            direction = pygame.math.Vector2(restriction["direction"])
+            pos = pygame.math.Vector2(restriction["position"])
+            angle = restriction["angle"] / 2
+            view_range = 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)
+
+            cone_mask = pygame.surface.Surface(screen.get_size(), pygame.SRCALPHA)
+            cone_mask.fill((255, 255, 255, 255))
+
+            offset_front = direction * grid_size * 0.7
+            if angle != 180:
+                shadow_cone_points = [
+                    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,
+                ]
+                light_cone_points = [pos - offset_front, left_beam, right_beam]
+                pygame.draw.polygon(
+                    cone_mask,
+                    mask_color,
+                    shadow_cone_points,
+                )
+
+            if view_range:
+                n_circle_points = 40
+
+                start_vec = np.array(-direction * view_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(cone_mask, mask_color, corners)
+
+            mask.blit(cone_mask, (0, 0), special_flags=pygame.BLEND_MAX)
+
+        screen.blit(
+            mask,
+            mask.get_rect(),
+            special_flags=pygame.BLEND_RGBA_MULT,
+        )
+
+    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):
+            for y in range(0, height, block_size):
+                rect = pygame.Rect(x, y, block_size, block_size)
+                pygame.draw.rect(
+                    surface,
+                    self.config["Kitchen"]["background_lines"],
+                    rect,
+                    1,
+                )
+
+    def draw_image(
+        self,
+        screen: pygame.Surface,
+        img_path: Path | str,
+        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 pygame_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 + ("-burnt" if burnt else "") in self.image_cache_dict:
+            image = self.image_cache_dict[cache_entry + ("-burnt" if burnt else "")]
+        else:
+            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 / "pygame_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 / "pygame_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)
+        rect = image.get_rect()
+        rect.center = pos
+
+        screen.blit(image, rect)
+
+    def draw_players(
+        self,
+        screen: pygame.Surface,
+        players: dict,
+        grid_size: float,
+    ):
+        """Visualizes the players as circles with a triangle for the facing direction.
+        If the player holds something in their hands, it is displayed
+
+        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"], dtype=float)
+
+            if USE_PLAYER_COOK_SPRITES:
+                pygame.draw.circle(
+                    screen,
+                    colors[self.player_colors[p_idx]],
+                    pos - facing * grid_size * 0.25,
+                    grid_size * 0.2,
+                )
+
+                img_path = self.config["Cook"]["parts"][0]["path"]
+                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)
+
+            else:
+                player_radius = 0.4
+                size = player_radius * grid_size
+                color1 = self.player_colors[p_idx]
+                color2 = colors["white"]
+
+                pygame.draw.circle(screen, color2, pos, size)
+                pygame.draw.circle(screen, colors["blue"], pos, size, width=1)
+                pygame.draw.circle(screen, colors[color1], pos, size // 2)
+
+                pygame.draw.polygon(
+                    screen,
+                    colors["blue"],
+                    (
+                        (
+                            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),
+                    ),
+                )
+
+            if SHOW_INTERACTION_RANGE:
+                pygame.draw.circle(
+                    screen,
+                    colors["blue"],
+                    pos + (facing * grid_size * 0.4),
+                    1.6 * grid_size,
+                    width=1,
+                )
+                pygame.draw.circle(
+                    screen, colors["red1"], pos + (facing * grid_size * 0.4), 4
+                )
+
+            if player_dict["holding"] is not None:
+                holding_item_pos = pos + (grid_size * 0.5 * facing)
+                self.draw_item(
+                    pos=holding_item_pos,
+                    grid_size=grid_size,
+                    item=player_dict["holding"],
+                    screen=screen,
+                )
+
+            if player_dict["current_nearest_counter_pos"]:
+                nearest_pos = (
+                    np.array(player_dict["current_nearest_counter_pos"]) * grid_size
+                )
+
+                pygame.draw.rect(
+                    screen,
+                    colors[self.player_colors[p_idx]],
+                    rect=pygame.Rect(
+                        *nearest_pos,
+                        grid_size,
+                        grid_size,
+                    ),
+                    width=2,
+                )
+
+    def draw_thing(
+        self,
+        screen: pygame.Surface,
+        pos: npt.NDArray[float],
+        grid_size: float,
+        parts: list[dict[str]],
+        scale: float = 1.0,
+        burnt: bool = False,
+        orientation: list[float] | None = None,
+        absolute_size=None,
+    ):
+        """Draws an item, based on its visual parts specified in the visualization config.
+
+        Args:
+            screen: the game screen to draw on.
+            grid_size: size of a grid cell.
+            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 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)
+                    size = (
+                        absolute_size
+                        if absolute_size is not None
+                        else part["size"] * scale * grid_size
+                    )
+                    self.draw_image(
+                        screen,
+                        part["path"],
+                        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)
+                        d[0] = -d[0]
+
+                        draw_pos += np.array(d)
+                    height = part["height"] * grid_size
+                    width = part["width"] * grid_size
+                    color = part["color"]
+                    rect = pygame.Rect(
+                        draw_pos[0] - (height / 2),
+                        draw_pos[1] - (width / 2),
+                        height,
+                        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 | EffectState,
+        scale: float = 1.0,
+        plate=False,
+        screen=None,
+    ):
+        """Visualization of an item at the specified position. On a counter or in the hands of the player.
+        The visual composition of the item is read in from visualization.yaml file, where it is specified as
+        different parts to be drawn.
+
+        Args:
+            grid_size: size of a grid cell.
+            pos: The position of the item to draw.
+            item: The item do be drawn in the game.
+            scale: Rescale the item by this factor.
+            screen: the pygame screen to draw on.
+            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?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,
+                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
+                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"].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"]), 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(
+                    pos=np.array(pos) + triangle_offsets[idx],
+                    item=o,
+                    scale=scale,
+                    plate="Plate" in item["type"],
+                    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(
+        screen: pygame.Surface,
+        pos: npt.NDArray[float],
+        percent: float,
+        grid_size: float,
+        attention: bool = False,
+    ):
+        """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(
+            bar_pos[0],
+            bar_pos[1] + grid_size - bar_height,
+            progress_width,
+            bar_height,
+        )
+        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
+    ):
+        """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:
+            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"],
+            orientation=counter_dict["orientation"],
+        )
+        if counter_type in self.config:
+            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"]
+            elif counter_type.endswith("Dispenser"):
+                parts = self.config["Dispenser"]["parts"]
+            else:
+                raise ValueError(f"Can not draw counter type {counter_type}")
+            self.draw_thing(
+                screen=screen,
+                pos=pos,
+                parts=parts,
+                grid_size=grid_size,
+                orientation=counter_dict["orientation"],
+            )
+
+    def draw_counter_occupier(
+        self,
+        screen: pygame.Surface,
+        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):
+                self.draw_item(
+                    screen=screen,
+                    pos=np.abs([pos[0], pos[1] - (i * 3)]),
+                    grid_size=grid_size,
+                    item=o,
+                    scale=item_scale,
+                )
+        # All other items:
+        else:
+            self.draw_item(
+                pos=pos,
+                grid_size=grid_size,
+                item=occupied_by,
+                screen=screen,
+                scale=item_scale,
+            )
+
+    def draw_counters(self, screen: pygame, counters: dict, grid_size: int):
+        """Visualizes the counters in 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=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:
+                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["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: 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(
+            (orders_width, orders_height),
+        )
+
+        bg_color = colors[config["GameWindow"]["background_color"]]
+        pygame.draw.rect(order_screen, bg_color, order_screen.get_rect())
+
+        order_rects_start = (orders_height // 2) - (grid_size // 2)
+        for idx, order in enumerate(state["orders"]):
+            order_upper_left = [
+                order_rects_start + idx * grid_size * 1.2,
+                order_rects_start,
+            ]
+            pygame.draw.rect(
+                order_screen,
+                colors["red"],
+                pygame.Rect(
+                    order_upper_left[0],
+                    order_upper_left[1],
+                    grid_size,
+                    grid_size,
+                ),
+                width=2,
+            )
+            center = np.array(order_upper_left)
+            self.draw_thing(
+                pos=center + (grid_size / 2),
+                parts=config["Plate"]["parts"],
+                screen=order_screen,
+                grid_size=grid_size,
+            )
+            self.draw_item(
+                pos=center + (grid_size / 2),
+                item={"type": order["meal"]},
+                plate=True,
+                screen=order_screen,
+                grid_size=grid_size,
+            )
+            order_done_seconds = (
+                (
+                    datetime.fromisoformat(order["start_time"])
+                    + timedelta(seconds=order["max_duration"])
+                )
+                - datetime.fromisoformat(state["env_time"])
+            ).total_seconds()
+
+            percentage = order_done_seconds / order["max_duration"]
+            self.draw_progress_bar(
+                pos=center + (grid_size / 2),
+                percent=percentage,
+                screen=order_screen,
+                grid_size=grid_size,
+                attention=percentage < 0.25,
+            )
+
+        orders_rect = order_screen.get_rect()
+        orders_rect.center = [
+            screen_margin + (orders_width // 2),
+            orders_height // 2,
+        ]
+        screen.blit(order_screen, orders_rect)
+
+    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))
+
+        flags = pygame.HIDDEN
+        screen = pygame.display.set_mode((width, height), flags=flags)
+
+        self.draw_gamescreen(screen, state, grid_size, [0 for _ in state["players"]])
+        pygame.image.save(screen, filename)
+
+    def get_state_image(self, grid_size: int, state: dict) -> npt.NDArray[np.uint8]:
+        width = int(np.ceil(state["kitchen"]["width"] * grid_size))
+        height = int(np.ceil(state["kitchen"]["height"] * grid_size))
+
+        flags = pygame.HIDDEN
+
+        if not self.observation_screen:
+            self.observation_screen = pygame.display.set_mode(
+                (width, height), flags=flags
+            )
+
+        self.draw_gamescreen(
+            self.observation_screen, state, grid_size, [0 for _ in state["players"]]
+        )
+
+        red = pygame.surfarray.array_red(self.observation_screen)
+        green = pygame.surfarray.array_green(self.observation_screen)
+        blue = pygame.surfarray.array_blue(self.observation_screen)
+        res = np.stack([red, green, blue], axis=2)
+        return res
+
+    def draw_recipe_image(self, screen, graph_dict, width, height, grid_size) -> None:
+        screen.fill(self.config["GameWindow"]["background_color"])
+        positions_dict = graph_dict["layout"]
+        positions = np.array(list(positions_dict.values()))
+        positions = positions - positions.min(axis=0)
+        positions[positions == 0] = 0.000001
+        positions = (
+            positions / positions.max(axis=0) * (np.array([width, height]) - grid_size)
+        )
+        positions += grid_size / 2
+
+        positions_dict = {
+            name: pos for name, pos in zip(positions_dict.keys(), positions)
+        }
+
+        for start, end in graph_dict["edges"]:
+            pygame.draw.line(
+                screen,
+                "black",
+                positions_dict[start],
+                positions_dict[end],
+                width=5,
+            )
+        for name, pos in positions_dict.items():
+            key = name.split("_")[0]
+            if key in [
+                "Chips",
+                "FriedFish",
+                "Burger",
+                "Salad",
+                "TomatoSoup",
+                "OnionSoup",
+                "FishAndChips",
+                "Pizza",
+            ]:
+                self.draw_thing(
+                    screen,
+                    np.array(pos),
+                    grid_size,
+                    self.config["Plate"]["parts"],
+                    absolute_size=grid_size,
+                )
+            if "Soup" in key:
+                self.draw_thing(
+                    screen,
+                    np.array(pos),
+                    grid_size,
+                    self.config[key + "Plate"]["parts"],
+                )
+            else:
+                viz = self.config[key]["parts"]
+                self.draw_thing(
+                    screen, np.array(pos), grid_size, viz, absolute_size=grid_size
+                )
+
+
+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()
+    pygame.font.init()
+    viz.save_state_image(grid_size=40, state=state, filename=filename)
+
+
+def generate_recipe_images(config: dict, folder_path: str | Path):
+    os.makedirs(ROOT_DIR / folder_path, exist_ok=True)
+    env = Environment(
+        env_config=str(ROOT_DIR / "configs" / "environment_config.yaml"),
+        layout_config=str(ROOT_DIR / "configs" / "layouts" / "basic.layout"),
+        item_info=str(ROOT_DIR / "configs" / "item_info.yaml"),
+        as_files=True,
+        env_name="0",
+    )
+
+    viz = Visualizer(config)
+    pygame.init()
+    pygame.font.init()
+
+    graph_dicts = env.get_recipe_graphs()
+    for graph_dict in graph_dicts:
+        width = 700
+        height = 400
+        grid_size = 80
+        flags = pygame.HIDDEN
+        screen = pygame.display.set_mode((width, height), flags=flags)
+        viz.draw_recipe_image(screen, graph_dict, width, height, grid_size)
+        pygame.image.save(screen, f"{folder_path}/{graph_dict['meal']}.png")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Cooperative Cuisine Image Generation",
+        description="Generate images for a state in json.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+    parser.add_argument(
+        "-s",
+        "--state",
+        type=argparse.FileType("r", encoding="UTF-8"),
+        default=ROOT_DIR / "pygame_2d_vis" / "sample_state.json",
+    )
+    parser.add_argument(
+        "-v",
+        "--visualization_config",
+        type=argparse.FileType("r", encoding="UTF-8"),
+        default=ROOT_DIR / "pygame_2d_vis" / "visualization.yaml",
+    )
+    parser.add_argument(
+        "-o",
+        "--output_file",
+        type=str,
+        default=ROOT_DIR / "pygame_2d_vis" / "generated" / "screenshot.jpg",
+    )
+    args = parser.parse_args()
+    with open(args.visualization_config, "r") as f:
+        viz_config = yaml.safe_load(f)
+    with open(args.state, "r") as f:
+        state = json.load(f)
+    save_screenshot(state, viz_config, args.output_file)
+    generate_recipe_images(viz_config, args.output_file.parent)
diff --git a/overcooked_simulator/gui_2d_vis/game_colors.py b/cooperative_cuisine/pygame_2d_vis/game_colors.py
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/game_colors.py
rename to cooperative_cuisine/pygame_2d_vis/game_colors.py
diff --git a/cooperative_cuisine/pygame_2d_vis/gui.py b/cooperative_cuisine/pygame_2d_vis/gui.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4ce3b3238cc01ae48a792277a1b2c84b61f4f22
--- /dev/null
+++ b/cooperative_cuisine/pygame_2d_vis/gui.py
@@ -0,0 +1,1835 @@
+import argparse
+import dataclasses
+import json
+import logging
+import os
+import random
+import signal
+import subprocess
+import sys
+import uuid
+from enum import Enum
+from subprocess import Popen
+
+import numpy as np
+import pygame
+import pygame_gui
+import requests
+import yaml
+from pygame import mixer
+from websockets.sync.client import connect
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.environment import (
+    Action,
+    ActionType,
+    InterActionData,
+)
+from cooperative_cuisine.game_server import CreateEnvironmentConfig
+from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
+from cooperative_cuisine.pygame_2d_vis.game_colors import colors
+from cooperative_cuisine.state_representation import StateRepresentation
+from cooperative_cuisine.utils import (
+    custom_asdict_factory,
+    url_and_port_arguments,
+    disable_websocket_logging_arguments,
+    add_list_of_manager_ids_arguments,
+)
+
+
+class MenuStates(Enum):
+    Start = "Start"
+    ControllerTutorial = "ControllerTutorial"
+    PreGame = "PreGame"
+    Game = "Game"
+    PostGame = "PostGame"
+    End = "End"
+
+
+log = logging.getLogger(__name__)
+
+
+class PlayerKeySet:
+    """Set of keyboard keys for controlling a player.
+    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,
+        move_keys: list[pygame.key],
+        interact_key: pygame.key,
+        pickup_key: pygame.key,
+        switch_key: pygame.key,
+        players: list[str],
+        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:
+            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.
+            joystick: number of joystick (later check if available)
+        """
+        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.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[str] = players
+        self.current_player: str = 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[str]) -> 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,
+        study_host: str,
+        study_port: int,
+        game_host: str,
+        game_port: int,
+        manager_ids: list[str],
+        CONNECT_WITH_STUDY_SERVER: bool,
+        USE_AAAMBOS_AGENT: bool,
+    ):
+        self.CONNECT_WITH_STUDY_SERVER = CONNECT_WITH_STUDY_SERVER
+        self.USE_AAAMBOS_AGENT = USE_AAAMBOS_AGENT
+
+        pygame.init()
+        pygame.display.set_icon(
+            pygame.image.load(ROOT_DIR / "pygame_2d_vis" / "images" / "fish3.png")
+        )
+
+        self.participant_id = uuid.uuid4().hex
+
+        self.game_screen: pygame.Surface = None
+        self.running = True
+
+        self.key_sets: list[PlayerKeySet] = []
+
+        self.websocket_url = f"ws://{game_host}:{game_port}/ws/player/"
+        self.websockets = {}
+
+        if CONNECT_WITH_STUDY_SERVER:
+            self.request_url = f"http://{study_host}:{study_port}"
+        else:
+            self.request_url = f"http://{game_host}:{game_port}"
+
+        self.manager_id = random.choice(manager_ids)
+
+        with open(ROOT_DIR / "pygame_2d_vis" / "visualization.yaml", "r") as file:
+            self.visualization_config = yaml.safe_load(file)
+
+        self.FPS = self.visualization_config["GameWindow"]["FPS"]
+
+        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_fullscreen,
+            self.window_height_fullscreen,
+        ) = pygame.display.get_desktop_sizes()[0]
+
+        if (
+            self.window_width_fullscreen >= 3840
+            and self.window_height_fullscreen >= 2160
+        ):
+            self.window_width_fullscreen /= 2
+            self.window_height_fullscreen /= 2
+
+        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.sub_processes = []
+
+        self.layout_file_paths = sorted(
+            (ROOT_DIR / "configs" / "layouts").rglob("*.layout")
+        )
+        self.current_layout_idx = 0
+
+        self.last_state: StateRepresentation
+
+        self.last_level = False
+        self.beeped_once = False
+        self.all_completed_meals = []
+        self.last_completed_meals = []
+        self.all_recipes_labels = []
+        self.last_recipes_labels = []
+
+    def setup_player_keys(self, players: list[str], number_key_sets=1, disjunct=False):
+        # 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.
+        if number_key_sets:
+            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 number_key_sets > 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[:number_key_sets]
+        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 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):
+                    if pressed:
+                        move_vec += key_set.move_vectors[idx]
+                if np.linalg.norm(move_vec) != 0:
+                    move_vec = move_vec / np.linalg.norm(move_vec)
+
+                action = Action(
+                    current_player_name,
+                    ActionType.MOVEMENT,
+                    move_vec,
+                    duration=self.time_delta,
+                )
+                self.send_action(action)
+
+    def handle_joy_stick_input(self, joysticks):
+        """Handles joystick inputs for movement every frame
+        Args:
+            joysticks: list of joysticks
+        """
+        # Axis 0: joy stick left: -1 = left, ~0 = center, 1 = right
+        # Axis 1: joy stick left: -1 = up, ~0 = center, 1 = down
+        # see control stuff here (at the end of the page): https://www.pygame.org/docs/ref/joystick.html
+        for key_set in self.key_sets:
+            current_player_name = str(key_set.current_player)
+            # if a joystick is connected for current player
+            if key_set.joystick in joysticks:
+                # Usually axis run in pairs, up/down for one, and left/right for the other. Triggers count as axes.
+                # You may want to take into account some tolerance to handle jitter, and
+                # joystick drift may keep the joystick from centering at 0 or using the full range of position values.
+                tolerance_threshold = 0.2
+                # axis 0 = joy stick left --> left & right
+                axis_left_right = joysticks[key_set.joystick].get_axis(0)
+                axis_up_down = joysticks[key_set.joystick].get_axis(1)
+                if (
+                    abs(axis_left_right) > tolerance_threshold
+                    or abs(axis_up_down) > tolerance_threshold
+                ):
+                    move_vec = np.zeros(2)
+                    if abs(axis_left_right) > tolerance_threshold:
+                        move_vec[0] += axis_left_right
+                    # axis 1 = joy stick right --> up & down
+                    if abs(axis_up_down) > tolerance_threshold:
+                        move_vec[1] += axis_up_down
+
+                    # if np.linalg.norm(move_vec) != 0:
+                    #     move_vec = move_vec / np.linalg.norm(move_vec)
+
+                    action = Action(
+                        current_player_name,
+                        ActionType.MOVEMENT,
+                        move_vec,
+                        duration=self.time_delta,
+                    )
+                    self.send_action(action)
+
+    def handle_key_event(self, event):
+        """Handles key events for the pickup and interaction keys. Pickup is a single action,
+        for interaction keydown and keyup is necessary, because the player has to be able to hold
+        the key down.
+
+        Args:
+            event: Pygame event for extracting the key action.
+        """
+
+        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(current_player_name, ActionType.PUT, "pickup")
+                self.send_action(action)
+
+            if event.key == key_set.interact_key:
+                if event.type == pygame.KEYDOWN:
+                    action = Action(
+                        current_player_name, ActionType.INTERACT, InterActionData.START
+                    )
+                    self.send_action(action)
+                elif event.type == pygame.KEYUP:
+                    action = Action(
+                        current_player_name, ActionType.INTERACT, InterActionData.STOP
+                    )
+                    self.send_action(action)
+            if event.key == key_set.switch_key:
+                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,
+        for interaction buttondown and buttonup is necessary, because the player has to be able to hold
+        the button down.
+
+        Args:
+            event: Pygame event for extracting the button action.
+            joysticks: list of joysticks
+        """
+        for key_set in self.key_sets:
+            current_player_name = str(key_set.current_player)
+            # if a joystick is connected for current player
+            if key_set.joystick in joysticks:
+                # pickup = Button A <-> 0
+                if (
+                    joysticks[key_set.joystick].get_button(0)
+                    and event.type == pygame.JOYBUTTONDOWN
+                ):
+                    action = Action(current_player_name, ActionType.PUT, "pickup")
+                    self.send_action(action)
+
+                # interact = Button X <-> 2
+                if (
+                    joysticks[key_set.joystick].get_button(2)
+                    and event.type == pygame.JOYBUTTONDOWN
+                ):
+                    action = Action(
+                        current_player_name, ActionType.INTERACT, InterActionData.START
+                    )
+                    self.send_action(action)
+                    # stop interaction if last pressed button was X <-> 2
+                if event.button == 2 and event.type == pygame.JOYBUTTONUP:
+                    action = Action(
+                        current_player_name, ActionType.INTERACT, InterActionData.STOP
+                    )
+                    self.send_action(action)
+                # switch button Y <-> 3
+                if joysticks[key_set.joystick].get_button(3):
+                    if event.type == pygame.JOYBUTTONDOWN:
+                        key_set.next_player()
+
+    def set_window_size(self):
+        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,
+        )
+
+    def reset_window_size(self):
+        self.game_width = 0
+        self.game_height = 0
+        self.set_window_size()
+
+    def set_game_size(self, max_width=None, max_height=None):
+        if max_width is None:
+            max_width = self.window_width - (2 * self.screen_margin)
+        if max_height is None:
+            max_height = self.window_height - (2 * self.screen_margin)
+
+        self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width
+        if self.kitchen_width > self.kitchen_height:
+            self.game_width = max_width
+            self.game_height = self.game_width * self.kitchen_aspect_ratio
+
+            if self.game_height > max_height:
+                self.game_height = max_height
+                self.game_width = self.game_height / self.kitchen_aspect_ratio
+        else:
+            self.game_height = max_height
+            self.game_width = self.game_height / self.kitchen_aspect_ratio
+
+            if self.game_width > max_width:
+                self.game_width = max_width
+                self.game_height = self.game_width * 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
+
+        self.game_screen = pygame.Surface(
+            (
+                self.game_width,
+                self.game_height,
+            )
+        )
+
+    def init_ui_elements(self):
+        self.manager = pygame_gui.UIManager((self.window_width, self.window_height))
+        self.manager.get_theme().load_theme(
+            ROOT_DIR / "pygame_2d_vis" / "gui_theme.json"
+        )
+
+        ########################################################################
+        # Start screen
+        ########################################################################
+        self.start_button = pygame_gui.elements.UIButton(
+            relative_rect=pygame.Rect(
+                (0, 0), (self.buttons_width, self.buttons_height)
+            ),
+            text="Start Game",
+            manager=self.manager,
+            anchors={"center": "center"},
+            object_id="#start_button",
+        )
+
+        img = pygame.image.load(
+            ROOT_DIR / "pygame_2d_vis" / "continue.drawio.png"
+        ).convert_alpha()
+
+        image_rect = img.get_rect()
+        image_rect.centery += 80
+        self.press_a_image = pygame_gui.elements.UIImage(
+            image_rect,
+            img,
+            manager=self.manager,
+            anchors={"centerx": "centerx", "centery": "centery"},
+        )
+        img_width = self.buttons_width * 1.5
+        img_height = img_width * (image_rect.height / image_rect.width)
+        new_dims = (img_width, img_height)
+        self.press_a_image.set_dimensions(new_dims)
+
+        rect = pygame.Rect((0, 0), (self.buttons_width, self.buttons_height))
+        rect.topright = (0, 0)
+        self.quit_button = pygame_gui.elements.UIButton(
+            relative_rect=rect,
+            text="Quit Game",
+            manager=self.manager,
+            object_id="#quit_button",
+            anchors={"right": "right", "top": "top"},
+        )
+
+        player_selection_rect = pygame.Rect(
+            (0, 0),
+            (
+                self.window_width * 0.9,
+                (self.window_height // 4),
+            ),
+        )
+        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",
+        )
+
+        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: -",
+            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: -",
+            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"},
+        )
+
+        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"},
+        )
+
+        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"},
+        )
+
+        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"},
+        )
+
+        ########################################################################
+        # Tutorial screen
+        ########################################################################
+
+        image = pygame.image.load(
+            ROOT_DIR / "pygame_2d_vis" / "tutorial_files" / "tutorial.drawio.png"
+        ).convert_alpha()
+        image_rect = image.get_rect()
+        image_rect.topleft = (20, self.buttons_height)
+        self.tutorial_image = pygame_gui.elements.UIImage(
+            image_rect,
+            image,
+            manager=self.manager,
+            anchors={"top": "top", "left": "left"},
+        )
+        img_width = self.window_width * 0.8
+        img_height = img_width * (image_rect.height / image_rect.width)
+        new_dims = (img_width, img_height)
+        self.tutorial_image.set_dimensions(new_dims)
+
+        button_rect = pygame.Rect((0, 0), (220, 80))
+        button_rect.bottom = -20
+        self.continue_button = pygame_gui.elements.UIButton(
+            relative_rect=button_rect,
+            text="Continue",
+            manager=self.manager,
+            anchors={"centerx": "centerx", "bottom": "bottom"},
+        )
+
+        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"},
+        )
+
+        ########################################################################
+        # PreGame screen
+        ########################################################################
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width, 50),
+        )
+        rect.top = 20
+        self.level_name_label = pygame_gui.elements.UILabel(
+            text=f"not set",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#level_name",
+            anchors={"centerx": "centerx", "top": "top"},
+        )
+
+        self.text_recipes_label = pygame_gui.elements.UILabel(
+            text=f"Recipes in this level:",
+            relative_rect=pygame.Rect(
+                (0, 0),
+                (self.window_width * 0.5, 50),
+            ),
+            manager=self.manager,
+            anchors={"centerx": "centerx", "top_target": self.level_name_label},
+        )
+
+        scroll_height = (
+            self.continue_button.get_abs_rect().top
+            - self.text_recipes_label.get_abs_rect().bottom
+        )
+
+        self.scroll_width = self.window_width
+        self.scroll_space = pygame_gui.elements.UIScrollingContainer(
+            relative_rect=pygame.Rect((0, 0), (self.scroll_width, scroll_height)),
+            manager=self.manager,
+            anchors={"centerx": "centerx", "top_target": self.text_recipes_label},
+        )
+
+        ########################################################################
+        # Game screen
+        ########################################################################
+
+        self.orders_label = pygame_gui.elements.UILabel(
+            text="Orders:",
+            relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin),
+            manager=self.manager,
+            object_id="#orders_label",
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width * 0.2, self.buttons_height),
+        )
+        rect.bottomleft = (0, 0)
+        self.score_label = pygame_gui.elements.UILabel(
+            text=f"Score not set",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#score_label",
+            anchors={"bottom": "bottom", "left": "left"},
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width * 0.4, self.buttons_height),
+        )
+        rect.bottom = 0
+        self.timer_label = pygame_gui.elements.UILabel(
+            text="GAMETIME not set",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#timer_label",
+            anchors={"bottom": "bottom", "centerx": "centerx"},
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width, self.screen_margin),
+        )
+        rect.right = 20
+        self.wait_players_label = pygame_gui.elements.UILabel(
+            text="WAITING FOR OTHER PLAYERS",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#wait_players_label",
+            anchors={"centery": "centery", "right": "right"},
+        )
+
+        ########################################################################
+        # PostGame screen
+        ########################################################################
+
+        rect = pygame.Rect((0, 0), (220, 80))
+        rect.bottom = -20
+        self.next_game_button = pygame_gui.elements.UIButton(
+            relative_rect=rect,
+            manager=self.manager,
+            text="Next game",
+            anchors={"centerx": "centerx", "bottom": "bottom"},
+            object_id="#split_players_button",
+        )
+
+        rect = pygame.Rect((0, 0), (220, 80))
+        rect.bottom = -20
+        self.finish_study_button = pygame_gui.elements.UIButton(
+            relative_rect=rect,
+            manager=self.manager,
+            text="Finish study",
+            anchors={"centerx": "centerx", "bottom": "bottom"},
+            object_id="#split_players_button",
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width, 50),
+        )
+        self.score_conclusion = pygame_gui.elements.UILabel(
+            text=f"not set",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#level_name",
+            anchors={"centerx": "centerx", "top_target": self.level_name_label},
+        )
+
+        self.completed_meals_text_label = pygame_gui.elements.UILabel(
+            text=f"Completed meals:",
+            relative_rect=pygame.Rect(
+                (0, 0),
+                (self.window_width * 0.7, 50),
+            ),
+            manager=self.manager,
+            object_id="#level_name",
+            anchors={"centerx": "centerx", "top_target": self.score_conclusion},
+        )
+
+        ########################################################################
+        # End screen
+        ########################################################################
+
+        conclusion_rect = pygame.Rect(
+            0, 0, self.window_width * 0.6, self.window_height * 0.4
+        )
+        self.thank_you_label = pygame_gui.elements.UILabel(
+            text="Thank you!",
+            relative_rect=conclusion_rect,
+            manager=self.manager,
+            object_id="#score_label",
+            anchors={"center": "center"},
+        )
+
+        ########################################################################
+
+        self.start_screen_elements = [
+            self.start_button,
+            self.quit_button,
+            self.fullscreen_button,
+            self.player_selection_container,
+            self.bot_number_container,
+            self.press_a_image,
+        ]
+
+        self.tutorial_screen_elements = [
+            self.tutorial_image,
+            self.continue_button,
+            self.quit_button,
+            self.fullscreen_button,
+        ]
+
+        self.pregame_screen_elements = [
+            self.level_name_label,
+            self.text_recipes_label,
+            self.scroll_space,
+            self.continue_button,
+            self.quit_button,
+            self.fullscreen_button,
+        ]
+
+        self.game_screen_elements = [
+            self.orders_label,
+            self.quit_button,
+            self.score_label,
+            self.timer_label,
+            self.wait_players_label,
+            self.fullscreen_button,
+        ]
+
+        self.postgame_screen_elements = [
+            self.score_conclusion,
+            self.quit_button,
+            self.scroll_space,
+            self.level_name_label,
+            self.next_game_button,
+            self.finish_study_button,
+            self.completed_meals_text_label,
+            self.fullscreen_button,
+        ]
+
+        self.end_screen_elements = [
+            self.fullscreen_button,
+            self.quit_button,
+            self.thank_you_label,
+        ]
+
+        self.rest = [
+            self.fullscreen_button,
+            self.quit_button,
+        ]
+
+    def show_screen_elements(self, elements: list):
+        for element in (
+            self.start_screen_elements
+            + self.tutorial_screen_elements
+            + self.pregame_screen_elements
+            + self.game_screen_elements
+            + self.postgame_screen_elements
+            + self.end_screen_elements
+            + self.rest
+        ):
+            element.hide()
+        for element in elements:
+            element.show()
+
+    def update_screen_elements(self):
+        match self.menu_state:
+            case MenuStates.Start:
+                self.show_screen_elements(self.start_screen_elements)
+
+                if self.CONNECT_WITH_STUDY_SERVER:
+                    self.bot_number_container.hide()
+
+                self.update_selection_elements()
+            case MenuStates.ControllerTutorial:
+                self.show_screen_elements(self.tutorial_screen_elements)
+
+                if self.CONNECT_WITH_STUDY_SERVER:
+                    self.get_game_connection(tutorial=True)
+                else:
+                    self.create_env_on_game_server(tutorial=True)
+                self.setup_game(tutorial=True)
+
+                self.set_game_size(
+                    max_height=self.window_height * 0.3,
+                    max_width=self.window_width * 0.3,
+                )
+                self.game_center = (
+                    self.window_width - self.game_width / 2 - 20,
+                    self.window_height - self.game_height / 2 - 20,
+                )
+            case MenuStates.PreGame:
+                self.init_ui_elements()
+                self.show_screen_elements(self.pregame_screen_elements)
+                self.update_pregame_screen()
+            case MenuStates.Game:
+                self.show_screen_elements(self.game_screen_elements)
+            case MenuStates.PostGame:
+                self.init_ui_elements()
+                self.show_screen_elements(self.postgame_screen_elements)
+                self.update_postgame_screen(self.last_state)
+                if self.last_level:
+                    self.next_game_button.hide()
+                    self.finish_study_button.show()
+                else:
+                    self.next_game_button.show()
+                    self.finish_study_button.hide()
+            case MenuStates.End:
+                self.show_screen_elements(self.end_screen_elements)
+
+    def draw_main_window(self):
+        self.main_window.fill(
+            colors[self.visualization_config["GameWindow"]["background_color"]]
+        )
+
+        match self.menu_state:
+            case MenuStates.ControllerTutorial:
+                self.draw_tutorial_screen_frame()
+            case MenuStates.Game:
+                self.draw_game_screen_frame()
+
+        self.manager.draw_ui(self.main_window)
+        self.manager.update(self.time_delta)
+        pygame.display.flip()
+
+    def draw_tutorial_screen_frame(self):
+        self.handle_keys()
+        self.handle_joy_stick_input(joysticks=self.joysticks)
+
+        state = self.request_state()
+        self.vis.draw_gamescreen(
+            self.game_screen,
+            state,
+            self.grid_size,
+            [int(k.current_player) for k in self.key_sets],
+        )
+
+        game_screen_rect = self.game_screen.get_rect()
+        game_screen_rect.center = self.game_center
+        self.main_window.blit(self.game_screen, game_screen_rect)
+
+    def update_postgame_screen(self, state):
+        score = state["score"]
+        self.score_conclusion.set_text(f"Your final score is {score}. Hurray!")
+        self.level_name_label.set_text(f"Completed: {self.level_info['name']}!")
+
+        served_meals = state["served_meals"]
+
+        row_height = 30
+        # icon_size = 50
+        container_width = self.scroll_width * 0.9
+        container_height = len(served_meals) * row_height
+
+        main_container = pygame_gui.elements.UIPanel(
+            relative_rect=pygame.Rect(
+                (0, 0),
+                (
+                    container_width,
+                    container_height,
+                ),
+            ),
+            object_id="#graph_container",
+            manager=self.manager,
+            container=self.scroll_space,
+        )
+
+        last_completed_meals = []
+
+        for idx, (player, meal) in enumerate(served_meals):
+            if idx == 0:
+                anchors = {"centerx": "centerx", "top": "top"}
+            else:
+                anchors = {
+                    "centerx": "centerx",
+                    "top_target": last_completed_meals[idx - 1],
+                }
+
+            container = pygame_gui.elements.UIPanel(
+                relative_rect=pygame.Rect(
+                    (0, 0),
+                    (
+                        container_width,
+                        row_height,
+                    ),
+                ),
+                object_id="#graph_container",
+                manager=self.manager,
+                container=main_container,
+                anchors=anchors,
+            )
+
+            text = f"Player {player} served meal {meal}."
+            meal_label = pygame_gui.elements.UILabel(
+                text=text,
+                relative_rect=pygame.Rect(
+                    (0, 0),
+                    (container_width, row_height),
+                ),
+                manager=self.manager,
+                container=container,
+                object_id="#recipe",
+                anchors={"centery": "centery", "left": "left"},
+            )
+            last_completed_meals.append(container)
+
+        self.scroll_space.set_scrollable_area_dimensions(
+            (self.scroll_width * 0.95, container_height)
+        )
+
+    def exit_game(self):
+        self.menu_state = MenuStates.PostGame
+
+        if self.CONNECT_WITH_STUDY_SERVER:
+            self.send_level_done()
+        else:
+            self.stop_game_on_server("finished_button_pressed")
+        self.disconnect_websockets()
+
+        self.update_postgame_screen(self.last_state)
+        self.update_screen_elements()
+        for el in self.last_completed_meals:
+            el.show()
+        self.beeped_once = False
+
+    def draw_game_screen_frame(self):
+        self.last_state = self.request_state()
+
+        self.handle_keys()
+        self.handle_joy_stick_input(joysticks=self.joysticks)
+
+        if not self.beeped_once and self.last_state["all_players_ready"]:
+            self.beeped_once = True
+            self.play_bell_sound()
+
+        if self.last_state["ended"]:
+            self.exit_game()
+
+        else:
+            self.draw_game(self.last_state)
+
+            game_screen_rect = self.game_screen.get_rect()
+
+            game_screen_rect.center = [
+                self.window_width // 2,
+                self.window_height // 2,
+            ]
+
+            self.main_window.blit(self.game_screen, game_screen_rect)
+
+            if not self.last_state["all_players_ready"]:
+                self.wait_players_label.show()
+            else:
+                self.wait_players_label.hide()
+
+    def draw_game(self, state):
+        """Main visualization function.
+
+        Args:            state: The game state returned by the environment."""
+        self.vis.draw_gamescreen(
+            self.game_screen,
+            state,
+            self.grid_size,
+            [int(k.current_player) for k in self.key_sets],
+        )
+
+        self.vis.draw_orders(
+            screen=self.main_window,
+            state=state,
+            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,
+        )
+
+        border = self.visualization_config["GameWindow"]["game_border_size"]
+        border_rect = pygame.Rect(
+            self.window_width // 2 - (self.game_width // 2) - border,
+            self.window_height // 2 - (self.game_height // 2) - border,
+            self.game_width + 2 * border,
+            self.game_height + 2 * border,
+        )
+        pygame.draw.rect(
+            self.main_window,
+            colors[self.visualization_config["GameWindow"]["game_border_color"]],
+            border_rect,
+            width=border,
+        )
+
+        self.update_score_label(state)
+        self.update_remaining_time(state["remaining_time"])
+
+        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 update_score_label(self, state):
+        score = state["score"]
+        self.score_label.set_text(f"Score {score}")
+
+    def update_remaining_time(self, remaining_time: float):
+        hours, rem = divmod(int(remaining_time), 3600)
+        minutes, seconds = divmod(rem, 60)
+        display_time = f"{minutes}:{'%02d' % seconds}"
+        self.timer_label.set_text(f"Time remaining: {display_time}")
+
+    def create_env_on_game_server(self, tutorial):
+        if tutorial:
+            layout_path = ROOT_DIR / "configs" / "layouts" / "tutorial.layout"
+            environment_config_path = ROOT_DIR / "configs" / "tutorial_env_config.yaml"
+        else:
+            environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml"
+            layout_path = self.layout_file_paths[self.current_layout_idx]
+
+        item_info_path = ROOT_DIR / "configs" / "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()
+        num_players = 1 if tutorial else self.number_players
+
+        seed = 1234
+        print("GUI MANAGER ID", self.manager_id)
+        creation_json = CreateEnvironmentConfig(
+            manager_id=self.manager_id,
+            number_players=num_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"]
+        if tutorial:
+            self.player_ids = [str(list(self.player_info.keys())[0])]
+
+        if tutorial:
+            self.key_sets = self.setup_player_keys(
+                players=["0"],
+                number_key_sets=1,
+                disjunct=False,
+            )
+        else:
+            num_key_set = 2 if self.multiple_keysets else 1
+            self.key_sets = self.setup_player_keys(
+                players=[str(i) for i in range(self.number_humans_to_be_added)],
+                number_key_sets=min(self.number_humans_to_be_added, num_key_set),
+                disjunct=self.split_players,
+            )
+
+    def update_pregame_screen(self):
+        self.level_name_label.set_text(f"Level: {self.level_info['name']}")
+
+        graph_width = self.window_width * 0.55
+        rows = 0
+        for rg in self.level_info["recipe_graphs"]:
+            rows += len(np.unique(np.array(list(rg["layout"].values()))[:, 1]))
+        row_height = self.window_height / 14
+        container_width = self.scroll_width * 0.9
+        container_height = rows * row_height
+        icon_size = row_height * 0.9
+
+        main_container = pygame_gui.elements.UIPanel(
+            relative_rect=pygame.Rect(
+                (0, 0),
+                (
+                    container_width,
+                    container_height,
+                ),
+            ),
+            object_id="#graph_container",
+            manager=self.manager,
+            container=self.scroll_space,
+        )
+
+        last_recipes_labels = []
+
+        for idx, rg in enumerate(self.level_info["recipe_graphs"]):
+            meal = rg["meal"]
+
+            positions = np.array(list(rg["layout"].values()))
+            unique_vals = np.unique(positions[:, 1])
+            height = row_height * len(unique_vals)
+
+            graph_height = height * 0.9
+            graph_surface = pygame.Surface(
+                (graph_width, graph_height), flags=pygame.SRCALPHA
+            )
+
+            self.vis.draw_recipe_image(
+                graph_surface, rg, graph_width, graph_height, icon_size
+            )
+
+            if idx == 0:
+                anchors = {"centerx": "centerx", "top": "top"}
+            else:
+                anchors = {
+                    "centerx": "centerx",
+                    "top_target": last_recipes_labels[idx - 1],
+                }
+
+            container = pygame_gui.elements.UIPanel(
+                relative_rect=pygame.Rect(
+                    (0, 0),
+                    (
+                        container_width,
+                        height,
+                    ),
+                ),
+                object_id="#graph_container",
+                manager=self.manager,
+                container=main_container,
+                anchors=anchors,
+            )
+
+            rect = pygame.Rect(
+                (0, 0),
+                (self.window_width / 5, height),
+            )
+            label = pygame_gui.elements.UILabel(
+                text=meal + ":",
+                relative_rect=rect,
+                manager=self.manager,
+                container=container,
+                object_id="#recipe_name",
+                anchors={"centery": "centery", "left": "left"},
+            )
+
+            rect = graph_surface.get_rect()
+            rect.right = 0
+            graph_image = pygame_gui.elements.UIImage(
+                relative_rect=rect,
+                image_surface=graph_surface,
+                manager=self.manager,
+                object_id="#recipe_graph",
+                container=container,
+                anchors={"centery": "centery", "right": "right"},
+            )
+
+            last_recipes_labels.append(container)
+
+        self.scroll_space.set_scrollable_area_dimensions(
+            (self.scroll_width * 0.95, container_height)
+        )
+
+    def get_game_connection(self, tutorial):
+        if self.menu_state == MenuStates.ControllerTutorial:
+            self.player_info = requests.post(
+                f"{self.request_url}/connect_to_tutorial/{self.participant_id}"
+            ).json()
+            self.player_info = {self.player_info["player_id"]: self.player_info}
+
+        else:
+            answer = requests.post(
+                f"{self.request_url}/get_game_connection/{self.participant_id}"
+            ).json()
+            self.player_info = answer["player_info"]
+            self.level_info = answer["level_info"]
+            self.last_level = self.level_info["last_level"]
+
+        if tutorial:
+            self.key_sets = self.setup_player_keys(["0"], 1, False)
+            self.vis.create_player_colors(1)
+        else:
+            self.number_players = (
+                self.number_humans_to_be_added + self.number_bots_to_be_added
+            )
+
+            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(
+                list(self.player_info.keys()),
+                min(self.number_humans_to_be_added, num_key_set),
+                self.split_players,
+            )
+        self.player_ids = list(self.player_info.keys())
+
+    def create_and_connect_bot(self, player_id, player_info):
+        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}"'
+        )
+        if self.USE_AAAMBOS_AGENT:
+            sub = Popen(
+                " ".join(
+                    [
+                        "exec",
+                        "aaambos",
+                        "run",
+                        "--arch_config",
+                        str(ROOT_DIR / "configs" / "agents" / "arch_config.yml"),
+                        "--run_config",
+                        str(ROOT_DIR / "configs" / "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,
+            )
+        else:
+            sub = Popen(
+                " ".join(
+                    [
+                        "python",
+                        str(ROOT_DIR / "configs" / "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)
+
+    def connect_websockets(self):
+        for p, (player_id, player_info) in enumerate(self.player_info.items()):
+            if p < self.number_humans_to_be_added:
+                # add player websockets
+                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:
+                # create bots and add bot websockets
+                self.create_and_connect_bot(player_id, player_info)
+
+            if p == 0:
+                self.state_player_id = player_id
+
+    def setup_game(self, tutorial=False):
+        self.connect_websockets()
+
+        state = self.request_state()
+
+        self.vis.create_player_colors(len(state["players"]))
+
+        self.kitchen_width = state["kitchen"]["width"]
+        self.kitchen_height = state["kitchen"]["height"]
+
+    def stop_game_on_server(self, reason: str) -> None:
+        log.debug(f"Stopping game: {reason}")
+        if not self.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": reason,
+                },
+            )
+
+    def send_tutorial_finished(self):
+        requests.post(
+            f"{self.request_url}/disconnect_from_tutorial/{self.participant_id}",
+        )
+
+    def finished_button_press(self):
+        if not self.CONNECT_WITH_STUDY_SERVER:
+            self.stop_game_on_server("finished_button_pressed")
+        self.menu_state = MenuStates.PostGame
+        log.debug("Pressed finished button")
+        self.update_screen_elements()
+
+    def fullscreen_button_press(self):
+        self.fullscreen = not self.fullscreen
+        self.set_window_size()
+        self.init_ui_elements()
+        self.set_game_size()
+        self.update_screen_elements()
+
+    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.number_players = (
+            self.number_humans_to_be_added + self.number_bots_to_be_added
+        )
+        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
+        )
+
+        self.number_players = (
+            self.number_humans_to_be_added + self.number_bots_to_be_added
+        )
+
+        text = "WASD+ARROW" if self.multiple_keysets else "WASD"
+        self.multiple_keysets_button.set_text(text)
+        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()
+
+        if self.number_players == 0:
+            self.start_button.disable()
+        else:
+            self.start_button.enable()
+
+    def send_action(self, action: Action):
+        """Sends an action to the game environment.
+
+        Args:
+            action: The action to be sent. Contains the player, action type and move direction if action is a movement.
+        """
+
+        if isinstance(action.action_data, np.ndarray):
+            action.action_data = [
+                float(action.action_data[0]),
+                float(action.action_data[1]),
+            ]
+
+        self.websockets[action.player].send(
+            json.dumps(
+                {
+                    "type": "action",
+                    "action": dataclasses.asdict(
+                        action, dict_factory=custom_asdict_factory
+                    ),
+                    "player_hash": self.player_info[action.player]["player_hash"],
+                }
+            )
+        )
+        self.websockets[action.player].recv()
+
+    def request_state(self):
+        self.websockets[self.state_player_id].send(
+            json.dumps(
+                {
+                    "type": "get_state",
+                    "player_hash": self.player_info[self.state_player_id][
+                        "player_hash"
+                    ],
+                }
+            )
+        )
+        state = json.loads(self.websockets[self.state_player_id].recv())
+        return state
+
+    def disconnect_websockets(self):
+        for sub in self.sub_processes:
+            try:
+                if self.USE_AAAMBOS_AGENT:
+                    pgrp = os.getpgid(sub.pid)
+                    os.killpg(pgrp, signal.SIGINT)
+                    subprocess.run(
+                        "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True
+                    )
+                else:
+                    sub.kill()
+
+            except ProcessLookupError:
+                pass
+
+        self.sub_processes = []
+        for websocket in self.websockets.values():
+            websocket.close()
+
+    def play_bell_sound(self):
+        bell_path = str(ROOT_DIR / "pygame_2d_vis" / "sync_bell.wav")
+        mixer.init()
+        mixer.music.load(bell_path)
+        mixer.music.set_volume(0.9)
+        mixer.music.play()
+        log.log(logging.INFO, "Started game, played bell sound")
+
+    def start_study(self):
+        self.player_info = requests.post(
+            f"{self.request_url}/start_study/{self.participant_id}/{self.number_humans_to_be_added}"
+        ).json()
+        self.last_level = False
+
+    def send_level_done(self):
+        _ = requests.post(f"{self.request_url}/level_done/{self.participant_id}").json()
+
+    def button_continue_postgame_pressed(self):
+        if not self.CONNECT_WITH_STUDY_SERVER:
+            self.current_layout_idx += 1
+            if self.current_layout_idx == len(self.layout_file_paths) - 1:
+                self.last_level = True
+            else:
+                log.debug(f"LEVEL: {self.layout_file_paths[self.current_layout_idx]}")
+        else:
+            if not self.last_level:
+                if self.CONNECT_WITH_STUDY_SERVER:
+                    self.get_game_connection(tutorial=False)
+                else:
+                    self.create_env_on_game_server(tutorial=False)
+        self.menu_state = MenuStates.PreGame
+
+    def manage_button_event(self, event):
+        if event.ui_element == self.quit_button:
+            self.running = False
+            self.disconnect_websockets()
+            self.stop_game_on_server("Quit button")
+            self.menu_state = MenuStates.Start
+            log.debug("Pressed quit button")
+            return
+
+        elif event.ui_element == self.fullscreen_button:
+            self.fullscreen_button_press()
+            log.debug("Pressed fullscreen button")
+            return
+
+        # Filter by shown screen page
+        match self.menu_state:
+            ############################################
+            case MenuStates.Start:
+                match event.ui_element:
+                    case self.start_button:
+                        if not (
+                            self.number_humans_to_be_added
+                            + self.number_bots_to_be_added
+                        ):
+                            pass
+                        else:
+                            self.menu_state = MenuStates.ControllerTutorial
+
+                    case self.add_human_player_button:
+                        self.number_humans_to_be_added += 1
+                    case self.add_bot_button:
+                        self.number_bots_to_be_added += 1
+
+                    case self.remove_human_button:
+                        self.number_humans_to_be_added = max(
+                            self.player_minimum, self.number_humans_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 = 0
+
+            ############################################
+
+            case MenuStates.ControllerTutorial:
+                match event.ui_element:
+                    case self.continue_button:
+                        self.exit_tutorial()
+
+            ############################################
+
+            case MenuStates.PreGame:
+                match event.ui_element:
+                    case self.continue_button:
+                        self.setup_game()
+
+                        self.set_game_size()
+                        self.menu_state = MenuStates.Game
+
+            ############################################
+
+            case MenuStates.Game:
+                pass
+                # match event.ui_element:
+                #     case self.finished_button:
+                #         self.menu_state = MenuStates.PostGame
+                #         self.disconnect_websockets()
+                #         self.finished_button_press()
+                #         self.handle_joy_stick_input(joysticks=self.joysticks)
+                #
+                #         if self.CONNECT_WITH_STUDY_SERVER:
+                #             self.send_level_done()
+
+            ############################################
+
+            case MenuStates.PostGame:
+                match event.ui_element:
+                    case self.next_game_button:
+                        self.button_continue_postgame_pressed()
+
+                    case self.finish_study_button:
+                        self.menu_state = MenuStates.End
+
+            ############################################
+
+            case MenuStates.End:
+                match event.ui_element:
+                    case other:
+                        pass
+
+    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.font.init()
+        self.comic_sans = pygame.font.SysFont("Comic Sans MS", 30)
+
+        pygame.display.set_caption("Cooperative Cuisine")
+
+        clock = pygame.time.Clock()
+
+        self.reset_window_size()
+        self.init_ui_elements()
+        self.reset_window_size()
+        self.reset_gui_values()
+        self.update_screen_elements()
+
+        # Game loop
+        self.running = True
+        # 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.
+        self.joysticks = {}
+
+        while self.running:
+            try:
+                self.time_delta = clock.tick(self.FPS) / 1000
+
+                # PROCESSING EVENTS
+                for event in pygame.event.get():
+                    if event.type == pygame.QUIT:
+                        self.disconnect_websockets()
+                        self.running = False
+
+                    # connect joystick
+                    if (
+                        pygame.joystick.get_count() > 0
+                        and 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)
+                        self.joysticks[joy.get_instance_id()] = joy
+                        print(f"Joystick {joy.get_instance_id()} connected")
+
+                    # disconnect joystick
+                    if event.type == pygame.JOYDEVICEREMOVED:
+                        del self.joysticks[event.instance_id]
+                        print(f"Joystick {event.instance_id} disconnected")
+                        print("Number of joysticks:", pygame.joystick.get_count())
+
+                    # Press enter key or controller start button instead of mouse button press
+                    if (
+                        event.type == pygame.JOYBUTTONDOWN
+                        and any(
+                            [
+                                self.joysticks and self.joysticks[i].get_button(7)
+                                for i in range(len(self.joysticks))
+                            ]
+                        )
+                        or (
+                            event.type == pygame.KEYDOWN
+                            and event.key == pygame.K_RETURN
+                        )
+                    ):
+                        match self.menu_state:
+                            case MenuStates.Start:
+                                self.menu_state = MenuStates.ControllerTutorial
+                                self.update_screen_elements()
+                            case MenuStates.ControllerTutorial:
+                                self.exit_tutorial()
+                                self.update_screen_elements()
+                            case MenuStates.PreGame:
+                                self.setup_game()
+                                self.set_game_size()
+                                self.menu_state = MenuStates.Game
+                                self.update_screen_elements()
+                            case MenuStates.PostGame:
+                                if self.last_level:
+                                    self.menu_state = MenuStates.End
+                                else:
+                                    self.button_continue_postgame_pressed()
+                                self.update_screen_elements()
+
+                    # if event.type == pygame.MOUSEWHEEL:
+                    #     print(event.x, event.y)
+                    #     self.scroll_space.process_event(event)
+
+                    if event.type == pygame_gui.UI_BUTTON_PRESSED:
+                        self.manage_button_event(event)
+                        self.update_screen_elements()
+
+                    if event.type in [
+                        pygame.KEYDOWN,
+                        pygame.KEYUP,
+                    ] and self.menu_state in [
+                        MenuStates.Game,
+                        MenuStates.ControllerTutorial,
+                    ]:
+                        self.handle_key_event(event)
+
+                    if event.type in [
+                        pygame.JOYBUTTONDOWN,
+                        pygame.JOYBUTTONUP,
+                    ] and self.menu_state in [
+                        MenuStates.Game,
+                        MenuStates.ControllerTutorial,
+                    ]:
+                        self.handle_joy_stick_event(event, joysticks=self.joysticks)
+
+                    self.manager.process_events(event)
+
+                # DRAWING
+                self.draw_main_window()
+
+            except (KeyboardInterrupt, SystemExit):
+                self.running = False
+                self.disconnect_websockets()
+                if not self.CONNECT_WITH_STUDY_SERVER:
+                    self.stop_game_on_server("Program exited.")
+
+        self.disconnect_websockets()
+        if not self.CONNECT_WITH_STUDY_SERVER:
+            self.stop_game_on_server("Program exited.")
+        pygame.quit()
+        sys.exit()
+
+    def exit_tutorial(self):
+        self.menu_state = MenuStates.PreGame
+        if self.CONNECT_WITH_STUDY_SERVER:
+            self.send_tutorial_finished()
+            self.start_study()
+            self.get_game_connection(tutorial=False)
+
+        else:
+            self.stop_game_on_server("tutorial_finished")
+            self.create_env_on_game_server(tutorial=False)
+
+        self.disconnect_websockets()
+
+
+def main(
+    study_url: str,
+    study_port: int,
+    game_url: str,
+    game_port: int,
+    manager_ids: list[str],
+    CONNECT_WITH_STUDY_SERVER=False,
+    USE_AAAMBOS_AGENT=False,
+):
+    # setup_logging()
+    gui = PyGameGUI(
+        study_host=study_url,
+        study_port=study_port,
+        game_host=game_url,
+        game_port=game_port,
+        manager_ids=manager_ids,
+        CONNECT_WITH_STUDY_SERVER=CONNECT_WITH_STUDY_SERVER,
+        USE_AAAMBOS_AGENT=USE_AAAMBOS_AGENT,
+    )
+    gui.start_pygame()
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Cooperative Cuisine 2D PyGame Visualization",
+        description="PyGameGUI: a PyGame 2D Visualization window.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+
+    url_and_port_arguments(parser)
+    disable_websocket_logging_arguments(parser)
+    add_list_of_manager_ids_arguments(parser)
+    args = parser.parse_args()
+    main(
+        args.study_url,
+        args.study_port,
+        args.game_url,
+        args.game_port,
+        args.manager_ids,
+        CONNECT_WITH_STUDY_SERVER=True,
+    )
diff --git a/cooperative_cuisine/pygame_2d_vis/gui_theme.json b/cooperative_cuisine/pygame_2d_vis/gui_theme.json
new file mode 100644
index 0000000000000000000000000000000000000000..0f3e1bf43358abc04837667e5c18c812bde10e06
--- /dev/null
+++ b/cooperative_cuisine/pygame_2d_vis/gui_theme.json
@@ -0,0 +1,199 @@
+{
+  "defaults": {
+    "colours": {
+      "normal_bg": "#45494e",
+      "hovered_bg": "#35393e",
+      "disabled_bg": "#25292e",
+      "selected_bg": "#193754",
+      "dark_bg": "#15191e",
+      "normal_text": "#000000",
+      "hovered_text": "#FFFFFF",
+      "selected_text": "#FFFFFF",
+      "disabled_text": "#6d736f",
+      "link_text": "#0000EE",
+      "link_hover": "#2020FF",
+      "link_selected": "#551A8B",
+      "text_shadow": "#777777",
+      "normal_border": "#DDDDDD",
+      "hovered_border": "#B0B0B0",
+      "disabled_border": "#808080",
+      "selected_border": "#8080B0",
+      "active_border": "#8080B0",
+      "filled_bar": "#f4251b",
+      "unfilled_bar": "#CCCCCC"
+    }
+  },
+  "button": {
+    "colours": {
+      "normal_bg": "#f3f2f2",
+      "hovered_bg": "#35393e",
+      "disabled_bg": "#25292e",
+      "selected_bg": "#193754",
+      "active_bg": "#193754",
+      "dark_bg": "#15191e",
+      "normal_text": "#000000",
+      "hovered_text": "#ffffff",
+      "selected_text": "#ffffff",
+      "disabled_text": "#6d736f",
+      "active_text": "#ffffff",
+      "normal_border": "#000000",
+      "hovered_border": "#B0B0B0",
+      "disabled_border": "#808080",
+      "selected_border": "#8080B0",
+      "active_border": "#8080B0"
+    },
+    "misc": {
+      "tool_tip_delay": "1.5"
+    },
+    "font": {
+      "size": 15,
+      "bold": 1
+    }
+  },
+  "#timer_label": {
+    "colours": {
+      "normal_text": "#000000"
+    },
+    "font": {
+      "size": 20,
+      "bold": 0
+    }
+  },
+  "#score_label": {
+    "colours": {
+      "normal_text": "#000000"
+    },
+    "font": {
+      "size": 20,
+      "bold": 1
+    }
+  },
+  "#orders_label": {
+    "colours": {
+      "normal_text": "#000000"
+    },
+    "font": {
+      "size": 20,
+      "bold": 0
+    }
+  },
+  "#quit_button": {
+    "colours": {
+      "normal_bg": "#f71b29",
+      "hovered_bg": "#bf0310",
+      "normal_border": "#000000",
+      "normal_text": "#000000"
+    }
+  },
+  "#reset_button": {
+    "colours": {
+      "normal_bg": "#f7a31b",
+      "hovered_bg": "#f58203",
+      "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"
+    }
+  },
+  "#wait_players_label": {
+    "colours": {
+      "normal_text": "#ff0000"
+    },
+    "font": {
+      "size": 45,
+      "bold": 1
+    }
+  },
+  "#level_name": {
+    "font": {
+      "size": 30,
+      "bold": 1,
+      "colour": "#000000"
+    }
+  },
+  "#recipe_name": {
+    "font": {
+      "size": 20,
+      "bold": 1,
+      "colour": "#000000"
+    }
+  },
+  "#graph_container": {
+    "colours": {
+      "dark_bg": "#fffacd",
+      "normal_border": "#fffacd",
+      "hovered_border": "#fffacd",
+      "disabled_border": "#fffacd",
+      "selected_border": "#fffacd",
+      "active_border": "#fffacd"
+    },
+    "misc": {
+      "shape": "rounded_rectangle",
+      "border_width": "0",
+      "shadow_width": "0"
+    }
+  }
+}
diff --git a/cooperative_cuisine/pygame_2d_vis/images/arrow_right.png b/cooperative_cuisine/pygame_2d_vis/images/arrow_right.png
new file mode 100644
index 0000000000000000000000000000000000000000..a1ea0946b67e89bed858a0312ed6ee70ea68c1c7
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/arrow_right.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/basket.png b/cooperative_cuisine/pygame_2d_vis/images/basket.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/basket.png
rename to cooperative_cuisine/pygame_2d_vis/images/basket.png
diff --git a/overcooked_simulator/gui_2d_vis/images/bell_gold.png b/cooperative_cuisine/pygame_2d_vis/images/bell_gold.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/bell_gold.png
rename to cooperative_cuisine/pygame_2d_vis/images/bell_gold.png
diff --git a/overcooked_simulator/gui_2d_vis/images/bell_silver.png b/cooperative_cuisine/pygame_2d_vis/images/bell_silver.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/bell_silver.png
rename to cooperative_cuisine/pygame_2d_vis/images/bell_silver.png
diff --git a/overcooked_simulator/gui_2d_vis/images/bun.png b/cooperative_cuisine/pygame_2d_vis/images/bun.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/bun.png
rename to cooperative_cuisine/pygame_2d_vis/images/bun.png
diff --git a/overcooked_simulator/gui_2d_vis/images/burger.png b/cooperative_cuisine/pygame_2d_vis/images/burger.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/burger.png
rename to cooperative_cuisine/pygame_2d_vis/images/burger.png
diff --git a/overcooked_simulator/gui_2d_vis/images/cheese3.png b/cooperative_cuisine/pygame_2d_vis/images/cheese3.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/cheese3.png
rename to cooperative_cuisine/pygame_2d_vis/images/cheese3.png
diff --git a/overcooked_simulator/gui_2d_vis/images/chopped_fish.png b/cooperative_cuisine/pygame_2d_vis/images/chopped_fish.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/chopped_fish.png
rename to cooperative_cuisine/pygame_2d_vis/images/chopped_fish.png
diff --git a/overcooked_simulator/gui_2d_vis/images/cooked_patty.png b/cooperative_cuisine/pygame_2d_vis/images/cooked_patty.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/cooked_patty.png
rename to cooperative_cuisine/pygame_2d_vis/images/cooked_patty.png
diff --git a/cooperative_cuisine/pygame_2d_vis/images/counter2.png b/cooperative_cuisine/pygame_2d_vis/images/counter2.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e88163e958c39f2186412e4838a3f9c08660ede
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/counter2.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/images/counter4.png b/cooperative_cuisine/pygame_2d_vis/images/counter4.png
new file mode 100644
index 0000000000000000000000000000000000000000..ad21220dfcb320f3cb5cadafbfb36afc30337064
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/counter4.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/images/counter5.png b/cooperative_cuisine/pygame_2d_vis/images/counter5.png
new file mode 100644
index 0000000000000000000000000000000000000000..646589514d340a8a02a1ab6c8e6143d139c93ffb
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/counter5.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/cut_fish.png b/cooperative_cuisine/pygame_2d_vis/images/cut_fish.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/cut_fish.png
rename to cooperative_cuisine/pygame_2d_vis/images/cut_fish.png
diff --git a/overcooked_simulator/gui_2d_vis/images/cutting_board_large.png b/cooperative_cuisine/pygame_2d_vis/images/cutting_board_large.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/cutting_board_large.png
rename to cooperative_cuisine/pygame_2d_vis/images/cutting_board_large.png
diff --git a/overcooked_simulator/gui_2d_vis/images/drip2.png b/cooperative_cuisine/pygame_2d_vis/images/drip2.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/drip2.png
rename to cooperative_cuisine/pygame_2d_vis/images/drip2.png
diff --git a/cooperative_cuisine/pygame_2d_vis/images/fire.png b/cooperative_cuisine/pygame_2d_vis/images/fire.png
new file mode 100644
index 0000000000000000000000000000000000000000..4d2e5237cbd6125eaeaa7781075a7dc382ab904e
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/fire.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/images/fire2.png b/cooperative_cuisine/pygame_2d_vis/images/fire2.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f28ad6e87d7e5985a1dbf9d72d13643a7936b00
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/fire2.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/images/fire3.png b/cooperative_cuisine/pygame_2d_vis/images/fire3.png
new file mode 100644
index 0000000000000000000000000000000000000000..65b883b7d5663f7c8f99032c9939e3f4479e03f0
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/fire3.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/images/fire_extinguisher.png b/cooperative_cuisine/pygame_2d_vis/images/fire_extinguisher.png
new file mode 100644
index 0000000000000000000000000000000000000000..a03d2d39285c6e286df992826af27c4f8ffc8d16
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/fire_extinguisher.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fish3.png b/cooperative_cuisine/pygame_2d_vis/images/fish3.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/fish3.png
rename to cooperative_cuisine/pygame_2d_vis/images/fish3.png
diff --git a/overcooked_simulator/gui_2d_vis/images/fried_fish.png b/cooperative_cuisine/pygame_2d_vis/images/fried_fish.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/fried_fish.png
rename to cooperative_cuisine/pygame_2d_vis/images/fried_fish.png
diff --git a/overcooked_simulator/gui_2d_vis/images/fries2.png b/cooperative_cuisine/pygame_2d_vis/images/fries2.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/fries2.png
rename to cooperative_cuisine/pygame_2d_vis/images/fries2.png
diff --git a/overcooked_simulator/gui_2d_vis/images/grated_cheese.png b/cooperative_cuisine/pygame_2d_vis/images/grated_cheese.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/grated_cheese.png
rename to cooperative_cuisine/pygame_2d_vis/images/grated_cheese.png
diff --git a/overcooked_simulator/gui_2d_vis/images/lettuce_cut_smaller.png b/cooperative_cuisine/pygame_2d_vis/images/lettuce_cut_smaller.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/lettuce_cut_smaller.png
rename to cooperative_cuisine/pygame_2d_vis/images/lettuce_cut_smaller.png
diff --git a/overcooked_simulator/gui_2d_vis/images/lettuce_smaller.png b/cooperative_cuisine/pygame_2d_vis/images/lettuce_smaller.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/lettuce_smaller.png
rename to cooperative_cuisine/pygame_2d_vis/images/lettuce_smaller.png
diff --git a/overcooked_simulator/gui_2d_vis/images/meat.png b/cooperative_cuisine/pygame_2d_vis/images/meat.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/meat.png
rename to cooperative_cuisine/pygame_2d_vis/images/meat.png
diff --git a/overcooked_simulator/gui_2d_vis/images/onion_cut.png b/cooperative_cuisine/pygame_2d_vis/images/onion_cut.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/onion_cut.png
rename to cooperative_cuisine/pygame_2d_vis/images/onion_cut.png
diff --git a/overcooked_simulator/gui_2d_vis/images/onion_large.png b/cooperative_cuisine/pygame_2d_vis/images/onion_large.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/onion_large.png
rename to cooperative_cuisine/pygame_2d_vis/images/onion_large.png
diff --git a/overcooked_simulator/gui_2d_vis/images/onion_soup_plate.png b/cooperative_cuisine/pygame_2d_vis/images/onion_soup_plate.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/onion_soup_plate.png
rename to cooperative_cuisine/pygame_2d_vis/images/onion_soup_plate.png
diff --git a/overcooked_simulator/gui_2d_vis/images/onion_soup_pot.png b/cooperative_cuisine/pygame_2d_vis/images/onion_soup_pot.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/onion_soup_pot.png
rename to cooperative_cuisine/pygame_2d_vis/images/onion_soup_pot.png
diff --git a/overcooked_simulator/gui_2d_vis/images/overcooked-end-screen.png b/cooperative_cuisine/pygame_2d_vis/images/overcooked-end-screen.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/overcooked-end-screen.png
rename to cooperative_cuisine/pygame_2d_vis/images/overcooked-end-screen.png
diff --git a/overcooked_simulator/gui_2d_vis/images/overcooked-level-screen.png b/cooperative_cuisine/pygame_2d_vis/images/overcooked-level-screen.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/overcooked-level-screen.png
rename to cooperative_cuisine/pygame_2d_vis/images/overcooked-level-screen.png
diff --git a/overcooked_simulator/gui_2d_vis/images/overcooked-start-screen.png b/cooperative_cuisine/pygame_2d_vis/images/overcooked-start-screen.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/overcooked-start-screen.png
rename to cooperative_cuisine/pygame_2d_vis/images/overcooked-start-screen.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pan.png b/cooperative_cuisine/pygame_2d_vis/images/pan.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pan.png
rename to cooperative_cuisine/pygame_2d_vis/images/pan.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pixel_cook.png b/cooperative_cuisine/pygame_2d_vis/images/pixel_cook.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pixel_cook.png
rename to cooperative_cuisine/pygame_2d_vis/images/pixel_cook.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pixel_cook_masked.png b/cooperative_cuisine/pygame_2d_vis/images/pixel_cook_masked.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pixel_cook_masked.png
rename to cooperative_cuisine/pygame_2d_vis/images/pixel_cook_masked.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza.png b/cooperative_cuisine/pygame_2d_vis/images/pizza.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pizza.png
rename to cooperative_cuisine/pygame_2d_vis/images/pizza.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza_base.png b/cooperative_cuisine/pygame_2d_vis/images/pizza_base.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pizza_base.png
rename to cooperative_cuisine/pygame_2d_vis/images/pizza_base.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza_dough.png b/cooperative_cuisine/pygame_2d_vis/images/pizza_dough.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pizza_dough.png
rename to cooperative_cuisine/pygame_2d_vis/images/pizza_dough.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza_wood.png b/cooperative_cuisine/pygame_2d_vis/images/pizza_wood.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pizza_wood.png
rename to cooperative_cuisine/pygame_2d_vis/images/pizza_wood.png
diff --git a/overcooked_simulator/gui_2d_vis/images/plate.png b/cooperative_cuisine/pygame_2d_vis/images/plate.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/plate.png
rename to cooperative_cuisine/pygame_2d_vis/images/plate.png
diff --git a/overcooked_simulator/gui_2d_vis/images/plate_clean.png b/cooperative_cuisine/pygame_2d_vis/images/plate_clean.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/plate_clean.png
rename to cooperative_cuisine/pygame_2d_vis/images/plate_clean.png
diff --git a/overcooked_simulator/gui_2d_vis/images/plate_dirty.png b/cooperative_cuisine/pygame_2d_vis/images/plate_dirty.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/plate_dirty.png
rename to cooperative_cuisine/pygame_2d_vis/images/plate_dirty.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pot.png b/cooperative_cuisine/pygame_2d_vis/images/pot.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pot.png
rename to cooperative_cuisine/pygame_2d_vis/images/pot.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pot_large.png b/cooperative_cuisine/pygame_2d_vis/images/pot_large.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pot_large.png
rename to cooperative_cuisine/pygame_2d_vis/images/pot_large.png
diff --git a/overcooked_simulator/gui_2d_vis/images/pot_smaller.png b/cooperative_cuisine/pygame_2d_vis/images/pot_smaller.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/pot_smaller.png
rename to cooperative_cuisine/pygame_2d_vis/images/pot_smaller.png
diff --git a/overcooked_simulator/gui_2d_vis/images/potato2.png b/cooperative_cuisine/pygame_2d_vis/images/potato2.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/potato2.png
rename to cooperative_cuisine/pygame_2d_vis/images/potato2.png
diff --git a/overcooked_simulator/gui_2d_vis/images/raw_fries.png b/cooperative_cuisine/pygame_2d_vis/images/raw_fries.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/raw_fries.png
rename to cooperative_cuisine/pygame_2d_vis/images/raw_fries.png
diff --git a/overcooked_simulator/gui_2d_vis/images/raw_patty.png b/cooperative_cuisine/pygame_2d_vis/images/raw_patty.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/raw_patty.png
rename to cooperative_cuisine/pygame_2d_vis/images/raw_patty.png
diff --git a/overcooked_simulator/gui_2d_vis/images/salad.png b/cooperative_cuisine/pygame_2d_vis/images/salad.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/salad.png
rename to cooperative_cuisine/pygame_2d_vis/images/salad.png
diff --git a/overcooked_simulator/gui_2d_vis/images/sausage.png b/cooperative_cuisine/pygame_2d_vis/images/sausage.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/sausage.png
rename to cooperative_cuisine/pygame_2d_vis/images/sausage.png
diff --git a/overcooked_simulator/gui_2d_vis/images/sausage_chopped.png b/cooperative_cuisine/pygame_2d_vis/images/sausage_chopped.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/sausage_chopped.png
rename to cooperative_cuisine/pygame_2d_vis/images/sausage_chopped.png
diff --git a/overcooked_simulator/gui_2d_vis/images/sink.png b/cooperative_cuisine/pygame_2d_vis/images/sink.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/sink.png
rename to cooperative_cuisine/pygame_2d_vis/images/sink.png
diff --git a/overcooked_simulator/gui_2d_vis/images/sink1.png b/cooperative_cuisine/pygame_2d_vis/images/sink1.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/sink1.png
rename to cooperative_cuisine/pygame_2d_vis/images/sink1.png
diff --git a/overcooked_simulator/gui_2d_vis/images/sink_large.png b/cooperative_cuisine/pygame_2d_vis/images/sink_large.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/sink_large.png
rename to cooperative_cuisine/pygame_2d_vis/images/sink_large.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato.png b/cooperative_cuisine/pygame_2d_vis/images/tomato.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato3_cut_smaller.png b/cooperative_cuisine/pygame_2d_vis/images/tomato3_cut_smaller.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato3_cut_smaller.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato3_cut_smaller.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato3_smaller.png b/cooperative_cuisine/pygame_2d_vis/images/tomato3_smaller.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato3_smaller.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato3_smaller.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_cut.png b/cooperative_cuisine/pygame_2d_vis/images/tomato_cut.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato_cut.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato_cut.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_soup.png b/cooperative_cuisine/pygame_2d_vis/images/tomato_soup.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato_soup.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato_soup.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_soup_plate.png b/cooperative_cuisine/pygame_2d_vis/images/tomato_soup_plate.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato_soup_plate.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato_soup_plate.png
diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_soup_pot.png b/cooperative_cuisine/pygame_2d_vis/images/tomato_soup_pot.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/tomato_soup_pot.png
rename to cooperative_cuisine/pygame_2d_vis/images/tomato_soup_pot.png
diff --git a/overcooked_simulator/gui_2d_vis/images/trash3.png b/cooperative_cuisine/pygame_2d_vis/images/trash3.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/images/trash3.png
rename to cooperative_cuisine/pygame_2d_vis/images/trash3.png
diff --git a/cooperative_cuisine/pygame_2d_vis/press_a.drawio.png b/cooperative_cuisine/pygame_2d_vis/press_a.drawio.png
new file mode 100644
index 0000000000000000000000000000000000000000..a79928d56e382d0a78a1dd6890946ef3547a570d
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/press_a.drawio.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/sample_state.json b/cooperative_cuisine/pygame_2d_vis/sample_state.json
new file mode 100644
index 0000000000000000000000000000000000000000..8d89362d6b345051b09cadadfc7dd72fa5898bb7
--- /dev/null
+++ b/cooperative_cuisine/pygame_2d_vis/sample_state.json
@@ -0,0 +1,751 @@
+{
+  "players": [
+    {
+      "id": "0",
+      "pos": [
+        3.0,
+        4.0
+      ],
+      "facing_direction": [
+        0,
+        1
+      ],
+      "holding": null,
+      "current_nearest_counter_pos": null,
+      "current_nearest_counter_id": null
+    }
+  ],
+  "counters": [
+    {
+      "id": "819fdb02a7fb4c42b45ffa5be4e4a475",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        0.0,
+        0.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "2a9bc5a9a2a8404686ba4ce7be7b7ded",
+      "category": "Counter",
+      "type": "Stove",
+      "pos": [
+        1.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "ba22928384814741bb1b40a8e9d90e20",
+        "category": "ItemCookingEquipment",
+        "type": "Pan",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": [],
+        "content_list": [],
+        "content_ready": null
+      },
+      "active_effects": []
+    },
+    {
+      "id": "76fed17b6b8e44f8b5058ef1c8b7e985",
+      "category": "Counter",
+      "type": "Stove",
+      "pos": [
+        2.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "018b874741e74b6e886ae3d618ab4c5b",
+        "category": "ItemCookingEquipment",
+        "type": "Pot",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": [],
+        "content_list": [],
+        "content_ready": null
+      },
+      "active_effects": []
+    },
+    {
+      "id": "fc11c7d9139149c0841772e61237bb36",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        3.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "c368566316be499199798a66e6e67936",
+      "category": "Counter",
+      "type": "DeepFryer",
+      "pos": [
+        4.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "0499b155f3d14a5ab29c250694781437",
+        "category": "ItemCookingEquipment",
+        "type": "Basket",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": [],
+        "content_list": [],
+        "content_ready": null
+      },
+      "active_effects": []
+    },
+    {
+      "id": "30e99d656950442693120b071d362df7",
+      "category": "Counter",
+      "type": "Oven",
+      "pos": [
+        5.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "7a53dc8c7ff74d4298e6194c22a4496f",
+        "category": "ItemCookingEquipment",
+        "type": "Peel",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": [],
+        "content_list": [],
+        "content_ready": null
+      },
+      "active_effects": []
+    },
+    {
+      "id": "68672d07face4654b341df651e2a9e18",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        6.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "5e164dbdefaa4b6ea2875a0fb2ee678a",
+      "category": "Counter",
+      "type": "TomatoDispenser",
+      "pos": [
+        7.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "e96572a3d637465182610e06e5568046",
+        "category": "Item",
+        "type": "Tomato",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "79dfbbe5648b48bfaaca0795fa867b05",
+      "category": "Counter",
+      "type": "OnionDispenser",
+      "pos": [
+        8.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "6c1d352c4a7042eebf012fb6d8296546",
+        "category": "Item",
+        "type": "Onion",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "e60972ae9cd141f7b2fe0d855726d348",
+      "category": "Counter",
+      "type": "LettuceDispenser",
+      "pos": [
+        9.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "f8332245ea194cfa8483ec62eeb7eeae",
+        "category": "Item",
+        "type": "Lettuce",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "31aaf4f559aa4d8788f9e78085f3f15e",
+      "category": "Counter",
+      "type": "BunDispenser",
+      "pos": [
+        10.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": {
+        "id": "0620c75ee8a54b8db0a08a793e7e06bd",
+        "category": "Item",
+        "type": "Bun",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "c1696096776d4a10aa05c434fa9c51a9",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        11.0,
+        0.0
+      ],
+      "orientation": [
+        0,
+        1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "26fcb7e5da2540f682d112ab22ecd981",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        0.0,
+        1.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "d75487f9fa8147df968f083c0d614043",
+      "category": "Counter",
+      "type": "MeatDispenser",
+      "pos": [
+        11.0,
+        1.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": {
+        "id": "895cf2010ee849848cf34ff75e8402af",
+        "category": "Item",
+        "type": "Meat",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "59e4887fe9734b45b7a6b4d2eb26bfdf",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        0.0,
+        2.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": {
+        "id": "c4a332c3ec224bb295d068c5941a9f75",
+        "category": "Item",
+        "type": "Extinguisher",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "f822fbdaa8b947f6a373308098530955",
+      "category": "Counter",
+      "type": "PotatoDispenser",
+      "pos": [
+        11.0,
+        2.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": {
+        "id": "4729948a21504531a89abc2e58564a6a",
+        "category": "Item",
+        "type": "Potato",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "3a768b2b149848979694e83b7ddf9ae3",
+      "category": "Counter",
+      "type": "ServingWindow",
+      "pos": [
+        0.0,
+        3.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "e722cd2a13cd45e6b8f410e1095972df",
+      "category": "Counter",
+      "type": "FishDispenser",
+      "pos": [
+        11.0,
+        3.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": {
+        "id": "c9788eae65eb4cc1893a1a658725022b",
+        "category": "Item",
+        "type": "Fish",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "079e5824edf74c91a206cef8c7e6a1cb",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        0.0,
+        4.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "c4e3620010784af4b952644831d9f8ed",
+      "category": "Counter",
+      "type": "DoughDispenser",
+      "pos": [
+        11.0,
+        4.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": {
+        "id": "59c0f10dc74f4ecdb559f24acd812cc3",
+        "category": "Item",
+        "type": "Dough",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "1182d2edae2f4756b8242ceabc7dddc0",
+      "category": "Counter",
+      "type": "CuttingBoard",
+      "pos": [
+        0.0,
+        5.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "90a3d17b698241368e02e56c90d387c4",
+      "category": "Counter",
+      "type": "CheeseDispenser",
+      "pos": [
+        11.0,
+        5.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": {
+        "id": "0d3a459f08494e67b854aaaa04d44628",
+        "category": "Item",
+        "type": "Cheese",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "0d73239968b943f4a545479af1e9d630",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        0.0,
+        6.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "3d3c2611a2fb4f75a92e991bb0691efd",
+      "category": "Counter",
+      "type": "SausageDispenser",
+      "pos": [
+        11.0,
+        6.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": {
+        "id": "c8c7a7352d024049b730193f41ad243e",
+        "category": "Item",
+        "type": "Sausage",
+        "progress_percentage": 0.0,
+        "inverse_progress": false,
+        "active_effects": []
+      },
+      "active_effects": []
+    },
+    {
+      "id": "2041cebba42c496abb362931926cff19",
+      "category": "Counter",
+      "type": "CuttingBoard",
+      "pos": [
+        0.0,
+        7.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "c8809b1e4f5349838c4bdbee4bc60cb3",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        11.0,
+        7.0
+      ],
+      "orientation": [
+        -1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "b6eb1077789749749564b6d139a907e7",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        0.0,
+        8.0
+      ],
+      "orientation": [
+        1,
+        0
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "5903eebafe47418fb2d908e4ef413895",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        1.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "d94d02901d6d4afeb4f0dd685a383a90",
+      "category": "Counter",
+      "type": "PlateDispenser",
+      "pos": [
+        2.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": [
+        {
+          "id": "2989438d9250431f96adfb9beecc1a64",
+          "category": "ItemCookingEquipment",
+          "type": "Plate",
+          "progress_percentage": 0.0,
+          "inverse_progress": false,
+          "active_effects": [],
+          "content_list": [],
+          "content_ready": null
+        },
+        {
+          "id": "7a3c01e1a4304ec7afd5f23553c85efc",
+          "category": "ItemCookingEquipment",
+          "type": "Plate",
+          "progress_percentage": 0.0,
+          "inverse_progress": false,
+          "active_effects": [],
+          "content_list": [],
+          "content_ready": null
+        }
+      ],
+      "active_effects": []
+    },
+    {
+      "id": "14e5cc8c37b4442db41fab14e8395ab6",
+      "category": "Counter",
+      "type": "Sink",
+      "pos": [
+        3.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": [],
+      "active_effects": []
+    },
+    {
+      "id": "e7b1daaf31894f4b9a061e34588fc796",
+      "category": "Counter",
+      "type": "SinkAddon",
+      "pos": [
+        4.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": [],
+      "active_effects": []
+    },
+    {
+      "id": "89e9cbc142bb43eeabd1f66c50fa070f",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        5.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "3d276d7435884de6b9cdd03fa8258b48",
+      "category": "Counter",
+      "type": "Trashcan",
+      "pos": [
+        6.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "bcc17cc501d64224b84110a3a3d485a0",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        7.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "a3396bf4217a4cc5bca66fffeccdbcce",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        8.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    },
+    {
+      "id": "ed59ba248e5d42dfbb5345fc8a9f951b",
+      "category": "Counter",
+      "type": "Sink",
+      "pos": [
+        9.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": [],
+      "active_effects": []
+    },
+    {
+      "id": "5f85b094511c421b9456c7b99cbdca0f",
+      "category": "Counter",
+      "type": "SinkAddon",
+      "pos": [
+        10.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": [],
+      "active_effects": []
+    },
+    {
+      "id": "c746964d6e2b47b7b69b89974759c885",
+      "category": "Counter",
+      "type": "Counter",
+      "pos": [
+        11.0,
+        8.0
+      ],
+      "orientation": [
+        0,
+        -1
+      ],
+      "occupied_by": null,
+      "active_effects": []
+    }
+  ],
+  "kitchen": {
+    "width": 12.0,
+    "height": 9.0
+  },
+  "score": 0.0,
+  "orders": [
+    {
+      "id": "803780e9583f4aa8a346ce344ad7f115",
+      "category": "Order",
+      "meal": "FriedFish",
+      "start_time": "2000-01-01T00:00:00",
+      "max_duration": 51.522081
+    },
+    {
+      "id": "cb18db2be449467db108ac6b7e6bf09e",
+      "category": "Order",
+      "meal": "Burger",
+      "start_time": "2000-01-01T00:00:00",
+      "max_duration": 42.046972
+    }
+  ],
+  "ended": false,
+  "env_time": "2000-01-01T00:00:00.290519",
+  "remaining_time": 299.709481,
+  "view_restriction": null,
+  "served_meals": [],
+  "info_msg": []
+}
diff --git a/cooperative_cuisine/pygame_2d_vis/sync_bell.wav b/cooperative_cuisine/pygame_2d_vis/sync_bell.wav
new file mode 100644
index 0000000000000000000000000000000000000000..9dc2767b1c89b39acadf87023e07387574775e7a
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/sync_bell.wav differ
diff --git a/cooperative_cuisine/pygame_2d_vis/tutorial.png b/cooperative_cuisine/pygame_2d_vis/tutorial.png
new file mode 100644
index 0000000000000000000000000000000000000000..151f8e85b9a76c30df20d334be4deffd8d4e3d6a
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/tutorial.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/tutorial_files/recipe_mock.png b/cooperative_cuisine/pygame_2d_vis/tutorial_files/recipe_mock.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ac719852dfd2470712a56ee249f86baa7a891d6
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/tutorial_files/recipe_mock.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/tutorial_files/tutorial.drawio.png b/cooperative_cuisine/pygame_2d_vis/tutorial_files/tutorial.drawio.png
new file mode 100644
index 0000000000000000000000000000000000000000..8824cce8164089cc251217e21263c75fcd552254
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/tutorial_files/tutorial.drawio.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/video_replay.py b/cooperative_cuisine/pygame_2d_vis/video_replay.py
new file mode 100644
index 0000000000000000000000000000000000000000..1c6c348a68e430c6488ecb794ee9536c973f49e1
--- /dev/null
+++ b/cooperative_cuisine/pygame_2d_vis/video_replay.py
@@ -0,0 +1,422 @@
+"""
+Generate images from a recording of json states or from the recording of the actions.
+
+The json state files grow very fast. Therefore, we recommend using the actions recording and create a video replay
+based on the actions. Until now, we did not find any deviations from the json state visualisation.
+
+# CLI
+Sequence of images replay from actions:
+```bash
+python video_replay.py -a ~/.local/state/cooperative_cuisine/log/ENV_NAME/actions.jsonl -e ~/.local/state/cooperative_cuisine/log/ENV_NAME/env_configs.jsonl -d -n 2 -p "0"
+```
+
+Sequence of images replay from json states:
+```bash
+python video_replay.py -j ~/.local/state/cooperative_cuisine/log/ENV_NAME/json_states.jsonl -d -p "0"
+```
+
+The `display` (`-d`, `--display`) requires `opencv-python` (cv2) installed. (`pip install opencv-python`)
+
+Generate a video file from images (requires also `opencv-python`):
+```bash
+python video_replay.py --video ~/.local/state/cooperative_cuisine/log/ENV_NAME/DIR_NAME_WITH_IMAGES
+```
+
+For additional CLI arguments:
+```bash
+python video_replay.py -h
+```
+
+# Code Documentation
+"""
+import argparse
+import json
+import os
+import os.path
+from argparse import ArgumentParser
+from datetime import datetime, timedelta
+from pathlib import Path
+
+import pygame
+import yaml
+from PIL import Image
+from tqdm import tqdm
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.environment import Environment, Action
+from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
+from cooperative_cuisine.recording import FileRecorder
+
+FPS_DEFAULT = 24
+STEP_DURATION_DEFAULT = 200
+GRID_SIZE_DEFAULT = 40
+NUMBER_PLAYERS_DEFAULT = 1
+
+
+def simulate(
+    action_file,
+    env_config,
+    viz_config,
+    target_directory,
+    player_id_filter=None,
+    step_duration=1 / STEP_DURATION_DEFAULT,
+    fps=FPS_DEFAULT,
+    display=False,
+    number_player=NUMBER_PLAYERS_DEFAULT,
+    break_when_no_action=False,
+    grid_size=GRID_SIZE_DEFAULT,
+):
+    """
+    Create images by simulating the game environment using a sequence of actions.
+
+    You can record the relevant files via hooks in the environment_config:
+    ```yaml
+    extra_setup_functions
+      env_configs:
+        func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+        kwargs:
+          hooks: [ env_initialized, item_info_config ]
+          callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+          callback_class_kwargs:
+            log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+            add_hook_ref: true
+      actions:
+        func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+        kwargs:
+          hooks: [ pre_perform_action ]
+          callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+          callback_class_kwargs:
+            log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+    ```
+
+    You can call simulation function via the command line. For example by replacing the ENVIRONMENT_ID (Linux system) or the complete path:
+    ```bash
+    python video_replay.py -a ~/.local/state/cooperative_cuisine/log/ENV_NAME/actions.jsonl -e ~/.local/state/cooperative_cuisine/log/ENV_NAME/env_configs.jsonl -d -n 2 -p "0"
+    ```
+
+    Args:
+        action_file: Path of the file containing the actions in JSON format.
+        env_config: Path of the environment configuration file.
+        viz_config: Path of the visualization configuration file.
+        target_directory: Path of the directory to save the generated images.
+        player_id_filter: (optional) Filter for player ids.
+        step_duration: (optional) Duration of each step in seconds. Default is 1 / `STEP_DURATION_DEFAULT`.
+        fps: (optional) Frames per second for visualization. Default is `FPS_DEFAULT`.
+        display: (optional) Whether to display the visualization. Default is `False`.
+        number_player: (optional) Number of players. Default is `NUMBER_PLAYERS_DEFAULT`.
+        break_when_no_action: (optional) Whether to stop simulation when there are no more actions. Default is `False`.
+        grid_size: (optional) Size of the grid in pixels. Default is `GRID_SIZE_DEFAULT`.
+    """
+    if display:
+        import cv2
+    if player_id_filter is None:
+        player_id_filter = "0"
+    if not os.path.isdir(os.path.expanduser(target_directory)):
+        os.makedirs(os.path.expanduser(target_directory), exist_ok=True)
+    env_data = {
+        "item_info_config": None,
+        "environment_config": None,
+        "seed": None,
+        "layout_config": None,
+    }
+
+    with open(os.path.expanduser(env_config), "r") as f:
+        for line in f:
+            e = json.loads(line)
+            # or just du update?
+            for k in env_data.keys():
+                if k in e:
+                    env_data[k] = e[k]
+    env = Environment(
+        env_config=env_data["environment_config"],
+        layout_config=env_data["layout_config"],
+        item_info=env_data["item_info_config"],
+        seed=env_data["seed"],
+        as_files=False,
+    )
+    env.reset_env_time()
+
+    # remove recorder hooks
+    for key, callback_list in env.hook.hooks.items():
+        recorder_idx = []
+        for idx, callback in enumerate(callback_list):
+            if isinstance(callback, FileRecorder):
+                recorder_idx.append(idx)
+        for idx in reversed(recorder_idx):
+            callback_list.pop(idx)
+
+    for p in range(number_player):
+        env.add_player(f"{p}")
+    viz = Visualizer(viz_config)
+    viz.create_player_colors(number_player)
+    pygame.init()
+    pygame.font.init()
+    action_idx = 0
+    next_action_time = env.env_time
+    next_frame_time = env.env_time
+    with open(os.path.expanduser(action_file), "r") as file:
+        actions = file.readlines()
+    with tqdm(
+        total=len(actions)
+        if break_when_no_action
+        else int((env.env_time_end - env.env_time).total_seconds())
+    ) as pbar:
+        while not env.game_ended:
+            if (
+                not break_when_no_action
+                and env.env_time.timestamp() - int(env.env_time.timestamp())
+                < step_duration
+            ):
+                pbar.update(1)
+            env.step(timedelta(seconds=step_duration))
+            while action_idx < len(actions) and next_action_time <= env.env_time:
+                action = json.loads(actions[action_idx])
+                next_action_time = datetime.fromisoformat(action["env_time"])
+                if next_action_time <= env.env_time:
+                    env.perform_action(Action(**action["action"]))
+                    action_idx += 1
+                    if break_when_no_action:
+                        pbar.update(1)
+            if break_when_no_action and action_idx >= len(actions):
+                break
+
+            if next_frame_time <= env.env_time:
+                next_frame_time += timedelta(seconds=1 / fps)
+                state = env.get_json_state(player_id_filter)
+                state = json.loads(state)
+                output_file = Path(os.path.expanduser(target_directory)) / (
+                    state["env_time"] + ".jpg"
+                )
+                # viz.save_state_image(grid_size=40, state=state, filename=output_file)
+                image = viz.get_state_image(grid_size=grid_size, state=state).transpose(
+                    (1, 0, 2)
+                )
+                Image.fromarray(image).save(output_file)
+                if display:
+                    cv2.imshow("Replay", image[:, :, ::-1])
+                    cv2.waitKey(1)
+
+
+def from_json_states(
+    json_states_file,
+    viz_config,
+    target_directory,
+    player_id_filter=None,
+    display=False,
+    grid_size=GRID_SIZE_DEFAULT,
+):
+    """
+    Generate images from recorded json strings in on jsonl file.
+
+    For single image creation based on one json state see `cooperative_cuisine.pygame_2d_vis.drawing`.
+
+    You can create the jsonl file recording via hooks in the environment_config. These files grow very fast!:
+    ```yaml
+    json_states:
+      func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+      kwargs:
+       hooks: [ json_state ]
+       callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+       callback_class_kwargs:
+         log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+    ```
+
+    You can call this function via the command line:
+    ```bash
+    python video_replay.py -j ~/.local/state/cooperative_cuisine/log/ENV_NAME/json_states.jsonl -d -p "0"
+    ```
+
+    Args:
+        json_states_file: The path to the JSON file containing the game states.
+        viz_config: The visualization configuration.
+        target_directory: The directory where the generated images will be saved.
+        player_id_filter: (Optional) The ID of the player to filter by. If None, all players will be included.
+        display: (Optional) A flag indicating whether to display the generated images using OpenCV.
+        grid_size: (Optional) The size of the grid to render the game state on.
+    """
+    if display:
+        import cv2
+    created_colors = False
+    if not os.path.isdir(target_directory):
+        os.makedirs(target_directory, exist_ok=True)
+    with open(os.path.expanduser(json_states_file), "r") as file:
+        viz = Visualizer(viz_config)
+        pygame.init()
+        pygame.font.init()
+        for line in tqdm(file):
+            state = json.loads(line)
+            if not created_colors:
+                viz.create_player_colors(len(state["players"]))
+                created_colors = True
+            if player_id_filter is None or player_id_filter == state["player_id"]:
+                output_file = Path(target_directory) / (state["env_time"] + ".jpg")
+                image = viz.get_state_image(grid_size=grid_size, state=state)
+                Image.fromarray(image).save(output_file)
+                if display:
+                    cv2.imshow("Replay", image.transpose((1, 0, 2))[:, :, ::-1])
+                    cv2.waitKey(1)
+
+
+def video_from_images(image_paths, video_name, fps):
+    """Generate a video from images in a directory.
+
+    Requires opencv installed.
+
+    CLI:
+    ```bash
+    python video_replay.py --video ~/.local/state/cooperative_cuisine/log/ENV_NAME/DIR_NAME_WITH_IMAGES
+    ```
+
+    Args:
+        image_paths: A string representing the path to the directory containing the images.
+        video_name: A string representing the name of the output video file.
+        fps: An integer representing the frames per second for the video.
+
+    """
+    assert os.path.isdir(image_paths), "image path is not a directory"
+    import cv2
+
+    images = sorted(os.listdir(os.path.expanduser(image_paths)))
+    frame = cv2.imread(os.path.expanduser(os.path.join(image_paths, images[0])))
+    height, width, layers = frame.shape
+
+    video = cv2.VideoWriter(
+        os.path.expanduser(video_name),
+        cv2.VideoWriter_fourcc(*"mp4v"),
+        fps,
+        (width, height),
+    )
+
+    for image in tqdm(images):
+        video.write(cv2.imread(os.path.expanduser(os.path.join(image_paths, image))))
+
+    cv2.destroyAllWindows()
+    print("Generate Video...")
+    video.release()
+    print("See:", video_name)
+
+
+if __name__ == "__main__":
+    parser = ArgumentParser(
+        prog="Cooperative Cuisine Video Generation",
+        description="Generate videos from recorded data.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+    parser.add_argument("-j", "--json_state", help="Json states file path", type=str)
+    parser.add_argument(
+        "-v",
+        "--visualization_config",
+        type=argparse.FileType("r", encoding="UTF-8"),
+        default=ROOT_DIR / "pygame_2d_vis" / "visualization.yaml",
+    )
+    parser.add_argument(
+        "-o",
+        "--output",
+        type=str,
+        default="<json_state_name>",
+    )
+    parser.add_argument(
+        "-p",
+        "--player_id",
+        type=str,
+        default=None,
+        help="Render view for specific player",
+    )
+    parser.add_argument(
+        "-g",
+        "--grid_size",
+        type=int,
+        default=GRID_SIZE_DEFAULT,
+        help="Number pixel for one cell in the grid.",
+    )
+    parser.add_argument(
+        "-a",
+        "--action_recording",
+        type=str,
+        default=None,
+        help="The path to the action recording",
+    )
+    parser.add_argument(
+        "-e",
+        "--env_configs",
+        type=str,
+        default=None,
+        help="The path to the environment config logs",
+    )
+    parser.add_argument(
+        "-s",
+        "--step_duration",
+        type=float,
+        default=1 / STEP_DURATION_DEFAULT,
+        help="Step duration in seconds between environment steps.",
+    )
+    parser.add_argument(
+        "-f",
+        "--fps",
+        type=int,
+        default=FPS_DEFAULT,
+        help="Frames per second to render images from the environment",
+    )
+    parser.add_argument(
+        "-d", "--display", action="store_true", help="Show generated images."
+    )
+    parser.add_argument(
+        "-n",
+        "--number_player",
+        type=int,
+        default=NUMBER_PLAYERS_DEFAULT,
+        help="Number of player to visualize. Should be the same as played the game.",
+    )
+    parser.add_argument(
+        "-b",
+        "--break_when_no_action",
+        action="store_true",
+        help="Stop rendering when no more actions are available.",
+    )
+    parser.add_argument(
+        "--video",
+        "--video_source",
+        type=str,
+        help="Create a video from a folder full of images.",
+    )
+    args = parser.parse_args()
+    with open(args.visualization_config, "r") as f:
+        viz_config = yaml.safe_load(f)
+    if args.json_state:
+        target_directory = (
+            args.json_state.rsplit(".", maxsplit=1)[0]
+            if args.output == "<json_state_name>"
+            else args.output
+        )
+        from_json_states(
+            args.json_state,
+            viz_config,
+            target_directory,
+            args.player_id,
+            args.display,
+            args.grid_size,
+        )
+    elif args.video:
+        target_directory = (
+            args.video + ".mp4" if args.output == "<json_state_name>" else args.output
+        )
+        video_from_images(args.video, target_directory, args.fps)
+    else:
+        target_directory = (
+            args.action_recording.rsplit(".", maxsplit=1)[0]
+            if args.output == "<json_state_name>"
+            else args.output
+        )
+        simulate(
+            args.action_recording,
+            args.env_configs,
+            viz_config,
+            target_directory,
+            args.player_id,
+            args.step_duration,
+            args.fps,
+            args.display,
+            args.number_player,
+            args.break_when_no_action,
+            args.grid_size,
+        )
diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/cooperative_cuisine/pygame_2d_vis/visualization.yaml
similarity index 73%
rename from overcooked_simulator/gui_2d_vis/visualization.yaml
rename to cooperative_cuisine/pygame_2d_vis/visualization.yaml
index 62587a97d0f8e052a989e587d931ee7298c1193b..d388e83cdc9fc14e2d46e8bb08d22b8594999db1 100644
--- a/overcooked_simulator/gui_2d_vis/visualization.yaml
+++ b/cooperative_cuisine/pygame_2d_vis/visualization.yaml
@@ -1,17 +1,18 @@
 # colors: https://www.webucator.com/article/python-color-constants-module/
 
 GameWindow:
-  WhatIsFixed: grid  # grid or window_width or window_height
-  size: 60
   screen_margin: 100
-  min_width: 700
+  min_width: 900
   min_height: 600
   buttons_width: 180
   buttons_height: 60
+  FPS: 60
 
   order_bar_height: 100
   order_size: 50
 
+  game_border_size: 1
+  game_border_color: black
   background_color: lemonchiffon1
 
 Kitchen:
@@ -20,73 +21,85 @@ 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 ]
+      size: 0.88
+      center_offset: [ 0, -0.05 ]
 
-TomatoDispenser:
-  parts:
-    - color: orangered1
-      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
 
-LettuceDispenser:
-  parts:
-    - color: palegreen3
-      type: rect
-      height: 0.8
-      width: 0.8
-
-OnionDispenser:
+Dispenser:
   parts:
-    - color: deeppink3
-      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 ]
 
-MeatDispenser:
-  parts:
-    - color: indianred1
-      type: rect
-      height: 0.8
-      width: 0.8
 
-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:
@@ -97,7 +110,8 @@ ServingWindow:
     - type: image
       path: images/bell_gold.png
       size: 0.5
-      center_offset: [ 0.1, -0.4 ]
+      center_offset: [ -0.2, -0.05 ]
+      rotate_image: False
 
 Stove:
   parts:
@@ -113,15 +127,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:
@@ -273,7 +321,7 @@ Oven:
       color: black
       height: 0.8
       width: 0.3
-      center_offset: [ -0.4, -0.1 ]
+      center_offset: [ 0, -0.1 ]
 
 Basket:
   parts:
diff --git a/cooperative_cuisine/recording.py b/cooperative_cuisine/recording.py
new file mode 100644
index 0000000000000000000000000000000000000000..79f98fb9d845edf4457eb9fcd17b413286ff433e
--- /dev/null
+++ b/cooperative_cuisine/recording.py
@@ -0,0 +1,116 @@
+"""
+Record events in jsonl-files.
+
+- `USER_LOG_DIR` corresponds to the log directory of your system for programs. See [platformdirs](
+https://pypi.org/project/platformdirs/) -> `user_log_dir`.
+- `ROOT_DIR` to the location of the cooperative_cuisine.
+- `ENV_NAME` to the name of the environment.
+
+```yaml
+extra_setup_functions:
+  json_states:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ json_state ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+      callback_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+```
+"""
+import json
+import logging
+import os
+import traceback
+from pathlib import Path
+
+import platformdirs
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.environment import Environment
+from cooperative_cuisine.hooks import HookCallbackClass
+from cooperative_cuisine.utils import NumpyAndDataclassEncoder
+
+log = logging.getLogger(__name__)
+
+
+class FileRecorder(HookCallbackClass):
+    """
+    Class: FileRecorder
+
+    This class is responsible for recording data to a file.
+
+    Attributes:
+        name (str): The name of the recorder.
+        env (Environment): The environment instance.
+        log_path (str): The path to the log file. Default value is "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl".
+        add_hook_ref (bool): Indicates whether to add a hook reference to the recorded data. Default value is False.
+
+    """
+
+    def __init__(
+        self,
+        name: str,
+        env: Environment,
+        log_path: str = "USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl",
+        add_hook_ref: bool = False,
+        **kwargs,
+    ):
+        super().__init__(name, env, **kwargs)
+        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("cooperative_cuisine"))
+                / 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/cooperative_cuisine/reinforcement_learning/__init__.py b/cooperative_cuisine/reinforcement_learning/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml b/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5e75d9bdaea71f52507fa31d3fb1ed47b268bf84
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml
@@ -0,0 +1,173 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 0
+  plate_delay: [ 2, 4 ]
+  return_dirty: False
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 300
+
+meals:
+  all: true
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - TomatoSoup
+    - OnionSoup
+    - Salad
+
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: 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
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  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:cooperative_cuisine.order.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in orders.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 6
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+  serving_not_ordered_meals: true
+  # can meals that are not ordered be served / dropped on the serving window
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 1
+  interaction_range: 1.6
+  restricted_view: False
+  view_angle: 95
+
+effect_manager: { }
+#  FireManager:
+#    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+#    kwargs:
+#      spreading_duration: [ 5, 10 ]
+#      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  # # ---------------  Scoring  ---------------
+  orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ completed_order ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 1
+
+  serve_not_ordered_meals:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ serve_not_ordered_meal ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 1
+  trashcan_usages:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ trashcan_usage ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -0.2
+  item_cut:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ cutting_board_100 ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 0.01
+  stepped:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ post_step ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -0.01
+  combine:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ drop_off_on_cooking_equipment ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 0.01
+  #  json_states:
+  #    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      log_class: !!python/name:cooperative_cuisine.recording.LogRecorder ''
+  #      log_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+#  actions:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ pre_perform_action ]
+#      log_class: !!python/name:cooperative_cuisine.recording.LogRecorder ''
+#      log_class_kwargs:
+#        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+#  random_env_events:
+#    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+#      log_class: !!python/name:cooperative_cuisine.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:cooperative_cuisine.hooks.hooks_via_callback_class ''
+#    kwargs:
+#      hooks: [ env_initialized, item_info_config ]
+#      log_class: !!python/name:cooperative_cuisine.recording.LogRecorder ''
+#      log_class_kwargs:
+#        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+#        add_hook_ref: true
+
diff --git a/cooperative_cuisine/reinforcement_learning/full_vectorization.py b/cooperative_cuisine/reinforcement_learning/full_vectorization.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4ee9d996eff75a9bc49e2f9b856b911b0cb3de6
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/full_vectorization.py
@@ -0,0 +1,244 @@
+# def setup_vectorization(self) -> VectorStateGenerationData:
+#     grid_base_array = np.zeros(
+#         (
+#             int(self.env.kitchen_width),
+#             int(self.env.kitchen_height),
+#             114 + 12 + 4,  # TODO calc based on item info
+#         ),
+#         dtype=np.float32,
+#     )
+#     counter_list = [
+#         "Counter",
+#         "CuttingBoard",
+#         "ServingWindow",
+#         "Trashcan",
+#         "Sink",
+#         "SinkAddon",
+#         "Stove",
+#         "DeepFryer",
+#         "Oven",
+#     ]
+#     grid_idxs = [
+#         (x, y)
+#         for x in range(int(self.env.kitchen_width))
+#         for y in range(int(self.env.kitchen_height))
+#     ]
+#     # counters do not move
+#     for counter in self.env.counters:
+#         grid_idx = np.floor(counter.pos).astype(int)
+#         counter_name = (
+#             counter.name
+#             if isinstance(counter, CookingCounter)
+#             else (
+#                 repr(counter)
+#                 if isinstance(Counter, Dispenser)
+#                 else counter.__class__.__name__
+#             )
+#         )
+#         assert counter_name in counter_list or counter_name.endswith(
+#             "Dispenser"
+#         ), f"Unknown Counter {counter}"
+#         oh_idx = len(counter_list)
+#         if counter_name in counter_list:
+#             oh_idx = counter_list.index(counter_name)
+#
+#         one_hot = [0] * (len(counter_list) + 2)
+#         one_hot[oh_idx] = 1
+#         grid_base_array[
+#             grid_idx[0], grid_idx[1], 4 : 4 + (len(counter_list) + 2)
+#         ] = np.array(one_hot, dtype=np.float32)
+#
+#         grid_idxs.remove((int(grid_idx[0]), int(grid_idx[1])))
+#
+#     for free_idx in grid_idxs:
+#         one_hot = [0] * (len(counter_list) + 2)
+#         one_hot[len(counter_list) + 1] = 1
+#         grid_base_array[
+#             free_idx[0], free_idx[1], 4 : 4 + (len(counter_list) + 2)
+#         ] = np.array(one_hot, dtype=np.float32)
+#
+#     player_info_base_array = np.zeros(
+#         (
+#             4,
+#             4 + 114,
+#         ),
+#         dtype=np.float32,
+#     )
+#     order_base_array = np.zeros((10 * (8 + 1)), dtype=np.float32)
+#
+#     return VectorStateGenerationData(
+#         grid_base_array=grid_base_array,
+#         oh_len=12,
+#     )
+#
+#
+# def get_simple_vectorized_item(self, item: Item) -> npt.NDArray[float]:
+#     name = item.name
+#     array = np.zeros(21, dtype=np.float32)
+#     if item.name.startswith("Burnt"):
+#         name = name[len("Burnt") :]
+#         array[0] = 1.0
+#     if name.startswith("Chopped"):
+#         array[1] = 1.0
+#         name = name[len("Chopped") :]
+#     if name in [
+#         "PizzaBase",
+#         "GratedCheese",
+#         "RawChips",
+#         "RawPatty",
+#     ]:
+#         array[1] = 1.0
+#         name = {
+#             "PizzaBase": "Dough",
+#             "GratedCheese": "Cheese",
+#             "RawChips": "Potato",
+#             "RawPatty": "Meat",
+#         }[name]
+#     if name == "CookedPatty":
+#         array[2] = 1.0
+#         name = "Meat"
+#
+#     if name in self.vector_state_generation.meals:
+#         idx = 3 + self.vector_state_generation.meals.index(name)
+#     elif name in self.vector_state_generation.ingredients:
+#         idx = (
+#             3
+#             + len(self.vector_state_generation.meals)
+#             + self.vector_state_generation.ingredients.index(name)
+#         )
+#     else:
+#         raise ValueError(f"Unknown item {name} - {item}")
+#     array[idx] = 1.0
+#     return array
+#
+#
+# def get_vectorized_item(self, item: Item) -> npt.NDArray[float]:
+#     item_array = np.zeros(114, dtype=np.float32)
+#
+#     if isinstance(item, CookingEquipment) or item.item_info.type == ItemType.Tool:
+#         assert (
+#             item.name in self.vector_state_generation.equipments
+#         ), f"unknown equipment {item}"
+#         idx = self.vector_state_generation.equipments.index(item.name)
+#         item_array[idx] = 1.0
+#         if isinstance(item, CookingEquipment):
+#             for s_idx, sub_item in enumerate(item.content_list):
+#                 if s_idx > 3:
+#                     print("Too much content in the content list, info dropped")
+#                     break
+#                 start_idx = len(self.vector_state_generation.equipments) + 21 + 2
+#                 item_array[
+#                     start_idx + (s_idx * (21)) : start_idx + ((s_idx + 1) * (21))
+#                 ] = self.get_simple_vectorized_item(sub_item)
+#
+#     else:
+#         item_array[
+#             len(self.vector_state_generation.equipments) : len(
+#                 self.vector_state_generation.equipments
+#             )
+#             + 21
+#         ] = self.get_simple_vectorized_item(item)
+#
+#     item_array[
+#         len(self.vector_state_generation.equipments) + 21 + 1
+#     ] = item.progress_percentage
+#
+#     if item.active_effects:
+#         item_array[
+#             len(self.vector_state_generation.equipments) + 21 + 2
+#         ] = 1.0  # TODO percentage of fire...
+#
+#     return item_array
+#
+#
+# def get_vectorized_state_full(
+#     self, player_id: str
+# ) -> Tuple[
+#     npt.NDArray[npt.NDArray[float]],
+#     npt.NDArray[npt.NDArray[float]],
+#     float,
+#     npt.NDArray[float],
+# ]:
+#     grid_array = self.vector_state_generation.grid_base_array.copy()
+#     for counter in self.env.counters:
+#         grid_idx = np.floor(counter.pos).astype(int)  # store in counter?
+#         if counter.occupied_by:
+#             if isinstance(counter.occupied_by, deque):
+#                 ...
+#             else:
+#                 item = counter.occupied_by
+#                 grid_array[
+#                     grid_idx[0],
+#                     grid_idx[1],
+#                     4 + self.vector_state_generation.oh_len :,
+#                 ] = self.get_vectorized_item(item)
+#         if counter.active_effects:
+#             grid_array[
+#                 grid_idx[0],
+#                 grid_idx[1],
+#                 4 + self.vector_state_generation.oh_len - 1,
+#             ] = 1.0  # TODO percentage of fire...
+#
+#     assert len(self.env.players) <= 4, "To many players for vector representation"
+#     player_vec = np.zeros(
+#         (
+#             4,
+#             4 + 114,
+#         ),
+#         dtype=np.float32,
+#     )
+#     player_pos = 1
+#     for player in self.env.players.values():
+#         if player.name == player_id:
+#             idx = 0
+#             player_vec[0, :4] = np.array(
+#                 [
+#                     player.pos[0],
+#                     player.pos[1],
+#                     player.facing_point[0],
+#                     player.facing_point[1],
+#                 ],
+#                 dtype=np.float32,
+#             )
+#         else:
+#             idx = player_pos
+#
+#         if not idx:
+#             player_pos += 1
+#         grid_idx = np.floor(player.pos).astype(int)  # store in counter?
+#         player_vec[idx, :4] = np.array(
+#             [
+#                 player.pos[0] - grid_idx[0],
+#                 player.pos[1] - grid_idx[1],
+#                 player.facing_point[0] / np.linalg.norm(player.facing_point),
+#                 player.facing_point[1] / np.linalg.norm(player.facing_point),
+#             ],
+#             dtype=np.float32,
+#         )
+#         grid_array[grid_idx[0], grid_idx[1], idx] = 1.0
+#
+#         if player.holding:
+#             player_vec[idx, 4:] = self.get_vectorized_item(player.holding)
+#
+#     order_array = np.zeros((10 * (8 + 1)), dtype=np.float32)
+#
+#     for i, order in enumerate(self.env.order_manager.open_orders):
+#         if i > 9:
+#             print("some orders are not represented in the vectorized state")
+#             break
+#         assert (
+#             order.meal.name in self.vector_state_generation.meals
+#         ), "unknown meal in order"
+#         idx = self.vector_state_generation.meals.index(order.meal.name)
+#         order_array[(i * 9) + idx] = 1.0
+#         order_array[(i * 9) + 8] = (
+#             self.env_time - order.start_time
+#         ).total_seconds() / order.max_duration.total_seconds()
+#
+#     return (
+#         grid_array,
+#         player_vec,
+#         (self.env.env_time - self.env.start_time).total_seconds()
+#         / (self.env.env_time_end - self.env.start_time).total_seconds(),
+#         order_array,
+#     )
diff --git a/cooperative_cuisine/reinforcement_learning/gym_env.py b/cooperative_cuisine/reinforcement_learning/gym_env.py
new file mode 100644
index 0000000000000000000000000000000000000000..7bacc68c42bafd29257976a351307ea4c268cc72
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/gym_env.py
@@ -0,0 +1,512 @@
+import json
+import random
+import time
+from collections import deque
+from datetime import timedelta
+from enum import Enum
+from pathlib import Path
+
+import cv2
+import numpy as np
+import wandb
+import yaml
+from gymnasium import spaces, Env
+from stable_baselines3 import A2C
+from stable_baselines3 import DQN
+from stable_baselines3 import PPO
+from stable_baselines3.common.callbacks import CallbackList, CheckpointCallback
+from stable_baselines3.common.env_checker import check_env
+from stable_baselines3.common.env_util import make_vec_env
+from stable_baselines3.common.vec_env import VecVideoRecorder
+from wandb.integration.sb3 import WandbCallback
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.counters import Counter, CookingCounter, Dispenser
+from cooperative_cuisine.environment import (
+    Environment,
+    Action,
+    ActionType,
+    InterActionData,
+)
+from cooperative_cuisine.game_items import CookingEquipment
+from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
+
+
+class SimpleActionSpace(Enum):
+    Up = "Up"
+    Down = "Down"
+    Left = "Left"
+    Right = "Right"
+    Interact = "Interact"
+    Put = "Put"
+
+
+def get_env_action(player_id, simple_action, duration):
+    match simple_action:
+        case SimpleActionSpace.Up:
+            return Action(
+                player_id,
+                ActionType.MOVEMENT,
+                np.array([0, -1]),
+                duration,
+            )
+
+        case SimpleActionSpace.Left:
+            return Action(
+                player_id,
+                ActionType.MOVEMENT,
+                np.array([-1, 0]),
+                duration,
+            )
+        case SimpleActionSpace.Down:
+            return Action(
+                player_id,
+                ActionType.MOVEMENT,
+                np.array([0, 1]),
+                duration,
+            )
+        case SimpleActionSpace.Right:
+            return Action(
+                player_id,
+                ActionType.MOVEMENT,
+                np.array([1, 0]),
+                duration,
+            )
+        case SimpleActionSpace.Put:
+            return Action(
+                player_id,
+                ActionType.PUT,
+                InterActionData.START,
+                duration,
+            )
+        case SimpleActionSpace.Interact:
+            return Action(
+                player_id,
+                ActionType.INTERACT,
+                InterActionData.START,
+                duration,
+            )
+
+
+environment_config_path = (
+    ROOT_DIR / "reinforcement_learning" / "environment_config_rl.yaml"
+)
+layout_path: Path = ROOT_DIR / "reinforcement_learning" / "rl_small.layout"
+item_info_path = ROOT_DIR / "reinforcement_learning" / "item_info_rl.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()
+with open(ROOT_DIR / "pygame_2d_vis" / "visualization.yaml", "r") as file:
+    visualization_config = yaml.safe_load(file)
+
+
+def shuffle_counters(env):
+    sample_counter = []
+    for counter in env.counters:
+        if counter.__class__ != Counter:
+            sample_counter.append(counter)
+    new_counter_pos = [c.pos for c in sample_counter]
+    random.shuffle(new_counter_pos)
+    for counter, new_pos in zip(sample_counter, new_counter_pos):
+        counter.pos = new_pos
+    env.counter_positions = np.array([c.pos for c in env.counters])
+
+
+class EnvGymWrapper(Env):
+    """Should enable this:
+    observation, reward, terminated, truncated, info = env.step(action)
+    """
+
+    metadata = {"render_modes": ["human", "rgb_array"], "render_fps": 10}
+
+    def __init__(self):
+        super().__init__()
+
+        self.gridsize = 20
+
+        self.randomize_counter_placement = True
+        self.use_rgb_obs = False  # if False uses simple vectorized state
+        self.full_vector_state = True
+        self.onehot_state = False
+
+        self.env: Environment = Environment(
+            env_config=environment_config,
+            layout_config=layout,
+            item_info=item_info,
+            as_files=False,
+        )
+
+        if self.randomize_counter_placement:
+            shuffle_counters(self.env)
+
+        self.player_name = str(0)
+        self.env.add_player(self.player_name)
+        self.player_id = list(self.env.players.keys())[0]
+
+        self.visualizer: Visualizer = Visualizer(config=visualization_config)
+        self.visualizer.create_player_colors(1)
+
+        # self.action_space = {idx: value for idx, value in enumerate(SimpleActionSpace)}
+        self.action_space_map = {}
+        for idx, item in enumerate(SimpleActionSpace):
+            self.action_space_map[idx] = item
+        self.global_step_time = 1
+        self.in_between_steps = 1
+
+        self.action_space = spaces.Discrete(len(self.action_space_map))
+
+        min_obs_val = -1 if not self.use_rgb_obs else 0
+        max_obs_val = 255 if self.use_rgb_obs else 1 if self.onehot_state else 9
+        dummy_obs = self.get_observation()
+        self.observation_space = spaces.Box(
+            low=min_obs_val,
+            high=max_obs_val,
+            shape=dummy_obs.shape,
+            dtype=np.uint8 if self.use_rgb_obs else int,
+        )
+        print(self.observation_space)
+
+        self.last_obs = dummy_obs
+
+        self.step_counter = 0
+        self.prev_score = 0
+
+    def vectorize_item(self, item, item_list):
+        item_one_hot = np.zeros(len(item_list))
+        if item is None:
+            item_name = "None"
+        elif isinstance(item, deque):
+            if len(item) > 0:
+                item_name = item[0].name
+            else:
+                item_name = "None"
+        else:
+            item_name = item.name
+
+        if isinstance(item, CookingEquipment):
+            if item.name == "Pot":
+                if len(item.content_list) > 0:
+                    if item.content_list[0].name == "TomatoSoup":
+                        item_name = "PotDone"
+                    elif len(item.content_list) == 1:
+                        item_name = "PotOne"
+                    elif len(item.content_list) == 2:
+                        item_name = "PotTwo"
+                    elif len(item.content_list) == 3:
+                        item_name = "PotThree"
+            elif item.name == "Plate":
+                if len(item.content_list) == 0:
+                    item_name = "Plate"
+                else:
+                    item_name = "PlateTomatoSoup"
+        assert item_name in item_list, f"Unknown item {item_name}."
+        item_idx = item_list.index(item_name)
+        item_one_hot[item_idx] = 1
+
+        return item_one_hot, item_idx
+
+    @staticmethod
+    def vectorize_counter(counter, counter_list):
+        counter_name = (
+            counter.name
+            if isinstance(counter, CookingCounter)
+            else (
+                repr(counter)
+                if isinstance(Counter, Dispenser)
+                else counter.__class__.__name__
+            )
+        )
+        if counter_name == "Dispenser":
+            counter_name = f"{counter.occupied_by.name}Dispenser"
+        assert counter_name in counter_list, f"Unknown Counter {counter}"
+
+        counter_oh_idx = counter_list.index("Empty")
+        if counter_name in counter_list:
+            counter_oh_idx = counter_list.index(counter_name)
+
+        counter_one_hot = np.zeros(len(counter_list), dtype=int)
+        counter_one_hot[counter_oh_idx] = 1
+        return counter_one_hot, counter_oh_idx
+
+    def get_vectorized_state_simple(self, player, onehot=True):
+        counter_list = [
+            "Empty",
+            "Counter",
+            "PlateDispenser",
+            "TomatoDispenser",
+            "ServingWindow",
+            "PlateReturn",
+            "Trashcan",
+            "Stove",
+            "CuttingBoard",
+        ]
+
+        item_list = [
+            "None",
+            "Pot",
+            "PotOne",
+            "PotTwo",
+            "PotThree",
+            "PotDone",
+            "Tomato",
+            "ChoppedTomato",
+            "Plate",
+            "PlateTomatoSoup",
+        ]
+
+        grid_width, grid_height = int(self.env.kitchen_width), int(
+            self.env.kitchen_height
+        )
+
+        grid_base_array = np.zeros(
+            (
+                grid_width,
+                grid_height,
+            ),
+            dtype=int,
+        )
+        grid_idxs = [(x, y) for x in range(grid_width) for y in range(grid_height)]
+
+        if onehot:
+            item_one_hot_length = len(item_list)
+            counter_items = np.zeros(
+                (grid_width, grid_height, item_one_hot_length), dtype=int
+            )
+            counter_one_hot_length = len(counter_list)
+            counters = np.zeros(
+                (grid_width, grid_height, counter_one_hot_length), dtype=int
+            )
+        else:
+            counter_items = np.zeros((grid_width, grid_height), dtype=int)
+            counters = np.zeros((grid_width, grid_height), dtype=int)
+
+        for counter in self.env.counters:
+            grid_idx = np.floor(counter.pos).astype(int)
+
+            counter_one_hot, counter_oh_idx = self.vectorize_counter(
+                counter, counter_list
+            )
+            grid_base_array[grid_idx[0], grid_idx[1]] = counter_oh_idx
+            grid_idxs.remove((int(grid_idx[0]), int(grid_idx[1])))
+
+            counter_item_one_hot, counter_item_oh_idx = self.vectorize_item(
+                counter.occupied_by, item_list
+            )
+            counter_items[grid_idx] = (
+                counter_item_one_hot if onehot else counter_item_oh_idx
+            )
+            counters[grid_idx] = counter_one_hot if onehot else counter_oh_idx
+
+        for free_idx in grid_idxs:
+            grid_base_array[free_idx[0], free_idx[1]] = counter_list.index("Empty")
+
+        player_pos = self.env.players[player].pos.astype(int)
+        player_dir = self.env.players[player].facing_direction.astype(int)
+        player_data = np.concatenate((player_pos, player_dir), axis=0)
+
+        player_item_one_hot, player_item_idx = self.vectorize_item(
+            self.env.players[player].holding, item_list
+        )
+        player_item = player_item_one_hot if onehot else [player_item_idx]
+
+        final = np.concatenate(
+            (
+                counters.flatten(),
+                counter_items.flatten(),
+                player_data.flatten(),
+                player_item,
+            ),
+            axis=0,
+        )
+        return final
+
+    def step(self, action):
+        simple_action = self.action_space_map[action]
+        env_action = get_env_action(
+            self.player_id, simple_action, self.global_step_time
+        )
+        self.env.perform_action(env_action)
+
+        for i in range(self.in_between_steps):
+            self.env.step(
+                timedelta(seconds=self.global_step_time / self.in_between_steps)
+            )
+
+        observation = self.get_observation()
+
+        reward = self.env.score - self.prev_score
+        self.prev_score = self.env.score
+
+        if reward > 0.6:
+            print("- - - - - - - - - - - - - - - - SCORED", reward)
+
+        terminated = self.env.game_ended
+        truncated = self.env.game_ended
+        info = {}
+
+        return observation, reward, terminated, truncated, info
+
+    def reset(self, seed=None, options=None):
+        self.env: Environment = Environment(
+            env_config=environment_config,
+            layout_config=layout,
+            item_info=item_info,
+            as_files=False,
+        )
+
+        if self.randomize_counter_placement:
+            shuffle_counters(self.env)
+
+        self.player_name = str(0)
+        self.env.add_player(self.player_name)
+        self.player_id = list(self.env.players.keys())[0]
+
+        info = {}
+        obs = self.get_observation()
+
+        self.prev_score = 0
+
+        return obs, info
+
+    def get_observation(self):
+        if self.use_rgb_obs:
+            obs = self.get_env_img(self.gridsize)
+        else:
+            obs = self.get_vector_state()
+        return obs
+
+    def render(self):
+        observation = self.get_env_img(self.gridsize)
+        img = observation.astype(np.uint8)
+        img = img.transpose((1, 2, 0))
+        img = cv2.resize(img, (img.shape[1], img.shape[0]))
+        return img
+
+    def close(self):
+        pass
+
+    def get_env_img(self, grid_size=20):
+        state = self.env.get_json_state(player_id=self.player_id)
+        json_dict = json.loads(state)
+        observation = self.visualizer.get_state_image(
+            grid_size=grid_size, state=json_dict
+        ).transpose((1, 0, 2))
+        return (observation.transpose((2, 0, 1))).astype(np.uint8)
+
+    def get_vector_state(self):
+        obs = self.get_vectorized_state_simple("0", self.onehot_state)
+        return obs
+
+    def sample_random_action(self):
+        act = self.action_space.sample()
+        return act
+
+
+def main():
+    rl_agent_checkpoints = Path("rl_agent_checkpoints")
+    rl_agent_checkpoints.mkdir(exist_ok=True)
+
+    config = {
+        "policy_type": "MlpPolicy",
+        "total_timesteps": 30_000_000,  # hendric sagt eher so 300_000_000 schritte
+        "env_id": "overcooked",
+        "number_envs_parallel": 4,
+    }
+
+    debug = False
+    do_training = True
+    vec_env = True
+    number_envs_parallel = config["number_envs_parallel"]
+
+    model_classes = [A2C, DQN, PPO]
+    model_class = model_classes[2]
+
+    if vec_env:
+        env = make_vec_env(EnvGymWrapper, n_envs=number_envs_parallel)
+    else:
+        env = EnvGymWrapper()
+
+    env.render_mode = "rgb_array"
+
+    if not debug:
+        run = wandb.init(
+            project="overcooked",
+            config=config,
+            sync_tensorboard=True,  # auto-upload sb3's tensorboard metrics
+            monitor_gym=True,
+            # save_code=True,  # optional
+        )
+
+        env = VecVideoRecorder(
+            env,
+            f"videos/{run.id}",
+            record_video_trigger=lambda x: x % 200_000 == 0,
+            video_length=300,
+        )
+
+    model_save_path = rl_agent_checkpoints / f"overcooked_{model_class.__name__}"
+
+    if do_training:
+        model = model_class(
+            config["policy_type"],
+            env,
+            verbose=1,
+            tensorboard_log=f"runs/{0}",
+            device="cpu"
+            # n_steps=2048,
+            # n_epochs=10,
+        )
+        if debug:
+            model.learn(
+                total_timesteps=config["total_timesteps"],
+                log_interval=1,
+                progress_bar=True,
+            )
+        else:
+            checkpoint_callback = CheckpointCallback(
+                save_freq=50_000,
+                save_path="logs",
+                name_prefix="rl_model",
+                save_replay_buffer=True,
+                save_vecnormalize=True,
+            )
+            wandb_callback = WandbCallback(
+                model_save_path=f"models/{run.id}",
+                verbose=0,
+            )
+
+            callback = CallbackList([checkpoint_callback, wandb_callback])
+            model.learn(
+                total_timesteps=config["total_timesteps"],
+                callback=callback,
+                log_interval=1,
+                progress_bar=True,
+            )
+            run.finish()
+        model.save(model_save_path)
+
+        del model
+    print("LEARNING DONE.")
+
+    model = model_class.load(model_save_path)
+    env = EnvGymWrapper()
+
+    check_env(env)
+    obs, info = env.reset()
+    while True:
+        action, _states = model.predict(obs, deterministic=False)
+        obs, reward, terminated, truncated, info = env.step(int(action))
+        print(reward)
+        rgb_img = env.render()
+        cv2.imshow("env", rgb_img)
+        cv2.waitKey(0)
+        if terminated or truncated:
+            obs, info = env.reset()
+        time.sleep(1 / env.metadata["render_fps"])
+
+
+if __name__ == "__main__":
+    main()
diff --git a/cooperative_cuisine/reinforcement_learning/item_info_rl.yaml b/cooperative_cuisine/reinforcement_learning/item_info_rl.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..22a79261177579ed91733b28a0fee5dc7ab5d9ed
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/item_info_rl.yaml
@@ -0,0 +1,232 @@
+CuttingBoard:
+  type: Equipment
+
+Sink:
+  type: Equipment
+
+Stove:
+  type: Equipment
+
+DeepFryer:
+  type: Equipment
+
+Oven:
+  type: Equipment
+
+Pot:
+  type: Equipment
+  equipment: Stove
+
+Pan:
+  type: Equipment
+  equipment: Stove
+
+Basket:
+  type: Equipment
+  equipment: DeepFryer
+
+Peel:
+  type: Equipment
+  equipment: Oven
+
+DirtyPlate:
+  type: Equipment
+
+Plate:
+  type: Equipment
+  needs: [ DirtyPlate ]
+  seconds: 2.0
+  equipment: Sink
+
+# --------------------------------------------------------------------------------
+
+Tomato:
+  type: Ingredient
+
+Lettuce:
+  type: Ingredient
+
+Onion:
+  type: Ingredient
+
+Meat:
+  type: Ingredient
+
+Bun:
+  type: Ingredient
+
+Potato:
+  type: Ingredient
+
+Fish:
+  type: Ingredient
+
+Dough:
+  type: Ingredient
+
+Cheese:
+  type: Ingredient
+
+Sausage:
+  type: Ingredient
+
+ChoppedTomato:
+  type: Ingredient
+  needs: [ Tomato ]
+  seconds: 1.0
+  equipment: CuttingBoard
+
+ChoppedLettuce:
+  type: Ingredient
+  needs: [ Lettuce ]
+  seconds: 3.0
+  equipment: CuttingBoard
+
+ChoppedOnion:
+  type: Ingredient
+  needs: [ Onion ]
+  seconds: 5.0
+  equipment: CuttingBoard
+
+RawPatty:
+  type: Ingredient
+  needs: [ Meat ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+RawChips:
+  type: Ingredient
+  needs: [ Potato ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedFish:
+  type: Ingredient
+  needs: [ Fish ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+PizzaBase:
+  type: Ingredient
+  needs: [ Dough ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+GratedCheese:
+  type: Ingredient
+  needs: [ Cheese ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedSausage:
+  type: Ingredient
+  needs: [ Sausage ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+CookedPatty:
+  type: Ingredient
+  seconds: 5.0
+  needs: [ RawPatty ]
+  equipment: Pan
+
+# --------------------------------------------------------------------------------
+
+Chips:
+  type: Meal
+  seconds: 5.0
+  needs: [ RawChips ]
+  equipment: Basket
+
+FriedFish:
+  type: Meal
+  seconds: 5.0
+  needs: [ ChoppedFish ]
+  equipment: Basket
+
+Burger:
+  type: Meal
+  needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
+  equipment: ~
+
+Salad:
+  type: Meal
+  needs: [ ChoppedLettuce, ChoppedTomato ]
+  equipment: ~
+
+TomatoSoup:
+  type: Meal
+  needs: [ ChoppedTomato ]
+  seconds: 3.0
+  equipment: Pot
+
+OnionSoup:
+  type: Meal
+  needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
+  seconds: 6.0
+  equipment: Pot
+
+FishAndChips:
+  type: Meal
+  needs: [ FriedFish, Chips ]
+  equipment: ~
+
+Pizza:
+  type: Meal
+  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/cooperative_cuisine/reinforcement_learning/rl.layout b/cooperative_cuisine/reinforcement_learning/rl.layout
new file mode 100644
index 0000000000000000000000000000000000000000..131e1b95d1d2e6eafcfc619dfeea74e9e5de15f8
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/rl.layout
@@ -0,0 +1,5 @@
+##X##
+#___#
+T___P
+U___#
+#C#$#
diff --git a/cooperative_cuisine/reinforcement_learning/rl_small.layout b/cooperative_cuisine/reinforcement_learning/rl_small.layout
new file mode 100644
index 0000000000000000000000000000000000000000..bbb4ad3ec813f50ee294f5bb118724bbf1b4d581
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/rl_small.layout
@@ -0,0 +1,4 @@
+##X#
+T__#
+U__P
+#C$#
diff --git a/cooperative_cuisine/scores.py b/cooperative_cuisine/scores.py
new file mode 100644
index 0000000000000000000000000000000000000000..b0f968bd7cfc3d2e44bafd91827b7d4ba3e7ccab
--- /dev/null
+++ b/cooperative_cuisine/scores.py
@@ -0,0 +1,110 @@
+"""
+Scores are managed via hooks. You can add them in the `environment_config` under `extra_setup_functions`.
+
+The here defined `ScoreViaHooks` is a `HookCallbackClass`. It allows you to define how the score is effected by
+specific hook events.
+
+You can:
+- score an occurrence of an event with a **static** value (`static_score`)
+- map the score based on the name of the hook (`score_map`)
+- score based on a specific value in the kwargs passed with the hook (`score_on_specific_kwarg` and `score_map`)
+
+You can filter the events via `kwarg_filter`.
+
+```yaml
+extra_setup_functions:
+  orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ completed_order ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 20
+        score_on_specific_kwarg: meal_name
+        score_map:
+          Burger: 15
+          OnionSoup: 10
+          Salad: 5
+          TomatoSoup: 10
+  not_ordered_meals:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ serve_not_ordered_meal ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: 2
+  trashcan_usages:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ trashcan_usage ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -5
+  expired_orders:
+    func: !!python/name:cooperative_cuisine.hooks.hooks_via_callback_class ''
+    kwargs:
+      hooks: [ order_expired ]
+      callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+      callback_class_kwargs:
+        static_score: -10
+```
+
+
+# Code Documentation
+"""
+
+from typing import Any
+
+from cooperative_cuisine.environment import Environment
+from cooperative_cuisine.hooks import HookCallbackClass
+
+
+class ScoreViaHooks(HookCallbackClass):
+    """
+    Defines a class ScoreViaHooks that extends the HookCallbackClass.
+
+    Attributes:
+        name (str): The name of the ScoreViaHooks instance.
+        env (Environment): The environment in which the ScoreViaHooks instance is being used.
+        static_score (float): The static score to be added if no other conditions are met.
+        score_map (dict[str, float]): Mapping of hook references to scores.
+        score_on_specific_kwarg (str): The specific keyword argument to score on.
+        kwarg_filter (dict[str, Any]): Filtering condition for keyword arguments.
+    """
+
+    def __init__(
+        self,
+        name: str,
+        env: Environment,
+        static_score: float = 0,
+        score_map: dict[str, float] = None,
+        score_on_specific_kwarg: str = None,
+        kwarg_filter: dict[str, Any] = None,
+        **kwargs,
+    ):
+        super().__init__(name, env, **kwargs)
+        self.score_map = score_map
+        self.static_score = static_score
+        self.kwarg_filter = kwarg_filter
+        self.score_on_specific_kwarg = score_on_specific_kwarg
+
+    def __call__(self, hook_ref: str, env: Environment, **kwargs):
+        if self.score_on_specific_kwarg:
+            if kwargs[self.score_on_specific_kwarg] in self.score_map:
+                self.env.increment_score(
+                    self.score_map[kwargs[self.score_on_specific_kwarg]],
+                    info=f"{hook_ref} - {kwargs[self.score_on_specific_kwarg]}",
+                )
+            else:
+                self.env.increment_score(self.static_score, info=hook_ref)
+        elif self.score_map and hook_ref in self.score_map:
+            if self.kwarg_filter:
+                if kwargs.items() <= self.kwarg_filter.items():
+                    self.env.increment_score(
+                        self.score_map[hook_ref],
+                        info=f"{hook_ref} - {self.kwarg_filter}",
+                    )
+            else:
+                self.env.increment_score(self.score_map[hook_ref], info=hook_ref)
+        else:
+            self.env.increment_score(self.static_score, info=hook_ref)
diff --git a/overcooked_simulator/server_results.py b/cooperative_cuisine/server_results.py
similarity index 85%
rename from overcooked_simulator/server_results.py
rename to cooperative_cuisine/server_results.py
index ed78c792d8c14ea95f2c47079cf41e1a4900fba3..74b84f7dcfdb44040a831cf376bd5658a0ab6969 100644
--- a/overcooked_simulator/server_results.py
+++ b/cooperative_cuisine/server_results.py
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
 from typing_extensions import TypedDict, Literal
 
 if TYPE_CHECKING:
-    from overcooked_simulator.game_server import PlayerRequestType
+    from cooperative_cuisine.game_server import PlayerRequestType
 
 
 class PlayerInfo(TypedDict):
@@ -18,6 +18,7 @@ class PlayerInfo(TypedDict):
 class CreateEnvResult(TypedDict):
     env_id: str
     player_info: dict[str, PlayerInfo]
+    recipe_graphs: list[dict]
 
 
 class PlayerRequestResult(TypedDict):
diff --git a/overcooked_simulator/state_representation.py b/cooperative_cuisine/state_representation.py
similarity index 57%
rename from overcooked_simulator/state_representation.py
rename to cooperative_cuisine/state_representation.py
index e8ad5d17a79b4db29c2977934f0127c7a09c83f9..4479c39216537278e8180166f21090ae541bc690 100644
--- a/overcooked_simulator/state_representation.py
+++ b/cooperative_cuisine/state_representation.py
@@ -1,4 +1,8 @@
+"""
+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
@@ -12,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 ?
 
 
@@ -30,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]
@@ -50,14 +60,49 @@ class PlayerState(TypedDict):
     current_nearest_counter_id: str | None
 
 
+class KitchenInfo(BaseModel):
+    """Basic information of the kitchen."""
+
+    width: float
+    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."""
+
     players: list[PlayerState]
     counters: list[CounterState]
+    kitchen: KitchenInfo
     score: float | int
     orders: list[OrderState]
+    all_players_ready: bool
     ended: bool
     env_time: datetime  # isoformat str
     remaining_time: float
+    view_restrictions: None | list[ViewRestriction]
+    served_meals: list[tuple[str, str]]
+    info_msg: list[tuple[str, str]]
 
 
 def create_json_schema():
diff --git a/cooperative_cuisine/study_server.py b/cooperative_cuisine/study_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..3319cdde14eb4748399c12b5b01cf821e703d879
--- /dev/null
+++ b/cooperative_cuisine/study_server.py
@@ -0,0 +1,442 @@
+"""
+# Usage
+- Set `CONNECT_WITH_STUDY_SERVER` in 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 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
+import os
+import random
+import signal
+import subprocess
+from pathlib import Path
+from subprocess import Popen
+from typing import Tuple, TypedDict
+
+import requests
+import uvicorn
+import yaml
+from fastapi import FastAPI
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.environment import EnvironmentConfig
+from cooperative_cuisine.game_server import CreateEnvironmentConfig
+from cooperative_cuisine.server_results import PlayerInfo
+from cooperative_cuisine.utils import (
+    url_and_port_arguments,
+    add_list_of_manager_ids_arguments,
+)
+
+NUMBER_PLAYER_PER_ENV = 2
+
+log = logging.getLogger(__name__)
+
+app = FastAPI()
+
+
+# HARDCODED_MANAGER_ID = "1234"
+
+USE_AAAMBOS_AGENT = False
+
+
+class LevelConfig(TypedDict):
+    name: str
+    config_path: str
+    layout_path: str
+    item_info_path: str
+
+
+class LevelInfo(TypedDict):
+    name: str
+    last_level: bool
+    recipes: list[str]
+    recipe_graphs: list[dict]
+
+
+class StudyConfig(TypedDict):
+    levels: list[LevelConfig]
+    num_players: int
+    num_bots: int
+
+
+class StudyState:
+    def __init__(self, study_config_path: str | Path, game_url, game_port):
+        with open(study_config_path, "r") as file:
+            env_config_f = file.read()
+
+        self.study_config: StudyConfig = yaml.load(
+            str(env_config_f), Loader=yaml.SafeLoader
+        )
+        self.levels: list[LevelConfig] = self.study_config["levels"]
+        self.current_level_idx: int = 0
+
+        self.participant_id_to_player_info = {}
+        self.player_ids = {}
+        self.num_connected_players: int = 0
+
+        self.current_running_env = None
+        self.next_level_env = None
+        self.players_done = {}
+
+        self.USE_AAAMBOS_AGENT = False
+
+        self.websocket_url = f"ws://{game_url}:{game_port}/ws/player/"
+        print("WS:", self.websocket_url)
+        self.sub_processes = []
+
+    @property
+    def study_done(self):
+        return self.current_level_idx >= len(self.levels)
+
+    @property
+    def last_level(self):
+        return self.current_level_idx >= len(self.levels) - 1
+
+    @property
+    def is_full(self):
+        return (
+            len(self.participant_id_to_player_info) == self.study_config["num_players"]
+        )
+
+    def can_add_participant(self, num_participants: int) -> bool:
+        filled = (
+            self.num_connected_players + num_participants
+            <= self.study_config["num_players"]
+        )
+        return filled and not self.is_full
+
+    def create_env(self, level):
+        with open(ROOT_DIR / "configs" / level["item_info_path"], "r") as file:
+            item_info = file.read()
+            self.current_item_info: EnvironmentConfig = yaml.load(
+                item_info, Loader=yaml.Loader
+            )
+        with open(ROOT_DIR / "configs" / "layouts" / level["layout_path"], "r") as file:
+            layout = file.read()
+        with open(ROOT_DIR / "configs" / level["config_path"], "r") as file:
+            environment_config = file.read()
+            self.current_config: EnvironmentConfig = yaml.load(
+                environment_config, Loader=yaml.Loader
+            )
+        seed = int(random.random() * 1000000)
+        print(seed)
+        creation_json = CreateEnvironmentConfig(
+            manager_id=study_manager.server_manager_id,
+            number_players=self.study_config["num_players"]
+            + self.study_config["num_bots"],
+            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")
+
+        env_info = requests.post(
+            study_manager.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()
+
+        player_info = env_info["player_info"]
+        for idx, (player_id, player_info) in enumerate(player_info.items()):
+            if idx >= self.study_config["num_players"]:
+                self.create_and_connect_bot(player_id, player_info)
+        return env_info
+
+    def start(self):
+        level = self.levels[self.current_level_idx]
+        self.current_running_env = self.create_env(level)
+
+    def next_level(self):
+        requests.post(
+            f"{study_manager.game_server_url}/manage/stop_env/",
+            json={
+                "manager_id": study_manager.server_manager_id,
+                "env_id": self.current_running_env["env_id"],
+                "reason": "Next level",
+            },
+        )
+
+        self.current_level_idx += 1
+        if not self.study_done:
+            level = self.levels[self.current_level_idx]
+            self.current_running_env = self.create_env(level)
+            for (
+                participant_id,
+                player_info,
+            ) in self.participant_id_to_player_info.items():
+                new_player_info = {
+                    player_name: self.current_running_env["player_info"][player_name]
+                    for player_name in player_info.keys()
+                }
+                self.participant_id_to_player_info[participant_id] = new_player_info
+
+            for key in self.players_done:
+                self.players_done[key] = False
+
+    def add_participant(self, participant_id: str, number_players: int):
+        player_names = [
+            str(self.num_connected_players + i) for i in range(number_players)
+        ]
+        player_info = {
+            player_name: self.current_running_env["player_info"][player_name]
+            for player_name in player_names
+        }
+        self.participant_id_to_player_info[participant_id] = player_info
+        self.num_connected_players += number_players
+        return player_info
+
+    def player_finished_level(self, participant_id):
+        self.players_done[participant_id] = True
+        level_done = all(self.players_done.values())
+        if level_done:
+            self.next_level()
+
+    def get_connection(self, participant_id: str):
+        player_info = self.participant_id_to_player_info[participant_id]
+        current_level = self.levels[self.current_level_idx]
+        if self.current_config["meals"]["all"]:
+            recipes = ["all"]
+        else:
+            recipes = self.current_config["meals"]["list"]
+        level_info = LevelInfo(
+            name=current_level["name"],
+            last_level=self.last_level,
+            recipes=recipes,
+            recipe_graphs=self.current_running_env["recipe_graphs"],
+        )
+        return player_info, level_info
+
+    def create_and_connect_bot(self, player_id, player_info):
+        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}"'
+        )
+        if self.USE_AAAMBOS_AGENT:
+            sub = Popen(
+                " ".join(
+                    [
+                        "exec",
+                        "aaambos",
+                        "run",
+                        "--arch_config",
+                        str(ROOT_DIR / "configs" / "agents" / "arch_config.yml"),
+                        "--run_config",
+                        str(ROOT_DIR / "configs" / "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,
+            )
+        else:
+            sub = Popen(
+                " ".join(
+                    [
+                        "python",
+                        str(ROOT_DIR / "configs" / "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)
+
+    def kill_bots(self):
+        for sub in self.sub_processes:
+            try:
+                if self.USE_AAAMBOS_AGENT:
+                    pgrp = os.getpgid(sub.pid)
+                    os.killpg(pgrp, signal.SIGINT)
+                    subprocess.run(
+                        "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True
+                    )
+                else:
+                    sub.kill()
+
+            except ProcessLookupError:
+                pass
+
+        self.sub_processes = []
+        for websocket in self.websockets.values():
+            websocket.close()
+
+    def __repr__(self):
+        return f"Study({self.current_running_env['env_id']})"
+
+
+class StudyManager:
+    def __init__(self):
+        self.game_host: str | None = None
+        self.game_port: str | None = None
+        self.game_server_url: str | None = None
+        self.server_manager_id: str | None = None
+
+        self.running_studies: list[StudyState] = []
+
+        self.participant_id_to_study_map: dict[str, StudyState] = {}
+        self.running_envs: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {}
+        self.current_free_envs = []
+
+        self.running_tutorials: dict[
+            str, Tuple[int, dict[str, PlayerInfo], list[str]]
+        ] = {}
+
+    def create_study(self):
+        study = StudyState(
+            ROOT_DIR / "configs" / "study" / "study_config.yaml",
+            self.game_host,
+            self.game_port,
+        )
+        study.start()
+        self.running_studies.append(study)
+
+    def add_participant(self, participant_id: str, number_players: int):
+        player_info = None
+        if not self.running_studies or all(
+            [not s.can_add_participant(number_players) for s in self.running_studies]
+        ):
+            self.create_study()
+
+        for study in self.running_studies:
+            if study.can_add_participant(number_players):
+                player_info = study.add_participant(participant_id, number_players)
+                self.participant_id_to_study_map[participant_id] = study
+        return player_info
+
+    def player_finished_level(self, participant_id: str):
+        assigned_study = self.participant_id_to_study_map[participant_id]
+        assigned_study.player_finished_level(participant_id)
+
+    def get_participant_game_connection(self, participant_id: str):
+        assigned_study = self.participant_id_to_study_map[participant_id]
+        player_info, level_info = assigned_study.get_connection(participant_id)
+        return player_info, level_info
+
+    def set_game_server_url(self, game_host, game_port):
+        self.game_host = game_host
+        self.game_port = game_port
+        self.game_server_url = f"http://{self.game_host}:{self.game_port}"
+
+    def set_manager_id(self, manager_id: str):
+        self.server_manager_id = manager_id
+
+
+study_manager = StudyManager()
+
+
+@app.post("/start_study/{participant_id}/{number_players}")
+async def start_study(participant_id: str, number_players: int):
+    player_info = study_manager.add_participant(participant_id, number_players)
+    return player_info
+
+
+@app.post("/level_done/{participant_id}")
+async def level_done(participant_id: str):
+    last_level = study_manager.player_finished_level(participant_id)
+
+
+@app.post("/get_game_connection/{participant_id}")
+async def get_game_connection(participant_id: str):
+    player_info, level_info = study_manager.get_participant_game_connection(
+        participant_id
+    )
+    return {"player_info": player_info, "level_info": level_info}
+
+
+@app.post("/connect_to_tutorial/{participant_id}")
+async def want_to_play_tutorial(participant_id: str):
+    environment_config_path = ROOT_DIR / "configs" / "tutorial_env_config.yaml"
+    layout_path = ROOT_DIR / "configs" / "layouts" / "tutorial.layout"
+    item_info_path = ROOT_DIR / "configs" / "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()
+
+    print("STUDY MANAGER ID", study_manager.server_manager_id)
+    creation_json = CreateEnvironmentConfig(
+        manager_id=study_manager.server_manager_id,
+        number_players=1,
+        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(
+        study_manager.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()
+    study_manager.running_tutorials[participant_id] = env_info
+    return env_info["player_info"]["0"]
+
+
+@app.post("/disconnect_from_tutorial/{participant_id}")
+async def want_to_play_tutorial(participant_id: str):
+    requests.post(
+        f"{study_manager.game_server_url}/manage/stop_env/",
+        json={
+            "manager_id": study_manager.server_manager_id,
+            "env_id": study_manager.running_tutorials[participant_id]["env_id"],
+            "reason": "Finished tutorial",
+        },
+    )
+
+
+def main(study_host, study_port, game_host, game_port, manager_ids):
+    study_manager.set_game_server_url(game_host=game_host, game_port=game_port)
+    study_manager.set_manager_id(manager_id=manager_ids[0])
+
+    print(
+        f"Use {study_manager.server_manager_id=} for game_server_url=http://{game_host}:{game_port}"
+    )
+    loop = asyncio.new_event_loop()
+    config = uvicorn.Config(app, host=study_host, port=study_port, loop=loop)
+    server = uvicorn.Server(config)
+    loop.run_until_complete(server.serve())
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Cooperative Cuisine 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_study_port=8080,
+        default_game_port=8000,
+    )
+    add_list_of_manager_ids_arguments(parser=parser)
+    args = parser.parse_args()
+
+    game_server_url = f"https://{args.game_url}:{args.game_port}"
+    main(
+        args.study_url,
+        args.port,
+        game_host=args.game_url,
+        game_port=args.game_port,
+        manager_ids=args.manager_ids,
+    )
diff --git a/cooperative_cuisine/utils.py b/cooperative_cuisine/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d0a7dc38472667f22e9673cddbbe4d8f29e7fe8
--- /dev/null
+++ b/cooperative_cuisine/utils.py
@@ -0,0 +1,260 @@
+"""
+Some utility functions.
+"""
+from __future__ import annotations
+
+import dataclasses
+import json
+import logging
+import os
+import sys
+import uuid
+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 cooperative_cuisine import ROOT_DIR
+
+if TYPE_CHECKING:
+    from cooperative_cuisine.counters import Counter
+from cooperative_cuisine.player import Player
+
+DEFAULT_SERVER_URL = "localhost"
+
+
+@dataclasses.dataclass
+class VectorStateGenerationData:
+    grid_base_array: npt.NDArray[npt.NDArray[float]]
+    oh_len: int
+
+    number_normal_ingredients = 10
+
+    meals = [
+        "Chips",
+        "FriedFish",
+        "Burger",
+        "Salad",
+        "TomatoSoup",
+        "OnionSoup",
+        "FishAndChips",
+        "Pizza",
+    ]
+    equipments = [
+        "Pot",
+        "Pan",
+        "Basket",
+        "Peel",
+        "Plate",
+        "DirtyPlate",
+        "Extinguisher",
+    ]
+    ingredients = [
+        "Tomato",
+        "Lettuce",
+        "Onion",
+        "Meat",
+        "Bun",
+        "Potato",
+        "Fish",
+        "Dough",
+        "Cheese",
+        "Sausage",
+    ]
+
+
+@dataclasses.dataclass
+class VectorStateGenerationDataSimple:
+    grid_base_array: npt.NDArray[npt.NDArray[float]]
+    oh_len: int
+
+    number_normal_ingredients = 1
+
+    meals = [
+        "TomatoSoup",
+    ]
+    equipments = [
+        "Pot",
+        "Plate",
+        "DirtyPlate",
+        "Extinguisher",
+    ]
+    ingredients = [
+        "Tomato",
+    ]
+
+
+def create_init_env_time():
+    """Init time of the environment time, because all environments should have the same internal time."""
+    return datetime(
+        year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
+    )
+
+
+def get_closest(point: npt.NDArray[float], counters: list[Counter]):
+    """Determines the closest counter for a given 2d-coordinate point in the env.
+
+    Args:
+        point: The point in the env for which to find the closest counter
+        counters: List of objects with a `pos` attribute to compare to.
+
+    Returns: The closest counter for the given point.
+    """
+
+    return counters[
+        np.argmin(distance_matrix([point], [counter.pos for counter in counters])[0])
+    ]
+
+
+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."""
+
+    def convert_value(obj):
+        if isinstance(obj, Enum):
+            return obj.value
+        return obj
+
+    return dict((k, convert_value(v)) for k, v in data)
+
+
+def setup_logging(enable_websocket_logging=False):
+    path_logs = ROOT_DIR.parent / "logs"
+    os.makedirs(path_logs, exist_ok=True)
+    logging.basicConfig(
+        level=logging.DEBUG,
+        format="%(asctime)s %(levelname)-8s %(name)-50s %(message)s",
+        handlers=[
+            logging.FileHandler(
+                path_logs / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_debug.log",
+                encoding="utf-8",
+            ),
+            logging.StreamHandler(sys.stdout),
+        ],
+    )
+    logging.getLogger("matplotlib").setLevel(logging.WARNING)
+    if not enable_websocket_logging:
+        logging.getLogger("asyncio").setLevel(logging.ERROR)
+        logging.getLogger("asyncio.coroutines").setLevel(logging.ERROR)
+        logging.getLogger("websockets.server").setLevel(logging.ERROR)
+        logging.getLogger("websockets.protocol").setLevel(logging.ERROR)
+        logging.getLogger("websockets.client").setLevel(logging.ERROR)
+
+
+def url_and_port_arguments(
+    parser, server_name="game server", default_study_port=8080, default_game_port=8000
+):
+    parser.add_argument(
+        "-study-url",
+        "--study-url",
+        "--study-host",
+        type=str,
+        default=DEFAULT_SERVER_URL,
+        help=f"Overcooked {server_name} study host url.",
+    )
+    parser.add_argument(
+        "-sp",
+        "--study-port",
+        type=int,
+        default=default_study_port,
+        help=f"Port number for the {server_name}",
+    )
+    parser.add_argument(
+        "-game-url",
+        "--game-url",
+        "--game-host",
+        type=str,
+        default=DEFAULT_SERVER_URL,
+        help=f"Overcooked {server_name} game server url.",
+    )
+    parser.add_argument(
+        "-gp",
+        "--game-port",
+        type=int,
+        default=default_game_port,
+        help=f"Port number for the {server_name}",
+    )
+
+
+def disable_websocket_logging_arguments(parser):
+    parser.add_argument(
+        "--enable-websocket-logging" "", action="store_true", default=True
+    )
+
+
+def add_list_of_manager_ids_arguments(parser):
+    parser.add_argument(
+        "-m",
+        "--manager_ids",
+        nargs="+",
+        type=str,
+        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/overcooked_simulator/__init__.py b/overcooked_simulator/__init__.py
deleted file mode 100644
index fb941f2fadd719d7f44a3c1977aac8ee5f416538..0000000000000000000000000000000000000000
--- a/overcooked_simulator/__init__.py
+++ /dev/null
@@ -1,78 +0,0 @@
-"""
-
-This is the documentation of the Overcooked Simulator.
-
-# About the package
-
-The package contains of an environment for cooperation between players/agents. A PyGameGUI visualizes the game to
-human or visual agents in 2D. A 3D web-enabled version (for example for online studies, currently under development)
-can be found [here](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/godot-overcooked-3d-visualization)
-
-# Background / Literature
-The overcooked/cooking domain is a well established cooperation domain/task. There exists
-environments designed for reinforcement learning agents as well as the game and adaptations of the game for human
-players in a more "real-time" environment. They all mostly differ in the visual and graphics dimension. 2D versions
-like overcooked-ai, ... are most known in the community. But more visual appealing 3D versions for cooperation with
-humans are getting developed more frequently (cite,...). Besides, the general adaptations of the original overcooked
-game.
-With this overcooked-simulator, we want to bring both worlds together: the reinforcement learning and real-time playable
-environment with an appealing visualisation. Enable the potential of developing artificial agents that play with humans
-like a "real" cooperative / human partner.
-
-# Usage / Examples
-Our overcooked simulator is designed for real time interaction but also with reinforcement learning in mind (gymnasium environment).
-It focuses on configurability, extensibility and appealing visualization options.
-
-## Human Player
-Run it via the command line (in your pyenv/conda environment):
-
-```bash
-overcooked-sim  --url "localhost" --port 8000
-```
-
-_The arguments are the defaults. Therefore, they are optional._
-
-You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
-
-```bash
-python3 overcooked_simulator/game_server.py --url "localhost" --port 8000
-
-python3 overcooked_simulator/gui_2d_vis/overcooked_simulator.py --url "localhost" --port 8000
-
-## Connect with agent and receive game state
-...
-
-## Direct integration into your code.
-Initialize an environment....
-
-**TODO** JSON State description.
-
-
-# Citation
-
-# Structure of the Documentation
-The API documentation follows the file and content structure in the repo.
-On the left you can find the navigation panel that brings you to the implementation of
-- the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven),
-  sink, etc.,
-- the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
-- in **main**, you find an example how to start a simulation,
-- the **orders**, how to sample incoming orders and their attributes,
-- the **environment**, handles the incoming actions and provides the state,
-- the **player**/agent, that interacts in the environment,
-- a **simulation runner**, that calls the step function of the environment for a real-time interaction, and
-- **util**ity code.
-
-
-"""
-import os
-from pathlib import Path
-
-ROOT_DIR = Path(os.path.dirname(os.path.abspath(__file__)))  # This is your Project Root
-"""A path variable to get access to the layouts coming with the package. For example,
-```python 
-from overcooked_simulator import ROOT_DIR
-
-environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
-```
-"""
diff --git a/overcooked_simulator/__main__.py b/overcooked_simulator/__main__.py
deleted file mode 100644
index f81398ece39babd670716b79b045ee3226176e97..0000000000000000000000000000000000000000
--- a/overcooked_simulator/__main__.py
+++ /dev/null
@@ -1,62 +0,0 @@
-import argparse
-import time
-from multiprocessing import Process
-
-from overcooked_simulator.utils import (
-    url_and_port_arguments,
-    disable_websocket_logging_arguments,
-)
-
-
-def start_game_server(cli_args):
-    from overcooked_simulator.game_server import main
-
-    main(cli_args.url, cli_args.port)
-
-
-def start_pygame_gui(cli_args):
-    from overcooked_simulator.gui_2d_vis.overcooked_gui import main
-
-    main(cli_args.url, cli_args.port)
-
-
-def main(cli_args=None):
-    if cli_args is None:
-        cli_args = parser.parse_args()
-    game_server = None
-    pygame_gui = None
-    try:
-        print("Start game engine:")
-        game_server = Process(target=start_game_server, args=(cli_args,))
-        game_server.start()
-        time.sleep(1)
-        print("Start PyGame GUI:")
-        pygame_gui = Process(target=start_pygame_gui, args=(cli_args,))
-        pygame_gui.start()
-        while pygame_gui.is_alive() and game_server.is_alive():
-            time.sleep(1)
-    except KeyboardInterrupt:
-        print("Received Keyboard interrupt")
-    finally:
-        if game_server is not None and game_server.is_alive():
-            print("Terminate game server")
-            game_server.terminate()
-        if pygame_gui is not None and pygame_gui.is_alive():
-            print("Terminate pygame gui")
-            game_server.terminate()
-        time.sleep(0.1)
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        prog="Overcooked Simulator",
-        description="Game Engine Server + PyGameGUI: Starts overcooked game engine server and a PyGame 2D Visualization window in two processes.",
-        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
-    )
-
-    url_and_port_arguments(parser)
-    disable_websocket_logging_arguments(parser)
-
-    args = parser.parse_args()
-    print(args)
-    main(args)
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
deleted file mode 100644
index cad2888da61dd5ddb4de812f175f4b978fd2affd..0000000000000000000000000000000000000000
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ /dev/null
@@ -1,60 +0,0 @@
-plates:
-  clean_plates: 1
-  dirty_plates: 2
-  plate_delay: [ 5, 10 ]
-  # range of seconds until the dirty plate arrives.
-
-game:
-  time_limit_seconds: 300
-
-meals:
-  all: true
-  # if all: false -> only orders for these meals are generated
-  # TODO: what if this list is empty?
-  list:
-    - TomatoSoup
-    - OnionSoup
-    - Salad
-
-orders:
-  order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
-  # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py
-  order_gen_kwargs:
-    order_duration_random_func:
-      # how long should the orders be alive
-      # 'random' library call with getattr, kwargs are passed to the function
-      func: uniform
-      kwargs:
-        a: 40
-        b: 60
-    max_orders: 6
-    # maximum number of active orders at the same time
-    num_start_meals: 2
-    # number of orders generated at the start of the environment
-    sample_on_dur_random_func:
-      # 'random' library call with getattr, kwargs are passed to the function
-      func: uniform
-      kwargs:
-        a: 10
-        b: 20
-    sample_on_serving: false
-    # Sample the delay for the next order only after a meal was served.
-    score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func ''
-    score_calc_gen_kwargs:
-      # the kwargs for the score_calc_gen_func
-      other: 0
-      scores:
-        Burger: 15
-        OnionSoup: 10
-        Salad: 5
-        TomatoSoup: 10
-    expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
-    expired_penalty_kwargs:
-      default: -5
-  serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score ''
-  # a func that calcs a store for not ordered but served meals. Input: meal
-
-player_config:
-  radius: 0.4
-  player_speed_units_per_seconds: 8
-  interaction_range: 1.6
diff --git a/overcooked_simulator/game_content/layouts/basic.layout b/overcooked_simulator/game_content/layouts/basic.layout
deleted file mode 100644
index ccc4076303e985a8b60c9f2dd091f323b5d6e7a6..0000000000000000000000000000000000000000
--- a/overcooked_simulator/game_content/layouts/basic.layout
+++ /dev/null
@@ -1,9 +0,0 @@
-#QU#FO#TNLB#
-#__________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/empty.layout b/overcooked_simulator/game_content/layouts/empty.layout
deleted file mode 100644
index 2fa1dd8c29c076e9d94e9a305843ff87bebb29f1..0000000000000000000000000000000000000000
--- a/overcooked_simulator/game_content/layouts/empty.layout
+++ /dev/null
@@ -1,7 +0,0 @@
-______
-______
-______
-______
-______
-______
-_____P
\ No newline at end of file
diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py
deleted file mode 100644
index 415e0ce302c93ce2857f58569e5ed89e41c0aacd..0000000000000000000000000000000000000000
--- a/overcooked_simulator/gui_2d_vis/drawing.py
+++ /dev/null
@@ -1,487 +0,0 @@
-import colorsys
-import math
-from datetime import datetime, timedelta
-from pathlib import Path
-
-import numpy as np
-import numpy.typing as npt
-import pygame
-from scipy.spatial import KDTree
-
-from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.gui_2d_vis.game_colors import colors
-from overcooked_simulator.state_representation import (
-    PlayerState,
-    CookingEquipmentState,
-    ItemState,
-)
-
-USE_PLAYER_COOK_SPRITES = True
-SHOW_INTERACTION_RANGE = False
-SHOW_COUNTER_CENTERS = False
-
-
-def create_polygon(n, length):
-    if n == 1:
-        return np.array([0, 0])
-
-    vector = np.array([length, 0])
-    angle = (2 * np.pi) / n
-
-    rot_matrix = np.array(
-        [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]
-    )
-
-    vecs = [vector]
-    for i in range(n - 1):
-        vector = np.dot(rot_matrix, vector)
-        vecs.append(vector)
-
-    return vecs
-
-
-class Visualizer:
-    def __init__(self, config):
-        self.image_cache_dict = {}
-        self.player_colors = []
-        self.config = config
-
-    def create_player_colors(self, n) -> None:
-        hue_values = np.linspace(0, 1, n + 1)
-
-        colors_vec = np.array([col for col in colors.values()])
-
-        tree = KDTree(colors_vec)
-
-        color_names = list(colors.keys())
-
-        self.player_colors = []
-        for hue in hue_values:
-            rgb = colorsys.hsv_to_rgb(hue, 1, 1)
-            query_color = np.array([int(c * 255) for c in rgb])
-            _, index = tree.query(query_color, k=1)
-            self.player_colors.append(color_names[index])
-
-    def draw_gamescreen(
-        self,
-        screen,
-        state,
-        width,
-        height,
-        grid_size,
-    ):
-        self.draw_background(
-            surface=screen,
-            width=width,
-            height=height,
-            grid_size=grid_size,
-        )
-        self.draw_counters(
-            screen,
-            state,
-            grid_size,
-        )
-
-        self.draw_players(
-            screen,
-            state,
-            grid_size,
-        )
-
-    def draw_background(self, surface, width, height, grid_size):
-        """Visualizes a game background."""
-        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):
-            for y in range(0, height, block_size):
-                rect = pygame.Rect(x, y, block_size, block_size)
-                pygame.draw.rect(
-                    surface,
-                    self.config["Kitchen"]["background_lines"],
-                    rect,
-                    1,
-                )
-
-    def draw_image(
-        self,
-        screen: pygame.Surface,
-        img_path: Path | str,
-        size: float,
-        pos: npt.NDArray,
-        rot_angle=0,
-    ):
-        cache_entry = f"{img_path}"
-        if cache_entry in self.image_cache_dict.keys():
-            image = self.image_cache_dict[cache_entry]
-        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)
-        rect = image.get_rect()
-        rect.center = pos
-
-        screen.blit(image, rect)
-
-    def draw_players(
-        self,
-        screen: pygame.Surface,
-        state_dict: dict,
-        grid_size: float,
-    ):
-        """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.
-        """
-        for p_idx, player_dict in enumerate(state_dict["players"]):
-            player_dict: PlayerState
-            pos = np.array(player_dict["pos"]) * grid_size
-
-            facing = np.array(player_dict["facing_direction"])
-
-            if USE_PLAYER_COOK_SPRITES:
-                img_path = self.config["Cook"]["parts"][0]["path"]
-                rel_x, rel_y = facing
-                angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90
-                size = self.config["Cook"]["parts"][0]["size"] * grid_size
-                self.draw_image(screen, img_path, size, pos, angle)
-
-            else:
-                size = 0.4 * grid_size
-                color1 = self.player_colors[p_idx]
-                color2 = colors["white"]
-
-                pygame.draw.circle(screen, color2, pos, size)
-                pygame.draw.circle(screen, colors["blue"], pos, size, width=1)
-                pygame.draw.circle(screen, colors[color1], pos, size // 2)
-
-                pygame.draw.polygon(
-                    screen,
-                    colors["blue"],
-                    (
-                        (
-                            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),
-                    ),
-                )
-
-            if SHOW_INTERACTION_RANGE:
-                facing_point = np.array(player_dict["facing"])
-
-                pygame.draw.circle(
-                    screen,
-                    colors["blue"],
-                    facing_point * grid_size,
-                    1.6 * grid_size,
-                    width=1,
-                )
-                pygame.draw.circle(
-                    screen,
-                    colors["red1"],
-                    facing * grid_size,
-                    4,
-                )
-                pygame.draw.circle(screen, colors["red1"], facing, 4)
-
-            if player_dict["holding"] is not None:
-                holding_item_pos = pos + (20 * facing)
-                self.draw_item(
-                    pos=holding_item_pos,
-                    grid_size=grid_size,
-                    item=player_dict["holding"],
-                    screen=screen,
-                )
-
-                if player_dict["current_nearest_counter_pos"]:
-                    pos = player_dict["current_nearest_counter_pos"]
-                    pygame.draw.rect(
-                        screen,
-                        colors[self.player_colors[p_idx]],
-                        rect=pygame.Rect(
-                            pos[0] * grid_size - (grid_size // 2),
-                            pos[1] * grid_size - (grid_size // 2),
-                            grid_size,
-                            grid_size,
-                        ),
-                        width=2,
-                    )
-
-    def draw_thing(
-        self,
-        screen: pygame.Surface,
-        pos: npt.NDArray[float],
-        grid_size: float,
-        parts: list[dict[str]],
-        scale: float = 1.0,
-    ):
-        """Draws an item, based on its visual parts specified in the visualization config.
-
-        Args:
-            screen: the game screen to draw on.
-            grid_size: size of a grid cell.
-            pos: Where to draw the item parts.
-            parts: The visual parts to draw.
-            scale: Rescale the item by this factor.
-        """
-        for part in parts:
-            part_type = part["type"]
-            match part_type:
-                case "image":
-                    if "center_offset" in part:
-                        d = np.array(part["center_offset"]) * grid_size
-                        pos += d
-
-                    self.draw_image(
-                        screen,
-                        part["path"],
-                        part["size"] * scale * grid_size,
-                        pos,
-                    )
-                case "rect":
-                    height = part["height"] * grid_size
-                    width = part["width"] * grid_size
-                    color = part["color"]
-                    if "center_offset" in part:
-                        dx, dy = np.array(part["center_offset"]) * grid_size
-                        rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width)
-                        pygame.draw.rect(screen, color, rect)
-                    else:
-                        rect = pygame.Rect(
-                            pos[0] - (height / 2),
-                            pos[1] - (width / 2),
-                            height,
-                            width,
-                        )
-                    pygame.draw.rect(screen, color, rect)
-                case "circle":
-                    radius = part["radius"] * grid_size
-                    color = colors[part["color"]]
-                    if "center_offset" in part:
-                        pygame.draw.circle(
-                            screen,
-                            color,
-                            np.array(pos)
-                            + (np.array(part["center_offset"]) * grid_size),
-                            radius,
-                        )
-                    else:
-                        pygame.draw.circle(screen, color, pos, radius)
-
-    def draw_item(
-        self,
-        pos: npt.NDArray[float] | list[float],
-        grid_size: float,
-        item: ItemState | CookingEquipmentState,
-        scale: float = 1.0,
-        plate=False,
-        screen=None,
-    ):
-        """Visualization of an item at the specified position. On a counter or in the hands of the player.
-        The visual composition of the item is read in from visualization.yaml file, where it is specified as
-        different parts to be drawn.
-
-        Args:
-            grid_size: size of a grid cell.
-            pos: The position of the item to draw.
-            item: The item do be drawn in the game.
-            scale: Rescale the item by this factor.
-            screen: the pygame screen to draw on.
-            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:
-                item_key = item["type"]
-                if "Soup" in item_key and plate:
-                    item_key += "Plate"
-                self.draw_thing(
-                    pos=pos,
-                    parts=self.config[item_key]["parts"],
-                    scale=scale,
-                    screen=screen,
-                    grid_size=grid_size,
-                )
-                #
-        if "progress_percentage" in item and item["progress_percentage"] > 0.0:
-            self.draw_progress_bar(
-                screen, pos, item["progress_percentage"], grid_size=grid_size
-            )
-
-        if (
-            "content_ready" in item
-            and item["content_ready"]
-            and item["content_ready"]["type"] in self.config
-        ):
-            self.draw_thing(
-                pos=pos,
-                parts=self.config[item["content_ready"]["type"]]["parts"],
-                screen=screen,
-                grid_size=grid_size,
-            )
-        elif "content_list" in item and item["content_list"]:
-            triangle_offsets = create_polygon(len(item["content_list"]), length=10)
-            scale = 1 if len(item["content_list"]) == 1 else 0.6
-            for idx, o in enumerate(item["content_list"]):
-                self.draw_item(
-                    pos=np.array(pos) + triangle_offsets[idx],
-                    item=o,
-                    scale=scale,
-                    plate="Plate" in item["type"],
-                    screen=screen,
-                    grid_size=grid_size,
-                )
-
-    def draw_progress_bar(
-        self,
-        screen: pygame.Surface,
-        pos: npt.NDArray[float],
-        percent: float,
-        grid_size: float,
-    ):
-        """Visualize progress of progressing item as a green bar under the item."""
-        bar_height = grid_size * 0.2
-        progress_width = percent * grid_size
-        progress_bar = pygame.Rect(
-            pos[0] - (grid_size / 2),
-            pos[1] - (grid_size / 2) + grid_size - bar_height,
-            progress_width,
-            bar_height,
-        )
-        pygame.draw.rect(screen, colors["green1"], progress_bar)
-
-    def draw_counter(
-        self, screen: pygame.Surface, counter_dict: dict, grid_size: float
-    ):
-        """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.
-        """
-        pos = np.array(counter_dict["pos"]) * grid_size
-        counter_type = counter_dict["type"]
-        self.draw_thing(screen, pos, grid_size, self.config["Counter"]["parts"])
-        if counter_type in self.config:
-            self.draw_thing(screen, pos, grid_size, self.config[counter_type]["parts"])
-        else:
-            if counter_type in self.config:
-                parts = self.config[counter_type]["parts"]
-            elif counter_type.endswith("Dispenser"):
-                parts = self.config["Dispenser"]["parts"]
-            else:
-                raise ValueError(f"Can not draw counter type {counter_type}")
-            self.draw_thing(
-                screen=screen,
-                pos=pos,
-                parts=parts,
-                grid_size=grid_size,
-            )
-
-        occupied_by = counter_dict["occupied_by"]
-        if occupied_by is not None:
-            # Multiple plates on plate return:
-            if isinstance(occupied_by, list):
-                for i, o in enumerate(occupied_by):
-                    self.draw_item(
-                        screen=screen,
-                        pos=np.abs([pos[0], pos[1] - (i * 3)]),
-                        grid_size=grid_size,
-                        item=o,
-                    )
-            # All other items:
-            else:
-                self.draw_item(
-                    pos=pos,
-                    grid_size=grid_size,
-                    item=occupied_by,
-                    screen=screen,
-                )
-
-    def draw_counters(
-        self, screen: pygame, state, grid_size, SHOW_COUNTER_CENTERS=False
-    ):
-        """Visualizes the counters in the environment.
-
-        Args:            state: The game state returned by the environment.
-        """
-        for counter in state["counters"]:
-            self.draw_counter(screen, counter, grid_size)
-            if SHOW_COUNTER_CENTERS:
-                pygame.draw.circle(screen, colors["green1"], counter.pos, 3)
-
-    def draw_orders(
-        self, screen, state, grid_size, width, height, screen_margin, config
-    ):
-        orders_width = width - 100
-        orders_height = screen_margin
-        order_screen = pygame.Surface(
-            (orders_width, orders_height),
-        )
-
-        bg_color = colors[config["GameWindow"]["background_color"]]
-        pygame.draw.rect(order_screen, bg_color, order_screen.get_rect())
-
-        order_rects_start = (orders_height // 2) - (grid_size // 2)
-        for idx, order in enumerate(state["orders"]):
-            order_upper_left = [
-                order_rects_start + idx * grid_size * 1.2,
-                order_rects_start,
-            ]
-            pygame.draw.rect(
-                order_screen,
-                colors["red"],
-                pygame.Rect(
-                    order_upper_left[0],
-                    order_upper_left[1],
-                    grid_size,
-                    grid_size,
-                ),
-                width=2,
-            )
-            center = np.array(order_upper_left) + np.array(
-                [grid_size / 2, grid_size / 2]
-            )
-            self.draw_thing(
-                pos=center,
-                parts=config["Plate"]["parts"],
-                screen=order_screen,
-                grid_size=grid_size,
-            )
-            self.draw_item(
-                pos=center,
-                item={"type": order["meal"]},
-                plate=True,
-                screen=order_screen,
-                grid_size=grid_size,
-            )
-            order_done_seconds = (
-                (
-                    datetime.fromisoformat(order["start_time"])
-                    + timedelta(seconds=order["max_duration"])
-                )
-                - datetime.fromisoformat(state["env_time"])
-            ).total_seconds()
-
-            percentage = order_done_seconds / order["max_duration"]
-            self.draw_progress_bar(
-                pos=center,
-                percent=percentage,
-                screen=order_screen,
-                grid_size=grid_size,
-            )
-
-        orders_rect = order_screen.get_rect()
-        orders_rect.center = [
-            screen_margin + (orders_width // 2),
-            orders_height // 2,
-        ]
-        screen.blit(order_screen, orders_rect)
diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json
deleted file mode 100644
index 862d3d963c63ae0abe29c2c37516be2bc750b1cf..0000000000000000000000000000000000000000
--- a/overcooked_simulator/gui_2d_vis/gui_theme.json
+++ /dev/null
@@ -1,87 +0,0 @@
-{
-  "defaults": {
-    "colours": {
-      "normal_bg": "#45494e",
-      "hovered_bg": "#35393e",
-      "disabled_bg": "#25292e",
-      "selected_bg": "#193754",
-      "dark_bg": "#15191e",
-      "normal_text": "#c5cbd8",
-      "hovered_text": "#FFFFFF",
-      "selected_text": "#FFFFFF",
-      "disabled_text": "#6d736f",
-      "link_text": "#0000EE",
-      "link_hover": "#2020FF",
-      "link_selected": "#551A8B",
-      "text_shadow": "#777777",
-      "normal_border": "#DDDDDD",
-      "hovered_border": "#B0B0B0",
-      "disabled_border": "#808080",
-      "selected_border": "#8080B0",
-      "active_border": "#8080B0",
-      "filled_bar": "#f4251b",
-      "unfilled_bar": "#CCCCCC"
-    }
-  },
-  "button": {
-    "colours": {
-      "normal_bg": "#45494e",
-      "hovered_bg": "#35393e",
-      "disabled_bg": "#25292e",
-      "selected_bg": "#193754",
-      "active_bg": "#193754",
-      "dark_bg": "#15191e",
-      "normal_text": "#c5cbd8",
-      "hovered_text": "#FFFFFF",
-      "selected_text": "#FFFFFF",
-      "disabled_text": "#6d736f",
-      "active_text": "#FFFFFF",
-      "normal_border": "#DDDDDD",
-      "hovered_border": "#B0B0B0",
-      "disabled_border": "#808080",
-      "selected_border": "#8080B0",
-      "active_border": "#8080B0"
-    },
-    "misc": {
-      "tool_tip_delay": "1.5"
-    },
-    "font": {
-      "size": 15,
-      "bold": 1
-    }
-  },
-  "#timer_label": {
-    "colours": {
-      "normal_text": "#000000"
-    },
-    "font": {
-      "size": 20,
-      "bold": 0
-    }
-  },
-  "#score_label": {
-    "colours": {
-      "normal_text": "#000000"
-    },
-    "font": {
-      "size": 20,
-      "bold": 1
-    }
-  },
-  "#orders_label": {
-    "colours": {
-      "normal_text": "#000000"
-    },
-    "font": {
-      "size": 20,
-      "bold": 0
-    }
-  },
-  "#quit_button": {
-    "colours": {
-      "normal_bg": "#f71b29",
-      "hovered_bg": "#bf0310",
-      "normal_border": "#DDDDDD"
-    }
-  }
-}
\ 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
deleted file mode 100644
index 522ec051e8f1ad938c8e53cd0e8b563f1e383cb1..0000000000000000000000000000000000000000
Binary files a/overcooked_simulator/gui_2d_vis/images/arrow_right.png and /dev/null differ
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
deleted file mode 100644
index 1370d8475ccf53668005af4902b9939b1420d201..0000000000000000000000000000000000000000
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ /dev/null
@@ -1,703 +0,0 @@
-import argparse
-import dataclasses
-import json
-import logging
-import sys
-from enum import Enum
-
-import numpy as np
-import pygame
-import pygame_gui
-import requests
-import yaml
-from websockets.sync.client import connect
-
-from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.game_server import CreateEnvironmentConfig
-from overcooked_simulator.gui_2d_vis.drawing import Visualizer
-from overcooked_simulator.gui_2d_vis.game_colors import colors
-from overcooked_simulator.overcooked_environment import (
-    Action,
-    ActionType,
-    InterActionData,
-)
-from overcooked_simulator.utils import (
-    custom_asdict_factory,
-    setup_logging,
-    url_and_port_arguments,
-    disable_websocket_logging_arguments,
-)
-
-
-class MenuStates(Enum):
-    Start = "Start"
-    Game = "Game"
-    End = "End"
-
-
-MANAGER_ID = "1233245425"
-
-
-log = logging.getLogger(__name__)
-
-
-class PlayerKeySet:
-    """Set of keyboard keys for controlling a player.
-    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]):
-        """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.
-        """
-        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.interact_key = self.player_keys[-2]
-        self.pickup_key = self.player_keys[-1]
-
-
-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: str,
-    ):
-        self.game_screen = 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)
-            for player_name, keys in zip(
-                self.player_names, self.player_keys[: len(self.player_names)]
-            )
-        ]
-
-        self.websocket_url = f"ws://{url}:{port}/ws/player/"
-        self.websockets = {}
-
-        self.request_url = f"http://{url}:{port}"
-
-        # TODO cache loaded images?
-        with open(ROOT_DIR / "gui_2d_vis" / "visualization.yaml", "r") as file:
-            self.visualization_config = yaml.safe_load(file)
-
-        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.images_path = ROOT_DIR / "pygame_gui" / "images"
-
-        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):
-        counter_positions = np.array([c["pos"] for c in state["counters"]])
-        kitchen_width = counter_positions[:, 0].max() + 0.5
-        kitchen_height = counter_positions[:, 1].max() + 0.5
-        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 / self.simulator.env.kitchen_width)
-
-        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 / self.simulator.env.kitchen_width)
-
-        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,
-            )
-
-        else:
-            game_width, game_height = 0, 0
-            grid_size = 0
-
-        window_width, window_height = (
-            game_width + (2 * self.screen_margin),
-            game_height + (2 * self.screen_margin),  # bar with orders
-        )
-
-        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 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]):
-                move_vec = np.zeros(2)
-                for idx, pressed in enumerate(relevant_keys[:-2]):
-                    if pressed:
-                        move_vec += key_set.move_vectors[idx]
-                if np.linalg.norm(move_vec) != 0:
-                    move_vec = move_vec / np.linalg.norm(move_vec)
-
-                action = Action(
-                    key_set.name, ActionType.MOVEMENT, move_vec, duration=1 / self.FPS
-                )
-                self.send_action(action)
-
-    def handle_key_event(self, event):
-        """Handles key events for the pickup and interaction keys. Pickup is a single action,
-        for interaction keydown and keyup is necessary, because the player has to be able to hold
-        the key down.
-
-        Args:
-            event: Pygame event for extracting the key action.
-        """
-        for key_set in self.player_key_sets:
-            if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN:
-                action = Action(key_set.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
-                    )
-                    self.send_action(action)
-                elif event.type == pygame.KEYUP:
-                    action = Action(
-                        key_set.name, ActionType.INTERACT, InterActionData.STOP
-                    )
-                    self.send_action(action)
-
-    def init_ui_elements(self):
-        self.manager = pygame_gui.UIManager((self.window_width, self.window_height))
-        self.manager.get_theme().load_theme(ROOT_DIR / "gui_2d_vis" / "gui_theme.json")
-
-        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),
-            ),
-            text="Start Game",
-            manager=self.manager,
-        )
-        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),
-            ),
-            text="Quit Game",
-            manager=self.manager,
-            object_id="#quit_button",
-        )
-        self.quit_button.can_hover()
-
-        self.reset_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    (self.screen_margin + self.game_width),
-                    self.screen_margin,
-                ),
-                (self.screen_margin, 100),
-            ),
-            text="RESET",
-            manager=self.manager,
-            object_id="#quit_button",
-        )
-        self.reset_button.can_hover()
-
-        self.finished_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    (self.window_width - self.buttons_width),
-                    (self.window_height - self.buttons_height),
-                ),
-                (self.buttons_width, self.buttons_height),
-            ),
-            text="Finish round",
-            manager=self.manager,
-        )
-        self.finished_button.can_hover()
-
-        self.back_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    (0),
-                    (self.window_height - self.buttons_height),
-                ),
-                (self.buttons_width, self.buttons_height),
-            ),
-            text="Back to menu",
-            manager=self.manager,
-        )
-        self.back_button.can_hover()
-
-        self.score_label = pygame_gui.elements.UILabel(
-            text=f"Score: _",
-            relative_rect=pygame.Rect(
-                (
-                    (0),
-                    self.window_height - self.screen_margin,
-                ),
-                (self.screen_margin * 2, self.screen_margin),
-            ),
-            manager=self.manager,
-            object_id="#score_label",
-        )
-
-        layout_file_paths = [
-            str(p.name)
-            for p in (ROOT_DIR / "game_content" / "layouts").glob("*.layout")
-        ]
-        assert len(layout_file_paths) != 0, "No layout files."
-        dropdown_width, dropdown_height = 200, 40
-        self.layout_selection = pygame_gui.elements.UIDropDownMenu(
-            relative_rect=pygame.Rect(
-                (
-                    0,
-                    0,
-                ),
-                (dropdown_width, dropdown_height),
-            ),
-            manager=self.manager,
-            options_list=layout_file_paths,
-            starting_option=layout_file_paths[-1],
-        )
-        self.timer_label = pygame_gui.elements.UILabel(
-            text="GAMETIME",
-            relative_rect=pygame.Rect(
-                (self.screen_margin, self.window_height - self.screen_margin),
-                (self.game_width, self.screen_margin),
-            ),
-            manager=self.manager,
-            object_id="#timer_label",
-        )
-
-        self.orders_label = pygame_gui.elements.UILabel(
-            text="Orders:",
-            relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin),
-            manager=self.manager,
-            object_id="#orders_label",
-        )
-
-        self.conclusion_label = pygame_gui.elements.UILabel(
-            text="Your final score was _",
-            relative_rect=pygame.Rect(0, 0, self.window_width, self.window_height),
-            manager=self.manager,
-            object_id="#score_label",
-        )
-
-    def draw(self, state):
-        """Main visualization function.
-
-        Args:            state: The game state returned by the environment."""
-        self.vis.draw_gamescreen(
-            self.game_screen,
-            state,
-            self.game_width,
-            self.game_height,
-            self.grid_size,
-        )
-
-        # self.manager.draw_ui(self.main_window)
-        self.update_remaining_time(state["remaining_time"])
-
-        self.vis.draw_orders(
-            screen=self.main_window,
-            state=state,
-            grid_size=self.grid_size,
-            width=self.game_width,
-            height=self.game_height,
-            screen_margin=self.screen_margin,
-            config=self.visualization_config,
-        )
-        self.update_score_label(state)
-
-    def set_window_size(self):
-        self.game_screen = pygame.Surface(
-            (
-                self.game_width,
-                self.game_height,
-            ),
-        )
-        self.main_window = pygame.display.set_mode(
-            (
-                self.window_width,
-                self.window_height,
-            )
-        )
-
-    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()
-        self.init_ui_elements()
-
-    def manage_button_visibility(self):
-        match self.menu_state:
-            case MenuStates.Start:
-                self.back_button.hide()
-                self.quit_button.show()
-                self.start_button.show()
-                self.score_label.hide()
-                self.finished_button.hide()
-                self.layout_selection.show()
-                self.timer_label.hide()
-                self.orders_label.hide()
-                self.conclusion_label.hide()
-            case MenuStates.Game:
-                self.start_button.hide()
-                self.back_button.hide()
-                self.score_label.show()
-                self.score_label.show()
-                self.finished_button.show()
-                self.layout_selection.hide()
-                self.timer_label.show()
-                self.orders_label.show()
-                self.conclusion_label.hide()
-            case MenuStates.End:
-                self.start_button.hide()
-                self.back_button.show()
-                self.score_label.hide()
-                self.finished_button.hide()
-                self.layout_selection.hide()
-                self.timer_label.hide()
-                self.orders_label.hide()
-                self.conclusion_label.show()
-
-    def update_score_label(self, state):
-        score = state["score"]
-        self.score_label.set_text(f"Score {score}")
-
-    def update_conclusion_label(self, state):
-        score = state["score"]
-        self.conclusion_label.set_text(f"Your final score is {score}. Hurray!")
-
-    def update_remaining_time(self, remaining_time: float):
-        hours, rem = divmod(int(remaining_time), 3600)
-        minutes, seconds = divmod(rem, 60)
-        display_time = f"{minutes}:{'%02d' % seconds}"
-        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 = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
-        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=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,
-        )
-        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():
-            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.window_width,
-            self.window_height,
-            self.game_width,
-            self.game_height,
-            self.grid_size,
-        ) = self.get_window_sizes(state)
-
-    def start_button_press(self):
-        self.menu_state = MenuStates.Game
-
-        self.setup_environment()
-
-        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()
-        log.debug("Pressed back button")
-
-    def quit_button_press(self):
-        self.running = False
-        self.menu_state = MenuStates.Start
-        log.debug("Pressed quit button")
-
-    def reset_button_press(self):
-        requests.post(
-            f"{self.request_url}/manage/stop_env",
-            json={
-                "manager_id": 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": 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 send_action(self, action: Action):
-        """Sends an action to the game environment.
-
-        Args:
-            action: The action to be sent. Contains the player, action type and move direction if action is a movement.
-        """
-        if isinstance(action.action_data, np.ndarray):
-            action.action_data = [
-                float(action.action_data[0]),
-                float(action.action_data[1]),
-            ]
-        self.websockets[action.player].send(
-            json.dumps(
-                {
-                    "type": "action",
-                    "action": dataclasses.asdict(
-                        action, dict_factory=custom_asdict_factory
-                    ),
-                    "player_hash": self.player_info[action.player]["player_hash"],
-                }
-            )
-        )
-        self.websockets[action.player].recv()
-
-    def request_state(self):
-        self.websockets[self.state_player_id].send(
-            json.dumps(
-                {
-                    "type": "get_state",
-                    "player_hash": self.player_info[self.state_player_id][
-                        "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 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()
-
-        pygame.display.set_caption("Simple Overcooked Simulator")
-
-        clock = pygame.time.Clock()
-
-        self.reset_window_size()
-        self.init_ui_elements()
-        self.manage_button_visibility()
-
-        # Game loop
-        self.running = True
-        while self.running:
-            try:
-                time_delta = clock.tick(self.FPS) / 1000.0
-
-                for event in pygame.event.get():
-                    if event.type == pygame.QUIT:
-                        self.running = False
-
-                        # UI Buttons:
-                    if event.type == pygame_gui.UI_BUTTON_PRESSED:
-                        match event.ui_element:
-                            case self.start_button:
-                                self.start_button_press()
-                            case self.back_button:
-                                self.back_button_press()
-                                self.disconnect_websockets()
-
-                            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()
-
-                        self.manage_button_visibility()
-
-                    if (
-                        event.type in [pygame.KEYDOWN, pygame.KEYUP]
-                        and self.menu_state == MenuStates.Game
-                    ):
-                        pass
-                        self.handle_key_event(event)
-
-                    self.manager.process_events(event)
-
-                    # drawing:
-                self.main_window.fill(
-                    colors[self.visualization_config["GameWindow"]["background_color"]]
-                )
-                self.manager.draw_ui(self.main_window)
-
-                match self.menu_state:
-                    case MenuStates.Start:
-                        pass
-
-                    case MenuStates.Game:
-                        state = self.request_state()
-
-                        self.handle_keys()
-
-                        if state["ended"]:
-                            self.finished_button_press()
-                            self.disconnect_websockets()
-                            self.manage_button_visibility()
-                        else:
-                            self.draw(state)
-
-                            game_screen_rect = self.game_screen.get_rect()
-                            game_screen_rect.center = [
-                                self.window_width // 2,
-                                self.window_height // 2,
-                            ]
-                            self.main_window.blit(self.game_screen, game_screen_rect)
-
-                    case MenuStates.End:
-                        self.update_conclusion_label(state)
-
-                self.manager.update(time_delta)
-                pygame.display.flip()
-
-            except (KeyboardInterrupt, SystemExit):
-                self.running = False
-
-        pygame.quit()
-        sys.exit()
-
-
-def main(url, port):
-    # TODO maybe read the player names and keyboard keys from config file?
-    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]
-
-    number_players = 2
-    gui = PyGameGUI(
-        list(map(str, range(number_players))), [keys1, keys2], url=url, port=port
-    )
-    gui.start_pygame()
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        prog="Overcooked Simulator 2D PyGame Visualization",
-        description="PyGameGUI: a PyGame 2D Visualization window.",
-        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
-    )
-
-    url_and_port_arguments(parser)
-    disable_websocket_logging_arguments(parser)
-    args = parser.parse_args()
-    setup_logging(enable_websocket_logging=args.enable_websocket_logging)
-    main(args.url, args.port)
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
deleted file mode 100644
index ce79e280b4760a59ab92129f86bd26c905639a1f..0000000000000000000000000000000000000000
--- a/overcooked_simulator/overcooked_environment.py
+++ /dev/null
@@ -1,799 +0,0 @@
-from __future__ import annotations
-
-import dataclasses
-import json
-import logging
-import random
-from datetime import timedelta, datetime
-from enum import Enum
-from pathlib import Path
-from typing import Literal
-
-import numpy as np
-import numpy.typing as npt
-import yaml
-from scipy.spatial import distance_matrix
-
-from overcooked_simulator.counters import (
-    Counter,
-    CuttingBoard,
-    Trashcan,
-    Dispenser,
-    ServingWindow,
-    CookingCounter,
-    Sink,
-    PlateDispenser,
-    SinkAddon,
-    PlateConfig,
-)
-from overcooked_simulator.game_items import (
-    ItemInfo,
-    ItemType,
-    CookingEquipment,
-)
-from overcooked_simulator.order import OrderAndScoreManager
-from overcooked_simulator.player import Player, PlayerConfig
-from overcooked_simulator.state_representation import StateRepresentation
-from overcooked_simulator.utils import create_init_env_time
-
-log = logging.getLogger(__name__)
-
-
-class ActionType(Enum):
-    """The 3 different types of valid actions. They can be extended via the `Action.action_data` attribute."""
-
-    MOVEMENT = "movement"
-    """move the agent."""
-    PUT = "pickup"
-    """interaction type 1, e.g., for pickup or drop off. Maybe other words: transplace?"""
-    # TODO change value to put
-    INTERACT = "interact"
-    """interaction type 2, e.g., for progressing. Start and stop interaction via `keydown` and `keyup` actions."""
-
-
-class InterActionData(Enum):
-    """The data for the interaction action: `ActionType.MOVEMENT`."""
-
-    START = "keydown"
-    "start an interaction."
-    STOP = "keyup"
-    "stop an interaction without moving away."
-
-
-@dataclasses.dataclass
-class Action:
-    """Action class, specifies player, action type and action itself."""
-
-    player: str
-    """Id of the player."""
-    action_type: ActionType
-    """Type of the action to perform. Defines what action data is valid."""
-    action_data: npt.NDArray[float] | InterActionData | Literal["pickup"]
-    """Data for the action, e.g., movement vector or start and stop interaction."""
-    duration: float | int = 0
-    """Duration of the action (relevant for movement)"""
-
-    def __repr__(self):
-        return f"Action({self.player},{self.action_type.value},{self.action_data},{self.duration})"
-
-    def __post_init__(self):
-        if isinstance(self.action_type, str):
-            self.action_type = ActionType(self.action_type)
-        if isinstance(self.action_data, str) and self.action_data != "pickup":
-            self.action_data = InterActionData(self.action_data)
-
-
-# TODO Abstract base class for different environments
-
-
-class Environment:
-    """Environment class which handles the game logic for the overcooked-inspired environment.
-
-    Handles player movement, collision-detection, counters, cooking processes, recipes, incoming orders, time.
-    """
-
-    PAUSED = None
-
-    def __init__(
-        self,
-        env_config: Path | str,
-        layout_config: Path | str,
-        item_info: Path | str,
-        as_files: bool = True,
-    ):
-        self.players: dict[str, Player] = {}
-        """the player, keyed by their id/name."""
-
-        self.as_files = as_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)
-        self.layout_config = layout_config
-        # self.counter_side_length = 1  # -> this changed! is 1 now
-
-        self.item_info = self.load_item_info(item_info)
-        """The loaded item info dict. Keys are the item names."""
-        # self.validate_item_info()
-        if self.environment_config["meals"]["all"]:
-            self.allowed_meal_names = set(
-                [
-                    item
-                    for item, info in self.item_info.items()
-                    if info.type == ItemType.Meal
-                ]
-            )
-        else:
-            self.allowed_meal_names = set(self.environment_config["meals"]["list"])
-            """The allowed meals depend on the `environment_config.yml` configured behaviour. Either all meals that 
-            are possible or only a limited subset."""
-        self.order_and_score = OrderAndScoreManager(
-            order_config=self.environment_config["orders"],
-            available_meals={
-                item: info
-                for item, info in self.item_info.items()
-                if info.type == ItemType.Meal and item in self.allowed_meal_names
-            },
-        )
-        """The manager for the orders and score update."""
-        plate_transitions = {
-            item: {
-                "seconds": info.seconds,
-                "needs": info.needs,
-                "info": info,
-            }
-            for item, info in self.item_info.items()
-            if info.type == ItemType.Meal
-        }
-
-        cooking_counter_equipments = {
-            cooking_counter: [
-                equipment
-                for equipment, e_info in self.item_info.items()
-                if e_info.equipment and e_info.equipment.name == cooking_counter
-            ]
-            for cooking_counter, info in self.item_info.items()
-            if info.type == ItemType.Equipment and info.equipment is None
-        }
-
-        self.SYMBOL_TO_CHARACTER_MAP = {
-            "#": Counter,
-            "C": lambda pos: CuttingBoard(
-                pos,
-                {
-                    info.needs[0]: {"seconds": info.seconds, "result": item}
-                    for item, info in self.item_info.items()
-                    if info.equipment is not None
-                    and info.equipment.name == "CuttingBoard"
-                },
-            ),
-            "X": Trashcan,
-            "W": lambda pos: ServingWindow(
-                pos,
-                self.order_and_score,
-                meals=self.allowed_meal_names,
-                env_time_func=self.get_env_time,
-            ),
-            "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]),
-            "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]),
-            "K": lambda pos: Dispenser(pos, self.item_info["Potato"]),  # Kartoffel
-            "I": lambda pos: Dispenser(pos, self.item_info["Fish"]),  # fIIIsh
-            "D": lambda pos: Dispenser(pos, self.item_info["Dough"]),
-            "E": lambda pos: Dispenser(pos, self.item_info["Cheese"]),  # chEEEEse
-            "G": lambda pos: Dispenser(pos, self.item_info["Sausage"]),  # sausaGe
-            "P": lambda pos: PlateDispenser(
-                plate_transitions=plate_transitions,
-                pos=pos,
-                dispensing=self.item_info["Plate"],
-                plate_config=PlateConfig(
-                    **(
-                        self.environment_config["plates"]
-                        if "plates" in self.environment_config
-                        else {}
-                    )
-                ),
-            ),
-            "N": lambda pos: Dispenser(pos, self.item_info["Onion"]),  # N for oNioN
-            "_": "Free",
-            "A": "Agent",
-            "U": lambda pos: CookingCounter(
-                name="Stove",
-                cooking_counter_equipments=cooking_counter_equipments,
-                pos=pos,
-                occupied_by=CookingEquipment(
-                    name="Pot",
-                    item_info=self.item_info["Pot"],
-                    transitions={
-                        item: {
-                            "seconds": info.seconds,
-                            "needs": info.needs,
-                            "info": info,
-                        }
-                        for item, info in self.item_info.items()
-                        if info.equipment is not None and info.equipment.name == "Pot"
-                    },
-                ),
-            ),  # Stove with pot: U because it looks like a pot
-            "Q": lambda pos: CookingCounter(
-                name="Stove",
-                cooking_counter_equipments=cooking_counter_equipments,
-                pos=pos,
-                occupied_by=CookingEquipment(
-                    name="Pan",
-                    item_info=self.item_info["Pan"],
-                    transitions={
-                        item: {
-                            "seconds": info.seconds,
-                            "needs": info.needs,
-                            "info": info,
-                        }
-                        for item, info in self.item_info.items()
-                        if info.equipment is not None and info.equipment.name == "Pan"
-                    },
-                ),
-            ),  # Stove with pan: Q because it looks like a pan
-            "O": lambda pos: CookingCounter(
-                name="Oven",
-                cooking_counter_equipments=cooking_counter_equipments,
-                pos=pos,
-                occupied_by=CookingEquipment(
-                    name="Peel",
-                    item_info=self.item_info["Peel"],
-                    transitions={
-                        item: {
-                            "seconds": info.seconds,
-                            "needs": info.needs,
-                            "info": info,
-                        }
-                        for item, info in self.item_info.items()
-                        if info.equipment is not None and info.equipment.name == "Peel"
-                    },
-                ),
-            ),
-            "F": lambda pos: CookingCounter(
-                name="DeepFryer",
-                cooking_counter_equipments=cooking_counter_equipments,
-                pos=pos,
-                occupied_by=CookingEquipment(
-                    name="Basket",
-                    item_info=self.item_info["Basket"],
-                    transitions={
-                        item: {
-                            "seconds": info.seconds,
-                            "needs": info.needs,
-                            "info": info,
-                        }
-                        for item, info in self.item_info.items()
-                        if info.equipment is not None
-                        and info.equipment.name == "Basket"
-                    },
-                ),
-            ),  # Stove with pan: Q because it looks like a pan
-            "B": lambda pos: Dispenser(pos, self.item_info["Bun"]),
-            "M": lambda pos: Dispenser(pos, self.item_info["Meat"]),
-            "S": lambda pos: Sink(
-                pos,
-                transitions={
-                    info.needs[0]: {"seconds": info.seconds, "result": item}
-                    for item, info in self.item_info.items()
-                    if info.equipment is not None and info.equipment.name == "Sink"
-                },
-            ),
-            "+": SinkAddon,
-        }
-        """Map of the characters in the layout file to callables returning the object/counter. In the future, 
-        maybe replaced with a factory and the characters defined elsewhere in an config."""
-
-        self.kitchen_height: int = 0
-        """The height of the kitchen, is set by the `Environment.parse_layout_file` method"""
-        self.kitchen_width: int = 0
-        """The width of the kitchen, is set by the `Environment.parse_layout_file` method"""
-
-        (
-            self.counters,
-            self.designated_player_positions,
-            self.free_positions,
-        ) = self.parse_layout_file()
-
-        self.init_counters()
-
-        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.order_and_score.create_init_orders(self.env_time)
-        self.start_time = self.env_time
-        """The relative env time when it started."""
-        self.env_time_end = self.env_time + timedelta(
-            seconds=self.environment_config["game"]["time_limit_seconds"]
-        )
-        """The relative env time when it will stop/end"""
-        log.debug(f"End time: {self.env_time_end}")
-
-    def get_env_time(self):
-        """the internal time of the environment. An environment starts always with the time from `create_init_env_time`.
-
-        Utility method to pass a reference to the serving window."""
-        return self.env_time
-
-    @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 load_item_info(self, data) -> dict[str, ItemInfo]:
-        """Load `item_info.yml` if only the path is given, 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)
-        for item_name in item_lookup:
-            item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name])
-
-        for item_name, item_info in item_lookup.items():
-            if item_info.equipment:
-                item_info.equipment = item_lookup[item_info.equipment]
-        return item_lookup
-
-    def validate_item_info(self):
-        """TODO"""
-        raise NotImplementedError
-        # infos = {t: [] for t in ItemType}
-        # graph = nx.DiGraph()
-        # for info in self.item_info.values():
-        #     infos[info.type].append(info)
-        #     graph.add_node(info.name)
-        #     match info.type:
-        #         case ItemType.Ingredient:
-        #             if info.is_cuttable:
-        #                 graph.add_edge(
-        #                     info.name, info.finished_progress_name[:-1] + info.name
-        #                 )
-        #         case ItemType.Equipment:
-        #             ...
-        #         case ItemType.Meal:
-        #             if info.equipment is not None:
-        #                 graph.add_edge(info.equipment.name, info.name)
-        #             for ingredient in info.needs:
-        #                 graph.add_edge(ingredient, info.name)
-
-        # graph = nx.DiGraph()
-        # for item_name, item_info in self.item_info.items():
-        #     graph.add_node(item_name, type=item_info.type.name)
-        #     if len(item_info.equipment) == 0:
-        #         for item in item_info.needs:
-        #             graph.add_edge(item, item_name)
-        #     else:
-        #         for item in item_info.needs:
-        #             for equipment in item_info.equipment:
-        #                 graph.add_edge(item, equipment)
-        #                 graph.add_edge(equipment, item_name)
-
-        # plt.figure(figsize=(10, 10))
-        # pos = nx.nx_agraph.graphviz_layout(graph, prog="twopi", args="")
-        # nx.draw(graph, pos=pos, with_labels=True, node_color="white", node_size=500)
-        # print(nx.multipartite_layout(graph, subset_key="type", align="vertical"))
-
-        # pos = {
-        #     node: (
-        #         len(nx.ancestors(graph, node)) - len(nx.descendants(graph, node)),
-        #         y,
-        #     )
-        #     for y, node in enumerate(graph)
-        # }
-        # nx.draw(
-        #     graph,
-        #     pos=pos,
-        #     with_labels=True,
-        #     node_shape="s",
-        #     node_size=500,
-        #     node_color="white",
-        # )
-        # TODO add colors for ingredients, equipment and meals
-        # plt.show()
-
-    def parse_layout_file(self):
-        """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
-        in layout).
-
-        Args:
-            layout_file: Path to the layout file.
-        """
-        current_y: float = 0.5
-        counters: list[Counter] = []
-        designated_player_positions: list[npt.NDArray] = []
-        free_positions: list[npt.NDArray] = []
-
-        self.kitchen_width = 0
-
-        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.kitchen_height = len(lines)
-
-        for line in lines:
-            line = line.replace("\n", "").replace(" ", "")  # remove newline char
-            current_x = 0.5
-            for character in line:
-                character = character.capitalize()
-                pos = np.array([current_x, current_y])
-                counter_class = self.SYMBOL_TO_CHARACTER_MAP[character]
-                if not isinstance(counter_class, str):
-                    counter = counter_class(pos)
-                    counters.append(counter)
-                else:
-                    if counter_class == "Agent":
-                        designated_player_positions.append(
-                            np.array([current_x, current_y])
-                        )
-                    elif counter_class == "Free":
-                        free_positions.append(np.array([current_x, current_y]))
-                current_x += 1
-                if current_x > self.kitchen_width:
-                    self.kitchen_width = current_x
-            current_y += 1
-
-        self.kitchen_width -= 0.5
-
-        return counters, designated_player_positions, free_positions
-
-    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.
-        Possible action types are movement, pickup and interact actions.
-
-        Args:
-            action: The action to be performed
-        """
-        assert action.player in self.players.keys(), "Unknown player."
-        player = self.players[action.player]
-
-        if action.action_type == ActionType.MOVEMENT:
-            player.set_movement(
-                action.action_data,
-                self.env_time + timedelta(seconds=action.duration),
-            )
-        else:
-            counter = self.get_facing_counter(player)
-            if player.can_reach(counter):
-                if action.action_type == ActionType.PUT:
-                    player.pick_action(counter)
-
-                elif action.action_type == ActionType.INTERACT:
-                    if action.action_data == InterActionData.START:
-                        player.perform_interact_hold_start(counter)
-                        player.last_interacted_counter = counter
-            if action.action_data == InterActionData.STOP:
-                if player.last_interacted_counter:
-                    player.perform_interact_hold_stop(player.last_interacted_counter)
-
-    def get_closest_counter(self, point: np.ndarray):
-        """Determines the closest counter for a given 2d-coordinate point in the env.
-
-        Args:
-            point: The point in the env for which to find the closest counter
-
-        Returns: The closest counter for the given point.
-        """
-        counter_distances = distance_matrix(
-            [point], [counter.pos for counter in self.counters]
-        )[0]
-
-        closest_counter_idx = np.argmin(counter_distances)
-        return self.counters[closest_counter_idx]
-
-    def get_facing_counter(self, player: Player):
-        """Determines the counter which the player is looking at.
-        Adds a multiple of the player facing direction onto the player position and finds the closest
-        counter for that point.
-
-        Args:
-            player: The player for which to find the facing counter.
-
-        Returns:
-
-        """
-        facing_counter = self.get_closest_counter(player.facing_point)
-        return facing_counter
-
-    def perform_movement(self, player: Player, 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.
-        This happens, when the player moves diagonally against the counters or world boundary.
-        This just checks if the single axis party of the movement could move the player and does so at a lower rate.)
-
-        The movement action is a unit 2d vector.
-
-        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)
-        )
-
-    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.
-
-        """
-        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 any(map(collide, other_players))
-
-    def detect_collision_counters(self, player: Player):
-        """Checks for collisions of the queried player with each counter.
-
-        Args:
-            player:  The player to check collisions with counters for.
-
-        Returns: True if the player collides with any counter, False if not.
-
-        """
-        return any(
-            map(
-                lambda counter: self.detect_collision_player_counter(player, counter),
-                self.counters,
-            )
-        )
-
-    @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.
-
-        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.
-
-        """
-        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
-
-    def add_player(self, player_name: str, pos: npt.NDArray = None):
-        """Add a player to the environment.
-
-        Args:
-            player_name: The id/name of the player to reference actions and in the state.
-            pos: The optional init position of the player.
-        """
-        # TODO check if the player name already exists in the environment and do not overwrite player.
-        log.debug(f"Add player {player_name} to the game")
-        player = Player(
-            player_name,
-            player_config=PlayerConfig(
-                **(
-                    self.environment_config["player_config"]
-                    if "player_config" in self.environment_config
-                    else {}
-                )
-            ),
-            pos=pos,
-        )
-        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)
-                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)
-                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()
-
-    def detect_collision_world_bounds(self, player: Player):
-        """Checks for detections of the player and the world bounds.
-
-        Args:
-            player: The player which to not let escape the world.
-
-        Returns: True if the player touches the world bounds, False if not.
-        """
-        collisions_lower = any((player.pos - (player.radius)) < 0)
-        collisions_upper = any(
-            (player.pos + (player.radius)) > [self.kitchen_width, self.kitchen_height]
-        )
-        return collisions_lower or collisions_upper
-
-    def step(self, passed_time: timedelta):
-        """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders
-        and time limits.
-        """
-        self.env_time += passed_time
-
-        if not self.game_ended:
-            for player in self.players.values():
-                if self.env_time <= player.movement_until:
-                    self.perform_movement(player, passed_time)
-
-            for counter in self.counters:
-                if isinstance(
-                    counter, (CuttingBoard, CookingCounter, Sink, PlateDispenser)
-                ):
-                    counter.progress(passed_time=passed_time, now=self.env_time)
-            self.order_and_score.progress(passed_time=passed_time, now=self.env_time)
-
-    def get_state(self):
-        """Get the current state of the game environment. The state here is accessible by the current python objects.
-
-        Returns: Dict of lists of the current relevant game objects.
-
-        """
-        return {
-            "players": self.players,
-            "counters": self.counters,
-            "score": self.order_and_score.score,
-            "orders": self.order_and_score.open_orders,
-            "ended": self.game_ended,
-            "env_time": self.env_time,
-            "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],
-            "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 init_counters(self):
-        """Initialize the counters in the environment.
-
-        Connect the `ServingWindow`(s) with the `PlateDispenser`.
-        Find and connect the `SinkAddon`s with the `Sink`s
-        """
-        plate_dispenser = self.get_counter_of_type(PlateDispenser)
-        assert len(plate_dispenser) > 0, "No Plate Return in the environment"
-
-        sink_addons = self.get_counter_of_type(SinkAddon)
-
-        for counter in self.counters:
-            match counter:
-                case ServingWindow():
-                    counter: ServingWindow  # Pycharm type checker does now work for match statements?
-                    counter.add_plate_dispenser(plate_dispenser[0])
-                case Sink(pos=pos):
-                    counter: Sink  # Pycharm type checker does now work for match statements?
-                    assert len(sink_addons) > 0, "No SinkAddon but normal Sink"
-                    closest_addon = self.get_closest(pos, sink_addons)
-                    assert 1 - (1 * 0.05) <= np.linalg.norm(
-                        closest_addon.pos - pos
-                    ), f"No SinkAddon connected to Sink at pos {pos}"
-                    counter.set_addon(closest_addon)
-
-    @staticmethod
-    def get_closest(pos: npt.NDArray[float], counters: list[Counter]):
-        """Find the closest counter for a position
-
-        Args:
-            pos: the position to find the closest one from. Needs to be the same shape as the `Counter.pos` array.
-            counters: target to find the closest one.
-        """
-        return min(counters, key=lambda c: np.linalg.norm(c.pos - pos))
-
-    def get_counter_of_type(self, counter_type) -> list[Counter]:
-        """Filter all counters in the environment for a counter type."""
-        return list(
-            filter(lambda counter: isinstance(counter, counter_type), self.counters)
-        )
-
-    def reset_env_time(self):
-        """Reset the env time to the initial time, defined by `create_init_env_time`."""
-        self.env_time = create_init_env_time()
-        log.debug(f"Reset env time to {self.env_time}")
diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py
deleted file mode 100644
index ecfb5958a982c084ce46db849fa9568483fea3e5..0000000000000000000000000000000000000000
--- a/overcooked_simulator/utils.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import logging
-import os
-import sys
-from datetime import datetime
-from enum import Enum
-
-from overcooked_simulator import ROOT_DIR
-
-
-def create_init_env_time():
-    """Init time of the environment time, because all environments should have the same internal time."""
-    return datetime(
-        year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
-    )
-
-
-def custom_asdict_factory(data):
-    def convert_value(obj):
-        if isinstance(obj, Enum):
-            return obj.value
-        return obj
-
-    return dict((k, convert_value(v)) for k, v in data)
-
-
-def setup_logging(enable_websocket_logging=False):
-    path_logs = ROOT_DIR.parent / "logs"
-    os.makedirs(path_logs, exist_ok=True)
-    logging.basicConfig(
-        level=logging.DEBUG,
-        format="%(asctime)s %(levelname)-8s %(name)-50s %(message)s",
-        handlers=[
-            logging.FileHandler(
-                path_logs / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_debug.log",
-                encoding="utf-8",
-            ),
-            logging.StreamHandler(sys.stdout),
-        ],
-    )
-    logging.getLogger("matplotlib").setLevel(logging.WARNING)
-    if not enable_websocket_logging:
-        logging.getLogger("asyncio").setLevel(logging.ERROR)
-        logging.getLogger("asyncio.coroutines").setLevel(logging.ERROR)
-        logging.getLogger("websockets.server").setLevel(logging.ERROR)
-        logging.getLogger("websockets.protocol").setLevel(logging.ERROR)
-        logging.getLogger("websockets.client").setLevel(logging.ERROR)
-
-
-def url_and_port_arguments(parser):
-    parser.add_argument(
-        "-url",
-        "--url",
-        "--host",
-        type=str,
-        default="localhost",
-        help="Overcooked game server host url.",
-    )
-    parser.add_argument(
-        "-p",
-        "--port",
-        type=int,
-        default=8000,
-        help="Port number for the game engine server",
-    )
-
-
-def disable_websocket_logging_arguments(parser):
-    parser.add_argument("--enable-websocket-logging", action="store_true", default=True)
diff --git a/setup.py b/setup.py
index d57d47c30a8eefd7e427ff97be2c4884bd1a4cfb..658851b4e072e83add01ebbf22717eb837a7801a 100644
--- a/setup.py
+++ b/setup.py
@@ -11,16 +11,23 @@ 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",
+    "tqdm>=4.65.0",
+    "networkx",
+    "matplotlib>=3.8.0",
+    "pygraphviz>=1.9",
+    "pydot>=2.0.0",
 ]
 
 test_requirements = [
@@ -41,18 +48,26 @@ setup(
     ],
     description="The real-time overcooked simulation for a cognitive cooperative system",
     entry_points={
-        "console_scripts": ["overcooked-sim = overcooked_simulator.__main__:main"]
+        "console_scripts": ["cooperative_cuisine = cooperative_cuisine.__main__:main"]
     },
     install_requires=requirements,
     license="MIT license",
     long_description=readme + "\n\n" + history,
     include_package_data=True,
-    keywords=["overcooked_simulator"],
-    name="overcooked_simulator",
-    packages=find_packages(include=["overcooked_simulator", "overcooked_simulator.*"]),
+    keywords=["cooperative_cuisine"],
+    name="cooperative_cuisine",
+    packages=find_packages(include=["cooperative_cuisine", "cooperative_cuisine.*"]),
     test_suite="tests",
     tests_require=test_requirements,
     url="https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator",
     version="0.1.0",
     zip_safe=False,
+    extras_require={
+        "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 1f7c27bbd83190ada56e869f60ce863b614cb1b7..eb4746dfb41dc33db2a548ef5b675adfc46e9d7d 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -3,22 +3,23 @@ from datetime import timedelta
 import numpy as np
 import pytest
 
-from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.counters import Counter, CuttingBoard
-from overcooked_simulator.game_items import Item
-from overcooked_simulator.overcooked_environment import (
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.counters import Counter, CuttingBoard
+from cooperative_cuisine.environment import (
     Action,
     Environment,
     ActionType,
     InterActionData,
 )
-from overcooked_simulator.utils import create_init_env_time
+from cooperative_cuisine.game_items import Item, ItemInfo, ItemType
+from cooperative_cuisine.hooks import Hooks
+from cooperative_cuisine.utils import create_init_env_time
 
-layouts_folder = ROOT_DIR / "game_content" / "layouts"
-environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
-layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
-layout_empty_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
-item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
+layouts_folder = ROOT_DIR / "configs" / "layouts"
+environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml"
+layout_path = ROOT_DIR / "configs" / "layouts" / "basic.layout"
+layout_empty_path = ROOT_DIR / "configs" / "layouts" / "basic.layout"
+item_info_path = ROOT_DIR / "configs" / "item_info.yaml"
 
 # TODO: TESTs are in absolute pixel coordinates still.
 
@@ -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([0, 0]))
-    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,15 +193,24 @@ 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,
-        transitions={"Tomato": {"seconds": 1, "result": "ChoppedTomato"}},
+        pos=counter_pos,
+        hook=Hooks(env),
+        transitions={
+            "ChoppedTomato": ItemInfo(
+                name="ChoppedTomato",
+                seconds=0.5,
+                equipment=ItemInfo(name="CuttingBoard", type=ItemType.Equipment),
+                type=ItemType.Ingredient,
+                needs=["Tomato"],
+            )
+        },
     )
     env.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)
@@ -262,10 +238,11 @@ def test_processing(env_config, layout_config, item_info):
 def test_time_passed():
     np.random.seed(42)
     env = Environment(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
+        ROOT_DIR / "configs" / "environment_config.yaml",
         layouts_folder / "empty.layout",
-        ROOT_DIR / "game_content" / "item_info.yaml",
+        ROOT_DIR / "configs" / "item_info.yaml",
     )
+    env.add_player("0")
     env.reset_env_time()
     passed_time = timedelta(seconds=10)
     env.step(passed_time)
@@ -283,10 +260,12 @@ def test_time_passed():
 def test_time_limit():
     np.random.seed(42)
     env = Environment(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
+        ROOT_DIR / "configs" / "environment_config.yaml",
         layouts_folder / "empty.layout",
-        ROOT_DIR / "game_content" / "item_info.yaml",
+        ROOT_DIR / "configs" / "item_info.yaml",
     )
+    env.add_player("0")
+
     env.reset_env_time()
 
     assert not env.game_ended, "Game has not ended yet"