diff --git a/overcooked_simulator/__main__.py b/overcooked_simulator/__main__.py
index f48180ac4aa23e525646e844d9e53b9f49a71595..1520c7e666ea6ed3919885240abb174d3b78e4bb 100644
--- a/overcooked_simulator/__main__.py
+++ b/overcooked_simulator/__main__.py
@@ -8,6 +8,8 @@ from overcooked_simulator.utils import (
     add_list_of_manager_ids_arguments,
 )
 
+USE_STUDY_SERVER = True
+
 
 def start_game_server(cli_args):
     from overcooked_simulator.game_server import main
@@ -15,40 +17,98 @@ def start_game_server(cli_args):
     main(cli_args.url, cli_args.port, cli_args.manager_ids)
 
 
+def start_study_server(cli_args):
+    from overcooked_simulator.example_study_server import main
+
+    main(
+        cli_args.url,
+        cli_args.port,
+        game_server_url_="localhost:8000",
+        manager_ids=cli_args.manager_ids,
+    )
+
+
 def start_pygame_gui(cli_args):
     from overcooked_simulator.gui_2d_vis.overcooked_gui import main
 
-    main(cli_args.url, cli_args.port, cli_args.manager_ids)
+    main(
+        cli_args.url,
+        cli_args.port,
+        cli_args.manager_ids,
+        CONNECT_WITH_STUDY_SERVER=USE_STUDY_SERVER,
+    )
 
 
 def main(cli_args=None):
+    study_server = None
+    study_server_args = None
+    if USE_STUDY_SERVER:
+        parser = argparse.ArgumentParser(
+            prog="Overcooked Simulator Study Server",
+            description="Study Server: Match Making, client pre and post managing.",
+            epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+        )
+        url_and_port_arguments(
+            parser=parser, server_name="Study Server", default_port=8080
+        )
+        add_list_of_manager_ids_arguments(parser=parser)
+        study_server_args = parser.parse_args()
+
     if cli_args is None:
         parser = argparse.ArgumentParser(
             prog="Overcooked Simulator",
             description="Game Engine Server + PyGameGUI: Starts overcooked game engine server and a PyGame 2D Visualization window in two processes.",
             epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
         )
-
         url_and_port_arguments(parser)
         disable_websocket_logging_arguments(parser)
         add_list_of_manager_ids_arguments(parser)
 
         cli_args = parser.parse_args()
+
     game_server = None
     pygame_gui = None
     try:
         print("Start game engine:")
         game_server = Process(target=start_game_server, args=(cli_args,))
         game_server.start()
-        time.sleep(1)
+        time.sleep(0.5)
+
+        if USE_STUDY_SERVER:
+            print("Start study server:")
+            study_server = Process(target=start_study_server, args=(study_server_args,))
+            study_server.start()
+            time.sleep(0.5)
+
         print("Start PyGame GUI:")
         pygame_gui = Process(target=start_pygame_gui, args=(cli_args,))
         pygame_gui.start()
-        while pygame_gui.is_alive() and game_server.is_alive():
-            time.sleep(1)
+
+        if USE_STUDY_SERVER:
+            print("Start PyGame GUI:")
+            pygame_gui_2 = Process(target=start_pygame_gui, args=(cli_args,))
+            pygame_gui_2.start()
+
+            print("Start PyGame GUI:")
+            pygame_gui_3 = Process(target=start_pygame_gui, args=(cli_args,))
+            pygame_gui_3.start()
+            while (
+                pygame_gui.is_alive()
+                and pygame_gui_2.is_alive()
+                and pygame_gui_3.is_alive()
+            ):
+                time.sleep(1)
+
+        else:
+            while pygame_gui.is_alive():
+                time.sleep(1)
+
     except KeyboardInterrupt:
         print("Received Keyboard interrupt")
     finally:
+        if USE_STUDY_SERVER and study_server is not None and study_server.is_alive():
+            print("Terminate study server")
+            study_server.terminate()
         if game_server is not None and game_server.is_alive():
             print("Terminate game server")
             game_server.terminate()
diff --git a/overcooked_simulator/counter_factory.py b/overcooked_simulator/counter_factory.py
index 639459a56508c627eb5015aa5b3080bfcd2a1737..a2c91e2cc4dd462e9377d689994a23d952ea9f58 100644
--- a/overcooked_simulator/counter_factory.py
+++ b/overcooked_simulator/counter_factory.py
@@ -195,8 +195,8 @@ class CounterFactory:
 
         assert self.can_map(c), f"Can't map counter char {c}"
         counter_class = None
-        if c == "@":
-            print("-")
+        # 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:
diff --git a/overcooked_simulator/example_study_server.py b/overcooked_simulator/example_study_server.py
index 778135f47054e0cdc4a948f7f3a1098b95542426..663976d3a9638bf70b639a3d7342839e722802e4 100644
--- a/overcooked_simulator/example_study_server.py
+++ b/overcooked_simulator/example_study_server.py
@@ -14,10 +14,12 @@ The environment starts when all players connected.
 import argparse
 import asyncio
 import logging
-from typing import Tuple
+from pathlib import Path
+from typing import Tuple, TypedDict
 
 import requests
 import uvicorn
+import yaml
 from fastapi import FastAPI
 
 from overcooked_simulator import ROOT_DIR
@@ -32,7 +34,6 @@ NUMBER_PLAYER_PER_ENV = 2
 
 log = logging.getLogger(__name__)
 
-
 app = FastAPI()
 
 game_server_url = "localhost:8000"
@@ -53,48 +54,74 @@ server_manager_id = None
 #     </html>
 #     """
 
-running_envs: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {}
-current_free_envs = []
+
+HARDCODED_MANAGER_ID = "1234"
+
+
+running_tutorials: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {}
+
+
+class LevelConfig(TypedDict):
+    name: str
+    config_path: str
+    layout_path: str
+    item_info_path: str
+
+
+class StudyConfig(TypedDict):
+    levels: list[LevelConfig]
+    num_players: int
 
 
-@app.post("/connect_to_game/{request_id}")
-async def want_to_play(request_id: str):
-    global current_free_envs
-    # TODO based on study desing / internal state of request id current state (which level to play)
-    if current_free_envs:
-        current_free_env = current_free_envs.pop()
+class StudyState:
+    def __init__(self, study_config_path: str | Path):
+        with open(study_config_path, "r") as file:
+            env_config_f = file.read()
 
-        running_envs[current_free_env][2].append(request_id)
-        new_running_env = (
-            running_envs[current_free_env][0] + 1,
-            running_envs[current_free_env][1],
-            running_envs[current_free_env][2],
+        self.study_config: StudyConfig = yaml.load(
+            str(env_config_f), Loader=yaml.SafeLoader
         )
-        player_info = running_envs[current_free_env][1][str(new_running_env[0])]
-        running_envs[current_free_env] = new_running_env
-        if new_running_env[0] < NUMBER_PLAYER_PER_ENV - 1:
-            current_free_env.append(current_free_env)
-        return player_info
-    else:
-        environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
-        layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
-        item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
-        with open(item_info_path, "r") as file:
+        self.levels: list[LevelConfig] = self.study_config["levels"]
+        self.current_level_idx: int = 0
+
+        self.participant_id_to_player_info = {}
+        self.player_ids = {}
+        self.num_connected_players: int = 0
+
+        self.current_running_env = None
+        self.next_level_env = None
+        self.players_done = {}
+
+    @property
+    def study_done(self):
+        return self.current_level_idx >= len(self.levels)
+
+    @property
+    def is_full(self):
+        return (
+            len(self.participant_id_to_player_info) == self.study_config["num_players"]
+        )
+
+    def create_env(self, level):
+        with open(ROOT_DIR / "game_content" / level["item_info_path"], "r") as file:
             item_info = file.read()
-        with open(layout_path, "r") as file:
+        with open(
+            ROOT_DIR / "game_content" / "layouts" / level["layout_path"], "r"
+        ) as file:
             layout = file.read()
-        with open(environment_config_path, "r") as file:
+        with open(ROOT_DIR / "game_content" / level["config_path"], "r") as file:
             environment_config = file.read()
+
         creation_json = CreateEnvironmentConfig(
             manager_id=server_manager_id,
-            number_players=NUMBER_PLAYER_PER_ENV,
+            number_players=self.study_config["num_players"],
             environment_settings={"all_player_can_pause_game": False},
             item_info_config=item_info,
             environment_config=environment_config,
             layout_config=layout,
             seed=1234567890,
         ).model_dump(mode="json")
-        # todo async
+
         env_info = requests.post(
             game_server_url + "/manage/create_env/", json=creation_json
         )
@@ -102,16 +129,179 @@ async def want_to_play(request_id: str):
         if env_info.status_code == 403:
             raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
         env_info = env_info.json()
-        print(env_info)
-        running_envs[env_info["env_id"]] = (0, env_info["player_info"], [request_id])
-        current_free_envs.append(env_info["env_id"])
-        return env_info["player_info"]["0"]
 
+        return env_info
+
+    def start(self):
+        level = self.levels[self.current_level_idx]
+        print("CREATING LEVEL:", level["name"])
+        self.current_running_env = self.create_env(level)
+        # if len(self.levels) > 1:
+        # next_level = self.levels[self.current_level_idx]
+        # print("CREATING NEXT LEVEL:", next_level["name"])
+        # self.next_level_env = self.create_env(next_level)
+
+    def next_level(self):
+        requests.post(
+            f"{game_server_url}/manage/stop_env/",
+            json={
+                "manager_id": HARDCODED_MANAGER_ID,
+                "env_id": self.current_running_env["env_id"],
+                "reason": "Next level",
+            },
+        )
+
+        self.current_level_idx += 1
+        if not self.study_done:
+            level = self.levels[self.current_level_idx]
+            print("CREATING LEVEL:", level["name"])
+            self.current_running_env = self.create_env(level)
+            for participant_id, player_id in self.player_ids.items():
+                player_id = self.player_ids[participant_id]
+                self.participant_id_to_player_info[
+                    participant_id
+                ] = self.current_running_env["player_info"][player_id]
+
+            for key in self.players_done:
+                self.players_done[key] = False
+            return False
+        else:
+            return True
+
+    def add_participant(self, participant_id: str):
+        player_name = str(self.num_connected_players)
+        player_info = self.current_running_env["player_info"][player_name]
+        self.participant_id_to_player_info[participant_id] = player_info
+        self.player_ids[participant_id] = player_info["player_id"]
+        self.num_connected_players += 1
+        return player_info
+
+    def player_finished_level(self, participant_id):
+        self.players_done[participant_id] = True
+        level_done = all(self.players_done.values())
+        if level_done:
+            self.next_level()
+        return {"study_finished": self.study_done}
+
+    def get_connection(self, participant_id: str):
+        player_info = self.participant_id_to_player_info[participant_id]
+        return player_info
+
+
+class StudyManager:
+    def __init__(self):
+        self.running_studies: list[StudyState] = []
 
-def main(host, port, game_server_url_, manager_id):
+        self.participant_id_to_study_map: dict[str, StudyState] = {}
+        self.running_envs: dict[str, Tuple[int, dict[str, PlayerInfo], list[str]]] = {}
+        self.current_free_envs = []
+
+    def create_study(self):
+        study = StudyState(ROOT_DIR / "game_content" / "study_config.yaml")
+        study.start()
+        self.running_studies.append(study)
+
+    def add_participant(self, participant_id):
+        player_info = None
+        if all([s.is_full for s in self.running_studies]):
+            self.create_study()
+
+        for study in self.running_studies:
+            if not study.is_full:
+                player_info = study.add_participant(participant_id)
+                self.participant_id_to_study_map[participant_id] = study
+        return player_info
+
+    def player_finished_level(self, participant_id: str):
+        assigned_study = self.participant_id_to_study_map[participant_id]
+        return assigned_study.player_finished_level(participant_id)
+
+    def get_participant_game_connection(self, participant_id: str):
+        assigned_study = self.participant_id_to_study_map[participant_id]
+        return assigned_study.participant_id_to_player_info[participant_id]
+
+
+study_manager = StudyManager()
+
+
+@app.post("/start_study/{participant_id}")
+async def start_study(participant_id: str):
+    player_info = study_manager.add_participant(participant_id)
+    print()
+    return player_info
+
+
+@app.post("/level_done/{participant_id}")
+async def next_level(participant_id: str):
+    is_there_next_level = study_manager.player_finished_level(participant_id)
+    return is_there_next_level
+
+
+@app.post("/get_game_connection/{participant_id}")
+async def finished_game(participant_id: str):
+    return study_manager.get_participant_game_connection(participant_id)
+
+
+@app.post("/finished_game/{participant_id}")
+async def finished_game(participant_id: str):
+    print(f"{participant_id} finished game.")
+    return None
+
+
+@app.post("/connect_to_tutorial/{participant_id}")
+async def want_to_play_tutorial(participant_id: str):
+    environment_config_path = (
+        ROOT_DIR / "game_content" / "tutorial" / "tutorial_env_config.yaml"
+    )
+    layout_path = ROOT_DIR / "game_content" / "tutorial" / "tutorial.layout"
+    item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
+    with open(item_info_path, "r") as file:
+        item_info = file.read()
+    with open(layout_path, "r") as file:
+        layout = file.read()
+    with open(environment_config_path, "r") as file:
+        environment_config = file.read()
+    creation_json = CreateEnvironmentConfig(
+        manager_id=server_manager_id,
+        number_players=1,
+        environment_settings={"all_player_can_pause_game": False},
+        item_info_config=item_info,
+        environment_config=environment_config,
+        layout_config=layout,
+        seed=1234567890,
+    ).model_dump(mode="json")
+    # todo async
+    print("CREATING TUTORIAL ENVIRONMENT")
+    env_info = requests.post(
+        game_server_url + "/manage/create_env/", json=creation_json
+    )
+
+    if env_info.status_code == 403:
+        raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
+    env_info = env_info.json()
+    running_tutorials[participant_id] = env_info
+    return env_info["player_info"]["0"]
+
+
+@app.post("/disconnect_from_tutorial/{participant_id}")
+async def want_to_play_tutorial(participant_id: str):
+    requests.post(
+        f"{game_server_url}/manage/stop_env/",
+        json={
+            "manager_id": HARDCODED_MANAGER_ID,
+            "env_id": running_tutorials[participant_id]["env_id"],
+            "reason": "Finished tutorial",
+        },
+    )
+
+
+def main(host, port, game_server_url_, manager_ids):
     global game_server_url, server_manager_id
+
+    manager_ids = ["1234"]
+
     game_server_url = "http://" + game_server_url_
-    server_manager_id = manager_id[0]
+    server_manager_id = manager_ids[0]
     print(f"Use {server_manager_id=} for {game_server_url=}")
     loop = asyncio.new_event_loop()
     config = uvicorn.Config(app, host=host, port=port, loop=loop)
@@ -132,5 +322,5 @@ if __name__ == "__main__":
         args.url,
         args.port,
         game_server_url_="localhost:8000",
-        manager_id=args.manager_ids,
+        manager_ids=args.manager_ids,
     )
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
index 00716059a6b64f9aba3a7ebb2fc00b541582d04f..11a443d00780a210bf368f4fcd00e8a66c3d8559 100644
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ b/overcooked_simulator/game_content/environment_config.yaml
@@ -5,7 +5,7 @@ plates:
   # range of seconds until the dirty plate arrives.
 
 game:
-  time_limit_seconds: 300
+  time_limit_seconds: 12
 
 meals:
   all: true
diff --git a/overcooked_simulator/game_content/environment_config_dark.yaml b/overcooked_simulator/game_content/environment_config_dark.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..b11ae4fdda0647e4cf772698dc961b0659e309d2
--- /dev/null
+++ b/overcooked_simulator/game_content/environment_config_dark.yaml
@@ -0,0 +1,157 @@
+plates:
+  clean_plates: 1
+  dirty_plates: 2
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 20
+
+meals:
+  all: true
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - TomatoSoup
+    - OnionSoup
+    - Salad
+
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: ServingWindow
+  S: Sink
+  +: SinkAddon
+  at: Plate  # @ just a clean plate on a counter
+  U: Pot  # with Stove
+  Q: Pan  # with Stove
+  O: Peel  # with Oven
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  G: Sausage  # sausaGe
+  B: Bun
+  M: Meat
+  question: Counter  # ? mushroom
+  ↓: Counter
+  ^: Counter
+  right: Counter
+  left: Counter
+  wave: Free  # ~ Water
+  minus: Free  # - Ice
+  dquote: Counter  # " wall/truck
+  p: Counter # second plate return ??
+
+
+orders:
+  order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 6
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+    score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func ''
+    score_calc_gen_kwargs:
+      # the kwargs for the score_calc_gen_func
+      other: 20
+      scores:
+        Burger: 15
+        OnionSoup: 10
+        Salad: 5
+        TomatoSoup: 10
+    expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
+    expired_penalty_kwargs:
+      default: -5
+  serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score ''
+  # a func that calcs a store for not ordered but served meals. Input: meal
+  penalty_for_trash: !!python/name:overcooked_simulator.order.penalty_for_each_item ''
+  # a func that calcs the penalty for items that the player puts into the trashcan.
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 6
+  interaction_range: 1.6
+  restricted_view: True
+  view_angle: 70
+  view_range: 4.5  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:overcooked_simulator.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  #  json_states:
+  #    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+  #      log_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+#  info_msg:
+#    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+#    kwargs:
+#      hooks: [ cutting_board_100 ]
+#      log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Glückwunsch du hast was geschnitten!
+#  fire_msg:
+#    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+#    kwargs:
+#      hooks: [ new_fire ]
+#      log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Feuer, Feuer, Feuer
+#        level: Warning
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/large_t.layout b/overcooked_simulator/game_content/layouts/large_t.layout
index 304e6f7746f4e0c7510f69395b55c7f691de84f6..de56d63203bcd0740d8663c1a945025704afecf9 100644
--- a/overcooked_simulator/game_content/layouts/large_t.layout
+++ b/overcooked_simulator/game_content/layouts/large_t.layout
@@ -4,7 +4,7 @@
 #____________________________________#
 #____________________________________#
 #____________________________________K
-W____________________________________I
+$____________________________________I
 #____________________________________#
 #____________________________________#
 #__A_____A___________________________D
diff --git a/overcooked_simulator/game_content/layouts/godot_test_layout.layout b/overcooked_simulator/game_content/layouts/test_layouts/godot_test_layout.layout
similarity index 100%
rename from overcooked_simulator/game_content/layouts/godot_test_layout.layout
rename to overcooked_simulator/game_content/layouts/test_layouts/godot_test_layout.layout
diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test1.layout b/overcooked_simulator/game_content/layouts/test_layouts/test1.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8ccd0a1152692fd1c9a0a911b0bb677bab98cb95
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/test_layouts/test1.layout
@@ -0,0 +1 @@
+____A___P
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test2.layout b/overcooked_simulator/game_content/layouts/test_layouts/test2.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8cfae98af3e7748e2df2dcdaae2ffffbe8c4a074
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/test_layouts/test2.layout
@@ -0,0 +1,9 @@
+_
+_
+_
+A
+_
+_
+_
+_
+P
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test3.layout b/overcooked_simulator/game_content/layouts/test_layouts/test3.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8847c681b18500df23abe068dac76fb4dfbdd6d2
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/test_layouts/test3.layout
@@ -0,0 +1,4 @@
+___
+_A_
+___
+__P
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/test_layouts/test4.layout b/overcooked_simulator/game_content/layouts/test_layouts/test4.layout
new file mode 100644
index 0000000000000000000000000000000000000000..09d7551fa64358eb61a97d4c562617dca82b9655
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/test_layouts/test4.layout
@@ -0,0 +1,3 @@
+____
+_A__
+___P
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/study_config.yaml b/overcooked_simulator/game_content/study_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..226b16638b0837b0c511fc80794969aa21ba5850
--- /dev/null
+++ b/overcooked_simulator/game_content/study_config.yaml
@@ -0,0 +1,23 @@
+levels:
+  - config_path: environment_config.yaml
+    layout_path: overcooked-1/1-1-far-apart.layout
+    item_info_path: item_info.yaml
+    name: "Level 1-1: Far Apart"
+
+  - config_path: environment_config.yaml
+    layout_path: overcooked-1/1-4-bottleneck.layout
+    item_info_path: item_info.yaml
+    name: "Level 1-4: Bottleneck"
+
+  #  - config_path: environment_config.yaml
+  #    layout_path: overcooked-1/1-5-circle.layout
+  #    item_info_path: item_info.yaml
+  #    name: "Level 1-5: Circle"
+
+  - config_path: environment_config_dark.yaml
+    layout_path: overcooked-1/4-2-dark.layout
+    item_info_path: item_info.yaml
+    name: "Level 4-2: Dark"
+
+
+num_players: 3
diff --git a/overcooked_simulator/game_content/tutorial/tutorial.layout b/overcooked_simulator/game_content/tutorial/tutorial.layout
new file mode 100644
index 0000000000000000000000000000000000000000..8f4cf0f8ffa67a79836f4f0e69bbb0f2ec60410e
--- /dev/null
+++ b/overcooked_simulator/game_content/tutorial/tutorial.layout
@@ -0,0 +1,5 @@
+#C##
+#__X
+T__#
+L__$
+#P##
diff --git a/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml b/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2d03eb20e13515ff609bdfdc7bc69e9c11363eaf
--- /dev/null
+++ b/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml
@@ -0,0 +1,157 @@
+plates:
+  clean_plates: 2
+  dirty_plates: 0
+  plate_delay: [ 5, 10 ]
+  # range of seconds until the dirty plate arrives.
+
+game:
+  time_limit_seconds: 90000
+
+meals:
+  all: true
+  # if all: false -> only orders for these meals are generated
+  # TODO: what if this list is empty?
+  list:
+    - TomatoSoup
+    - OnionSoup
+    - Salad
+
+layout_chars:
+  _: Free
+  hash: Counter  # #
+  A: Agent
+  pipe: Extinguisher
+  P: PlateDispenser
+  C: CuttingBoard
+  X: Trashcan
+  $: ServingWindow
+  S: Sink
+  +: SinkAddon
+  at: Plate  # @ just a clean plate on a counter
+  U: Pot  # with Stove
+  Q: Pan  # with Stove
+  O: Peel  # with Oven
+  F: Basket  # with DeepFryer
+  T: Tomato
+  N: Onion  # oNioN
+  L: Lettuce
+  K: Potato  # Kartoffel
+  I: Fish  # fIIIsh
+  D: Dough
+  E: Cheese  # chEEEse
+  G: Sausage  # sausaGe
+  B: Bun
+  M: Meat
+  question: Counter  # ? mushroom
+  ↓: Counter
+  ^: Counter
+  right: Counter
+  left: Counter
+  wave: Free  # ~ Water
+  minus: Free  # - Ice
+  dquote: Counter  # " wall/truck
+  p: Counter # second plate return ??
+
+
+orders:
+  order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration ''
+  # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py
+  order_gen_kwargs:
+    order_duration_random_func:
+      # how long should the orders be alive
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 40
+        b: 60
+    max_orders: 0
+    # maximum number of active orders at the same time
+    num_start_meals: 2
+    # number of orders generated at the start of the environment
+    sample_on_dur_random_func:
+      # 'random' library call with getattr, kwargs are passed to the function
+      func: uniform
+      kwargs:
+        a: 10
+        b: 20
+    sample_on_serving: false
+    # Sample the delay for the next order only after a meal was served.
+    score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func ''
+    score_calc_gen_kwargs:
+      # the kwargs for the score_calc_gen_func
+      other: 20
+      scores:
+        Burger: 15
+        OnionSoup: 10
+        Salad: 5
+        TomatoSoup: 10
+    expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
+    expired_penalty_kwargs:
+      default: -5
+  serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score ''
+  # a func that calcs a store for not ordered but served meals. Input: meal
+  penalty_for_trash: !!python/name:overcooked_simulator.order.penalty_for_each_item ''
+  # a func that calcs the penalty for items that the player puts into the trashcan.
+
+player_config:
+  radius: 0.4
+  player_speed_units_per_seconds: 6
+  interaction_range: 1.6
+  restricted_view: False
+  view_angle: 70
+  view_range: 5.5  # in grid units, can be "null"
+
+effect_manager:
+  FireManager:
+    class: !!python/name:overcooked_simulator.effect_manager.FireEffectManager ''
+    kwargs:
+      spreading_duration: [ 5, 10 ]
+      fire_burns_ingredients_and_meals: true
+
+
+extra_setup_functions:
+  #  json_states:
+  #    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+  #    kwargs:
+  #      hooks: [ json_state ]
+  #      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+  #      log_class_kwargs:
+  #        log_path: USER_LOG_DIR/ENV_NAME/json_states.jsonl
+  actions:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ pre_perform_action ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+  random_env_events:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ order_duration_sample, plate_out_of_kitchen_time ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+  env_configs:
+    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+    kwargs:
+      hooks: [ env_initialized, item_info_config ]
+      log_class: !!python/name:overcooked_simulator.recording.LogRecorder ''
+      log_class_kwargs:
+        log_path: USER_LOG_DIR/ENV_NAME/LOG_RECORD_NAME.jsonl
+        add_hook_ref: true
+#  info_msg:
+#    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+#    kwargs:
+#      hooks: [ cutting_board_100 ]
+#      log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Glückwunsch du hast was geschnitten!
+#  fire_msg:
+#    func: !!python/name:overcooked_simulator.recording.class_recording_with_hooks ''
+#    kwargs:
+#      hooks: [ new_fire ]
+#      log_class: !!python/name:overcooked_simulator.info_msg.InfoMsgManager ''
+#      log_class_kwargs:
+#        msg: Feuer, Feuer, Feuer
+#        level: Warning
\ No newline at end of file
diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py
index da019095c73fe56a3d58917e5b67db608cd3f2e7..995d579df9208abd26d2f76d18d4b418d59a3685 100644
--- a/overcooked_simulator/game_server.py
+++ b/overcooked_simulator/game_server.py
@@ -36,7 +36,6 @@ from overcooked_simulator.server_results import (
     PlayerRequestResult,
 )
 from overcooked_simulator.utils import (
-    setup_logging,
     url_and_port_arguments,
     add_list_of_manager_ids_arguments,
     disable_websocket_logging_arguments,
@@ -204,6 +203,7 @@ class EnvironmentHandler:
                     env_id=config.env_id,
                     player_id=player_id,
                 )
