diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..7906f53
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+global-include *.txt *.png *.jpg
\ No newline at end of file
diff --git a/README.md b/README.md
index 6b16c79..9b29f31 100644
--- a/README.md
+++ b/README.md
@@ -3,14 +3,11 @@
 Tackling emergent dysfunctions (EDYs) in cooperation with Fraunhofer-IKS
 
 ## Setup
-1. Make sure to install `virtualenv` using `pip install virtualenv`  
-2. Create a new virtual environment `virtualenv venv`
-3. Activate the virtual environment `source venv/bin/activate`
-4. Install the required dependencies `pip install -r requirements.txt`
-## 
+Just install this environment by `pip install marl-factory-grid`.
 
 ## First Steps
 
+
 ### Quickstart
 Most of the env. objects (entites, rules and assets) can be loaded automatically. 
 Just define what your environment needs in a *yaml*-configfile like:
diff --git a/algorithms/static/TSP_base_agent.py b/algorithms/static/TSP_base_agent.py
index 0bdd8c4..eabd99d 100644
--- a/algorithms/static/TSP_base_agent.py
+++ b/algorithms/static/TSP_base_agent.py
@@ -1,10 +1,11 @@
+import itertools
 from random import choice
 
 import numpy as np
 
+import networkx as nx
 from networkx.algorithms.approximation import traveling_salesman as tsp
 
-from environment.utils.helpers import points_to_graph
 
 from modules.doors import constants as do
 from environment import constants as c
@@ -15,6 +16,41 @@ from abc import abstractmethod, ABC
 future_planning = 7
 
 
+def points_to_graph(coordiniates_or_tiles, allow_euclidean_connections=True, allow_manhattan_connections=True):
+    """
+    Given a set of coordinates, this function contructs a non-directed graph, by conncting adjected points.
+    There are three combinations of settings:
+        Allow all neigbors:     Distance(a, b) <= sqrt(2)
+        Allow only manhattan:   Distance(a, b) == 1
+        Allow only euclidean:   Distance(a, b) == sqrt(2)
+
+
+    :param coordiniates_or_tiles: A set of coordinates.
+    :type coordiniates_or_tiles: Tiles
+    :param allow_euclidean_connections: Whether to regard diagonal adjected cells as neighbors
+    :type: bool
+    :param allow_manhattan_connections: Whether to regard directly adjected cells as neighbors
+    :type: bool
+
+    :return: A graph with nodes that are conneceted as specified by the parameters.
+    :rtype: nx.Graph
+    """
+    assert allow_euclidean_connections or allow_manhattan_connections
+    if hasattr(coordiniates_or_tiles, 'positions'):
+        coordiniates_or_tiles = coordiniates_or_tiles.positions
+    possible_connections = itertools.combinations(coordiniates_or_tiles, 2)
+    graph = nx.Graph()
+    for a, b in possible_connections:
+        diff = np.linalg.norm(np.asarray(a)-np.asarray(b))
+        if allow_manhattan_connections and allow_euclidean_connections and diff <= np.sqrt(2):
+            graph.add_edge(a, b)
+        elif not allow_manhattan_connections and allow_euclidean_connections and diff == np.sqrt(2):
+            graph.add_edge(a, b)
+        elif allow_manhattan_connections and not allow_euclidean_connections and diff == 1:
+            graph.add_edge(a, b)
+    return graph
+
+
 class TSPBaseAgent(ABC):
 
     def __init__(self, state, agent_i, static_problem: bool = True):
diff --git a/environment/assets/agent/__init__.py b/environment/assets/agent/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/environment/factory.py b/environment/factory.py
index cc56600..a7251f6 100644
--- a/environment/factory.py
+++ b/environment/factory.py
@@ -57,7 +57,7 @@ class BaseFactory(gym.Env):
         self.conf = FactoryConfigParser(self._config_file)
         # Attribute Assignment
         self.level_filepath = Path(__file__).parent.parent / h.LEVELS_DIR / f'{self.conf.level_name}.txt'
