diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbce8a9795d9d6d58e37a19a876a1b5319985a15..c62dca6a65b6a2b0fa8c9cfcfb66f64bd68aa6b5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,13 @@
 
 - Send full websocket url in player_info.
 - ">90"% code coverage in tests
+- i18n for the gui
+- Controller hotplugging
+- Hook when returning items to dispensers
+- Displaying image of served meals on game conclusion screen
+- Pathfinding in random agent
+- Level layouts from 2d-grid-overcooked-literature
+- Caching of graph recipe layouts
 
 ### Changed
 
@@ -28,6 +35,9 @@
 - Player config in the environment class is now a dataclass and not a dict. The content remains the same. Just the
   access changes from dict access to normal object like access.
 - Some type hint additions
+- Better drawing of orders, now in a pygame_gui UIImage
+- Buttons for setting player controls in the GUI disappear depending on number of players
+- Icon for serving window, now a star
 
 ### Deprecated
 
@@ -35,6 +45,9 @@
 
 ### Fixed
 
+- Orders are sampled correctly again
+- Orders with no time limit, sample_on_serving=true works again
+
 ### Security
 
 ## [1.0.0] (2024-03-08)
diff --git a/README.md b/README.md
index 41178079ab45f382edc3ba1c8cb9b84af288801e..a4d18ba3c632c9023e719086b710e7636be44ee3 100644
--- a/README.md
+++ b/README.md
@@ -20,13 +20,14 @@ the [Documentation](https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simu
 
 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 newer environment. Either conda or PyEnv.
+You need a Python 3.10 or newer environment conda environment.
 
 ### Local Editable Installation
 
 In your `repo`, `PyCharmProjects` or similar directory with the correct environment active:
 
 ```bash
+conda install -c conda-forge pygraphviz
 git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
 cd overcooked-simulator
 pip install -e .
diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py
index 4ec76825b50da6103fa25529f88754827d16013a..17dfc078daff4bf21f66270c4c72fa6d2c460cbf 100644
--- a/cooperative_cuisine/__init__.py
+++ b/cooperative_cuisine/__init__.py
@@ -21,12 +21,14 @@ like a "real", cooperative, human partner.
 
 # Installation
 
-You need a Python **3.10** or newer environment.
+You need a Python **3.10** or newer codna environment.
 ```bash
+conda install -c conda-forge pygraphviz
 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
+conda install -c conda-forge pygraphviz
 git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
 cd overcooked-simulator
 pip install -e .
diff --git a/cooperative_cuisine/configs/agents/random_agent.py b/cooperative_cuisine/configs/agents/random_agent.py
index 3b1b5832b32f74367582d93846681e7d9562b1b0..6d8830d94c5570bc6642936a0f34b47b94a8c8bc 100644
--- a/cooperative_cuisine/configs/agents/random_agent.py
+++ b/cooperative_cuisine/configs/agents/random_agent.py
@@ -7,25 +7,53 @@ import time
 from collections import defaultdict
 from datetime import datetime, timedelta
 
+import networkx
 import numpy as np
+import numpy.typing as npt
 from websockets import connect
 
 from cooperative_cuisine.action import ActionType, InterActionData, Action
+from cooperative_cuisine.state_representation import (
+    create_movement_graph,
+    astar_heuristic,
+    restrict_movement_graph,
+)
 from cooperative_cuisine.utils import custom_asdict_factory
 
 TIME_TO_STOP_ACTION = 3.0
 
+ADD_RANDOM_MOVEMENTS = False
+DIAGONAL_MOVEMENTS = True
+AVOID_OTHER_PLAYERS = True
+
+
+def get_free_neighbours(
+    state: dict, counter_pos: list[float] | tuple[float, float] | npt.NDArray
+) -> list[tuple[float, float]]:
+    width, height = state["kitchen"]["width"], state["kitchen"]["height"]
+    free_space = np.ones((width, height), dtype=bool)
+    for counter in state["counters"]:
+        grid_idx = np.array(counter["pos"]).astype(int)
+        free_space[grid_idx[0], grid_idx[1]] = False
+    i, j = np.array(counter_pos).astype(int)
+    free = []
+
+    for x, y in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
+        if 0 < x < width and 0 < y < height and free_space[x, y]:
+            free.append((x, y))
+    return free
+
 
 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)
+    parser.add_argument("--step_time", type=float, default=0.1)
 
     args = parser.parse_args()
 
-    async with connect(args.uri) as websocket:
+    async with (connect(args.uri) as websocket):
         await websocket.send(
             json.dumps({"type": "ready", "player_hash": args.player_hash})
         )
@@ -34,6 +62,9 @@ async def agent():
         ended = False
 
         counters = None
+        all_counters = None
+
+        movement_graph = None
 
         player_info = {}
         current_agent_pos = None
@@ -60,10 +91,16 @@ async def agent():
             if not state["all_players_ready"]:
                 continue
 
+            if movement_graph is None:
+                movement_graph = create_movement_graph(
+                    state, diagonal=DIAGONAL_MOVEMENTS
+                )
+
             if counters is None:
                 counters = defaultdict(list)
                 for counter in state["counters"]:
                     counters[counter["type"]].append(counter)
+                all_counters = state["counters"]
 
             for player in state["players"]:
                 if player["id"] == args.player_id:
@@ -125,10 +162,77 @@ async def agent():
                     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:
+                        target_diff = np.array(task_args) - np.array(current_agent_pos)
+                        target_dist = np.linalg.norm(target_diff)
+
+                        source = tuple(
+                            np.round(np.array(current_agent_pos)).astype(int)
+                        )
+                        target = tuple(np.array(task_args).astype(int))
+                        target_free_spaces = get_free_neighbours(state, target)
+                        paths = []
+                        for free in target_free_spaces:
+                            try:
+                                path = networkx.astar_path(
+                                    restrict_movement_graph(
+                                        graph=movement_graph,
+                                        player_positions=[
+                                            p["pos"]
+                                            for p in state["players"]
+                                            if p["id"] != args.player_id
+                                        ],
+                                    )
+                                    if AVOID_OTHER_PLAYERS
+                                    else movement_graph,
+                                    source,
+                                    free,
+                                    heuristic=astar_heuristic,
+                                )
+                                paths.append(path)
+                            except networkx.exception.NetworkXNoPath:
+                                pass
+                            except networkx.exception.NodeNotFound:
+                                pass
+
+                        if paths:
+                            shortest_path = paths[np.argmin([len(p) for p in paths])]
+                            if len(shortest_path) > 1:
+                                node_diff = shortest_path[1] - np.array(
+                                    current_agent_pos
+                                )
+                                node_dist = np.linalg.norm(node_diff)
+                                movement = node_diff / node_dist
+                            else:
+                                movement = target_diff / target_dist
+                            do_movement = True
+                        else:
+                            # no paths available
+                            print("NO PATHS")
+
+                            # task_type = None
+                            # task_args = None
+                            do_movement = False
+
+                        if target_dist > 1.2 and do_movement:
+                            if target_dist != 0:
+                                if ADD_RANDOM_MOVEMENTS:
+                                    random_small_rotation_angle = (
+                                        np.random.random() * np.pi * 0.1
+                                    )
+                                    rotation_matrix = np.array(
+                                        [
+                                            [
+                                                np.cos(random_small_rotation_angle),
+                                                -np.sin(random_small_rotation_angle),
+                                            ],
+                                            [
+                                                np.sin(random_small_rotation_angle),
+                                                np.cos(random_small_rotation_angle),
+                                            ],
+                                        ]
+                                    )
+                                    movement = rotation_matrix @ movement
+
                                 await websocket.send(
                                     json.dumps(
                                         {
@@ -137,7 +241,7 @@ async def agent():
                                                 Action(
                                                     args.player_id,
                                                     ActionType.MOVEMENT,
-                                                    (diff / dist).tolist(),
+                                                    movement.tolist(),
                                                     args.step_time + 0.01,
                                                 ),
                                                 dict_factory=custom_asdict_factory,
@@ -148,6 +252,8 @@ async def agent():
                                 )
                                 await websocket.recv()
                         else:
+                            # Target reached here.
+                            print("TARGET REACHED")
                             task_type = None
                             task_args = None
                     case "INTERACT":
@@ -207,9 +313,19 @@ async def agent():
                 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)
+                    # counter_type = random.choice(list(counters.keys()))
+                    # task_args = random.choice(counters[counter_type])["pos"]
+
+                    random_counter = random.choice(all_counters)
+                    counter_type = random_counter["type"]
+                    task_args = random_counter["pos"]
+                    print(
+                        args.player_hash,
+                        args.player_id,
+                        task_type,
+                        counter_type,
+                        task_args,
+                    )
                 else:
                     print(args.player_hash, args.player_id, task_type)
                     task_args = None
diff --git a/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml
index b005d07a014b62adac291f8716d42a709f789fac..d6a51ea95225f77ae00cb56e49a2d4eda40c95e2 100644
--- a/cooperative_cuisine/configs/environment_config.yaml
+++ b/cooperative_cuisine/configs/environment_config.yaml
@@ -65,8 +65,8 @@ orders:
       # 'random' library call with getattr, kwargs are passed to the function
       func: uniform
       kwargs:
-        a: 40
-        b: 60
+        a: 55
+        b: 65
     max_orders: 6
     # maximum number of active orders at the same time
     num_start_meals: 2
@@ -75,17 +75,18 @@ orders:
       # 'random' library call with getattr, kwargs are passed to the function
       func: uniform
       kwargs:
-        a: 10
-        b: 20
+        a: 35
+        b: 45
     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
   speed_units_per_seconds: 6
-  interaction_range: 1.6
+  interaction_range: 1.25
   restricted_view: False
   view_angle: 70
   view_range: 4  # in grid units, can be "null"
diff --git a/cooperative_cuisine/configs/environment_config_no_validation.yaml b/cooperative_cuisine/configs/environment_config_no_validation.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..1802c28a539cd92d7879ea14643f545bcebd3c99
--- /dev/null
+++ b/cooperative_cuisine/configs/environment_config_no_validation.yaml
@@ -0,0 +1,208 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 1
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 300
+  undo_dispenser_pickup: true
+  validate_recipes: false
+
+
+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:
+  meals:
+    all: true
+    # if all: false -> only orders for these meals are generated
+    # TODO: what if this list is empty?
+    list:
+      #      - TomatoSoup
+      #      - OnionSoup
+      #      - Salad
+      - FriedFish
+  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
+  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.effects.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+hook_callbacks:
+  # # ---------------  Scoring  ---------------
+  orders:
+    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:
+    hooks: [ serve_not_ordered_meal ]
+    callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+    callback_class_kwargs:
+      static_score: 2
+  trashcan_usages:
+    hooks: [ trashcan_usage ]
+    callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+    callback_class_kwargs:
+      static_score: -5
+  expired_orders:
+    hooks: [ order_expired ]
+    callback_class: !!python/name:cooperative_cuisine.scores.ScoreViaHooks ''
+    callback_class_kwargs:
+      static_score: -10
+    # --------------- Recording ---------------
+  #  json_states:
+  #    hooks: [ json_state ]
+  #    callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+  #    callback_class_kwargs:
+  #      record_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    hooks: [ pre_perform_action ]
+    callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+    callback_class_kwargs:
+      record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+    callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+    callback_class_kwargs:
+      record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+      add_hook_ref: true
+  env_configs:
+    hooks: [ env_initialized, item_info_config ]
+    callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+    callback_class_kwargs:
+      record_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+      add_hook_ref: true
+
+  # Game event recording
+  game_events:
+    hooks:
+      - post_counter_pick_up
+      - post_counter_drop_off
+      - post_dispenser_pick_up
+      - cutting_board_100
+      - player_start_interaction
+      - player_end_interact
+      - post_serving
+      - no_serving
+      - dirty_plate_arrives
+      - trashcan_usage
+      - plate_cleaned
+      - added_plate_to_sink
+      - drop_on_sink_addon
+      - pick_up_from_sink_addon
+      - serve_not_ordered_meal
+      - serve_without_plate
+      - completed_order
+      - new_orders
+      - order_expired
+      - action_on_not_reachable_counter
+      - new_fire
+      - fire_spreading
+      - drop_off_on_cooking_equipment
+      - players_collide
+      - post_plate_dispenser_pick_up
+      - post_plate_dispenser_drop_off
+      - on_item_transition
+      - progress_started
+      - progress_finished
+      - content_ready
+      - dispenser_item_returned
+
+    callback_class: !!python/name:cooperative_cuisine.recording.FileRecorder ''
+    callback_class_kwargs:
+      record_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_overcooked-ai.yaml b/cooperative_cuisine/configs/item_info_overcooked-ai.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ade54dfbf7821178cb447cc274b4285b4d3083e8
--- /dev/null
+++ b/cooperative_cuisine/configs/item_info_overcooked-ai.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: [ Onion, Onion, Onion ]
+  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/cooperative_cuisine/configs/layouts/gym-cooking/1-open-divider.layout b/cooperative_cuisine/configs/layouts/gym-cooking/1-open-divider.layout
new file mode 100644
index 0000000000000000000000000000000000000000..b21b3150931f3357b7441619f0362eab71ee1f2e
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/gym-cooking/1-open-divider.layout
@@ -0,0 +1,7 @@
+#####T#
+C_____L
+C_____#
+$A___A#
+#_____#
+#_____P
+#####P#
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/gym-cooking/2-partial-divider.layout b/cooperative_cuisine/configs/layouts/gym-cooking/2-partial-divider.layout
new file mode 100644
index 0000000000000000000000000000000000000000..5d9d0d66be8e354eb79a84a491470bdae2be0da7
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/gym-cooking/2-partial-divider.layout
@@ -0,0 +1,7 @@
+#####T#
+C__#__L
+C__#__#
+$A_#_A#
+#__#__#
+#_____P
+#####P#
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/gym-cooking/3-full-divider.layout b/cooperative_cuisine/configs/layouts/gym-cooking/3-full-divider.layout
new file mode 100644
index 0000000000000000000000000000000000000000..9bd6b29a7bfc7dac73137b43a4f6eae6b2ae5054
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/gym-cooking/3-full-divider.layout
@@ -0,0 +1,7 @@
+#####T#
+C__#__L
+C__#__#
+$A_#_A#
+#__#__#
+#__#__P
+#####P#
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/large.layout b/cooperative_cuisine/configs/layouts/large.layout
similarity index 100%
rename from cooperative_cuisine/configs/layouts_archive/test_layouts/large.layout
rename to cooperative_cuisine/configs/layouts/large.layout
diff --git a/cooperative_cuisine/configs/layouts_archive/test_layouts/large_t.layout b/cooperative_cuisine/configs/layouts/large_t.layout
similarity index 100%
rename from cooperative_cuisine/configs/layouts_archive/test_layouts/large_t.layout
rename to cooperative_cuisine/configs/layouts/large_t.layout
diff --git a/cooperative_cuisine/configs/layouts/overcooked-ai/1-cramped-room.layout b/cooperative_cuisine/configs/layouts/overcooked-ai/1-cramped-room.layout
new file mode 100644
index 0000000000000000000000000000000000000000..49655d1ce3c4217a1fd2385cd5b33918fd395137
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-ai/1-cramped-room.layout
@@ -0,0 +1,4 @@
+##U##
+NA_AN
+#___#
+#P#$#
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/overcooked-ai/2-asymmetric-advantages.layout b/cooperative_cuisine/configs/layouts/overcooked-ai/2-asymmetric-advantages.layout
new file mode 100644
index 0000000000000000000000000000000000000000..3b87f3e58caaa5a5c59f732889986704ce2dce8f
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-ai/2-asymmetric-advantages.layout
@@ -0,0 +1,5 @@
+#########
+N_#$#N#_$
+#_A_U_A_#
+#___U___#
+###P#P###
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/layouts/overcooked-ai/3-coordination-ring.layout b/cooperative_cuisine/configs/layouts/overcooked-ai/3-coordination-ring.layout
new file mode 100644
index 0000000000000000000000000000000000000000..f83c2adf9045825e86de62ae3626e3325cbb0a7f
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-ai/3-coordination-ring.layout
@@ -0,0 +1,5 @@
+###U#
+#__AU
+P_#_#
+NA__#
+#N$##
diff --git a/cooperative_cuisine/configs/layouts/overcooked-ai/4-forced-coordination.layout b/cooperative_cuisine/configs/layouts/overcooked-ai/4-forced-coordination.layout
new file mode 100644
index 0000000000000000000000000000000000000000..bc835ae37db299eb0cf7c61f7c5de85478becb99
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-ai/4-forced-coordination.layout
@@ -0,0 +1,5 @@
+###U#
+N_#AU
+N_#_#
+PA#_#
+###$#
diff --git a/cooperative_cuisine/configs/layouts/overcooked-ai/5-counter-circuit.layout b/cooperative_cuisine/configs/layouts/overcooked-ai/5-counter-circuit.layout
new file mode 100644
index 0000000000000000000000000000000000000000..6c5839500ace8c8314b87b17dde31c257c994243
--- /dev/null
+++ b/cooperative_cuisine/configs/layouts/overcooked-ai/5-counter-circuit.layout
@@ -0,0 +1,5 @@
+###UU###
+#A_____#
+P_####_$
+#_____A#
+###NN###
\ No newline at end of file
diff --git a/cooperative_cuisine/configs/study/gym-cooking_study.yaml b/cooperative_cuisine/configs/study/gym-cooking_study.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..e33204735f77732a7f28d6bc1fb891c6ce74a5d0
--- /dev/null
+++ b/cooperative_cuisine/configs/study/gym-cooking_study.yaml
@@ -0,0 +1,43 @@
+levels:
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/gym-cooking/1-open-divider.layout
+    item_info_path: CONFIGS_DIR/item_info.yaml
+    name: "Level 1: Open Divider"
+    config_overwrite:
+      game:
+        time_limit_seconds: 10
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/gym-cooking/2-partial-divider.layout
+    item_info_path: CONFIGS_DIR/item_info.yaml
+    name: "Level 2: Partial Divider"
+    config_overwrite:
+      game:
+        time_limit_seconds: 10
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/gym-cooking/3-full-divider.layout
+    item_info_path: CONFIGS_DIR/item_info.yaml
+    name: "Level 3: Full Divider"
+    config_overwrite:
+      game:
+        time_limit_seconds: 10
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+
+
+
+num_players: 1
+num_bots: 0
diff --git a/cooperative_cuisine/configs/study/overcooked-ai_study.yaml b/cooperative_cuisine/configs/study/overcooked-ai_study.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b5cd79f6a2950f35bf88386dc3c5dc9a138517bc
--- /dev/null
+++ b/cooperative_cuisine/configs/study/overcooked-ai_study.yaml
@@ -0,0 +1,65 @@
+levels:
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-ai/1-cramped-room.layout
+    item_info_path: CONFIGS_DIR/item_info_overcooked-ai.yaml
+    name: "Level 1: Cramped Room"
+    config_overwrite:
+      game:
+        time_limit_seconds: 120
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-ai/2-asymmetric-advantages.layout
+    item_info_path: CONFIGS_DIR/item_info_overcooked-ai.yaml
+    name: "Level 2: Asymmetric Advantages"
+    config_overwrite:
+      game:
+        time_limit_seconds: 120
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-ai/3-coordination-ring.layout
+    item_info_path: CONFIGS_DIR/item_info_overcooked-ai.yaml
+    name: "Level 3: Coordination Ring"
+    config_overwrite:
+      game:
+        time_limit_seconds: 120
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-ai/4-forced-coordination.layout
+    item_info_path: CONFIGS_DIR/item_info_overcooked-ai.yaml
+    name: "Level 4: Forced Coordination"
+    config_overwrite:
+      game:
+        time_limit_seconds: 120
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-ai/5-counter-circuit.layout
+    item_info_path: CONFIGS_DIR/item_info_overcooked-ai.yaml
+    name: "Level 5: Counter Circuit"
+    config_overwrite:
+      game:
+        time_limit_seconds: 120
+      plates:
+        clean_plates: 3
+        dirty_plates: 0
+        return_dirty: false
+
+
+
+num_players: 1
+num_bots: 0
diff --git a/cooperative_cuisine/configs/study/study_config.yaml b/cooperative_cuisine/configs/study/study_config.yaml
index 8318e39610e0f984fe1070a9cea039e56ef09d05..0c2ef412c98b6497e7b70d3c355157557b928ddf 100644
--- a/cooperative_cuisine/configs/study/study_config.yaml
+++ b/cooperative_cuisine/configs/study/study_config.yaml
@@ -1,35 +1,52 @@
 levels:
+
   - config_path: CONFIGS_DIR/environment_config.yaml
     layout_path: LAYOUTS_DIR/overcooked-1/1-1-far-apart.layout
     item_info_path: CONFIGS_DIR/item_info.yaml
-    name: "Level 1-1: Far Apart"
+    name: "Level 1"
     config_overwrite:
       game:
         time_limit_seconds: 300
       plates:
         clean_plates: 0
         dirty_plates: 0
+    orders:
+      order_gen_kwargs:
+        order_duration_random_func:
+          kwargs:
+            a: 60
+            b: 70
 
   - config_path: CONFIGS_DIR/environment_config.yaml
-    layout_path: LAYOUTS_DIR/basic.layout
+    layout_path: LAYOUTS_DIR/overcooked-1/1-4-bottleneck.layout
     item_info_path: CONFIGS_DIR/item_info.yaml
-    name: "Basic"
+    name: "Level 2"
     config_overwrite:
       game:
         time_limit_seconds: 300
 
   - config_path: CONFIGS_DIR/environment_config.yaml
-    layout_path: LAYOUTS_DIR/overcooked-1/1-4-bottleneck.layout
+    layout_path: LAYOUTS_DIR/overcooked-1/1-5-circle.layout
     item_info_path: CONFIGS_DIR/item_info.yaml
-    name: "Level 1-4: Bottleneck"
+    name: "Level 3"
     config_overwrite:
-      player_config:
-        restricted_view: true
+      game:
+        time_limit_seconds: 300
       plates:
-        clean_plates: 0
+        clean_plates: 1
         dirty_plates: 0
+        return_dirty: false
+
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-1/4-1-moving-counters.layout
+    item_info_path: CONFIGS_DIR/item_info.yaml
+    name: "Level 4"
+    config_overwrite:
       game:
         time_limit_seconds: 300
+      plates:
+        clean_plates: 0
+        dirty_plates: 0
 
 
 
diff --git a/cooperative_cuisine/counter_factory.py b/cooperative_cuisine/counter_factory.py
index 5232a5bf70e7df880ea7bba28e1257c5275aa130..b1aae2f6f647daa6a47eda343ab5eb56a1d44d35 100644
--- a/cooperative_cuisine/counter_factory.py
+++ b/cooperative_cuisine/counter_factory.py
@@ -197,8 +197,6 @@ class CounterFactory:
 
         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:
@@ -548,7 +546,6 @@ def determine_counter_orientations(
                 [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:
diff --git a/cooperative_cuisine/game_server.py b/cooperative_cuisine/game_server.py
index c017a1d0a38dcebb25e51b63286eb18aadcd60b9..01a3b02cbecd8cf515d929bca83152bc81e71cc1 100644
--- a/cooperative_cuisine/game_server.py
+++ b/cooperative_cuisine/game_server.py
@@ -41,6 +41,7 @@ from cooperative_cuisine.utils import (
     add_list_of_manager_ids_arguments,
     disable_websocket_logging_arguments,
     setup_logging,
+    UUID_CUTOFF,
 )
 
 log = logging.getLogger(__name__)
@@ -162,8 +163,7 @@ class EnvironmentHandler:
         """
         if environment_config.manager_id not in self.allowed_manager:
             return 1
-        env_id = uuid.uuid4().hex
-
+        env_id = f"{environment_config.env_name}_env_{uuid.uuid4().hex[:UUID_CUTOFF]}"  # todo uuid cutoff
         if environment_config.number_players < 1:
             raise HTTPException(
                 status_code=409, detail="Number players need to be positive."
@@ -749,6 +749,7 @@ class CreateEnvironmentConfig(BaseModel):
     environment_config: str  # file content
     layout_config: str  # file content
     seed: int
+    env_name: str
 
 
 class ManageEnv(BaseModel):
diff --git a/cooperative_cuisine/movement.py b/cooperative_cuisine/movement.py
index b0c9d50a034d83f82f0736d2213bdb71e4d6c11d..2523e50f871758f6e9e3049a2b8ca60f0d59e042 100644
--- a/cooperative_cuisine/movement.py
+++ b/cooperative_cuisine/movement.py
@@ -193,32 +193,37 @@ class Movement:
             updated_movement * (self.player_movement_speed * d_time)
         )
 
-        # Check collisions with counters
+        # check if players collided with counters through movement or through being pushed
         (
             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)
+        # If collided, check if the players could still move along the axis, starting with x
+        # This leads to players beeing able to slide along counters, which feels alot nicer.
+        projected_x = updated_movement.copy()
+        projected_x[collided, 1] = 0
+        new_targeted_positions[collided] = player_positions[collided] + (
+            projected_x[collided] * (self.player_movement_speed * d_time)
+        )
+        # checking collisions again
+        (
+            collided,
+            relevant_axes,
+            nearest_counter_to_player,
+        ) = self.get_counter_collisions(new_targeted_positions)
+        new_targeted_positions[collided] = player_positions[collided]
+        # and now y axis collisions
+        projected_y = updated_movement.copy()
+        projected_y[collided, 0] = 0
+        new_targeted_positions[collided] = player_positions[collided] + (
+            projected_y[collided] * (self.player_movement_speed * d_time)
         )
+        new_positions = new_targeted_positions
 
-        # Check collisions with counters again, now absolute with no sliding possible
+        # Check collisions with counters a final time, now absolute with no sliding possible.
+        # Players should never be able to enter counters this way.
         (
             collided,
             relevant_axes,
@@ -226,7 +231,7 @@ class Movement:
         ) = self.get_counter_collisions(new_positions)
         new_positions[collided] = player_positions[collided]
 
-        # Collisions player world borders
+        # Collisions of players with world borders
         new_positions = np.clip(
             new_positions,
             self.world_borders_lower + self.player_radius,
diff --git a/cooperative_cuisine/orders.py b/cooperative_cuisine/orders.py
index 58db96b624eacf9b52a52bc36eb6bc6cdb998b7a..33c841f71c12a69fec1c8c656bed01c3ce7a30cf 100644
--- a/cooperative_cuisine/orders.py
+++ b/cooperative_cuisine/orders.py
@@ -251,6 +251,8 @@ class OrderManager:
         self.hook(INIT_ORDERS)
         self.open_orders.extend(init_orders)
 
+        # self.update_next_relevant_time()
+
     def progress(self, passed_time: timedelta, now: datetime):
         """Check expired orders and check order generation."""
         new_orders = self.order_gen.get_orders(
@@ -434,6 +436,14 @@ class RandomOrderGeneration(OrderGeneration):
             if new_finished_orders:
                 self.create_random_next_time_delta(now)
                 return []
+        # print(
+        #     " - -",
+        #     self.needed_orders,
+        #     self.number_cur_orders,
+        #     self.next_order_time,
+        #     now,
+        # )
+
         if self.needed_orders:
             self.needed_orders -= len(new_finished_orders)
             self.needed_orders = max(self.needed_orders, 0)
@@ -442,6 +452,7 @@ class RandomOrderGeneration(OrderGeneration):
                 self.random.choices(self.available_meals, k=len(new_finished_orders)),
                 now,
             )
+
         if self.next_order_time <= now:
             if self.number_cur_orders >= self.kwargs.max_orders:
                 self.needed_orders += 1
@@ -473,7 +484,7 @@ class RandomOrderGeneration(OrderGeneration):
         orders = []
         for meal in meals:
             if no_time_limit:
-                duration = datetime.max - now
+                duration = timedelta(days=365)
             else:
                 if isinstance(self.kwargs.order_duration_random_func["func"], str):
                     seconds = getattr(
@@ -502,11 +513,11 @@ class RandomOrderGeneration(OrderGeneration):
     def create_random_next_time_delta(self, now: datetime):
         if isinstance(self.kwargs.order_duration_random_func["func"], str):
             seconds = getattr(
-                self.random, self.kwargs.order_duration_random_func["func"]
-            )(**self.kwargs.order_duration_random_func["kwargs"])
+                self.random, self.kwargs.sample_on_dur_random_func["func"]
+            )(**self.kwargs.sample_on_dur_random_func["kwargs"])
         else:
-            seconds = self.kwargs.order_duration_random_func["func"](
-                **self.kwargs.order_duration_random_func["kwargs"]
+            seconds = self.kwargs.sample_on_dur_random_func["func"](
+                **self.kwargs.sample_on_dur_random_func["kwargs"]
             )
 
         self.next_order_time = now + timedelta(seconds=seconds)
diff --git a/cooperative_cuisine/player.py b/cooperative_cuisine/player.py
index 4da8e8b1e3e757616a1c535f71c5cc5ad3fd510c..81226bfceadd3782a207d5f2de3e59c573f5155f 100644
--- a/cooperative_cuisine/player.py
+++ b/cooperative_cuisine/player.py
@@ -133,9 +133,7 @@ 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.player_config.radius * 0.5
-        )
+        self.facing_point = self.pos + (self.facing_direction * 0.62)
 
     def can_reach(self, counter: Counter) -> bool:
         """Checks whether the player can reach the counter in question. Simple check if the distance is not larger
diff --git a/cooperative_cuisine/pygame_2d_vis/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py
index 1207752c1c6c246c4574f0049435337c37dfe81f..899f0559d2e4da9cdd53fdc5ba6725f6f19ee36a 100644
--- a/cooperative_cuisine/pygame_2d_vis/drawing.py
+++ b/cooperative_cuisine/pygame_2d_vis/drawing.py
@@ -149,7 +149,7 @@ class Visualizer:
             grid_size,
         )
 
-        for idx, col in zip(controlled_player_idxs, [colors["blue"], colors["red"]]):
+        for idx, col in zip(controlled_player_idxs, [colors["red"], colors["blue"]]):
             pygame.draw.circle(
                 screen,
                 col,
@@ -537,7 +537,7 @@ class Visualizer:
             plate: item is on a plate (soup are is different on a plate and pot)
         """
 
-        if not isinstance(item, list):  # can we remove this check?w
+        if not isinstance(item, list):  # can we remove this check?
             if item["type"] in self.config or (
                 item["type"].startswith("Burnt")
                 and item["type"].replace("Burnt", "") in self.config
@@ -598,7 +598,7 @@ class Visualizer:
             )
         elif "content_list" in item and item["content_list"]:
             triangle_offsets = create_polygon(
-                len(item["content_list"]), np.array([0, 10])
+                len(item["content_list"]), np.array([0, grid_size * 0.15])
             )
             scale = 1 if len(item["content_list"]) == 1 else 0.6
             for idx, o in enumerate(item["content_list"]):
@@ -709,9 +709,10 @@ class Visualizer:
         # Multiple plates on plate return:
         if isinstance(occupied_by, list):
             for i, o in enumerate(occupied_by):
+                stack_pos = np.abs([pos[0], pos[1] - (i * grid_size * 0.075)])
                 self.draw_item(
                     screen=screen,
-                    pos=np.abs([pos[0], pos[1] - (i * 3)]),
+                    pos=stack_pos,
                     grid_size=grid_size,
                     item=o,
                     scale=item_scale,
@@ -922,7 +923,7 @@ class Visualizer:
     def draw_recipe_image(
         self, screen: pygame.Surface, graph_dict, width, height, grid_size
     ) -> None:
-        screen.fill(self.config["GameWindow"]["background_color"])
+        # 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)
diff --git a/cooperative_cuisine/pygame_2d_vis/gui.py b/cooperative_cuisine/pygame_2d_vis/gui.py
index f6b8946357324fa55359821a99e35f186d25ab51..df472228b29051133df2872bc8b1b28db9d3d767 100644
--- a/cooperative_cuisine/pygame_2d_vis/gui.py
+++ b/cooperative_cuisine/pygame_2d_vis/gui.py
@@ -28,6 +28,7 @@ from cooperative_cuisine.game_server import (
 )
 from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
 from cooperative_cuisine.pygame_2d_vis.game_colors import colors
+from cooperative_cuisine.server_results import PlayerInfo
 from cooperative_cuisine.state_representation import StateRepresentation
 from cooperative_cuisine.utils import (
     url_and_port_arguments,
@@ -42,7 +43,7 @@ class MenuStates(Enum):
     """Enumeration of "Page" types in the 2D pygame vis."""
 
     Start = "Start"
-    ControllerTutorial = "ControllerTutorial"
+    Tutorial = "Tutorial"
     PreGame = "PreGame"
     Game = "Game"
     PostGame = "PostGame"
@@ -67,7 +68,7 @@ class PlayerKeySet:
         pickup_key: pygame.key,
         switch_key: pygame.key,
         players: list[str],
-        joystick: int,
+        joystick: int | None,
     ):
         """Creates a player key set which contains information about which keyboard keys control the player.
 
@@ -96,11 +97,16 @@ class PlayerKeySet:
         self.joystick = joystick
 
     def set_controlled_players(self, controlled_players: list[str]) -> None:
+        """Sets the controlled players for this keyset.
+        Args:
+            controlled_players: The players controlled by this keyset.
+        """
         self.controlled_players = controlled_players
         self.current_player = self.controlled_players[0]
         self.current_idx = 0
 
     def next_player(self) -> None:
+        """Switches to the next player in the list of controlled players."""
         self.current_idx = (self.current_idx + 1) % len(self.controlled_players)
         if self.other_keyset:
             for ok in self.other_keyset:
@@ -109,6 +115,9 @@ class PlayerKeySet:
                     return
         self.current_player = self.controlled_players[self.current_idx]
 
+    def __repr__(self) -> str:
+        return f"Keyset(current={self.current_player}, players={self.controlled_players}, joy={self.joystick})"
+
 
 class PyGameGUI:
     """Visualisation of the overcooked environment and reading keyboard inputs using pygame."""
@@ -184,6 +193,8 @@ class PyGameGUI:
             self.window_width_fullscreen /= 2
             self.window_height_fullscreen /= 2
 
+        self.game_width = 0
+        self.game_height = 0
         self.window_width_windowed = self.min_width
         self.window_height_windowed = self.min_height
         self.kitchen_width = 1
@@ -232,7 +243,7 @@ class PyGameGUI:
                 pickup_key=pygame.K_e,
                 switch_key=pygame.K_SPACE,
                 players=players,
-                joystick=0,
+                joystick=None,
             )
             key_set2 = PlayerKeySet(
                 move_keys=[pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN],
@@ -240,10 +251,16 @@ class PyGameGUI:
                 pickup_key=pygame.K_o,
                 switch_key=pygame.K_p,
                 players=players,
-                joystick=1,
+                joystick=None,
             )
             key_sets = [key_set1, key_set2]
 
+            if self.joysticks:
+                for idx, key in enumerate(self.joysticks.keys()):
+                    if idx >= len(key_sets):
+                        break
+                    key_sets[idx].joystick = key
+
             if disjunct:
                 key_set1.set_controlled_players(players[::2])
                 key_set2.set_controlled_players(players[1::2])
@@ -282,7 +299,7 @@ class PyGameGUI:
                 )
                 self.send_action(action)
 
-    def handle_joy_stick_input(self, joysticks):
+    def handle_joy_stick_input(self, joysticks: dict[int, pygame.joystick.Joystick]):
         """Handles joystick inputs for movement every frame
         Args:
             joysticks: list of joysticks
@@ -323,7 +340,7 @@ class PyGameGUI:
                     )
                     self.send_action(action)
 
-    def handle_key_event(self, event):
+    def handle_key_event(self, event: pygame.event.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.
@@ -353,7 +370,9 @@ class PyGameGUI:
                 if event.type == pygame.KEYDOWN:
                     key_set.next_player()
 
-    def handle_joy_stick_event(self, event, joysticks):
+    def handle_joy_stick_event(
+        self, event: pygame.event.Event, joysticks: dict[int, pygame.joystick.Joystick]
+    ):
         """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.
@@ -395,6 +414,7 @@ class PyGameGUI:
                         key_set.next_player()
 
     def set_window_size(self):
+        """Sets the window size based on fullscreen or not."""
         if self.fullscreen:
             flags = pygame.FULLSCREEN
             self.window_width = self.window_width_fullscreen
@@ -412,12 +432,13 @@ class PyGameGUI:
             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: float = None, max_height: float = None):