+                log.debug(f"Added player {player_id} to env {config.env_id}")
         return new_player_info
 
     def start_env(self, env_id: str):
@@ -219,6 +219,7 @@ class EnvironmentHandler:
             self.envs[env_id].start_time = start_time
             self.envs[env_id].last_step_time = time.time_ns()
             self.envs[env_id].environment.reset_env_time()
+            self.envs[env_id].environment.all_players_ready = True
 
     def get_state(
         self, player_hash: str
@@ -300,8 +301,11 @@ class EnvironmentHandler:
             if self.envs[env_id].status != EnvironmentStatus.STOPPED:
                 self.envs[env_id].status = EnvironmentStatus.STOPPED
                 self.envs[env_id].stop_reason = reason
+                log.debug(f"Stopped environment: id={env_id}, reason={reason}")
                 return 0
+            log.debug(f"Could not stop environment: id={env_id}, env is not running")
             return 2
+        log.debug(f"Could not stop environment: id={env_id}, no env with this id")
         return 1
 
     def set_player_ready(self, player_hash) -> bool:
@@ -755,7 +759,9 @@ async def websocket_player_endpoint(websocket: WebSocket, client_id: str):
 def main(
     host: str, port: int, manager_ids: list[str], enable_websocket_logging: bool = False
 ):
-    setup_logging(enable_websocket_logging)
+    manager_ids = ["1234"]
+
+    # setup_logging(enable_websocket_logging)
     loop = asyncio.new_event_loop()
     asyncio.set_event_loop(loop)
     environment_handler.extend_allowed_manager(manager_ids)
diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json
index 8e7e819a989131890f69df5df701b99f810ecb50..9f41fea590a7d36ecb6d2f3a175c46c5500a348d 100644
--- a/overcooked_simulator/gui_2d_vis/gui_theme.json
+++ b/overcooked_simulator/gui_2d_vis/gui_theme.json
@@ -157,5 +157,14 @@
       "bold": 1,
       "colour": "#000000"
     }
+  },
+  "#wait_players_label": {
+    "colours": {
+      "normal_text": "#ff0000"
+    },
+    "font": {
+      "size": 45,
+      "bold": 1
+    }
   }
 }
\ No newline at end of file
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
index ef3d93f75aa5ec48a18cbc6f46582320c104b3ee..2c8d00604ebab4e85be0350a6eb46680069558d5 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -2,11 +2,13 @@ import argparse
 import dataclasses
 import json
 import logging
+import os
 import random
+import signal
+import subprocess
 import sys
 import uuid
 from enum import Enum
-from pathlib import Path
 from subprocess import Popen
 
 import numpy as np
@@ -31,15 +33,15 @@ from overcooked_simulator.utils import (
     url_and_port_arguments,
     disable_websocket_logging_arguments,
     add_list_of_manager_ids_arguments,
-    setup_logging,
 )
 
-CONNECT_WITH_STUDY_SERVER = False
-
 
 class MenuStates(Enum):
     Start = "Start"
+    ControllerTutorial = "ControllerTutorial"
+    PreGame = "PreGame"
     Game = "Game"
+    PostGame = "PostGame"
     End = "End"
 
 
@@ -58,6 +60,7 @@ class PlayerKeySet:
         pickup_key: pygame.key,
         switch_key: pygame.key,
         players: list[int],
+        joystick: int,
     ):
         """Creates a player key set which contains information about which keyboard keys control the player.
 
@@ -69,6 +72,7 @@ class PlayerKeySet:
             pickup_key: The key to pick items up or put them down.
             switch_key: The key for switching through controllable players.
             players: The player indices which this keyset can control.
+            joystick: number of joystick (later check if available)
         """
         self.move_vectors: list[list[int]] = [[-1, 0], [1, 0], [0, -1], [0, 1]]
         self.key_to_movement: dict[pygame.key, list[int]] = {
@@ -82,6 +86,7 @@ class PlayerKeySet:
         self.current_player: int = players[0] if players else 0
         self.current_idx = 0
         self.other_keyset: list[PlayerKeySet] = []
+        self.joystick = joystick
 
     def set_controlled_players(self, controlled_players: list[int]) -> None:
         self.controlled_players = controlled_players
@@ -106,17 +111,23 @@ class PyGameGUI:
         url: str,
         port: int,
         manager_ids: list[str],
+        CONNECT_WITH_STUDY_SERVER: bool,
+        USE_AAAMBOS_AGENT: bool,
     ):
+        self.CONNECT_WITH_STUDY_SERVER = CONNECT_WITH_STUDY_SERVER
+        self.USE_AAAMBOS_AGENT = USE_AAAMBOS_AGENT
+
         pygame.init()
         pygame.display.set_icon(
             pygame.image.load(ROOT_DIR / "gui_2d_vis" / "images" / "fish3.png")
         )
 
+        self.participant_id = uuid.uuid4().hex
+
         self.game_screen: pygame.Surface = None
         self.FPS = 60
         self.running = True
 
-        self.reset_gui_values()
         self.key_sets: list[PlayerKeySet] = []
 
         self.websocket_url = f"ws://{url}:{port}/ws/player/"
@@ -155,61 +166,26 @@ class PyGameGUI:
 
         self.sub_processes = []
 
-        self.beeped_once = False
-
-    def get_window_sizes_from_state(self, state: dict):
-        self.kitchen_width = state["kitchen"]["width"]
-        self.kitchen_height = state["kitchen"]["height"]
-        self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width
-        game_width = self.visualization_config["GameWindow"]["min_width"] - (
-            2 * self.screen_margin
-        )
-        game_height = self.visualization_config["GameWindow"]["min_height"] - (
-            2 * self.screen_margin
+        self.layout_file_paths = sorted(
+            (ROOT_DIR / "game_content" / "layouts").rglob("*.layout")
         )
+        self.current_layout_idx = 0
 
-        if self.kitchen_width > game_width:
-            self.game_height = game_width * self.kitchen_aspect_ratio
-            self.grid_size = game_width / self.kitchen_width
-        else:
-            self.game_width = game_height / self.kitchen_aspect_ratio
-            self.grid_size = game_width / self.kitchen_width
-
-        self.window_width_windowed = self.min_width
-        self.window_height_windowed = self.min_height
-
-    def recalc_game_size(self):
-        log.debug("Resizing game screen")
-        max_width = self.window_width - (2 * self.screen_margin)
-        max_height = self.window_height - (2 * self.screen_margin)
-        if max_width < max_height:
-            self.game_width = max_width
-            self.game_height = max_width * self.kitchen_aspect_ratio
-            self.grid_size = int(self.game_height / self.kitchen_height)
-
-        else:
-            self.game_height = max_height
-            self.game_width = max_height / self.kitchen_aspect_ratio
-            self.grid_size = int(self.game_width / self.kitchen_width)
-
-        self.game_width = max(self.game_width, 100)
-        self.game_height = max(self.game_height, 100)
-        self.grid_size = max(self.grid_size, 1)
-
-        residual_x = self.game_width - (self.kitchen_width * self.grid_size)
-        residual_y = self.game_height - (self.kitchen_height * self.grid_size)
-        self.game_width -= residual_x
-        self.game_height -= residual_y
+        self.beeped_once = False
 
-    def setup_player_keys(self, n=1, disjunct=False):
-        if n:
-            players = list(range(self.number_humans_to_be_added))
+    def setup_player_keys(self, number_players, number_key_sets=1, disjunct=False):
+        # First four keys are for movement. Order: Down, Up, Left, Right.
+        # 5th key is for interacting with counters.
+        # 6th key ist for picking up things or dropping them.
+        if number_key_sets:
+            players = list(range(number_players))
             key_set1 = PlayerKeySet(
                 move_keys=[pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s],
                 interact_key=pygame.K_f,
                 pickup_key=pygame.K_e,
                 switch_key=pygame.K_SPACE,
                 players=players,
+                joystick=0,
             )
             key_set2 = PlayerKeySet(
                 move_keys=[pygame.K_LEFT, pygame.K_RIGHT, pygame.K_UP, pygame.K_DOWN],
@@ -217,19 +193,20 @@ class PyGameGUI:
                 pickup_key=pygame.K_o,
                 switch_key=pygame.K_p,
                 players=players,
+                joystick=1,
             )
             key_sets = [key_set1, key_set2]
 
             if disjunct:
                 key_set1.set_controlled_players(players[::2])
                 key_set2.set_controlled_players(players[1::2])
-            elif n > 1:
+            elif number_key_sets > 1:
                 key_set1.set_controlled_players(players)
                 key_set2.set_controlled_players(players)
                 key_set1.other_keyset = [key_set2]
                 key_set2.other_keyset = [key_set1]
                 key_set2.next_player()
-            return key_sets[:n]
+            return key_sets[:number_key_sets]
         else:
             return []
 
@@ -258,6 +235,47 @@ class PyGameGUI:
                 )
                 self.send_action(action)
 