-        self._renderer = None  # expensive - don't use it when not required !
+        self._renderer = None  # expensive - don't use; unless required !
 
         parsed_entities = self.conf.load_entities()
         self.map = LevelParser(self.level_filepath, parsed_entities, self.conf.pomdp_r)
@@ -173,7 +173,7 @@ class BaseFactory(gym.Env):
         if not self._renderer:  # lazy init
             from environment.utils.renderer import Renderer
             global Renderer
-            self._renderer = Renderer(self.map.level_shape,  view_radius=self.conf.pomdp_r, fps=20)
+            self._renderer = Renderer(self.map.level_shape,  view_radius=self.conf.pomdp_r, fps=10)
 
         render_entities = self.state.entities.render()
         if self.conf.pomdp_r:
diff --git a/environment/utils/config_parser.py b/environment/utils/config_parser.py
index fe5db31..ffb27d5 100644
--- a/environment/utils/config_parser.py
+++ b/environment/utils/config_parser.py
@@ -1,4 +1,6 @@
+from os import PathLike
 from pathlib import Path
+from typing import Union
 
 import yaml
 
@@ -20,9 +22,10 @@ class FactoryConfigParser(object):
     default_actions = [c.MOVE8, c.NOOP]
     default_observations = [c.WALLS, c.AGENTS]
 
-    def __init__(self, config_path):
+    def __init__(self, config_path, custom_modules_path: Union[None, PathLike] = None):
         self.config_path = Path(config_path)
-        self.config = yaml.safe_load(config_path.open())
+        self.custom_modules_path = Path(config_path) if custom_modules_path is not None else custom_modules_path
+        self.config = yaml.safe_load(self.config_path.open())
         self.do_record = False
 
     def __getattr__(self, item):
@@ -67,8 +70,13 @@ class FactoryConfigParser(object):
         entities.extend(x for x in self.entities if x != c.DEFAULTS)
 
         for entity in entities:
-            folder_path = MODULE_PATH if entity not in self.default_entites else DEFAULT_PATH
-            entity_class = locate_and_import_class(entity, folder_path)
+            try:
+                folder_path = MODULE_PATH if entity not in self.default_entites else DEFAULT_PATH
+                folder_path = (Path(__file__) / '..' / '..' / '..' / folder_path)
+                entity_class = locate_and_import_class(entity, folder_path)
+            except AttributeError:
+                folder_path = self.custom_modules_path
+                entity_class = locate_and_import_class(entity, folder_path)
             entity_kwargs = self.entities.get(entity, {})
             entity_symbol = entity_class.symbol if hasattr(entity_class, 'symbol') else None
             entity_classes.update({entity: {'class': entity_class, 'kwargs': entity_kwargs, 'symbol': entity_symbol}})
@@ -86,11 +94,15 @@ class FactoryConfigParser(object):
             parsed_actions = list()
             for action in actions:
                 folder_path = MODULE_PATH if action not in base_env_actions else DEFAULT_PATH
-                class_or_classes = locate_and_import_class(action, folder_path)
+                try:
+                    class_or_classes = locate_and_import_class(action, folder_path)
+                except AttributeError:
+                    class_or_classes = locate_and_import_class(action, self.custom_modules_path)
                 try:
                     parsed_actions.extend(class_or_classes)
                 except TypeError:
                     parsed_actions.append(class_or_classes)
+
             parsed_actions = [x() for x in parsed_actions]
 
             # Observation
@@ -114,7 +126,10 @@ class FactoryConfigParser(object):
 
         for rule in rules:
             folder_path = MODULE_PATH if rule not in self.default_rules else DEFAULT_PATH
