From a9bda66c8e41da6b51d3bdf844b75f7fef4d431d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20Schr=C3=B6der?=
 <fschroeder@techfak.uni-bielefeld.de>
Date: Thu, 7 Mar 2024 17:22:34 +0100
Subject: [PATCH] Refactor code and add comprehensive docstrings

Implemented multiple code adjustments across various Python modules, including renaming variables for clarity and improving data type definition. Added comprehensive docstrings to all functions and classes to enhance code readability and maintainability. Corrected a bug in debug parameter where it incorrectly employed args.debug, replacing it with args.do_study for correct implementation.
---
 cooperative_cuisine/__main__.py          |   2 +-
 cooperative_cuisine/counter_factory.py   |  75 ++++++++++++++---
 cooperative_cuisine/pygame_2d_vis/gui.py |   2 +-
 cooperative_cuisine/validation.py        | 101 +++++++++++++++++++++--
 4 files changed, 162 insertions(+), 18 deletions(-)

diff --git a/cooperative_cuisine/__main__.py b/cooperative_cuisine/__main__.py
index 428f41ea..685d2402 100644
--- a/cooperative_cuisine/__main__.py
+++ b/cooperative_cuisine/__main__.py
@@ -42,7 +42,7 @@ def start_pygame_gui(cli_args):
         cli_args.game_port,
         cli_args.manager_ids,
         CONNECT_WITH_STUDY_SERVER=USE_STUDY_SERVER,
-        debug=cli_args.debug,
+        debug=cli_args.do_study,
     )
 
 
