diff --git a/README.md b/README.md
index 7222709f10f94cc0b0ab6343079ab35dfd8f970e..135e869b284165af0d9eebe765a90478cc7f1f31 100644
--- a/README.md
+++ b/README.md
@@ -20,14 +20,30 @@ In your `repo`, `PyCharmProjects` or similar directory with the correct environm
 git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
 cd overcooked_simulator
 pip install -e .
-``
+```
 
 #### Run
-You can use it in your Python code or run the `main.py`from the command line:
+
+Run it via the command line (in your pyenv/conda environment):
+
 ```bash
-python3 overcooked_simulator/main.py
+overcooked-sim  --url "localhost" --port 8000
 ```
 
+_The arguments are the defaults. Therefore, they are optional._
+
+You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
+
+```bash
+python3 overcooked_simulator/game_server.py --url "localhost" --port 8000
+
+python3 overcooked_simulator/gui_2d_vis/overcooked_simulator.py --url "localhost" --port 8000
+```
+
+You can start also several GUIs.
+
+You can replace the GUI with your own GUI (+ study server/matchmaking server).
+
 ### Library Installation
 
 The correct environment needs to be active:
diff --git a/overcooked_simulator/__init__.py b/overcooked_simulator/__init__.py
index 2c7bb07c452710b36d9c0a5aa12cd56325842530..fb941f2fadd719d7f44a3c1977aac8ee5f416538 100644
--- a/overcooked_simulator/__init__.py
+++ b/overcooked_simulator/__init__.py
@@ -24,12 +24,22 @@ Our overcooked simulator is designed for real time interaction but also with rei
 It focuses on configurability, extensibility and appealing visualization options.
 
 ## Human Player
-Start `main.py` in your python/conda environment:
+Run it via the command line (in your pyenv/conda environment):
+
 ```bash
-python overcooked_simulator/main.py
+overcooked-sim  --url "localhost" --port 8000
 ```
 
-## Connect with player and receive game state
+_The arguments are the defaults. Therefore, they are optional._
+
+You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
+
+```bash
+python3 overcooked_simulator/game_server.py --url "localhost" --port 8000
+
+python3 overcooked_simulator/gui_2d_vis/overcooked_simulator.py --url "localhost" --port 8000
+
+## Connect with agent and receive game state
 ...
 
 ## Direct integration into your code.
@@ -43,7 +53,8 @@ Initialize an environment....
 # Structure of the Documentation
 The API documentation follows the file and content structure in the repo.
 On the left you can find the navigation panel that brings you to the implementation of
-- the **counters**, including the kitchen utility objects like dispenser, stove, sink, etc.,
+- the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven),
+  sink, etc.,
 - the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
 - in **main**, you find an example how to start a simulation,
 - the **orders**, how to sample incoming orders and their attributes,
diff --git a/overcooked_simulator/__main__.py b/overcooked_simulator/__main__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f81398ece39babd670716b79b045ee3226176e97
--- /dev/null
+++ b/overcooked_simulator/__main__.py
@@ -0,0 +1,62 @@
+import argparse
+import time
+from multiprocessing import Process
+
+from overcooked_simulator.utils import (
+    url_and_port_arguments,
+    disable_websocket_logging_arguments,
+)
+
+
+def start_game_server(cli_args):
+    from overcooked_simulator.game_server import main
+
+    main(cli_args.url, cli_args.port)
+
+
+def start_pygame_gui(cli_args):
+    from overcooked_simulator.gui_2d_vis.overcooked_gui import main
+
+    main(cli_args.url, cli_args.port)
+
+
+def main(cli_args=None):
+    if cli_args is None:
+        cli_args = parser.parse_args()
+    game_server = None
+    pygame_gui = None
+    try:
+        print("Start game engine:")
+        game_server = Process(target=start_game_server, args=(cli_args,))
+        game_server.start()
+        time.sleep(1)
+        print("Start PyGame GUI:")
+        pygame_gui = Process(target=start_pygame_gui, args=(cli_args,))
+        pygame_gui.start()
+        while pygame_gui.is_alive() and game_server.is_alive():
+            time.sleep(1)
+    except KeyboardInterrupt:
+        print("Received Keyboard interrupt")
+    finally:
+        if game_server is not None and game_server.is_alive():
+            print("Terminate game server")
+            game_server.terminate()
+        if pygame_gui is not None and pygame_gui.is_alive():
+            print("Terminate pygame gui")
+            game_server.terminate()
+        time.sleep(0.1)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Overcooked Simulator",
+        description="Game Engine Server + PyGameGUI: Starts overcooked game engine server and a PyGame 2D Visualization window in two processes.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+
+    url_and_port_arguments(parser)
+    disable_websocket_logging_arguments(parser)
+
+    args = parser.parse_args()
+    print(args)
+    main(args)
diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py
index 6d1196bccf11a16ff9455bf694bb17cb1963cd2d..75152544acb3267dca4dd78ae531826168d442dc 100644
--- a/overcooked_simulator/counters.py
+++ b/overcooked_simulator/counters.py
@@ -2,11 +2,12 @@
 what should happen when the agent wants to pick something up from the counter. On the other side,
 the `Counter.drop_off` method receives the item what should be put on the counter. Before that the
 `Counter.can_drop_off` method checked if the item can be put on the counter. The progress on Counters or on objects
-on the counters are handled via the Counters. They have the task to delegate the progress call via the
-`progress` method, e.g., the `CuttingBoard.progress`. On which type of counter the progress method is called is currently defined in the
-environment class.
+on the counters are handled via the Counters. They have the task to delegate the progress call via the `progress`
+method, e.g., the `CuttingBoard.progress`. On which type of counter the progress method is called is currently
+defined in the environment class.
 
-Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the interaction/requirements for the interaction.
+Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the
+interaction/requirements for the interaction.
 
     CuttingBoard:
       type: Equipment
@@ -24,7 +25,7 @@ The defined counter classes are:
 - `Dispenser`
 - `PlateDispenser`
 - `Trashcan`
-- `Stove` (maybe abstracted in a class for all cooking machine counters (stove, deep fryer, oven))
+- `CookingCounter`
 - `Sink`
 - `SinkAddon`
 
@@ -34,7 +35,9 @@ from __future__ import annotations
 
 import dataclasses
 import logging
+import uuid
 from collections import deque
+from collections.abc import Iterable
 from datetime import datetime, timedelta
 from typing import TYPE_CHECKING, Optional, Callable, Set
 
@@ -56,6 +59,8 @@ from overcooked_simulator.game_items import (
 
 log = logging.getLogger(__name__)
 
+COUNTER_CATEGORY = "Counter"
+
 
 class Counter:
     """Simple class for a counter at a specified position (center of counter). Can hold things on top.
@@ -63,13 +68,19 @@ class Counter:
     The character `#` in the `layout` file represents the standard Counter.
     """
 
-    def __init__(self, pos: npt.NDArray[float], occupied_by: Optional[Item] = None):
+    def __init__(
+        self,
+        pos: npt.NDArray[float],
+        occupied_by: Optional[Item] = None,
+        uid: hex = None,
+    ):
         """Constructor setting the arguments as attributes.
 
         Args:
             pos: Position of the counter in the environment. 2-element vector.
             occupied_by: The item on top of the counter.
         """
+        self.uuid = uuid.uuid4().hex if uid is None else None
         self.pos: npt.NDArray[float] = pos
         self.occupied_by: Optional[Item] = occupied_by
 
@@ -138,6 +149,21 @@ class Counter:
             f"{self.__class__.__name__}(pos={self.pos},occupied_by={self.occupied_by})"
         )
 
+    def to_dict(self) -> dict:
+        return {
+            "id": self.uuid,
+            "category": COUNTER_CATEGORY,
+            "type": self.__class__.__name__,
+            "pos": self.pos.tolist(),
+            "occupied_by": None
+            if self.occupied_by is None
+            else (
+                [o.to_dict() for o in self.occupied_by]
+                if isinstance(self.occupied_by, Iterable)
+                else self.occupied_by.to_dict()
+            ),
+        }
+
 
 class CuttingBoard(Counter):
     """Cutting ingredients on. The requirement in a new object could look like
@@ -205,9 +231,15 @@ class CuttingBoard(Counter):
         """Handles player interaction, stopping to hold key down."""
         self.pause_progress()
 
+    def to_dict(self) -> dict:
+        d = super().to_dict()
+        d.update((("progressing", self.progressing),))
+        return d
+
 
 class ServingWindow(Counter):
-    """The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for the info about a plate outside of the kitchen.
+    """The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for
+    the info about a plate outside of the kitchen.
 
     All items in the `item_info.yml` with the type meal are considered to be servable, if they are ordered. Not
     ordered meals can also be served, if a `serving_not_ordered_meals` function is set in the `environment_config.yml`.
@@ -300,6 +332,11 @@ class Dispenser(Counter):
         }
         return Item(**kwargs)
 
+    def to_dict(self) -> dict:
+        d = super().to_dict()
+        d.update((("type", self.__repr__()),))
+        return d
+
 
 @dataclasses.dataclass
 class PlateConfig:
@@ -443,20 +480,34 @@ class Trashcan(Counter):
         return True
 
 
-class Stove(Counter):
-    """Cooking machine. Currently, the stove which can have a pot and pan on top. In the future one class for stove,
-    deep fryer, and oven.
+class CookingCounter(Counter):
+    """Cooking machine. Class for the stove, deep fryer, and oven.
 
     The character depends on the cooking equipment on top of it:
     ```yaml
     U: Stove with a pot
     Q: Stove with a pan
+    O: Oven with a (pizza) peel
+    F: DeepFryer with a basket
     ```
     """
 
+    def __init__(
+        self,
+        name: str,
+        cooking_counter_equipments: dict[str, list[str]],
+        **kwargs,
+    ):
+        self.name = name
+        self.cooking_counter_equipments = cooking_counter_equipments
+        super().__init__(**kwargs)
+
     def can_drop_off(self, item: Item) -> bool:
         if self.occupied_by is None:
-            return isinstance(item, CookingEquipment) and item.name in ["Pot", "Pan"]
+            return (
+                isinstance(item, CookingEquipment)
+                and item.name in self.cooking_counter_equipments[self.name]
+            )
         else:
             return self.occupied_by.can_combine(item)
 
@@ -465,10 +516,19 @@ class Stove(Counter):
         if (
             self.occupied_by
             and isinstance(self.occupied_by, CookingEquipment)
+            and self.occupied_by.name in self.cooking_counter_equipments[self.name]
             and self.occupied_by.can_progress()
         ):
             self.occupied_by.progress(passed_time, now)
 
+    def __repr__(self):
+        return f"{self.name}(pos={self.pos},occupied_by={self.occupied_by})"
+
+    def to_dict(self) -> dict:
+        d = super().to_dict()
+        d.update((("type", self.name),))
+        return d
+
 
 class Sink(Counter):
     """The counter in which the dirty plates can be washed to clean plates.
@@ -560,6 +620,11 @@ class Sink(Counter):
     def set_addon(self, sink_addon: SinkAddon):
         self.sink_addon = sink_addon
 
+    def to_dict(self) -> dict:
+        d = super().to_dict()
+        d.update((("progressing", self.progressing),))
+        return d
+
 
 class SinkAddon(Counter):
     """The counter on which the clean plates appear after cleaning them in the `Sink`
diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml
index 2698e1ffb5a81a2074e85fceb9452ff40caa3eaf..cad2888da61dd5ddb4de812f175f4b978fd2affd 100644
--- a/overcooked_simulator/game_content/environment_config.yaml
+++ b/overcooked_simulator/game_content/environment_config.yaml
@@ -5,10 +5,10 @@ plates:
   # range of seconds until the dirty plate arrives.
 
 game:
-  time_limit_seconds: 180
+  time_limit_seconds: 300
 
 meals:
-  all: false
+  all: true
   # if all: false -> only orders for these meals are generated
   # TODO: what if this list is empty?
   list:
@@ -51,10 +51,10 @@ orders:
     expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty ''
     expired_penalty_kwargs:
       default: -5
-  serving_not_ordered_meals: null
+  serving_not_ordered_meals: !!python/name:overcooked_simulator.order.serving_not_ordered_meals_with_zero_score ''
   # a func that calcs a store for not ordered but served meals. Input: meal
 
 player_config:
   radius: 0.4
-  move_dist: 0.15
-  interaction_range: 1.6
\ No newline at end of file
+  player_speed_units_per_seconds: 8
+  interaction_range: 1.6
diff --git a/overcooked_simulator/game_content/item_info.yaml b/overcooked_simulator/game_content/item_info.yaml
index 777e5f28e106587285ecf8eb6d1e77e23911914e..a6458c6329cbf0323e6fb59338f34e0739c9786e 100644
--- a/overcooked_simulator/game_content/item_info.yaml
+++ b/overcooked_simulator/game_content/item_info.yaml
@@ -7,6 +7,12 @@ Sink:
 Stove:
   type: Equipment
 
+DeepFryer:
+  type: Equipment
+
+Oven:
+  type: Equipment
+
 Pot:
   type: Equipment
   equipment: Stove
@@ -15,6 +21,14 @@ Pan:
   type: Equipment
   equipment: Stove
 
+Basket:
+  type: Equipment
+  equipment: DeepFryer
+
+Peel:
+  type: Equipment
+  equipment: Oven
+
 DirtyPlate:
   type: Equipment
 
@@ -41,6 +55,21 @@ Meat:
 Bun:
   type: Ingredient
 
+Potato:
+  type: Ingredient
+
+Fish:
+  type: Ingredient
+
+Dough:
+  type: Ingredient
+
+Cheese:
+  type: Ingredient
+
+Sausage:
+  type: Ingredient
+
 ChoppedTomato:
   type: Ingredient
   needs: [ Tomato ]
@@ -59,20 +88,62 @@ ChoppedOnion:
   seconds: 5.0
   equipment: CuttingBoard
 
-ChoppedMeat:
+RawPatty:
   type: Ingredient
   needs: [ Meat ]
   seconds: 4.0
   equipment: CuttingBoard
 
+RawChips:
+  type: Ingredient
+  needs: [ Potato ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedFish:
+  type: Ingredient
+  needs: [ Fish ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+PizzaBase:
+  type: Ingredient
+  needs: [ Dough ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+GratedCheese:
+  type: Ingredient
+  needs: [ Cheese ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
+ChoppedSausage:
+  type: Ingredient
+  needs: [ Sausage ]
+  seconds: 4.0
+  equipment: CuttingBoard
+
 CookedPatty:
   type: Ingredient
   seconds: 5.0
-  needs: [ ChoppedMeat ]
+  needs: [ RawPatty ]
   equipment: Pan
 
 # --------------------------------------------------------------------------------
 
+Chips:
+  type: Meal
+  seconds: 5.0
+  needs: [ RawChips ]
+  equipment: Basket
+
+FriedFish:
+  type: Meal
+  seconds: 5.0
+  needs: [ ChoppedFish ]
+  equipment: Basket
+
 Burger:
   type: Meal
   needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
@@ -94,3 +165,14 @@ OnionSoup:
   needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
   seconds: 6.0
   equipment: Pot
+
+FishAndChips:
+  type: Meal
+  needs: [ FriedFish, Chips ]
+  equipment: ~
+
+Pizza:
+  type: Meal
+  needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
+  seconds: 7.0
+  equipment: Peel
diff --git a/overcooked_simulator/game_content/item_info_debug.yaml b/overcooked_simulator/game_content/item_info_debug.yaml
index 32c8696610ab0e1b3f059071da01d7c182e6b80f..c2282253e9539c9686cd6746f536e0316c3b21ec 100644
--- a/overcooked_simulator/game_content/item_info_debug.yaml
+++ b/overcooked_simulator/game_content/item_info_debug.yaml
@@ -7,6 +7,12 @@ Sink:
 Stove:
   type: Equipment
 
+DeepFryer:
+  type: Equipment
+
+Oven:
+  type: Equipment
+
 Pot:
   type: Equipment
   equipment: Stove
@@ -15,13 +21,21 @@ Pan:
   type: Equipment
   equipment: Stove
 
+Basket:
+  type: Equipment
+  equipment: DeepFryer
+
+Peel:
+  type: Equipment
+  equipment: Oven
+
 DirtyPlate:
   type: Equipment
 
 Plate:
   type: Equipment
   needs: [ DirtyPlate ]
-  seconds: 1.0
+  seconds: 0.1
   equipment: Sink
 
 # --------------------------------------------------------------------------------
@@ -41,6 +55,21 @@ Meat:
 Bun:
   type: Ingredient
 
+Potato:
+  type: Ingredient
+
+Fish:
+  type: Ingredient
+
+Dough:
+  type: Ingredient
+
+Cheese:
+  type: Ingredient
+
+Sausage:
+  type: Ingredient
+
 ChoppedTomato:
   type: Ingredient
   needs: [ Tomato ]
@@ -59,20 +88,63 @@ ChoppedOnion:
   seconds: 0.1
   equipment: CuttingBoard
 
-ChoppedMeat:
+RawPatty:
   type: Ingredient
   needs: [ Meat ]
   seconds: 0.1
   equipment: CuttingBoard
 
+RawChips:
+  type: Ingredient
+  needs: [ Potato ]
+  seconds: 0.1
+  equipment: CuttingBoard
+
+ChoppedFish:
+  type: Ingredient
+  needs: [ Fish ]
+  seconds: 0.1
+  equipment: CuttingBoard
+
+PizzaBase:
+  type: Ingredient
+  needs: [ Dough ]
+  seconds: 0.1
+  equipment: CuttingBoard
+
+GratedCheese:
+  type: Ingredient
+  needs: [ Cheese ]
+  seconds: 0.1
+  equipment: CuttingBoard
+
+ChoppedSausage:
+  type: Ingredient
+  needs: [ Sausage ]
+  seconds: 0.1
+  equipment: CuttingBoard
+
 CookedPatty:
   type: Ingredient
-  seconds: 2.0
-  needs: [ ChoppedMeat ]
+  seconds: 0.1
+  needs: [ RawPatty ]
   equipment: Pan
 
 # --------------------------------------------------------------------------------
 
+Chips:
+  type: Meal
+  seconds: 0.1
+  needs: [ RawChips ]
+  equipment: Basket
+
+FriedFish:
+  type: Meal
+  seconds: 0.1
+  needs: [ ChoppedFish ]
+  equipment: Basket
+
+
 Burger:
   type: Meal
   needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ]
@@ -86,11 +158,22 @@ Salad:
 TomatoSoup:
   type: Meal
   needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
-  seconds: 3.0
+  seconds: 0.1
   equipment: Pot
 
 OnionSoup:
   type: Meal
   needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ]
-  seconds: 3.0
+  seconds: 0.1
   equipment: Pot
+
+FishAndChips:
+  type: Meal
+  needs: [ FriedFish, Chips ]
+  equipment: ~
+
+Pizza:
+  type: Meal
+  needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ]
+  seconds: 0.1
+  equipment: Peel
diff --git a/overcooked_simulator/game_content/layouts/basic.layout b/overcooked_simulator/game_content/layouts/basic.layout
index 2e7395aa296304bae3d354eea5613affcc0ee6b0..ccc4076303e985a8b60c9f2dd091f323b5d6e7a6 100644
--- a/overcooked_simulator/game_content/layouts/basic.layout
+++ b/overcooked_simulator/game_content/layouts/basic.layout
@@ -1,9 +1,9 @@
-#QU#T###NLB#
+#QU#FO#TNLB#
 #__________M
+#__________K
+W__________I
+#__A_____A_D
+C__________E
+C__________G
 #__________#
-W___________
-#__A_____A__
-C___________
-C__________#
-#__________X
-#P#S+####S+#
\ No newline at end of file
+#P#S+#X##S+#
\ No newline at end of file
diff --git a/overcooked_simulator/game_content/layouts/godot_test_layout.layout b/overcooked_simulator/game_content/layouts/godot_test_layout.layout
new file mode 100644
index 0000000000000000000000000000000000000000..06db451a2f644732cc693835a9a5aaee6a38d612
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/godot_test_layout.layout
@@ -0,0 +1,9 @@
+##########
+#________#
+#________#
+#________#
+#________#
+#________#
+#________#
+#________#
+#########P
diff --git a/overcooked_simulator/game_content/layouts/split.layout b/overcooked_simulator/game_content/layouts/split.layout
new file mode 100644
index 0000000000000000000000000000000000000000..39bace3e0a94b0594d8ef9f294daeb40aae4492f
--- /dev/null
+++ b/overcooked_simulator/game_content/layouts/split.layout
@@ -0,0 +1,9 @@
+#QU#T###NLB#
+#__________M
+#____A_____#
+W__________#
+############
+C__________#
+C_____A____#
+#__________X
+#P#S+####S+#
\ No newline at end of file
diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py
index babd1ec2713aa21ac84472137f339711e0b0932f..a77f8d78159ee6e6542209230e7bc50a6c0e76df 100644
--- a/overcooked_simulator/game_items.py
+++ b/overcooked_simulator/game_items.py
@@ -24,11 +24,15 @@ import collections
 import dataclasses
 import datetime
 import logging
+import uuid
 from enum import Enum
 from typing import Optional, TypedDict
 
 log = logging.getLogger(__name__)
 
+ITEM_CATEGORY = "Item"
+COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment"
+
 
 class ItemType(Enum):
     Ingredient = "Ingredient"
@@ -100,11 +104,16 @@ class ActiveTransitionTypedDict(TypedDict):
 class Item:
     """Base class for game items which can be held by a player."""
 
-    def __init__(self, name: str, item_info: ItemInfo, *args, **kwargs):
+    item_category = ITEM_CATEGORY
+
+    def __init__(
+        self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs
+    ):
         self.name = self.__class__.__name__ if name is None else name
         self.item_info = item_info
         self.progress_equipment = None
         self.progress_percentage = 0.0
+        self.uuid = uuid.uuid4().hex if uid is None else uid
 
     def __repr__(self):
         if self.progress_equipment is None:
@@ -142,10 +151,19 @@ class Item:
         self.progress_equipment = None
         self.progress_percentage = 0.0
 
+    def to_dict(self) -> dict:
+        return {
+            "id": self.uuid,
+            "category": self.item_category,
+            "type": self.name,
+            "progress_percentage": self.progress_percentage,
+        }
+
 
 class CookingEquipment(Item):
     """Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself (
     progress_percentage) and not in the items in the content list."""
+    item_category = "Cooking Equipment"
 
     def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs):
         super().__init__(*args, **kwargs)