+    def handle_joy_stick_input(self, joysticks):
+        """Handles joystick inputs for movement every frame
+        Args:
+            joysticks: list of joysticks
+        """
+        # Axis 0: joy stick left: -1 = left, ~0 = center, 1 = right
+        # Axis 1: joy stick left: -1 = up, ~0 = center, 1 = down
+        # see control stuff here (at the end of the page): https://www.pygame.org/docs/ref/joystick.html
+        for key_set in self.key_sets:
+            current_player_name = str(key_set.current_player)
+            # if a joystick is connected for current player
+            if key_set.joystick in joysticks:
+                # Usually axis run in pairs, up/down for one, and left/right for the other. Triggers count as axes.
+                # You may want to take into account some tolerance to handle jitter, and
+                # joystick drift may keep the joystick from centering at 0 or using the full range of position values.
+                tolerance_threshold = 0.2
+                # axis 0 = joy stick left --> left & right
+                axis_left_right = joysticks[key_set.joystick].get_axis(0)
+                axis_up_down = joysticks[key_set.joystick].get_axis(1)
+                if (
+                    abs(axis_left_right) > tolerance_threshold
+                    or abs(axis_up_down) > tolerance_threshold
+                ):
+                    move_vec = np.zeros(2)
+                    if abs(axis_left_right) > tolerance_threshold:
+                        move_vec[0] += axis_left_right
+                    # axis 1 = joy stick right --> up & down
+                    if abs(axis_up_down) > tolerance_threshold:
+                        move_vec[1] += axis_up_down
+
+                    # if np.linalg.norm(move_vec) != 0:
+                    #     move_vec = move_vec / np.linalg.norm(move_vec)
+
+                    action = Action(
+                        current_player_name,
+                        ActionType.MOVEMENT,
+                        move_vec,
+                        duration=self.time_delta,
+                    )
+                    self.send_action(action)
+
     def handle_key_event(self, event):
         """Handles key events for the pickup and interaction keys. Pickup is a single action,
         for interaction keydown and keyup is necessary, because the player has to be able to hold
@@ -270,7 +288,7 @@ class PyGameGUI:
         for key_set in self.key_sets:
             current_player_name = str(key_set.current_player)
             if event.key == key_set.pickup_key and event.type == pygame.KEYDOWN:
-                action = Action(current_player_name, ActionType.PUT, "pickup")
+                action = Action(self.player_id, ActionType.PUT, "pickup")
                 self.send_action(action)
 
             if event.key == key_set.interact_key:
@@ -281,159 +299,146 @@ class PyGameGUI:
                     self.send_action(action)
                 elif event.type == pygame.KEYUP:
                     action = Action(
-                        current_player_name, ActionType.INTERACT, InterActionData.STOP
+                        self.player_id, ActionType.INTERACT, InterActionData.STOP
                     )
                     self.send_action(action)
-            if event.key == key_set.switch_key and not CONNECT_WITH_STUDY_SERVER:
+            if event.key == key_set.switch_key and not self.CONNECT_WITH_STUDY_SERVER:
                 if event.type == pygame.KEYDOWN:
                     key_set.next_player()
 
-    def init_ui_elements(self):
-        self.manager = pygame_gui.UIManager((self.window_width, self.window_height))
-        self.manager.get_theme().load_theme(ROOT_DIR / "gui_2d_vis" / "gui_theme.json")
+    def handle_joy_stick_event(self, event, joysticks):
+        """Handles joy stick events for the pickup and interaction keys. Pickup is a single action,
+        for interaction buttondown and buttonup is necessary, because the player has to be able to hold
+        the button down.
 
