-
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.
Florian Schröder authoredSeveral 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))