Skip to content
Snippets Groups Projects
  • Florian Schröder's avatar
    fb060696
    Add Overcooked 1 layout files and minor code adjustments · fb060696
    Florian Schröder authored
    Several Overcooked 1 layout files have been added along with changes in the counter mappings for the game simulator. This includes serving window character changes and addition of clean plate character mappings. Additionally, `overcooked_simulator/utils.py` has been modified to include a method that creates a basic layout grid. These changes help in enhancing and expanding the game simulator.
    fb060696
    History
    Add Overcooked 1 layout files and minor code adjustments
    Florian Schröder authored
    Several Overcooked 1 layout files have been added along with changes in the counter mappings for the game simulator. This includes serving window character changes and addition of clean plate character mappings. Additionally, `overcooked_simulator/utils.py` has been modified to include a method that creates a basic layout grid. These changes help in enhancing and expanding the game simulator.
counter_factory.py 14.35 KiB
"""
The `CounterFactory` initializes the `Counter` classes from the characters in the layout config.
The mapping depends on the definition in the `environment_config.yml` in the `layout_chars` section.

```yaml
layout_chars:
  _: Free
  hash: Counter
  A: Agent
  P: PlateDispenser
  C: CuttingBoard
  X: Trashcan
  W: ServingWindow
  S: Sink
  +: SinkAddon
  U: Pot  # with Stove
  Q: Pan  # with Stove
  O: Peel  # with Oven
  F: Basket  # with DeepFryer
  T: Tomato
  N: Onion  # oNioN
  L: Lettuce
  K: Potato  # Kartoffel
  I: Fish  # fIIIsh
  D: Dough
  E: Cheese  # chEEEse
  G: Sausage  # sausaGe
  B: Bun
  M: Meat
```

# Code Documentation
"""
import inspect
import sys
from random import Random
from typing import Any, Type, TypeVar

import numpy as np
import numpy.typing as npt

from overcooked_simulator.counters import (
    Counter,
    CookingCounter,
    Dispenser,
    ServingWindow,
    CuttingBoard,
    PlateDispenser,
    Sink,
    PlateConfig,
    SinkAddon,
    Trashcan,
)
from overcooked_simulator.effect_manager import EffectManager
from overcooked_simulator.game_items import (
    ItemInfo,
    ItemType,
    CookingEquipment,
    Plate,
    Item,
)
from overcooked_simulator.hooks import Hooks
from overcooked_simulator.order import OrderAndScoreManager
from overcooked_simulator.utils import get_closest

T = TypeVar("T")


def convert_words_to_chars(layout_chars_config: dict[str, str]) -> dict[str, str]:
    """Converts words in a given layout chars configuration dictionary to their corresponding characters.

    This is useful for characters that can not be keys in a yaml file. For example, `#` produces a comment.
    Therefore, you can specify `hash` as a key (`hash: Counter`). `word_refs` defines the conversions. *Click on `▶ View Source`.*

    Args:
        layout_chars_config: A dictionary containing layout character configurations, where the keys are words
            representing layout characters and the values are the corresponding character representations.

    Returns:
        A dictionary where the keys are the layout characters and the values are their corresponding words.
    """
    word_refs = {
        "hash": "#",
        # "space": " ",
        "dot": ".",
        "comma": ",",
        # "semicolon": ";",
        "colon": ":",
        "minus": "-",
        "exclamation": "!",
        "question": "?",
        "dquote": '"',
        "squote": "'",
        "star": "*",
        "ampersand": "&",
        "equal": "=",
        "right": ">",
        "left": "<",
        "pipe": "|",
        "at": "@",
        "wave": "~",  # ~ is None / null in yaml
        "ocurlybracket": "{",
        "ccurlybracket": "}",
        "osquarebracket": "[",
        "csquarebracket": "]",
    }
    return {word_refs.get(c, c): name for c, name in layout_chars_config.items()}