+        """Sets the game size based on the kitchen size and the current window size.
 
-    def set_game_size(self, max_width=None, max_height=None):
+        Args:
+            max_width: Maximum width of the game screen.
+            max_height: Maximum height of the game screen.
+        """
         if max_width is None:
             max_width = self.window_width - (2 * self.screen_margin)
         if max_height is None:
@@ -458,6 +479,8 @@ class PyGameGUI:
         )
 
     def init_ui_elements(self):
+        """Creates all UI elements. Creates lists of which elements belong on which screen."""
+
         self.manager = pygame_gui.UIManager(
             (self.window_width, self.window_height),
             starting_language=self.language,
@@ -632,9 +655,8 @@ class PyGameGUI:
         )
 
         size = 50
-        add_player_button_rect = pygame.Rect((0, 0), (size, size))
         self.add_human_player_button = pygame_gui.elements.UIButton(
-            relative_rect=add_player_button_rect,
+            relative_rect=pygame.Rect((0, 0), (size, size)),
             text="+",
             manager=self.manager,
             object_id="#quantity_button",
@@ -642,10 +664,10 @@ class PyGameGUI:
             anchors={"left_target": self.added_bots_label, "centery": "centery"},
         )
 
-        remove_player_button_rect = pygame.Rect((0, 0), (size, size))
-        remove_player_button_rect.right = 0
+        rect = pygame.Rect((0, 0), (size, size))
+        rect.right = 0
         self.remove_human_button = pygame_gui.elements.UIButton(
-            relative_rect=remove_player_button_rect,
+            relative_rect=rect,
             text="-",
             manager=self.manager,
             object_id="#quantity_button",
@@ -657,9 +679,8 @@ class PyGameGUI:
             },
         )
 