@@ -153,6 +171,7 @@ class CookingEquipment(Item):
         self.active_transition: Optional[ActiveTransitionTypedDict] = None
         """The info how and when to convert the content_list to a new item."""
 
+        # TODO change content ready just to str (name of the item)?
         self.content_ready: Item | None = None
         """Helper attribute that can have a ready meal which is also represented via it ingredients in the 
         content_list. But soups or other processed meals are not covered here. For a Burger or Salad, this attribute 
@@ -179,7 +198,6 @@ class CookingEquipment(Item):
         ingredients = collections.Counter(
             item.name for item in self.content_list + other
         )
-        print(ingredients)
         return any(ingredients <= recipe.recipe for recipe in self.transitions.values())
 
     def combine(self, other) -> Item | None:
@@ -203,7 +221,6 @@ class CookingEquipment(Item):
                         "seconds": transition.seconds,
                         "result": Item(name=result, item_info=transition),
                     }
-                    print(f"{self.name} {self.active_transition}, {self.content_list}")
                 break
         else:
             self.content_ready = None
@@ -245,6 +262,21 @@ class CookingEquipment(Item):
             return self.content_list[0]
         return None
 
+    def to_dict(self) -> dict:
+        d = super().to_dict()
+        d.update(
+            (
+                ("content_list", [c.to_dict() for c in self.content_list]),
+                (
+                    "content_ready",
+                    self.content_ready.to_dict()
+                    if self.content_ready is not None
+                    else None,
+                ),
+            )
+        )
+        return d
+
 
 class Plate(CookingEquipment):
     """The plate can have to states: clean and dirty. In the clean state it can hold content/other items."""
diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..35db3885b24d70c21ee3c37db3a95ddad423613c
--- /dev/null
+++ b/overcooked_simulator/game_server.py
@@ -0,0 +1,737 @@
+"""A game server that manages the environments.
+
+Study server or single instance games/GUIs can create environments. The game server returns client ids for the player
+individual websockets. These players should then connect to the websockets under `/ws/player/{client_id}`. Each
+player can state that them are ready to play, request the current game state, and pass the players action. See
+`PlayerRequestType` and `manage_websocket_message`.
+
+
+"""
+from __future__ import annotations
+
+import argparse
+import asyncio
+import dataclasses
+import json
+import logging
+import time
+import uuid
+from collections import defaultdict
+from datetime import datetime, timedelta
+from enum import Enum
+from typing import Set
+
+import numpy as np
+import uvicorn
+from fastapi import FastAPI, HTTPException
+from fastapi import WebSocket
+from pydantic import BaseModel
+from starlette.websockets import WebSocketDisconnect
+from typing_extensions import TypedDict
+
+from overcooked_simulator.overcooked_environment import Action, Environment
+from overcooked_simulator.server_results import (
+    CreateEnvResult,
+    PlayerInfo,
+    PlayerRequestResult,
+)
+from overcooked_simulator.utils import setup_logging, url_and_port_arguments
+
+log = logging.getLogger(__name__)
+
+
+app = FastAPI()
+
+TIME_AFTER_STOP_TO_DEL_ENV = 30
+
+
+@dataclasses.dataclass
+class PlayerData:
+    player_id: str
+    env_id: str
+    websocket_id: str | None = None
+    connected: bool = False
+    ready: bool = False
+    last_action: datetime | None = None
+    name: str = ""
+
+
+class EnvironmentSettings(TypedDict):
+    all_player_can_pause_game: bool
+    # env_steps_per_second: int
+
+
+class EnvironmentStatus(Enum):
+    WAITING_FOR_PLAYERS = "waitingForPlayers"
+    PAUSED = "paused"
+    RUNNING = "running"
+    STOPPED = "stopped"
+
+
+@dataclasses.dataclass
+class EnvironmentData:
+    environment: Environment
+    player_hashes: Set[str] = dataclasses.field(default_factory=set)
+    environment_settings: EnvironmentSettings = dataclasses.field(default_factory=dict)
+    status: EnvironmentStatus = EnvironmentStatus.WAITING_FOR_PLAYERS
+    stop_reason: str = ""
+    start_time: datetime | None = None
+    last_step_time: int | None = None
+
+    # add manager_id?
+
+
+class EnvironmentHandler:
+    """Running several environments for a game server."""
+
+    def __init__(self, env_step_frequency: int = 200):
+        self.envs: dict[str, EnvironmentData] = {}
+        """A dictionary of environment IDs and their respective data."""
+        self.player_data: dict[str, PlayerData] = {}
+        """A dictionary of player hashes and their respective data."""
+        self.manager_envs: dict[str, Set[str]] = defaultdict(set)
+        """A dictionary of manager IDs and the environment IDs managed by each manager."""
+        self.env_step_frequency = env_step_frequency
+        """The frequency at which the environment steps."""
+        self.preferred_sleep_time_ns = 1e9 / self.env_step_frequency
+        """The preferred sleep time between environment steps in nanoseconds based on the `env_step_frequency`."""
+        self.client_ids_to_player_hashes = {}
+        """A dictionary mapping client IDs to player hashes."""
+
+    def create_env(
+        self, environment_config: CreateEnvironmentConfig
+    ) -> CreateEnvResult:
+        """Create a new environment.
+
+        Args:
+            environment_config: An instance of CreateEnvironmentConfig class that contains the configuration for creating the environment.
+
+        Returns:
+            A dictionary containing the created environment ID and player information.
+
+        """
+        env_id = uuid.uuid4().hex
+
+        env = Environment(
+            env_config=environment_config.environment_config,
+            layout_config=environment_config.layout_config,
+            item_info=environment_config.item_info_config,
+            as_files=False,
+        )
+        player_info = {}
+        for player_id in range(environment_config.number_players):
+            player_id = str(player_id)
+            player_info[player_id] = self.create_player(env, env_id, player_id)
+
+        self.envs[env_id] = EnvironmentData(
+            environment=env,
+            player_hashes={info["player_hash"] for info in player_info.values()},
+        )
+
+        self.manager_envs[environment_config.manager_id].update([env_id])
+
+        return {"env_id": env_id, "player_info": player_info}
+
+    def create_player(
+        self, env: Environment, env_id: str, player_id: str
+    ) -> PlayerInfo:
+        """Create a player in an environment and the information for the client to send the control data to the server.
+
+        This method creates a player within the specified environment. It generates a unique player hash
+        and client ID using the uuid module. The player data, including the player ID, environment ID, and websocket
+        ID (client ID), is stored in the player_data dictionary. The client ID and player hash are also stored in the
+        client_ids_to_player_hashes dictionary for easy lookup. Finally, the player is added to the environment using
+        the add_player() method. The method returns a dictionary containing the client ID, player hash, and player ID.
+
+        Args:
+            env (Environment): The instance of the Environment class in which the player is being created.
+            env_id (str): The identifier for the environment.
+            player_id (str): The identifier for the player being created.
+
+        Returns:
+            PlayerInfo: A dictionary containing the client ID, player hash, and player ID.
+        """
+        player_hash = uuid.uuid4().hex
+        client_id = uuid.uuid4().hex
+        player_data = PlayerData(
+            player_id=player_id,
+            env_id=env_id,
+            websocket_id=client_id,
+        )
+        self.player_data[player_hash] = player_data
+        self.client_ids_to_player_hashes[client_id] = player_hash
+        env.add_player(player_id)
+
+        return {
+            "client_id": client_id,
+            "player_hash": player_hash,
+            "player_id": player_id,
+        }
+
+    def add_player(self, config: AdditionalPlayer) -> dict[str, PlayerInfo]:
+        """Add new player(s) to the environment.
+
+        Args:
+            config (AdditionalPlayer): Configuration for adding new player(s) to the environment.
+
+        Returns:
+            dict[str, PlayerInfo]: A dictionary containing information about the newly added player(s).
+
+        Example Usage:
+            config = AdditionalPlayer(manager_id='manager_1', env_id='env_1', number_players=2)
+            new_players = add_player(config)
+        """
+        new_player_info = {}
+        if (
+            config.manager_id in self.manager_envs
+            and config.env_id in self.manager_envs[config.manager_id]
+            and self.envs[config.env_id].status != EnvironmentStatus.STOPPED
+        ):
+            n_players = len(self.envs[config.env_id].player_hashes)
+            for player_id in range(n_players, n_players + config.number_players):
+                player_id = str(player_id)
+                new_player_info[player_id] = self.create_player(
+                    env=self.envs[config.env_id].environment,
+                    env_id=config.env_id,
+                    player_id=player_id,
+                )
+        return new_player_info
+
+    def start_env(self, env_id: str):
+        """Start the specified environment and already created environment.
+
+        Args:
+            env_id (str): The ID of the environment to start.
+        """
+        if env_id in self.envs:
+            log.info(f"Start environment {env_id=}")
+            start_time = datetime.now()
+            self.envs[env_id].status = EnvironmentStatus.RUNNING
+            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()
+
+    def get_state(self, player_hash: str) -> str:  # -> StateRepresentation as json
+        """Get the current state representation of the environment for a player.
+
+        Args:
+            player_hash (str): The unique identifier of the player.
+
+        Returns: str: The state representation of the environment for a player. Is
+        `overcooked_simulator.state_representation.StateRepresentation` as a json.
+
+        """
+        if (
+            player_hash in self.player_data
+            and self.player_data[player_hash].env_id in self.envs
+        ):
+            return self.envs[
+                self.player_data[player_hash].env_id
+            ].environment.get_json_state()
+
+    def pause_env(self, manager_id: str, env_id: str, reason: str):
+        """Pause the specified environment.
+
+        Args:
+            manager_id (str): The ID of the manager that manages the environment.
+            env_id (str): The ID of the environment.
+            reason (str): The reason for pausing the environment.
+
+        """
+        if (
+            manager_id in self.manager_envs
+            and env_id in self.manager_envs[manager_id]
+            and self.envs[env_id].status
+            not in [EnvironmentStatus.STOPPED, Environment.PAUSED]
+        ):
+            self.envs[env_id].status = EnvironmentStatus.PAUSED
+
+    def unpause_env(self, manager_id: str, env_id: str, reason: str):
+        """Unpause the specified environment.
+
+        Args:
+            manager_id (str): The ID of the manager that manages the environment.
+            env_id (str): The ID of the environment.
+            reason (str): The reason for unpausing the environment.
+        """
+        if (
+            manager_id in self.manager_envs
+            and env_id in self.manager_envs[manager_id]
+            and self.envs[env_id].status
+            not in [EnvironmentStatus.STOPPED, Environment.PAUSED]
+        ):
+            self.envs[env_id].status = EnvironmentStatus.PAUSED
+            self.envs[env_id].last_step_time = time.time_ns()
+
+    def stop_env(self, manager_id: str, env_id: str, reason: str) -> int:
+        """Stop the specified environment.
+
+        Args:
+            manager_id: A string representing the id of the manager.
+            env_id: A string representing the id of the environment.
+            reason: A string representing the reason for stopping the environment.
+
+        Returns:
+            An integer code indicating the result of stopping the environment.
+            - 0: The environment was successfully stopped.
+            - 1: The manager_id or env_id is invalid.
+            - 2: The environment is already stopped.
+        """
+        if manager_id in self.manager_envs and env_id in self.manager_envs[manager_id]:
+            if self.envs[env_id].status != EnvironmentStatus.STOPPED:
+                self.envs[env_id].status = EnvironmentStatus.STOPPED
+                self.envs[env_id].stop_reason = reason
+                return 0
+            return 2
+        return 1
+
+    def set_player_ready(self, player_hash) -> bool:
+        """Set the specified player as ready.
+
+        Args: player_hash (str): The hash that allows access to the player data (should only know the player client
+        and not other players).
+
+        Returns:
+            bool: True if the player is successfully set as ready, False otherwise.
+        """
+        if player_hash in self.player_data:
+            self.player_data[player_hash].ready = True
+            return True
+        return False
+
+    def set_player_connected(self, client_id: str) -> bool:
+        """Set the connected status of a player.
+
+        Args:
+            client_id (str): The client ID of the player.
+
+        Returns:
+            bool: True if the connected status was successfully set, False otherwise.
+        """
+        if client_id in self.client_ids_to_player_hashes:
+            self.player_data[
+                self.client_ids_to_player_hashes[client_id]
+            ].connected = True
+            return True
+        return False
+
+    def set_player_disconnected(self, client_id: str) -> bool:
+        """Set player as disconnected.
+
+        Args:
+            client_id: The ID of the client.
+
+        Returns:
+            True if the player was successfully set as disconnected, False otherwise.
+        """
+        if client_id in self.client_ids_to_player_hashes:
+            log.warning(
+                f"Player {self.player_data[self.client_ids_to_player_hashes[client_id]].player_id} in env {self.player_data[self.client_ids_to_player_hashes[client_id]].env_id} disconnected"
+            )
+            self.player_data[
+                self.client_ids_to_player_hashes[client_id]
+            ].connected = False
+            return True
+        return False
+
+    def check_all_player_ready(self, env_id: str) -> bool:
+        """Check if all players in the specified environment are ready.
+
+        Args:
+            self (object): The current object instance.
+            env_id (str): The ID of the environment to check.
+
+        Returns:
+            bool: True if all players are ready, False otherwise.
+        """
+        return env_id in self.envs and all(
+            self.player_data[player_hash].connected
+            and self.player_data[player_hash].ready
+            for player_hash in self.envs[env_id].player_hashes
+        )
+
+    def check_all_players_connected(self, env_id: str) -> bool:
+        """Check if all players in a given environment are connected.
+
+        Args:
+            env_id: The ID of the environment to check.
+
+        Returns:
+            bool: True if all players are connected, False otherwise.
+        """
+        return env_id in self.envs and all(
+            self.player_data[player_hash].connected
+            for player_hash in self.envs[env_id].player_hashes
+        )
+
+    def list_not_connected_players(self, env_id: str) -> list[str]:
+        """List player_ids of all players that aren't connected to the server.
+
+        Args:
+            env_id: The ID of the environment for which to retrieve the list of not connected players
+
+        Returns:
+            A list of player IDs of players who are not connected to the specified environment
+        """
+        if env_id in self.envs:
+            return [
+                self.player_data[player_hash].player_id
+                for player_hash in self.envs[env_id].player_hashes
+                if not self.player_data[player_hash].connected
+            ]
+
+    def list_not_ready_players(self, env_id: str) -> list[str]:
+        """List player IDs for players who are not ready to play in a specific environment.
+
+        Args:
+            env_id (str): The ID of the environment.
+
+        Returns:
+            list[str]: A list of player IDs who are not ready to play in the specified environment.
+        """
+        if env_id in self.envs:
+            return [
+                self.player_data[player_hash].player_id
+                for player_hash in self.envs[env_id].player_hashes
+                if not self.player_data[player_hash].ready
+            ]
+
+    async def environment_steps(self):
+        """Asynchronous method that performs environmental steps for all running environments.
+
+        Should run asynchronously alongside the server.
+        """
+        # TODO environment dependent steps.
+        overslept_in_ns = 0
+        # TODO add checking if player disconnects
+        #    - also what should happen when all disconnect for a time -> stop env.
+        while True:
+            pre_step_start = time.time_ns()
+            to_remove = []
+            for env_id, env_data in self.envs.items():
+                if env_data.status == EnvironmentStatus.RUNNING:
+                    step_start = time.time_ns()
+                    env_data.environment.step(
+                        timedelta(
+                            seconds=(step_start - env_data.last_step_time)
+                            / 1_000_000_000
+                        )
+                    )
+                    env_data.last_step_time = step_start
+                    if env_data.environment.game_ended:
+                        log.info(f"Env {env_id} ended. Set env to STOPPED.")
+                        env_data.status = EnvironmentStatus.STOPPED
+                elif (
+                    env_data.status == EnvironmentStatus.WAITING_FOR_PLAYERS
+                    and self.check_all_player_ready(env_id)
+                ):
+                    self.start_env(env_id)
+                elif (
+                    env_data.status == EnvironmentStatus.STOPPED
+                    and env_data.last_step_time + (TIME_AFTER_STOP_TO_DEL_ENV * 1e9)
+                    < pre_step_start
+                ):
+                    to_remove.append(env_id)
+            if to_remove:
+                for env_id in to_remove:
+                    del self.envs[env_id]
+            step_duration = time.time_ns() - pre_step_start
+
+            time_to_sleep_ns = self.preferred_sleep_time_ns - (
+                step_duration + overslept_in_ns
+            )
+
+            sleep_start = time.time_ns()
+            await asyncio.sleep(max(time_to_sleep_ns / 1e9, 0))
+            sleep_function_duration = time.time_ns() - sleep_start
+            overslept_in_ns = sleep_function_duration - time_to_sleep_ns
+
+    def is_known_client_id(self, client_id: str) -> bool:
+        """Check if a client ID is known.
+
+        Client IDs are generated by the server for players to connect to a websocket. Therefore, unknown IDs are ignored.
+
+        Args:
+            client_id (str): The client ID to be checked.
+
+        Returns:
+            bool: True if the client ID is known, False otherwise.
+        """
+        return client_id in self.client_ids_to_player_hashes
+
+    def player_action(self, player_hash: str, action: Action) -> bool:
+        """Pass an action of a player to the environment.
+
+        Args:
+            player_hash (str): The hash that allows access to the player data (should only know the player client and not other players).
+            action (Action): The action to be performed.
+
+        Returns:
+            bool: True if the action was performed successfully, False otherwise.
+        """
+        if (
+            player_hash in self.player_data
+            and action.player == self.player_data[player_hash].player_id
+            and self.player_data[player_hash].env_id in self.envs
+            and player_hash
+            in self.envs[self.player_data[player_hash].env_id].player_hashes
+        ):
+            self.envs[self.player_data[player_hash].env_id].environment.perform_action(
+                action
+            )
+            return True
+        return False
+
+
+class PlayerConnectionManager:
+    """
+    PlayerConnectionManager is a class responsible for managing player connections in a server.
+    """
+
+    def __init__(self):
+        """Initializes the PlayerConnectionManager object."""
+        self.player_connections: dict[str, WebSocket] = {}
+        """
+        A dictionary holding the client ID as the key and the corresponding WebSocket connection as the value.
+        """
+
+    async def connect_player(self, websocket: WebSocket, client_id: str) -> bool:
+        """Connect a player to the server by adding their WebSocket connection to the player_connections dictionary.
+
+        Args:
+            websocket (WebSocket): The WebSocket connection of the player.
+            client_id (str): The ID of the player.
+
+        Returns:
+            bool: True if the player is successfully connected, False if the player is already connected.
+        """
+        if client_id not in self.player_connections:
+            await websocket.accept()
+            self.player_connections[client_id] = websocket
+            return True
+        return False
+
+    def disconnect(self, client_id: str):
+        """Disconnect a player from the server by removing their entry from the player_connections dictionary.
+
+        Args:
+            client_id (str): The ID of the player.
+        """
+        if client_id in self.player_connections:
+            del self.player_connections[client_id]
+
+    @staticmethod
+    async def send_personal_message(message: str, websocket: WebSocket):
+        """Send a personal message to a specific player.
+
+        Args:
+            message (str): The message to send.
+            websocket (WebSocket): The WebSocket connection of the player.
+        """
+        await websocket.send_text(message)
+
+    async def broadcast(self, message: str):
+        """Send a message to all connected players.
+
+        Args:
+            message (str): The message to broadcast.
+        """
+        for connection in self.player_connections.values():
+            await connection.send_text(message)
+
+
+manager = PlayerConnectionManager()
+environment_handler: EnvironmentHandler = EnvironmentHandler()
+
+
+class PlayerRequestType(Enum):
+    """Enumerates the possible types of websocket messages for a connected player."""
+
+    READY = "ready"
+    """Indicates that the player is ready to play the game."""
+    GET_STATE = "get_state"
+    """Indicates a request to get the current (player-specific) state."""
+    ACTION = "action"
+    """Indicates a request to pass an action of a player to the environment."""
+
+
+def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResult | str:
+    """Manage WebSocket Message by validating the message and passing it to the environment.
+
+    Args:
+        message: The WebSocket message.
+        client_id: The client ID.
+
+    Returns:
+        PlayerRequestResult or str: The result of the managed message.
+    """
+    message_dict = json.loads(message)
+    request_type = None
+    try:
+        assert "type" in message_dict, "message needs a type"
+
+        request_type = PlayerRequestType(message_dict["type"])
+        assert (
+            "player_hash" in message_dict
+        ), "'player_hash' key not in message dictionary'"
+        match request_type:
+            case PlayerRequestType.READY:
+                accepted = environment_handler.set_player_ready(
+                    message_dict["player_hash"]
+                )
+                return {
+                    "request_type": request_type.value,
+                    "msg": f"ready{' ' if accepted else ' not '}accepted",
+                    "status": 200 if accepted else 400,
+                    "player_hash": message_dict["player_hash"],
+                }
+
+            case PlayerRequestType.GET_STATE:
+                return environment_handler.get_state(message_dict["player_hash"])
+
+            case PlayerRequestType.ACTION:
+                assert (
+                    "action" in message_dict
+                ), "'action' key not in message dictionary of 'action' request"
+                assert (
+                    "action_data" in message_dict["action"]
+                ), "'action_data' key not in message dictionary['action'] of 'action' request"
+                if isinstance(message_dict["action"]["action_data"], list):
+                    message_dict["action"]["action_data"] = np.array(
+                        message_dict["action"]["action_data"], dtype=float
+                    )
+                accepted = environment_handler.player_action(
+                    message_dict["player_hash"], Action(**message_dict["action"])
+                )
+                return {
+                    "request_type": request_type.value,
+                    "status": 200 if accepted else 400,
+                    "msg": f"action{' ' if accepted else ' not '}accepted",
+                    "player_hash": message_dict["player_hash"],
+                }
+        return {
+            "request_type": request_type.value,
+            "status": 400,
+            "msg": "request not handled",
+            "player_hash": message_dict["player_hash"],
+        }
+    except ValueError as e:
+        return {
+            "request_type": message_dict["type"],
+            "status": 400,
+            "msg": e.args[0],
+            "player_hash": None,
+        }
+    except AssertionError as e:
+        return {
+            "request_type": request_type.value if request_type else None,
+            "status": 400,
+            "msg": e.args[0],
+            "player_hash": None,
+        }
+
+
+@app.get("/")
+def read_root():
+    return {"OVER": "COOKED"}
+
+
+class CreateEnvironmentConfig(BaseModel):
+    manager_id: str
+    number_players: int
+    same_websocket_player: list[list[str]] | None = None
+    environment_settings: EnvironmentSettings
+    item_info_config: str  # file content
+    environment_config: str  # file content
+    layout_config: str  # file content
+
+
+class AdditionalPlayer(BaseModel):
+    manager_id: str
+    env_id: str
+    number_players: int
+    existing_websocket: str | None = None
+
+
+@app.post("/manage/create_env/")
+async def create_env(creation: CreateEnvironmentConfig) -> CreateEnvResult:
+    result = environment_handler.create_env(creation)
+    return result
+
+
+@app.post("/manage/additional_player/")
+async def additional_player(creation: AdditionalPlayer) -> dict[str, PlayerInfo]:
+    result = environment_handler.add_player(creation)
+    return result
+
+
+@app.post("/manage/stop_env/")
+async def stop_env(manager_id: str, env_id: str, reason: str) -> str:
+    accept = environment_handler.stop_env(manager_id, env_id, reason)
+    if accept:
+        raise HTTPException(
+            status_code=403 if accept == 1 else 409,
+            detail="Environment does not belong to manager"
+            if accept == 1
+            else "Environment already stopped",
+        )
+    return "Ok"
+
+
+# pause / unpause
+# close all envs for a manager
+# control access / functions / data
+
+
+@app.websocket("/ws/player/{client_id}")
+async def websocket_player_endpoint(websocket: WebSocket, client_id: str):
+    """The method that recives messages from the websocket of a player and sends the results back to the client.
+
+    Args:
+        websocket (WebSocket): The WebSocket connection object.
+        client_id (str): The ID of the client.
+    """
+    if not environment_handler.is_known_client_id(client_id):
+        log.warning(f"wrong websocket connection with {client_id=}")
+        return
+    await manager.connect_player(websocket, client_id)
+    log.debug(f"Client #{client_id} connected")
+    environment_handler.set_player_connected(client_id)
+    try:
+        while True:
+            message = await websocket.receive_text()
+            answer = manage_websocket_message(message, client_id)
+            if isinstance(answer, dict):
+                answer = json.dumps(answer)
+            await manager.send_personal_message(answer, websocket)
+
+    except WebSocketDisconnect:
+        manager.disconnect(client_id)
+        environment_handler.set_player_disconnected(client_id)
+        log.debug(f"Client #{client_id} disconnected")
+
+
+def main(host: str, port: int):
+    loop = asyncio.new_event_loop()
+    asyncio.set_event_loop(loop)
+    loop.create_task(environment_handler.environment_steps())
+    config = uvicorn.Config(app, host=host, port=port, loop=loop)
+    server = uvicorn.Server(config)
+    loop.run_until_complete(server.serve())
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Overcooked Simulator Game Server",
+        description="Game Engine Server: Starts overcooked game engine server.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+
+    url_and_port_arguments(parser)
+    args = parser.parse_args()
+    setup_logging(args.enable_websocket_logging)
+    main(args.url, args.port)
+    """
+    Or in console: 
+    uvicorn overcooked_simulator.fastapi_game_server:app --reload
+    """
diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py
new file mode 100644
index 0000000000000000000000000000000000000000..415e0ce302c93ce2857f58569e5ed89e41c0aacd
--- /dev/null
+++ b/overcooked_simulator/gui_2d_vis/drawing.py
@@ -0,0 +1,487 @@
+import colorsys
+import math
+from datetime import datetime, timedelta
+from pathlib import Path
+
+import numpy as np
+import numpy.typing as npt
+import pygame
+from scipy.spatial import KDTree
+
+from overcooked_simulator import ROOT_DIR
+from overcooked_simulator.gui_2d_vis.game_colors import colors
+from overcooked_simulator.state_representation import (
+    PlayerState,
+    CookingEquipmentState,
+    ItemState,
+)
+
+USE_PLAYER_COOK_SPRITES = True
+SHOW_INTERACTION_RANGE = False
+SHOW_COUNTER_CENTERS = False
+
+
+def create_polygon(n, length):
+    if n == 1:
+        return np.array([0, 0])
+
+    vector = np.array([length, 0])
+    angle = (2 * np.pi) / n
+
+    rot_matrix = np.array(
+        [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]
+    )
+
+    vecs = [vector]
+    for i in range(n - 1):
+        vector = np.dot(rot_matrix, vector)
+        vecs.append(vector)
+
+    return vecs
+
+
+class Visualizer:
+    def __init__(self, config):
+        self.image_cache_dict = {}
+        self.player_colors = []
+        self.config = config
+
+    def create_player_colors(self, n) -> None:
+        hue_values = np.linspace(0, 1, n + 1)
+
+        colors_vec = np.array([col for col in colors.values()])
+
+        tree = KDTree(colors_vec)
+
+        color_names = list(colors.keys())
+
+        self.player_colors = []
+        for hue in hue_values:
+            rgb = colorsys.hsv_to_rgb(hue, 1, 1)
+            query_color = np.array([int(c * 255) for c in rgb])
+            _, index = tree.query(query_color, k=1)
+            self.player_colors.append(color_names[index])
+
+    def draw_gamescreen(
+        self,
+        screen,
+        state,
+        width,
+        height,
+        grid_size,
+    ):
+        self.draw_background(
+            surface=screen,
+            width=width,
+            height=height,
+            grid_size=grid_size,
+        )
+        self.draw_counters(
+            screen,
+            state,
+            grid_size,
+        )
+
+        self.draw_players(
+            screen,
+            state,
+            grid_size,
+        )
+
+    def draw_background(self, surface, width, height, grid_size):
+        """Visualizes a game background."""
+        block_size = grid_size // 2  # Set the size of the grid block
+        surface.fill(colors[self.config["Kitchen"]["ground_tiles_color"]])
+        for x in range(0, width, block_size):
+            for y in range(0, height, block_size):
+                rect = pygame.Rect(x, y, block_size, block_size)
+                pygame.draw.rect(
+                    surface,
+                    self.config["Kitchen"]["background_lines"],
+                    rect,
+                    1,
+                )
+
+    def draw_image(
+        self,
+        screen: pygame.Surface,
+        img_path: Path | str,
+        size: float,
+        pos: npt.NDArray,
+        rot_angle=0,
+    ):
+        cache_entry = f"{img_path}"
+        if cache_entry in self.image_cache_dict.keys():
+            image = self.image_cache_dict[cache_entry]
+        else:
+            image = pygame.image.load(
+                ROOT_DIR / "gui_2d_vis" / img_path
+            ).convert_alpha()
+            self.image_cache_dict[cache_entry] = image
+
+        image = pygame.transform.scale(image, (size, size))
+        if rot_angle != 0:
+            image = pygame.transform.rotate(image, rot_angle)
+        rect = image.get_rect()
+        rect.center = pos
+
+        screen.blit(image, rect)
+
+    def draw_players(
+        self,
+        screen: pygame.Surface,
+        state_dict: dict,
+        grid_size: float,
+    ):
+        """Visualizes the players as circles with a triangle for the facing direction.
+        If the player holds something in their hands, it is displayed
+        Args:            state: The game state returned by the environment.
+        """
+        for p_idx, player_dict in enumerate(state_dict["players"]):
+            player_dict: PlayerState
+            pos = np.array(player_dict["pos"]) * grid_size
+
+            facing = np.array(player_dict["facing_direction"])
+
+            if USE_PLAYER_COOK_SPRITES:
+                img_path = self.config["Cook"]["parts"][0]["path"]
+                rel_x, rel_y = facing
+                angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90
+                size = self.config["Cook"]["parts"][0]["size"] * grid_size
+                self.draw_image(screen, img_path, size, pos, angle)
+
+            else:
+                size = 0.4 * grid_size
+                color1 = self.player_colors[p_idx]
+                color2 = colors["white"]
+
+                pygame.draw.circle(screen, color2, pos, size)
+                pygame.draw.circle(screen, colors["blue"], pos, size, width=1)
+                pygame.draw.circle(screen, colors[color1], pos, size // 2)
+
+                pygame.draw.polygon(
+                    screen,
+                    colors["blue"],
+                    (
+                        (
+                            pos[0] + (facing[1] * 0.1 * grid_size),
+                            pos[1] - (facing[0] * 0.1 * grid_size),
+                        ),
+                        (
+                            pos[0] - (facing[1] * 0.1 * grid_size),
+                            pos[1] + (facing[0] * 0.1 * grid_size),
+                        ),
+                        pos + (facing * 0.5 * grid_size),
+                    ),
+                )
+
+            if SHOW_INTERACTION_RANGE:
+                facing_point = np.array(player_dict["facing"])
+
+                pygame.draw.circle(
+                    screen,
+                    colors["blue"],
+                    facing_point * grid_size,
+                    1.6 * grid_size,
+                    width=1,
+                )
+                pygame.draw.circle(
+                    screen,
+                    colors["red1"],
+                    facing * grid_size,
+                    4,
+                )
+                pygame.draw.circle(screen, colors["red1"], facing, 4)
+
+            if player_dict["holding"] is not None:
+                holding_item_pos = pos + (20 * facing)
+                self.draw_item(
+                    pos=holding_item_pos,
+                    grid_size=grid_size,
+                    item=player_dict["holding"],
+                    screen=screen,
+                )
+
+                if player_dict["current_nearest_counter_pos"]:
+                    pos = player_dict["current_nearest_counter_pos"]
+                    pygame.draw.rect(
+                        screen,
+                        colors[self.player_colors[p_idx]],
+                        rect=pygame.Rect(
+                            pos[0] * grid_size - (grid_size // 2),
+                            pos[1] * grid_size - (grid_size // 2),
+                            grid_size,
+                            grid_size,
+                        ),
+                        width=2,
+                    )
+
+    def draw_thing(
+        self,
+        screen: pygame.Surface,
+        pos: npt.NDArray[float],
+        grid_size: float,
+        parts: list[dict[str]],
+        scale: float = 1.0,
+    ):
+        """Draws an item, based on its visual parts specified in the visualization config.
+
+        Args:
+            screen: the game screen to draw on.
+            grid_size: size of a grid cell.
+            pos: Where to draw the item parts.
+            parts: The visual parts to draw.
+            scale: Rescale the item by this factor.
+        """
+        for part in parts:
+            part_type = part["type"]
+            match part_type:
+                case "image":
+                    if "center_offset" in part:
+                        d = np.array(part["center_offset"]) * grid_size
+                        pos += d
+
+                    self.draw_image(
+                        screen,
+                        part["path"],
+                        part["size"] * scale * grid_size,
+                        pos,
+                    )
+                case "rect":
+                    height = part["height"] * grid_size
+                    width = part["width"] * grid_size
+                    color = part["color"]
+                    if "center_offset" in part:
+                        dx, dy = np.array(part["center_offset"]) * grid_size
+                        rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width)
+                        pygame.draw.rect(screen, color, rect)
+                    else:
+                        rect = pygame.Rect(
+                            pos[0] - (height / 2),
+                            pos[1] - (width / 2),
+                            height,
+                            width,
+                        )
+                    pygame.draw.rect(screen, color, rect)
+                case "circle":
+                    radius = part["radius"] * grid_size
+                    color = colors[part["color"]]
+                    if "center_offset" in part:
+                        pygame.draw.circle(
+                            screen,
+                            color,
+                            np.array(pos)
+                            + (np.array(part["center_offset"]) * grid_size),
+                            radius,
+                        )
+                    else:
+                        pygame.draw.circle(screen, color, pos, radius)
+
+    def draw_item(
+        self,
+        pos: npt.NDArray[float] | list[float],
+        grid_size: float,
+        item: ItemState | CookingEquipmentState,
+        scale: float = 1.0,
+        plate=False,
+        screen=None,
+    ):
+        """Visualization of an item at the specified position. On a counter or in the hands of the player.
+        The visual composition of the item is read in from visualization.yaml file, where it is specified as
+        different parts to be drawn.
+
+        Args:
+            grid_size: size of a grid cell.
+            pos: The position of the item to draw.
+            item: The item do be drawn in the game.
+            scale: Rescale the item by this factor.
+            screen: the pygame screen to draw on.
+            plate: item is on a plate (soup are is different on a plate and pot)
+        """
+
+        if not isinstance(item, list):  # can we remove this check?
+            if item["type"] in self.config:
+                item_key = item["type"]
+                if "Soup" in item_key and plate:
+                    item_key += "Plate"
+                self.draw_thing(
+                    pos=pos,
+                    parts=self.config[item_key]["parts"],
+                    scale=scale,
+                    screen=screen,
+                    grid_size=grid_size,
+                )
+                #
+        if "progress_percentage" in item and item["progress_percentage"] > 0.0:
+            self.draw_progress_bar(
+                screen, pos, item["progress_percentage"], grid_size=grid_size
+            )
+
+        if (
+            "content_ready" in item
+            and item["content_ready"]
+            and item["content_ready"]["type"] in self.config
+        ):
+            self.draw_thing(
+                pos=pos,
+                parts=self.config[item["content_ready"]["type"]]["parts"],
+                screen=screen,
+                grid_size=grid_size,
+            )
+        elif "content_list" in item and item["content_list"]:
+            triangle_offsets = create_polygon(len(item["content_list"]), length=10)
+            scale = 1 if len(item["content_list"]) == 1 else 0.6
+            for idx, o in enumerate(item["content_list"]):
+                self.draw_item(
+                    pos=np.array(pos) + triangle_offsets[idx],
+                    item=o,
+                    scale=scale,
+                    plate="Plate" in item["type"],
+                    screen=screen,
+                    grid_size=grid_size,
+                )
+
+    def draw_progress_bar(
+        self,
+        screen: pygame.Surface,
+        pos: npt.NDArray[float],
+        percent: float,
+        grid_size: float,
+    ):
+        """Visualize progress of progressing item as a green bar under the item."""
+        bar_height = grid_size * 0.2
+        progress_width = percent * grid_size
+        progress_bar = pygame.Rect(
+            pos[0] - (grid_size / 2),
+            pos[1] - (grid_size / 2) + grid_size - bar_height,
+            progress_width,
+            bar_height,
+        )
+        pygame.draw.rect(screen, colors["green1"], progress_bar)
+
+    def draw_counter(
+        self, screen: pygame.Surface, counter_dict: dict, grid_size: float
+    ):
+        """Visualization of a counter at its position. If it is occupied by an item, it is also shown.
+        The visual composition of the counter is read in from visualization.yaml file, where it is specified as
+        different parts to be drawn.
+        Args:            counter: The counter to visualize.
+        """
+        pos = np.array(counter_dict["pos"]) * grid_size
+        counter_type = counter_dict["type"]
+        self.draw_thing(screen, pos, grid_size, self.config["Counter"]["parts"])
+        if counter_type in self.config:
+            self.draw_thing(screen, pos, grid_size, self.config[counter_type]["parts"])
+        else:
+            if counter_type in self.config:
+                parts = self.config[counter_type]["parts"]
+            elif counter_type.endswith("Dispenser"):
+                parts = self.config["Dispenser"]["parts"]
+            else:
+                raise ValueError(f"Can not draw counter type {counter_type}")
+            self.draw_thing(
+                screen=screen,
+                pos=pos,
+                parts=parts,
+                grid_size=grid_size,
+            )
+
+        occupied_by = counter_dict["occupied_by"]
+        if occupied_by is not None:
+            # Multiple plates on plate return:
+            if isinstance(occupied_by, list):
+                for i, o in enumerate(occupied_by):
+                    self.draw_item(
+                        screen=screen,
+                        pos=np.abs([pos[0], pos[1] - (i * 3)]),
+                        grid_size=grid_size,
+                        item=o,
+                    )
+            # All other items:
+            else:
+                self.draw_item(
+                    pos=pos,
+                    grid_size=grid_size,
+                    item=occupied_by,
+                    screen=screen,
+                )
+
+    def draw_counters(
+        self, screen: pygame, state, grid_size, SHOW_COUNTER_CENTERS=False
+    ):
+        """Visualizes the counters in the environment.
+
+        Args:            state: The game state returned by the environment.
+        """
+        for counter in state["counters"]:
+            self.draw_counter(screen, counter, grid_size)
+            if SHOW_COUNTER_CENTERS:
+                pygame.draw.circle(screen, colors["green1"], counter.pos, 3)
+
+    def draw_orders(
+        self, screen, state, grid_size, width, height, screen_margin, config
+    ):
+        orders_width = width - 100
+        orders_height = screen_margin
+        order_screen = pygame.Surface(
+            (orders_width, orders_height),
+        )
+
+        bg_color = colors[config["GameWindow"]["background_color"]]
+        pygame.draw.rect(order_screen, bg_color, order_screen.get_rect())
+
+        order_rects_start = (orders_height // 2) - (grid_size // 2)
+        for idx, order in enumerate(state["orders"]):
+            order_upper_left = [
+                order_rects_start + idx * grid_size * 1.2,
+                order_rects_start,
+            ]
+            pygame.draw.rect(
+                order_screen,
+                colors["red"],
+                pygame.Rect(
+                    order_upper_left[0],
+                    order_upper_left[1],
+                    grid_size,
+                    grid_size,
+                ),
+                width=2,
+            )
+            center = np.array(order_upper_left) + np.array(
+                [grid_size / 2, grid_size / 2]
+            )
+            self.draw_thing(
+                pos=center,
+                parts=config["Plate"]["parts"],
+                screen=order_screen,
+                grid_size=grid_size,
+            )
+            self.draw_item(
+                pos=center,
+                item={"type": order["meal"]},
+                plate=True,
+                screen=order_screen,
+                grid_size=grid_size,
+            )
+            order_done_seconds = (
+                (
+                    datetime.fromisoformat(order["start_time"])
+                    + timedelta(seconds=order["max_duration"])
+                )
+                - datetime.fromisoformat(state["env_time"])
+            ).total_seconds()
+
+            percentage = order_done_seconds / order["max_duration"]
+            self.draw_progress_bar(
+                pos=center,
+                percent=percentage,
+                screen=order_screen,
+                grid_size=grid_size,
+            )
+
+        orders_rect = order_screen.get_rect()
+        orders_rect.center = [
+            screen_margin + (orders_width // 2),
+            orders_height // 2,
+        ]
+        screen.blit(order_screen, orders_rect)
diff --git a/overcooked_simulator/gui_2d_vis/images/basket.png b/overcooked_simulator/gui_2d_vis/images/basket.png
new file mode 100644
index 0000000000000000000000000000000000000000..64b1fd5fa30c24f9b4c5083d0d11f1402d913473
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/basket.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/bell_gold.png b/overcooked_simulator/gui_2d_vis/images/bell_gold.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c8f6dd4764808460c47d815a1c53fbfe944fa71
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/bell_gold.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/bell_silver.png b/overcooked_simulator/gui_2d_vis/images/bell_silver.png
new file mode 100644
index 0000000000000000000000000000000000000000..dcbe26374716ad470ceed59fdbca7955290f12d2
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/bell_silver.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/cheese3.png b/overcooked_simulator/gui_2d_vis/images/cheese3.png
new file mode 100644
index 0000000000000000000000000000000000000000..5e496d7a460aabf894d011bdb290a57f6eb14be1
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/cheese3.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/chopped_fish.png b/overcooked_simulator/gui_2d_vis/images/chopped_fish.png
new file mode 100644
index 0000000000000000000000000000000000000000..370a7e915b8fec7fdfe70f460421ed74a1faaf48
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/chopped_fish.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/cut_fish.png b/overcooked_simulator/gui_2d_vis/images/cut_fish.png
new file mode 100644
index 0000000000000000000000000000000000000000..5695519dfdb586208a2bf637c73b3b591e7cc2bd
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/cut_fish.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/drip2.png b/overcooked_simulator/gui_2d_vis/images/drip2.png
new file mode 100644
index 0000000000000000000000000000000000000000..71e93af76a9ae174232ecf59957b026d039dcb07
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/drip2.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fish3.png b/overcooked_simulator/gui_2d_vis/images/fish3.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a2425b0b291a051108f4b7643a80e313c561f09
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fish3.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fried_fish.png b/overcooked_simulator/gui_2d_vis/images/fried_fish.png
new file mode 100644
index 0000000000000000000000000000000000000000..0331afa82ee6f86924e1194c081bfc5fa6a74f23
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fried_fish.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/fries2.png b/overcooked_simulator/gui_2d_vis/images/fries2.png
new file mode 100644
index 0000000000000000000000000000000000000000..199e8e86099ee8cf43dd1a64e390974210fbc637
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fries2.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/grated_cheese.png b/overcooked_simulator/gui_2d_vis/images/grated_cheese.png
new file mode 100644
index 0000000000000000000000000000000000000000..d7d5f9b05a1a9c9aad300c71147f87dfe6ec8561
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/grated_cheese.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/pixel_cook_masked.png b/overcooked_simulator/gui_2d_vis/images/pixel_cook_masked.png
new file mode 100644
index 0000000000000000000000000000000000000000..6fd50039c26f85225c578d94ae2326792220daf2
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pixel_cook_masked.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza.png b/overcooked_simulator/gui_2d_vis/images/pizza.png
new file mode 100644
index 0000000000000000000000000000000000000000..0fd2a0a9fe8e759595b39408f4a5b766409ac894
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pizza.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza_base.png b/overcooked_simulator/gui_2d_vis/images/pizza_base.png
new file mode 100644
index 0000000000000000000000000000000000000000..06c5c93c72d3ca9c2d993089535c3249d1c6028d
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pizza_base.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza_dough.png b/overcooked_simulator/gui_2d_vis/images/pizza_dough.png
new file mode 100644
index 0000000000000000000000000000000000000000..a36531c53a4f69e377fd6d40a90b811ad6468bd1
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pizza_dough.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/pizza_wood.png b/overcooked_simulator/gui_2d_vis/images/pizza_wood.png
new file mode 100644
index 0000000000000000000000000000000000000000..398b0b7e98f2a4aa02bdaf93a978729eed839637
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pizza_wood.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/potato2.png b/overcooked_simulator/gui_2d_vis/images/potato2.png
new file mode 100644
index 0000000000000000000000000000000000000000..6384278192219a406b5ca2c651c4d830a13dbdd5
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/potato2.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/raw_fries.png b/overcooked_simulator/gui_2d_vis/images/raw_fries.png
new file mode 100644
index 0000000000000000000000000000000000000000..32f9114d96165dc99179ad30a3d970af51e1121c
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/raw_fries.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/sausage.png b/overcooked_simulator/gui_2d_vis/images/sausage.png
new file mode 100644
index 0000000000000000000000000000000000000000..8431ff7763163669829cb0034bb0a4119d7db66a
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/sausage.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/sausage_chopped.png b/overcooked_simulator/gui_2d_vis/images/sausage_chopped.png
new file mode 100644
index 0000000000000000000000000000000000000000..d0b86fc0418f6961827ee9a6cd775d7177305593
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/sausage_chopped.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/sink.png b/overcooked_simulator/gui_2d_vis/images/sink.png
new file mode 100644
index 0000000000000000000000000000000000000000..2e41d5624d7b69b0cca3626749de6ff8b6cd9c87
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/sink.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/sink1.png b/overcooked_simulator/gui_2d_vis/images/sink1.png
new file mode 100644
index 0000000000000000000000000000000000000000..5ac81630f6325efdf8d6c56e5ca4336f908a01e4
Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/sink1.png differ
diff --git a/overcooked_simulator/gui_2d_vis/images/trash2.png b/overcooked_simulator/gui_2d_vis/images/trash2.png
deleted file mode 100644
index 76c76c47209ab25636a3952ab77e605fd3a7dd8f..0000000000000000000000000000000000000000
Binary files a/overcooked_simulator/gui_2d_vis/images/trash2.png and /dev/null differ
diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
index ed86bfadef2c895c956222e211e6c8aab0066e16..1370d8475ccf53668005af4902b9939b1420d201 100644
--- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py
+++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py
@@ -1,38 +1,32 @@
-import colorsys
+import argparse
+import dataclasses
+import json
 import logging
-import math
 import sys
-from collections import deque
-from datetime import timedelta
 from enum import Enum
 
 import numpy as np
-import numpy.typing as npt
 import pygame
 import pygame_gui
+import requests
 import yaml
-from scipy.spatial import KDTree
+from websockets.sync.client import connect
 
 from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.counters import Counter
-from overcooked_simulator.game_items import (
-    Item,
-    CookingEquipment,
-    Plate,
-)
-from overcooked_simulator.gui_2d_vis.game_colors import BLUE
-from overcooked_simulator.gui_2d_vis.game_colors import colors, Color
-from overcooked_simulator.order import Order
+from overcooked_simulator.game_server import CreateEnvironmentConfig
+from overcooked_simulator.gui_2d_vis.drawing import Visualizer
+from overcooked_simulator.gui_2d_vis.game_colors import colors
 from overcooked_simulator.overcooked_environment import (
     Action,
     ActionType,
     InterActionData,
 )
-from overcooked_simulator.simulation_runner import Simulator
-
-USE_PLAYER_COOK_SPRITES = True
-SHOW_INTERACTION_RANGE = False
-SHOW_COUNTER_CENTERS = False
+from overcooked_simulator.utils import (
+    custom_asdict_factory,
+    setup_logging,
+    url_and_port_arguments,
+    disable_websocket_logging_arguments,
+)
 
 
 class MenuStates(Enum):
@@ -41,23 +35,7 @@ class MenuStates(Enum):
     End = "End"
 
 
-def create_polygon(n, length):
-    if n == 1:
-        return np.array([0, 0])
-
-    vector = np.array([length, 0])
-    angle = (2 * np.pi) / n
-
-    rot_matrix = np.array(
-        [[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]
-    )
-
-    vecs = [vector]
-    for i in range(n - 1):
-        vector = np.dot(rot_matrix, vector)
-        vecs.append(vector)
-
-    return vecs
+MANAGER_ID = "1233245425"
 
 
 log = logging.getLogger(__name__)
@@ -65,14 +43,12 @@ log = logging.getLogger(__name__)
 
 class PlayerKeySet:
     """Set of keyboard keys for controlling a player.