-        self.start_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (0, 0), (self.buttons_width, self.buttons_height)
-            ),
-            text="Start Game",
-            manager=self.manager,
-            anchors={"center": "center"},
-        )
-        self.start_button.can_hover()
+        Args:
+            event: Pygame event for extracting the button action.
+            joysticks: list of joysticks
+        """
+        for key_set in self.key_sets:
+            current_player_name = str(key_set.current_player)
+            # if a joystick is connected for current player
+            if key_set.joystick in joysticks:
+                # pickup = Button A <-> 0
+                if (
+                    joysticks[key_set.joystick].get_button(0)
+                    and event.type == pygame.JOYBUTTONDOWN
+                ):
+                    action = Action(current_player_name, ActionType.PUT, "pickup")
+                    self.send_action(action)
+
+                # interact = Button X <-> 2
+                if (
+                    joysticks[key_set.joystick].get_button(2)
+                    and event.type == pygame.JOYBUTTONDOWN
+                ):
+                    action = Action(
+                        current_player_name, ActionType.INTERACT, InterActionData.START
+                    )
+                    self.send_action(action)
+                    # stop interaction if last pressed button was X <-> 2
+                if event.button == 2 and event.type == pygame.JOYBUTTONUP:
+                    action = Action(
+                        current_player_name, ActionType.INTERACT, InterActionData.STOP
+                    )
+                    self.send_action(action)
+                # switch button Y <-> 3
+                if (
+                    joysticks[key_set.joystick].get_button(3)
+                    and not self.CONNECT_WITH_STUDY_SERVER
+                ):
+                    if event.type == pygame.JOYBUTTONDOWN:
+                        key_set.next_player()
+
+    def set_window_size(self):
+        if self.fullscreen:
+            flags = pygame.FULLSCREEN
+            self.window_width = self.window_width_fullscreen
+            self.window_height = self.window_height_fullscreen
+        else:
+            flags = 0
+            self.window_width = self.window_width_windowed
+            self.window_height = self.window_height_windowed
 
-        quit_rect = pygame.Rect(
+        self.main_window = pygame.display.set_mode(
             (
-                0,
-                0,
+                self.window_width,
+                self.window_height,
             ),
-            (self.buttons_width, self.buttons_height),
-        )
-        quit_rect.topright = (0, 0)
-        self.quit_button = pygame_gui.elements.UIButton(
-            relative_rect=quit_rect,
-            text="Quit Game",
-            manager=self.manager,
-            object_id="#quit_button",
-            anchors={"right": "right", "top": "top"},
+            flags=flags,
         )
-        self.quit_button.can_hover()
 
-        fullscreen_button_rect = pygame.Rect(
-            (0, 0), (self.buttons_width * 0.7, self.buttons_height)
-        )
-        fullscreen_button_rect.topright = (-self.buttons_width, 0)
-        self.fullscreen_button = pygame_gui.elements.UIButton(
-            relative_rect=fullscreen_button_rect,
-            text="Fullscreen",
-            manager=self.manager,
-            object_id="#fullscreen_button",
-            anchors={"right": "right", "top": "top"},
-        )
-        self.fullscreen_button.can_hover()
+    def reset_window_size(self):
+        self.game_width = 0
+        self.game_height = 0
+        self.set_window_size()
 
-        reset_button_rect = pygame.Rect((0, 0), (self.screen_margin * 0.75, 50))
-        reset_button_rect.topright = (0, 2 * self.buttons_height)
-        self.reset_button = pygame_gui.elements.UIButton(
-            relative_rect=reset_button_rect,
-            text="RESET",
-            manager=self.manager,
-            object_id="#reset_button",
-            anchors={"right": "right", "top": "top"},
-        )
-        self.reset_button.can_hover()
+    def set_game_size(self, max_width=None, max_height=None):
+        if max_width is None:
+            max_width = self.window_width - (2 * self.screen_margin)
+        if max_height is None:
+            max_height = self.window_height - (2 * self.screen_margin)
 
-        self.finished_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    (self.window_width - self.buttons_width),
-                    (self.window_height - self.buttons_height),
-                ),
-                (self.buttons_width, self.buttons_height),
-            ),
-            text="Finish round",
-            manager=self.manager,
-        )
-        self.finished_button.can_hover()
+        self.kitchen_aspect_ratio = self.kitchen_height / self.kitchen_width
+        if self.kitchen_width > self.kitchen_height:
+            self.game_width = max_width
+            self.game_height = self.game_width * self.kitchen_aspect_ratio
 
-        self.back_button = pygame_gui.elements.UIButton(
-            relative_rect=pygame.Rect(
-                (
-                    (0),
-                    (self.window_height - self.buttons_height),
-                ),
-                (self.buttons_width, self.buttons_height),
-            ),
-            text="Back to menu",
-            manager=self.manager,
-        )
-        self.back_button.can_hover()
+            if self.game_height > max_height:
+                self.game_height = max_height
+                self.game_width = self.game_height / self.kitchen_aspect_ratio
+        else:
+            self.game_height = max_height
+            self.game_width = self.game_height / self.kitchen_aspect_ratio
 
-        self.score_label = pygame_gui.elements.UILabel(
-            text=f"Score: _",
-            relative_rect=pygame.Rect(
-                (
-                    (0),
-                    self.window_height - self.screen_margin,
-                ),
-                (self.screen_margin * 2, self.screen_margin),
-            ),
-            manager=self.manager,
-            object_id="#score_label",
-        )
+            if self.game_width > max_width:
+                self.game_width = max_width
+                self.game_height = self.game_width * self.kitchen_aspect_ratio
 
-        self.layout_file_paths = {
-            str(p.name): p
-            for p in [
-                Path(f)
-                for f in sorted(
-                    (ROOT_DIR / "game_content" / "layouts").rglob("*.layout")
-                )
-            ]
-        }
+        self.grid_size = int(self.game_width / self.kitchen_width)
 
-        assert len(self.layout_file_paths) != 0, "No layout files."
-        dropdown_width, dropdown_height = 200, 40
-        self.layout_selection = pygame_gui.elements.UIDropDownMenu(
-            relative_rect=pygame.Rect(
-                (
-                    0,
-                    0,
-                ),
-                (dropdown_width, dropdown_height),
-            ),
-            manager=self.manager,
-            options_list=list(self.layout_file_paths.keys()),
-            starting_option="basic.layout"
-            if "basic.layout" in self.layout_file_paths
-            else random.choice(list(self.layout_file_paths.keys())),
+        self.game_width = max(self.game_width, 100)
+        self.game_height = max(self.game_height, 100)
+        self.grid_size = max(self.grid_size, 1)
+
+        residual_x = self.game_width - (self.kitchen_width * self.grid_size)
+        residual_y = self.game_height - (self.kitchen_height * self.grid_size)
+        self.game_width -= residual_x
+        self.game_height -= residual_y
+
+        self.game_screen = pygame.Surface(
+            (
+                self.game_width,
+                self.game_height,
+            )
         )
-        self.timer_label = pygame_gui.elements.UILabel(
-            text="GAMETIME",
+
+    def init_ui_elements(self):
+        self.manager = pygame_gui.UIManager((self.window_width, self.window_height))
+        self.manager.get_theme().load_theme(ROOT_DIR / "gui_2d_vis" / "gui_theme.json")
+
+        ########################################################################
+        # Start screen
+        ########################################################################
+        self.start_button = pygame_gui.elements.UIButton(
             relative_rect=pygame.Rect(
-                (self.screen_margin, self.window_height - self.screen_margin),
-                (self.game_width, self.screen_margin),
+                (0, 0), (self.buttons_width, self.buttons_height)
             ),
+            text="Start Game",
             manager=self.manager,
-            object_id="#timer_label",
-        )
-
-        self.orders_label = pygame_gui.elements.UILabel(
-            text="Orders:",
-            relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin),
-            manager=self.manager,
-            object_id="#orders_label",
+            anchors={"center": "center"},
         )
 
-        self.conclusion_label = pygame_gui.elements.UILabel(
-            text="Your final score was _",
-            relative_rect=pygame.Rect(0, 0, self.window_width, self.window_height),
+        rect = pygame.Rect((0, 0), (self.buttons_width, self.buttons_height))
+        rect.topright = (0, 0)
+        self.quit_button = pygame_gui.elements.UIButton(
+            relative_rect=rect,
+            text="Quit Game",
             manager=self.manager,
-            object_id="#score_label",
+            object_id="#quit_button",
+            anchors={"right": "right", "top": "top"},
         )
 
-        #######################
-
         player_selection_rect = pygame.Rect(
             (0, 0),
             (
@@ -468,34 +473,6 @@ class PyGameGUI:
             anchors={"centerx": "centerx", "centery": "centery"},
             object_id="#split_players_button",
         )
-        if self.multiple_keysets:
-            self.split_players_button.show()
-        else:
-            self.split_players_button.hide()
-
-        xbox_controller_button_rect = pygame.Rect((0, 0), (190, 50))
-        xbox_controller_button_rect.right = 0
-        self.xbox_controller_button = pygame_gui.elements.UIButton(
-            relative_rect=xbox_controller_button_rect,
-            manager=self.manager,
-            container=self.player_selection_container,
-            text="Controller?",
-            anchors={"right": "right", "centery": "centery"},
-            object_id="#controller_button",
-        )
-
-        ########
-        #
-        # panel = pygame_gui.elements.UIPanel(
-        #     pygame.Rect((50, 50), (700, 500)),
-        #     manager=manager,
-        #     anchors={
-        #         "left": "left",
-        #         "right": "right",
-        #         "top": "top",
-        #         "bottom": "bottom",
-        #     },
-        # )
 
         players_container_rect = pygame.Rect(
             (0, 0),
@@ -534,7 +511,7 @@ class PyGameGUI:
             manager=self.manager,
             object_id="#number_players_label",
             container=self.player_number_container,
-            text=f"Humans to be added: {self.number_humans_to_be_added}",
+            text=f"Humans to be added: -",
             anchors={"center": "center"},
         )
 
@@ -544,7 +521,7 @@ class PyGameGUI:
             manager=self.manager,
             object_id="#number_bots_label",
             container=self.bot_number_container,
-            text=f"Bots to be added: {self.number_bots_to_be_added}",
+            text=f"Bots to be added: -",
             anchors={"center": "center"},
         )
 
@@ -559,7 +536,6 @@ class PyGameGUI:
             container=self.player_number_container,
             anchors={"right": "right", "centery": "centery"},
         )
-        self.add_human_player_button.can_hover()
 
         remove_player_button_rect = pygame.Rect((0, 0), (size, size))
         remove_player_button_rect.left = 0
@@ -571,7 +547,6 @@ class PyGameGUI:
             container=self.player_number_container,
             anchors={"left": "left", "centery": "centery"},
         )
-        self.remove_human_button.can_hover()
 
         add_bot_button_rect = pygame.Rect((0, 0), (size, size))
         add_bot_button_rect.right = 0
@@ -583,7 +558,6 @@ class PyGameGUI:
             container=self.bot_number_container,
             anchors={"right": "right", "centery": "centery"},
         )
-        self.add_bot_button.can_hover()
 
         remove_bot_button_rect = pygame.Rect((0, 0), (size, size))
         remove_bot_button_rect.left = 0
@@ -595,9 +569,370 @@ class PyGameGUI:
             container=self.bot_number_container,
             anchors={"left": "left", "centery": "centery"},
         )
-        self.remove_bot_button.can_hover()
 
-    def draw(self, state):
+        ########################################################################
+        # Tutorial screen
+        ########################################################################
+
+        image = pygame.image.load(
+            ROOT_DIR / "gui_2d_vis" / "tutorial_files" / "tutorial.drawio.png"
+        ).convert_alpha()
+        image_rect = image.get_rect()
+        image_rect.topleft = (20, self.buttons_height)
+        self.tutorial_image = pygame_gui.elements.UIImage(
+            image_rect,
+            image,
+            manager=self.manager,
+            anchors={"top": "top", "left": "left"},
+        )
+        img_width = self.window_width * 0.8
+        # img_width = img_height * (image_rect.width / image_rect.height)
+        img_height = img_width * (image_rect.height / image_rect.width)
+        new_dims = (img_width, img_height)
+        self.tutorial_image.set_dimensions(new_dims)
+
+        button_rect = pygame.Rect((0, 0), (220, 80))
+        button_rect.bottom = -20
+        self.continue_button = pygame_gui.elements.UIButton(
+            relative_rect=button_rect,
+            text="Continue",
+            manager=self.manager,
+            anchors={"centerx": "centerx", "bottom": "bottom"},
+        )
+
+        fullscreen_button_rect = pygame.Rect(
+            (0, 0), (self.buttons_width * 0.7, self.buttons_height)
+        )
+        fullscreen_button_rect.topright = (-self.buttons_width, 0)
+        self.fullscreen_button = pygame_gui.elements.UIButton(
+            relative_rect=fullscreen_button_rect,
+            text="Fullscreen",
+            manager=self.manager,
+            object_id="#fullscreen_button",
+            anchors={"right": "right", "top": "top"},
+        )
+
+        ########################################################################
+        # PreGame screen
+        ########################################################################
+
+        image = pygame.image.load(
+            ROOT_DIR / "gui_2d_vis" / "tutorial_files" / "recipe_mock.png"
+        ).convert_alpha()
+        image_rect = image.get_rect()
+        image_rect.top = 50
+        self.recipe_image = pygame_gui.elements.UIImage(
+            image_rect,
+            image,
+            manager=self.manager,
+            anchors={"centerx": "centerx", "top": "top"},
+        )
+        img_height = self.window_height * 0.7
+        img_width = img_height * (image_rect.width / image_rect.height)
+        new_dims = (img_width, img_height)
+        self.recipe_image.set_dimensions(new_dims)
+
+        self.level_name = pygame_gui.elements.UILabel(
+            text=f"Next level: {self.layout_file_paths[self.current_layout_idx].stem}",
+            relative_rect=pygame.Rect(
+                (0, 0),
+                (self.window_width * 0.7, self.window_height * 0.2),
+            ),
+            manager=self.manager,
+            object_id="#score_label",
+            anchors={"centerx": "centerx", "top": "top"},
+        )
+
+        ########################################################################
+        # Game screen
+        ########################################################################
+
+        self.finished_button = pygame_gui.elements.UIButton(
+            relative_rect=pygame.Rect(
+                (
+                    (self.window_width - self.buttons_width),
+                    (self.window_height - self.buttons_height),
+                ),
+                (self.buttons_width, self.buttons_height),
+            ),
+            text="Finish round",
+            manager=self.manager,
+        )
+
+        self.orders_label = pygame_gui.elements.UILabel(
+            text="Orders:",
+            relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin),
+            manager=self.manager,
+            object_id="#orders_label",
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width * 0.2, self.buttons_height),
+        )
+        rect.bottomleft = (0, 0)
+        self.score_label = pygame_gui.elements.UILabel(
+            text=f"Score not set",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#score_label",
+            anchors={"bottom": "bottom", "left": "left"},
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width * 0.4, self.buttons_height),
+        )
+        rect.bottom = 0
+        self.timer_label = pygame_gui.elements.UILabel(
+            text="GAMETIME not set",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#timer_label",
+            anchors={"bottom": "bottom", "centerx": "centerx"},
+        )
+
+        rect = pygame.Rect(
+            (0, 0),
+            (self.window_width, self.screen_margin),
+        )
+        rect.right = 20
+        self.wait_players_label = pygame_gui.elements.UILabel(
+            text="WAITING FOR OTHER PLAYERS",
+            relative_rect=rect,
+            manager=self.manager,
+            object_id="#wait_players_label",
+            anchors={"centery": "centery", "right": "right"},
+        )
+
+        ########################################################################
+        # PostGame screen
+        ########################################################################
+
+        conclusion_rect = pygame.Rect(0, 0, self.window_width, self.window_height * 0.4)
+        conclusion_rect.top = 50
+        self.conclusion_label = pygame_gui.elements.UILabel(
+            text="not set",
+            relative_rect=conclusion_rect,
+            manager=self.manager,
+            object_id="#score_label",
+            anchors={"centerx": "centerx", "top": "top"},
+        )
+
+        next_game_button_rect = pygame.Rect((0, 0), (190, 50))
+        next_game_button_rect.center = (self.buttons_width // 2, 200)
+        self.next_game_button = pygame_gui.elements.UIButton(
+            relative_rect=next_game_button_rect,
+            manager=self.manager,
+            text="Next game",
+            anchors={"centerx": "centerx", "centery": "centery"},
+            object_id="#split_players_button",
+        )
+
+        retry_button_rect = pygame.Rect((0, 0), (190, 50))
+        retry_button_rect.center = (self.buttons_width // 2 - 200, 200)
+        self.retry_button = pygame_gui.elements.UIButton(
+            relative_rect=retry_button_rect,
+            manager=self.manager,
+            text="Retry last game",
+            anchors={"center": "center"},
+            object_id="#split_players_button",
+        )
+
+        finish_study_rect = pygame.Rect((0, 0), (190, 50))
+        finish_study_rect.center = (self.buttons_width // 2 + 200, 200)
+        self.finish_study_button = pygame_gui.elements.UIButton(
+            relative_rect=finish_study_rect,
+            manager=self.manager,
+            text="Finish study",
+            anchors={"center": "center"},
+            object_id="#split_players_button",
+        )
+
+        ########################################################################
+        # End screen
+        ########################################################################
+
+        conclusion_rect = pygame.Rect(
+            0, 0, self.window_width * 0.6, self.window_height * 0.4
+        )
+        self.thank_you_label = pygame_gui.elements.UILabel(
+            text="Thank you!",
+            relative_rect=conclusion_rect,
+            manager=self.manager,
+            object_id="#score_label",
+            anchors={"center": "center"},
+        )
+
+        ########################################################################
+
+        self.start_screen_elements = [
+            self.start_button,
+            self.quit_button,
+            self.fullscreen_button,
+            self.player_selection_container,
+        ]
+
+        self.tutorial_screen_elements = [
+            self.tutorial_image,
+            self.continue_button,
+        ]
+
+        self.pregame_screen_elements = [
+            self.recipe_image,
+            self.level_name,
+            self.continue_button,
+        ]
+
+        self.game_screen_elements = [
+            self.orders_label,
+            self.score_label,
+            self.timer_label,
+            self.wait_players_label,
+        ]
+
+        self.postgame_screen_elements = [
+            self.conclusion_label,
+            self.next_game_button,
+        ]
+
+        self.end_screen_elements = [
+            self.fullscreen_button,
+            self.quit_button,
+            self.thank_you_label,
+        ]
+
+        self.rest = [
+            self.fullscreen_button,
+            self.quit_button,
+            self.retry_button,
+            self.finish_study_button,
+            self.finished_button,
+        ]
+
+    def show_screen_elements(self, elements: list):
+        for element in (
+            self.start_screen_elements
+            + self.tutorial_screen_elements
+            + self.pregame_screen_elements
+            + self.game_screen_elements
+            + self.postgame_screen_elements
+            + self.end_screen_elements
+            + self.rest
+        ):
+            element.hide()
+        for element in elements:
+            element.show()
+
+    def update_screen_elements(self):
+        match self.menu_state:
+            case MenuStates.Start:
+                self.show_screen_elements(self.start_screen_elements)
+
+                if self.CONNECT_WITH_STUDY_SERVER:
+                    self.player_selection_container.hide()
+
+                self.update_selection_elements()
+            case MenuStates.ControllerTutorial:
+                self.show_screen_elements(self.tutorial_screen_elements)
+                self.setup_game(tutorial=True)
+                self.set_game_size(
+                    max_height=self.window_height * 0.3,
+                    max_width=self.window_width * 0.3,
+                )
+                self.set_window_size()
+                self.game_center = (
+                    self.window_width - self.game_width / 2 - 20,
+                    self.window_height - self.game_height / 2 - 20,
+                )
+            case MenuStates.PreGame:
+                self.show_screen_elements(self.pregame_screen_elements)
+            case MenuStates.Game:
+                self.show_screen_elements(self.game_screen_elements)
+            case MenuStates.PostGame:
+                self.show_screen_elements(self.postgame_screen_elements)
+                if self.study_finished:
+                    self.next_game_button.hide()
+                    self.finish_study_button.show()
+                else:
+                    self.next_game_button.show()
+                    self.finish_study_button.hide()
+            case MenuStates.End:
+                self.show_screen_elements(self.end_screen_elements)
+
+    def draw_main_window(self):
+        self.main_window.fill(
+            colors[self.visualization_config["GameWindow"]["background_color"]]
+        )
+
+        match self.menu_state:
+            case MenuStates.Start:
+                pass
+            case MenuStates.ControllerTutorial:
+                self.draw_tutorial_screen_frame()
+            case MenuStates.Game:
+                self.draw_game_screen_frame()
+            case MenuStates.PostGame:
+                self.update_conclusion_label(self.last_state)
+
+        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,
+            [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 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["play_beep"]:
+            self.beeped_once = True
+            self.play_bell_sound()
+
+        if self.last_state["ended"]:
+            self.menu_state = MenuStates.PostGame
+            self.finished_button_press()
+            self.disconnect_websockets()
+
+            if self.CONNECT_WITH_STUDY_SERVER:
+                self.send_level_done()
+
+            self.update_screen_elements()
+
+        else:
+            self.draw_game(self.last_state)
+
+            game_screen_rect = self.game_screen.get_rect()
+
+            game_screen_rect.center = [
+                self.window_width // 2,
+                self.window_height // 2,
+            ]
+
+            self.main_window.blit(self.game_screen, game_screen_rect)
+
+            if not self.last_state["all_players_ready"]:
+                self.wait_players_label.show()
+            else:
+                self.wait_players_label.hide()
+
+    def draw_game(self, state):
         """Main visualization function.
 
         Args:            state: The game state returned by the environment."""
@@ -608,9 +943,6 @@ class PyGameGUI:
             [k.current_player for k in self.key_sets],
         )
 
-        # self.manager.draw_ui(self.main_window)
-        self.update_remaining_time(state["remaining_time"])
-
         self.vis.draw_orders(
             screen=self.main_window,
             state=state,
@@ -636,6 +968,7 @@ class PyGameGUI:
         )
 
         self.update_score_label(state)
+        self.update_remaining_time(state["remaining_time"])
 
         if state["info_msg"]:
             for idx, msg in enumerate(reversed(state["info_msg"])):
@@ -655,80 +988,6 @@ class PyGameGUI:
                     ),
                 )
 
-    def set_window_size(self):
-        self.game_screen = pygame.Surface(
-            (
-                self.game_width,
-                self.game_height,
-            )
-        )
-
-        if self.fullscreen:
-            flags = pygame.FULLSCREEN
-            self.window_width = self.window_width_fullscreen
-            self.window_height = self.window_height_fullscreen
-        else:
-            flags = 0
-            self.window_width = self.window_width_windowed
-            self.window_height = self.window_height_windowed
-
-        self.main_window = pygame.display.set_mode(
-            (
-                self.window_width,
-                self.window_height,
-            ),
-            flags=flags,
-            display=0,
-        )
-
-    def reset_window_size(self):
-        self.game_width = 0
-        self.game_height = 0
-        self.set_window_size()
-        self.init_ui_elements()
-
-    def manage_button_visibility(self):
-        match self.menu_state:
-            case MenuStates.Start:
-                self.back_button.hide()
-                self.quit_button.show()
-                self.start_button.show()
-                self.reset_button.hide()
-                self.score_label.hide()
-                self.finished_button.hide()
-                self.layout_selection.show()
-                self.timer_label.hide()
-                self.orders_label.hide()
-                self.conclusion_label.hide()
-
-                self.player_selection_container.show()
-            case MenuStates.Game:
-                self.start_button.hide()
-                self.back_button.hide()
-                self.score_label.show()
-                self.reset_button.show()
-                self.score_label.show()
-                self.finished_button.show()
-                self.layout_selection.hide()
-                self.timer_label.show()
-                self.orders_label.show()
-                self.conclusion_label.hide()
-
-                self.player_selection_container.hide()
-
-            case MenuStates.End:
-                self.start_button.hide()
-                self.back_button.show()
-                self.score_label.hide()
-                self.reset_button.hide()
-                self.finished_button.hide()
-                self.layout_selection.hide()
-                self.timer_label.hide()
-                self.orders_label.hide()
-                self.conclusion_label.show()
-
-                self.player_selection_container.hide()
-
     def update_score_label(self, state):
         score = state["score"]
         self.score_label.set_text(f"Score {score}")
@@ -743,52 +1002,109 @@ class PyGameGUI:
         display_time = f"{minutes}:{'%02d' % seconds}"
         self.timer_label.set_text(f"Time remaining: {display_time}")
 
-    def setup_environment(self):
-        if CONNECT_WITH_STUDY_SERVER:
+    def create_env_on_game_server(self, tutorial):
+        if tutorial:
+            layout_path = ROOT_DIR / "game_content" / "tutorial" / "tutorial.layout"
+            environment_config_path = (
+                ROOT_DIR / "game_content" / "tutorial" / "tutorial_env_config.yaml"
+            )
+        else:
+            environment_config_path = (
+                ROOT_DIR / "game_content" / "environment_config.yaml"
+            )
+            layout_path = self.layout_file_paths[self.current_layout_idx]
+
+        item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
+        with open(item_info_path, "r") as file:
+            item_info = file.read()
+        with open(layout_path, "r") as file:
+            layout = file.read()
+        with open(environment_config_path, "r") as file:
+            environment_config = file.read()
+
+        num_players = 1 if tutorial else self.number_players
+        seed = 161616161616
+        creation_json = CreateEnvironmentConfig(
+            manager_id=self.manager_id,
+            number_players=num_players,
+            environment_settings={"all_player_can_pause_game": False},
+            item_info_config=item_info,
+            environment_config=environment_config,
+            layout_config=layout,
+            seed=seed,
+        ).model_dump(mode="json")
+
+        # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
+        env_info = requests.post(
+            f"{self.request_url}/manage/create_env/",
+            json=creation_json,
+        )
+        if env_info.status_code == 403:
+            raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
+        env_info = env_info.json()
+        assert isinstance(env_info, dict), "Env info must be a dictionary"
+        self.current_env_id = env_info["env_id"]
+        self.player_info = env_info["player_info"]
+        if tutorial:
+            self.player_id = str(list(self.player_info.keys())[0])
+
+    def get_game_connection(self):
+        if self.menu_state == MenuStates.ControllerTutorial:
             self.player_info = requests.post(
-                f"http://localhost:8080/connect_to_game/{uuid.uuid4().hex}"
+                f"http://localhost:8080/connect_to_tutorial/{self.participant_id}"
             ).json()
             self.key_sets[0].current_player = int(self.player_info["player_id"])
+            self.player_id = self.player_info["player_id"]
             self.player_info = {self.player_info["player_id"]: self.player_info}
         else:
-            environment_config_path = (
-                ROOT_DIR / "game_content" / "environment_config.yaml"
+            self.player_info = requests.post(
+                f"http://localhost:8080/get_game_connection/{self.participant_id}"
+            ).json()
+            self.key_sets[0].current_player = int(self.player_info["player_id"])
+            self.player_id = self.player_info["player_id"]
+            self.player_info = {self.player_info["player_id"]: self.player_info}
+
+    def create_and_connect_bot(self, player_id, player_info):
+        player_hash = player_info["player_hash"]
+        print(
+            f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"'
+        )
+        if self.USE_AAAMBOS_AGENT:
+            sub = Popen(
+                " ".join(
+                    [
+                        "exec",
+                        "aaambos",
+                        "run",
+                        "--arch_config",
+                        str(ROOT_DIR / "game_content" / "agents" / "arch_config.yml"),
+                        "--run_config",
+                        str(ROOT_DIR / "game_content" / "agents" / "run_config.yml"),
+                        f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"',
+                        f"--instance={player_hash}",
+                    ]
+                ),
+                shell=True,
             )
-            layout_path = self.layout_file_paths[self.layout_selection.selected_option]
-            item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
-            with open(item_info_path, "r") as file:
-                item_info = file.read()
-            with open(layout_path, "r") as file:
-                layout = file.read()
-            with open(environment_config_path, "r") as file:
-                environment_config = file.read()
-
-            seed = 161616161616
-            creation_json = CreateEnvironmentConfig(
-                manager_id=self.manager_id,
-                number_players=self.number_players,
-                environment_settings={"all_player_can_pause_game": False},
-                item_info_config=item_info,
-                environment_config=environment_config,
-                layout_config=layout,
-                seed=seed,
-            ).model_dump(mode="json")
-
-            # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
-            env_info = requests.post(
-                f"{self.request_url}/manage/create_env/",
-                json=creation_json,
+        else:
+            sub = Popen(
+                " ".join(
+                    [
+                        "python",
+                        str(ROOT_DIR / "game_content" / "agents" / "random_agent.py"),
+                        f'--uri {self.websocket_url + player_info["client_id"]}',
+                        f"--player_hash {player_hash}",
+                        f"--player_id {player_id}",
+                    ]
+                ),
+                shell=True,
             )
-            if env_info.status_code == 403:
-                raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
-            env_info = env_info.json()
-            assert isinstance(env_info, dict), "Env info must be a dictionary"
-            self.current_env_id = env_info["env_id"]
-            self.player_info = env_info["player_info"]
-
-        state = None
+        self.sub_processes.append(sub)
+
+    def connect_websockets(self):
         for p, (player_id, player_info) in enumerate(self.player_info.items()):
             if p < self.number_humans_to_be_added:
+                # add player websockets
                 websocket = connect(self.websocket_url + player_info["client_id"])
                 websocket.send(
                     json.dumps(
@@ -799,147 +1115,95 @@ class PyGameGUI:
                     json.loads(websocket.recv())["status"] == 200
                 ), "not accepted player"
                 self.websockets[player_id] = websocket
+
             else:
-                player_hash = player_info["player_hash"]
-                print(
-                    f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"'
-                )
-                # sub = Popen(
-                #     " ".join(
-                #         [
-                #             "exec",
-                #             "aaambos",
-                #             "run",
-                #             "--arch_config",
-                #             str(
-                #                 ROOT_DIR / "game_content" / "agents" / "arch_config.yml"
-                #             ),
-                #             "--run_config",
-                #             str(
-                #                 ROOT_DIR / "game_content" / "agents" / "run_config.yml"
-                #             ),
-                #             f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"',
-                #             f"--instance={player_hash}",
-                #         ]
-                #     ),
-                #     shell=True,
-                # )
-                sub = Popen(
-                    " ".join(
-                        [
-                            "python",
-                            str(
-                                ROOT_DIR / "game_content" / "agents" / "random_agent.py"
-                            ),
-                            f'--uri {self.websocket_url + player_info["client_id"]}',
-                            f"--player_hash {player_hash}",
-                            f"--player_id {player_id}",
-                        ]
-                    ),
-                    shell=True,
-                )
-                self.sub_processes.append(sub)
+                # create bots and add bot websockets
+                self.create_and_connect_bot(player_id, player_info)
 
-            if p + 1 == self.number_humans_to_be_added:
+            if p == 0:
+                # set initial player_id for requesting the state
                 self.state_player_id = player_id
-                websocket.send(
-                    json.dumps(
-                        {"type": "get_state", "player_hash": player_info["player_hash"]}
-                    )
-                )
-                state = json.loads(websocket.recv())
-
-        if not self.number_humans_to_be_added:
-            player_id = "0"
-            player_info = self.player_info[player_id]
-            websocket = connect(self.websocket_url + player_info["client_id"])
-            websocket.send(
-                json.dumps({"type": "ready", "player_hash": player_info["player_hash"]})
-            )
-            assert json.loads(websocket.recv())["status"] == 200, "not accepted player"
-            self.websockets[player_id] = websocket
-            self.state_player_id = player_id
-            websocket.send(
-                json.dumps(
-                    {"type": "get_state", "player_hash": player_info["player_hash"]}
-                )
-            )
-            state = json.loads(websocket.recv())
-
-        self.get_window_sizes_from_state(state)
+                # websocket.send(
+                #     json.dumps(
+                #         {"type": "get_state", "player_hash": player_info["player_hash"]}
+                #     )
+                # )
+                # state = json.loads(websocket.recv())
 
-    def start_button_press(self):
-        self.menu_state = MenuStates.Game
+        # return state
 
-        self.number_players = (
-            self.number_humans_to_be_added + self.number_bots_to_be_added
-        )
-        self.vis.create_player_colors(self.number_players)
-
-        if self.split_players:
-            assert (
-                self.number_humans_to_be_added > 1
-            ), "Not enough players for key configuration."
-        num_key_set = 2 if self.multiple_keysets else 1
-        self.key_sets = self.setup_player_keys(
-            min(self.number_humans_to_be_added, num_key_set), self.split_players
-        )
+    def setup_game(self, tutorial=False):
+        if tutorial:
+            self.key_sets = self.setup_player_keys(1, 1, False)
+            self.vis.create_player_colors(1)
+        else:
+            self.number_players = (
+                self.number_humans_to_be_added + self.number_bots_to_be_added
+            )
 
-        self.setup_environment()
+            if self.split_players:
+                assert (
+                    self.number_humans_to_be_added > 1
+                ), "Not enough players for key configuration."
+            num_key_set = 2 if self.multiple_keysets else 1
+            self.key_sets = self.setup_player_keys(
+                self.number_humans_to_be_added,
+                min(self.number_humans_to_be_added, num_key_set),
+                self.split_players,
+            )
 
-        self.recalc_game_size()
-        self.set_window_size()
-        self.init_ui_elements()
-        self.beeped_once = False
-        log.debug("Pressed start button")
+        if self.CONNECT_WITH_STUDY_SERVER:
+            self.get_game_connection()
+        else:
+            self.create_env_on_game_server(tutorial)
 
-    def back_button_press(self):
-        self.menu_state = MenuStates.Start
-        self.reset_window_size()
+        self.connect_websockets()
 
-        self.update_selection_elements()
+        state = self.request_state()
 
-        self.beeped_once = False
-        log.debug("Pressed back button")
+        self.vis.create_player_colors(len(state["players"]))
 
-    def quit_button_press(self):
-        self.running = False
-        self.menu_state = MenuStates.Start
-        log.debug("Pressed quit button")
+        self.kitchen_width = state["kitchen"]["width"]
+        self.kitchen_height = state["kitchen"]["height"]
 
-    def reset_button_press(self):
-        # self.reset_gui_values()
-        if not CONNECT_WITH_STUDY_SERVER:
+    def stop_game(self, reason: str) -> None:
+        log.debug(f"Stopping game: {reason}")
+        if not self.CONNECT_WITH_STUDY_SERVER:
             requests.post(
-                f"{self.request_url}/manage/stop_env",
+                f"{self.request_url}/manage/stop_env/",
                 json={
                     "manager_id": self.manager_id,
                     "env_id": self.current_env_id,
-                    "reason": "reset button pressed",
+                    "reason": reason,
                 },
             )
-        self.beeped_once = False
-        # self.websocket.send(json.dumps("reset_game"))
-        # answer = self.websocket.recv()        log.debug("Pressed reset button")
+
+    def send_tutorial_finished(self):
+        requests.post(
+            f"http://localhost:8080/disconnect_from_tutorial/{self.participant_id}",
+        )
 
     def finished_button_press(self):
-        if not CONNECT_WITH_STUDY_SERVER:
-            requests.post(
-                f"{self.request_url}/manage/stop_env/",
-                json={
-                    "manager_id": self.manager_id,
-                    "env_id": self.current_env_id,
-                    "reason": "finish button pressed",
-                },
-            )
-        self.menu_state = MenuStates.End
+        if not self.CONNECT_WITH_STUDY_SERVER:
+            self.stop_game("finished_button_pressed")
+        self.menu_state = MenuStates.PostGame
         self.reset_window_size()
         log.debug("Pressed finished button")
+        self.update_screen_elements()
+
+    def fullscreen_button_press(self):
+        self.fullscreen = not self.fullscreen
+        self.set_window_size()
+        self.init_ui_elements()
+        self.set_game_size()
 
     def reset_gui_values(self):
         self.currently_controlled_player_idx = 0
         self.number_humans_to_be_added = 1
         self.number_bots_to_be_added = 0
+        self.number_players = (
+            self.number_humans_to_be_added + self.number_bots_to_be_added
+        )
         self.split_players = False
         self.multiple_keysets = False
         self.player_minimum = 1
@@ -950,13 +1214,17 @@ class PyGameGUI:
             self.number_humans_to_be_added = self.player_minimum
         else:
             self.remove_human_button.enable()
+
         self.number_humans_to_be_added = max(
             self.player_minimum, self.number_humans_to_be_added
         )
 
+        self.number_players = (
+            self.number_humans_to_be_added + self.number_bots_to_be_added
+        )
+
         text = "WASD+ARROW" if self.multiple_keysets else "WASD"
         self.multiple_keysets_button.set_text(text)
-        # self.split_players_button
         self.added_players_label.set_text(
             f"Humans to be added: {self.number_humans_to_be_added}"
         )
@@ -971,6 +1239,11 @@ class PyGameGUI:
         else:
             self.split_players_button.hide()
 
+        if self.number_players == 0:
+            self.start_button.disable()
+        else:
+            self.start_button.enable()
+
     def send_action(self, action: Action):
         """Sends an action to the game environment.
 