-        add_bot_button_rect = pygame.Rect((0, 0), (size, size))
         self.add_bot_button = pygame_gui.elements.UIButton(
-            relative_rect=add_bot_button_rect,
+            relative_rect=pygame.Rect((0, 0), (size, size)),
             text="+",
             manager=self.manager,
             object_id="#quantity_button",
@@ -667,10 +688,10 @@ class PyGameGUI:
             anchors={"left_target": self.added_bots_label, "centery": "centery"},
         )
 
-        remove_bot_button_rect = pygame.Rect((0, 0), (size, size))
-        remove_bot_button_rect.right = 0
+        rect = pygame.Rect((0, 0), (size, size))
+        rect.right = 0
         self.remove_bot_button = pygame_gui.elements.UIButton(
-            relative_rect=remove_bot_button_rect,
+            relative_rect=rect,
             text="-",
             manager=self.manager,
             object_id="#quantity_button",
@@ -830,22 +851,6 @@ class PyGameGUI:
         self.orders_container_width = (
             self.window_width - (2 * self.buttons_width) - (self.buttons_width * 0.7)
         )
-        # rect = pygame.Rect(
-        #     0,
-        #     0,
-        #     self.orders_container_width,
-        #     self.screen_margin,
-        # )
-        # self.orders_container = pygame_gui.elements.UIPanel(
-        #     relative_rect=rect,
-        #     manager=self.manager,
-        #     object_id="#graph_container",
-        #     anchors={
-        #         "top": "top",
-        #         "left": "left",
-        #         "left_target": self.orders_label,
-        #     },
-        # )
 
         self.orders_image = pygame_gui.elements.UIImage(
             relative_rect=pygame.Rect(
@@ -1067,7 +1072,12 @@ class PyGameGUI:
             final_text_container,
         ]
 
-    def show_screen_elements(self, elements: list):
+    def show_screen_elements(self, elements: list[pygame_gui.core.UIElement]):
+        """Hides all UI elements and shows the elements in the list and elements in self.on_all_screens.
+
+        Args:
+            elements: List of UI elements to show.
+        """
         all_elements = (
             self.start_screen_elements
             + self.tutorial_screen_elements
@@ -1083,7 +1093,8 @@ class PyGameGUI:
         for element in elements + self.on_all_screens:
             element.show()
 
-    def update_tutorial_screen(self):
+    def setup_tutorial_screen(self):
+        """Updates the tutorial screen with the current tutorial image and the continue button."""
         self.show_screen_elements(self.tutorial_screen_elements)
 
         self.set_game_size(
@@ -1107,19 +1118,17 @@ class PyGameGUI:
             grid_size=self.window_height / 18,
         )
         self.tutorial_graph_image.set_image(tutorial_graph_surface)
-        # self.tutorial_graph_image.set_dimensions((self.game_width, self.game_height))
 
     def update_screen_elements(self):
+        """Shows and hides the UI elements based on the current menu state."""
         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.update_tutorial_screen()
+            case MenuStates.Tutorial:
+                self.setup_tutorial_screen()
             case MenuStates.PreGame:
                 self.init_ui_elements()
                 self.show_screen_elements(self.pregame_screen_elements)
@@ -1129,7 +1138,7 @@ class PyGameGUI:
             case MenuStates.PostGame:
                 self.init_ui_elements()
                 self.show_screen_elements(self.postgame_screen_elements)
-                self.update_postgame_screen(self.last_state)
+                self.update_post_game_screen(self.last_state)
                 if self.last_level:
                     self.next_game_button.hide()
                     self.finish_study_button.show()
@@ -1140,37 +1149,27 @@ class PyGameGUI:
                 self.show_screen_elements(self.end_screen_elements)
 
     def draw_main_window(self):
+        """Main draw function. Draws the main window and updates the UI.
+        Draws the game screen in Game and ControllerTutorial screen."""
         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()
+        if self.menu_state == MenuStates.Tutorial:
+            self.draw_game_screen_frame(tutorial=True)
+        elif self.menu_state == MenuStates.Game:
+            self.draw_game_screen_frame(tutorial=False)
 
         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_post_game_screen(self, state: dict):
+        """Updates the post game screen with the final score and the completed meals.
 
-    def update_postgame_screen(self, state):
+        Args:
+            state: The game state returned by the environment, containing served meals and score.
+        """
         score = state["score"]
         # self.score_conclusion.set_text(f"Your final score is {score}. Hurray!")
         self.score_conclusion.set_text(
@@ -1225,47 +1224,6 @@ class PyGameGUI:
                 anchors=anchors,
             )
 
-            text = ":"
-            rect = pygame.Rect(
-                (0, 0),
-                (container_width / 10, row_height),
-            )
-            rect.left = 0
-            meal_label = pygame_gui.elements.UILabel(
-                text=text,
-                relative_rect=rect,
-                manager=self.manager,
-                container=container,
-                object_id="#served_meal",
-                anchors={"center": "center"},
-            )
-
-            cook_surface = pygame.Surface(
-                (row_height, row_height), flags=pygame.SRCALPHA
-            )
-            player_idx = int(player)
-            player_color = colors[self.vis.player_colors[player_idx]]
-            self.vis.draw_cook(
-                screen=cook_surface,
-                grid_size=row_height,
-                pos=np.array([row_height / 2, row_height / 2]),
-                color=player_color,
-                facing=np.array([0, 1]),
-            )
-            rect = cook_surface.get_rect()
-            rect.right = 0
-            cook_image = pygame_gui.elements.UIImage(
-                relative_rect=rect,
-                image_surface=cook_surface,
-                manager=self.manager,
-                container=container,
-                anchors={
-                    "centery": "centery",
-                    "right": "right",
-                    "right_target": meal_label,
-                },
-            )
-
             meal_surface = pygame.Surface(
                 (row_height, row_height), flags=pygame.SRCALPHA
             )
@@ -1276,23 +1234,65 @@ class PyGameGUI:
                 screen=meal_surface,
                 grid_size=row_height,
             )
+
+            meal_name = meal.split("(")[0]
             self.vis.draw_item(
                 pos=np.array([row_height / 2, row_height / 2]),
-                item={"type": meal},
+                item={"type": meal_name},
                 plate=True,
                 screen=meal_surface,
                 grid_size=row_height,
             )
             rect = meal_surface.get_rect()
-            # rect.left = 0
+            rect.center = (rect.center[0] - (self.buttons_width * 0.3), rect.center[1])
             meal_image = pygame_gui.elements.UIImage(
                 relative_rect=rect,
                 image_surface=meal_surface,
                 manager=self.manager,
                 container=container,
-                anchors={"centery": "centery", "left_target": meal_label},
+                anchors={"center": "center"},
             )
 
+            rect = pygame.Rect(
+                (0, 0),
+                (container_width / 4, row_height),
+            )
+            rect.left = 0
+            meal_label = pygame_gui.elements.UILabel(
+                text="translations.was_served",
+                relative_rect=rect,
+                manager=self.manager,
+                container=container,
+                object_id="#was_served",
+                anchors={"centery": "centery", "left_target": meal_image},
+            )
+
+            # cook_surface = pygame.Surface(
+            #     (row_height, row_height), flags=pygame.SRCALPHA
+            # )
+            # player_idx = int(player)
+            # player_color = colors[self.vis.player_colors[player_idx]]
+            # self.vis.draw_cook(
+            #     screen=cook_surface,
+            #     grid_size=row_height,
+            #     pos=np.array([row_height / 2, row_height / 2]),
+            #     color=player_color,
+            #     facing=np.array([0, 1]),
+            # )
+            # rect = cook_surface.get_rect()
+            # rect.right = 0
+            # cook_image = pygame_gui.elements.UIImage(
+            #     relative_rect=rect,
+            #     image_surface=cook_surface,
+            #     manager=self.manager,
+            #     container=container,
+            #     anchors={
+            #         "centery": "centery",
+            #         "right": "right",
+            #         "right_target": meal_label,
+            #     },
+            # )
+
             last_completed_meals.append(container)
 
         self.scroll_space_completed_meals.set_scrollable_area_dimensions(
@@ -1300,50 +1300,23 @@ class PyGameGUI:
         )
 
     def exit_game(self):
+        """Exits the game."""
         self.menu_state = MenuStates.PostGame
 
         if self.CONNECT_WITH_STUDY_SERVER:
             self.send_level_done()
         self.disconnect_websockets()
 
-        self.update_postgame_screen(self.last_state)
+        self.update_post_game_screen(self.last_state)
         self.update_screen_elements()
         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):
+    def draw_game(self, state: dict):
         """Main visualization function.
 
-        Args:            state: The game state returned by the environment."""
+        Args:
+            state: The game state returned by the environment.
+        """
         self.vis.draw_gamescreen(
             self.game_screen,
             state,
@@ -1351,7 +1324,6 @@ class PyGameGUI:
             [int(k.current_player) for k in self.key_sets],
         )
 
-        # orders_surface = pygame.Surface((self.orders_container_width, self.screen_margin))
         self.vis.draw_orders(
             screen=self.orders_image.image,
             state=state,
@@ -1360,7 +1332,6 @@ class PyGameGUI:
             height=self.screen_margin,
             config=self.visualization_config,
         )
-        # self.orders_image.set_image(orders_surface)
 
         border = self.visualization_config["GameWindow"]["game_border_size"]
         border_rect = pygame.Rect(
@@ -1397,13 +1368,69 @@ class PyGameGUI:
                     ),
                 )
 
-    def update_score_label(self, state):
+    def draw_game_screen_frame(self, tutorial: bool = False):
+        """Main visualization function for the game screen.
+
+        Args:
+            tutorial: If True, the tutorial screen is drawn, which is a simplified version of the game screen.
+        """
+        self.last_state = self.request_state()
+
+        self.handle_keys()
+        self.handle_joy_stick_input(joysticks=self.joysticks)
+
+        if tutorial:
+            self.vis.draw_gamescreen(
+                self.game_screen,
+                self.last_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)
+            return
+        else:
+            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 update_score_label(self, state: dict):
+        """Updates the score label.
+
+        Args:
+            state: The game state returned by the environment.
+
+        """
         score = state["score"]
         self.score_label.set_text(
             "translations.score", text_kwargs={"score": str(score)}
         )
 
     def update_remaining_time(self, remaining_time: float):
+        """Updates the remaining time label.
+
+        Args:
+            remaining_time: The remaining time in seconds.
+        """
         hours, rem = divmod(int(remaining_time), 3600)
         minutes, seconds = divmod(rem, 60)
         display_time = f"{minutes}:{'%02d' % seconds}"
@@ -1411,7 +1438,12 @@ class PyGameGUI:
             "translations.time_remaining", text_kwargs={"time": display_time}
         )
 
-    def create_env_on_game_server(self, tutorial):
+    def create_env_on_game_server(self, tutorial: bool):
+        """Starts an environment on the game server.
+
+        Args:
+            tutorial: If True, a tutorial environment is created. Else a normal game environment is created.
+        """
         if tutorial:
             layout_path = ROOT_DIR / "configs" / "layouts" / "tutorial.layout"
             environment_config_path = ROOT_DIR / "configs" / "tutorial_env_config.yaml"
@@ -1422,7 +1454,7 @@ class PyGameGUI:
             ]
             # layout_path = self.layout_file_paths[self.current_layout_idx]
 
-        item_info_path = ROOT_DIR / "configs" / "item_info.yaml"
+        item_info_path = ROOT_DIR / "configs" / "item_info_debug.yaml"
         with open(item_info_path, "r") as file:
             item_info = file.read()
         with open(layout_path, "r") as file:
@@ -1440,6 +1472,7 @@ class PyGameGUI:
             environment_config=environment_config,
             layout_config=layout,
             seed=seed,
+            env_name=layout_path.stem
         ).model_dump(mode="json")
 
         # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
@@ -1450,7 +1483,7 @@ class PyGameGUI:
         if env_info.status_code == 403:
             raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
         elif env_info.status_code == 409:
-            print("CONFLICT")
+            log.warning("CONFLICT")
         env_info = env_info.json()
         assert isinstance(env_info, dict), "Env info must be a dictionary"
         self.current_env_id = env_info["env_id"]
@@ -1476,6 +1509,7 @@ class PyGameGUI:
         self.level_info["number_players"] = num_players
 
     def update_pregame_screen(self):
+        """Updates the pregame screen. Possible recipes in the level are displayed."""
         self.level_name_label.set_text(
             "translations.level_name", text_kwargs={"level": self.level_info["name"]}
         )
@@ -1574,6 +1608,8 @@ class PyGameGUI:
         )
 
     def setup_tutorial(self):
+        """Sets up the tutorial environment. This includes creating the environment
+        on the game server, and setting up the connection."""
         answer = requests.post(
             f"{self.request_url}/connect_to_tutorial/{self.participant_id}"
         )
@@ -1584,7 +1620,6 @@ class PyGameGUI:
             if answer.status_code == 200:
                 answer_json = answer.json()
                 self.player_info = answer_json["player_info"]["0"]
-                print("TUTORIAL PLAYER INFO", self.player_info)
                 self.level_info = answer_json["level_info"]
                 self.player_info = {self.player_info["player_id"]: self.player_info}
             else:
@@ -1594,7 +1629,9 @@ class PyGameGUI:
             log.warning("Could not create tutorial.")
 
     def get_game_connection(self):
-        if self.menu_state == MenuStates.ControllerTutorial:
+        """Sets up a connection to the game server.
+        This includes getting the player info, level info and player keys"""
+        if self.menu_state == MenuStates.Tutorial:
             self.setup_tutorial()
             self.key_sets = self.setup_player_keys(["0"], 1, False)
             self.vis.create_player_colors(1)
@@ -1605,7 +1642,6 @@ class PyGameGUI:
             if answer.status_code == 200:
                 answer_json = answer.json()
                 self.player_info = answer_json["player_info"]
-                print("GAME PLAYER INFO", self.player_info)
 
                 self.level_info = answer_json["level_info"]
                 self.last_level = self.level_info["last_level"]
@@ -1626,10 +1662,17 @@ class PyGameGUI:
             )
         self.player_ids = list(self.player_info.keys())
 
-    def create_and_connect_bot(self, player_id, player_info):
+    def create_and_connect_bot(self, player_id: str, player_info: PlayerInfo):
+        """Creates a bot process and connects it to the game server.
+
+        Args:
+            player_id: The id/name of the player.
+            player_info: Player info containing client_id, player_hash player_id and websocket_url.
+        """
         player_hash = player_info["player_hash"]
         print(
-            f'--general_plus="agent_websocket:{player_info["websocket_url"]};player_hash:{player_hash};agent_id:{player_id}"'
+            f'--general_plus="agent_websocket:{player_info["websocket_url"]};'
+            f'player_hash:{player_hash};agent_id:{player_id}"'
         )
         if self.USE_AAAMBOS_AGENT:
             sub = Popen(
@@ -1642,7 +1685,8 @@ class PyGameGUI:
                         str(ROOT_DIR / "configs" / "agents" / "arch_config.yml"),
                         "--run_config",
                         str(ROOT_DIR / "configs" / "agents" / "run_config.yml"),
-                        f'--general_plus="agent_websocket:{player_info["websocket_url"]};player_hash:{player_hash};agent_id:{player_id}"',
+                        f'--general_plus="agent_websocket:{player_info["websocket_url"]};'
+                        f'player_hash:{player_hash};agent_id:{player_id}"',
                         f"--instance={player_hash}",
                     ]
                 ),
@@ -1664,6 +1708,8 @@ class PyGameGUI:
         self.sub_processes.append(sub)
 
     def connect_websockets(self):
+        """Connects the websockets of the players to the game server.
+        If the player is a bot, a bot process is created"""
         for p, (player_id, player_info) in enumerate(self.player_info.items()):
             if p < self.number_humans_to_be_added:
                 # add player websockets
@@ -1680,18 +1726,23 @@ class PyGameGUI:
                 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):
+        """Sets up prerequisites for the game. This includes connecting the websockets, creating player colors
+        in the vis and setting the kitchen size."""
         self.connect_websockets()
         self.vis.create_player_colors(self.level_info["number_players"])
         self.kitchen_width, self.kitchen_height = self.level_info["kitchen_size"]
 
     def stop_game_on_server(self, reason: str) -> None:
+        """Stops the game directly on the game server.
+        Args:
+            reason: The reason for stopping the game.
+        """
         log.debug(f"Stopping game: {reason}")
         if not self.CONNECT_WITH_STUDY_SERVER:
             answer = requests.post(
@@ -1710,11 +1761,14 @@ class PyGameGUI:
                 )
 
     def send_tutorial_finished(self):
+        """Signals the study server that the tutorial was finished."""
         requests.post(
             f"{self.request_url}/disconnect_from_tutorial/{self.participant_id}",
         )
 
     def finished_button_press(self):
+        """Gets called when the finished button is pressed.
+        Stops the game on the game server if the study server is not used."""
         if not self.CONNECT_WITH_STUDY_SERVER:
             self.stop_game_on_server("finished_button_pressed")
         self.menu_state = MenuStates.PostGame
@@ -1722,6 +1776,7 @@ class PyGameGUI:
         self.update_screen_elements()
 
     def fullscreen_button_press(self):
+        """Toggles between fullscreen and windowed mode."""
         self.fullscreen = not self.fullscreen
         self.set_window_size()
         self.init_ui_elements()
@@ -1729,6 +1784,7 @@ class PyGameGUI:
         self.update_screen_elements()
 
     def reset_gui_values(self):
+        """Reset the values of the GUI elements to their default values. Default values are defined here."""
         self.currently_controlled_player_idx = 0
         self.number_humans_to_be_added = 1
         self.number_bots_to_be_added = 0
@@ -1739,6 +1795,8 @@ class PyGameGUI:
         self.multiple_keysets = False
 
     def update_selection_elements(self):
+        """Updates the selection elements of the GUI. This includes the number of players,
+        the number of bots, the split players button and the multiple keysets button."""
         if self.number_humans_to_be_added == 1:
             self.remove_human_button.disable()
             self.multiple_keysets_button.hide()
@@ -1813,6 +1871,7 @@ class PyGameGUI:
         self.websockets[action.player].recv()
 
     def request_state(self):
+        """Requests the current state of the game environment from the game server."""
         message_dict = {
             "type": PlayerRequestType.GET_STATE.value,
             "action": None,
@@ -1823,7 +1882,19 @@ class PyGameGUI:
         state = json.loads(self.websockets[self.state_player_id].recv())
         return state
 
+    def exit_tutorial(self):
+        """Exits the tutorial. Disconnects the websockets and signals the study server that the tutorial was
+        finished if the study server is used. Otherwise, the game is stopped on the game server.
+        """
+        self.disconnect_websockets()
+        self.menu_state = MenuStates.PreGame
+        if self.CONNECT_WITH_STUDY_SERVER:
+            self.send_tutorial_finished()
+        else:
+            self.stop_game_on_server("tutorial_finished")
+
     def disconnect_websockets(self):
+        """Disconnects the websockets. Kills all subprocesses that are running bots."""
         for sub in self.sub_processes:
             try:
                 if self.USE_AAAMBOS_AGENT:
@@ -1842,15 +1913,18 @@ class PyGameGUI:
         for websocket in self.websockets.values():
             websocket.close()
 
-    def play_bell_sound(self):
+    @staticmethod
+    def play_bell_sound():
+        """Plays a bell sound when the game starts."""
         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.set_volume(0.7)
         mixer.music.play()
         log.log(logging.INFO, "Started game, played bell sound")
 
     def start_study(self):
+        """Starts the study on the study server."""
         answer = requests.post(
             f"{self.request_url}/start_study/{self.participant_id}/{self.number_humans_to_be_added}"
         )
@@ -1859,13 +1933,14 @@ class PyGameGUI:
             self.get_game_connection()
         else:
             self.menu_state = MenuStates.Start
-            print(
+            log.warning(
                 "COULD NOT START STUDY; Response:",
                 answer.status_code,
                 answer.json()["detail"],
             )
 
     def send_level_done(self):
+        """Sends a message to the study server that the level was finished."""
         answer = requests.post(f"{self.request_url}/level_done/{self.participant_id}")
         if answer.status_code != 200:
             log.warning(
@@ -1875,22 +1950,24 @@ class PyGameGUI:
             )
 
     def button_continue_postgame_pressed(self):
+        """Handles the continue button press on the postgame screen. If the study server is used, the connection to
+        a new game is set up. Otherwise, the start screen is shown.
+        """
         if self.CONNECT_WITH_STUDY_SERVER:
             if not self.last_level:
                 self.get_game_connection()
         else:
-            # self.current_layout_idx += 1
             self.menu_state = MenuStates.Start
             return
-            # self.create_env_on_game_server(tutorial=False)
-            # 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]}")
-
         self.menu_state = MenuStates.PreGame
 
     def manage_button_event(self, button: pygame_gui.core.UIElement | None = None):
+        """Manages the button events. The button events are filtered by the current menu state and the button that
+        was pressed.
+
+        Args:
+            button: The button that was pressed.
+        """
         if button == self.quit_button:
             if self.fullscreen:
                 self.fullscreen_button_press()
@@ -1918,7 +1995,7 @@ class PyGameGUI:
                         ):
                             pass
                         else:
-                            self.menu_state = MenuStates.ControllerTutorial
+                            self.menu_state = MenuStates.Tutorial
                             if self.CONNECT_WITH_STUDY_SERVER:
                                 self.get_game_connection()
                             else:
@@ -1946,7 +2023,7 @@ class PyGameGUI:
 
             ############################################
 
-            case MenuStates.ControllerTutorial:
+            case MenuStates.Tutorial:
                 self.exit_tutorial()
                 if self.CONNECT_WITH_STUDY_SERVER:
                     self.start_study()
@@ -1976,6 +2053,89 @@ class PyGameGUI:
 
             ############################################
 
+    def add_joystick(self, event: pygame.event.Event):
+        """Adds a joystick to the list of joysticks and assigns it to a key set.
+        A pygame.JOYDEVICEADDED event is generated for every joystick connected at the start of the program.
+
+        Args:
+            event: The event that is triggered when a joystick is connected.
+        """
+        joy = pygame.joystick.Joystick(event.device_index)
+        self.joysticks[joy.get_instance_id()] = joy
+
+        for key_set in self.key_sets:
+            if key_set.joystick is None:
+                key_set.joystick = joy.get_instance_id()
+                break
+        log.debug(f"Joystick {joy.get_instance_id()} connected")
+
+    def remove_joystick(self, event: pygame.event.Event):
+        """Removes a joystick from the list of joysticks and unassigns it from a key set.
+
+        Args:
+            event: The event that is triggered when a joystick is disconnected.
+        """
+        del self.joysticks[event.instance_id]
+        for key_set in self.key_sets:
+            if key_set.joystick == event.instance_id:
+                key_set.joystick = None
+        log.debug(f"Joystick {event.instance_id} disconnected")
+        log.debug(f"Number of joysticks:" + str(pygame.joystick.get_count()))
+
+    def process_gui_event(self, event: pygame.event.Event):
+        """Processes the pygame events. The events are filtered by their type and the current menu state.
+
+        Args:
+            event: The pygame event that is processed.
+        """
+        if event.type == pygame.QUIT:
+            self.disconnect_websockets()
+            self.running = False
+
+        if event.type == pygame.JOYDEVICEADDED and pygame.joystick.get_count() > 0:
+            self.add_joystick(event)
+        if event.type == pygame.JOYDEVICEREMOVED:
+            self.remove_joystick(event)
+
+        # Press enter key or controller start button instead of mouse button press
+        if (
+            event.type == pygame.JOYBUTTONDOWN
+            and any([joy.get_button(7) for joy in self.joysticks.values()])
+            or (event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN)
+        ):
+            if self.menu_state == MenuStates.Start:
+                self.manage_button_event(self.start_button)
+                self.update_screen_elements()
+
+            elif self.menu_state in [
+                MenuStates.Tutorial,
+                MenuStates.PreGame,
+                MenuStates.PostGame,
+            ]:
+                self.manage_button_event(self.continue_button)
+                self.update_screen_elements()
+
+        if event.type == pygame_gui.UI_BUTTON_PRESSED:
+            button = event.ui_element
+            self.manage_button_event(button)
+            if button in [
+                self.start_button,
+                self.continue_button,
+                self.finish_study_button,
+                self.next_game_button,
+            ]:
+                self.update_screen_elements()
+            elif self.menu_state == MenuStates.Start:
+                self.update_selection_elements()
+
+        if self.menu_state in [MenuStates.Game, MenuStates.Tutorial]:
+            if event.type in [pygame.KEYDOWN, pygame.KEYUP]:
+                self.handle_key_event(event)
+            if event.type in [pygame.JOYBUTTONDOWN, pygame.JOYBUTTONUP]:
+                self.handle_joy_stick_event(event, joysticks=self.joysticks)
+
+        self.manager.process_events(event)
+
     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")
@@ -1986,17 +2146,14 @@ class PyGameGUI:
 
         clock = pygame.time.Clock()
 
-        self.reset_window_size()
+        self.set_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.
+        # pygame.JOYDEVICEADDED event is generated for every joystick connected at the start of the program.
         self.joysticks = {}
 
         while self.running:
@@ -2005,113 +2162,22 @@ class PyGameGUI:
 
                 # 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
-                        )
-                    ):
-                        if self.menu_state == MenuStates.Start:
-                            self.manage_button_event(self.start_button)
-                            self.update_screen_elements()
-
-                        elif self.menu_state in [
-                            MenuStates.ControllerTutorial,
-                            MenuStates.PreGame,
-                            MenuStates.PostGame,
-                        ]:
-                            self.manage_button_event(self.continue_button)
-                            self.update_screen_elements()
-
-                    if event.type == pygame_gui.UI_BUTTON_PRESSED:
-                        button = event.ui_element
-                        self.manage_button_event(button)
-                        if button in [
-                            self.start_button,
-                            self.continue_button,
-                            self.finish_study_button,
-                            self.next_game_button,
-                        ]:
-                            self.update_screen_elements()
-                        elif self.menu_state == MenuStates.Start:
-                            self.update_selection_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)
+                    self.process_gui_event(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.")
-                if self.fullscreen_button:
-                    self.fullscreen_button_press()
 
         self.disconnect_websockets()
         if not self.CONNECT_WITH_STUDY_SERVER:
             self.stop_game_on_server("Program exited.")
-
         if self.fullscreen:
             self.fullscreen_button_press()
         pygame.quit()
         sys.exit()
 
-    def exit_tutorial(self):
-        self.disconnect_websockets()
-        self.menu_state = MenuStates.PreGame
-        if self.CONNECT_WITH_STUDY_SERVER:
-            self.send_tutorial_finished()
-        else:
-            self.stop_game_on_server("tutorial_finished")
-
 
 def main(
     study_url: str,
@@ -2141,7 +2207,8 @@ 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",
+        epilog="For further information, "
+        "see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
     )
 
     url_and_port_arguments(parser)
diff --git a/cooperative_cuisine/pygame_2d_vis/gui_theme.json b/cooperative_cuisine/pygame_2d_vis/gui_theme.json
index a67753356698864486856ae56c7e23ab65f296a8..5ceaf4397e562c1e82e53566b5f09e91598e41f9 100644
--- a/cooperative_cuisine/pygame_2d_vis/gui_theme.json
+++ b/cooperative_cuisine/pygame_2d_vis/gui_theme.json
@@ -226,5 +226,10 @@
       "border_width": "0",
       "shadow_width": "0"
     }
+  },
+  "#was_served": {
+    "misc": {
+      "text_horiz_alignment": "left"
+    }
   }
 }
diff --git a/cooperative_cuisine/pygame_2d_vis/images/star.png b/cooperative_cuisine/pygame_2d_vis/images/star.png
new file mode 100644
index 0000000000000000000000000000000000000000..2df3847f26947b617909bebc0d43ac842cd783b6
Binary files /dev/null and b/cooperative_cuisine/pygame_2d_vis/images/star.png differ
diff --git a/cooperative_cuisine/pygame_2d_vis/locales/translations.de.json b/cooperative_cuisine/pygame_2d_vis/locales/translations.de.json
index f792c21ff567d7a47aecbe1c44a843251253a2b4..a772563275311776808d4eaf1b377bdf4c072406 100644
--- a/cooperative_cuisine/pygame_2d_vis/locales/translations.de.json
+++ b/cooperative_cuisine/pygame_2d_vis/locales/translations.de.json
@@ -12,7 +12,8 @@
     "continue": "Weiter",
     "salad_recipe": "Rezept für Salat:",
     "recipes_in_this_level": "Rezepte in diesem Level:",
-    "level_name": "Level: %{level}",
+    "level_name": "%{level}",
+    "was_served": " wurde serviert",
     "waiting_for_players": "WARTE AUF ANDERE SPIELER",
     "orders": "Bestellungen:",
     "score": "Punktestand: %{score}",
diff --git a/cooperative_cuisine/pygame_2d_vis/locales/translations.en.json b/cooperative_cuisine/pygame_2d_vis/locales/translations.en.json
index 5aae6e456bf73bcde431f6ecb64a7d2678bfbca5..3e1b3f392fb7cc071e60695cf5f89798eb793516 100644
--- a/cooperative_cuisine/pygame_2d_vis/locales/translations.en.json
+++ b/cooperative_cuisine/pygame_2d_vis/locales/translations.en.json
@@ -12,7 +12,8 @@
     "continue": "Continue",
     "salad_recipe": "Salad recipe:",
     "recipes_in_this_level": "Recipes in this level:",
-    "level_name": "Level: %{level}",
+    "level_name": "%{level}",
+    "was_served": " was served",
     "waiting_for_players": "WAITING FOR OTHER PLAYERS",
     "orders": "Orders:",
     "score": "Score: %{score}",
@@ -22,7 +23,7 @@
     "hurray": "Hurray!",
     "completed_meals": "Completed meals:",
     "completed_level": "Completed Level: %{level}",
-    "next_game": "Next study",
+    "next_game": "Next game",
     "finish_study": " Finish study",
     "thank_you": "Thank you for participating in this study!",
     "signal_supervisor": " Please signal the study supervisor that the study is finished.",
diff --git a/cooperative_cuisine/pygame_2d_vis/visualization.yaml b/cooperative_cuisine/pygame_2d_vis/visualization.yaml
index 954ff07d534c3d383d720fe7c1c4abf347cd977c..6f1fbe54331287555d4c9e25f4e2fd1bdf4fdb41 100644
--- a/cooperative_cuisine/pygame_2d_vis/visualization.yaml
+++ b/cooperative_cuisine/pygame_2d_vis/visualization.yaml
@@ -1,7 +1,7 @@
 # colors: https://www.webucator.com/article/python-color-constants-module/
 
 Gui:
-  language: "en"
+  language: "de"
   use_player_cook_sprites: True
   show_interaction_range: False
   show_counter_centers: False
@@ -109,9 +109,10 @@ Dispenser:
 ServingWindow:
   parts:
     - type: image
-      path: images/arrow_right.png
-      size: 1
-      center_offset: [ 0, 0 ]
+      path: images/star.png
+      size: 0.8
+      center_offset: [ 0, -0.02 ]
+      rotate_image: False
     - type: image
       path: images/bell_gold.png
       size: 0.5
diff --git a/cooperative_cuisine/reinforcement_learning/gym_env.py b/cooperative_cuisine/reinforcement_learning/gym_env.py
index 4bb079e3b45372e8d208df59b49da035b419a67a..db6357bac6a62b1c73c731a0c25dcd55f5dac13b 100644
--- a/cooperative_cuisine/reinforcement_learning/gym_env.py
+++ b/cooperative_cuisine/reinforcement_learning/gym_env.py
@@ -129,12 +129,12 @@ class EnvGymWrapper(Env):
     def __init__(self):
         super().__init__()
 
-        self.gridsize = 30
+        self.gridsize = 40
 
-        self.randomize_counter_placement = True
+        self.randomize_counter_placement = False
         self.use_rgb_obs = False  # if False uses simple vectorized state
         self.full_vector_state = True
-        self.onehot_state = False
+        self.onehot_state = True
 
         self.env: Environment = Environment(
             env_config=environment_config,
diff --git a/cooperative_cuisine/reinforcement_learning/pearl_test.py b/cooperative_cuisine/reinforcement_learning/pearl_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..c4cded0bfed470c2bcf60ea7e306ba21de4053ab
--- /dev/null
+++ b/cooperative_cuisine/reinforcement_learning/pearl_test.py
@@ -0,0 +1,68 @@
+import cv2
+from pearl.action_representation_modules.one_hot_action_representation_module import (
+    OneHotActionTensorRepresentationModule,
+)
+from pearl.pearl_agent import PearlAgent
+from pearl.policy_learners.sequential_decision_making.deep_q_learning import (
+    DeepQLearning,
+)
+from pearl.replay_buffers.sequential_decision_making.fifo_off_policy_replay_buffer import (
+    FIFOOffPolicyReplayBuffer,
+)
+from pearl.utils.instantiations.environments.gym_environment import GymEnvironment
+
+from cooperative_cuisine.reinforcement_learning.gym_env import EnvGymWrapper
+
+custom = True
+if custom:
+    env = GymEnvironment(EnvGymWrapper())
+else:
+    env = GymEnvironment("LunarLander-v2", render_mode="rgb_array")
+
+num_actions = env.action_space.n
+agent = PearlAgent(
+    policy_learner=DeepQLearning(
+        state_dim=env.observation_space.shape[0],
+        action_space=env.action_space,
+        hidden_dims=[64, 64],
+        training_rounds=20,
+        action_representation_module=OneHotActionTensorRepresentationModule(
+            max_number_actions=num_actions
+        ),
+    ),
+    replay_buffer=FIFOOffPolicyReplayBuffer(10_000),
+)
+
+for i in range(40):
+    print(i)
+    observation, action_space = env.reset()
+    agent.reset(observation, action_space)
+    done = False
+    while not done:
+        action = agent.act(exploit=False)
+        action_result = env.step(action)
+        agent.observe(action_result)
+        agent.learn()
+        done = action_result.done
+
+if custom:
+    env = GymEnvironment(EnvGymWrapper())
+else:
+    env = GymEnvironment("LunarLander-v2", render_mode="human")
+
+for i in range(40):
+    print(i)
+    observation, action_space = env.reset()
+    agent.reset(observation, action_space)
+    done = False
+    while not done:
+        action = agent.act(exploit=False)
+        action_result = env.step(action)
+        agent.observe(action_result)
+        agent.learn()
+        done = action_result.done
+
+        if custom:
+            img = env.env.render()
+            cv2.imshow("image", img[:, :, ::-1])
+            cv2.waitKey(1)
diff --git a/cooperative_cuisine/state_representation.py b/cooperative_cuisine/state_representation.py
index 277be355dcf54fc66cc51d9a923ced451d37f897..209bf7d98ad3157f093071b23a479fd89563a637 100644
--- a/cooperative_cuisine/state_representation.py
+++ b/cooperative_cuisine/state_representation.py
@@ -6,6 +6,10 @@ from datetime import datetime
 from enum import Enum
 from typing import Any
 
+import networkx
+import numpy as np
+import numpy.typing as npt
+from networkx import Graph
 from pydantic import BaseModel
 from typing_extensions import Literal, TypedDict
 
@@ -186,6 +190,90 @@ class StateRepresentation(BaseModel):
     """Added by the game server, indicate if all players are ready and actions are passed to the environment."""
 
 
+def astar_heuristic(x, y):
+    """Heuristic distance function used in astar algorithm."""
+    return np.linalg.norm(np.array(x) - np.array(y))
+
+
+def create_movement_graph(state: StateRepresentation, diagonal=True) -> Graph:
+    """
+    Creates a graph which represents the connections of empty kitchen tiles and such
+    possible coarse movements of an agent.
+    Args:
+        state: State representation to determine the graph to.
+        diagonal: if True use 8 way connection, i.e. diagonal connections between the spaces.
+
+    Returns: Graph representing the connections between empty kitchen tiles.
+    """
+    width, height = state["kitchen"]["width"], state["kitchen"]["height"]
+    free_space = np.ones((width, height), dtype=bool)
+    for counter in state["counters"]:
+        grid_idx = np.array(counter["pos"]).round().astype(int)
+        free_space[grid_idx[0], grid_idx[1]] = False
+
+    graph = networkx.Graph()
+    for i in range(width):
+        for j in range(height):
+            if free_space[i, j]:
+                graph.add_node((i, j))
+
+                if diagonal:
+                    for di in range(-1, 2):
+                        for dj in range(-1, 2):
+                            x, y = i + di, j + dj
+                            if (
+                                0 <= x < width
+                                and 0 < y < height
+                                and free_space[x, y]
+                                and (di, dj) != (0, 0)
+                            ):
+                                if np.sum(np.abs(np.array([di, dj]))) == 2:
+                                    if free_space[i + di, j] and free_space[i, j + dj]:
+                                        graph.add_edge(
+                                            (i, j),
+                                            (x, y),
+                                            weight=np.linalg.norm(
+                                                np.array([i - x, j - y])
+                                            ),
+                                        )
+                                else:
+                                    graph.add_edge(
+                                        (i, j),
+                                        (x, y),
+                                        weight=np.linalg.norm(np.array([i - x, j - y])),
+                                    )
+                else:
+                    for x, y in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
+                        if 0 <= x < width and 0 <= y < height and free_space[x, y]:
+                            graph.add_edge(
+                                (i, j),
+                                (x, y),
+                                weight=1,
+                            )
+    return graph
+
+
+def restrict_movement_graph(
+    graph: Graph,
+    player_positions: list[tuple[float, float] | list[float]] | npt.NDArray[float],
+) -> Graph:
+    """Modifies a given movement graph. Removed the nodes of spaces on which players stand.
+
+    Args:
+        graph: The graph to modify.
+        player_positions: Positions of players.
+
+    Returns: The modified graph without nodes where players stand.
+
+    """
+    copied = graph.copy()
+    for pos in player_positions:
+        tup = tuple(np.array(pos).round().astype(int))
+        if tup in copied.nodes.keys():
+            copied.remove_node(tup)
+    return copied
+
+
 def create_json_schema() -> dict[str, Any]:
     """Create a json scheme of the state representation of an environment."""
     return StateRepresentation.model_json_schema()
diff --git a/cooperative_cuisine/study_server.py b/cooperative_cuisine/study_server.py
index 55ab0275c72f08724a32f525014d258fcbff181b..27896e71b25d73424f49e90a8fe6aa6ddd5afd23 100644
--- a/cooperative_cuisine/study_server.py
+++ b/cooperative_cuisine/study_server.py
@@ -18,6 +18,7 @@ import os
 import random
 import signal
 import subprocess
+import uuid
 from pathlib import Path
 from subprocess import Popen
 from typing import Tuple, Any
@@ -38,6 +39,7 @@ from cooperative_cuisine.utils import (
     expand_path,
     add_study_arguments,
     deep_update,
+    UUID_CUTOFF,
 )
 
 log = logging.getLogger(__name__)
@@ -108,6 +110,9 @@ class Study:
         with open(study_config_path, "r") as file:
             env_config_f = file.read()
 
+        self.study_id = uuid.uuid4().hex[:UUID_CUTOFF]
+        """Unique ID of the study."""
+
         self.study_config: StudyConfig = yaml.load(
             str(env_config_f), Loader=yaml.Loader
         )
@@ -208,6 +213,7 @@ class Study:
             environment_config=environment_config,
             layout_config=layout,
             seed=seed,
+            env_name=f"study_{self.study_id}_level_{self.current_level_idx}",
         ).model_dump(mode="json")
 
         env_info = request_game_server(
@@ -547,6 +553,7 @@ class StudyManager:
             environment_config=environment_config,
             layout_config=layout,
             seed=1234567890,
+            env_name="tutorial",
         ).model_dump(mode="json")
         # todo async
         env_info = request_game_server(
@@ -556,7 +563,6 @@ class StudyManager:
         match env_info.status_code:
             case 200:
                 env_info = env_info.json()
-                print("CREATE TUTORIAL:", env_info)
                 study_manager.running_tutorials[participant_id] = env_info
             case 403:
                 raise HTTPException(
diff --git a/cooperative_cuisine/utils.py b/cooperative_cuisine/utils.py
index 9314a01e29c0d7abb23c90323067280460f1057f..88719d54d8fc2fc00cf67d464af0a8c764f9457e 100644
--- a/cooperative_cuisine/utils.py
+++ b/cooperative_cuisine/utils.py
@@ -35,6 +35,9 @@ DEFAULT_SERVER_PORT = 8080
 DEFAULT_GAME_PORT = 8000
 """Default game server port."""
 
+UUID_CUTOFF = 8
+"""The cutoff length for UUIDs."""
+
 
 def expand_path(path: str, env_name: str = "") -> str:
     """Expand a path with VARIABLES to the path variables based on the user's OS or installation location of the Cooperative Cuisine.
@@ -459,7 +462,6 @@ def create_layout_with_counters(w, h) -> str:
             else:
                 string += "_"
         string += "\n"
-    print(string)
     return string
 
 
diff --git a/cooperative_cuisine/validation.py b/cooperative_cuisine/validation.py
index a76dc0335bb7b865514b97d3c1b61f4eaacb1a38..e6b615cb68126dcf457f5a7f5d6d534427bef75c 100644
--- a/cooperative_cuisine/validation.py
+++ b/cooperative_cuisine/validation.py
@@ -1,5 +1,6 @@
 """ Validation of configs and tutorial/guide creation for recipes. """
-
+import hashlib
+import json
 import os
 import warnings
 from typing import TypedDict, Tuple, Iterator, Set
@@ -61,6 +62,8 @@ class Validation:
         """For the available meals for orders."""
         self.do_validation: bool = do_validation
         """A boolean indicating whether to perform validation tasks."""
+        self.recipe_graph_dicts: dict | None = None
+        """A dictionary containing recipe graphs for each meal. For visualisation of the recipes."""
 
     @staticmethod
     def infer_recipe_graph(item_info) -> DiGraph:
@@ -95,7 +98,8 @@ class Validation:
         return graph
 
     def get_meal_graph(self, meal: ItemInfo) -> MealGraphDict:
-        """Create tutorial/guide graphs for each recipe/meal.
+        """Create tutorial/guide graphs for each recipe/meal. Created graphs are cached in a json file
+        because the creation of the graph layout is time-consuming. They are indexed by a hash of the graph edges.
 
         Args:
             meal: An instance of ItemInfo representing the meal to create a graph for.
@@ -155,11 +159,37 @@ class Validation:
                                 current,
                             )
 