-    First four keys are for movement. Order: Down, Up, Left, Right.
-    5th key is for interacting with counters.
-    6th key ist for picking up things or dropping them.
-
+    First four keys are for movement. Order: Down, Up, Left, Right.    5th key is for interacting with counters.    6th key ist for picking up things or dropping them.
     """
 
-    def __init__(self, player_name: str, keys: list[pygame.key]):
+    def __init__(self, player_name: str | int, keys: list[pygame.key]):
         """Creates a player key set which contains information about which keyboard keys control the player.
+
         Movement keys in the following order: Down, Up, Left, Right
 
         Args:
@@ -94,13 +70,13 @@ class PyGameGUI:
 
     def __init__(
         self,
-        simulator: Simulator,
-        player_names: list[str],
+        player_names: list[str | int],
         player_keys: list[pygame.key],
+        url: str,
+        port: str,
     ):
         self.game_screen = None
         self.FPS = 60
-        self.simulator: Simulator = simulator
         self.running = True
 
         self.player_names = player_names
@@ -113,6 +89,11 @@ class PyGameGUI:
             )
         ]
 
+        self.websocket_url = f"ws://{url}:{port}/ws/player/"
+        self.websockets = {}
+
+        self.request_url = f"http://{url}:{port}"
+
         # TODO cache loaded images?
         with open(ROOT_DIR / "gui_2d_vis" / "visualization.yaml", "r") as file:
             self.visualization_config = yaml.safe_load(file)
