diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index f63d77aac4d9a2ae6a146b164051c23a4ca3e90f..75152544acb3267dca4dd78ae531826168d442dc 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -39,7 +39,7 @@ import uuid from collections import deque from collections.abc import Iterable from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Optional, Callable, TypedDict +from typing import TYPE_CHECKING, Optional, Callable, Set if TYPE_CHECKING: from overcooked_simulator.overcooked_environment import ( @@ -62,26 +62,6 @@ log = logging.getLogger(__name__) COUNTER_CATEGORY = "Counter" -class TransitionsValueDict(TypedDict): - """The values in the transitions dicts of the `CookingEquipment`.""" - - seconds: int | float - """The needed seconds to progress for the transition.""" - needs: list[str] - """The names of the needed items for the transition.""" - info: ItemInfo | str - """The ItemInfo of the resulting item.""" - - -class TransitionsValueByNameDict(TypedDict): - """The values in the transitions dicts of the `CuttingBoard` and the `Sink`.""" - - seconds: int | float - """The needed seconds to progress for the transition.""" - result: str - """The new name of the item after the transition.""" - - class Counter: """Simple class for a counter at a specified position (center of counter). Can hold things on top. @@ -198,11 +178,12 @@ class CuttingBoard(Counter): The character `C` in the `layout` file represents the CuttingBoard. """ - def __init__( - self, pos: np.ndarray, transitions: dict[str, TransitionsValueByNameDict] - ): + def __init__(self, pos: np.ndarray, transitions: dict[str, ItemInfo]): self.progressing = False self.transitions = transitions + self.inverted_transition_dict = { + info.needs[0]: info for name, info in self.transitions.items() + } super().__init__(pos=pos) def progress(self, passed_time: timedelta, now: datetime): @@ -219,20 +200,20 @@ class CuttingBoard(Counter): if ( self.occupied and self.progressing - and self.occupied_by.name in self.transitions + and self.occupied_by.name in self.inverted_transition_dict ): percent = ( passed_time.total_seconds() - / self.transitions[self.occupied_by.name]["seconds"] + / self.inverted_transition_dict[self.occupied_by.name].seconds ) self.occupied_by.progress( equipment=self.__class__.__name__, percent=percent ) if self.occupied_by.progress_percentage == 1.0: self.occupied_by.reset() - self.occupied_by.name = self.transitions[self.occupied_by.name][ - "result" - ] + self.occupied_by.name = self.inverted_transition_dict[ + self.occupied_by.name + ].name def start_progress(self): """Starts the cutting process.""" @@ -390,7 +371,7 @@ class PlateDispenser(Counter): pos: npt.NDArray[float], dispensing: ItemInfo, plate_config: PlateConfig, - plate_transitions: dict, + plate_transitions: dict[str, ItemInfo], **kwargs, ) -> None: super().__init__(pos=pos, **kwargs) @@ -399,7 +380,7 @@ class PlateDispenser(Counter): self.out_of_kitchen_timer = [] self.plate_config = plate_config self.next_plate_time = datetime.max - self.plate_transitions: dict[str, TransitionsValueDict] = plate_transitions + self.plate_transitions = plate_transitions self.setup_plates() def pick_up(self, on_hands: bool = True) -> Item | None: @@ -565,17 +546,25 @@ class Sink(Counter): def __init__( self, pos: npt.NDArray[float], - transitions: dict[str, TransitionsValueByNameDict], + transitions: dict[str, ItemInfo], sink_addon: SinkAddon = None, ): super().__init__(pos=pos) self.progressing = False self.sink_addon: SinkAddon = sink_addon """The connected sink addon which will receive the clean plates""" - self.occupied_by = deque() + self.occupied_by: deque[Plate] = deque() """The queue of dirty plates. Only the one on the top is progressed.""" self.transitions = transitions """The allowed transitions for the items in the sink. Here only clean plates transfer from dirty plates.""" + self.transition_needs: Set[str] = set() + """Set of all first needs of the transition item info.""" + + for name, info in transitions.items(): + assert ( + len(info.needs) >= 1 + ), "transitions in a Sink need at least one item need." + self.transition_needs.update([info.needs[0]]) @property def occupied(self): @@ -586,23 +575,21 @@ class Sink(Counter): if ( self.occupied and self.progressing - and self.occupied_by[-1].name in self.transitions + and self.occupied_by[-1].name in self.transition_needs ): - percent = ( - passed_time.total_seconds() - / self.transitions[self.occupied_by[-1].name]["seconds"] - ) - self.occupied_by[-1].progress( - equipment=self.__class__.__name__, percent=percent - ) - if self.occupied_by[-1].progress_percentage == 1.0: - self.occupied_by[-1].reset() - self.occupied_by[-1].name = self.transitions[self.occupied_by[-1].name][ - "result" - ] - plate = self.occupied_by.pop() - plate.clean = True - self.sink_addon.add_clean_plate(plate) + for name, info in self.transitions.items(): + if info.needs[0] == self.occupied_by[-1].name: + percent = passed_time.total_seconds() / info.seconds + self.occupied_by[-1].progress( + equipment=self.__class__.__name__, percent=percent + ) + if self.occupied_by[-1].progress_percentage == 1.0: + self.occupied_by[-1].reset() + self.occupied_by[-1].name = name + plate = self.occupied_by.pop() + plate.clean = True + self.sink_addon.add_clean_plate(plate) + break def start_progress(self): """Starts the cutting process.""" diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 8f1b39bd52c35fdc923ce714a88b4c7f815fe471..a77f8d78159ee6e6542209230e7bc50a6c0e76df 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -26,7 +26,7 @@ import datetime import logging import uuid from enum import Enum -from typing import Optional +from typing import Optional, TypedDict log = logging.getLogger(__name__) @@ -85,10 +85,22 @@ class ItemInfo: equipment: ItemInfo | None = dataclasses.field(compare=False, default=None) """On which the item can be created. `null`, `~` (None) converts to Plate.""" + recipe: collections.Counter | None = None + """Internally set in CookingEquipment""" + def __post_init__(self): self.type = ItemType(self.type) +class ActiveTransitionTypedDict(TypedDict): + """The values in the active transitions dicts of `CookingEquipment`.""" + + seconds: int | float + """The needed seconds to progress for the transition.""" + result: str + """The new name of the item after the transition.""" + + class Item: """Base class for game items which can be held by a player.""" @@ -151,13 +163,12 @@ class Item: class CookingEquipment(Item): """Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself ( progress_percentage) and not in the items in the content list.""" - item_category = "Cooking Equipment" - def __init__(self, transitions: dict, *args, **kwargs): + def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs): super().__init__(*args, **kwargs) self.transitions = transitions - self.active_transition: Optional[dict] = None + self.active_transition: Optional[ActiveTransitionTypedDict] = None """The info how and when to convert the content_list to a new item.""" # TODO change content ready just to str (name of the item)? @@ -171,7 +182,7 @@ class CookingEquipment(Item): log.debug(f"Initialize {self.name}: {self.transitions}") for transition in self.transitions.values(): - transition["recipe"] = collections.Counter(transition["needs"]) + transition.recipe = collections.Counter(transition.needs) def can_combine(self, other) -> bool: # already cooking or nothing to combine @@ -187,9 +198,7 @@ class CookingEquipment(Item): ingredients = collections.Counter( item.name for item in self.content_list + other ) - return any( - ingredients <= recipe["recipe"] for recipe in self.transitions.values() - ) + return any(ingredients <= recipe.recipe for recipe in self.transitions.values()) def combine(self, other) -> Item | None: return_value = None @@ -204,14 +213,13 @@ class CookingEquipment(Item): ingredients = collections.Counter(item.name for item in self.content_list) for result, transition in self.transitions.items(): - recipe = transition["recipe"] - if ingredients == recipe: - if transition["seconds"] == 0: - self.content_ready = Item(name=result, item_info=transition["info"]) + if ingredients == transition.recipe: + if transition.seconds == 0: + self.content_ready = Item(name=result, item_info=transition) else: self.active_transition = { - "seconds": transition["seconds"], - "result": Item(name=result, item_info=transition["info"]), + "seconds": transition.seconds, + "result": Item(name=result, item_info=transition), } break else: @@ -279,9 +287,7 @@ class Plate(CookingEquipment): """All meals can be hold by a clean plate""" super().__init__( name=self.create_name(), - transitions={ - k: v for k, v in transitions.items() if not v["info"].equipment - }, + transitions={k: v for k, v in transitions.items() if not v.equipment}, *args, **kwargs, ) diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py index 415e0ce302c93ce2857f58569e5ed89e41c0aacd..5be3a8aa1892bea0450ed3f6752a61d48ef864a4 100644 --- a/overcooked_simulator/gui_2d_vis/drawing.py +++ b/overcooked_simulator/gui_2d_vis/drawing.py @@ -239,6 +239,7 @@ class Visualizer: case "image": if "center_offset" in part: d = np.array(part["center_offset"]) * grid_size + pos = np.array(pos) pos += d self.draw_image( diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index ce79e280b4760a59ab92129f86bd26c905639a1f..90694f30f479e5bf8ec437d97b49e67b46426af3 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -138,15 +138,6 @@ class Environment: }, ) """The manager for the orders and score update.""" - plate_transitions = { - item: { - "seconds": info.seconds, - "needs": info.needs, - "info": info, - } - for item, info in self.item_info.items() - if info.type == ItemType.Meal - } cooking_counter_equipments = { cooking_counter: [ @@ -161,13 +152,10 @@ class Environment: self.SYMBOL_TO_CHARACTER_MAP = { "#": Counter, "C": lambda pos: CuttingBoard( - pos, - { - info.needs[0]: {"seconds": info.seconds, "result": item} - for item, info in self.item_info.items() - if info.equipment is not None - and info.equipment.name == "CuttingBoard" - }, + pos=pos, + transitions=self.filter_item_info( + self.item_info, by_equipment_name="CuttingBoard" + ), ), "X": Trashcan, "W": lambda pos: ServingWindow( @@ -184,7 +172,9 @@ class Environment: "E": lambda pos: Dispenser(pos, self.item_info["Cheese"]), # chEEEEse "G": lambda pos: Dispenser(pos, self.item_info["Sausage"]), # sausaGe "P": lambda pos: PlateDispenser( - plate_transitions=plate_transitions, + plate_transitions=self.filter_item_info( + item_info=self.item_info, by_item_type=ItemType.Meal + ), pos=pos, dispensing=self.item_info["Plate"], plate_config=PlateConfig( @@ -205,15 +195,9 @@ class Environment: occupied_by=CookingEquipment( name="Pot", item_info=self.item_info["Pot"], - transitions={ - item: { - "seconds": info.seconds, - "needs": info.needs, - "info": info, - } - for item, info in self.item_info.items() - if info.equipment is not None and info.equipment.name == "Pot" - }, + transitions=self.filter_item_info( + self.item_info, by_equipment_name="Pot" + ), ), ), # Stove with pot: U because it looks like a pot "Q": lambda pos: CookingCounter( @@ -223,15 +207,9 @@ class Environment: occupied_by=CookingEquipment( name="Pan", item_info=self.item_info["Pan"], - transitions={ - item: { - "seconds": info.seconds, - "needs": info.needs, - "info": info, - } - for item, info in self.item_info.items() - if info.equipment is not None and info.equipment.name == "Pan" - }, + transitions=self.filter_item_info( + self.item_info, by_equipment_name="Pan" + ), ), ), # Stove with pan: Q because it looks like a pan "O": lambda pos: CookingCounter( @@ -241,15 +219,9 @@ class Environment: occupied_by=CookingEquipment( name="Peel", item_info=self.item_info["Peel"], - transitions={ - item: { - "seconds": info.seconds, - "needs": info.needs, - "info": info, - } - for item, info in self.item_info.items() - if info.equipment is not None and info.equipment.name == "Peel" - }, + transitions=self.filter_item_info( + self.item_info, by_equipment_name="Peel" + ), ), ), "F": lambda pos: CookingCounter( @@ -259,27 +231,18 @@ class Environment: occupied_by=CookingEquipment( name="Basket", item_info=self.item_info["Basket"], - transitions={ - item: { - "seconds": info.seconds, - "needs": info.needs, - "info": info, - } - for item, info in self.item_info.items() - if info.equipment is not None - and info.equipment.name == "Basket" - }, + transitions=self.filter_item_info( + self.item_info, by_equipment_name="Basket" + ), ), ), # Stove with pan: Q because it looks like a pan "B": lambda pos: Dispenser(pos, self.item_info["Bun"]), "M": lambda pos: Dispenser(pos, self.item_info["Meat"]), "S": lambda pos: Sink( pos, - transitions={ - info.needs[0]: {"seconds": info.seconds, "result": item} - for item, info in self.item_info.items() - if info.equipment is not None and info.equipment.name == "Sink" - }, + transitions=self.filter_item_info( + item_info=self.item_info, by_equipment_name="Sink" + ), ), "+": SinkAddon, } @@ -311,19 +274,19 @@ class Environment: """The relative env time when it will stop/end""" log.debug(f"End time: {self.env_time_end}") + @property + def game_ended(self) -> bool: + """Whether the game is over or not based on the calculated `Environment.env_time_end`""" + return self.env_time >= self.env_time_end + 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 - @property - def game_ended(self) -> bool: - """Whether the game is over or not based on the calculated `Environment.env_time_end`""" - return self.env_time >= self.env_time_end - def load_item_info(self, data) -> dict[str, ItemInfo]: - """Load `item_info.yml` if only the path is given, create ItemInfo classes and replace equipment strings with item infos.""" + """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos.""" if self.as_files: with open(data, "r") as file: item_lookup = yaml.safe_load(file) @@ -797,3 +760,25 @@ class Environment: """Reset the env time to the initial time, defined by `create_init_env_time`.""" self.env_time = create_init_env_time() log.debug(f"Reset env time to {self.env_time}") + + @staticmethod + def filter_item_info( + item_info: dict[str, ItemInfo], + by_item_type: ItemType = None, + by_equipment_name: str = None, + ) -> dict[str, ItemInfo]: + """Filter the item info dict by item type or equipment name""" + if by_item_type is not None: + return { + name: info + for name, info in item_info.items() + if info.type == by_item_type + } + if by_equipment_name is not None: + return { + name: info + for name, info in item_info.items() + if info.equipment is not None + and info.equipment.name == by_equipment_name + } + return item_info diff --git a/tests/test_start.py b/tests/test_start.py index 1f7c27bbd83190ada56e869f60ce863b614cb1b7..45cbf4dd6cb30fb1ef7693c6acc02f6032e2b2de 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -5,7 +5,7 @@ import pytest from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter, CuttingBoard -from overcooked_simulator.game_items import Item +from overcooked_simulator.game_items import Item, ItemInfo, ItemType from overcooked_simulator.overcooked_environment import ( Action, Environment, @@ -227,7 +227,15 @@ def test_processing(env_config, layout_config, item_info): counter_pos = np.array([2, 2]) counter = CuttingBoard( counter_pos, - transitions={"Tomato": {"seconds": 1, "result": "ChoppedTomato"}}, + transitions={ + "ChoppedTomato": ItemInfo( + name="ChoppedTomato", + seconds=0.5, + equipment=ItemInfo(name="CuttingBoard", type=ItemType.Equipment), + type=ItemType.Ingredient, + needs=["Tomato"], + ) + }, ) env.counters.append(counter)