diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py
index c32875fb9546c2096f130f0f95c37af06046c29e..18fc18e11ab51ab9b8d97b4c3e40a3972dc591ea 100644
--- a/cooperative_cuisine/__init__.py
+++ b/cooperative_cuisine/__init__.py
@@ -309,7 +309,7 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 8
+  speed_units_per_seconds: 8
   interaction_range: 1.6
 ```
 
@@ -356,17 +356,19 @@ num_bots: 0
 # 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 **action**s the player can perform,
 - the **counter factory** converts the characters in the layout file to counter instances,
 - the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven),
   sink, etc.,
 - the **effect manager**, how the fire effect is defined,
 - the **environment**, handles the incoming actions and provides the state,
 - the **study server**, the match making server,
-- the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
 - the **game server**, which can manage several running environments and can communicates via FastAPI post requests and
 websockets,
 - the **hooks**, you can adapt and extend recording, scoring, msgs to players, etc. via hooks,
 - the **info msgs** to players, based on hook events, short text msgs can be generated,
+- the game **items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
+- the calculation of player **movement**,
 - the **orders**, how to sample incoming orders and their attributes,
 - the **player**/agent, that interacts in the environment,
 - the **pygame 2d visualization**, GUI, drawing, and video generation,
@@ -374,7 +376,8 @@ websockets,
 - the **scores**, via hooks, events can affect the scores,
 - type hints are defined in **state representation** for the json state and **server results** for the data returned by
 the game server in post requests.
-- **util**ity code.
+- **util**ity code,
+- the config **validation** and graph generation.
 
 
 """
diff --git a/cooperative_cuisine/__main__.py b/cooperative_cuisine/__main__.py
index 6c067da2020b3b758b03c83f9abfdbf8da1d4eae..257ff656ae58f81e6ec88650132d910e091e8296 100644
--- a/cooperative_cuisine/__main__.py
+++ b/cooperative_cuisine/__main__.py
@@ -6,6 +6,7 @@ from cooperative_cuisine.utils import (
     url_and_port_arguments,
     disable_websocket_logging_arguments,
     add_list_of_manager_ids_arguments,
+    add_study_arguments,
 )
 
 USE_STUDY_SERVER = True
@@ -26,6 +27,7 @@ def start_study_server(cli_args):
         game_host=cli_args.game_url,
         game_port=cli_args.game_port,
         manager_ids=cli_args.manager_ids,
+        study_config_path=cli_args.study_config,
     )
 
 
@@ -53,6 +55,7 @@ def main(cli_args=None):
     url_and_port_arguments(parser)
     disable_websocket_logging_arguments(parser)
     add_list_of_manager_ids_arguments(parser)
+    add_study_arguments(parser)
 
     cli_args = parser.parse_args()
 
diff --git a/cooperative_cuisine/action.py b/cooperative_cuisine/action.py
new file mode 100644
index 0000000000000000000000000000000000000000..a080070b23a716556ca33841cf0226928c52334a
--- /dev/null
+++ b/cooperative_cuisine/action.py
@@ -0,0 +1,51 @@
+from __future__ import annotations
+
+import dataclasses
+from enum import Enum
+from typing import Literal
+
+from numpy import typing as npt
+
+
+class ActionType(Enum):
+    """The 3 different types of valid actions. They can be extended via the `Action.action_data` attribute."""
+
+    MOVEMENT = "movement"
+    """move the agent."""
+    PUT = "pickup"
+    """interaction type 1, e.g., for pickup or drop off."""
+    # TODO change value to put
+    INTERACT = "interact"
+    """interaction type 2, e.g., for progressing. Start and stop interaction via `keydown` and `keyup` actions."""
+
+
+class InterActionData(Enum):
+    """The data for the interaction action: `ActionType.MOVEMENT`."""
+
+    START = "keydown"
+    "start an interaction."
+    STOP = "keyup"
+    "stop an interaction without moving away."
+
+
+@dataclasses.dataclass
+class Action:
+    """Action class, specifies player, action type and action itself."""
+
+    player: str
+    """Id of the player."""
+    action_type: ActionType
+    """Type of the action to perform. Defines what action data is valid."""
+    action_data: npt.NDArray[float] | InterActionData | Literal["pickup"]
+    """Data for the action, e.g., movement vector or start and stop interaction."""
+    duration: float | int = 0
+    """Duration of the action (relevant for movement)"""
+
+    def __repr__(self):
+        return f"Action({self.player},{self.action_type.value},{self.action_data},{self.duration})"
+
+    def __post_init__(self):
+        if isinstance(self.action_type, str):
+            self.action_type = ActionType(self.action_type)
+        if isinstance(self.action_data, str) and self.action_data != "pickup":
+            self.action_data = InterActionData(self.action_data)
diff --git a/cooperative_cuisine/configs/agents/random_agent.py b/cooperative_cuisine/configs/agents/random_agent.py
index 1c36a303bdfd238d4ef0c6c41a06aed8c3c21436..f2208817bbdc55b0942c2c18a6bc8b65e235ec9f 100644
--- a/cooperative_cuisine/configs/agents/random_agent.py
+++ b/cooperative_cuisine/configs/agents/random_agent.py
@@ -10,11 +10,7 @@ from datetime import datetime, timedelta
 import numpy as np
 from websockets import connect
 
-from cooperative_cuisine.environment import (
-    ActionType,
-    Action,
-    InterActionData,
-)
+from cooperative_cuisine.action import ActionType, InterActionData, Action
 from cooperative_cuisine.utils import custom_asdict_factory
 
 TIME_TO_STOP_ACTION = 3.0
@@ -61,6 +57,8 @@ async def agent():
                 json.dumps({"type": "get_state", "player_hash": args.player_hash})
             )
             state = json.loads(await websocket.recv())
+            if not state["all_players_ready"]:
+                continue
 
             if counters is None:
                 counters = defaultdict(list)
diff --git a/cooperative_cuisine/configs/environment_config.yaml b/cooperative_cuisine/configs/environment_config.yaml
index 49a90ef585937eecbdd9a919a5a27be62bd3b2cb..c765eff0671f6e03ad9a8c47d0c02999779f9309 100644
--- a/cooperative_cuisine/configs/environment_config.yaml
+++ b/cooperative_cuisine/configs/environment_config.yaml
@@ -83,7 +83,7 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 6
+  speed_units_per_seconds: 6
   interaction_range: 1.6
   restricted_view: False
   view_angle: 70
@@ -91,7 +91,7 @@ player_config:
 
 effect_manager:
   FireManager:
-    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    class: !!python/name:cooperative_cuisine.effects.FireEffectManager ''
     kwargs:
       spreading_duration: [ 5, 10 ]
       fire_burns_ingredients_and_meals: true
diff --git a/cooperative_cuisine/configs/study/level1/level1_config.yaml b/cooperative_cuisine/configs/study/level1/level1_config.yaml
index d5ff23731253b196263ea97da05146f4f4f1ce74..0a6b785cdb6c874271052400629ebac79aaab120 100644
--- a/cooperative_cuisine/configs/study/level1/level1_config.yaml
+++ b/cooperative_cuisine/configs/study/level1/level1_config.yaml
@@ -82,7 +82,7 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 6
+  speed_units_per_seconds: 6
   interaction_range: 1.6
   restricted_view: False
   view_angle: 70
@@ -90,7 +90,7 @@ player_config:
 
 effect_manager:
   FireManager:
-    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    class: !!python/name:cooperative_cuisine.effects.FireEffectManager ''
     kwargs:
       spreading_duration: [ 5, 10 ]
       fire_burns_ingredients_and_meals: true
diff --git a/cooperative_cuisine/configs/study/level2/level2_config.yaml b/cooperative_cuisine/configs/study/level2/level2_config.yaml
index 918a653361e4e3ef1d3c1eef99319dcd44de4288..6d5c16fd044ed82cf2ea387f2102a2fdf4f9e940 100644
--- a/cooperative_cuisine/configs/study/level2/level2_config.yaml
+++ b/cooperative_cuisine/configs/study/level2/level2_config.yaml
@@ -81,7 +81,7 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 6
+  speed_units_per_seconds: 6
   interaction_range: 1.6
   restricted_view: True
   view_angle: 100
@@ -89,7 +89,7 @@ player_config:
 
 effect_manager:
   FireManager:
-    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    class: !!python/name:cooperative_cuisine.effects.FireEffectManager ''
     kwargs:
       spreading_duration: [ 5, 10 ]
       fire_burns_ingredients_and_meals: true
diff --git a/cooperative_cuisine/configs/study/study_config.yaml b/cooperative_cuisine/configs/study/study_config.yaml
index f39f2adaf68830a7282917fb74728747c4114df0..5d33271ee3b7bae3bff70bd0fe29e459eebaddc0 100644
--- a/cooperative_cuisine/configs/study/study_config.yaml
+++ b/cooperative_cuisine/configs/study/study_config.yaml
@@ -1,21 +1,17 @@
-# Config paths are relative to configs folder.
-# Layout files are relative to layouts folder.
-
-
 levels:
-  #  - config_path: study/level1/level1_config.yaml
-  #    layout_path: overcooked-1/1-1-far-apart.layout
-  #    item_info_path: study/level1/level1_item_info.yaml
-  #    name: "Level 1-1: Far Apart"
+  - config_path: STUDY_DIR/level1/level1_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-1/1-1-far-apart.layout
+    item_info_path: STUDY_DIR/level1/level1_item_info.yaml
+    name: "Level 1-1: Far Apart"
 
-  - config_path: environment_config.yaml
-    layout_path: basic.layout
-    item_info_path: item_info.yaml
+  - config_path: CONFIGS_DIR/environment_config.yaml
+    layout_path: LAYOUTS_DIR/basic.layout
+    item_info_path: CONFIGS_DIR/item_info.yaml
     name: "Basic"
 
-  - config_path: study/level2/level2_config.yaml
-    layout_path: overcooked-1/1-4-bottleneck.layout
-    item_info_path: study/level2/level2_item_info.yaml
+  - config_path: STUDY_DIR/level2/level2_config.yaml
+    layout_path: LAYOUTS_DIR/overcooked-1/1-4-bottleneck.layout
+    item_info_path: STUDY_DIR/level2/level2_item_info.yaml
     name: "Level 1-4: Bottleneck"
 
 
diff --git a/cooperative_cuisine/configs/tutorial_env_config.yaml b/cooperative_cuisine/configs/tutorial_env_config.yaml
index 1a41ba318239c52e435ae32ad9bdd34d34de6c6e..90f686ac69293d29d5704828038ddbe4dd79301a 100644
--- a/cooperative_cuisine/configs/tutorial_env_config.yaml
+++ b/cooperative_cuisine/configs/tutorial_env_config.yaml
@@ -79,7 +79,7 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 6
+  speed_units_per_seconds: 6
   interaction_range: 1.6
   restricted_view: False
   view_angle: 70
@@ -87,7 +87,7 @@ player_config:
 
 effect_manager:
   FireManager:
-    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+    class: !!python/name:cooperative_cuisine.effects.FireEffectManager ''
     kwargs:
       spreading_duration: [ 5, 10 ]
       fire_burns_ingredients_and_meals: true
diff --git a/cooperative_cuisine/counter_factory.py b/cooperative_cuisine/counter_factory.py
index 2dc90d33f9ef70c837f042b1e991304f732bfe6a..592def76bf602af71e453b3e4e6543858c04642f 100644
--- a/cooperative_cuisine/counter_factory.py
+++ b/cooperative_cuisine/counter_factory.py
@@ -34,7 +34,7 @@ layout_chars:
 import inspect
 import sys
 from random import Random
-from typing import Any, Type, TypeVar
+from typing import Any, Type, TypeVar, Tuple
 
 import numpy as np
 import numpy.typing as npt
@@ -51,15 +51,15 @@ from cooperative_cuisine.counters import (
     SinkAddon,
     Trashcan,
 )
-from cooperative_cuisine.effect_manager import EffectManager
-from cooperative_cuisine.game_items import (
+from cooperative_cuisine.effects import EffectManager
+from cooperative_cuisine.hooks import Hooks
+from cooperative_cuisine.items import (
     ItemInfo,
     ItemType,
     CookingEquipment,
     Plate,
     Item,
 )
-from cooperative_cuisine.hooks import Hooks
 from cooperative_cuisine.orders import OrderManager
 from cooperative_cuisine.utils import get_closest
 
@@ -384,3 +384,129 @@ class CounterFactory:
     def get_counter_of_type(counter_type: Type[T], counters: list[Counter]) -> list[T]:
         """Filter all counters in the environment for a counter type."""
         return list(filter(lambda counter: isinstance(counter, counter_type), counters))
+
+    def parse_layout_file(
+        self,
+        layout_config: str,
+    ) -> Tuple[list[Counter], list[npt.NDArray], list[npt.NDArray], int, int]:
+        """Creates layout of kitchen counters in the environment based on layout file.
+        Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at
+        [counter_size/2, counter_size/2], counters are directly next to each other (of no empty space is specified
+        in layout).
+
+        Args:
+            layout_config: the layout config string. Each character corresponds to a counter and each line to a row.
+        """
+
+        starting_at: float = 0.0
+        current_y: float = starting_at
+        counters: list[Counter] = []
+        designated_player_positions: list[npt.NDArray] = []
+        free_positions: list[npt.NDArray] = []
+
+        lines = layout_config.split("\n")
+
+        grid = []
+
+        max_width = 0
+
+        lines = list(filter(lambda l: l != "", lines))
+        for line in lines:
+            line = line.replace(" ", "")
+            if not line or line.startswith(";"):
+                break
+            current_x: float = starting_at
+            grid_line = []
+
+            for character in line:
+                # character = character.capitalize()
+                pos = np.array([current_x, current_y])
+
+                assert self.can_map(
+                    character
+                ), f"{character=} in layout file can not be mapped"
+                if self.is_counter(character):
+                    counters.append(self.get_counter_object(character, pos))
+                    grid_line.append(1)
+                else:
+                    grid_line.append(0)
+                    match self.map_not_counter(character):
+                        case "Agent":
+                            designated_player_positions.append(pos)
+                        case "Free":
+                            free_positions.append(np.array([current_x, current_y]))
+
+                current_x += 1
+
+            if len(line) >= max_width:
+                max_width = len(line)
+
+            grid.append(grid_line)
+            current_y += 1
+
+        grid = [line + ([0] * (max_width - len(line))) for line in grid]
+
+        kitchen_width = int(max_width + starting_at)
+        kitchen_height = int(current_y)
+
+        determine_counter_orientations(
+            counters, grid, np.array([kitchen_width / 2, kitchen_height / 2])
+        )
+
+        self.post_counter_setup(counters)
+
+        return (
+            counters,
+            designated_player_positions,
+            free_positions,
+            kitchen_width,
+            kitchen_height,
+        )
+
+
+def determine_counter_orientations(counters, grid, kitchen_center):
+    grid = np.array(grid).T
+
+    grid_width = grid.shape[0]
+    grid_height = grid.shape[1]
+
+    last_counter = None
+    fst_counter_in_row = None
+    for c in counters:
+        grid_idx = np.floor(c.pos).astype(int)
+        neighbour_offsets = np.array([[0, 1], [0, -1], [1, 0], [-1, 0]], dtype=int)
+
+        neighbours_free = []
+        for offset in neighbour_offsets:
+            neighbour_pos = grid_idx + offset
+            if (
+                neighbour_pos[0] > (grid_width - 1)
+                or neighbour_pos[0] < 0
+                or neighbour_pos[1] > (grid_height - 1)
+                or neighbour_pos[1] < 0
+            ):
+                pass
+            else:
+                if grid[neighbour_pos[0]][neighbour_pos[1]] == 0:
+                    neighbours_free.append(offset)
+        if len(neighbours_free) > 0:
+            vector_to_center = c.pos - kitchen_center
+            vector_to_center /= np.linalg.norm(vector_to_center)
+            n_idx = np.argmin(
+                np.linalg.norm(vector_to_center - n) for n in neighbours_free
+            )
+            nearest_vec = neighbours_free[n_idx]
+            # print(nearest_vec, type(nearest_vec))
+            c.set_orientation(nearest_vec)
+
+        elif grid_idx[0] == 0:
+            if grid_idx[1] == 0 or fst_counter_in_row is None:
+                # counter top left
+                c.set_orientation(np.array([1, 0]))
+            else:
+                c.set_orientation(fst_counter_in_row.orientation)
+            fst_counter_in_row = c
+        else:
+            c.set_orientation(last_counter.orientation)
+
+        last_counter = c
diff --git a/cooperative_cuisine/counters.py b/cooperative_cuisine/counters.py
index ff0c420dc192ae7b4bb448f5b80fd5dc0a52748a..2853e59eafc010736fa7ed2938d5a6bbe4d7cec9 100644
--- a/cooperative_cuisine/counters.py
+++ b/cooperative_cuisine/counters.py
@@ -65,7 +65,6 @@ from cooperative_cuisine.hooks import (
 )
 
 if TYPE_CHECKING:
-    from cooperative_cuisine.effect_manager import Effect
     from cooperative_cuisine.environment import (
         OrderManager,
     )
@@ -73,12 +72,13 @@ if TYPE_CHECKING:
 import numpy as np
 import numpy.typing as npt
 
-from cooperative_cuisine.game_items import (
+from cooperative_cuisine.items import (
     Item,
     CookingEquipment,
     Plate,
     ItemInfo,
     EffectType,
+    Effect,
 )
 
 
@@ -181,7 +181,6 @@ class Counter:
 
         Args:
             item: The item for which to check, if it can be placed on the counter.
-            player: The player name that tries to drop something on the counter.
 
 
         Returns: True if the item can be placed on the counter, False if not.
@@ -194,7 +193,7 @@ class Counter:
 
         Args:
             item: The item to be placed on the counter.
-
+            player: The player name that tries to drop something on the counter.
 
         Returns:
             Item or None what should be put back on the players hand, e.g., the cooking equipment.
@@ -232,8 +231,9 @@ class Counter:
         if not successful:
             self._do_single_tool_interaction(passed_time, tool, self)
 
+    @staticmethod
     def _do_single_tool_interaction(
-        self, passed_time: timedelta, tool: Item, target: Item | Counter
+        passed_time: timedelta, tool: Item, target: Item | Counter
     ) -> bool:
         suitable_effects = [
             e for e in target.active_effects if e.name in tool.item_info.needs
diff --git a/cooperative_cuisine/effect_manager.py b/cooperative_cuisine/effects.py
similarity index 98%
rename from cooperative_cuisine/effect_manager.py
rename to cooperative_cuisine/effects.py
index 2fb36740e748f9662574955c5d2190f4f9cb9e45..0e5d3ec63a257022f5edaedb7d9046f6802ac015 100644
--- a/cooperative_cuisine/effect_manager.py
+++ b/cooperative_cuisine/effects.py
@@ -12,20 +12,23 @@ from datetime import timedelta, datetime
 from random import Random
 from typing import TYPE_CHECKING, Tuple
 
-from cooperative_cuisine.game_items import (
+from cooperative_cuisine.hooks import Hooks, NEW_FIRE, FIRE_SPREADING
+from cooperative_cuisine.items import (
     ItemInfo,
     Item,
     ItemType,
-    Effect,
     CookingEquipment,
+    Effect,
 )
-from cooperative_cuisine.hooks import Hooks, NEW_FIRE, FIRE_SPREADING
 from cooperative_cuisine.utils import get_touching_counters, find_item_on_counters
 
 if TYPE_CHECKING:
     from cooperative_cuisine.counters import Counter
 
 
+# the effect class is located in the items.py
+
+
 class EffectManager:
     def __init__(self, hook: Hooks, random: Random) -> None:
         self.effects = []
diff --git a/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py
index 82ffb4f8e3289dcc22e3ee31c5fc559859f4c3f8..8a724f5d6d90e47a9ba618152d4353227b0cbab5 100644
--- a/cooperative_cuisine/environment.py
+++ b/cooperative_cuisine/environment.py
@@ -1,41 +1,27 @@
 from __future__ import annotations
 
-import dataclasses
 import inspect
 import json
 import logging
-import os
 import sys
-import warnings
 from collections import defaultdict
-from concurrent.futures import ThreadPoolExecutor
 from datetime import timedelta, datetime
-from enum import Enum
 from pathlib import Path
 from random import Random
-from typing import Literal, TypedDict, Callable, Tuple, Iterator
+from typing import Literal, TypedDict, Callable
 
-import networkx
-import networkx.drawing.layout
 import numpy as np
 import numpy.typing as npt
 import yaml
-from networkx import DiGraph
-from networkx import Graph
-from scipy.spatial import distance_matrix
 
-from cooperative_cuisine import ROOT_DIR
-from cooperative_cuisine.counter_factory import CounterFactory
+from cooperative_cuisine.action import ActionType, InterActionData, Action
+from cooperative_cuisine.counter_factory import (
+    CounterFactory,
+)
 from cooperative_cuisine.counters import (
-    Counter,
-    Dispenser,
     PlateConfig,
-    PlateDispenser,
-    CuttingBoard,
-    CookingCounter,
 )
-from cooperative_cuisine.effect_manager import EffectManager
-from cooperative_cuisine.game_items import ItemInfo, ItemType, Item
+from cooperative_cuisine.effects import EffectManager
 from cooperative_cuisine.hooks import (
     ITEM_INFO_LOADED,
     LAYOUT_FILE_PARSED,
@@ -56,13 +42,22 @@ from cooperative_cuisine.hooks import (
     ITEM_INFO_CONFIG,
     POST_STEP,
 )
+from cooperative_cuisine.items import (
+    ItemInfo,
+    ItemType,
+)
+from cooperative_cuisine.movement import Movement
 from cooperative_cuisine.orders import (
     OrderManager,
     OrderConfig,
 )
 from cooperative_cuisine.player import Player, PlayerConfig
 from cooperative_cuisine.state_representation import InfoMsg
-from cooperative_cuisine.utils import create_init_env_time, get_closest
+from cooperative_cuisine.utils import (
+    create_init_env_time,
+    get_closest,
+)
+from cooperative_cuisine.validation import Validation
 
 log = logging.getLogger(__name__)
 
@@ -70,53 +65,6 @@ log = logging.getLogger(__name__)
 PREVENT_SQUEEZING_INTO_OTHER_PLAYERS = True
 
 
-class ActionType(Enum):
-    """The 3 different types of valid actions. They can be extended via the `Action.action_data` attribute."""
-
-    MOVEMENT = "movement"
-    """move the agent."""
-    PUT = "pickup"
-    """interaction type 1, e.g., for pickup or drop off. Maybe other words: transplace?"""
-    # TODO change value to put
-    INTERACT = "interact"
-    """interaction type 2, e.g., for progressing. Start and stop interaction via `keydown` and `keyup` actions."""
-
-
-class InterActionData(Enum):
-    """The data for the interaction action: `ActionType.MOVEMENT`."""
-
-    START = "keydown"
-    "start an interaction."
-    STOP = "keyup"
-    "stop an interaction without moving away."
-
-
-@dataclasses.dataclass
-class Action:
-    """Action class, specifies player, action type and action itself."""
-
-    player: str
-    """Id of the player."""
-    action_type: ActionType
-    """Type of the action to perform. Defines what action data is valid."""
-    action_data: npt.NDArray[float] | InterActionData | Literal["pickup"]
-    """Data for the action, e.g., movement vector or start and stop interaction."""
-    duration: float | int = 0
-    """Duration of the action (relevant for movement)"""
-
-    def __repr__(self):
-        return f"Action({self.player},{self.action_type.value},{self.action_data},{self.duration})"
-
-    def __post_init__(self):
-        if isinstance(self.action_type, str):
-            self.action_type = ActionType(self.action_type)
-        if isinstance(self.action_data, str) and self.action_data != "pickup":
-            self.action_data = InterActionData(self.action_data)
-
-
-# TODO Abstract base class for different environments
-
-
 class EnvironmentConfig(TypedDict):
     plates: PlateConfig
     game: dict[
@@ -169,6 +117,10 @@ class Environment:
         if self.as_files:
             with open(env_config, "r") as file:
                 env_config = file.read()
+            with open(layout_config, "r") as layout_file:
+                layout_config = layout_file.read()
+            with open(item_info, "r") as file:
+                item_info = file.read()
 
         self.environment_config: EnvironmentConfig = yaml.load(
             env_config, Loader=yaml.Loader
@@ -194,7 +146,7 @@ class Environment:
 
         self.item_info: dict[str, ItemInfo] = self.load_item_info(item_info)
         """The loaded item info dict. Keys are the item names."""
-        self.hook(ITEM_INFO_LOADED, item_info=item_info, as_files=as_files)
+        self.hook(ITEM_INFO_LOADED, item_info=item_info)
 
         if self.environment_config["meals"]["all"]:
             self.allowed_meal_names = set(
@@ -216,11 +168,6 @@ class Environment:
         )
         """The manager for the orders and score update."""
 
-        self.kitchen_height: int = 0
-        """The height of the kitchen, is set by the `Environment.parse_layout_file` method"""
-        self.kitchen_width: int = 0
-        """The width of the kitchen, is set by the `Environment.parse_layout_file` method"""
-
         self.counter_factory = CounterFactory(
             layout_chars_config=self.environment_config["layout_chars"],
             item_info=self.item_info,
@@ -251,41 +198,35 @@ class Environment:
             self.counters,
             self.designated_player_positions,
             self.free_positions,
-        ) = self.parse_layout_file()
+            self.kitchen_width,
+            self.kitchen_height,
+        ) = self.counter_factory.parse_layout_file(self.layout_config)
         self.hook(LAYOUT_FILE_PARSED)
 
-        self.world_borders = np.array(
-            [[-0.5, self.kitchen_width - 0.5], [-0.5, self.kitchen_height - 0.5]],
-            dtype=float,
+        self.movement = Movement(
+            counter_positions=np.array([c.pos for c in self.counters]),
+            player_config=self.environment_config["player_config"],
+            world_borders=np.array(
+                [[-0.5, self.kitchen_width - 0.5], [-0.5, self.kitchen_height - 0.5]],
+                dtype=float,
+            ),
         )
 
-        self.player_movement_speed = self.environment_config["player_config"][
-            "player_speed_units_per_seconds"
-        ]
-        self.player_radius = self.environment_config["player_config"]["radius"]
-        self.player_interaction_range = self.environment_config["player_config"][
-            "interaction_range"
-        ]
-
-        progress_counter_classes = list(
-            filter(
-                lambda cl: hasattr(cl, "progress"),
-                dict(
-                    inspect.getmembers(
-                        sys.modules["cooperative_cuisine.counters"], inspect.isclass
-                    )
-                ).values(),
-            )
-        )
-        self.progressing_counters = list(
-            filter(
-                lambda c: c.__class__ in progress_counter_classes,
-                self.counters,
-            )
-        )
+        self.progressing_counters = []
         """Counters that needs to be called in the step function via the `progress` method."""
+        self.overwrite_counters(self.counters)
 
-        self.counter_positions = np.array([c.pos for c in self.counters])
+        self.recipe_validation = Validation(
+            meals=[m for m in self.item_info.values() if m.type == ItemType.Meal]
+            if self.environment_config["meals"]["all"]
+            else [
+                self.item_info[m]
+                for m in self.environment_config["meals"]["list"]
+                if self.item_info[m].type == ItemType.Meal
+            ],
+            item_info=self.item_info,
+            order_manager=self.order_manager,
+        )
 
         if (
             "validate_recipes" in self.environment_config["game"].keys()
@@ -308,6 +249,8 @@ class Environment:
                 if not self.environment_config["meals"]["all"]
                 else all_meals
             )
+        meals_to_be_ordered = self.recipe_validation.validate_environment(self.counters)
+        assert meals_to_be_ordered, "Need possible meals for order generation."
 
         self.order_manager.set_available_meals(available_meals)
         self.order_manager.create_init_orders(self.env_time)
@@ -333,11 +276,9 @@ class Environment:
             env_start_time_worldtime=datetime.now(),
         )
 
-        self.all_players_ready = False
-
     def overwrite_counters(self, counters):
         self.counters = counters
-        self.counter_positions = np.array([c.pos for c in self.counters])
+        self.movement.counter_positions = np.array([c.pos for c in self.counters])
 
         progress_counter_classes = list(
             filter(
@@ -361,28 +302,16 @@ class Environment:
         """Whether the game is over or not based on the calculated `Environment.env_time_end`"""
         return self.env_time >= self.env_time_end
 
-    def set_collision_arrays(self):
-        number_players = len(self.players)
-        self.world_borders_lower = self.world_borders[np.newaxis, :, 0].repeat(
-            number_players, axis=0
-        )
-        self.world_borders_upper = self.world_borders[np.newaxis, :, 1].repeat(
-            number_players, axis=0
-        )
-
     def get_env_time(self):
         """the internal time of the environment. An environment starts always with the time from `create_init_env_time`.
 
         Utility method to pass a reference to the serving window."""
         return self.env_time
 
-    def load_item_info(self, data) -> dict[str, ItemInfo]:
+    def load_item_info(self, item_info) -> dict[str, ItemInfo]:
         """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos."""
-        if self.as_files:
-            with open(data, "r") as file:
-                data = file.read()
-        self.hook(ITEM_INFO_CONFIG, item_info_config=data)
-        item_lookup = yaml.safe_load(data)
+        self.hook(ITEM_INFO_CONFIG, item_info_config=item_info)
+        item_lookup = yaml.safe_load(item_info)
         for item_name in item_lookup:
             item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name])
 
@@ -391,451 +320,6 @@ class Environment:
                 item_info.equipment = item_lookup[item_info.equipment]
         return item_lookup
 
-    @staticmethod
-    def infer_recipe_graph(item_info) -> DiGraph:
-        colors = {
-            ItemType.Ingredient: "black",
-            ItemType.Equipment: "red",
-            ItemType.Meal: "green",
-            ItemType.Waste: "brown",
-        }
-
-        graph = DiGraph(directed=True)
-        for item_name, item_info in item_info.items():
-            graph.add_node(item_name, color=colors.get(item_info.type, "blue"))
-            if item_info.equipment is None:
-                for item in item_info.needs:
-                    graph.add_edge(item, item_name)
-            else:
-                if len(item_info.needs) > 0:
-                    for item in item_info.needs:
-                        graph.add_edge(item, item_info.equipment.name)
-                        graph.add_edge(item_info.equipment.name, item_name)
-                else:
-                    graph.add_edge(item_name, item_info.equipment.name)
-        return graph
-
-    def get_meal_graph(self, meal: ItemInfo) -> tuple[Graph, dict[str, list[float]]]:
-        graph = DiGraph(directed=True, rankdir="LR")
-
-        root = meal.name + "_0"
-
-        graph.add_node(root)
-        add_queue = ["Plate_0", root]
-
-        start = True
-        while add_queue:
-            current = add_queue.pop()
-
-            current_info = self.item_info[current.split("_")[0]]
-            current_index = current.split("_")[-1]
-
-            if start:
-                graph.add_edge("Plate_0", current)
-                current = "Plate_0"
-                start = False
-
-            if current_info.needs:
-                if len(current_info.needs) == 1:
-                    need = current_info.needs[0] + f"_{current_index}"
-                    add_queue.append(need)
-
-                    if current_info.equipment:
-                        equip_id = current_info.equipment.name + f"_{current_index}"
-                        if current_info.equipment.equipment:
-                            equip_equip_id = (
-                                current_info.equipment.equipment.name
-                                + f"_{current_index}"
-                            )
-                            graph.add_edge(equip_equip_id, current)
-                            graph.add_edge(equip_id, equip_equip_id)
-                            graph.add_edge(need, equip_id)
-                        else:
-                            graph.add_edge(equip_id, current)
-                            graph.add_edge(need, equip_id)
-                    else:
-                        graph.add_edge(need, current)
-
-                elif len(current_info.needs) > 1:
-                    for idx, item_name in enumerate(current_info.needs):
-                        add_queue.append(item_name + f"_{idx}")
-
-                        if current_info.equipment and current_info.equipment.equipment:
-                            equip_id = current_info.equipment.name + f"_{current_index}"
-                            equip_equip_id = (
-                                current_info.equipment.equipment.name
-                                + f"_{current_index}"
-                            )
-                            graph.add_edge(equip_equip_id, current)
-                            graph.add_edge(equip_id, equip_equip_id)
-                            graph.add_edge(item_name + f"_{idx}", equip_id)
-                        else:
-                            graph.add_edge(
-                                item_name + f"_{idx}",
-                                current,
-                            )
-
-        agraph = networkx.nx_agraph.to_agraph(graph)
-        layout = networkx.nx_agraph.graphviz_layout(graph, prog="dot")
-        agraph.draw(
-            ROOT_DIR / "generated" / f"recipe_graph_{meal.name}.png",
-            format="png",
-            prog="dot",
-        )
-
-        return graph, layout
-
-    def reduce_item_node(self, graph, base_ingredients, item, visited):
-        visited.append(item)
-        if item in base_ingredients:
-            return True
-        else:
-            return all(
-                self.reduce_item_node(graph, base_ingredients, pred, visited)
-                for pred in graph.predecessors(item)
-                if pred not in visited
-            )
-
-    def assert_equipment_is_present(self):
-        expected = set(
-            name
-            for name, info in self.item_info.items()
-            if info.type == ItemType.Equipment and "Plate" not in info.name
-        )
-        counters = set(c.__class__.__name__ for c in self.counters).union(
-            set(c.name for c in self.counters if hasattr(c, "name"))
-        )
-        items = set(
-            c.occupied_by.name
-            for c in self.counters
-            if c.occupied_by is not None and isinstance(c.occupied_by, Item)
-        )
-        for equipment in expected:
-            if equipment not in counters and equipment not in items:
-                raise ValueError(
-                    f"Equipment '{equipment}' from config files not found in the environment layout.\n"
-                    f"Config Equipment: {sorted(expected)}\n"
-                    f"Layout Counters: {sorted(counters)}\n"
-                    f"Layout Items: {sorted(items)}"
-                )
-
-    def assert_plate_cycle_present(self):
-        for plate in ["Plate", "DirtyPlate"]:
-            if plate not in self.item_info:
-                raise ValueError(f"{plate} not found in item info")
-
-        relevant_counters = ["PlateDispenser", "ServingWindow"]
-        for counter in self.counters:
-            if isinstance(counter, PlateDispenser):
-                if counter.plate_config.return_dirty:
-                    relevant_counters = [
-                        "PlateDispenser",
-                        "ServingWindow",
-                        "Sink",
-                        "SinkAddon",
-                    ]
-
-        counter_names = [c.__class__.__name__ for c in self.counters]
-        for counter in relevant_counters:
-            if counter not in counter_names:
-                raise ValueError(f"{counter} not found in counters")
-
-    @staticmethod
-    def assert_no_orphans(graph: DiGraph):
-        orphans = [
-            n
-            for n in graph.nodes()
-            if graph.in_degree(n) == 0 and graph.out_degree(n) == 0
-        ]
-        if orphans:
-            raise ValueError(
-                f"Expected all items to be part of a recipe, but found orphans: {orphans}"
-            )
-
-    @staticmethod
-    def assert_roots_are_dispensable(graph, base_ingredients):
-        root_nodes = [
-            n for n in graph.nodes() if graph.in_degree(n) == 0 and "Plate" not in n
-        ]
-        if set(root_nodes) != set(base_ingredients):
-            raise ValueError(
-                f"Expected root nodes in the recipe graph and dispensable items to be identical, but found\n "
-                f"Root nodes: {sorted(root_nodes)}\n"
-                f"Dispensable items: {sorted(base_ingredients)}"
-            )
-
-    def assert_meals_are_reducible(self, graph, base_ingredients):
-        meals = [n for n in graph.nodes() if self.item_info[n].type == ItemType.Meal]
-
-        for meal in meals:
-            visited = []
-            if not self.reduce_item_node(graph, base_ingredients, meal, visited):
-                raise ValueError(
-                    f"Meal '{meal}' can not be reduced to base ingredients"
-                )
-
-    def get_requirements(self, item_name: str) -> Iterator[str]:
-        """
-        Get all base ingredients and equipment required to create the given meal.
-        """
-        item = self.item_info[item_name]
-        is_equipment = item.type == ItemType.Equipment
-        is_base_ingredient = item.type == ItemType.Ingredient and not item.needs
-
-        if is_equipment or is_base_ingredient:
-            yield item_name
-        for need in item.needs:
-            yield from self.get_requirements(need)
-        if item.equipment is not None:
-            yield from self.get_requirements(item.equipment.name)
-
-    def get_item_info_requirements(self) -> dict[str, set[str]]:
-        recipes = {}
-        for item_name, item_info in self.item_info.items():
-            if item_info.type == ItemType.Meal:
-                requirements = set(r for r in self.get_requirements(item_name))
-                recipes[item_name] = requirements | {"Plate"}
-        return recipes
-
-    def get_layout_requirements(self):
-        layout_requirements = set()
-        for counter in self.counters:
-            if isinstance(counter, (Dispenser, PlateDispenser)):
-                layout_requirements.add(counter.dispensing.name)
-            if isinstance(counter, CuttingBoard):
-                layout_requirements.add("CuttingBoard")
-            if isinstance(counter, CookingCounter):
-                layout_requirements.add(counter.name)
-            if counter.occupied_by is not None and hasattr(counter.occupied_by, "name"):
-                layout_requirements.add(counter.occupied_by.name)
-        return layout_requirements
-
-    def validate_environment(self):
-        graph = self.infer_recipe_graph(self.item_info)
-        os.makedirs(ROOT_DIR / "generated", exist_ok=True)
-        networkx.nx_agraph.to_agraph(graph).draw(
-            ROOT_DIR / "generated" / "recipe_graph.png", format="png", prog="dot"
-        )
-
-        expected = self.get_item_info_requirements()
-        present = self.get_layout_requirements()
-        possible_meals = set(meal for meal in expected if expected[meal] <= present)
-        defined_meals = set(
-            possible_meals
-            if self.environment_config["meals"]["all"]
-            else self.environment_config["meals"]["list"]
-        )
-
-        # print(f"Ordered meals: {defined_meals}, Possible meals: {possible_meals}")
-        if len(defined_meals - possible_meals) > 0:
-            warnings.warn(
-                f"Ordered meals are not possible: {defined_meals - possible_meals}"
-            )
-
-        meals_to_be_ordered = possible_meals.intersection(defined_meals)
-        return meals_to_be_ordered
-        # print("FINAL MEALS:", meals_to_be_ordered)
-
-    def get_meal_graph(self, meal: ItemInfo) -> dict:
-        graph = DiGraph(
-            directed=True, rankdir="LR", graph_attr={"nslimit": "0", "nslimit1": "2"}
-        )
-
-        root = meal.name + "_0"
-
-        graph.add_node(root)
-        add_queue = [root]
-
-        start = True
-        while add_queue:
-            current = add_queue.pop()
-
-            current_info = self.item_info[current.split("_")[0]]
-            current_index = current.split("_")[-1]
-
-            if start:
-                graph.add_edge("Plate_0", current)
-                current = "Plate_0"
-                start = False
-
-            if current_info.needs:
-                if len(current_info.needs) == 1:
-                    need = current_info.needs[0] + f"_{current_index}"
-                    add_queue.append(need)
-
-                    if current_info.equipment:
-                        equip_id = current_info.equipment.name + f"_{current_index}"
-                        if current_info.equipment.equipment:
-                            equip_equip_id = (
-                                current_info.equipment.equipment.name
-                                + f"_{current_index}"
-                            )
-                            graph.add_edge(equip_equip_id, current)
-                            graph.add_edge(equip_id, equip_equip_id)
-                            graph.add_edge(need, equip_id)
-                        else:
-                            graph.add_edge(equip_id, current)
-                            graph.add_edge(need, equip_id)
-                    else:
-                        graph.add_edge(need, current)
-
-                elif len(current_info.needs) > 1:
-                    for idx, item_name in enumerate(current_info.needs):
-                        add_queue.append(item_name + f"_{idx}")
-
-                        if current_info.equipment and current_info.equipment.equipment:
-                            equip_id = current_info.equipment.name + f"_{current_index}"
-                            equip_equip_id = (
-                                current_info.equipment.equipment.name
-                                + f"_{current_index}"
-                            )
-                            graph.add_edge(equip_equip_id, current)
-                            graph.add_edge(equip_id, equip_equip_id)
-                            graph.add_edge(item_name + f"_{idx}", equip_id)
-                        else:
-                            graph.add_edge(
-                                item_name + f"_{idx}",
-                                current,
-                            )
-
-        layout = networkx.nx_agraph.graphviz_layout(graph, prog="dot")
-
-        edges = [(start, end) for start, end in graph.edges]
-
-        return {"meal": meal.name, "edges": edges, "layout": layout}
-
-    def validate_item_info(self):
-        """TODO"""
-        raise NotImplementedError
-
-    def parse_layout_file(
-        self,
-    ) -> Tuple[list[Counter], list[npt.NDArray], list[npt.NDArray]]:
-        """Creates layout of kitchen counters in the environment based on layout file.
-        Counters are arranged in a fixed size grid starting at [0,0]. The center of the first counter is at
-        [counter_size/2, counter_size/2], counters are directly next to each other (of no empty space is specified
-        in layout).
-        """
-
-        starting_at: float = 0.0
-        current_y: float = starting_at
-        counters: list[Counter] = []
-        designated_player_positions: list[npt.NDArray] = []
-        free_positions: list[npt.NDArray] = []
-
-        if self.as_files:
-            with open(self.layout_config, "r") as layout_file:
-                self.layout_config = layout_file.read()
-        lines = self.layout_config.split("\n")
-
-        grid = []
-
-        max_width = 0
-
-        lines = list(filter(lambda l: l != "", lines))
-        for line in lines:
-            line = line.replace(" ", "")
-            if not line or line.startswith(";"):
-                break
-            current_x: float = starting_at
-            grid_line = []
-
-            for character in line:
-                # character = character.capitalize()
-                pos = np.array([current_x, current_y])
-
-                assert self.counter_factory.can_map(
-                    character
-                ), f"{character=} in layout file can not be mapped"
-                if self.counter_factory.is_counter(character):
-                    counters.append(
-                        self.counter_factory.get_counter_object(character, pos)
-                    )
-                    grid_line.append(1)
-                else:
-                    grid_line.append(0)
-                    match self.counter_factory.map_not_counter(character):
-                        case "Agent":
-                            designated_player_positions.append(pos)
-                        case "Free":
-                            free_positions.append(np.array([current_x, current_y]))
-
-                current_x += 1
-
-            if len(line) >= max_width:
-                max_width = len(line)
-
-            grid.append(grid_line)
-            current_y += 1
-
-        grid = [line + ([0] * (max_width - len(line))) for line in grid]
-
-        self.kitchen_width: float = max_width + starting_at
-        self.kitchen_height = current_y
-
-        self.determine_counter_orientations(
-            counters, grid, np.array([self.kitchen_width / 2, self.kitchen_height / 2])
-        )
-
-        self.counter_factory.post_counter_setup(counters)
-
-        return counters, designated_player_positions, free_positions
-
-    def determine_counter_orientations(self, counters, grid, kitchen_center):
-        grid = np.array(grid).T
-
-        grid_width = grid.shape[0]
-        grid_height = grid.shape[1]
-
-        last_counter = None
-        fst_counter_in_row = None
-        for c in counters:
-            grid_idx = np.floor(c.pos).astype(int)
-            neighbour_offsets = np.array([[0, 1], [0, -1], [1, 0], [-1, 0]], dtype=int)
-
-            neighbours_free = []
-            for offset in neighbour_offsets:
-                neighbour_pos = grid_idx + offset
-                if (
-                    neighbour_pos[0] > (grid_width - 1)
-                    or neighbour_pos[0] < 0
-                    or neighbour_pos[1] > (grid_height - 1)
-                    or neighbour_pos[1] < 0
-                ):
-                    pass
-                else:
-                    if grid[neighbour_pos[0]][neighbour_pos[1]] == 0:
-                        neighbours_free.append(offset)
-            if len(neighbours_free) > 0:
-                vector_to_center = c.pos - kitchen_center
-                vector_to_center /= np.linalg.norm(vector_to_center)
-                n_idx = np.argmin(
-                    np.linalg.norm(vector_to_center - n) for n in neighbours_free
-                )
-                nearest_vec = neighbours_free[n_idx]
-                # print(nearest_vec, type(nearest_vec))
-                c.set_orientation(nearest_vec)
-
-            elif grid_idx[0] == 0:
-                if grid_idx[1] == 0 or fst_counter_in_row is None:
-                    # counter top left
-                    c.set_orientation(np.array([1, 0]))
-                else:
-                    c.set_orientation(fst_counter_in_row.orientation)
-                fst_counter_in_row = c
-            else:
-                c.set_orientation(last_counter.orientation)
-
-            last_counter = c
-
-        # for c in counters:
-        #     near_counters = [
-        #         other
-        #         for other in counters
-        #         if np.isclose(np.linalg.norm(c.pos - other.pos), 1)
-        #     ]
-        #     # print(c.pos, len(near_counters))
-
     def perform_action(self, action: Action):
         """Performs an action of a player in the environment. Maps different types of action inputs to the
         correct execution of the players.
@@ -854,7 +338,7 @@ class Environment:
                 self.env_time + timedelta(seconds=action.duration),
             )
         else:
-            counter = self.get_facing_counter(player)
+            counter = get_closest(player.facing_point, self.counters)
             if player.can_reach(counter):
                 if action.action_type == ActionType.PUT:
                     player.put_action(counter)
@@ -872,169 +356,6 @@ class Environment:
 
         self.hook(POST_PERFORM_ACTION, action=action)
 
-    def get_facing_counter(self, player: Player):
-        """Determines the counter which the player is looking at.
-        Adds a multiple of the player facing direction onto the player position and finds the closest
-        counter for that point.
-
-        Args:
-            player: The player for which to find the facing counter.
-
-        Returns:
-
-        """
-        facing_counter = get_closest(player.facing_point, self.counters)
-        return facing_counter
-
-    def get_counter_collisions(self, player_positions):
-        counter_diff_vecs = (
-            player_positions[:, np.newaxis, :]
-            - self.counter_positions[np.newaxis, :, :]
-        )
-        counter_distances = np.max((np.abs(counter_diff_vecs)), axis=2)
-        closest_counter_positions = self.counter_positions[
-            np.argmin(counter_distances, axis=1)
-        ]
-        nearest_counter_to_player = player_positions - closest_counter_positions
-        relevant_axes = np.abs(nearest_counter_to_player).argmax(axis=1)
-
-        distances = np.linalg.norm(
-            np.max(
-                [
-                    np.abs(counter_diff_vecs) - 0.5,
-                    np.zeros(counter_diff_vecs.shape),
-                ],
-                axis=0,
-            ),
-            axis=2,
-        )
-
-        collided = np.any(distances < self.player_radius, axis=1)
-
-        return collided, relevant_axes, nearest_counter_to_player
-
-    def get_player_push(self, player_positions):
-        distances_players_after_scipy = distance_matrix(
-            player_positions, player_positions
-        )
-
-        player_diff_vecs = -(
-            player_positions[:, np.newaxis, :] - player_positions[np.newaxis, :, :]
-        )
-        collisions = distances_players_after_scipy < (2 * self.player_radius)
-        eye_idxs = np.eye(len(player_positions), len(player_positions), dtype=bool)
-        collisions[eye_idxs] = False
-        player_diff_vecs[collisions == False] = 0
-        push_vectors = np.sum(player_diff_vecs, axis=0)
-        collisions = np.any(collisions, axis=1)
-        return collisions, push_vectors
-
-    def perform_movement(self, duration: timedelta):
-        """Moves a player in the direction specified in the action.action. If the player collides with a
-        counter or other player through this movement, then they are not moved.
-        (The extended code with the two ifs is for sliding movement at the counters, which feels a bit smoother.
-        This happens, when the player moves diagonally against the counters or world boundary.
-        This just checks if the single axis party of the movement could move the player and does so at a lower rate.)
-
-        The movement action is a unit 2d vector.
-
-        Detects collisions with other players and pushes them out of the way.
-
-        Args:
-            duration: The duration for how long the movement to perform.
-        """
-        d_time = duration.total_seconds()
-
-        player_positions = np.array([p.pos for p in self.players.values()], dtype=float)
-        player_movement_vectors = np.array(
-            [
-                p.current_movement if self.env_time <= p.movement_until else [0, 0]
-                for p in self.players.values()
-            ],
-            dtype=float,
-        )
-
-        targeted_positions = player_positions + (
-            player_movement_vectors * (self.player_movement_speed * d_time)
-        )
-
-        # Collisions player between player
-        force_factor = 1.2
-        _, push_vectors = self.get_player_push(targeted_positions)
-        updated_movement = (force_factor * push_vectors) + player_movement_vectors
-        new_targeted_positions = player_positions + (
-            updated_movement * (self.player_movement_speed * d_time)
-        )
-        # same again to prevent squeezing into other players
-        _, push_vectors2 = self.get_player_push(new_targeted_positions)
-        updated_movement = (force_factor * push_vectors2) + updated_movement
-        new_targeted_positions = player_positions + (
-            updated_movement * (self.player_movement_speed * d_time)
-        )
-
-        # Check collisions with counters
-        (
-            collided,
-            relevant_axes,
-            nearest_counter_to_player,
-        ) = self.get_counter_collisions(new_targeted_positions)
-
-        # Check if sliding against counters is possible
-        for idx, player in enumerate(player_positions):
-            axis = relevant_axes[idx]
-            if collided[idx]:
-                # collide with counter left or top
-                if nearest_counter_to_player[idx][axis] > 0:
-                    updated_movement[idx, axis] = np.max(
-                        [updated_movement[idx, axis], 0]
-                    )
-                # collide with counter right or bottom
-                if nearest_counter_to_player[idx][axis] < 0:
-                    updated_movement[idx, axis] = np.min(
-                        [updated_movement[idx, axis], 0]
-                    )
-        new_positions = player_positions + (
-            updated_movement * (self.player_movement_speed * d_time)
-        )
-
-        # Check collisions with counters again, now absolute with no sliding possible
-        (
-            collided,
-            relevant_axes,
-            nearest_counter_to_player,
-        ) = self.get_counter_collisions(new_positions)
-        new_positions[collided] = player_positions[collided]
-
-        # Check player collisions a final time
-        # collided, _ = self.get_player_push(new_positions)
-        # if np.any(collided):
-        #     print(".", end="")
-
-        # Collisions player world borders
-        new_positions = np.clip(
-            new_positions,
-            self.world_borders_lower + self.player_radius,
-            self.world_borders_upper - self.player_radius,
-        )
-
-        for idx, p in enumerate(self.players.values()):
-            if not (new_positions[idx] == player_positions[idx]).all():
-                p.pos = new_positions[idx]
-
-            p.turn(player_movement_vectors[idx])
-
-            facing_distances = np.linalg.norm(
-                p.facing_point - self.counter_positions, axis=1
-            )
-            closest_counter = self.counters[facing_distances.argmin()]
-            p.current_nearest_counter = (
-                closest_counter
-                if facing_distances.min() <= self.player_interaction_range
-                else None
-            )
-            if p.last_interacted_counter != p.current_nearest_counter:
-                p.perform_interact_stop()
-
     def add_player(self, player_name: str, pos: npt.NDArray = None):
         """Add a player to the environment.
 
@@ -1042,7 +363,8 @@ 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.
+        if player_name in self.players:
+            raise ValueError(f"Player {player_name} already exists.")
         log.debug(f"Add player {player_name} to the game")
         player = Player(
             player_name,
@@ -1071,27 +393,9 @@ class Environment:
                 log.debug("No free positions left in kitchens")
             player.update_facing_point()
 
-        self.set_collision_arrays()
+        self.movement.set_collision_arrays(len(self.players))
         self.hook(PLAYER_ADDED, player_name=player_name, pos=pos)
 
-    def detect_collision_world_bounds(self, player: Player):
-        """Checks for detections of the player and the world bounds.
-
-        Args:
-            player: The player which to not let escape the world.
-
-        Returns: True if the player touches the world bounds, False if not.
-        """
-        collisions_lower = any(
-            (player.pos - (player.radius))
-            < [self.world_borders_x[0], self.world_borders_y[0]]
-        )
-        collisions_upper = any(
-            (player.pos + (player.radius))
-            > [self.world_borders_x[1], self.world_borders_y[1]]
-        )
-        return collisions_lower or collisions_upper
-
     def step(self, passed_time: timedelta):
         """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders
         and time limits.
@@ -1105,7 +409,9 @@ class Environment:
             for player in self.players.values():
                 player.progress(passed_time, self.env_time)
 
-            self.perform_movement(passed_time)
+            self.movement.perform_movement(
+                passed_time, self.env_time, self.players, self.counters
+            )
 
             for counter in self.progressing_counters:
                 counter.progress(passed_time=passed_time, now=self.env_time)
@@ -1114,33 +420,14 @@ class Environment:
                 effect_manager.progress(passed_time=passed_time, now=self.env_time)
         self.hook(POST_STEP, passed_time=passed_time)
 
-    def get_state(self):
+    def get_state(self, player_id: str = None, additional_key_values: dict = None):
         """Get the current state of the game environment. The state here is accessible by the current python objects.
 
-        Returns: Dict of lists of the current relevant game objects.
-
-        """
-        return {
-            "players": self.players,
-            "counters": self.counters,
-            "score": self.score,
-            "orders": self.order_manager.open_orders,
-            "ended": self.game_ended,
-            "env_time": self.env_time,
-            "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)),
-        }
-
-    def get_json_state(
-        self,
-        player_id: str = None,
-    ) -> str:
-        """Return the current state of the game formatted in json dict.
-
         Args:
             player_id: The player for which to get the state.
-            play_beep: Signal the GUI to play a beep when all connected players are ready to play the game.
+            additional_key_values: Additional dict that is added to the state
 
-        Returns: The state of the game formatted as a json-string
+        Returns: The state of the game as a dict.
 
         """
         if player_id in self.players:
@@ -1151,7 +438,6 @@ class Environment:
                 "kitchen": {"width": self.kitchen_width, "height": self.kitchen_height},
                 "score": self.score,
                 "orders": self.order_manager.order_state(),
-                "all_players_ready": self.all_players_ready,
                 "ended": self.game_ended,
                 "env_time": self.env_time.isoformat(),
                 "remaining_time": max(
@@ -1179,28 +465,29 @@ class Environment:
                     if msg["start_time"] < self.env_time
                     and msg["end_time"] > self.env_time
                 ],
+                **(additional_key_values if additional_key_values else {}),
             }
             self.hook(STATE_DICT, state=state, player_id=player_id)
-            json_data = json.dumps(state)
-            self.hook(JSON_STATE, json_data=json_data, player_id=player_id)
-            # assert StateRepresentation.model_validate_json(json_data=json_data)
-            return json_data
+            return state
         raise ValueError(f"No valid {player_id=}")
 
-    def get_recipe_graphs(self) -> list:
-        os.makedirs(ROOT_DIR / "generated", exist_ok=True)
+    def get_json_state(
+        self, player_id: str = None, additional_key_values: dict = None
+    ) -> str:
+        """Return the current state of the game formatted in json dict.
 
-        # time_start = time.time()
-        with ThreadPoolExecutor(
-            max_workers=len(self.order_manager.available_meals)
-        ) as executor:
-            graph_dicts = list(
-                executor.map(
-                    self.get_meal_graph, self.order_manager.available_meals.values()
-                )
-            )
-        # print("DURATION", time.time() - time_start)
-        return graph_dicts
+        Args:
+            player_id: The player for which to get the state.
+            additional_key_values: Additional dict that is added to the state
+
+        Returns: The state of the game formatted as a json-string
+
+        """
+        state = self.get_state(player_id, additional_key_values)
+        json_data = json.dumps(state)
+        self.hook(JSON_STATE, json_data=json_data, player_id=player_id)
+        # assert additional_key_values is None or StateRepresentation.model_validate_json(json_data=json_data)
+        return json_data
 
     def reset_env_time(self):
         """Reset the env time to the initial time, defined by `create_init_env_time`."""
diff --git a/cooperative_cuisine/game_server.py b/cooperative_cuisine/game_server.py
index 0497f1c99601aa1a4d864778876146c1fb3836f5..f92edefb2daa64a8596fd53ab5861aa909d89b95 100644
--- a/cooperative_cuisine/game_server.py
+++ b/cooperative_cuisine/game_server.py
@@ -29,7 +29,8 @@ from pydantic import BaseModel
 from starlette.websockets import WebSocketDisconnect
 from typing_extensions import TypedDict
 
-from cooperative_cuisine.environment import Action, Environment
+from cooperative_cuisine.action import Action
+from cooperative_cuisine.environment import Environment
 from cooperative_cuisine.server_results import (
     CreateEnvResult,
     PlayerInfo,
@@ -82,6 +83,7 @@ class EnvironmentData:
     stop_reason: str = ""
     start_time: datetime | None = None
     last_step_time: int | None = None
+    all_players_ready: bool = False
 
     # add manager_id?
 
@@ -140,7 +142,7 @@ class EnvironmentHandler:
 
         self.manager_envs[environment_config.manager_id].update([env_id])
 
-        graphs = env.get_recipe_graphs()
+        graphs = env.recipe_validation.get_recipe_graphs()
 
         res = CreateEnvResult(
             env_id=env_id, player_info=player_info, recipe_graphs=graphs
@@ -226,7 +228,7 @@ class EnvironmentHandler:
             self.envs[env_id].start_time = start_time
             self.envs[env_id].last_step_time = time.time_ns()
             self.envs[env_id].environment.reset_env_time()
-            self.envs[env_id].environment.all_players_ready = True
+            self.envs[env_id].all_players_ready = True
 
     def get_state(
         self, player_hash: str
@@ -244,10 +246,10 @@ class EnvironmentHandler:
             player_hash in self.player_data
             and self.player_data[player_hash].env_id in self.envs
         ):
-            state = self.envs[
-                self.player_data[player_hash].env_id
-            ].environment.get_json_state(
+            env_data = self.envs[self.player_data[player_hash].env_id]
+            state = env_data.environment.get_json_state(
                 self.player_data[player_hash].player_id,
+                additional_key_values={"all_players_ready": env_data.all_players_ready},
             )
             return state
         if player_hash not in self.player_data:
diff --git a/cooperative_cuisine/game_items.py b/cooperative_cuisine/items.py
similarity index 99%
rename from cooperative_cuisine/game_items.py
rename to cooperative_cuisine/items.py
index c728897de471bce2cb395743e03e1668fe537a5a..3c6bd7e8c32d96ae6966414ef0c95f683608547c 100644
--- a/cooperative_cuisine/game_items.py
+++ b/cooperative_cuisine/items.py
@@ -29,7 +29,7 @@ from enum import Enum
 from typing import Optional, TypedDict, TYPE_CHECKING
 
 if TYPE_CHECKING:
-    from cooperative_cuisine.effect_manager import EffectManager
+    from cooperative_cuisine.effects import EffectManager
 
 log = logging.getLogger(__name__)
 """The logger for this module."""
@@ -414,6 +414,7 @@ class Plate(CookingEquipment):
         return False
 
 
+# this is here, due to a circular import if it would be in the effects.py
 class Effect:
     def __init__(self, name: str, item_info: ItemInfo, uid: str = None):
         self.uuid: str = uuid.uuid4().hex if uid is None else uid
diff --git a/cooperative_cuisine/movement.py b/cooperative_cuisine/movement.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a082ed5509694eff0ba2cd9512fcdc526455f5c
--- /dev/null
+++ b/cooperative_cuisine/movement.py
@@ -0,0 +1,181 @@
+from datetime import timedelta, datetime
+
+import numpy as np
+from scipy.spatial import distance_matrix
+
+from cooperative_cuisine.counters import Counter
+from cooperative_cuisine.player import Player
+
+
+class Movement:
+    world_borders_lower = None
+    world_borders_upper = None
+
+    def __init__(self, counter_positions, player_config, world_borders):
+        self.counter_positions = counter_positions
+        self.player_radius = player_config["radius"]
+        self.player_interaction_range = player_config["interaction_range"]
+        self.player_movement_speed = player_config["speed_units_per_seconds"]
+        self.world_borders = world_borders
+        self.set_collision_arrays(1)
+
+    def set_collision_arrays(self, number_players):
+        self.world_borders_lower = self.world_borders[np.newaxis, :, 0].repeat(
+            number_players, axis=0
+        )
+        self.world_borders_upper = self.world_borders[np.newaxis, :, 1].repeat(
+            number_players, axis=0
+        )
+
+    def get_counter_collisions(self, player_positions):
+        counter_diff_vecs = (
+            player_positions[:, np.newaxis, :]
+            - self.counter_positions[np.newaxis, :, :]
+        )
+        counter_distances = np.max((np.abs(counter_diff_vecs)), axis=2)
+        closest_counter_positions = self.counter_positions[
+            np.argmin(counter_distances, axis=1)
+        ]
+        nearest_counter_to_player = player_positions - closest_counter_positions
+        relevant_axes = np.abs(nearest_counter_to_player).argmax(axis=1)
+
+        distances = np.linalg.norm(
+            np.max(
+                [
+                    np.abs(counter_diff_vecs) - 0.5,
+                    np.zeros(counter_diff_vecs.shape),
+                ],
+                axis=0,
+            ),
+            axis=2,
+        )
+
+        collided = np.any(distances < self.player_radius, axis=1)
+
+        return collided, relevant_axes, nearest_counter_to_player
+
+    def get_player_push(self, player_positions):
+        distances_players_after_scipy = distance_matrix(
+            player_positions, player_positions
+        )
+
+        player_diff_vecs = -(
+            player_positions[:, np.newaxis, :] - player_positions[np.newaxis, :, :]
+        )
+        collisions = distances_players_after_scipy < (2 * self.player_radius)
+        eye_idxs = np.eye(len(player_positions), len(player_positions), dtype=bool)
+        collisions[eye_idxs] = False
+        player_diff_vecs[collisions == False] = 0
+        push_vectors = np.sum(player_diff_vecs, axis=0)
+        collisions = np.any(collisions, axis=1)
+        return collisions, push_vectors
+
+    def perform_movement(
+        self,
+        duration: timedelta,
+        env_time: datetime,
+        players: dict[str, Player],
+        counters: list[Counter],
+    ):
+        """Moves a player in the direction specified in the action.action. If the player collides with a
+        counter or other player through this movement, then they are not moved.
+        (The extended code with the two ifs is for sliding movement at the counters, which feels a bit smoother.
+        This happens, when the player moves diagonally against the counters or world boundary.
+        This just checks if the single axis party of the movement could move the player and does so at a lower rate.)
+
+        The movement action is a unit 2d vector.
+
+        Detects collisions with other players and pushes them out of the way.
+
+        Args:
+            duration: The duration for how long the movement to perform.
+            env_time: The current time of the environment.
+            players:  The players in the environment.
+            counters:  The counters in the environment.
+        """
+        d_time = duration.total_seconds()
+
+        player_positions = np.array([p.pos for p in players.values()], dtype=float)
+        player_movement_vectors = np.array(
+            [
+                p.current_movement if env_time <= p.movement_until else [0, 0]
+                for p in players.values()
+            ],
+            dtype=float,
+        )
+
+        targeted_positions = player_positions + (
+            player_movement_vectors * (self.player_movement_speed * d_time)
+        )
+
+        # Collisions player between player
+        force_factor = 1.2
+        _, push_vectors = self.get_player_push(targeted_positions)
+        updated_movement = (force_factor * push_vectors) + player_movement_vectors
+        new_targeted_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
+        # same again to prevent squeezing into other players
+        _, push_vectors2 = self.get_player_push(new_targeted_positions)
+        updated_movement = (force_factor * push_vectors2) + updated_movement
+        new_targeted_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
+
+        # Check collisions with counters
+        (
+            collided,
+            relevant_axes,
+            nearest_counter_to_player,
+        ) = self.get_counter_collisions(new_targeted_positions)
+
+        # Check if sliding against counters is possible
+        for idx, player in enumerate(player_positions):
+            axis = relevant_axes[idx]
+            if collided[idx]:
+                # collide with counter left or top
+                if nearest_counter_to_player[idx][axis] > 0:
+                    updated_movement[idx, axis] = np.max(
+                        [updated_movement[idx, axis], 0]
+                    )
+                # collide with counter right or bottom
+                if nearest_counter_to_player[idx][axis] < 0:
+                    updated_movement[idx, axis] = np.min(
+                        [updated_movement[idx, axis], 0]
+                    )
+        new_positions = player_positions + (
+            updated_movement * (self.player_movement_speed * d_time)
+        )
+
+        # Check collisions with counters again, now absolute with no sliding possible
+        (
+            collided,
+            relevant_axes,
+            nearest_counter_to_player,
+        ) = self.get_counter_collisions(new_positions)
+        new_positions[collided] = player_positions[collided]
+
+        # Collisions player world borders
+        new_positions = np.clip(
+            new_positions,
+            self.world_borders_lower + self.player_radius,
+            self.world_borders_upper - self.player_radius,
+        )
+
+        for idx, p in enumerate(players.values()):
+            if not (new_positions[idx] == player_positions[idx]).all():
+                p.pos = new_positions[idx]
+
+            p.turn(player_movement_vectors[idx])
+
+            facing_distances = np.linalg.norm(
+                p.facing_point - self.counter_positions, axis=1
+            )
+            closest_counter = counters[facing_distances.argmin()]
+            p.current_nearest_counter = (
+                closest_counter
+                if facing_distances.min() <= self.player_interaction_range
+                else None
+            )
+            if p.last_interacted_counter != p.current_nearest_counter:
+                p.perform_interact_stop()
diff --git a/cooperative_cuisine/orders.py b/cooperative_cuisine/orders.py
index a5b48ec743e6b70980ea02f706b0bb5946c8d858..0bb26170726813b31d6e0a2becfec3880fb61cb2 100644
--- a/cooperative_cuisine/orders.py
+++ b/cooperative_cuisine/orders.py
@@ -44,7 +44,6 @@ from datetime import datetime, timedelta
 from random import Random
 from typing import Callable, Tuple, Any, Deque, TypedDict, Type
 
-from cooperative_cuisine.game_items import Item, Plate, ItemInfo
 from cooperative_cuisine.hooks import (
     Hooks,
     SERVE_NOT_ORDERED_MEAL,
@@ -55,6 +54,7 @@ from cooperative_cuisine.hooks import (
     ORDER_DURATION_SAMPLE,
     ORDER_EXPIRED,
 )
+from cooperative_cuisine.items import Item, Plate, ItemInfo
 
 log = logging.getLogger(__name__)
 """The logger for this module."""
diff --git a/cooperative_cuisine/player.py b/cooperative_cuisine/player.py
index b85c76661433e2f899fba4ce93bde25df1883d16..f4e69c3aa66310f893b854944566a9826bf070f0 100644
--- a/cooperative_cuisine/player.py
+++ b/cooperative_cuisine/player.py
@@ -16,7 +16,7 @@ import numpy as np
 import numpy.typing as npt
 
 from cooperative_cuisine.counters import Counter
-from cooperative_cuisine.game_items import Item, ItemType
+from cooperative_cuisine.items import Item, ItemType
 from cooperative_cuisine.state_representation import PlayerState
 
 log = logging.getLogger(__name__)
@@ -29,7 +29,7 @@ class PlayerConfig:
 
     radius: float = 0.4
     """The size of the player. The size of a counter is 1"""
-    player_speed_units_per_seconds: float | int = 8
+    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."""
diff --git a/cooperative_cuisine/pygame_2d_vis/drawing.py b/cooperative_cuisine/pygame_2d_vis/drawing.py
index 0031ca7204136918ac36c94da8bd1bb334cefc5d..49aeb6a98be9833ee982f6ff1a2b62d79e4c65e5 100644
--- a/cooperative_cuisine/pygame_2d_vis/drawing.py
+++ b/cooperative_cuisine/pygame_2d_vis/drawing.py
@@ -990,7 +990,7 @@ def generate_recipe_images(config: dict, folder_path: str | Path):
     pygame.init()
     pygame.font.init()
 
-    graph_dicts = env.get_recipe_graphs()
+    graph_dicts = env.recipe_validation.get_recipe_graphs()
     for graph_dict in graph_dicts:
         width = 700
         height = 400
diff --git a/cooperative_cuisine/pygame_2d_vis/gui.py b/cooperative_cuisine/pygame_2d_vis/gui.py
index bb5d31e63646113414601f7232aee25ceb625a7a..d6008f504681e60a48bdc0c02e6ea85f4af747e8 100644
--- a/cooperative_cuisine/pygame_2d_vis/gui.py
+++ b/cooperative_cuisine/pygame_2d_vis/gui.py
@@ -20,11 +20,7 @@ from pygame import mixer
 from websockets.sync.client import connect
 
 from cooperative_cuisine import ROOT_DIR
-from cooperative_cuisine.environment import (
-    Action,
-    ActionType,
-    InterActionData,
-)
+from cooperative_cuisine.action import ActionType, InterActionData, Action
 from cooperative_cuisine.game_server import CreateEnvironmentConfig
 from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
 from cooperative_cuisine.pygame_2d_vis.game_colors import colors
diff --git a/cooperative_cuisine/pygame_2d_vis/video_replay.py b/cooperative_cuisine/pygame_2d_vis/video_replay.py
index 1c6c348a68e430c6488ecb794ee9536c973f49e1..acb08cbaefafc6ad20e855f28eb6f7d274318956 100644
--- a/cooperative_cuisine/pygame_2d_vis/video_replay.py
+++ b/cooperative_cuisine/pygame_2d_vis/video_replay.py
@@ -43,7 +43,8 @@ from PIL import Image
 from tqdm import tqdm
 
 from cooperative_cuisine import ROOT_DIR
-from cooperative_cuisine.environment import Environment, Action
+from cooperative_cuisine.action import Action
+from cooperative_cuisine.environment import Environment
 from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
 from cooperative_cuisine.recording import FileRecorder
 
diff --git a/cooperative_cuisine/recording.py b/cooperative_cuisine/recording.py
index 79f98fb9d845edf4457eb9fcd17b413286ff433e..01f0de86833e7f81290fb70438e3c98ec36f9775 100644
--- a/cooperative_cuisine/recording.py
+++ b/cooperative_cuisine/recording.py
@@ -46,12 +46,9 @@ import os
 import traceback
 from pathlib import Path
 
-import platformdirs
-
-from cooperative_cuisine import ROOT_DIR
 from cooperative_cuisine.environment import Environment
 from cooperative_cuisine.hooks import HookCallbackClass
-from cooperative_cuisine.utils import NumpyAndDataclassEncoder
+from cooperative_cuisine.utils import NumpyAndDataclassEncoder, expand_path
 
 log = logging.getLogger(__name__)
 
@@ -80,18 +77,8 @@ class FileRecorder(HookCallbackClass):
     ):
         super().__init__(name, env, **kwargs)
         self.add_hook_ref = add_hook_ref
-        log_path = log_path.replace("ENV_NAME", env.env_name).replace(
-            "LOG_RECORD_NAME", name
-        )
-        if log_path.startswith("USER_LOG_DIR/"):
-            log_path = (
-                Path(platformdirs.user_log_dir("cooperative_cuisine"))
-                / log_path[len("USER_LOG_DIR/") :]
-            )
-        elif log_path.startswith("ROOT_DIR/"):
-            log_path = ROOT_DIR / log_path[len("ROOT_DIR/") :]
-        else:
-            log_path = Path(log_path)
+        log_path = log_path.replace("LOG_RECORD_NAME", name)
+        log_path = Path(expand_path(log_path, env_name=env.env_name))
         self.log_path = log_path
         log.info(f"Recorder record for {name} in file://{log_path}")
         os.makedirs(log_path.parent, exist_ok=True)
diff --git a/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml b/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml
index 5e75d9bdaea71f52507fa31d3fb1ed47b268bf84..60ea66aa61340bdcd9d7818096de38736e435f89 100644
--- a/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml
+++ b/cooperative_cuisine/reinforcement_learning/environment_config_rl.yaml
@@ -82,14 +82,14 @@ orders:
 
 player_config:
   radius: 0.4
-  player_speed_units_per_seconds: 1
+  speed_units_per_seconds: 1
   interaction_range: 1.6
   restricted_view: False
   view_angle: 95
 
 effect_manager: { }
 #  FireManager:
-#    class: !!python/name:cooperative_cuisine.effect_manager.FireEffectManager ''
+#    class: !!python/name:cooperative_cuisine.effects.FireEffectManager ''
 #    kwargs:
 #      spreading_duration: [ 5, 10 ]
 #      fire_burns_ingredients_and_meals: true
diff --git a/cooperative_cuisine/reinforcement_learning/gym_env.py b/cooperative_cuisine/reinforcement_learning/gym_env.py
index 7bacc68c42bafd29257976a351307ea4c268cc72..465701fe70ee1771f07e1fe1636744cef7864e0e 100644
--- a/cooperative_cuisine/reinforcement_learning/gym_env.py
+++ b/cooperative_cuisine/reinforcement_learning/gym_env.py
@@ -21,14 +21,12 @@ from stable_baselines3.common.vec_env import VecVideoRecorder
 from wandb.integration.sb3 import WandbCallback
 
 from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.action import ActionType, InterActionData, Action
 from cooperative_cuisine.counters import Counter, CookingCounter, Dispenser
 from cooperative_cuisine.environment import (
     Environment,
-    Action,
-    ActionType,
-    InterActionData,
 )
-from cooperative_cuisine.game_items import CookingEquipment
+from cooperative_cuisine.items import CookingEquipment
 from cooperative_cuisine.pygame_2d_vis.drawing import Visualizer
 
 
@@ -105,14 +103,18 @@ with open(ROOT_DIR / "pygame_2d_vis" / "visualization.yaml", "r") as file:
 
 def shuffle_counters(env):
     sample_counter = []
+    other_counters = []
     for counter in env.counters:
         if counter.__class__ != Counter:
             sample_counter.append(counter)
+        else:
+            other_counters.append()
     new_counter_pos = [c.pos for c in sample_counter]
     random.shuffle(new_counter_pos)
     for counter, new_pos in zip(sample_counter, new_counter_pos):
         counter.pos = new_pos
-    env.counter_positions = np.array([c.pos for c in env.counters])
+    sample_counter.extend(other_counters)
+    env.overwrite_counters(sample_counter)
 
 
 class EnvGymWrapper(Env):
diff --git a/cooperative_cuisine/state_representation.py b/cooperative_cuisine/state_representation.py
index 4479c39216537278e8180166f21090ae541bc690..cfa023f82f88bc153c90ef44dec20374e61a6dcb 100644
--- a/cooperative_cuisine/state_representation.py
+++ b/cooperative_cuisine/state_representation.py
@@ -96,13 +96,14 @@ class StateRepresentation(BaseModel):
     kitchen: KitchenInfo
     score: float | int
     orders: list[OrderState]
-    all_players_ready: bool
     ended: bool
     env_time: datetime  # isoformat str
     remaining_time: float
     view_restrictions: None | list[ViewRestriction]
     served_meals: list[tuple[str, str]]
     info_msg: list[tuple[str, str]]
+    # is added:
+    # all_players_ready: bool
 
 
 def create_json_schema():
diff --git a/cooperative_cuisine/study_server.py b/cooperative_cuisine/study_server.py
index 633865f19e23149a25e3ef82df15a65ec4567474..8c2ce335525007daa44146243bc4206c6a90d7f8 100644
--- a/cooperative_cuisine/study_server.py
+++ b/cooperative_cuisine/study_server.py
@@ -35,6 +35,8 @@ from cooperative_cuisine.server_results import PlayerInfo
 from cooperative_cuisine.utils import (
     url_and_port_arguments,
     add_list_of_manager_ids_arguments,
+    expand_path,
+    add_study_arguments,
 )
 
 NUMBER_PLAYER_PER_ENV = 2
@@ -93,11 +95,14 @@ class StudyState:
         self.next_level_env = None
         self.players_done = {}
 
-        self.USE_AAAMBOS_AGENT = False
+        self.use_aaambos_agent = False
 
         self.websocket_url = f"ws://{game_url}:{game_port}/ws/player/"
         self.sub_processes = []
 
+        self.current_item_info = None
+        self.current_config = None
+
     @property
     def study_done(self):
         return self.current_level_idx >= len(self.levels)
@@ -120,14 +125,18 @@ class StudyState:
         return filled and not self.is_full
 
     def create_env(self, level):
-        with open(ROOT_DIR / "configs" / level["item_info_path"], "r") as file:
+        item_info_path = expand_path(level["item_info_path"])
+        layout_path = expand_path(level["layout_path"])
+        config_path = expand_path(level["config_path"])
+
+        with open(item_info_path, "r") as file:
             item_info = file.read()
             self.current_item_info: EnvironmentConfig = yaml.load(
                 item_info, Loader=yaml.Loader
             )
-        with open(ROOT_DIR / "configs" / "layouts" / level["layout_path"], "r") as file:
+        with open(layout_path, "r") as file:
             layout = file.read()
-        with open(ROOT_DIR / "configs" / level["config_path"], "r") as file:
+        with open(config_path, "r") as file:
             environment_config = file.read()
             self.current_config: EnvironmentConfig = yaml.load(
                 environment_config, Loader=yaml.Loader
@@ -158,7 +167,7 @@ class StudyState:
                 self.create_and_connect_bot(player_id, player_info)
         return env_info
 
-    def start(self):
+    def start_level(self):
         level = self.levels[self.current_level_idx]
         self.current_running_env = self.create_env(level)
 
@@ -174,8 +183,7 @@ class StudyState:
 
         self.current_level_idx += 1
         if not self.study_done:
-            level = self.levels[self.current_level_idx]
-            self.current_running_env = self.create_env(level)
+            self.start_level()
             for (
                 participant_id,
                 player_info,
@@ -203,8 +211,7 @@ class StudyState:
 
     def player_finished_level(self, participant_id):
         self.players_done[participant_id] = True
-        level_done = all(self.players_done.values())
-        if level_done:
+        if all(self.players_done.values()):
             self.next_level()
 
     def get_connection(
@@ -232,7 +239,7 @@ class StudyState:
         print(
             f'--general_plus="agent_websocket:{self.websocket_url + player_info["client_id"]};player_hash:{player_hash};agent_id:{player_id}"'
         )
-        if self.USE_AAAMBOS_AGENT:
+        if self.use_aaambos_agent:
             sub = Popen(
                 " ".join(
                     [
@@ -267,7 +274,7 @@ class StudyState:
     def kill_bots(self):
         for sub in self.sub_processes:
             try:
-                if self.USE_AAAMBOS_AGENT:
+                if self.use_aaambos_agent:
                     pgrp = os.getpgid(sub.pid)
                     os.killpg(pgrp, signal.SIGINT)
                     subprocess.run(
@@ -280,8 +287,6 @@ class StudyState:
                 pass
 
         self.sub_processes = []
-        for websocket in self.websockets.values():
-            websocket.close()
 
     def __repr__(self):
         return f"Study({self.current_running_env['env_id']})"
@@ -304,13 +309,15 @@ class StudyManager:
             str, Tuple[int, dict[str, PlayerInfo], list[str]]
         ] = {}
 
+        self.study_config_path = ROOT_DIR / "configs" / "study" / "study_config.yml"
+
     def create_study(self):
         study = StudyState(
-            ROOT_DIR / "configs" / "study" / "study_config.yaml",
+            self.study_config_path,
             self.game_host,
             self.game_port,
         )
-        study.start()
+        study.start_level()
         self.running_studies.append(study)
 
     def add_participant(self, participant_id: str, number_players: int):
@@ -352,6 +359,10 @@ class StudyManager:
     def set_manager_id(self, manager_id: str):
         self.server_manager_id = manager_id
 
+    def set_study_config(self, study_config_path: str):
+        # TODO validate study_config?
+        self.study_config_path = study_config_path
+
 
 study_manager = StudyManager()
 
@@ -435,9 +446,10 @@ async def disconnect_from_tutorial(participant_id: str):
         return "Not Ok"
 
 
-def main(study_host, study_port, game_host, game_port, manager_ids):
+def main(study_host, study_port, game_host, game_port, manager_ids, study_config_path):
     study_manager.set_game_server_url(game_host=game_host, game_port=game_port)
     study_manager.set_manager_id(manager_id=manager_ids[0])
+    study_manager.set_study_config(study_config_path=study_config_path)
 
     print(
         f"Use {study_manager.server_manager_id=} for game_server_url=http://{game_host}:{game_port}"
@@ -461,6 +473,7 @@ if __name__ == "__main__":
         default_game_port=8000,
     )
     add_list_of_manager_ids_arguments(parser=parser)
+    add_study_arguments(parser=parser)
     args = parser.parse_args()
 
     game_server_url = f"https://{args.game_url}:{args.game_port}"
@@ -470,4 +483,5 @@ if __name__ == "__main__":
         game_host=args.game_url,
         game_port=args.game_port,
         manager_ids=args.manager_ids,
+        study_config_path=args.study_config,
     )
diff --git a/cooperative_cuisine/utils.py b/cooperative_cuisine/utils.py
index 2d0a7dc38472667f22e9673cddbbe4d8f29e7fe8..d6e0bfdb341d1686cc99a2fb94d8b04e6952745a 100644
--- a/cooperative_cuisine/utils.py
+++ b/cooperative_cuisine/utils.py
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING
 
 import numpy as np
 import numpy.typing as npt
+import platformdirs
 from scipy.spatial import distance_matrix
 
 from cooperative_cuisine import ROOT_DIR
@@ -27,6 +28,17 @@ from cooperative_cuisine.player import Player
 DEFAULT_SERVER_URL = "localhost"
 
 
+def expand_path(path: str, env_name: str = "") -> str:
+    return os.path.expanduser(
+        path.replace("ROOT_DIR", str(ROOT_DIR))
+        .replace("ENV_NAME", env_name)
+        .replace("USER_LOG_DIR", platformdirs.user_log_dir("cooperative_cuisine"))
+        .replace("LAYOUTS_DIR", str(ROOT_DIR / "configs" / "layouts"))
+        .replace("STUDY_DIR", str(ROOT_DIR / "configs" / "study"))
+        .replace("CONFIGS_DIR", str(ROOT_DIR / "configs"))
+    )
+
+
 @dataclasses.dataclass
 class VectorStateGenerationData:
     grid_base_array: npt.NDArray[npt.NDArray[float]]
@@ -180,7 +192,7 @@ def url_and_port_arguments(
     parser, server_name="game server", default_study_port=8080, default_game_port=8000
 ):
     parser.add_argument(
-        "-study-url",
+        "-s",
         "--study-url",
         "--study-host",
         type=str,
@@ -195,7 +207,7 @@ def url_and_port_arguments(
         help=f"Port number for the {server_name}",
     )
     parser.add_argument(
-        "-game-url",
+        "-g",
         "--game-url",
         "--game-host",
         type=str,
@@ -228,6 +240,15 @@ def add_list_of_manager_ids_arguments(parser):
     )
 
 
+def add_study_arguments(parser):
+    parser.add_argument(
+        "--study-config",
+        type=str,
+        default=ROOT_DIR / "configs" / "study" / "study_config.yaml",
+        help="The config of the study.",
+    )
+
+
 class NumpyAndDataclassEncoder(json.JSONEncoder):
     """Special json encoder for numpy types"""
 
diff --git a/cooperative_cuisine/validation.py b/cooperative_cuisine/validation.py
new file mode 100644
index 0000000000000000000000000000000000000000..f4e8420435b89b03b3df85e3a6bc5a2b36a2d216
--- /dev/null
+++ b/cooperative_cuisine/validation.py
@@ -0,0 +1,280 @@
+import os
+import warnings
+from concurrent.futures import ThreadPoolExecutor
+from typing import TypedDict, Tuple, Iterator
+
+import networkx as nx
+from networkx import DiGraph
+
+from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.counters import (
+    Dispenser,
+    CuttingBoard,
+    CookingCounter,
+    PlateDispenser,
+    Counter,
+)
+from cooperative_cuisine.items import ItemInfo, ItemType, Item
+from cooperative_cuisine.orders import OrderManager
+
+
+class MealGraphDict(TypedDict):
+    meal: str
+    edges: list[Tuple[str, str]]
+    layout: dict[str, Tuple[float, float]]
+
+
+class Validation:
+    def __init__(self, meals, item_info, order_manager):
+        self.meals: list[ItemInfo] = meals
+        self.item_info: dict[str, ItemInfo] = item_info
+        self.order_manager: OrderManager = order_manager
+
+    @staticmethod
+    def infer_recipe_graph(item_info) -> DiGraph:
+        colors = {
+            ItemType.Ingredient: "black",
+            ItemType.Equipment: "red",
+            ItemType.Meal: "green",
+            ItemType.Waste: "brown",
+        }
+
+        graph = DiGraph(directed=True)
+        for item_name, item_info in item_info.items():
+            graph.add_node(item_name, color=colors.get(item_info.type, "blue"))
+            if item_info.equipment is None:
+                for item in item_info.needs:
+                    graph.add_edge(item, item_name)
+            else:
+                if len(item_info.needs) > 0:
+                    for item in item_info.needs:
+                        graph.add_edge(item, item_info.equipment.name)
+                        graph.add_edge(item_info.equipment.name, item_name)
+                else:
+                    graph.add_edge(item_name, item_info.equipment.name)
+        return graph
+
+    def get_meal_graph(self, meal: ItemInfo) -> MealGraphDict:
+        graph = DiGraph(directed=True, rankdir="LR")
+
+        root = f"{meal.name}_0"
+
+        graph.add_node(root)
+        add_queue = [root]  # Add "Plate_0" if dishwashing should be part of the recipe
+
+        start = True
+        while add_queue:
+            current = add_queue.pop()
+
+            current_info = self.item_info[current.split("_")[0]]
+            current_index = current.split("_")[-1]
+
+            if start:
+                graph.add_edge("Plate_0", current)
+                current = "Plate_0"
+                start = False
+
+            if current_info.needs:
+                if len(current_info.needs) == 1:
+                    need = f"{current_info.needs[0]}_{current_index}"
+                    add_queue.append(need)
+
+                    if current_info.equipment:
+                        equip_id = f"{current_info.equipment.name}_{current_index}"
+                        if current_info.equipment.equipment:
+                            equip_equip_id = f"{current_info.equipment.equipment.name}_{current_index}"
+                            graph.add_edge(equip_equip_id, current)
+                            graph.add_edge(equip_id, equip_equip_id)
+                            graph.add_edge(need, equip_id)
+                        else:
+                            graph.add_edge(equip_id, current)
+                            graph.add_edge(need, equip_id)
+                    else:
+                        graph.add_edge(need, current)
+
+                elif len(current_info.needs) > 1:
+                    for idx, item_name in enumerate(current_info.needs):
+                        add_queue.append(f"{item_name}_{idx}")
+
+                        if current_info.equipment and current_info.equipment.equipment:
+                            equip_id = f"{current_info.equipment.name}_{current_index}"
+                            equip_equip_id = f"{current_info.equipment.equipment.name}_{current_index}"
+                            graph.add_edge(equip_equip_id, current)
+                            graph.add_edge(equip_id, equip_equip_id)
+                            graph.add_edge(f"{item_name}_{idx}", equip_id)
+                        else:
+                            graph.add_edge(
+                                f"{item_name}_{idx}",
+                                current,
+                            )
+
+        return {
+            "meal": meal.name,
+            "edges": list(graph.edges),
+            "layout": nx.nx_agraph.graphviz_layout(graph, prog="dot"),
+        }
+
+    def reduce_item_node(self, graph, base_ingredients, item, visited):
+        visited.append(item)
+        if item in base_ingredients:
+            return True
+        else:
+            return all(
+                self.reduce_item_node(graph, base_ingredients, pred, visited)
+                for pred in graph.predecessors(item)
+                if pred not in visited
+            )
+
+    def assert_equipment_is_present(self, counters):
+        # TODO until now not called
+        expected = set(
+            name
+            for name, info in self.item_info.items()
+            if info.type == ItemType.Equipment and "Plate" not in info.name
+        )
+        counters = set(c.__class__.__name__ for c in counters).union(
+            set(c.name for c in counters if hasattr(c, "name"))
+        )
+        items = set(
+            c.occupied_by.name
+            for c in counters
+            if c.occupied_by is not None and isinstance(c.occupied_by, Item)
+        )
+        for equipment in expected:
+            if equipment not in counters and equipment not in items:
+                raise ValueError(
+                    f"Equipment '{equipment}' from config files not found in the environment layout.\n"
+                    f"Config Equipment: {sorted(expected)}\n"
+                    f"Layout Counters: {sorted(counters)}\n"
+                    f"Layout Items: {sorted(items)}"
+                )
+
+    def assert_plate_cycle_present(self, counters: list[Counter]):
+        # TODO until now not called
+        for plate in ["Plate", "DirtyPlate"]:
+            if plate not in self.item_info:
+                raise ValueError(f"{plate} not found in item info")
+
+        relevant_counters = ["PlateDispenser", "ServingWindow"]
+        for counter in counters:
+            if isinstance(counter, PlateDispenser):
+                if counter.plate_config.return_dirty:
+                    relevant_counters = [
+                        "PlateDispenser",
+                        "ServingWindow",
+                        "Sink",
+                        "SinkAddon",
+                    ]
+
+        counter_names = [c.__class__.__name__ for c in counters]
+        for counter in relevant_counters:
+            if counter not in counter_names:
+                raise ValueError(f"{counter} not found in counters")
+
+    @staticmethod
+    def assert_no_orphans(graph: DiGraph):
+        # TODO until now not called
+        orphans = [
+            n
+            for n in graph.nodes()
+            if graph.in_degree(n) == 0 and graph.out_degree(n) == 0
+        ]
+        if orphans:
+            raise ValueError(
+                f"Expected all items to be part of a recipe, but found orphans: {orphans}"
+            )
+
+    @staticmethod
+    def assert_roots_are_dispensable(graph, base_ingredients):
+        root_nodes = [
+            n for n in graph.nodes() if graph.in_degree(n) == 0 and "Plate" not in n
+        ]
+        if set(root_nodes) != set(base_ingredients):
+            raise ValueError(
+                f"Expected root nodes in the recipe graph and dispensable items to be identical, but found\n "
+                f"Root nodes: {sorted(root_nodes)}\n"
+                f"Dispensable items: {sorted(base_ingredients)}"
+            )
+
+    def assert_meals_are_reducible(self, graph, base_ingredients):
+        meals = [n for n in graph.nodes() if self.item_info[n].type == ItemType.Meal]
+
+        for meal in meals:
+            visited = []
+            if not self.reduce_item_node(graph, base_ingredients, meal, visited):
+                raise ValueError(
+                    f"Meal '{meal}' can not be reduced to base ingredients"
+                )
+
+    def get_requirements(self, item_name: str) -> Iterator[str]:
+        """
+        Get all base ingredients and equipment required to create the given meal.
+        """
+        item = self.item_info[item_name]
+        is_equipment = item.type == ItemType.Equipment
+        is_base_ingredient = item.type == ItemType.Ingredient and not item.needs
+
+        if is_equipment or is_base_ingredient:
+            yield item_name
+        for need in item.needs:
+            yield from self.get_requirements(need)
+        if item.equipment is not None:
+            yield from self.get_requirements(item.equipment.name)
+
+    def get_item_info_requirements(self) -> dict[str, set[str]]:
+        recipes = {}
+        for item_name, item_info in self.item_info.items():
+            if item_info.type == ItemType.Meal:
+                requirements = set(r for r in self.get_requirements(item_name))
+                recipes[item_name] = requirements | {"Plate"}
+        return recipes
+
+    def get_layout_requirements(self, counters: list[Counter]):
+        layout_requirements = set()
+        for counter in counters:
+            if isinstance(counter, (Dispenser, PlateDispenser)):
+                layout_requirements.add(counter.dispensing.name)
+            if isinstance(counter, CuttingBoard):
+                layout_requirements.add("CuttingBoard")
+            if isinstance(counter, CookingCounter):
+                layout_requirements.add(counter.name)
+            if counter.occupied_by is not None and hasattr(counter.occupied_by, "name"):
+                layout_requirements.add(counter.occupied_by.name)
+        return layout_requirements
+
+    def validate_environment(self, counters: list[Counter]):
+        graph = self.infer_recipe_graph(self.item_info)
+        os.makedirs(ROOT_DIR / "generated", exist_ok=True)
+        nx.nx_agraph.to_agraph(graph).draw(
+            ROOT_DIR / "generated" / "recipe_graph.png", format="png", prog="dot"
+        )
+
+        expected = self.get_item_info_requirements()
+        present = self.get_layout_requirements(counters)
+        possible_meals = set(meal for meal in expected if expected[meal] <= present)
+        defined_meals = set(map(lambda i: i.name, self.meals))
+
+        # print(f"Ordered meals: {defined_meals}, Possible meals: {possible_meals}")
+        if len(defined_meals - possible_meals) > 0:
+            warnings.warn(
+                f"Ordered meals are not possible: {defined_meals - possible_meals}"
+            )
+
+        meals_to_be_ordered = possible_meals.intersection(defined_meals)
+        return meals_to_be_ordered
+        # print("FINAL MEALS:", meals_to_be_ordered)
+
+    def get_recipe_graphs(self) -> list[MealGraphDict]:
+        os.makedirs(ROOT_DIR / "generated", exist_ok=True)
+
+        # time_start = time.time()
+        with ThreadPoolExecutor(
+            max_workers=len(self.order_manager.available_meals)
+        ) as executor:
+            graph_dicts = list(
+                executor.map(
+                    self.get_meal_graph, self.order_manager.available_meals.values()
+                )
+            )
+        # print("DURATION", time.time() - time_start)
+        return graph_dicts
diff --git a/tests/test_cooking_equipment.py b/tests/test_cooking_equipment.py
index 5bc9fa3bb4dc16a8464d7a2284d2cdbd9b0a67a5..03f936f1a1625b318e46905668009ed8fce659db 100644
--- a/tests/test_cooking_equipment.py
+++ b/tests/test_cooking_equipment.py
@@ -1,6 +1,6 @@
 import pytest
 
-from cooperative_cuisine.game_items import ItemInfo, CookingEquipment, Item, ItemType
+from cooperative_cuisine.items import ItemInfo, CookingEquipment, Item, ItemType
 
 
 def test_can_combine_single_other_item():
diff --git a/tests/test_counter.py b/tests/test_counter.py
index b0fb93a9bdd3f63d4d98f0377da8e9060fe3c88d..d6860acb3e00eb02a6cfc8703bf3bb9fd8e10f7b 100644
--- a/tests/test_counter.py
+++ b/tests/test_counter.py
@@ -1,8 +1,8 @@
 import numpy as np
 
 from cooperative_cuisine.counters import ServingWindow, Dispenser
-from cooperative_cuisine.game_items import Item, Plate, ItemInfo, ItemType
 from cooperative_cuisine.hooks import Hooks
+from cooperative_cuisine.items import Item, Plate, ItemInfo, ItemType
 from cooperative_cuisine.utils import create_init_env_time
 
 
diff --git a/tests/test_item.py b/tests/test_item.py
index 9b3cdb64deb08be2331a1570198dd14c6577b372..5a72924d2c05898adc18be0dc6585435f04471bf 100644
--- a/tests/test_item.py
+++ b/tests/test_item.py
@@ -1,6 +1,6 @@
 import pytest
 
-from cooperative_cuisine.game_items import ItemInfo, Item, ItemType
+from cooperative_cuisine.items import ItemInfo, Item, ItemType
 
 
 @pytest.fixture
diff --git a/tests/test_start.py b/tests/test_start.py
index fd7e3addee6018e26691c138746a6aa6813f9806..ae0121ec0daefb6cbff0bb13292cabc8e453dfc1 100644
--- a/tests/test_start.py
+++ b/tests/test_start.py
@@ -4,16 +4,14 @@ import numpy as np
 import pytest
 
 from cooperative_cuisine import ROOT_DIR
+from cooperative_cuisine.action import ActionType, InterActionData, Action
 from cooperative_cuisine.counters import Counter, CuttingBoard
 from cooperative_cuisine.environment import (
-    Action,
     Environment,
-    ActionType,
-    InterActionData,
 )
-from cooperative_cuisine.game_items import Item, ItemInfo, ItemType
 from cooperative_cuisine.game_server import PlayerRequestType
 from cooperative_cuisine.hooks import Hooks
+from cooperative_cuisine.items import Item, ItemInfo, ItemType
 from cooperative_cuisine.server_results import (
     PlayerInfo,
     CreateEnvResult,
@@ -82,8 +80,8 @@ def test_player_registration(env_config, layout_config, item_info):
     env.add_player("2")
     assert len(env.players) == 2, "Wrong number of players"
 
-    env.add_player("2")
-    assert len(env.players) == 2, "Wrong number of players"
+    with pytest.raises(ValueError):
+        env.add_player("2")
 
 
 def test_movement(env_config, layout_empty_config, item_info):
@@ -91,7 +89,7 @@ def test_movement(env_config, layout_empty_config, item_info):
     player_name = "1"
     start_pos = np.array([3, 4])
     env.add_player(player_name, start_pos)
-    env.player_movement_speed = 1
+    env.movement.player_movement_speed = 1
     move_direction = np.array([1, 0])
     move_action = Action(player_name, ActionType.MOVEMENT, move_direction, duration=0.1)
     do_moves_number = 3
@@ -100,7 +98,7 @@ def test_movement(env_config, layout_empty_config, item_info):
         env.step(timedelta(seconds=0.1))
 
     expected = start_pos + do_moves_number * (
-        move_direction * env.player_movement_speed * move_action.duration
+        move_direction * env.movement.player_movement_speed * move_action.duration
     )
     assert np.isclose(
         np.linalg.norm(expected - env.players[player_name].pos), 0
@@ -112,7 +110,7 @@ def test_player_movement_speed(env_config, layout_empty_config, item_info):
     player_name = "1"
     start_pos = np.array([3, 4])
     env.add_player(player_name, start_pos)
-    env.player_movement_speed = 2
+    env.movement.player_movement_speed = 2
     move_direction = np.array([1, 0])
     move_action = Action(player_name, ActionType.MOVEMENT, move_direction, duration=0.1)
     do_moves_number = 3
@@ -121,7 +119,7 @@ def test_player_movement_speed(env_config, layout_empty_config, item_info):
         env.step(timedelta(seconds=0.1))
 
     expected = start_pos + do_moves_number * (
-        move_direction * env.player_movement_speed * move_action.duration
+        move_direction * env.movement.player_movement_speed * move_action.duration
     )
 
     assert np.isclose(
@@ -140,7 +138,7 @@ def test_player_reach(env_config, layout_empty_config, item_info):
     counter = Counter(pos=counter_pos, hook=Hooks(env))
     env.overwrite_counters([counter])
     env.add_player("1", np.array([2, 4]))
-    env.player_movement_speed = 1
+    env.movement.player_movement_speed = 1
     player = env.players["1"]
     assert not player.can_reach(counter), "Player is too far away."
 
@@ -162,7 +160,7 @@ def test_pickup(env_config, layout_config, item_info):
 
     env.add_player("1", np.array([2, 3]))
     player = env.players["1"]
-    env.player_movement_speed = 1
+    env.movement.player_movement_speed = 1
 
     move_down = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)
     move_up = Action("1", ActionType.MOVEMENT, np.array([0, 1]), duration=1)
@@ -224,7 +222,7 @@ def test_processing(env_config, layout_config, item_info):
     tomato = Item(name="Tomato", item_info=None)
     env.add_player("1", np.array([2, 3]))
     player = env.players["1"]
-    env.player_movement_speed = 1
+    env.movement.player_movement_speed = 1
     player.holding = tomato
 
     move = Action("1", ActionType.MOVEMENT, np.array([0, -1]), duration=1)