@@ -139,13 +120,14 @@ class PyGameGUI:
 
         self.images_path = ROOT_DIR / "pygame_gui" / "images"
 
-        self.image_cache_dict = {}
-
         self.menu_state = MenuStates.Start
         self.manager: pygame_gui.UIManager
 
-    def get_window_sizes(self, state: dir):
-        counter_positions = np.array([c.pos for c in state["counters"]])
+        self.vis = Visualizer(self.visualization_config)
+        self.vis.create_player_colors(len(self.player_names))
+
+    def get_window_sizes(self, state: dict):
+        counter_positions = np.array([c["pos"] for c in state["counters"]])
         kitchen_width = counter_positions[:, 0].max() + 0.5
         kitchen_height = counter_positions[:, 1].max() + 0.5
         if self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_width":
@@ -181,38 +163,11 @@ class PyGameGUI:
         return (
             int(window_width),
             int(window_height),
-            game_width,
-            game_height,
+            int(game_width),
+            int(game_height),
             grid_size,
         )
 
-    def create_player_colors(self) -> list[Color]:
-        number_player = len(self.simulator.env.players)
-        hue_values = np.linspace(0, 1, number_player + 1)
-
-        colors_vec = np.array([col for col in colors.values()])
-
-        tree = KDTree(colors_vec)
-
-        color_names = list(colors.keys())
-
-        player_colors = []
-        for hue in hue_values:
-            rgb = colorsys.hsv_to_rgb(hue, 1, 1)
-            query_color = np.array([int(c * 255) for c in rgb])
-            _, index = tree.query(query_color, k=1)
-            player_colors.append(color_names[index])
-
-        return player_colors
-
-    def send_action(self, action: Action):
-        """Sends an action to the game environment.
-
-        Args:
-            action: The action to be sent. Contains the player, action type and move direction if action is a movement.
-        """
-        self.simulator.enter_action(action)
-
     def handle_keys(self):
         """Handles keyboard inputs. Sends action for the respective players. When a key is held down, every frame
         an action is sent in this function.
@@ -228,7 +183,9 @@ class PyGameGUI:
                 if np.linalg.norm(move_vec) != 0:
                     move_vec = move_vec / np.linalg.norm(move_vec)
 
-                action = Action(key_set.name, ActionType.MOVEMENT, move_vec)
+                action = Action(
+                    key_set.name, ActionType.MOVEMENT, move_vec, duration=1 / self.FPS
+                )
                 self.send_action(action)
 
     def handle_key_event(self, event):
@@ -256,401 +213,6 @@ class PyGameGUI:
                     )
                     self.send_action(action)
 
-    def draw_background(self):
-        """Visualizes a game background."""
-        block_size = self.grid_size // 2  # Set the size of the grid block
-        self.game_screen.fill(
-            colors[self.visualization_config["Kitchen"]["ground_tiles_color"]]
-        )
-        for x in range(0, self.window_width, block_size):
-            for y in range(0, self.window_height, block_size):
-                rect = pygame.Rect(x, y, block_size, block_size)
-                pygame.draw.rect(
-                    self.game_screen,
-                    self.visualization_config["Kitchen"]["background_lines"],
-                    rect,
-                    1,
-                )
-
-    def draw_image(
-        self, img_path, size, pos, rot_angle=0, screen: pygame.Surface = None
-    ):
-        cache_entry = f"{img_path}"
-        if cache_entry in self.image_cache_dict.keys():
-            image = self.image_cache_dict[cache_entry]
-        else:
-            image = pygame.image.load(
-                ROOT_DIR / "gui_2d_vis" / img_path
-            ).convert_alpha()
-            self.image_cache_dict[cache_entry] = image
-
-        image = pygame.transform.scale(image, (size, size))
-        if rot_angle != 0:
-            image = pygame.transform.rotate(image, rot_angle)
-        rect = image.get_rect()
-        rect.center = pos
-
-        if screen is None:
-            self.game_screen.blit(image, rect)
-        else:
-            screen.blit(image, rect)
-
-    def draw_players(self, state):
-        """Visualizes the players as circles with a triangle for the facing direction.
-        If the player holds something in their hands, it is displayed
-
-        Args:
-            state: The game state returned by the environment.
-        """
-        for p_idx, player in enumerate(state["players"].values()):
-            pos = player.pos * self.grid_size
-
-            if USE_PLAYER_COOK_SPRITES:
-                img_path = self.visualization_config["Cook"]["parts"][0]["path"]
-                rel_x, rel_y = player.facing_direction
-                angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90
-                size = (
-                    self.visualization_config["Cook"]["parts"][0]["size"]
-                    * self.grid_size
-                )
-                self.draw_image(img_path, size, pos, angle)
-
-            else:
-                size = player.radius * self.grid_size
-                color1 = self.player_colors[p_idx]
-                color2 = colors["white"]
-
-                pygame.draw.circle(self.game_screen, color2, pos, size)
-                pygame.draw.circle(self.game_screen, BLUE, pos, size, width=1)
-                pygame.draw.circle(self.game_screen, colors[color1], pos, size // 2)
-
-                facing = player.facing_direction
-                pygame.draw.polygon(
-                    self.game_screen,
-                    BLUE,
-                    (
-                        (
-                            pos[0] + (facing[1] * 0.1 * self.grid_size),
-                            pos[1] - (facing[0] * 0.1 * self.grid_size),
-                        ),
-                        (
-                            pos[0] - (facing[1] * 0.1 * self.grid_size),
-                            pos[1] + (facing[0] * 0.1 * self.grid_size),
-                        ),
-                        pos + (facing * 0.5 * self.grid_size),
-                    ),
-                )
-
-            if SHOW_INTERACTION_RANGE:
-                pygame.draw.circle(
-                    self.game_screen,
-                    BLUE,
-                    player.facing_point * self.grid_size,
-                    player.interaction_range * self.grid_size,
-                    width=1,
-                )
-                pygame.draw.circle(
-                    self.game_screen,
-                    colors["red1"],
-                    player.facing_point * self.grid_size,
-                    4,
-                )
-                pygame.draw.circle(
-                    self.game_screen, colors["red1"], player.facing_point, 4
-                )
-
-            if player.holding is not None:
-                holding_item_pos = (player.pos * self.grid_size) + (
-                    20 * player.facing_direction
-                )
-                self.draw_item(holding_item_pos, player.holding)
-
-            if player.current_nearest_counter:
-                counter: Counter = player.current_nearest_counter
-                pos = counter.pos * self.grid_size
-                pygame.draw.rect(
-                    self.game_screen,
-                    colors[self.player_colors[p_idx]],
-                    rect=pygame.Rect(
-                        pos[0] - (self.grid_size // 2),
-                        pos[1] - (self.grid_size // 2),
-                        self.grid_size,
-                        self.grid_size,
-                    ),
-                    width=2,
-                )
-
-    def draw_thing(
-        self,
-        pos: npt.NDArray[float],
-        parts: list[dict[str]],
-        scale: float = 1.0,
-        screen: pygame.Surface = None,
-    ):
-        """Draws an item, based on its visual parts specified in the visualization config.
-
-        Args:
-            pos: Where to draw the item parts.
-            parts: The visual parts to draw.
-            scale: Rescale the item by this factor.
-        """
-
-        if screen is None:
-            screen = self.game_screen
-
-        for part in parts:
-            part_type = part["type"]
-            match part_type:
-                case "image":
-                    if "center_offset" in part:
-                        d = np.array(part["center_offset"]) * self.grid_size
-                        pos += d
-
-                    self.draw_image(
-                        part["path"],
-                        part["size"] * scale * self.grid_size,
-                        pos,
-                        screen=screen,
-                    )
-                case "rect":
-                    height = part["height"] * self.grid_size
-                    width = part["width"] * self.grid_size
-                    color = part["color"]
-                    if "center_offset" in part:
-                        dx, dy = np.array(part["center_offset"]) * self.grid_size
-                        rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width)
-                        pygame.draw.rect(screen, color, rect)
-                    else:
-                        rect = pygame.Rect(
-                            pos[0] - (height / 2),
-                            pos[1] - (width / 2),
-                            height,
-                            width,
-                        )
-                    pygame.draw.rect(screen, color, rect)
-                case "circle":
-                    radius = part["radius"] * self.grid_size
-                    color = colors[part["color"]]
-                    if "center_offset" in part:
-                        pygame.draw.circle(
-                            self.game_screen,
-                            color,
-                            pos + (np.array(part["center_offset"]) * self.grid_size),
-                            radius,
-                        )
-                    else:
-                        pygame.draw.circle(screen, color, pos, radius)
-
-    def draw_item(
-        self,
-        pos: npt.NDArray[float],
-        item: Item,
-        scale: float = 1.0,
-        plate=False,
-        screen=None,
-    ):
-        """Visualization of an item at the specified position. On a counter or in the hands of the player.
-        The visual composition of the item is read in from visualization.yaml file, where it is specified as
-        different parts to be drawn.
-
-        Args:
-            pos: The position of the item to draw.
-            item: The item do be drawn in the game.
-            scale: Rescale the item by this factor.
-            screen: the pygame screen to draw on.
-            plate: item is on a plate (soup are is different on a plate and pot)
-        """
-
-        if not isinstance(item, list):
-            if item.name in self.visualization_config:
-                item_key = item.name
-                if "Soup" in item.name and plate:
-                    item_key += "Plate"
-                self.draw_thing(
-                    pos,
-                    self.visualization_config[item_key]["parts"],
-                    scale=scale,
-                    screen=screen,
-                )
-
-        if isinstance(item, (Item, Plate)) and item.progress_percentage > 0.0:
-            self.draw_progress_bar(pos, item.progress_percentage)
-
-        if isinstance(item, CookingEquipment) and item.content_list:
-            if (
-                item.content_ready
-                and item.content_ready.name in self.visualization_config
-            ):
-                self.draw_thing(
-                    pos,
-                    self.visualization_config[item.content_ready.name]["parts"],
-                    screen=screen,
-                )
-            else:
-                triangle_offsets = create_polygon(len(item.content_list), length=10)
-                scale = 1 if len(item.content_list) == 1 else 0.6
-                for idx, o in enumerate(item.content_list):
-                    self.draw_item(
-                        pos + triangle_offsets[idx],
-                        o,
-                        scale=scale,
-                        plate=isinstance(item, Plate),
-                        screen=screen,
-                    )
-
-        # if isinstance(item, Meal):
-        #     if item.finished:
-        #         if item.name in self.visualization_config:
-        #             self.draw_thing(pos, self.visualization_config[item.name]["parts"])
-        #     else:
-        #         for idx, o in enumerate(item.parts):
-        #             triangle_offsets = create_polygon(len(item.parts), length=10)
-        #             self.draw_item(pos + triangle_offsets[idx], o, scale=0.6)
-
-    def draw_progress_bar(self, pos, percent, screen=None):
-        """Visualize progress of progressing item as a green bar under the item."""
-        bar_height = self.grid_size * 0.2
-        progress_width = percent * self.grid_size
-        progress_bar = pygame.Rect(
-            pos[0] - (self.grid_size / 2),
-            pos[1] - (self.grid_size / 2) + self.grid_size - bar_height,
-            progress_width,
-            bar_height,
-        )
-        if screen is None:
-            pygame.draw.rect(self.game_screen, colors["green1"], progress_bar)
-        else:
-            pygame.draw.rect(screen, colors["green1"], progress_bar)
-
-    def draw_counter(self, counter):
-        """Visualization of a counter at its position. If it is occupied by an item, it is also shown.
-        The visual composition of the counter is read in from visualization.yaml file, where it is specified as
-        different parts to be drawn.
-
-        Args:
-            counter: The counter to visualize.
-        """
-
-        pos = counter.pos * self.grid_size
-        self.draw_thing(pos, self.visualization_config["Counter"]["parts"])
-        if str(counter) in self.visualization_config:
-            self.draw_thing(pos, self.visualization_config[str(counter)]["parts"])
-        else:
-            self.draw_thing(
-                pos,
-                self.visualization_config[counter.__class__.__name__]["parts"],
-            )
-
-        if counter.occupied_by is not None:
-            # Multiple plates on plate return:
-            if isinstance(counter.occupied_by, (list, deque)):
-                with self.simulator.env.lock:
-                    for i, o in enumerate(counter.occupied_by):
-                        self.draw_item(np.abs([pos[0], pos[1] - (i * 3)]), o)
-            # All other items:
-            else:
-                self.draw_item(pos, counter.occupied_by)
-
-    def draw_counters(self, state):
-        """Visualizes the counters in the environment.
-
-        Args:
-            state: The game state returned by the environment.
-        """
-        for counter in state["counters"]:
-            self.draw_counter(counter)
-            if SHOW_COUNTER_CENTERS:
-                pygame.draw.circle(self.game_screen, colors["green1"], counter.pos, 3)
-
-    def update_score_label(self, state):
-        score = state["score"]
-        self.score_label.set_text(f"Score {score}")
-
-    def update_conclusion_label(self, state):
-        score = state["score"]
-        self.conclusion_label.set_text(f"Your final score is {score}. Hurray!")
-
-    def update_remaining_time(self, remaining_time: timedelta):
-        hours, rem = divmod(remaining_time.seconds, 3600)
-        minutes, seconds = divmod(rem, 60)
-        display_time = f"{minutes}:{'%02d' % seconds}"
-        self.timer_label.set_text(f"Time remaining: {display_time}")
-
-    def draw_orders(self, state):
-        orders_width = self.game_width - 100
-        orders_height = self.screen_margin
-
-        order_screen = pygame.Surface(
-            (orders_width, orders_height),
-        )
-
-        bg_color = colors[self.visualization_config["GameWindow"]["background_color"]]
-        pygame.draw.rect(order_screen, bg_color, order_screen.get_rect())
-
-        order_rects_start = (orders_height // 2) - (self.grid_size // 2)
-        with self.simulator.env.lock:
-            for idx, order in enumerate(state["orders"]):
-                order: Order
-                order_upper_left = [
-                    order_rects_start + idx * self.grid_size * 1.2,
-                    order_rects_start,
-                ]
-                pygame.draw.rect(
-                    order_screen,
-                    colors["red"],
-                    pygame.Rect(
-                        order_upper_left[0],
-                        order_upper_left[1],
-                        self.grid_size,
-                        self.grid_size,
-                    ),
-                    width=2,
-                )
-                center = np.array(order_upper_left) + np.array(
-                    [self.grid_size / 2, self.grid_size / 2]
-                )
-                self.draw_thing(
-                    center,
-                    self.visualization_config["Plate"]["parts"],
-                    screen=order_screen,
-                )
-                self.draw_item(
-                    center,
-                    order.meal,
-                    plate=True,
-                    screen=order_screen,
-                )
-                order_done_seconds = (
-                    (order.start_time + order.max_duration) - state["env_time"]
-                ).total_seconds()
-
-                percentage = order_done_seconds / order.max_duration.total_seconds()
-                self.draw_progress_bar(center, percentage, screen=order_screen)
-
-        orders_rect = order_screen.get_rect()
-        orders_rect.center = [
-            self.screen_margin + (orders_width // 2),
-            orders_height // 2,
-        ]
-        self.main_window.blit(order_screen, orders_rect)
-
-    def draw(self, state):
-        """Main visualization function.
-
-        Args:
-            state: The game state returned by the environment.
-        """
-
-        self.draw_background()
-
-        self.draw_counters(state)
-        self.draw_players(state)
-        self.manager.draw_ui(self.main_window)
-        self.update_remaining_time(state["remaining_time"])
-
-        self.draw_orders(state)
-        self.update_score_label(state)
-
     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")
@@ -682,6 +244,20 @@ class PyGameGUI:
         )
         self.quit_button.can_hover()
 
+        self.reset_button = pygame_gui.elements.UIButton(
+            relative_rect=pygame.Rect(
+                (
+                    (self.screen_margin + self.game_width),
+                    self.screen_margin,
+                ),
+                (self.screen_margin, 100),
+            ),
+            text="RESET",
+            manager=self.manager,
+            object_id="#quit_button",
+        )
+        self.reset_button.can_hover()
+
         self.finished_button = pygame_gui.elements.UIButton(
             relative_rect=pygame.Rect(
                 (
@@ -763,17 +339,43 @@ class PyGameGUI:
             object_id="#score_label",
         )
 
-    def set_window_size(self, window_width, window_height, game_width, game_height):
+    def draw(self, state):
+        """Main visualization function.
+
+        Args:            state: The game state returned by the environment."""
+        self.vis.draw_gamescreen(
+            self.game_screen,
+            state,
+            self.game_width,
+            self.game_height,
+            self.grid_size,
+        )
+
+        # self.manager.draw_ui(self.main_window)
+        self.update_remaining_time(state["remaining_time"])
+
+        self.vis.draw_orders(
+            screen=self.main_window,
+            state=state,
+            grid_size=self.grid_size,
+            width=self.game_width,
+            height=self.game_height,
+            screen_margin=self.screen_margin,
+            config=self.visualization_config,
+        )
+        self.update_score_label(state)
+
+    def set_window_size(self):
         self.game_screen = pygame.Surface(
             (
-                game_width,
-                game_height,
+                self.game_width,
+                self.game_height,
             ),
         )
         self.main_window = pygame.display.set_mode(
             (
-                window_width,
-                window_height,
+                self.window_width,
+                self.window_height,
             )
         )
 
@@ -782,25 +384,9 @@ class PyGameGUI:
         self.window_height = self.min_height
         self.game_width = 0
         self.game_height = 0
-        self.set_window_size(self.min_width, self.min_height, 0, 0)
+        self.set_window_size()
         self.init_ui_elements()
 
-    def setup_simulation(self, config_path, layout_path):
-        self.simulator = Simulator(config_path, layout_path, 600)
-        number_player = len(self.player_names)
-        for i in range(number_player):
-            player_name = f"p{i}"
-            self.simulator.register_player(player_name)
-        self.simulator.start()
-        (
-            self.window_width,
-            self.window_height,
-            self.game_width,
-            self.game_height,
-            self.grid_size,
-        ) = self.get_window_sizes(self.simulator.get_state())
-        self.player_colors = self.create_player_colors()
-
     def manage_button_visibility(self):
         match self.menu_state:
             case MenuStates.Start:
@@ -833,41 +419,160 @@ class PyGameGUI:
                 self.orders_label.hide()
                 self.conclusion_label.show()
 
-    def start_button_press(self):
-        self.menu_state = MenuStates.Game
+    def update_score_label(self, state):
+        score = state["score"]
+        self.score_label.set_text(f"Score {score}")
 
-        layout_path = (
-            ROOT_DIR
-            / "game_content"
-            / "layouts"
-            / self.layout_selection.selected_option
+    def update_conclusion_label(self, state):
+        score = state["score"]
+        self.conclusion_label.set_text(f"Your final score is {score}. Hurray!")
+
+    def update_remaining_time(self, remaining_time: float):
+        hours, rem = divmod(int(remaining_time), 3600)
+        minutes, seconds = divmod(rem, 60)
+        display_time = f"{minutes}:{'%02d' % seconds}"
+        self.timer_label.set_text(f"Time remaining: {display_time}")
+
+    def setup_environment(self):
+        environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
+        layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
+        item_info_path = ROOT_DIR / "game_content" / "item_info_debug.yaml"
+        with open(item_info_path, "r") as file:
+            item_info = file.read()
+        with open(layout_path, "r") as file:
+            layout = file.read()
+        with open(environment_config_path, "r") as file:
+            environment_config = file.read()
+        creation_json = CreateEnvironmentConfig(
+            manager_id=MANAGER_ID,
+            number_players=2,
+            environment_settings={"all_player_can_pause_game": False},
+            item_info_config=item_info,
+            environment_config=environment_config,
+            layout_config=layout,
+        ).model_dump(mode="json")
+        # print(CreateEnvironmentConfig.model_validate_json(json_data=creation_json))
+        env_info = requests.post(
+            f"{self.request_url}/manage/create_env/",
+            json=creation_json,
         )
-        config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
+        env_info = env_info.json()
+        assert isinstance(env_info, dict), "Env info must be a dictionary"
+        self.current_env_id = env_info["env_id"]
+        self.player_info = env_info["player_info"]
+        for player_id, player_info in env_info["player_info"].items():
+            websocket = connect(self.websocket_url + player_info["client_id"])
+            websocket.send(
+                json.dumps({"type": "ready", "player_hash": player_info["player_hash"]})
+            )
+            assert json.loads(websocket.recv())["status"] == 200, "not accepted player"
+            self.websockets[player_id] = websocket
+        self.state_player_id = player_id
+        websocket.send(
+            json.dumps({"type": "get_state", "player_hash": player_info["player_hash"]})
+        )
+        state = json.loads(websocket.recv())
 
-        self.setup_simulation(config_path, layout_path)
+        (
+            self.window_width,
+            self.window_height,
+            self.game_width,
+            self.game_height,
+            self.grid_size,
+        ) = self.get_window_sizes(state)
+
+    def start_button_press(self):
+        self.menu_state = MenuStates.Game
+
+        self.setup_environment()
 
-        self.set_window_size(*(self.get_window_sizes(self.simulator.get_state()))[:-1])
+        self.set_window_size()
 
         self.init_ui_elements()
         log.debug("Pressed start button")
 
+        # self.api.set_sim(self.simulator)
+
     def back_button_press(self):
         self.menu_state = MenuStates.Start
         self.reset_window_size()
-        self.simulator.stop()
         log.debug("Pressed back button")
 
     def quit_button_press(self):
-        self.simulator.stop()
         self.running = False
+        self.menu_state = MenuStates.Start
         log.debug("Pressed quit button")
 
+    def reset_button_press(self):
+        requests.post(
+            f"{self.request_url}/manage/stop_env",
+            json={
+                "manager_id": MANAGER_ID,
+                "env_id": self.current_env_id,
+                "reason": "reset button pressed",
+            },
+        )
+
+        # self.websocket.send(json.dumps("reset_game"))
+        # answer = self.websocket.recv()        log.debug("Pressed reset button")
+
     def finished_button_press(self):
+        requests.post(
+            f"{self.request_url}/manage/stop_env",
+            json={
+                "manager_id": MANAGER_ID,
+                "env_id": self.current_env_id,
+                "reason": "finish button pressed",
+            },
+        )
         self.menu_state = MenuStates.End
         self.reset_window_size()
-        self.simulator.stop()
         log.debug("Pressed finished button")
 
+    def send_action(self, action: Action):
+        """Sends an action to the game environment.
+
+        Args:
+            action: The action to be sent. Contains the player, action type and move direction if action is a movement.
+        """
+        if isinstance(action.action_data, np.ndarray):
+            action.action_data = [
+                float(action.action_data[0]),
+                float(action.action_data[1]),
+            ]
+        self.websockets[action.player].send(
+            json.dumps(
+                {
+                    "type": "action",
+                    "action": dataclasses.asdict(
+                        action, dict_factory=custom_asdict_factory
+                    ),
+                    "player_hash": self.player_info[action.player]["player_hash"],
+                }
+            )
+        )
+        self.websockets[action.player].recv()
+
+    def request_state(self):
+        self.websockets[self.state_player_id].send(
+            json.dumps(
+                {
+                    "type": "get_state",
+                    "player_hash": self.player_info[self.state_player_id][
+                        "player_hash"
+                    ],
+                }
+            )
+        )
+        # self.websocket.send(json.dumps("get_state"))
+        # state_dict = json.loads(self.websocket.recv())
+        state = json.loads(self.websockets[self.state_player_id].recv())
+        return state
+
+    def disconnect_websockets(self):
+        for websocket in self.websockets.values():
+            websocket.close()
+
     def start_pygame(self):
         """Starts pygame and the gui loop. Each frame the game state is visualized and keyboard inputs are read."""
         log.debug(f"Starting pygame gui at {self.FPS} fps")
@@ -892,17 +597,25 @@ class PyGameGUI:
                     if event.type == pygame.QUIT:
                         self.running = False
 
-                    # UI Buttons:
+                        # UI Buttons:
                     if event.type == pygame_gui.UI_BUTTON_PRESSED:
                         match event.ui_element:
                             case self.start_button:
                                 self.start_button_press()
                             case self.back_button:
                                 self.back_button_press()
+                                self.disconnect_websockets()
+
                             case self.finished_button:
                                 self.finished_button_press()
+                                self.disconnect_websockets()
                             case self.quit_button:
                                 self.quit_button_press()
+                                self.disconnect_websockets()
+                            case self.reset_button:
+                                self.reset_button_press()
+                                self.disconnect_websockets()
+                                self.start_button_press()
 
                         self.manage_button_visibility()
 
@@ -910,14 +623,12 @@ class PyGameGUI:
                         event.type in [pygame.KEYDOWN, pygame.KEYUP]
                         and self.menu_state == MenuStates.Game
                     ):
+                        pass
                         self.handle_key_event(event)
 
                     self.manager.process_events(event)
 
-                # drawing:
-
-                state = self.simulator.get_state()
-
+                    # drawing:
                 self.main_window.fill(
                     colors[self.visualization_config["GameWindow"]["background_color"]]
                 )
@@ -928,12 +639,13 @@ class PyGameGUI:
                         pass
 
                     case MenuStates.Game:
-                        self.draw_background()
+                        state = self.request_state()
 
                         self.handle_keys()
 
                         if state["ended"]:
                             self.finished_button_press()
+                            self.disconnect_websockets()
                             self.manage_button_visibility()
                         else:
                             self.draw(state)
@@ -943,7 +655,6 @@ class PyGameGUI:
                                 self.window_width // 2,
                                 self.window_height // 2,
                             ]
-
                             self.main_window.blit(self.game_screen, game_screen_rect)
 
                     case MenuStates.End:
@@ -952,11 +663,41 @@ class PyGameGUI:
                 self.manager.update(time_delta)
                 pygame.display.flip()
 
-            except KeyboardInterrupt:
-                self.simulator.stop()
-                pygame.quit()
-                sys.exit()
+            except (KeyboardInterrupt, SystemExit):
+                self.running = False
 
-        self.simulator.stop()
         pygame.quit()
         sys.exit()
+
+
+def main(url, port):
+    # TODO maybe read the player names and keyboard keys from config file?
+    keys1 = [
+        pygame.K_LEFT,
+        pygame.K_RIGHT,
+        pygame.K_UP,
+        pygame.K_DOWN,
+        pygame.K_SPACE,
+        pygame.K_i,
+    ]
+    keys2 = [pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s, pygame.K_f, pygame.K_e]
+
+    number_players = 2
+    gui = PyGameGUI(
+        list(map(str, range(number_players))), [keys1, keys2], url=url, port=port
+    )
+    gui.start_pygame()
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        prog="Overcooked Simulator 2D PyGame Visualization",
+        description="PyGameGUI: a PyGame 2D Visualization window.",
+        epilog="For further information, see https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator/overcooked_simulator.html",
+    )
+
+    url_and_port_arguments(parser)
+    disable_websocket_logging_arguments(parser)
+    args = parser.parse_args()
+    setup_logging(enable_websocket_logging=args.enable_websocket_logging)
+    main(args.url, args.port)
diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml
index a4ba4f7ec354aa825fe8eb09b44ee19995015eaa..62587a97d0f8e052a989e587d931ee7298c1193b 100644
--- a/overcooked_simulator/gui_2d_vis/visualization.yaml
+++ b/overcooked_simulator/gui_2d_vis/visualization.yaml
@@ -2,7 +2,7 @@
 
 GameWindow:
   WhatIsFixed: grid  # grid or window_width or window_height
-  size: 50
+  size: 60
   screen_margin: 100
   min_width: 700
   min_height: 600
@@ -81,6 +81,12 @@ BunDispenser:
       height: 0.8
       width: 0.8
 
+Dispenser:
+  parts:
+    - color: gray83
+      type: rect
+      height: 0.8
+      width: 0.8
 
 ServingWindow:
   parts:
@@ -88,6 +94,10 @@ ServingWindow:
       path: images/arrow_right.png
       size: 1
       center_offset: [ 0, 0 ]
+    - type: image
+      path: images/bell_gold.png
+      size: 0.5
+      center_offset: [ 0.1, -0.4 ]
 
 Stove:
   parts:
@@ -102,30 +112,17 @@ Stove:
 Sink:
   parts:
     - type: image
-      path: images/sink_large.png
-      size: 0.9
+      path: images/sink1.png
+      size: 1
+      center_offset: [ 0, -0.05 ]
 
 SinkAddon:
   parts:
-    - color: black
-      type: rect
-      height: 0.875
-      width: 0.625
-    - type: rect
-      color: gray83
-      height: 0.8
-      width: 0.1
-      center_offset: [ -0.4, 0.1 ]
-    - type: rect
-      color: gray83
-      height: 0.8
-      width: 0.1
-      center_offset: [ -0.4, -0.1 ]
-    - type: rect
-      color: gray83
-      height: 0.8
-      width: 0.1
-      center_offset: [ -0.4, -0.3 ]
+    - type: image
+      path: images/drip2.png
+      size: 0.85
+      center_offset: [ 0, 0.03 ]
+
 # Items
 Tomato:
   parts:
@@ -175,7 +172,7 @@ ChoppedOnion:
       path: images/onion_cut.png
       size: 0.95
 
-ChoppedMeat:
+RawPatty:
   parts:
     - type: image
       path: images/raw_patty.png
@@ -204,7 +201,7 @@ TomatoSoup:
     - type: image
       path: images/tomato_soup_pot.png
       size: 1.05
-      center_offset: [ 0, 0 ]
+      center_offset: [ -0.02, -0.1 ]
 
 TomatoSoupPlate:
   parts:
@@ -217,7 +214,7 @@ OnionSoup:
     - type: image
       path: images/onion_soup_pot.png
       size: 1.05
-      center_offset: [ 0, 0 ]
+      center_offset: [ -0.02, -0.1 ]
 
 OnionSoupPlate:
   parts:
@@ -228,7 +225,7 @@ OnionSoupPlate:
 Cook:
   parts:
     - type: image
-      path: images/pixel_cook.png
+      path: images/pixel_cook_masked.png
       size: 1
 
 Plate:
@@ -254,4 +251,128 @@ Pan:
   parts:
     - type: image
       path: images/pan.png
-      size: 1.1
\ No newline at end of file
+      size: 1.1
+
+DeepFryer:
+  parts:
+    - color: gray5
+      type: rect
+      height: 0.875
+      width: 0.875
+    - color: lightyellow2
+      type: rect
+      height: 0.675
+      width: 0.675
+Oven:
+  parts:
+    - color: gray83
+      type: rect
+      height: 0.875
+      width: 0.625
+    - type: rect
+      color: black
+      height: 0.8
+      width: 0.3
+      center_offset: [ -0.4, -0.1 ]
+
+Basket:
+  parts:
+    - type: image
+      path: images/basket.png
+      size: 0.8
+
+Peel:
+  parts:
+    - type: image
+      path: images/pizza_wood.png
+      size: 1.2
+      center_offset: [ 0, 0.2 ]
+
+Potato:
+  parts:
+    - type: image
+      path: images/potato2.png
+      size: 0.7
+
+RawChips:
+  parts:
+    - type: image
+      path: images/raw_fries.png
+      size: 0.8
+
+Chips:
+  parts:
+    - type: image
+      path: images/fries2.png
+      size: 0.8
+
+Fish:
+  parts:
+    - type: image
+      path: images/fish3.png
+      size: 0.9
+
+ChoppedFish:
+  parts:
+    - type: image
+      path: images/cut_fish.png
+      size: 0.8
+
+FriedFish:
+  parts:
+    - type: image
+      path: images/fried_fish.png
+      size: 0.8
+
+FishAndChips:
+  parts:
+    - type: image
+      path: images/fries2.png
+      size: 0.8
+      center_offset: [ -0.1, 0 ]
+    - type: image
+      path: images/fried_fish.png
+      size: 0.8
+      center_offset: [ +0.2, 0 ]
+
+Dough:
+  parts:
+    - type: image
+      path: images/pizza_dough.png
+      size: 0.7
+
+PizzaBase:
+  parts:
+    - type: image
+      path: images/pizza_base.png
+      size: 0.9
+
+Sausage:
+  parts:
+    - type: image
+      path: images/sausage.png
+      size: 0.8
+
+ChoppedSausage:
+  parts:
+    - type: image
+      path: images/sausage_chopped.png
+      size: 0.8
+
+Cheese:
+  parts:
+    - type: image
+      path: images/cheese3.png
+      size: 0.7
+
+GratedCheese:
+  parts:
+    - type: image
+      path: images/grated_cheese.png
+      size: 1.1
+
+Pizza:
+  parts:
+    - type: image
+      path: images/pizza.png
+      size: 0.9
\ No newline at end of file
diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py
deleted file mode 100644
index a95d8821d023d7f7c879060a752f66a839717fff..0000000000000000000000000000000000000000
--- a/overcooked_simulator/main.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import logging
-import os
-import sys
-import threading
-from datetime import datetime
-
-import pygame
-
-from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.gui_2d_vis.overcooked_gui import PyGameGUI
-from overcooked_simulator.simulation_runner import Simulator
-
-log = logging.getLogger(__name__)
-
-
-def setup_logging():
-    path_logs = ROOT_DIR.parent / "logs"
-    os.makedirs(path_logs, exist_ok=True)
-    logging.basicConfig(
-        level=logging.DEBUG,
-        format="%(asctime)s %(levelname)-8s %(name)-50s %(message)s",
-        handlers=[
-            logging.FileHandler(
-                path_logs / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_debug.log",
-                encoding="utf-8",
-            ),
-            logging.StreamHandler(sys.stdout),
-        ],
-    )
-    logging.getLogger("matplotlib").setLevel(logging.WARNING)
-
-
-def main():
-    simulator = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        ROOT_DIR / "game_content" / "layouts" / "basic.layout",
-        600,
-    )
-    number_player = 2
-    for i in range(number_player):
-        player_name = f"p{i}"
-        simulator.register_player(player_name)
-
-    # TODO maybe read the player names and keyboard keys from config file?
-    keys1 = [
-        pygame.K_LEFT,
-        pygame.K_RIGHT,
-        pygame.K_UP,
-        pygame.K_DOWN,
-        pygame.K_SPACE,
-        pygame.K_i,
-    ]
-    keys2 = [pygame.K_a, pygame.K_d, pygame.K_w, pygame.K_s, pygame.K_f, pygame.K_e]
-
-    gui = PyGameGUI(simulator, [f"p{i}" for i in range(number_player)], [keys1, keys2])
-    gui.start_pygame()
-    sys.exit()
-
-
-if __name__ == "__main__":
-    setup_logging()
-    try:
-        main()
-    except Exception as e:
-        log.exception(e)
-        for thread in threading.enumerate():
-            if isinstance(thread, Simulator):
-                thread.stop()
-                thread.join()
-        sys.exit(1)
diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py
index 5f39694045fcbf80887ddeafa120eea6f647f558..68e5d311cf9cca0f30795486787a5597b5d82f93 100644
--- a/overcooked_simulator/order.py
+++ b/overcooked_simulator/order.py
@@ -44,6 +44,7 @@ from __future__ import annotations
 import dataclasses
 import logging
 import random
+import uuid
 from abc import abstractmethod
 from collections import deque
 from datetime import datetime, timedelta
@@ -53,6 +54,8 @@ from overcooked_simulator.game_items import Item, Plate, ItemInfo
 
 log = logging.getLogger(__name__)
 
+ORDER_CATEGORY = "Order"
+
 
 @dataclasses.dataclass
 class Order:
@@ -72,6 +75,7 @@ class Order:
     """List of timed penalties when the order is not fulfilled."""
     expired_penalty: float
     """The penalty to the score if the order expires"""
+    uuid: str = dataclasses.field(default_factory=lambda: uuid.uuid4().hex)
 
     finished_info: dict[str, Any] = dataclasses.field(default_factory=dict)
     """Is set after the order is completed."""
@@ -178,13 +182,13 @@ class OrderAndScoreManager:
                             accept, score = self.serving_not_ordered_meals(meal)
                             if accept:
                                 log.info(
-                                    f"Serving meal without order {meal.name} with score {score}"
+                                    f"Serving meal without order {meal.name!r} with score {score}"
                                 )
                                 self.increment_score(score)
                                 self.served_meals.append((meal, env_time))
                             return accept
                         log.info(
-                            f"Do not serve meal {meal.name} because it is not ordered"
+                            f"Do not serve meal {meal.name!r} because it is not ordered"
                         )
                         return False
                     order, index = order
@@ -197,7 +201,9 @@ class OrderAndScoreManager:
                         "end_time": env_time,
                         "score": score,
                     }
-                    log.info(f"Serving meal {meal.name} with order with score {score}")
+                    log.info(
+                        f"Serving meal {meal.name!r} with order with score {score}"
+                    )
                     self.last_finished.append(order)
                     del self.open_orders[index]
                     self.served_meals.append((meal, env_time))
@@ -267,6 +273,18 @@ class OrderAndScoreManager:
         for order in new_orders:
             order.create_penalties(env_time)
 
+    def order_state(self) -> list[dict]:
+        return [
+            {
+                "id": order.uuid,
+                "category": ORDER_CATEGORY,
+                "meal": order.meal.name,
+                "start_time": order.start_time.isoformat(),
+                "max_duration": order.max_duration.total_seconds(),
+            }
+            for order in self.open_orders
+        ]
+
 
 class ScoreCalcFuncType(Protocol):
     """Typed kwargs of the expected `Order.score_calc` function. Which is also returned by the
