diff --git a/overcooked_simulator/__main__.py b/overcooked_simulator/__main__.py
index f48180ac4aa23e525646e844d9e53b9f49a71595..668071aebe23ac0f55a8829d8bab3b07e4d3a483 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,92 @@ 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():
+
+        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():
             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/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 9895330f46f1436dfa1d61e9a2ef632a79f16d06..eadb5b3dde08eddc106ffb0b8eb81ccf2aa77280 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: 100
+  time_limit_seconds: 20
 
 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/study_config.yaml b/overcooked_simulator/game_content/study_config.yaml
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..226b16638b0837b0c511fc80794969aa21ba5850 100644
--- a/overcooked_simulator/game_content/study_config.yaml
+++ 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/layouts/tutorial.layout b/overcooked_simulator/game_content/tutorial/tutorial.layout
similarity index 100%
rename from overcooked_simulator/game_content/layouts/tutorial.layout
rename to overcooked_simulator/game_content/tutorial/tutorial.layout
diff --git a/overcooked_simulator/game_content/tutorial_env_config.yaml b/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml
similarity index 99%
rename from overcooked_simulator/game_content/tutorial_env_config.yaml
rename to overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml
index cff3c29bc1aad863e9c1a8c3e57a5bca57b5d7fe..2d03eb20e13515ff609bdfdc7bc69e9c11363eaf 100644
--- a/overcooked_simulator/game_content/tutorial_env_config.yaml
+++ b/overcooked_simulator/game_content/tutorial/tutorial_env_config.yaml
@@ -64,7 +64,7 @@ orders:
       kwargs:
         a: 40
         b: 60
-    max_orders: 6
+    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
diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py
index 84879915dce1ff120d6f7a99e405466aa9f7adce..6a382616542459a44fc2fe5b9609f4b2f2b90b7e 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
@@ -296,8 +297,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:
@@ -751,7 +755,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 772504b2fa21e6aeed410c0164500945d9d42fca..e0a9e1aaeb121fc4469f95c060778df60fea4142 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -32,13 +32,8 @@ 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
-USE_AAAMBOS_AGENT = False
-SHOW_TUTORIAL_GAME = True
-
 
 class MenuStates(Enum):
     Start = "Start"
@@ -112,17 +107,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/"
@@ -166,9 +167,9 @@ class PyGameGUI:
         )
         self.current_layout_idx = 0
 
-    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):
+        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,
@@ -188,13 +189,13 @@ class PyGameGUI:
             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 []
 
@@ -235,7 +236,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:
@@ -246,10 +247,10 @@ 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()
 
@@ -414,7 +415,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"},
         )
 
@@ -424,7 +425,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"},
         )
 
@@ -478,18 +479,19 @@ class PyGameGUI:
         ########################################################################
 
         image = pygame.image.load(
-            ROOT_DIR / "gui_2d_vis" / "tutorial.png"
+            ROOT_DIR / "gui_2d_vis" / "tutorial_files" / "tutorial.drawio.png"
         ).convert_alpha()
         image_rect = image.get_rect()
-        image_rect.topleft = (40, 40)
+        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_height = self.window_height * 0.75
-        img_width = img_height * (image_rect.width / image_rect.height)
+        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)
 
@@ -519,7 +521,7 @@ class PyGameGUI:
         ########################################################################
 
         image = pygame.image.load(
-            ROOT_DIR / "gui_2d_vis" / "recipe_mock.png"
+            ROOT_DIR / "gui_2d_vis" / "tutorial_files" / "recipe_mock.png"
         ).convert_alpha()
         image_rect = image.get_rect()
         image_rect.top = 50
@@ -568,24 +570,43 @@ class PyGameGUI:
             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: _",
-            relative_rect=pygame.Rect(
-                (0, self.window_height - self.screen_margin),
-                (self.screen_margin * 2, self.screen_margin),
-            ),
+            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",
-            relative_rect=pygame.Rect(
-                (self.screen_margin, self.window_height - self.screen_margin),
-                (self.game_width, self.screen_margin),
-            ),
+            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"},
         )
 
         ########################################################################
@@ -659,40 +680,40 @@ class PyGameGUI:
         self.tutorial_screen_elements = [
             self.tutorial_image,
             self.continue_button,
-            self.fullscreen_button,
-            self.quit_button,
         ]
 
         self.pregame_screen_elements = [
             self.recipe_image,
             self.level_name,
             self.continue_button,
-            self.quit_button,
-            self.fullscreen_button,
         ]
 
         self.game_screen_elements = [
-            self.finished_button,
             self.orders_label,
             self.score_label,
-            self.quit_button,
-            self.fullscreen_button,
             self.timer_label,
+            self.wait_players_label,
         ]