-        return {
-            "meal": meal.name,
-            "edges": list(graph.edges),
-            "layout": nx.nx_agraph.graphviz_layout(graph, prog="dot"),
-        }
+        before_hash = "".join(sorted(str(sorted([sorted(i) for i in graph.edges]))))
+        h = hashlib.new("sha1")
+        h.update(before_hash.encode())
+        graph_hash = h.hexdigest()
+        generated_graph_layouts_path = (
+            ROOT_DIR / "generated" / "recipe_graph_layouts.json"
+        )
+        if self.recipe_graph_dicts is None:
+            if not os.path.exists(generated_graph_layouts_path.parent):
+                os.makedirs(generated_graph_layouts_path.parent)
+            if os.path.exists(generated_graph_layouts_path):
+                with open(generated_graph_layouts_path, "r") as f:
+                    self.recipe_graph_dicts = json.loads(f.read())
+            else:
+                self.recipe_graph_dicts = {}
+
+        if graph_hash in self.recipe_graph_dicts.keys():
+            graph_dict = self.recipe_graph_dicts[graph_hash]
+            return graph_dict
+        else:
+            layout = nx.nx_agraph.graphviz_layout(graph, prog="dot")
+            graph_dict = {
+                "meal": meal.name,
+                "edges": list(graph.edges),
+                "layout": layout,
+            }
+            with open(generated_graph_layouts_path, "w") as f:
+                self.recipe_graph_dicts[graph_hash] = graph_dict
+                f.write(json.dumps(self.recipe_graph_dicts, indent=4))
+
+            return graph_dict
 
     def reduce_item_node(self, graph, base_ingredients, item, visited):
         # until now not called