diff --git a/cooperative_cuisine/counter_factory.py b/cooperative_cuisine/counter_factory.py
index 02f6d5d7..d991daec 100644
--- a/cooperative_cuisine/counter_factory.py
+++ b/cooperative_cuisine/counter_factory.py
@@ -112,8 +112,6 @@ class CounterFactory:
     item information provided. It also provides methods for mapping and filtering the item information.
     """
 
-    additional_counter_names = {"Counter"}
-
     def __init__(
         self,
         layout_chars_config: dict[str, str],
@@ -154,7 +152,7 @@ class CounterFactory:
         """The plate config from the `environment_config`"""
         self.order_manager: OrderManager = order_manager
         """The order and score manager to pass to `ServingWindow` and the `Tashcan` which can affect the scores."""
-        self.effect_manager_config = effect_manager_config
+        self.effect_manager_config: dict = effect_manager_config
         """The effect manager config to setup the effect manager based on the defined effects in the item info."""
 
         self.no_counter_chars: set[str] = set(
@@ -185,13 +183,13 @@ class CounterFactory:
         }
         """A dictionary mapping cooking counters to the list of equipment items associated with them."""
 
-        self.undo_dispenser_pickup = undo_dispenser_pickup
+        self.undo_dispenser_pickup: bool = undo_dispenser_pickup
         """Put back ingredients of the same type on a dispenser."""
 
-        self.hook = hook
+        self.hook: Hooks = hook
         """Reference to the hook manager."""
 
-        self.random = random
+        self.random: Random = random
         """Random instance."""
 
     def get_counter_object(self, c: str, pos: npt.NDArray[float]) -> Counter:
@@ -303,7 +301,17 @@ class CounterFactory:
         by_equipment_name: str = None,
         add_effects: bool = False,
     ) -> dict[str, ItemInfo]:
-        """Filter the item info dict by item type or equipment name"""
+        """Filter the item info dict by item type or equipment name.
+
+        Args:
+            by_item_type: An optional parameter of ItemType enum type. If provided, filters the item info by item type.
+            by_equipment_name: An optional parameter of string type. If provided, filters the item info by equipment name.
+            add_effects: A boolean parameter indicating whether to include items with effects in the filtered result.
+
+        Returns:
+            A dictionary mapping item names (strings) to ItemInfo objects. The filtered item info based on the provided parameters.
+
+        """
         filtered = {}
         if by_item_type is not None:
             filtered = {
@@ -358,6 +366,42 @@ class CounterFactory:
                     counter.set_addon(closest_addon)
 
     def setup_effect_manger(self, counters: list[Counter]) -> dict[str, EffectManager]:
+        """Setup effect manager for effects in the item info.
+
+        Args:
+            counters (list[Counter]): A list of Counter objects.
+
+        Returns:
+            dict[str, EffectManager]: A dictionary where the keys are manager names and the values are EffectManager objects.
+
+        Raises:
+            AssertionError: If the manager for an effect is not found in the effect_manager_config.
+
+        Example Usage:
+            counters = [counter1, counter2]
+            effect_manager = setup_effect_manger(counters)
+
+        This method sets up the effect managers for the given counters. It iterates over the effects obtained from
+        the filter_item_info() method using the by_item_type argument set to ItemType.Effect. For each effect,
+        it checks if the effect's manager is present in the self.effect_manager_config dictionary. If not,
+        it raises an AssertionError.
+
+        If the effect's manager is already present in the effect_manager dictionary, it assigns the manager to the
+        variable 'manager'. Otherwise, it creates a new instance of the effect manager class using the values from
+        self.effect_manager_config[effect.manager]["class"] and self.effect_manager_config[effect.manager]["kwargs"].
+        It then adds the effect to the manager and updates the effect's manager attribute with the assigned manager.
+
+        Finally, it returns the effect_manager dictionary containing the assigned effect managers.
+
+        Note: This method assumes the following variables and methods are available within the class:
+        - self.filter_item_info(by_item_type: ItemType) -> dict[str, Effect]: This method returns a dictionary of effects filtered by item type.
+        - self.effect_manager_config: A dictionary containing the configuration for the effect managers.
+        - self.hook: An object representing the hook.
+        - self.random: An object representing a random generator.
+        - Counter: A class representing a counter.
+        - EffectManager: A class representing an effect manager.
+        - ItemType: An enum representing different types of items.
+        """
         effect_manager = {}
         for name, effect in self.filter_item_info(by_item_type=ItemType.Effect).items():
             assert (
@@ -387,7 +431,9 @@ class CounterFactory:
     def parse_layout_file(
         self,
         layout_config: str,
-    ) -> Tuple[list[Counter], list[npt.NDArray], list[npt.NDArray], int, int]:
+    ) -> Tuple[
+        list[Counter], list[npt.NDArray[float]], list[npt.NDArray[float]], 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
@@ -463,7 +509,16 @@ class CounterFactory:
         )
 
 
-def determine_counter_orientations(counters, grid, kitchen_center):
+def determine_counter_orientations(
+    counters: list[Counter], grid: list[list[int]], kitchen_center: npt.NDArray[float]
+):
+    """Determines the orientation of counters on a grid based on free positions around it.
+
+    Args:
+        counters: A list of Counter objects.
+        grid: The grid representing the kitchen layout.
+        kitchen_center: The coordinates of the kitchen center.
+    """
     grid = np.array(grid).T
 
     grid_width = grid.shape[0]
@@ -492,7 +547,7 @@ def determine_counter_orientations(counters, grid, kitchen_center):
             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
+                [np.linalg.norm(vector_to_center - n) for n in neighbours_free]
             )
             nearest_vec = neighbours_free[n_idx]
             # print(nearest_vec, type(nearest_vec))
diff --git a/cooperative_cuisine/pygame_2d_vis/gui.py b/cooperative_cuisine/pygame_2d_vis/gui.py
index 02ee0136..63f570b3 100644
--- a/cooperative_cuisine/pygame_2d_vis/gui.py
+++ b/cooperative_cuisine/pygame_2d_vis/gui.py
@@ -1988,5 +1988,5 @@ if __name__ == "__main__":
         args.game_port,
         args.manager_ids,
         CONNECT_WITH_STUDY_SERVER=True,
-        debug=args.debug,
+        debug=args.do_study,
     )
diff --git a/cooperative_cuisine/validation.py b/cooperative_cuisine/validation.py
index ff14a88f..2260d873 100644
--- a/cooperative_cuisine/validation.py
+++ b/cooperative_cuisine/validation.py
@@ -1,6 +1,8 @@
+""" Validation of configs and tutorial/guide creation for recipes. """
+
 import os
 import warnings
-from typing import TypedDict, Tuple, Iterator
+from typing import TypedDict, Tuple, Iterator, Set
 
 import networkx as nx
 from networkx import DiGraph
@@ -18,12 +20,24 @@ from cooperative_cuisine.orders import OrderManager
 
 
 class MealGraphDict(TypedDict):
+    """Represents a graph for meal creation with edges and layout information."""
+
     meal: str
+    """The name of the meal."""
     edges: list[Tuple[str, str]]
+    """A list of tuples representing the edges between cooking steps."""
     layout: dict[str, Tuple[float, float]]
+    """A dictionary mapping cooking step names to their layout coordinates."""
 
 
 class Validation:
+    """Class for performing validation tasks.
+
+    This class provides methods for performing various validation tasks, such as generating recipe graphs from item
+    information, creating guide graphs for each recipe/meal, asserting the presence of equipment and plate cycles,
+    and retrieving base ingredients and equipment required to create a meal.
+    """
+
     def __init__(
         self,
         meals: list[ItemInfo],
@@ -32,12 +46,24 @@ class Validation:
         do_validation: bool,
     ):
         self.meals: list[ItemInfo] = meals
+        """A list of ItemInfo objects representing meals."""
         self.item_info: dict[str, ItemInfo] = item_info
+        """All ItemInfos from the config."""
         self.order_manager: OrderManager = order_manager
+        """For the available meals for orders."""
         self.do_validation: bool = do_validation
+        """A boolean indicating whether to perform validation tasks."""
 
     @staticmethod
     def infer_recipe_graph(item_info) -> DiGraph:
+        """Generate a graph from ingredients and meals and their dependencies.
+
+        Args:
+            item_info: A dictionary containing information about items.
+
+        Returns:
+            DiGraph: A directed graph representing the recipe graph.
+        """
         colors = {
             ItemType.Ingredient: "black",
             ItemType.Equipment: "red",
@@ -61,6 +87,13 @@ class Validation:
         return graph
 
     def get_meal_graph(self, meal: ItemInfo) -> MealGraphDict:
+        """Create tutorial/guide graphs for each recipe/meal.
+
+        Args:
+            meal: An instance of ItemInfo representing the meal to create a graph for.
+        Returns:
+            A dictionary containing the meal name, the edges of the graph, and a layout of the graph.
+        """
         graph = DiGraph(directed=True, rankdir="LR")
 
         root = f"{meal.name}_0"
@@ -121,6 +154,7 @@ class Validation:
         }
 
     def reduce_item_node(self, graph, base_ingredients, item, visited):
+        # until now not called
         visited.append(item)
         if item in base_ingredients:
             return True
@@ -132,7 +166,7 @@ class Validation:
             )
 
     def assert_equipment_is_present(self, counters):
-        # TODO until now not called
+        # until now not called
         expected = set(
             name
             for name, info in self.item_info.items()
@@ -156,7 +190,7 @@ class Validation:
                 )
 
     def assert_plate_cycle_present(self, counters: list[Counter]):
-        # TODO until now not called
+        # 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")
@@ -179,7 +213,7 @@ class Validation:
 
     @staticmethod
     def assert_no_orphans(graph: DiGraph):
-        # TODO until now not called
+        # until now not called
         orphans = [
             n
             for n in graph.nodes()
@@ -192,6 +226,7 @@ class Validation:
 
     @staticmethod
     def assert_roots_are_dispensable(graph, base_ingredients):
+        # until now not called
         root_nodes = [
             n for n in graph.nodes() if graph.in_degree(n) == 0 and "Plate" not in n
         ]
@@ -203,6 +238,7 @@ class Validation:
             )
 
     def assert_meals_are_reducible(self, graph, base_ingredients):
+        # until now not called
         meals = [n for n in graph.nodes() if self.item_info[n].type == ItemType.Meal]
 
         for meal in meals:
@@ -235,7 +271,37 @@ class Validation:
                 recipes[item_name] = requirements | {"Plate"}
         return recipes
 
-    def get_layout_requirements(self, counters: list[Counter]):
+    @staticmethod
+    def get_layout_requirements(counters: list[Counter]):
+        """Infer layout requirements from a list of counters.
+
+        Args:
+            counters: A list of Counter objects representing various counters in a layout.
+
+        Returns:
+            layout_requirements: A set of layout requirements based on the given counters.
+
+        This static method takes a list of Counter objects as input and returns a set of layout requirements. The
+        layout requirements are determined based on the type of counters and their properties.
+
+        The method iterates over each counter in the counters list. For each counter, it checks its type using the
+        isinstance() function and performs the required actions:
+            - If the counter is an instance of Dispenser or PlateDispenser, it adds the counter's dispensing name to the layout_requirements set.
+            - If the counter is an instance of CuttingBoard, it adds "CuttingBoard" to the layout_requirements set.
+            - If the counter is an instance of CookingCounter, it adds the counter's name to the layout_requirements set.
+            - If the counter's occupied_by property is not None and the occupied_by object has a "name" attribute, it adds the occupied_by object's name to the layout_requirements set.
+
+        Finally, the method returns the layout_requirements set.
+
+        Note: The method uses type checking and attribute checking to determine the layout requirements based on the counters provided.
+
+        Example usage:
+            ```python
+            counters = [counter1, counter2, counter3]
+            requirements = MyClass.get_layout_requirements(counters)
+            print(requirements)
+            ```
+        """
         layout_requirements = set()
         for counter in counters:
             if isinstance(counter, (Dispenser, PlateDispenser)):
@@ -248,7 +314,16 @@ class Validation:
                 layout_requirements.add(counter.occupied_by.name)
         return layout_requirements
 
-    def validate_environment(self, counters: list[Counter]):
+    def validate_environment(self, counters: list[Counter]) -> Set[str]:
+        """Validates the environment by generating and saving a recipe graph, checking if ordered meals are possible,
+        and returning a set of meals that can be ordered.
+
+        Args:
+            counters (list[Counter]): A list of counters.
+
+        Returns:
+            meals_to_be_ordered (set): A set of meals that can be ordered, based on the environment validation.
+        """
         if self.do_validation:
             graph = self.infer_recipe_graph(self.item_info)
             os.makedirs(ROOT_DIR / "generated", exist_ok=True)
@@ -273,6 +348,20 @@ class Validation:
             return {m.name for m in self.meals}
 
     def get_recipe_graphs(self) -> list[MealGraphDict]:
+        """Returns a list of recipe graphs for all available meals.
+
+        If there are no available meals, an empty list is returned.
+
+        The recipe graphs are generated and stored in the "generated" directory in the root directory (defined as
+        ROOT_DIR) of the application. If the directory does not exist, it will be created.
+
+        The recipe graphs are obtained by calling the `get_meal_graph` method for each available meal in the
+        `order_manager`. The results are collected and returned as a list.
+
+        Returns:
+            list[MealGraphDict]: A list of recipe graphs for all available meals.
+
+        """
         if not self.order_manager.available_meals:
             return []
         os.makedirs(ROOT_DIR / "generated", exist_ok=True)
-- 
GitLab