+
         self.postgame_screen_elements = [
             self.conclusion_label,
             self.next_game_button,
-            self.fullscreen_button,
-            self.retry_button,
-            self.quit_button,
-            self.finish_study_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
@@ -701,6 +722,7 @@ class PyGameGUI:
             + self.game_screen_elements
             + self.postgame_screen_elements
             + self.end_screen_elements
+            + self.rest
         ):
             element.hide()
         for element in elements:
@@ -710,19 +732,22 @@ class PyGameGUI:
         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.reset_gui_values()
                 self.setup_game(tutorial=True)
                 self.set_game_size(
-                    max_height=self.window_height * 0.4,
-                    max_width=self.window_width * 0.4,
+                    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 - 30,
-                    self.window_height - self.game_height / 2 - 50,
+                    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)
@@ -730,6 +755,12 @@ class PyGameGUI:
                 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)
 
@@ -737,20 +768,18 @@ class PyGameGUI:
         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.ControllerTutorial:
-                if SHOW_TUTORIAL_GAME:
-                    self.draw_tutorial_screen_frame()
+                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()
 
@@ -775,8 +804,15 @@ class PyGameGUI:
         self.handle_keys()
 
         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)
 
@@ -789,6 +825,11 @@ class PyGameGUI:
 
             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.
 
@@ -859,59 +900,109 @@ class PyGameGUI:
         display_time = f"{minutes}:{'%02d' % seconds}"
         self.timer_label.set_text(f"Time remaining: {display_time}")
 
-    def setup_environment(self, tutorial=False):
-        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:
-            if tutorial:
-                layout_path = ROOT_DIR / "game_content" / "layouts" / "tutorial.layout"
-                environment_config_path = (
-                    ROOT_DIR / "game_content" / "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()
-
-            seed = 161616161616
-            creation_json = CreateEnvironmentConfig(
-                manager_id=self.manager_id,
-                number_players=1 if tutorial else 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,
+            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,
+            )
+        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(
@@ -922,133 +1013,60 @@ 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}"'
-                )
-                if 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,
-                    )
-                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,
-                    )
-                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())
+                # websocket.send(
+                #     json.dumps(
+                #         {"type": "get_state", "player_hash": player_info["player_hash"]}
+                #     )
+                # )
+                # state = json.loads(websocket.recv())
 
-        self.kitchen_width = state["kitchen"]["width"]
-        self.kitchen_height = state["kitchen"]["height"]
+        # return state
 
     def setup_game(self, tutorial=False):
-        self.number_players = (
-            self.number_humans_to_be_added + self.number_bots_to_be_added
-        )
-        self.vis.create_player_colors(self.number_players)
-
-        if self.split_players:
-            assert (
-                self.number_humans_to_be_added > 1
-            ), "Not enough players for key configuration."
-        num_key_set = 2 if self.multiple_keysets else 1
-        self.key_sets = self.setup_player_keys(
-            min(self.number_humans_to_be_added, num_key_set), self.split_players
-        )
-
-        self.setup_environment(tutorial=tutorial)
+        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.set_game_size()
-        # self.set_window_size()
+            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,
+            )
 
-    def back_button_press(self):
-        self.menu_state = MenuStates.Start
-        self.reset_window_size()
+        if self.CONNECT_WITH_STUDY_SERVER:
+            self.get_game_connection()
+        else:
+            self.create_env_on_game_server(tutorial)
 
-        self.update_selection_elements()
+        self.connect_websockets()
 
-        log.debug("Pressed back button")
+        state = self.request_state()
 
-    def reset_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": "reset button pressed",
-                },
-            )
+        self.vis.create_player_colors(len(state["players"]))
 
-        # self.websocket.send(json.dumps("reset_game"))
-        # answer = self.websocket.recv()        log.debug("Pressed reset button")
+        self.kitchen_width = state["kitchen"]["width"]
+        self.kitchen_height = state["kitchen"]["height"]
 
     def stop_game(self, reason: str) -> None:
-        self.disconnect_websockets()
         log.debug(f"Stopping game: {reason}")
-        if not CONNECT_WITH_STUDY_SERVER:
+        if not self.CONNECT_WITH_STUDY_SERVER:
             requests.post(
                 f"{self.request_url}/manage/stop_env/",
                 json={
@@ -1058,11 +1076,18 @@ class PyGameGUI:
                 },
             )
 