@@ -982,7 +1255,7 @@ class PyGameGUI:
                 float(action.action_data[0]),
                 float(action.action_data[1]),
             ]
-        self.websockets[action.player].send(
+        self.websockets[self.player_id].send(
             json.dumps(
                 {
                     "type": "action",
@@ -993,16 +1266,16 @@ class PyGameGUI:
                 }
             )
         )
-        self.websockets[action.player].recv()
+        self.websockets[self.player_id].recv()
 
     def request_state(self):
         self.websockets[self.state_player_id].send(
             json.dumps(
                 {
                     "type": "get_state",
-                    "player_hash": self.player_info[
-                        str(self.key_sets[0].current_player)
-                    ]["player_hash"],
+                    "player_hash": self.player_info[self.state_player_id][
+                        "player_hash"
+                    ],
                 }
             )
         )
@@ -1012,12 +1285,15 @@ class PyGameGUI:
     def disconnect_websockets(self):
         for sub in self.sub_processes:
             try:
-                sub.kill()
-                # pgrp = os.getpgid(sub.pid)
-                # os.killpg(pgrp, signal.SIGINT)
-                # subprocess.run(
-                #     "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True
-                # )
+                if self.USE_AAAMBOS_AGENT:
+                    pgrp = os.getpgid(sub.pid)
+                    os.killpg(pgrp, signal.SIGINT)
+                    subprocess.run(
+                        "kill $(ps aux | grep 'aaambos' | awk '{print $2}')", shell=True
+                    )
+                else:
+                    sub.kill()
+
             except ProcessLookupError:
                 pass
 
@@ -1033,6 +1309,136 @@ class PyGameGUI:
         mixer.music.play()
         log.log(logging.INFO, "Started game, played bell sound")
 
+    def start_study(self):
+        self.player_info = requests.post(
+            f"http://localhost:8080/start_study/{self.participant_id}"
+        ).json()
+        self.study_finished = False
+
+    def send_level_done(self):
+        answer = requests.post(
+            f"http://localhost:8080/level_done/{self.participant_id}"
+        ).json()
+        self.study_finished = answer["study_finished"]
+        print("\nSTUDY DONE:", self.study_finished, "\n")
+
+    def manage_button_event(self, event):
+        if event.ui_element == self.quit_button:
+            self.running = False
+            self.disconnect_websockets()
+            self.stop_game("Quit button")
+            self.menu_state = MenuStates.Start
+            log.debug("Pressed quit button")
+            return
+
+        elif event.ui_element == self.fullscreen_button:
+            self.fullscreen_button_press()
+            log.debug("Pressed fullscreen button")
+            return
+
+        # Filter by shown screen page
+        match self.menu_state:
+            ############################################
+            case MenuStates.Start:
+                match event.ui_element:
+                    case self.start_button:
+                        if not (
+                            self.number_humans_to_be_added
+                            + self.number_bots_to_be_added
+                        ):
+                            pass
+                        else:
+                            self.menu_state = MenuStates.ControllerTutorial
+
+                    case self.add_human_player_button:
+                        self.number_humans_to_be_added += 1
+                    case self.add_bot_button:
+                        self.number_bots_to_be_added += 1
+
+                    case self.remove_human_button:
+                        self.number_humans_to_be_added = max(
+                            self.player_minimum, self.number_humans_to_be_added - 1
+                        )
+                    case self.remove_bot_button:
+                        self.number_bots_to_be_added = max(
+                            0, self.number_bots_to_be_added - 1
+                        )
+                    case self.multiple_keysets_button:
+                        self.multiple_keysets = not self.multiple_keysets
+                        self.split_players = False
+                    case self.split_players_button:
+                        self.split_players = not self.split_players
+                        if self.split_players:
+                            self.player_minimum = 2
+                        else:
+                            self.player_minimum = 0
+
+            ############################################
+
+            case MenuStates.ControllerTutorial:
+                match event.ui_element:
+                    case self.continue_button:
+                        self.menu_state = MenuStates.PreGame
+
+                        if self.CONNECT_WITH_STUDY_SERVER:
+                            self.send_tutorial_finished()
+                            self.start_study()
+                        else:
+                            self.stop_game("tutorial_finished")
+                        self.disconnect_websockets()
+
+            ############################################
+
+            case MenuStates.PreGame:
+                match event.ui_element:
+                    case self.continue_button:
+                        self.setup_game()
+                        self.set_game_size()
+                        self.menu_state = MenuStates.Game
+
+            ############################################
+
+            case MenuStates.Game:
+                match event.ui_element:
+                    case self.finished_button:
+                        self.menu_state = MenuStates.PostGame
+                        self.disconnect_websockets()
+                        self.finished_button_press()
+                        self.handle_joy_stick_input(joysticks=self.joysticks)
+
+                        if self.CONNECT_WITH_STUDY_SERVER:
+                            self.send_level_done()
+
+            ############################################
+
+            case MenuStates.PostGame:
+                match event.ui_element:
+                    case self.retry_button:
+                        if not self.CONNECT_WITH_STUDY_SERVER:
+                            self.stop_game("Retry button")
+                        self.menu_state = MenuStates.PreGame
+
+                    case self.next_game_button:
+                        if not self.CONNECT_WITH_STUDY_SERVER:
+                            self.current_layout_idx += 1
+                            if self.current_layout_idx == len(self.layout_file_paths):
+                                self.current_layout_idx = 0
+                            else:
+                                log.debug(
+                                    f"LEVEL: {self.layout_file_paths[self.current_layout_idx]}"
+                                )
+                        self.menu_state = MenuStates.PreGame
+
+                    case self.finish_study_button:
+                        self.menu_state = MenuStates.End
+
+            ############################################
+
+            case MenuStates.End:
+                match event.ui_element:
+                    case other:
+                        pass
+
     def start_pygame(self):
         """Starts pygame and the gui loop. Each frame the game state is visualized and keyboard inputs are read."""
         log.debug(f"Starting pygame gui at {self.FPS} fps")
@@ -1044,164 +1450,98 @@ class PyGameGUI:
         clock = pygame.time.Clock()
 
         self.reset_window_size()
-
         self.init_ui_elements()
-        self.manage_button_visibility()
-
-        self.update_selection_elements()
+        self.reset_gui_values()
+        self.update_screen_elements()
 
         # Game loop
         self.running = True
+        # This dict can be left as-is, since pygame will generate a
+        # pygame.JOYDEVICEADDED event for every joystick connected
+        # at the start of the program.
+        self.joysticks = {}
+
         while self.running:
             try:
                 self.time_delta = clock.tick(self.FPS) / 1000
 
-                # print(clock.get_time())
+                # PROCESSING EVENTS
                 for event in pygame.event.get():
                     if event.type == pygame.QUIT:
+                        self.disconnect_websockets()
                         self.running = False
 
-                    # elif event.type == pygame.VIDEORESIZE:
-                    #     # scrsize = event.size
-                    #     self.window_width_windowed = event.w
-                    #     self.window_height_windowed = event.h
-                    #     self.recalc_game_size()
-                    #     self.set_window_size()
-                    #     self.init_ui_elements()
-                    #     self.manage_button_visibility()
-
-                    if event.type == pygame_gui.UI_BUTTON_PRESSED:
-                        match event.ui_element:
-                            case self.start_button:
-                                if not (
-                                    self.number_humans_to_be_added
-                                    + self.number_bots_to_be_added
-                                ):
-                                    continue
-                                self.start_button_press()
-
-                            case self.back_button:
-                                self.back_button_press()
-                                self.disconnect_websockets()
-
-                            case self.finished_button:
-                                self.finished_button_press()
-                                self.disconnect_websockets()
-
-                            case self.quit_button:
-                                self.quit_button_press()
-                                self.disconnect_websockets()
-
-                            case self.reset_button:
-                                self.reset_button_press()
-                                self.disconnect_websockets()
-                                self.start_button_press()
-
-                            case self.add_human_player_button:
-                                self.number_humans_to_be_added += 1
-                            case self.remove_human_button:
-                                self.number_humans_to_be_added = max(
-                                    0, self.number_humans_to_be_added - 1
-                                )
-                            case self.add_bot_button:
-                                self.number_bots_to_be_added += 1
-                            case self.remove_bot_button:
-                                self.number_bots_to_be_added = max(
-                                    0, self.number_bots_to_be_added - 1
-                                )
-                            case self.multiple_keysets_button:
-                                self.multiple_keysets = not self.multiple_keysets
-                                self.split_players = False
-                            case self.split_players_button:
-                                self.split_players = not self.split_players
-                                if self.split_players:
-                                    self.player_minimum = 2
-                                else:
-                                    self.player_minimum = 1
-
-                            case self.xbox_controller_button:
-                                print("xbox_controller_button pressed.")
-
-                            case self.fullscreen_button:
-                                self.fullscreen = not self.fullscreen
-                                if self.fullscreen:
-                                    self.window_width = self.window_width_fullscreen
-                                    self.window_height = self.window_height_fullscreen
-                                else:
-                                    self.window_width = self.window_width_windowed
-                                    self.window_height = self.window_height_windowed
-                                self.recalc_game_size()
-                                self.set_window_size()
-                                self.init_ui_elements()
-
-                        self.update_selection_elements()
-
-                        self.manage_button_visibility()
-
+                    # connect joystick
                     if (
-                        event.type in [pygame.KEYDOWN, pygame.KEYUP]
-                        and self.menu_state == MenuStates.Game
+                        pygame.joystick.get_count() > 0
+                        and event.type == pygame.JOYDEVICEADDED
                     ):
-                        pass
-                        self.handle_key_event(event)
+                        # 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())
 
-                    self.manager.process_events(event)
-
-                    # drawing:
-                self.main_window.fill(
-                    colors[self.visualization_config["GameWindow"]["background_color"]]
-                )
-                self.manager.draw_ui(self.main_window)
-
-                match self.menu_state:
-                    case MenuStates.Start:
-                        pass
-
-                    case MenuStates.Game:
-                        state = self.request_state()
-
-                        if not self.beeped_once and state["play_beep"]:
-                            self.beeped_once = True
-                            self.play_bell_sound()
-
-                        self.handle_keys()
-
-                        if state["ended"]:
-                            self.finished_button_press()
-                            self.disconnect_websockets()
-                            self.manage_button_visibility()
-                        else:
-                            self.draw(state)
-
-                            game_screen_rect = self.game_screen.get_rect()
-
-                            game_screen_rect.center = [
-                                self.window_width // 2,
-                                self.window_height // 2,
-                            ]
+                    if event.type == pygame_gui.UI_BUTTON_PRESSED:
+                        self.manage_button_event(event)
+                        self.update_screen_elements()
+
+                    if event.type in [
+                        pygame.KEYDOWN,
+                        pygame.KEYUP,
+                    ] and self.menu_state in [
+                        MenuStates.Game,
+                        MenuStates.ControllerTutorial,
+                    ]:
+                        self.handle_key_event(event)
 
-                            self.main_window.blit(self.game_screen, game_screen_rect)
+                    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)
 
