diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 50d76412e0d235ef047dcc6b5ce5756ee0dcd209..6d1196bccf11a16ff9455bf694bb17cb1963cd2d 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -36,7 +36,7 @@ import dataclasses import logging from collections import deque 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 ( @@ -57,26 +57,6 @@ from overcooked_simulator.game_items import ( log = logging.getLogger(__name__) -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. @@ -172,11 +152,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): @@ -193,20 +174,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.""" @@ -353,7 +334,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) @@ -362,7 +343,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: @@ -505,17 +486,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): @@ -526,24 +515,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() - print(self.transitions[self.occupied_by[-1].name]["result"]) - 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 08e255e3eb139c98589c7ad681b82054bc2c2b75..babd1ec2713aa21ac84472137f339711e0b0932f 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -25,7 +25,7 @@ import dataclasses import datetime import logging from enum import Enum -from typing import Optional +from typing import Optional, TypedDict log = logging.getLogger(__name__) @@ -81,10 +81,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.""" @@ -135,10 +147,10 @@ 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.""" - 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.""" self.content_ready: Item | None = None @@ -151,7 +163,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 @@ -168,9 +180,7 @@ class CookingEquipment(Item): item.name for item in self.content_list + other ) print(ingredients) - 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 @@ -185,14 +195,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), } print(f"{self.name} {self.active_transition}, {self.content_list}") break @@ -246,9 +255,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/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index b60ed369dbeab16833d0a2dad499ec0bac84d59a..daa3a360b222b55b05d358f45a3bec635e079ccf 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -125,26 +125,14 @@ 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 - } 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( @@ -156,7 +144,9 @@ class Environment: "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]), "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]), "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( @@ -175,15 +165,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: Stove( @@ -191,26 +175,18 @@ 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 "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, } @@ -242,17 +218,17 @@ 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) -> dict[str, ItemInfo]: """Load `item_info.yml`, create ItemInfo classes and replace equipment strings with item infos.""" with open(self.item_info_path, "r") as file: @@ -708,3 +684,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 13ac792cf56ecf5f25d1c0b80e2c5417a7a597ab..ae27dd25a987c5e04c4e03114cc53b859b90095a 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -6,7 +6,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, @@ -228,7 +228,15 @@ def test_processing(): 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"], + ) + }, ) sim.env.counters.append(counter) @@ -248,7 +256,7 @@ def test_processing(): assert tomato.name != "ChoppedTomato", "Tomato is not finished yet." - time.sleep(1) + time.sleep(0.6) assert tomato.name == "ChoppedTomato", "Tomato should be finished."