+    def send_tutorial_finished(self):
+        requests.post(
+            f"http://localhost:8080/disconnect_from_tutorial/{self.participant_id}",
+        )
+
     def finished_button_press(self):
-        self.stop_game("finished_button_pressed")
+        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
@@ -1087,10 +1112,15 @@ 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.added_players_label.set_text(
@@ -1107,6 +1137,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.
 
@@ -1118,7 +1153,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",
@@ -1129,16 +1164,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"
+                    ],
                 }
             )
         )
@@ -1148,7 +1183,7 @@ class PyGameGUI:
     def disconnect_websockets(self):
         for sub in self.sub_processes:
             try:
-                if USE_AAAMBOS_AGENT:
+                if self.USE_AAAMBOS_AGENT:
                     pgrp = os.getpgid(sub.pid)
                     os.killpg(pgrp, signal.SIGINT)
                     subprocess.run(
@@ -1164,16 +1199,32 @@ class PyGameGUI:
         for websocket in self.websockets.values():
             websocket.close()
 
+    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:
@@ -1191,12 +1242,13 @@ class PyGameGUI:
 
                     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(
-                            0, self.number_humans_to_be_added - 1
+                            self.player_minimum, 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
@@ -1209,7 +1261,7 @@ class PyGameGUI:
                         if self.split_players:
                             self.player_minimum = 2
                         else:
-                            self.player_minimum = 1
+                            self.player_minimum = 0
 
             ############################################
 
@@ -1217,7 +1269,13 @@ class PyGameGUI:
                 match event.ui_element:
                     case self.continue_button:
                         self.menu_state = MenuStates.PreGame
-                        self.stop_game("tutorial_finished")
+
+                        if self.CONNECT_WITH_STUDY_SERVER:
+                            self.send_tutorial_finished()
+                            self.start_study()
+                        else:
+                            self.stop_game("tutorial_finished")
+                        self.disconnect_websockets()
 
             ############################################
 
@@ -1226,7 +1284,6 @@ class PyGameGUI:
                     case self.continue_button:
                         self.setup_game()
                         self.set_game_size()
-                        self.set_window_size()
                         self.menu_state = MenuStates.Game
 
             ############################################
@@ -1238,23 +1295,27 @@ class PyGameGUI:
                         self.disconnect_websockets()
                         self.finished_button_press()
 
+                        if self.CONNECT_WITH_STUDY_SERVER:
+                            self.send_level_done()
+
             ############################################
 
             case MenuStates.PostGame:
                 match event.ui_element:
                     case self.retry_button:
-                        self.setup_game()
+                        if not self.CONNECT_WITH_STUDY_SERVER:
+                            self.stop_game("Retry button")
                         self.menu_state = MenuStates.PreGame
 
                     case self.next_game_button:
-                        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.setup_game()
+                        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:
@@ -1280,6 +1341,8 @@ class PyGameGUI:
         self.reset_window_size()
         self.init_ui_elements()
 
+        self.reset_gui_values()
+
         self.update_screen_elements()
 
         # Game loop
@@ -1291,12 +1354,12 @@ class PyGameGUI:
                 # PROCESSING EVENTS
                 for event in pygame.event.get():
                     if event.type == pygame.QUIT:
+                        self.disconnect_websockets()
                         self.running = False
 
                     if event.type == pygame_gui.UI_BUTTON_PRESSED:
                         self.manage_button_event(event)
                         self.update_screen_elements()
-                        self.update_selection_elements()
 
                     if event.type in [
                         pygame.KEYDOWN,
@@ -1314,18 +1377,31 @@ class PyGameGUI:
 
             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/recipe_mock.png b/overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png
similarity index 100%
rename from overcooked_simulator/gui_2d_vis/recipe_mock.png
rename to overcooked_simulator/gui_2d_vis/tutorial_files/recipe_mock.png
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/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index 15bf39c86263f76e795990919533e9f08c047995..41c6bc619520873037d2ba1b170a32cd82ce90cc 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])
@@ -469,11 +469,7 @@ class Environment:
             grid.append(grid_line)
             current_y += 1
 
-        for line in grid:
-            print(line)
         grid = [line + ([0] * (max_width - len(line))) for line in grid]
-        for line in grid:
-            print(line)
 
         self.kitchen_width: float = max_width + starting_at
         self.kitchen_height = current_y
@@ -826,6 +822,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(