-            rule_class = locate_and_import_class(rule, folder_path)
+            try:
+                rule_class = locate_and_import_class(rule, folder_path)
+            except AttributeError:
+                rule_class = locate_and_import_class(rule, self.custom_modules_path)
             rule_kwargs = self.rules.get(rule, {})
             rules_classes.update({rule: {'class': rule_class, 'kwargs': rule_kwargs}})
         return rules_classes
diff --git a/environment/utils/helpers.py b/environment/utils/helpers.py
index bdcca7c..2650729 100644
--- a/environment/utils/helpers.py
+++ b/environment/utils/helpers.py
@@ -1,10 +1,9 @@
 import importlib
-import itertools
+
 from collections import defaultdict
 from pathlib import PurePath, Path
 from typing import Union, Dict, List
 
-import networkx as nx
 import numpy as np
 from numpy.typing import ArrayLike
 
@@ -210,58 +209,25 @@ def asset_str(agent):
         return c.AGENT, 'idle'
 
 
-def points_to_graph(coordiniates_or_tiles, allow_euclidean_connections=True, allow_manhattan_connections=True):
-    """
-    Given a set of coordinates, this function contructs a non-directed graph, by conncting adjected points.
-    There are three combinations of settings:
-        Allow all neigbors:     Distance(a, b) <= sqrt(2)
-        Allow only manhattan:   Distance(a, b) == 1
-        Allow only euclidean:   Distance(a, b) == sqrt(2)
-
-
-    :param coordiniates_or_tiles: A set of coordinates.
-    :type coordiniates_or_tiles: Tiles
-    :param allow_euclidean_connections: Whether to regard diagonal adjected cells as neighbors
-    :type: bool
-    :param allow_manhattan_connections: Whether to regard directly adjected cells as neighbors
-    :type: bool
-
-    :return: A graph with nodes that are conneceted as specified by the parameters.
-    :rtype: nx.Graph
-    """
-    assert allow_euclidean_connections or allow_manhattan_connections
-    if hasattr(coordiniates_or_tiles, 'positions'):
-        coordiniates_or_tiles = coordiniates_or_tiles.positions
-    possible_connections = itertools.combinations(coordiniates_or_tiles, 2)
-    graph = nx.Graph()
-    for a, b in possible_connections:
-        diff = np.linalg.norm(np.asarray(a)-np.asarray(b))
-        if allow_manhattan_connections and allow_euclidean_connections and diff <= np.sqrt(2):
-            graph.add_edge(a, b)
-        elif not allow_manhattan_connections and allow_euclidean_connections and diff == np.sqrt(2):
-            graph.add_edge(a, b)
-        elif allow_manhattan_connections and not allow_euclidean_connections and diff == 1:
-            graph.add_edge(a, b)
-    return graph
-
-
 def locate_and_import_class(class_name, folder_path: Union[str, PurePath] = ''):
     """Locate an object by name or dotted path, importing as necessary."""
     import sys
     sys.path.append("..")
-    folder_path = Path(folder_path)
-    module_paths = [x for x in folder_path.rglob('*.py') if x.is_file() and '__init__' not in x.name]
+    folder_path = Path(folder_path).resolve()
+    module_paths = [x.resolve() for x in folder_path.rglob('*.py') if x.is_file() and '__init__' not in x.name]
     # possible_package_path = folder_path / '__init__.py'
     # package = str(possible_package_path) if possible_package_path.exists() else None
     all_found_modules = list()
+    package_pos = next(idx for idx, x in enumerate(Path(__file__).resolve().parts) if x == 'environment')
     for module_path in module_paths:
-        mod = importlib.import_module('.'.join([x.replace('.py', '') for x in module_path.parts]))
+        module_parts = [x.replace('.py', '') for idx, x in enumerate(module_path.parts) if idx >= package_pos]
+        mod = importlib.import_module('.'.join(module_parts))
         all_found_modules.extend([x for x in dir(mod) if not(x.startswith('__') or len(x) < 2 or x.isupper())
                                   and x not in ['Entity',  'NamedTuple', 'List', 'Rule', 'Union', 'random', 'Floor'
                                                 'TickResult', 'ActionResult', 'Action', 'Agent', 'deque',
                                                 'BoundEntityMixin', 'RenderEntity', 'TemplateRule', 'defaultdict',
                                                 'is_move', 'Objects', 'PositionMixin', 'IsBoundMixin', 'EnvObject',
-                                                'EnvObjects',]])
+                                                'EnvObjects']])
         try:
             model_class = mod.__getattribute__(class_name)
             return model_class
@@ -269,4 +235,4 @@ def locate_and_import_class(class_name, folder_path: Union[str, PurePath] = ''):
             continue
     raise AttributeError(f'Class "{class_name}" was not found!!!"\n'
                          f'Check the {folder_path.name} name.\n'
-                         f'Possible Options are:\n{set(all_found_modules)}')
\ No newline at end of file
+                         f'Possible Options are:\n{set(all_found_modules)}')
diff --git a/environment/utils/observation_builder.py b/environment/utils/observation_builder.py
index 4421aff..153e11d 100644
--- a/environment/utils/observation_builder.py
+++ b/environment/utils/observation_builder.py
@@ -191,6 +191,9 @@ class RayCaster:
         self.ray_targets = self.build_ray_targets()
         self.obs_shape_cube = np.array([self.pomdp_r, self.pomdp_r])
 