-                    case MenuStates.End:
-                        self.update_conclusion_label(state)
+                    self.manager.process_events(event)
 
-                self.manager.update(self.time_delta)
-                pygame.display.flip()
+                # DRAWING
+                self.draw_main_window()
 
             except (KeyboardInterrupt, SystemExit):
                 self.running = False
+                self.disconnect_websockets()
+                self.stop_game("Program exited.")
 
         self.disconnect_websockets()
+        self.stop_game("Program exited")
         pygame.quit()
         sys.exit()
 
 
-def main(url: str, port: int, manager_ids: list[str]):
-    setup_logging()
+def main(
+    url: str,
+    port: int,
+    manager_ids: list[str],
+    CONNECT_WITH_STUDY_SERVER=False,
+    USE_AAAMBOS_AGENT=False,
+):
+    manager_ids = ["1234"]
+
+    # setup_logging()
     gui = PyGameGUI(
         url=url,
         port=port,
         manager_ids=manager_ids,
+        CONNECT_WITH_STUDY_SERVER=CONNECT_WITH_STUDY_SERVER,
+        USE_AAAMBOS_AGENT=CONNECT_WITH_STUDY_SERVER,
     )
     gui.start_pygame()
 
diff --git a/overcooked_simulator/gui_2d_vis/tutorial.png b/overcooked_simulator/gui_2d_vis/tutorial.png
new file mode 100644
index 0000000000000000000000000000000000000000..151f8e85b9a76c30df20d334be4deffd8d4e3d6a
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/tutorial.png differ
diff --git a/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png b/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png
new file mode 100644
index 0000000000000000000000000000000000000000..4ac719852dfd2470712a56ee249f86baa7a891d6
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png differ
diff --git a/overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png b/overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png
new file mode 100644
index 0000000000000000000000000000000000000000..8824cce8164089cc251217e21263c75fcd552254
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/tutorial_files/tutorial.drawio.png differ
diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml
index a0d7989c1c1bf9c2a96fcc28e1baf09bcd96e55e..7c664c1c372923ebec5bffb14a21a9656813389b 100644
--- a/overcooked_simulator/gui_2d_vis/visualization.yaml
+++ b/overcooked_simulator/gui_2d_vis/visualization.yaml
@@ -109,7 +109,7 @@ ServingWindow:
     - type: image
       path: images/bell_gold.png
       size: 0.5