diff --git a/tests/test_start.py b/tests/test_start.py
index 4bb89d7dc54f02f4f091dac84b271b2aa817b420..6fe0c0187b494a7a6bc2c24ef432ab1c5ca8b5e9 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -43,8 +43,11 @@ from cooperative_cuisine.utils import create_init_env_time, get_touching_counter
 
 layouts_folder = ROOT_DIR / "configs" / "layouts"
 environment_config_path = ROOT_DIR / "configs" / "environment_config.yaml"
+environment_config_no_validation_path = (
+    ROOT_DIR / "configs" / "environment_config_no_validation.yaml"
+)
 layout_path = ROOT_DIR / "configs" / "layouts" / "basic.layout"
-layout_empty_path = ROOT_DIR / "configs" / "layouts" / "basic.layout"
+layout_empty_path = ROOT_DIR / "configs" / "layouts" / "empty.layout"
 item_info_path = ROOT_DIR / "configs" / "item_info.yaml"
 
 # TODO: TESTs are in absolute pixel coordinates still.
@@ -54,6 +57,9 @@ item_info_path = ROOT_DIR / "configs" / "item_info.yaml"
 def test_file_availability():
     assert layouts_folder.is_dir(), "layouts folder does not exists"
     assert environment_config_path.is_file(), "environment config file does not exists"