+    def __repr__(self):
+        return f'{self.__class__.__name__}({self.agent.name})'
+
     def build_ray_targets(self):
         north = np.array([0, -1])*self.pomdp_r
         thetas = [np.deg2rad(deg) for deg in np.linspace(-self.degs // 2, self.degs // 2, self.n_rays)[::-1]]
@@ -202,12 +205,9 @@ class RayCaster:
         rot_M = np.unique(np.round(rot_M @ north), axis=0)
         return rot_M.astype(int)
 
-    @staticmethod
-    def ray_block_cache(cache_dict, key, callback, ents):
+    def ray_block_cache(self, cache_dict, key, callback, ents):
         if key not in cache_dict:
             cache_dict[key] = callback()
-        if any(True for e in ents.pos_dict[key] if e.is_blocking_light) and not cache_dict[key]:
-            print()
         return cache_dict[key]
 
     def visible_entities(self, entities):
@@ -222,14 +222,23 @@ class RayCaster:
                 entities_hit = entities.pos_dict[(x, y)]
                 hits = self.ray_block_cache(cache_blocking,
                                             (x, y),
-                                            lambda: any(True for e in entities_hit if e.is_blocking_light),
+                                            lambda: any(e.is_blocking_light for e in entities_hit),
                                             entities)
 
-                diag_hits = all([
+                try:
+                    d = next(x for x in entities_hit if 'Door' in x.name)
+                    if d.pos in entities.pos_dict.keys():
+                        if d.is_closed and not entities.pos_dict[d.pos]:
+                            print()
+                except StopIteration:
+                    pass
+
+                diag_hits = any([
                     self.ray_block_cache(
                         cache_blocking,
                         key,
-                        lambda: all(False for e in entities.pos_dict[key] if not e.is_blocking_light),
+                        # lambda: all(False for e in entities.pos_dict[key] if not e.is_blocking_light),
+                        lambda: any(e.is_blocking_light for e in entities.pos_dict[key]),
                                             entities)
                     for key in ((x, y-cy), (x-cx, y))
                 ]) if (cx != 0 and cy != 0) else False
@@ -238,13 +247,6 @@ class RayCaster:
                 if hits or diag_hits:
                     break
                 rx, ry = x, y
-        try:
-            d = next(x for x in visible if 'Door' in x.name)
-            v = [x for x in visible if tuple(np.subtract(x.pos, d.pos)) in [(1, 0), (0, 1), (-1, 0), (0, -1)] and x.name.startswith('Floor')]
-            if len(v) > 2:
-                pass
-        except StopIteration:
-            pass
         return visible
 
     def get_rays(self):
diff --git a/environment/utils/renderer.py b/environment/utils/renderer.py
index 2275c71..ea2c9b7 100644
--- a/environment/utils/renderer.py
+++ b/environment/utils/renderer.py
@@ -3,12 +3,12 @@ import sys
 from pathlib import Path
 from collections import deque
 from itertools import product
+
+import numpy as np
 import pygame
 from typing import Tuple, Union
 import time
 
-import torch
-
 from environment.utils.render import RenderEntity
 
 AGENT: str = 'agent'
@@ -133,7 +133,8 @@ class Renderer:
         pygame.display.flip()
         self.clock.tick(self.fps)
         rgb_obs = pygame.surfarray.array3d(self.screen)
-        return torch.from_numpy(rgb_obs).permute(2, 0, 1)
+        return np.transpose(rgb_obs, (2, 0, 1))
+        # return torch.from_numpy(rgb_obs).permute(2, 0, 1)
 
 
 if __name__ == '__main__':
diff --git a/quickstart/all_test_config.yaml b/quickstart/all_test_config.yaml
index ae7f6c5..9e491ef 100644
--- a/quickstart/all_test_config.yaml
+++ b/quickstart/all_test_config.yaml
@@ -29,8 +29,9 @@ Entities:
 Agents:
   Wolfgang:
     Actions:
-      - Move8
-      - DoorUse
+      - Noop
+      - Noop
+      - Noop
       - CleanUp
     Observations:
       - Self
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 40c6cd6..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1,12 +0,0 @@
-numpy~=1.24.3
-tqdm~=4.60.0
-pandas~=1.2.3
-seaborn~=0.12.2
-matplotlib>=3.3.4
-pygame~=2.0.1
-networkx>=2.6.3
-simplejson~=3.17.2
-PyYAML~=5.3.1
-natsort~=7.1.1
-torch~=1.10.0
-gymnasium~=0.28.1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index f76b435..f7c2889 100644
--- a/setup.py
+++ b/setup.py
@@ -4,29 +4,34 @@ this_directory = Path(__file__).parent
 long_description = (this_directory / "README.md").read_text()
 
 
-setup(name='Marl-Neon-Grid',
-      version='0.1.4.4',
-      description='A collection of MARL gridworlds to study coordination and cooperation.',
-      author='Robert Müller',
+setup(name='Marl-Factory-Grid',
+      version='0.0.5',
+      description='A framework to research MARL agents in various setings.',
+      author='Steffen Illium',
       author_email='steffen.illium@ifi.lmu.de',
-      url='https://github.com/romue404/marl-neon-grid',
+      url='https://github.com/illiumst/marl-factory-grid/import',
       license='MIT',
       keywords=[
             'artificial intelligence',
             'pytorch',
             'multiagent reinforcement learning',
-            'simulation'
+            'simulation',
+            'emergence',
+            'gymnasium',
+            'environment',
+
       ],
       classifiers=[
             'Development Status :: 4 - Beta',
             'Intended Audience :: Developers',
             'Topic :: Scientific/Engineering :: Artificial Intelligence',
             'License :: OSI Approved :: MIT License',
-            'Programming Language :: Python :: 3.6',
+            'Programming Language :: Python :: 3.11',
       ],
       long_description=long_description,
       long_description_content_type='text/markdown',
       packages=find_packages(exclude=['examples']),
       include_package_data=True,
-      install_requires=['numpy', 'pygame>=2.0', 'numba>=0.56', 'gymnasium>=0.26', 'seaborn', 'pandas']
-      )
\ No newline at end of file
+      install_requires=['numpy', 'pygame>=2.0', 'numba>=0.56', 'gymnasium>=0.26', 'seaborn', 'pandas',
+                        'pyyaml', 'networkx', 'torch', 'tqdm']
+      )