-      center_offset: [ -0.4, 0.1 ]
+      center_offset: [ -0.2, -0.05 ]
       rotate_image: False
 
 Stove:
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index f4b511604740e19206f5f339c31fe7b76218c194..3e1759b8d26030a112da2f31906a63c02dd4ef65 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -56,8 +56,6 @@ from overcooked_simulator.utils import create_init_env_time, get_closest
 
 log = logging.getLogger(__name__)
 
-FOG_OF_WAR = True
-
 
 PREVENT_SQUEEZING_INTO_OTHER_PLAYERS = False
 
@@ -296,6 +294,8 @@ class Environment:
             env_start_time_worldtime=datetime.now(),
         )
 
+        self.all_players_ready = False
+
     def overwrite_counters(self, counters):
         self.counters = counters
         self.counter_positions = np.array([c.pos for c in self.counters])
@@ -431,6 +431,8 @@ class Environment:
 
         grid = []
 
+        max_width = 0
+
         lines = list(filter(lambda l: l != "", lines))
         for line in lines:
             line = line.replace(" ", "")
@@ -438,6 +440,7 @@ class Environment:
                 break
             current_x: float = starting_at
             grid_line = []
+
             for character in line:
                 # character = character.capitalize()
                 pos = np.array([current_x, current_y])
@@ -460,10 +463,15 @@ class Environment:
 
                 current_x += 1
 
+            if len(line) >= max_width:
+                max_width = len(line)
+
             grid.append(grid_line)
             current_y += 1
 
-        self.kitchen_width: float = len(lines[0]) + starting_at
+        grid = [line + ([0] * (max_width - len(line))) for line in grid]
+
+        self.kitchen_width: float = max_width + starting_at
         self.kitchen_height = current_y
 
         self.determine_counter_orientations(
@@ -823,6 +831,7 @@ class Environment:
                 "kitchen": {"width": self.kitchen_width, "height": self.kitchen_height},
                 "score": self.order_and_score.score,
                 "orders": self.order_and_score.order_state(),
+                "all_players_ready": self.all_players_ready,
                 "ended": self.game_ended,
                 "env_time": self.env_time.isoformat(),
                 "remaining_time": max(