+    assert (
+        environment_config_no_validation_path.is_file()
+    ), "environment config file does not exists"
     assert layout_path.is_file(), "layout config file does not exists"
     assert layout_empty_path.is_file(), "layout empty config file does not exists"
     assert item_info_path.is_file(), "item info config file does not exists"
@@ -67,6 +73,13 @@ def env_config():
     return env_config
 
 
+@pytest.fixture
+def env_config_no_validation():
+    with open(environment_config_no_validation_path, "r") as file:
+        env_config = file.read()
+    return env_config
+
+
 @pytest.fixture
 def layout_config():
     with open(layout_path, "r") as file:
@@ -76,7 +89,7 @@ def layout_config():
 
 @pytest.fixture
 def layout_empty_config():
-    with open(layout_path, "r") as file:
+    with open(layout_empty_path, "r") as file:
         layout = file.read()
     return layout
 
@@ -101,8 +114,10 @@ def test_player_registration(env_config, layout_config, item_info):
         env.add_player("2")
 
 
-def test_movement(env_config, layout_empty_config, item_info):
-    env = Environment(env_config, layout_empty_config, item_info, as_files=False)
+def test_movement(env_config_no_validation, layout_empty_config, item_info):
+    env = Environment(
+        env_config_no_validation, layout_empty_config, item_info, as_files=False
+    )
     player_name = "1"
     start_pos = np.array([3, 4])
     env.add_player(player_name, start_pos)