@@ -569,3 +587,8 @@ def simple_expired_penalty(item: ItemInfo, default: float, **kwargs) -> float:
         ```
     """
     return default
+
+
+def serving_not_ordered_meals_with_zero_score(meal: Item) -> Tuple[bool, float | int]:
+    """Not ordered meals are accepted but do not affect the score."""
+    return True, 0
diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py
index daa3a360b222b55b05d358f45a3bec635e079ccf..ee28b6de83ab4a0391f439ea82cbdccea2d1eb68 100644
--- a/overcooked_simulator/overcooked_environment.py
+++ b/overcooked_simulator/overcooked_environment.py
@@ -1,13 +1,13 @@
 from __future__ import annotations
 
 import dataclasses
+import json
 import logging
 import random
-from datetime import timedelta
+from datetime import timedelta, datetime
 from enum import Enum
 from pathlib import Path
-from threading import Lock
-from typing import Literal, Any
+from typing import Literal
 
 import numpy as np
 import numpy.typing as npt
@@ -20,7 +20,7 @@ from overcooked_simulator.counters import (
     Trashcan,
     Dispenser,
     ServingWindow,
-    Stove,
+    CookingCounter,
     Sink,
     PlateDispenser,
     SinkAddon,
@@ -33,6 +33,7 @@ from overcooked_simulator.game_items import (
 )
 from overcooked_simulator.order import OrderAndScoreManager
 from overcooked_simulator.player import Player, PlayerConfig
+from overcooked_simulator.state_representation import StateRepresentation
 from overcooked_simulator.utils import create_init_env_time
 
 log = logging.getLogger(__name__)
@@ -69,9 +70,17 @@ class Action:
     """Type of the action to perform. Defines what action data is valid."""
     action_data: npt.NDArray[float] | InterActionData | Literal["pickup"]
     """Data for the action, e.g., movement vector or start and stop interaction."""
+    duration: float | int = 0
+    """Duration of the action (relevant for movement)"""
 
     def __repr__(self):
-        return f"Action({self.player},{self.action_type.value},{self.action_data})"
+        return f"Action({self.player},{self.action_type.value},{self.action_data},{self.duration})"
+
+    def __post_init__(self):
+        if isinstance(self.action_type, str):
+            self.action_type = ActionType(self.action_type)
+        if isinstance(self.action_data, str) and self.action_data != "pickup":
+            self.action_data = InterActionData(self.action_data)
 
 
 # TODO Abstract base class for different environments
@@ -83,27 +92,31 @@ class Environment:
     Handles player movement, collision-detection, counters, cooking processes, recipes, incoming orders, time.
     """
 
-    def __init__(self, env_config_path: Path, layout_path, item_info_path: Path):
-        self.lock = Lock()
-        """temporal lock for GUI until it uses the json state."""
+    PAUSED = None
+
+    def __init__(
+        self,
+        env_config: Path | str,
+        layout_config: Path | str,
+        item_info: Path | str,
+        as_files: bool = True,
+    ):
         self.players: dict[str, Player] = {}
         """the player, keyed by their id/name."""
 
-        with open(env_config_path, "r") as file:
-            self.environment_config: dict[str, Any] = yaml.load(
-                file, Loader=yaml.Loader
-            )
-            """The environment configuration dictionary. Maybe in the future also a dataclass."""
-        self.layout_path: Path = layout_path
-        """The path to the layout file. Loaded by `Environment.parse_layout_file` method. """
+        self.as_files = as_files
+
+        if self.as_files:
+            with open(env_config, "r") as file:
+                self.environment_config = yaml.load(file, Loader=yaml.Loader)
+        else:
+            self.environment_config = yaml.load(env_config, Loader=yaml.Loader)
+        self.layout_config = layout_config
         # self.counter_side_length = 1  # -> this changed! is 1 now
 
-        self.item_info_path: Path = item_info_path
-        """The path to the `item_info.yml`. A default file is in `ROOT_DIR / "game_content" / "item_info.yaml"`."""
-        self.item_info: dict[str, ItemInfo] = self.load_item_info()
+        self.item_info = self.load_item_info(item_info)
         """The loaded item info dict. Keys are the item names."""
         # self.validate_item_info()
-
         if self.environment_config["meals"]["all"]:
             self.allowed_meal_names = set(
                 [
@@ -126,6 +139,16 @@ class Environment:
         )
         """The manager for the orders and score update."""
 
+        cooking_counter_equipments = {
+            cooking_counter: [
+                equipment
+                for equipment, e_info in self.item_info.items()
+                if e_info.equipment and e_info.equipment.name == cooking_counter
+            ]
+            for cooking_counter, info in self.item_info.items()
+            if info.type == ItemType.Equipment and info.equipment is None
+        }
+
         self.SYMBOL_TO_CHARACTER_MAP = {
             "#": Counter,
             "C": lambda pos: CuttingBoard(
@@ -143,6 +166,11 @@ class Environment:
             ),
             "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]),
             "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]),
+            "K": lambda pos: Dispenser(pos, self.item_info["Potato"]),  # Kartoffel
+            "I": lambda pos: Dispenser(pos, self.item_info["Fish"]),  # fIIIsh
+            "D": lambda pos: Dispenser(pos, self.item_info["Dough"]),
+            "E": lambda pos: Dispenser(pos, self.item_info["Cheese"]),  # chEEEEse
+            "G": lambda pos: Dispenser(pos, self.item_info["Sausage"]),  # sausaGe
             "P": lambda pos: PlateDispenser(
                 plate_transitions=self.filter_item_info(
                     item_info=self.item_info, by_item_type=ItemType.Meal
@@ -160,8 +188,10 @@ class Environment:
             "N": lambda pos: Dispenser(pos, self.item_info["Onion"]),  # N for oNioN
             "_": "Free",
             "A": "Agent",
-            "U": lambda pos: Stove(
-                pos,
+            "U": lambda pos: CookingCounter(
+                name="Stove",
+                cooking_counter_equipments=cooking_counter_equipments,
+                pos=pos,
                 occupied_by=CookingEquipment(
                     name="Pot",
                     item_info=self.item_info["Pot"],
@@ -170,8 +200,10 @@ class Environment:
                     ),
                 ),
             ),  # Stove with pot: U because it looks like a pot
-            "Q": lambda pos: Stove(
-                pos,
+            "Q": lambda pos: CookingCounter(
+                name="Stove",
+                cooking_counter_equipments=cooking_counter_equipments,
+                pos=pos,
                 occupied_by=CookingEquipment(
                     name="Pan",
                     item_info=self.item_info["Pan"],
@@ -180,6 +212,43 @@ class Environment:
                     ),
                 ),
             ),  # Stove with pan: Q because it looks like a pan
+            "O": lambda pos: CookingCounter(
+                name="Oven",
+                cooking_counter_equipments=cooking_counter_equipments,
+                pos=pos,
+                occupied_by=CookingEquipment(
+                    name="Peel",
+                    item_info=self.item_info["Peel"],
+                    transitions={
+                        item: {
+                            "seconds": info.seconds,
+                            "needs": info.needs,
+                            "info": info,
+                        }
+                        for item, info in self.item_info.items()
+                        if info.equipment is not None and info.equipment.name == "Peel"
+                    },
+                ),
+            ),
+            "F": lambda pos: CookingCounter(
+                name="DeepFryer",
+                cooking_counter_equipments=cooking_counter_equipments,
+                pos=pos,
+                occupied_by=CookingEquipment(
+                    name="Basket",
+                    item_info=self.item_info["Basket"],
+                    transitions={
+                        item: {
+                            "seconds": info.seconds,
+                            "needs": info.needs,
+                            "info": info,
+                        }
+                        for item, info in self.item_info.items()
+                        if info.equipment is not None
+                        and info.equipment.name == "Basket"
+                    },
+                ),
+            ),  # Stove with pan: Q because it looks like a pan
             "B": lambda pos: Dispenser(pos, self.item_info["Bun"]),
             "M": lambda pos: Dispenser(pos, self.item_info["Meat"]),
             "S": lambda pos: Sink(
@@ -202,15 +271,15 @@ class Environment:
             self.counters,
             self.designated_player_positions,
             self.free_positions,
-        ) = self.parse_layout_file(self.layout_path)
+        ) = self.parse_layout_file()
 
         self.init_counters()
 
-        self.env_time = create_init_env_time()
+        self.env_time: datetime = create_init_env_time()
         """the internal time of the environment. An environment starts always with the time from 
         `create_init_env_time`."""
         self.order_and_score.create_init_orders(self.env_time)
-        self.beginning_time = self.env_time
+        self.start_time = self.env_time
         """The relative env time when it started."""
         self.env_time_end = self.env_time + timedelta(
             seconds=self.environment_config["game"]["time_limit_seconds"]
@@ -229,10 +298,13 @@ class Environment:
         Utility method to pass a reference to the serving window."""
         return self.env_time
 
-    def load_item_info(self) -> dict[str, ItemInfo]:
+    def load_item_info(self, data) -> dict[str, ItemInfo]:
         """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos."""
-        with open(self.item_info_path, "r") as file:
-            item_lookup = yaml.safe_load(file)
+        if self.as_files:
+            with open(data, "r") as file:
+                item_lookup = yaml.safe_load(file)
+        else:
+            item_lookup = yaml.safe_load(data)
         for item_name in item_lookup:
             item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name])
 
@@ -298,7 +370,7 @@ class Environment:
         # TODO add colors for ingredients, equipment and meals
         # plt.show()
 
-    def parse_layout_file(self, layout_file: Path):
+    def parse_layout_file(self):
         """Creates layout of kitchen counters in the environment based on layout file.
         Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at
         [counter_size/2, counter_size/2], counters are directly next to each other (of no empty space is specified
@@ -314,9 +386,12 @@ class Environment:
 
         self.kitchen_width = 0
 
-        with open(layout_file, "r") as layout_file:
-            lines = layout_file.readlines()
-            self.kitchen_height = len(lines)
+        if self.as_files:
+            with open(self.layout_config, "r") as layout_file:
+                lines = layout_file.readlines()
+        else:
+            lines = self.layout_config.split("\n")
+        self.kitchen_height = len(lines)
 
         for line in lines:
             line = line.replace("\n", "").replace(" ", "")  # remove newline char
@@ -356,15 +431,15 @@ class Environment:
         player = self.players[action.player]
 
         if action.action_type == ActionType.MOVEMENT:
-            with self.lock:
-                self.perform_movement(player, action.action_data)
-
+            player.set_movement(
+                action.action_data,
+                self.env_time + timedelta(seconds=action.duration),
+            )
         else:
             counter = self.get_facing_counter(player)
             if player.can_reach(counter):
                 if action.action_type == ActionType.PUT:
-                    with self.lock:
-                        player.pick_action(counter)
+                    player.pick_action(counter)
 
                 elif action.action_type == ActionType.INTERACT:
                     if action.action_data == InterActionData.START:
@@ -372,10 +447,7 @@ class Environment:
                         player.last_interacted_counter = counter
             if action.action_data == InterActionData.STOP:
                 if player.last_interacted_counter:
-                    with self.lock:
-                        player.perform_interact_hold_stop(
-                            player.last_interacted_counter
-                        )
+                    player.perform_interact_hold_stop(player.last_interacted_counter)
 
     def get_closest_counter(self, point: np.ndarray):
         """Determines the closest counter for a given 2d-coordinate point in the env.
@@ -406,7 +478,7 @@ class Environment:
         facing_counter = self.get_closest_counter(player.facing_point)
         return facing_counter
 
-    def perform_movement(self, player: Player, move_vector: np.array):
+    def perform_movement(self, player: Player, duration: timedelta):
         """Moves a player in the direction specified in the action.action. If the player collides with a
         counter or other player through this movement, then they are not moved.
         (The extended code with the two ifs is for sliding movement at the counters, which feels a bit smoother.
@@ -419,11 +491,15 @@ class Environment:
 
         Args:
             player: The player to move.
-            move_vector: The movement vector which is a unit-2d-vector of the movement direction
+            duration: The duration for how long the movement to perform.
         """
         old_pos = player.pos.copy()
 
-        step = move_vector * player.move_dist
+        move_vector = player.current_movement
+
+        d_time = duration.total_seconds()
+        step = move_vector * (player.player_speed_units_per_seconds * d_time)
+
         player.move(step)
         if self.detect_collision(player):
             collided_players = self.get_collided_players(player)
@@ -433,7 +509,8 @@ class Environment:
                     pushing_vector = pushing_vector / np.linalg.norm(pushing_vector)
 
                 old_pos_other = collided_player.pos.copy()
-                self.perform_movement(collided_player, pushing_vector)
+                collided_player.current_movement = pushing_vector
+                self.perform_movement(collided_player, duration)
                 if self.detect_collision_counters(
                     collided_player
                 ) or self.detect_collision_world_bounds(collided_player):
@@ -560,6 +637,7 @@ class Environment:
             player_name: The id/name of the player to reference actions and in the state.
             pos: The optional init position of the player.
         """
+        # TODO check if the player name already exists in the environment and do not overwrite player.
         log.debug(f"Add player {player_name} to the game")
         player = Player(
             player_name,
@@ -606,14 +684,17 @@ class Environment:
         """
         self.env_time += passed_time
 
-        with self.lock:
-            if not self.game_ended:
-                for counter in self.counters:
-                    if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)):
-                        counter.progress(passed_time=passed_time, now=self.env_time)
-                self.order_and_score.progress(
-                    passed_time=passed_time, now=self.env_time
-                )
+        if not self.game_ended:
+            for player in self.players.values():
+                if self.env_time <= player.movement_until:
+                    self.perform_movement(player, passed_time)
+
+            for counter in self.counters:
+                if isinstance(
+                    counter, (CuttingBoard, CookingCounter, Sink, PlateDispenser)
+                ):
+                    counter.progress(passed_time=passed_time, now=self.env_time)
+            self.order_and_score.progress(passed_time=passed_time, now=self.env_time)
 
     def get_state(self):
         """Get the current state of the game environment. The state here is accessible by the current python objects.
@@ -631,13 +712,21 @@ class Environment:
             "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)),
         }
 
-    def get_state_json(self):
-        """Get the current state of the game environment as a json-like nested dictionary.
-
-        Returns: Json-like string of the current game state.
-
-        """
-        pass
+    def get_json_state(self, player_id: str = None):
+        state = {
+            "players": [p.to_dict() for p in self.players.values()],
+            "counters": [c.to_dict() for c in self.counters],
+            "score": self.order_and_score.score,
+            "orders": self.order_and_score.order_state(),
+            "ended": self.game_ended,
+            "env_time": self.env_time.isoformat(),
+            "remaining_time": max(
+                self.env_time_end - self.env_time, timedelta(0)
+            ).total_seconds(),
+        }
+        json_data = json.dumps(state)
+        assert StateRepresentation.model_validate_json(json_data=json_data)
+        return json_data
 
     def init_counters(self):
         """Initialize the counters in the environment.
diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py
index 2e09040d474e98066aab5e86fa430dc16ec1d9c9..0f5f25424210d595d8290c61231d641b16d934a7 100644
--- a/overcooked_simulator/player.py
+++ b/overcooked_simulator/player.py
@@ -7,6 +7,7 @@ holding object**. If so, it picks up the content and combines it on its hands.
 """
 
 import dataclasses
+import datetime
 import logging
 from collections import deque
 from typing import Optional
@@ -16,6 +17,7 @@ import numpy.typing as npt
 
 from overcooked_simulator.counters import Counter
 from overcooked_simulator.game_items import Item, Plate
+from overcooked_simulator.state_representation import PlayerState
 
 log = logging.getLogger(__name__)
 
@@ -26,7 +28,7 @@ class PlayerConfig:
 
     radius: float = 0.4
     """The size of the player. The size of a counter is 1"""
-    move_dist: float = 0.15
+    player_speed_units_per_seconds: float | int = 8
     """The move distance/speed of the player per action call."""
     interaction_range: float = 1.6
     """How far player can interact with counters."""
@@ -47,7 +49,6 @@ class Player:
     ):
         self.name: str = name
         """Reference for the player"""
-
         self.pos: npt.NDArray[float] | None = None
         """The initial/suggested position of the player."""
         if pos is not None:
@@ -58,7 +59,9 @@ class Player:
 
         self.radius: float = player_config.radius
         """See `PlayerConfig.radius`."""
-        self.move_dist: float = player_config.move_dist
+        self.player_speed_units_per_seconds: float | int = (
+            player_config.player_speed_units_per_seconds
+        )
         """See `PlayerConfig.move_dist`."""
         self.interaction_range: float = player_config.interaction_range
         """See `PlayerConfig.interaction_range`."""
@@ -75,6 +78,13 @@ class Player:
         """A point on the "circle" of the players border in the `facing_direction` with which the closest counter is 
         calculated with."""
 
+        self.current_movement: npt.NDArray[2] = np.zeros(2, float)
+        self.movement_until: datetime.datetime = datetime.datetime.min
+
+    def set_movement(self, move_vector, move_until):
+        self.current_movement = move_vector
+        self.movement_until = move_until
+
     def move(self, movement: npt.NDArray[float]):
         """Moves the player position by the given movement vector.
         A unit direction vector multiplied by move_dist is added to the player position.
@@ -170,3 +180,18 @@ class Player:
 
     def __repr__(self):
         return f"Player(name:{self.name},pos:{str(self.pos)},holds:{self.holding})"
+
+    def to_dict(self) -> PlayerState:
+        # TODO add color to player class for vis independent player color
+        return {
+            "id": self.name,
+            "pos": self.pos.tolist(),
+            "facing_direction": self.facing_direction.tolist(),
+            "holding": self.holding.to_dict() if self.holding else None,
+            "current_nearest_counter_pos": self.current_nearest_counter.pos.tolist()
+            if self.current_nearest_counter
+            else None,
+            "current_nearest_counter_id": self.current_nearest_counter.uuid
+            if self.current_nearest_counter
+            else None,
+        }
diff --git a/overcooked_simulator/server_results.py b/overcooked_simulator/server_results.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed78c792d8c14ea95f2c47079cf41e1a4900fba3
--- /dev/null
+++ b/overcooked_simulator/server_results.py
@@ -0,0 +1,27 @@
+"""Type hint classes for the returned data for the post calls."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from typing_extensions import TypedDict, Literal
+
+if TYPE_CHECKING:
+    from overcooked_simulator.game_server import PlayerRequestType
+
+
+class PlayerInfo(TypedDict):
+    client_id: str
+    player_hash: str
+    player_id: str
+
+
+class CreateEnvResult(TypedDict):
+    env_id: str
+    player_info: dict[str, PlayerInfo]
+
+
+class PlayerRequestResult(TypedDict):
+    request_type: PlayerRequestType | None
+    status: Literal[200] | Literal[400]
+    msg: str
+    player_hash: str | None
diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py
deleted file mode 100644
index d4deebcd2ac2e35e2ae0b59476cb2a7c30ef16ac..0000000000000000000000000000000000000000
--- a/overcooked_simulator/simulation_runner.py
+++ /dev/null
@@ -1,132 +0,0 @@
-import logging
-import time
-from datetime import timedelta
-from threading import Thread
-
-import numpy as np
-
-from overcooked_simulator import ROOT_DIR
-from overcooked_simulator.overcooked_environment import Environment, Action
-
-log = logging.getLogger(__name__)
-
-
-class Simulator(Thread):
-    """Simulator main class which runs manages the environment and player inputs and game state outputs.
-
-    Main Simulator class which runs the game environment. Players can be registered in the game.
-    The simulator is run as its own thread.
-
-    Is a child class of the `Thread` class from the `threading` library.
-
-    Typical usage example:
-    ```python
-    sim = Simulator()
-    sim.register_player(Player("p1", [x,y]))
-    sim.start()
-    ```
-    """
-
-    def __init__(
-        self,
-        env_config_path,
-        layout_path,
-        frequency: int,
-        item_info_path=ROOT_DIR / "game_content" / "item_info.yaml",
-        seed: int = 8654321,
-    ):
-        """Constructor of the `Simulator class.
-
-        Args:
-            env_config_path: Path to the environment configuration file.
-            layout_path: Path to the layout file.
-            frequency: Frequency of the environment step function call.
-            item_info_path: Path to the item information configuration file.
-            seed: Random seed to set the numpy random number generator.
-        """
-        # TODO look at https://builtin.com/data-science/numpy-random-seed to change to other random
-        np.random.seed(seed)
-        self.finished: bool = False
-        """The environment runs as long it is `True`"""
-
-        self.step_frequency: int = frequency
-        """Frequency of the environment step function call."""
-        self.preferred_sleep_time_ns: float = 1e9 / self.step_frequency
-        """If the environment step call would need no computation time. The duration for one "frame"."""
-        self.env: Environment = Environment(
-            env_config_path, layout_path, item_info_path
-        )
-        """Reference to the `Environment`."""
-
-        super().__init__()
-
-    def step(self, passed_time: timedelta):
-        """One simulation step of the environment."""
-        self.env.step(passed_time)
-
-    def enter_action(self, action: Action):
-        """Takes an action and executes it in the environment.
-
-        Args:
-            action (Action): The action object to be executed.
-        """
-        self.env.perform_action(action)
-
-    def get_state(self):
-        """Get the current game state as python objects.
-
-        Returns:
-            The current state of the game. Currently, as dict with lists of environment objects.
-        """
-        return self.env.get_state()
-
-    def get_state_json(self):
-        """Get the current game state in json-like dict.
-
-        Returns:
-            The gamest ate encoded in a json style nested dict.
-        """
-        return self.env.get_state_json()
-
-    def register_player(self, player_name: str, pos=None):
-        """Adds a player to the environment.
-
-        Args:
-            player_name: the reference to the player (name/id).
-            pos: optional position of the player.
-        """
-        self.env.add_player(player_name, pos)
-
-    def register_players(self, players: list[str]):
-        """Registers multiple players from a list
-
-        Args:
-            players: List of players to be added.
-        """
-        for p in players:
-            self.register_player(p)
-
-    def run(self):
-        """Starts the simulator thread. Runs in a loop until stopped."""
-        overslept_in_ns = 0
-        self.env.reset_env_time()
-        last_step_start = time.time_ns()
-        while not self.finished:
-            step_start = time.time_ns()
-            self.step(timedelta(seconds=(step_start - last_step_start) / 1_000_000_000))
-            last_step_start = step_start
-            step_duration = time.time_ns() - step_start
-
-            time_to_sleep_ns = self.preferred_sleep_time_ns - (
-                step_duration + overslept_in_ns
-            )
-
-            sleep_start = time.time_ns()
-            time.sleep(max(time_to_sleep_ns / 1e9, 0))
-            sleep_function_duration = time.time_ns() - sleep_start
-            overslept_in_ns = sleep_function_duration - time_to_sleep_ns
-
-    def stop(self):
-        """Stops the simulator"""
-        log.debug("Stopping the simulation")
-        self.finished = True
diff --git a/overcooked_simulator/state_representation.py b/overcooked_simulator/state_representation.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8ad5d17a79b4db29c2977934f0127c7a09c83f9
--- /dev/null
+++ b/overcooked_simulator/state_representation.py
@@ -0,0 +1,68 @@
+from datetime import datetime
+
+from pydantic import BaseModel
+from typing_extensions import Literal, TypedDict
+
+
+class OrderState(TypedDict):
+    id: str
+    category: Literal["Order"]
+    meal: str
+    start_time: datetime  # isoformat str
+    max_duration: float
+
+
+class ItemState(TypedDict):
+    id: str
+    category: Literal["Item"] | Literal["ItemCookingEquipment"]
+    type: str
+    progress_percentage: float | int
+    # add ItemType Meal ?
+
+
+class CookingEquipmentState(TypedDict):
+    content_list: list[ItemState]
+    content_ready: None | ItemState
+
+
+class CounterState(TypedDict):
+    id: str
+    category: Literal["Counter"]
+    type: str
+    pos: list[float]
+    occupied_by: None | list[
+        ItemState | CookingEquipmentState
+    ] | ItemState | CookingEquipmentState
+    # list[ItemState] -> type in ["Sink", "PlateDispenser"]
+
+
+class CuttingBoardAndSinkState(TypedDict):
+    type: Literal["CuttingBoard"] | Literal["Sink"]
+    progressing: bool
+
+
+class PlayerState(TypedDict):
+    id: str
+    pos: list[float]
+    facing_direction: list[float]
+    holding: ItemState | CookingEquipmentState | None
+    current_nearest_counter_pos: list[float] | None
+    current_nearest_counter_id: str | None
+
+
+class StateRepresentation(BaseModel):
+    players: list[PlayerState]
+    counters: list[CounterState]
+    score: float | int
+    orders: list[OrderState]
+    ended: bool
+    env_time: datetime  # isoformat str
+    remaining_time: float
+
+
+def create_json_schema():
+    return StateRepresentation.model_json_schema()
+
+
+if __name__ == "__main__":
+    print(create_json_schema())
diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py
index dfb5da0068a533134ad72e85bf14289d295cc585..ecfb5958a982c084ce46db849fa9568483fea3e5 100644
--- a/overcooked_simulator/utils.py
+++ b/overcooked_simulator/utils.py
@@ -1,4 +1,10 @@
+import logging
+import os
+import sys
 from datetime import datetime
+from enum import Enum
+
+from overcooked_simulator import ROOT_DIR
 
 
 def create_init_env_time():
@@ -6,3 +12,57 @@ def create_init_env_time():
     return datetime(
         year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
     )
+
+
+def custom_asdict_factory(data):
+    def convert_value(obj):
+        if isinstance(obj, Enum):
+            return obj.value
+        return obj
+
+    return dict((k, convert_value(v)) for k, v in data)
+
+
+def setup_logging(enable_websocket_logging=False):
+    path_logs = ROOT_DIR.parent / "logs"
+    os.makedirs(path_logs, exist_ok=True)
+    logging.basicConfig(
+        level=logging.DEBUG,
+        format="%(asctime)s %(levelname)-8s %(name)-50s %(message)s",
+        handlers=[
+            logging.FileHandler(
+                path_logs / f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_debug.log",
+                encoding="utf-8",
+            ),
+            logging.StreamHandler(sys.stdout),
+        ],
+    )
+    logging.getLogger("matplotlib").setLevel(logging.WARNING)
+    if not enable_websocket_logging:
+        logging.getLogger("asyncio").setLevel(logging.ERROR)
+        logging.getLogger("asyncio.coroutines").setLevel(logging.ERROR)
+        logging.getLogger("websockets.server").setLevel(logging.ERROR)
+        logging.getLogger("websockets.protocol").setLevel(logging.ERROR)
+        logging.getLogger("websockets.client").setLevel(logging.ERROR)
+
+
+def url_and_port_arguments(parser):
+    parser.add_argument(
+        "-url",
+        "--url",
+        "--host",
+        type=str,
+        default="localhost",
+        help="Overcooked game server host url.",
+    )
+    parser.add_argument(
+        "-p",
+        "--port",
+        type=int,
+        default=8000,
+        help="Port number for the game engine server",
+    )
+
+
+def disable_websocket_logging_arguments(parser):
+    parser.add_argument("--enable-websocket-logging", action="store_true", default=True)
diff --git a/setup.py b/setup.py
index f50b81aaa0c54c9e70528b6b2aaf230355244185..d57d47c30a8eefd7e427ff97be2c4884bd1a4cfb 100644
--- a/setup.py
+++ b/setup.py
@@ -10,7 +10,18 @@ with open("README.md") as readme_file:
 with open("CHANGELOG.md") as history_file:
     history = history_file.read()
 
-requirements = ["numpy", "pygame", "scipy", "pytest>=3", "pyyaml", "pygame-gui"]
+requirements = [
+    "numpy",
+    "pygame",
+    "scipy",
+    "pytest>=3",
+    "pyyaml",
+    "pygame-gui",
+    "fastapi",
+    "uvicorn",
+    "websockets",
+    "requests",
+]
 
 test_requirements = [
     "pytest>=3",
@@ -29,6 +40,9 @@ setup(
         "Programming Language :: Python :: 3.10",
     ],
     description="The real-time overcooked simulation for a cognitive cooperative system",
+    entry_points={
+        "console_scripts": ["overcooked-sim = overcooked_simulator.__main__:main"]
+    },
     install_requires=requirements,
     license="MIT license",
     long_description=readme + "\n\n" + history,
diff --git a/tests/test_start.py b/tests/test_start.py
index ae27dd25a987c5e04c4e03114cc53b859b90095a..45cbf4dd6cb30fb1ef7693c6acc02f6032e2b2de 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -1,4 +1,3 @@
-import time
 from datetime import timedelta
 
 import numpy as np
@@ -13,11 +12,13 @@ from overcooked_simulator.overcooked_environment import (
     ActionType,
     InterActionData,
 )
-from overcooked_simulator.simulation_runner import Simulator
 from overcooked_simulator.utils import create_init_env_time
 
 layouts_folder = ROOT_DIR / "game_content" / "layouts"
-
+environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml"
+layout_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
+layout_empty_path = ROOT_DIR / "game_content" / "layouts" / "basic.layout"
+item_info_path = ROOT_DIR / "game_content" / "item_info.yaml"
 
 # TODO: TESTs are in absolute pixel coordinates still.
 
@@ -25,190 +26,195 @@ layouts_folder = ROOT_DIR / "game_content" / "layouts"
 @pytest.fixture(autouse=True)
 def test_file_availability():
     assert layouts_folder.is_dir(), "layouts folder does not exists"
-    assert (
-        layouts_folder / "basic.layout"
-    ).is_file(), "basic layouts file does not exists"
-
+    assert environment_config_path.is_file(), "environment config file does not exists"
+    assert layout_path.is_file(), "layout config file does not exists"
+    assert layout_empty_path.is_file(), "layout empty config file does not exists"
+    assert item_info_path.is_file(), "item info config file does not exists"
 
-def test_player_registration():
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "basic.layout",
-        200,
-    )
 
-    sim.register_player("player1", np.array([0, 0]))
+@pytest.fixture
+def env_config():
+    with open(environment_config_path, "r") as file:
+        env_config = file.read()
 
-    assert len(sim.env.players) != 0, "Wrong number of players"
-    assert len(sim.env.players) == 1, "Wrong number of players"
+    return env_config
 
-    sim.register_player("player2", np.array([100, 100]))
 
-    assert len(sim.env.players) == 2, "Wrong number of players"
+@pytest.fixture
+def layout_config():
+    with open(layout_path, "r") as file:
+        layout = file.read()
+    return layout
 
-    sim.register_player("player2", np.array([100, 100]))  # same player name
-    assert len(sim.env.players) == 2, "Wrong number of players"
 
-    sim.start()
-    sim.stop()
+@pytest.fixture
+def layout_empty_config():
+    with open(layout_path, "r") as file:
+        layout = file.read()
+    return layout
 
 
-def test_simulator_frequency():
-    class TestEnv:
-        def __init__(self):
-            self.c = 0
+@pytest.fixture
+def item_info():
+    with open(item_info_path, "r") as file:
+        item_info = file.read()
+    return item_info
 
-        def step(self, passed_time):
-            self.c += 1
 
-        def reset_env_time(self):
-            pass
+def test_player_registration(env_config, layout_config, item_info):
+    env = Environment(env_config, layout_config, item_info, as_files=False)
 
-    frequency = 2000
-    running_time_seconds = 2
+    env.add_player("1")
+    assert len(env.players) == 1, "Wrong number of players"
 
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "empty.layout",
-        frequency,
-    )
-    sim.env = TestEnv()  # Overwrite environment with a simple counting env
+    env.add_player("2")
+    assert len(env.players) == 2, "Wrong number of players"
 
-    sim.start()
-    time.sleep(running_time_seconds)
-    sim.stop()
+    env.add_player("2")
+    assert len(env.players) == 2, "Wrong number of players"
 
-    accepted_tolerance = 0.02
-    lower = frequency * running_time_seconds * (1 - accepted_tolerance)
-    upper = frequency * running_time_seconds * (1 + accepted_tolerance)
-    assert lower < sim.env.c < upper, "Timing error in the environment at 1000hz"
 
+def test_movement(env_config, layout_empty_config, item_info):
+    env = Environment(env_config, layout_empty_config, item_info, as_files=False)
+    player_name = "1"
+    start_pos = np.array([3, 4])
+    env.add_player(player_name, start_pos)
+    env.players[player_name].player_speed_units_per_seconds = 1
+    move_direction = np.array([1, 0])
+    move_action = Action(player_name, ActionType.MOVEMENT, move_direction, duration=0.1)
+    do_moves_number = 3
+    for i in range(do_moves_number):
+        env.perform_action(action=move_action)
+        env.step(timedelta(seconds=0.1))
 
-def test_movement():
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "empty.layout",
-        200,
+    expected = start_pos + do_moves_number * (
+        move_direction
+        * env.players[player_name].player_speed_units_per_seconds
+        * move_action.duration
     )
-    player_name = "p1"
-    start_pos = np.array([1, 2])
-    sim.register_player(player_name, start_pos)
+
+    assert np.isclose(
+        np.linalg.norm(expected - env.players[player_name].pos), 0
+    ), "Performed movement do not move the player as expected."
+
+
+def test_player_speed_units_per_seconds(env_config, layout_empty_config, item_info):
+    env = Environment(env_config, layout_empty_config, item_info, as_files=False)
+    player_name = "1"
+    start_pos = np.array([3, 4])
+    env.add_player(player_name, start_pos)
+    env.players[player_name].player_speed_units_per_seconds = 2
     move_direction = np.array([1, 0])
-    move_action = Action(player_name, ActionType.MOVEMENT, move_direction)
-    do_moves_number = 6
+    move_action = Action(player_name, ActionType.MOVEMENT, move_direction, duration=0.1)
+    do_moves_number = 3
     for i in range(do_moves_number):
-        sim.enter_action(move_action)
+        env.perform_action(action=move_action)
+        env.step(timedelta(seconds=0.1))
 
     expected = start_pos + do_moves_number * (
-        move_direction * sim.env.players[player_name].move_dist
+        move_direction
+        * env.players[player_name].player_speed_units_per_seconds
+        * move_action.duration
     )
 
     assert np.isclose(
-        np.linalg.norm(expected - sim.env.players[player_name].pos), 0
-    ), "Should be here?"
+        np.linalg.norm(expected - env.players[player_name].pos), 0
+    ), "Performed movement do not move the player as expected."
 
 
-def test_collision_detection():
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "basic.layout",
-        200,
-    )
+def test_collision_detection(env_config, layout_config, item_info):
+    env = Environment(env_config, layout_config, item_info, as_files=False)
+
     counter_pos = np.array([1, 2])
     counter = Counter(counter_pos)
-    sim.env.counters = [counter]
-    sim.register_player("p1", np.array([1, 1]))
-    sim.register_player("p2", np.array([1, 4]))  # same player name
-    player1 = sim.env.players["p1"]
-    player2 = sim.env.players["p2"]
-
-    sim.start()
-    try:
-        assert not sim.env.detect_collision_counters(player1), "Should not collide"
-        assert not sim.env.detect_player_collision(player1), "Should not collide yet."
-
-        assert not sim.env.detect_collision(player1), "Does not collide yet."
-
-        player1.move_abs(counter_pos)
-        assert sim.env.detect_collision_counters(
-            player1
-        ), "Player and counter at same pos. Not detected."
-        player2.move_abs(counter_pos)
-        assert sim.env.detect_player_collision(
-            player1
-        ), "Players at same pos. Not detected."
-
-        player1.move_abs(np.array([0, 0]))
-        assert sim.env.detect_collision_world_bounds(
-            player1
-        ), "Player collides with world bounds."
-    finally:
-        sim.stop()
-
-
-def test_player_reach():
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "basic.layout",
-        200,
-    )
+    env.counters = [counter]
+    env.add_player("1", np.array([1, 1]))
+    env.add_player("2", np.array([1, 4]))
+
+    player1 = env.players["1"]
+    player2 = env.players["2"]
+
+    assert not env.detect_collision_counters(player1), "Should not collide"
+    assert not env.detect_player_collision(player1), "Should not collide yet."
+
+    assert not env.detect_collision(player1), "Does not collide yet."
+
+    player1.move_abs(counter_pos)
+    assert env.detect_collision_counters(
+        player1
+    ), "Player and counter at same pos. Not detected."
+    player2.move_abs(counter_pos)
+    assert env.detect_player_collision(player1), "Players at same pos. Not detected."
+
+    player1.move_abs(np.array([0, 0]))
+    assert env.detect_collision_world_bounds(
+        player1
+    ), "Player collides with world bounds."
+
+
+def test_player_reach(env_config, layout_empty_config, item_info):
+    env = Environment(env_config, layout_empty_config, item_info, as_files=False)
 
     counter_pos = np.array([2, 2])
     counter = Counter(counter_pos)
-    sim.env.counters = [counter]
-    sim.register_player("p1", np.array([2, 4]))
-    player = sim.env.players["p1"]
+    env.counters = [counter]
+    env.add_player("1", np.array([2, 4]))
+    env.players["1"].player_speed_units_per_seconds = 1
+    player = env.players["1"]
     assert not player.can_reach(counter), "Player is too far away."
 
     do_moves_number = 30
     for i in range(do_moves_number):
-        move_action = Action("p1", ActionType.MOVEMENT, np.array([0, -1]))
-        sim.enter_action(move_action)
+        move_action = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)
+        env.perform_action(move_action)
+        env.step(passed_time=timedelta(seconds=1))
     assert player.can_reach(counter), "Player can reach counter?"
 
 
-def test_pickup():
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "empty.layout",
-        200,
-    )
+def test_pickup(env_config, layout_config, item_info):
+    env = Environment(env_config, layout_config, item_info, as_files=False)
 
     counter_pos = np.array([2, 2])
     counter = Counter(counter_pos)
     counter.occupied_by = Item(name="Tomato", item_info=None)
-    sim.env.counters = [counter]
+    env.counters = [counter]
 
-    sim.register_player("p1", np.array([2, 3]))
-    player = sim.env.players["p1"]
+    env.add_player("1", np.array([2, 3]))
+    player = env.players["1"]
+    player.player_speed_units_per_seconds = 1
 
-    move_down = Action("p1", ActionType.MOVEMENT, np.array([0, -1]))
-    move_up = Action("p1", ActionType.MOVEMENT, np.array([0, 1]))
-    pick = Action("p1", ActionType.PUT, "pickup")
+    move_down = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)
+    move_up = Action("1", ActionType.MOVEMENT, np.array([0, 1]), duration=1)
+    pick = Action("1", ActionType.PUT, "pickup")
 
-    sim.enter_action(move_down)
+    env.perform_action(move_down)
+    env.step(timedelta(seconds=1))
     assert player.can_reach(counter), "Player can reach counter?"
 
-    sim.enter_action(pick)
+    env.perform_action(pick)
     assert player.holding is not None, "Player should have picked up tomato."
     assert player.holding.name == "Tomato", "Should be tomato."
 
-    sim.enter_action(move_up)
-    sim.enter_action(move_up)
-    sim.enter_action(move_up)
-    sim.enter_action(move_up)
+    for _ in range(5):
+        env.perform_action(move_up)
+        env.step(timedelta(seconds=1))
 
-    sim.enter_action(pick)
+    env.perform_action(pick)
     assert (
         player.holding is not None
     ), "Player should be too far away to put tomato down."
 
-    sim.enter_action(move_down)
-    sim.enter_action(move_down)
-    sim.enter_action(move_down)
-    sim.enter_action(move_down)
+    for _ in range(4):
+        env.perform_action(move_down)
+        env.step(timedelta(seconds=1))
+        env.perform_action(move_down)
+        env.step(timedelta(seconds=1))
+        env.perform_action(move_down)
+        env.step(timedelta(seconds=1))
+        env.perform_action(move_down)
+        env.step(timedelta(seconds=1))
 
-    sim.enter_action(pick)
+    env.perform_action(pick)
 
     assert player.holding is None, "Player should have put tomato down."
     assert (
@@ -216,54 +222,49 @@ def test_pickup():
     ), "Tomato should be here now."
 
 
-def test_processing():
-    sim_frequency = 1000
-    sim = Simulator(
-        ROOT_DIR / "game_content" / "environment_config.yaml",
-        layouts_folder / "empty.layout",
-        sim_frequency,
-    )
-    sim.start()
-    try:
-        counter_pos = np.array([2, 2])
-        counter = CuttingBoard(
-            counter_pos,
-            transitions={
-                "ChoppedTomato": ItemInfo(
-                    name="ChoppedTomato",
-                    seconds=0.5,
-                    equipment=ItemInfo(name="CuttingBoard", type=ItemType.Equipment),
-                    type=ItemType.Ingredient,
-                    needs=["Tomato"],
+def test_processing(env_config, layout_config, item_info):
+    env = Environment(env_config, layout_config, item_info, as_files=False)
+    counter_pos = np.array([2, 2])
+    counter = CuttingBoard(
+        counter_pos,
+        transitions={
+            "ChoppedTomato": ItemInfo(
+                name="ChoppedTomato",
+                seconds=0.5,
+                equipment=ItemInfo(name="CuttingBoard", type=ItemType.Equipment),
+                type=ItemType.Ingredient,
+                needs=["Tomato"],
                 )
             },
-        )
-        sim.env.counters.append(counter)
+    )
+    env.counters.append(counter)
 
-        tomato = Item(name="Tomato", item_info=None)
-        sim.register_player("p1", np.array([2, 3]))
-        player = sim.env.players["p1"]
-        player.holding = tomato
+    tomato = Item(name="Tomato", item_info=None)
+    env.add_player("1", np.array([2, 3]))
+    player = env.players["1"]
+    player.player_speed_units_per_seconds = 1
+    player.holding = tomato
 
-        move = Action("p1", ActionType.MOVEMENT, np.array([0, -1]))
-        pick = Action("p1", ActionType.PUT, "pickup")
+    move = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)
+    pick = Action("1", ActionType.PUT, "pickup")
 
-        sim.enter_action(move)
-        sim.enter_action(pick)
+    env.perform_action(move)
+    env.step(timedelta(seconds=1))
+    env.perform_action(pick)
 
-        hold_down = Action("p1", ActionType.INTERACT, InterActionData.START)
-        sim.enter_action(hold_down)
+    hold_down = Action("1", ActionType.INTERACT, InterActionData.START)
+    env.perform_action(hold_down)
 
-        assert tomato.name != "ChoppedTomato", "Tomato is not finished yet."
+    assert tomato.name != "ChoppedTomato", "Tomato is not finished yet."
 
-        time.sleep(0.6)
+    env.step(timedelta(seconds=1))
 
-        assert tomato.name == "ChoppedTomato", "Tomato should be finished."
+    assert tomato.name == "ChoppedTomato", "Tomato should be finished."
 
-        button_up = Action("p1", ActionType.INTERACT, InterActionData.STOP)
-        sim.enter_action(button_up)
-    finally:
-        sim.stop()
+    button_up = Action("1", ActionType.INTERACT, InterActionData.STOP)
+    env.perform_action(button_up)
+    env.perform_action(pick)
+    assert player.holding.name == "ChoppedTomato", "Tomato should be finished."
 
 
 def test_time_passed():
@@ -304,7 +305,7 @@ def test_time_limit():
     assert not env.game_ended, "Game has not ended yet"
 
     passed_time_2 = timedelta(
-        seconds=(env.env_time_end - env.beginning_time).total_seconds()
+        seconds=(env.env_time_end - env.start_time).total_seconds()
     )
     env.step(passed_time_2)