class CounterFactory:
    """The `CounterFactory` class is responsible for creating counter objects based on the layout configuration and
    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],
        item_info: dict[str, ItemInfo],
        serving_window_additional_kwargs: dict[str, Any],
        plate_config: PlateConfig,
        order_and_score: OrderAndScoreManager,
        effect_manager_config: dict,
        hook: Hooks,
        random: Random,
    ) -> None:
        """Constructor for the `CounterFactory` class. Set up the attributes necessary to instantiate the counters.

        Initializes the object with the provided parameters. It performs the following tasks:
        - Converts the layout character configuration from words to characters.
        - Sets the item information dictionary.
        - Sets the additional keyword arguments for serving window configuration.
        - Sets the plate configuration.

        Args:
            layout_chars_config: A dictionary mapping layout characters to their corresponding names.
            item_info: A dictionary containing information about different items.
            serving_window_additional_kwargs: Additional keyword arguments for serving window configuration.
            plate_config: The configuration for plate usage.
        """
        self.layout_chars_config: dict[str, str] = convert_words_to_chars(
            layout_chars_config
        )
        """Layout chars to the counter names."""
        self.item_info: dict[str, ItemInfo] = item_info
        """All item infos from the `item_info` config."""
        self.serving_window_additional_kwargs: dict[
            str, Any
        ] = serving_window_additional_kwargs
        """The additional keyword arguments for the serving window."""
        self.plate_config: PlateConfig = plate_config
        """The plate config from the `environment_config`"""
        self.order_and_score: OrderAndScoreManager = order_and_score
        """The order and score manager to pass to `ServingWindow` and the `Tashcan` which can affect the scores."""
        self.effect_manager_config = 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(
            c
            for c, name in self.layout_chars_config.items()
            if name in ["Agent", "Free"]
        )
        """A set of characters that represent counters for agents or free spaces."""

        self.counter_classes: dict[str, Type] = dict(
            filter(
                lambda k: issubclass(k[1], Counter),
                inspect.getmembers(
                    sys.modules["overcooked_simulator.counters"], inspect.isclass
                ),
            )
        )
        """A dictionary of counter classes imported from the 'overcooked_simulator.counters' module."""

        self.cooking_counter_equipments: dict[str, set[str]] = {
            cooking_counter: {
                equipment
                for equipment, e_info in self.item_info.items()
                if e_info.equipment and e_info.equipment.name == cooking_counter
            }
            for cooking_counter, info in self.item_info.items()
            if info.type == ItemType.Equipment and info.equipment is None
        }
        """A dictionary mapping cooking counters to the list of equipment items associated with them."""

        self.hook = hook
        """Reference to the hook manager."""

        self.random = random
        """Random instance."""

    def get_counter_object(self, c: str, pos: npt.NDArray[float]) -> Counter:
        """Create and returns a counter object based on the provided character and position."""

        assert self.can_map(c), f"Can't map counter char {c}"
        counter_class = None
        if c == "@":
            print("-")
        if self.layout_chars_config[c] in self.item_info:
            item_info = self.item_info[self.layout_chars_config[c]]
            if item_info.type == ItemType.Equipment and item_info.equipment:
                if item_info.equipment.name not in self.counter_classes:
                    return CookingCounter(
                        name=item_info.equipment.name,
                        equipments=self.cooking_counter_equipments[
                            item_info.equipment.name
                        ],
                        pos=pos,
                        occupied_by=CookingEquipment(
                            name=item_info.name,
                            item_info=item_info,
                            transitions=self.filter_item_info(
                                by_equipment_name=item_info.name,
                                add_effects=True,
                            ),
                        ),
                        hook=self.hook,
                    )
            elif item_info.type == ItemType.Ingredient:
                return Dispenser(pos=pos, hook=self.hook, dispensing=item_info)
            elif item_info.type == ItemType.Tool:
                return Counter(
                    pos=pos,
                    hook=self.hook,
                    occupied_by=Item(name=item_info.name, item_info=item_info),
                )

        if counter_class is None:
            if self.layout_chars_config[c] in self.counter_classes:
                counter_class = self.counter_classes[self.layout_chars_config[c]]
            elif self.layout_chars_config[c] == "Plate":
                return Counter(
                    pos=pos,
                    hook=self.hook,
                    occupied_by=Plate(
                        transitions=self.filter_item_info(
                            by_item_type=ItemType.Meal, add_effects=True
                        ),
                        clean=True,
                        item_info=self.item_info[Plate.__name__],
                    ),
                )
        kwargs = {
            "pos": pos,
            "hook": self.hook,
        }
        if issubclass(counter_class, (CuttingBoard, Sink)):
            kwargs["transitions"] = self.filter_item_info(
                by_equipment_name=counter_class.__name__,
                add_effects=True,
            )
        elif issubclass(counter_class, PlateDispenser):
            kwargs.update(
                {
                    "plate_transitions": self.filter_item_info(
                        by_item_type=ItemType.Meal, add_effects=True
                    ),
                    "plate_config": self.plate_config,
                    "dispensing": self.item_info[Plate.__name__],
                    "random": self.random,
                }
            )
        elif issubclass(counter_class, ServingWindow):
            kwargs.update(self.serving_window_additional_kwargs)
        if issubclass(counter_class, (ServingWindow, Trashcan)):
            kwargs[
                "order_and_score"
            ] = self.order_and_score  # individual because for the later trash scorer
        return counter_class(**kwargs)

    def can_map(self, char) -> bool:
        """Check if the provided character can be mapped to a counter object."""
        return char in self.layout_chars_config and (
            not self.is_counter(char)
            or self.layout_chars_config[char] in self.item_info
            or self.layout_chars_config[char] in self.counter_classes
        )

    def is_counter(self, c: str) -> bool:
        """Checks if the provided character represents a counter."""
        return c in self.layout_chars_config and c not in self.no_counter_chars

    def map_not_counter(self, c: str) -> str:
        """Maps the provided character to a non-counter word based on the layout configuration."""
        assert self.can_map(c) and not self.is_counter(
            c
        ), "Cannot map char {c} as a 'not counter'"
        return self.layout_chars_config[c]

    def filter_item_info(
        self,
        by_item_type: ItemType = None,
        by_equipment_name: str = None,
        add_effects: bool = False,
    ) -> dict[str, ItemInfo]:
        """Filter the item info dict by item type or equipment name"""
        filtered = {}
        if by_item_type is not None:
            filtered = {
                name: info
                for name, info in self.item_info.items()
                if info.type == by_item_type
            }
        if by_equipment_name is not None:
            filtered = {
                name: info
                for name, info in self.item_info.items()
                if info.equipment is not None
                and info.equipment.name == by_equipment_name
            }
        if add_effects:
            for name, effect in self.filter_item_info(
                by_item_type=ItemType.Effect
            ).items():
                for need in effect.needs:
                    if need in filtered:
                        filtered.update({name: effect})
        if by_item_type or by_equipment_name:
            return filtered
        return self.item_info

    def post_counter_setup(self, counters: list[Counter]):
        """Initialize the counters in the environment.

        Connect the `ServingWindow`(s) with the `PlateDispenser`.
        Find and connect the `SinkAddon`s with the `Sink`s

        Args:
            counters: list of counters to perform the post setup on.
        """
        plate_dispenser = self.get_counter_of_type(PlateDispenser, counters)
        assert len(plate_dispenser) > 0, "No Plate Dispenser in the environment"

        sink_addons = self.get_counter_of_type(SinkAddon, counters)

        for counter in counters:
            match counter:
                case ServingWindow():
                    counter: ServingWindow  # Pycharm type checker does now work for match statements?
                    counter.add_plate_dispenser(plate_dispenser[0])
                case Sink(pos=pos):
                    counter: Sink  # Pycharm type checker does now work for match statements?
                    assert len(sink_addons) > 0, "No SinkAddon but normal Sink"
                    closest_addon = get_closest(pos, sink_addons)
                    assert 1.0 == np.linalg.norm(
                        closest_addon.pos - pos
                    ), f"No SinkAddon connected to Sink at pos {pos}"
                    counter.set_addon(closest_addon)

    def setup_effect_manger(self, counters: list[Counter]) -> dict[str, EffectManager]:
        effect_manager = {}
        for name, effect in self.filter_item_info(by_item_type=ItemType.Effect).items():
            assert (
                effect.manager in self.effect_manager_config
            ), f"Manager for effect not found: {name} -> {effect.manager} not in {list(self.effect_manager_config.keys())}"
            if effect.manager in effect_manager:
                manager = effect_manager[effect.manager]
            else:
                manager = self.effect_manager_config[effect.manager]["class"](
                    hook=self.hook,
                    random=self.random,
                    **self.effect_manager_config[effect.manager]["kwargs"],
                )
                manager.set_counters(counters)
                effect_manager[effect.manager] = manager

            manager.add_effect(effect)

            effect.manager = manager

        return effect_manager

    @staticmethod
    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))