@@ -122,8 +137,12 @@ def test_movement(env_config, layout_empty_config, item_info):
     ), "Performed movement do not move the player as expected."
 
 
-def test_player_movement_speed(env_config, layout_empty_config, item_info):
-    env = Environment(env_config, layout_empty_config, item_info, as_files=False)
+def test_player_movement_speed(
+    env_config_no_validation, layout_empty_config, item_info
+):
+    env = Environment(
+        env_config_no_validation, layout_empty_config, item_info, as_files=False
+    )
     player_name = "1"
     start_pos = np.array([3, 4])
     env.add_player(player_name, start_pos)
@@ -148,8 +167,10 @@ def test_player_movement_speed(env_config, layout_empty_config, item_info):
     ), "json state does not match expected StateRepresentation."
 
 
-def test_player_reach(env_config, layout_empty_config, item_info):
-    env = Environment(env_config, layout_empty_config, item_info, as_files=False)
+def test_player_reach(env_config_no_validation, layout_empty_config, item_info):
+    env = Environment(
+        env_config_no_validation, layout_empty_config, item_info, as_files=False
+    )
 
     counter_pos = np.array([2, 2])
     counter = Counter(pos=counter_pos, hook=Hooks(env))
diff --git a/tests/test_utils.py b/tests/test_utils.py
index ad9fb6b67b7af5a3a281072cfe59ea838ae9c38e..2802bb3a73f7deea7d1ee169adc0eb132b399096 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,5 +1,15 @@
+import json
 from argparse import ArgumentParser
 
+import networkx
+import pytest
+
+from cooperative_cuisine.environment import Environment
+from cooperative_cuisine.state_representation import (
+    create_movement_graph,
+    restrict_movement_graph,
+    astar_heuristic,
+)
 from cooperative_cuisine.utils import (
     url_and_port_arguments,
     add_list_of_manager_ids_arguments,
@@ -9,6 +19,8 @@ from cooperative_cuisine.utils import (
     create_layout_with_counters,
     setup_logging,
 )
+from tests.test_start import env_config_no_validation
+from tests.test_start import layout_empty_config, item_info
 
 
 def test_parser_gen():
@@ -44,3 +56,92 @@ def test_layout_creation():
 
 def test_setup_logging():
     setup_logging()
+
+
+def test_movement_graph(env_config_no_validation, layout_empty_config, item_info):
+    env = Environment(
+        env_config_no_validation, layout_empty_config, item_info, as_files=False
+    )
+    player_name = "0"
+    env.add_player(player_name)
+
+    state_string = env.get_json_state(player_id=player_name)
+    state = json.loads(state_string)
+    graph_diag = create_movement_graph(state, diagonal=True)
+
+    graph = create_movement_graph(
+        json.loads(env.get_json_state(player_id=player_name)), diagonal=False
+    )
+    path = networkx.astar_path(
+        graph,
+        source=(0, 0),
+        target=(3, 3),
+        heuristic=astar_heuristic,
+    )
+    assert len(path) != 0, "No path found, but should have."
+
+    graph_restricted = restrict_movement_graph(graph_diag, [(1, 0), (0, 1), (1, 1)])
+    with pytest.raises(networkx.exception.NetworkXNoPath) as e_info:
+        path = networkx.astar_path(
+            graph_restricted,
+            source=(0, 0),
+            target=(3, 3),
+            heuristic=astar_heuristic,
+        )
+    with pytest.raises(networkx.exception.NodeNotFound) as e_info:
+        path = networkx.astar_path(
+            graph_restricted,
+            source=(20, 20),
+            target=(40, 40),
+            heuristic=astar_heuristic,
+        )
+
+    path = networkx.astar_path(
+        restrict_movement_graph(
+            graph=graph_diag,
+            player_positions=[],
+        ),
+        source=(0, 0),
+        target=(5, 5),
+        heuristic=astar_heuristic,
+    )
+    assert len(path) != 0, "No path found, but should have."
+
+    # now with diagonal movement
+    graph = create_movement_graph(
+        json.loads(env.get_json_state(player_id=player_name)), diagonal=True
+    )
+    path = networkx.astar_path(
+        graph,
+        source=(0, 0),
+        target=(3, 3),
+        heuristic=astar_heuristic,
+    )
+    assert len(path) != 0, "No path found, but should have."
+
+    graph_restricted = restrict_movement_graph(graph_diag, [(1, 0), (0, 1), (1, 1)])
+    with pytest.raises(networkx.exception.NetworkXNoPath) as e_info:
+        path = networkx.astar_path(
+            graph_restricted,
+            source=(0, 0),
+            target=(3, 3),
+            heuristic=astar_heuristic,
+        )
+    with pytest.raises(networkx.exception.NodeNotFound) as e_info:
+        path = networkx.astar_path(
+            graph_restricted,
+            source=(20, 20),
+            target=(40, 40),
+            heuristic=astar_heuristic,
+        )
+
+    path = networkx.astar_path(
+        restrict_movement_graph(
+            graph=graph_diag,
+            player_positions=[],
+        ),
+        source=(0, 0),
+        target=(5, 5),
+        heuristic=astar_heuristic,
+    )
+    assert len(path) != 0, "No path found, but should have."