diff --git a/overcooked_simulator/counter_factory.py b/overcooked_simulator/counter_factory.py index 02abcf47a6e4f1ce408bd673528934a96f127585..757cddfe27582ba50880a26f72fb8a624561e40c 100644 --- a/overcooked_simulator/counter_factory.py +++ b/overcooked_simulator/counter_factory.py @@ -50,7 +50,14 @@ from overcooked_simulator.counters import ( SinkAddon, Trashcan, ) -from overcooked_simulator.game_items import ItemInfo, ItemType, CookingEquipment, Plate +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 @@ -112,6 +119,7 @@ class CounterFactory: serving_window_additional_kwargs: dict[str, Any], plate_config: PlateConfig, order_and_score: OrderAndScoreManager, + effect_manager_config: dict, hook: Hooks, ) -> None: """Constructor for the `CounterFactory` class. Set up the attributes necessary to instantiate the counters. @@ -142,6 +150,8 @@ class CounterFactory: """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 @@ -190,13 +200,20 @@ class CounterFactory: name=item_info.name, item_info=item_info, transitions=self.filter_item_info( - by_equipment_name=item_info.name + 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: counter_class = self.counter_classes[self.layout_chars_config[c]] @@ -206,13 +223,14 @@ class CounterFactory: } if issubclass(counter_class, (CuttingBoard, Sink)): kwargs["transitions"] = self.filter_item_info( - by_equipment_name=counter_class.__name__ + 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 + by_item_type=ItemType.Meal, add_effects=True ), "plate_config": self.plate_config, "dispensing": self.item_info[Plate.__name__], @@ -249,21 +267,32 @@ class CounterFactory: 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: - return { + filtered = { name: info for name, info in self.item_info.items() if info.type == by_item_type } if by_equipment_name is not None: - return { + 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]): @@ -289,11 +318,33 @@ class CounterFactory: 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 - (1 * 0.05) <= np.linalg.norm( + 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, + **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.""" diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index aa721e0ba74e01b6f529b2bae3c441a20d58ec72..9bb481d3cc1d1444b9840cc42d8d5ac99b425d31 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -48,8 +48,6 @@ from overcooked_simulator.hooks import ( PRE_DISPENSER_PICK_UP, CUTTING_BOARD_PROGRESS, CUTTING_BOARD_100, - CUTTING_BOARD_START_INTERACT, - CUTTING_BOARD_END_INTERACT, PRE_COUNTER_PICK_UP, POST_COUNTER_PICK_UP, PRE_SERVING, @@ -58,14 +56,13 @@ from overcooked_simulator.hooks import ( DIRTY_PLATE_ARRIVES, TRASHCAN_USAGE, PLATE_CLEANED, - SINK_START_INTERACT, - SINK_END_INTERACT, ADDED_PLATE_TO_SINK, DROP_ON_SINK_ADDON, PICK_UP_FROM_SINK_ADDON, ) if TYPE_CHECKING: + from overcooked_simulator.effect_manager import Effect from overcooked_simulator.overcooked_environment import ( OrderAndScoreManager, ) @@ -78,6 +75,7 @@ from overcooked_simulator.game_items import ( CookingEquipment, Plate, ItemInfo, + EffectType, ) @@ -114,6 +112,8 @@ class Counter: """The position of the counter.""" self.occupied_by: Optional[Item] = occupied_by """What is on top of the counter, e.g., `Item`s.""" + self.active_effects: list[Effect] = [] + """The effects that currently affect the usage of the counter.""" self.hook = hook """Reference to the hook manager.""" self.orientation: npt.NDArray[float] = np.array([0, 1], dtype=float) @@ -198,19 +198,45 @@ class Counter: return self.occupied_by.combine(item) return None - def interact_start(self): - """Starts an interaction by the player. Nothing happens for the standard counter.""" - pass - - def interact_stop(self): - """Stops an interaction by the player. Nothing happens for the standard counter.""" - pass - def __repr__(self): return ( f"{self.__class__.__name__}(pos={self.pos},occupied_by={self.occupied_by})" ) + def do_tool_interaction(self, passed_time: timedelta, tool: Item): + successful = False + if self.occupied_by: + if isinstance(self.occupied_by, deque): + for item in self.occupied_by: + successful |= self._do_single_tool_interaction( + passed_time, tool, item + ) + else: + successful = self._do_single_tool_interaction( + passed_time, tool, self.occupied_by + ) + if not successful: + self._do_single_tool_interaction(passed_time, tool, self) + + def _do_single_tool_interaction( + self, passed_time: timedelta, tool: Item, target: Item | Counter + ) -> bool: + suitable_effects = [ + e for e in target.active_effects if e.name in tool.item_info.needs + ] + if suitable_effects: + effect = suitable_effects[0] + percent = passed_time.total_seconds() / tool.item_info.seconds + effect.progres_percentage += percent + if effect.progres_percentage > 1.0: + effect.item_info.manager.remove_active_effect(effect, target) + target.active_effects.remove(effect) + return True + return False + + def do_hand_free_interaction(self, passed_time: timedelta, now: datetime): + ... + def to_dict(self) -> dict: """For the state representation. Only the relevant attributes are put into the dict.""" return { @@ -226,6 +252,7 @@ class Counter: if isinstance(self.occupied_by, Iterable) else self.occupied_by.to_dict() ), + "active_effects": [e.to_dict() for e in self.active_effects], } @@ -242,8 +269,6 @@ class CuttingBoard(Counter): """ def __init__(self, transitions: dict[str, ItemInfo], **kwargs): - self.progressing: bool = False - """Is a player progressing/cutting on the board.""" self.transitions: dict[str, ItemInfo] = transitions """The allowed transitions to a new item. Keys are the resulting items and the `ItemInfo` (value) contains the needed items in the `need` attribute.""" @@ -254,7 +279,7 @@ class CuttingBoard(Counter): board.""" super().__init__(**kwargs) - def progress(self, passed_time: timedelta, now: datetime): + def do_hand_free_interaction(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression. Args: @@ -267,8 +292,15 @@ class CuttingBoard(Counter): """ if ( self.occupied - and self.progressing and self.occupied_by.name in self.inverted_transition_dict + and not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.occupied_by.active_effects + ) + and not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.active_effects + ) ): percent = ( passed_time.total_seconds() @@ -290,29 +322,6 @@ class CuttingBoard(Counter): ].name self.hook(CUTTING_BOARD_100, counter=self) - def start_progress(self): - """Starts the cutting process.""" - self.progressing = True - - def pause_progress(self): - """Pauses the cutting process""" - self.progressing = False - - def interact_start(self): - """Handles player interaction, starting to hold key down.""" - self.start_progress() - self.hook(CUTTING_BOARD_START_INTERACT, counter=self) - - def interact_stop(self): - """Handles player interaction, stopping to hold key down.""" - self.pause_progress() - self.hook(CUTTING_BOARD_END_INTERACT, counter=self) - - def to_dict(self) -> dict: - d = super().to_dict() - d.update((("progressing", self.progressing),)) - return d - class ServingWindow(Counter): """The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for @@ -358,6 +367,14 @@ class ServingWindow(Counter): return item def can_drop_off(self, item: Item) -> bool: + if any( + e.item_info.effect_type == EffectType.Unusable for e in self.active_effects + ): + return False + if any( + e.item_info.effect_type == EffectType.Unusable for e in item.active_effects + ): + return False return isinstance(item, CookingEquipment) and ( (item.content_ready is not None and item.content_ready.name in self.meals) or (len(item.content_list) == 1 and item.content_list[0].name in self.meals) @@ -582,6 +599,12 @@ class Trashcan(Counter): pass def drop_off(self, item: Item) -> Item | None: + if any( + e.item_info.effect_type == EffectType.Unusable for e in item.active_effects + ) or any( + e.item_info.effect_type == EffectType.Unusable for e in self.active_effects + ): + return item if isinstance(item, CookingEquipment): penalty = self.order_and_score.apply_penalty_for_using_trash( item.content_list @@ -634,6 +657,10 @@ class CookingCounter(Counter): and isinstance(self.occupied_by, CookingEquipment) and self.occupied_by.name in self.equipments and self.occupied_by.can_progress() + and not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.active_effects + ) ): self.occupied_by.progress(passed_time, now) @@ -666,8 +693,6 @@ class Sink(Counter): **kwargs, ): super().__init__(**kwargs) - self.progressing: bool = False - """If a player currently cleans a plate.""" self.sink_addon: SinkAddon = sink_addon """The connected sink addon which will receive the clean plates""" self.occupied_by: deque[Plate] = deque() @@ -688,12 +713,30 @@ class Sink(Counter): """If there is a plate in the sink.""" return len(self.occupied_by) != 0 - def progress(self, passed_time: timedelta, now: datetime): + def do_hand_free_interaction(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression""" if ( self.occupied - and self.progressing and self.occupied_by[-1].name in self.transition_needs + and not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.active_effects + ) + and not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.sink_addon.active_effects + ) + and not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.occupied_by[-1].active_effects + ) + and ( + not self.sink_addon.occupied_by + or not any( + e.item_info.effect_type == EffectType.Unusable + for e in self.sink_addon.occupied_by[-1].active_effects + ) + ) ): for name, info in self.transitions.items(): if info.needs[0] == self.occupied_by[-1].name: @@ -710,24 +753,6 @@ class Sink(Counter): self.sink_addon.add_clean_plate(plate) break - def start_progress(self): - """Starts the cutting process.""" - self.progressing = True - - def pause_progress(self): - """Pauses the cutting process""" - self.progressing = False - - def interact_start(self): - """Handles player interaction, starting to hold key down.""" - self.start_progress() - self.hook(SINK_START_INTERACT, counter=self) - - def interact_stop(self): - """Handles player interaction, stopping to hold key down.""" - self.pause_progress() - self.hook(SINK_END_INTERACT, counter=self) - def can_drop_off(self, item: Item) -> bool: return isinstance(item, Plate) and not item.clean @@ -743,11 +768,6 @@ class Sink(Counter): """Set the closest addon in post_setup.""" self.sink_addon = sink_addon - def to_dict(self) -> dict: - d = super().to_dict() - d.update((("progressing", self.progressing),)) - return d - class SinkAddon(Counter): """The counter on which the clean plates appear after cleaning them in the `Sink` diff --git a/overcooked_simulator/effect_manager.py b/overcooked_simulator/effect_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..905fef957bf5f200b84ab84c141681838d0a9241 --- /dev/null +++ b/overcooked_simulator/effect_manager.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import random +from collections import deque +from datetime import timedelta, datetime +from typing import TYPE_CHECKING, Tuple + +from overcooked_simulator.game_items import ( + ItemInfo, + Item, + ItemType, + Effect, + CookingEquipment, +) +from overcooked_simulator.hooks import Hooks +from overcooked_simulator.utils import get_touching_counters, find_item_on_counters + +if TYPE_CHECKING: + from overcooked_simulator.counters import Counter + + +class EffectManager: + def __init__(self, hook: Hooks): + self.effects = [] + self.counters = [] + self.hook = hook + self.new_effects: list[Tuple[Effect, Item | Counter]] = [] + + def add_effect(self, effect: ItemInfo): + self.effects.append(effect) + + def set_counters(self, counters: list[Counter]): + self.counters.extend(counters) + + def register_active_effect(self, effect: Effect, target: Item | Counter): + target.active_effects.append(effect) + self.new_effects.append((effect, target)) + + def progress(self, passed_time: timedelta, now: datetime): + ... + + def can_start_effect_transition( + self, effect: ItemInfo, target: Item | Counter + ) -> bool: + return effect.name not in [e.name for e in target.active_effects] + + def remove_active_effect(self, effect: Effect, target: Item | Counter): + ... + + +class FireEffectManager(EffectManager): + # TODO add Random object + + def __init__( + self, + spreading_duration: list[float], + fire_burns_ingredients_and_meals: bool, + **kwargs, + ): + super().__init__(**kwargs) + self.spreading_duration = spreading_duration + self.fire_burns_ingredients_and_meals = fire_burns_ingredients_and_meals + self.effect_to_timer: dict[str:datetime] = {} + self.next_finished_timer = datetime.max + self.active_effects: list[Tuple[Effect, Item | Counter]] = [] + + def progress(self, passed_time: timedelta, now: datetime): + if self.new_effects: + for effect, target in self.new_effects: + self.effect_to_timer[effect.uuid] = now + timedelta( + seconds=random.uniform(*self.spreading_duration) + ) + self.next_finished_timer = min( + self.next_finished_timer, self.effect_to_timer[effect.uuid] + ) + self.active_effects.append((effect, target)) + self.new_effects = [] + if self.next_finished_timer < now: + for effect, target in self.active_effects: + if self.effect_to_timer[effect.uuid] < now: + if isinstance(target, Item): + target = find_item_on_counters(target.uuid, self.counters) + if target: + touching = get_touching_counters(target, self.counters) + for counter in touching: + if counter.occupied_by: + if isinstance(counter.occupied_by, deque): + self.apply_effect(effect, counter.occupied_by[-1]) + else: + self.apply_effect(effect, counter.occupied_by) + else: + self.apply_effect(effect, counter) + self.effect_to_timer[effect.uuid] = now + timedelta( + seconds=random.uniform(*self.spreading_duration) + ) + if self.effect_to_timer: + self.next_finished_timer = min(self.effect_to_timer.values()) + else: + self.next_finished_timer = datetime.max + + def apply_effect(self, effect: Effect, target: Item | Counter): + if ( + isinstance(target, Item) + and target.item_info.type == ItemType.Tool + and effect.name in target.item_info.needs + ): + # Tools that reduce fire can not burn + return + if effect.name not in target.active_effects and target.uuid not in [ + t.uuid for _, t in self.active_effects + ]: + if isinstance(target, CookingEquipment): + if target.content_list: + for content in target.content_list: + self.burn_content(content) + if self.fire_burns_ingredients_and_meals: + self.burn_content(target.content_ready) + elif isinstance(target, Item): + self.burn_content(target) + self.register_active_effect( + Effect(effect.name, item_info=effect.item_info), target + ) + + def burn_content(self, content: Item): + if self.fire_burns_ingredients_and_meals and content: + if not content.name.startswith("Burnt"): + content.name = "Burnt" + content.name + + def remove_active_effect(self, effect: Effect, target: Item | Counter): + if (effect, target) in self.active_effects: + self.active_effects.remove((effect, target)) + if effect.uuid in self.effect_to_timer: + del self.effect_to_timer[effect.uuid] diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index c0de86d2afc35fad41a5c206b448a0f1eecdad6e..3cf44456a89edc420129101ff912b5ba4203c7d4 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -20,6 +20,7 @@ layout_chars: _: Free hash: Counter A: Agent + pipe: Extinguisher P: PlateDispenser C: CuttingBoard X: Trashcan @@ -41,6 +42,7 @@ layout_chars: B: Bun M: Meat + orders: order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py @@ -85,3 +87,11 @@ player_config: radius: 0.4 player_speed_units_per_seconds: 8 interaction_range: 1.6 + + +effect_manager: + FireManager: + class: !!python/name:overcooked_simulator.effect_manager.FireEffectManager '' + kwargs: + spreading_duration: [ 5, 10 ] + fire_burns_ingredients_and_meals: true \ No newline at end of file diff --git a/overcooked_simulator/game_content/item_info.yaml b/overcooked_simulator/game_content/item_info.yaml index a6458c6329cbf0323e6fb59338f34e0739c9786e..1266f61ebd611cd5c2a9097b1be9dd7eff65b7f7 100644 --- a/overcooked_simulator/game_content/item_info.yaml +++ b/overcooked_simulator/game_content/item_info.yaml @@ -176,3 +176,57 @@ Pizza: needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ] seconds: 7.0 equipment: Peel + +# -------------------------------------------------------------------------------- + +BurntCookedPatty: + type: Waste + seconds: 5.0 + needs: [ CookedPatty ] + equipment: Pan + +BurntChips: + type: Waste + seconds: 5.0 + needs: [ Chips ] + equipment: Basket + +BurntFriedFish: + type: Waste + seconds: 5.0 + needs: [ FriedFish ] + equipment: Basket + +BurntTomatoSoup: + type: Waste + needs: [ TomatoSoup ] + seconds: 6.0 + equipment: Pot + +BurntOnionSoup: + type: Waste + needs: [ OnionSoup ] + seconds: 6.0 + equipment: Pot + +BurntPizza: + type: Waste + needs: [ Pizza ] + seconds: 7.0 + equipment: Peel + +# -------------------------------------------------------------------------------- + +Fire: + type: Effect + seconds: 5.0 + needs: [ BurntCookedPatty, BurntChips, BurntFriedFish, BurntTomatoSoup, BurntOnionSoup, BurntPizza ] + manager: FireManager + effect_type: Unusable + +# -------------------------------------------------------------------------------- + +Extinguisher: + type: Tool + seconds: 1.0 + needs: [ Fire ] diff --git a/overcooked_simulator/game_content/item_info_debug.yaml b/overcooked_simulator/game_content/item_info_debug.yaml index c2282253e9539c9686cd6746f536e0316c3b21ec..fd871d1d5685df542317bf772b67f3a3fd8ed7d8 100644 --- a/overcooked_simulator/game_content/item_info_debug.yaml +++ b/overcooked_simulator/game_content/item_info_debug.yaml @@ -177,3 +177,57 @@ Pizza: needs: [ PizzaBase, ChoppedTomato, GratedCheese, ChoppedSausage ] seconds: 0.1 equipment: Peel + +# -------------------------------------------------------------------------------- + +BurntCookedPatty: + type: Waste + seconds: 5.0 + needs: [ CookedPatty ] + equipment: Pan + +BurntChips: + type: Waste + seconds: 1.0 + needs: [ Chips ] + equipment: Basket + +BurntFriedFish: + type: Waste + seconds: 5.0 + needs: [ FriedFish ] + equipment: Basket + +BurntTomatoSoup: + type: Waste + needs: [ TomatoSoup ] + seconds: 6.0 + equipment: Pot + +BurntOnionSoup: + type: Waste + needs: [ OnionSoup ] + seconds: 6.0 + equipment: Pot + +BurntPizza: + type: Waste + needs: [ Pizza ] + seconds: 7.0 + equipment: Peel + +# -------------------------------------------------------------------------------- + +Fire: + type: Effect + seconds: 1.0 + needs: [ BurntCookedPatty, BurntChips, BurntFriedFish, BurntTomatoSoup, BurntOnionSoup, BurntPizza ] + manager: FireManager + effect_type: Unusable + +# -------------------------------------------------------------------------------- + +Extinguisher: + type: Tool + seconds: 0.1 + needs: [ Fire ] \ No newline at end of file diff --git a/overcooked_simulator/game_content/layouts/basic.layout b/overcooked_simulator/game_content/layouts/basic.layout index ccc4076303e985a8b60c9f2dd091f323b5d6e7a6..94e5fb1dc055cefacfa6ef562ad1fe04424e726b 100644 --- a/overcooked_simulator/game_content/layouts/basic.layout +++ b/overcooked_simulator/game_content/layouts/basic.layout @@ -1,6 +1,6 @@ #QU#FO#TNLB# #__________M -#__________K +|__________K W__________I #__A_____A_D C__________E diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 9a406367ff71e802b5ba31e6d7bf2cd95304f062..0b7c6d6db4f6a687dd587bed7a0bdfb733ecc9c7 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -26,7 +26,10 @@ import datetime import logging import uuid from enum import Enum -from typing import Optional, TypedDict +from typing import Optional, TypedDict, TYPE_CHECKING + +if TYPE_CHECKING: + from overcooked_simulator.effect_manager import EffectManager log = logging.getLogger(__name__) """The logger for this module.""" @@ -38,6 +41,10 @@ COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment" """The string for the `category` value in the json state representation for all cooking equipments.""" +class EffectType(Enum): + Unusable = "Unusable" + + class ItemType(Enum): Ingredient = "Ingredient" """All ingredients and process ingredients.""" @@ -45,6 +52,12 @@ class ItemType(Enum): """All combined ingredients that can be served.""" Equipment = "Equipment" """All counters and cooking equipments.""" + Waste = "Waste" + """Burnt ingredients and meals.""" + Effect = "Effect" + """Does not change the item but the object attributes, like adding fire.""" + Tool = "Tool" + """Item that remains in hands in extends the interactive abilities of the player.""" @dataclasses.dataclass @@ -87,16 +100,24 @@ class ItemInfo: """The name of the item, is set automatically by the "group" name of the item.""" seconds: float = dataclasses.field(compare=False, default=0) """If progress is needed this argument defines how long it takes to complete the process in seconds.""" + + # TODO maybe as a lambda/based on Prefix? needs: list[str] = dataclasses.field(compare=False, default_factory=list) """The ingredients/items which are needed to create the item/start the progress.""" equipment: ItemInfo | None = dataclasses.field(compare=False, default=None) """On which the item can be created. `null`, `~` (None) converts to Plate.""" + manager: str | None | EffectManager = None + """The manager for the effect.""" + effect_type: None | EffectType = None + """How does the effect effect interaction, combine actions etc.""" recipe: collections.Counter | None = None """Internally set in CookingEquipment""" def __post_init__(self): self.type = ItemType(self.type) + if self.effect_type: + self.effect_type = EffectType(self.effect_type) class ActiveTransitionTypedDict(TypedDict): @@ -104,7 +125,7 @@ class ActiveTransitionTypedDict(TypedDict): seconds: int | float """The needed seconds to progress for the transition.""" - result: str + result: str | Item | Effect """The new name of the item after the transition.""" @@ -125,8 +146,12 @@ class Item: """The equipment with which the item was last progressed.""" self.progress_percentage: float = 0.0 """The current progress percentage of the item if it is progress-able.""" + self.inverse_progress: bool = False + """Whether the progress will produce waste.""" self.uuid: str = uuid.uuid4().hex if uid is None else uid """A unique identifier for the item. Useful for GUIs that handles specific asset instances.""" + self.active_effects: list[Effect] = [] + """The effects that affect the item.""" def __repr__(self): if self.progress_equipment is None: @@ -167,6 +192,7 @@ class Item: """Reset the progress.""" self.progress_equipment = None self.progress_percentage = 0.0 + self.inverse_progress = False def to_dict(self) -> dict: """For the state representation. Only the relevant attributes are put into the dict.""" @@ -175,6 +201,8 @@ class Item: "category": self.item_category, "type": self.name, "progress_percentage": self.progress_percentage, + "inverse_progress": self.inverse_progress, + "active_effects": [e.to_dict() for e in self.active_effects], } @@ -209,6 +237,12 @@ class CookingEquipment(Item): if other is None: return False + if any( + e.item_info.effect_type == EffectType.Unusable for e in other.active_effects + ) or any( + e.item_info.effect_type == EffectType.Unusable for e in self.active_effects + ): + return False if isinstance(other, CookingEquipment): other = other.content_list else: @@ -231,34 +265,66 @@ class CookingEquipment(Item): else: self.content_list.append(other) - ingredients = collections.Counter(item.name for item in self.content_list) - for result, transition in self.transitions.items(): - 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), - } - break - else: - self.content_ready = None + self.check_active_transition() return return_value def can_progress(self) -> bool: """Check if the cooking equipment can progress items at all.""" - return self.active_transition is not None + return self.active_transition is not None and not any( + e.item_info.effect_type == EffectType.Unusable for e in self.active_effects + ) def progress(self, passed_time: datetime.timedelta, now: datetime.datetime): percent = passed_time.total_seconds() / self.active_transition["seconds"] super().progress(equipment=self.name, percent=percent) if self.progress_percentage == 1.0: - self.content_list = [self.active_transition["result"]] + if isinstance(self.active_transition["result"], Effect): + self.active_transition[ + "result" + ].item_info.manager.register_active_effect( + self.active_transition["result"], self + ) + else: + self.content_list = [self.active_transition["result"]] self.reset() + self.check_active_transition() # todo set active transition for fire/burnt? + def check_active_transition(self): + ingredients = collections.Counter(item.name for item in self.content_list) + for result, transition in self.transitions.items(): + if transition.type == ItemType.Effect: + if set(ingredients.keys()).issubset( + transition.needs + ) and transition.manager.can_start_effect_transition(transition, self): + if transition.seconds == 0: + transition.manager.register_active_effect( + Effect(name=transition.name, item_info=transition), self + ) + else: + self.active_transition = { + "seconds": transition.seconds, + "result": Effect( + name=transition.name, item_info=transition + ), + } + self.inverse_progress = True + break # ? + else: + 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), + } + self.inverse_progress = transition.type == ItemType.Waste + break + else: + self.content_ready = None + def reset_content(self): """Reset the content attributes after the content was picked up from the equipment.""" self.content_list = [] @@ -340,3 +406,19 @@ class Plate(CookingEquipment): elif self.clean: return True return False + + +class Effect: + def __init__(self, name: str, item_info: ItemInfo, uid: str = None): + self.uuid: str = uuid.uuid4().hex if uid is None else uid + self.name = name + self.item_info = item_info + self.progres_percentage = 0.0 + + def to_dict(self) -> dict: + return { + "id": self.uuid, + "type": self.name, + "progress_percentage": self.progres_percentage, + "inverse_progress": True, + } diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py index 3fb6d9efc48701ce843629ef4400887b21e828d3..1b5753a97afb85a6f612950bc12cdac17dbf26df 100644 --- a/overcooked_simulator/gui_2d_vis/drawing.py +++ b/overcooked_simulator/gui_2d_vis/drawing.py @@ -16,6 +16,7 @@ from overcooked_simulator.state_representation import ( PlayerState, CookingEquipmentState, ItemState, + EffectState, ) USE_PLAYER_COOK_SPRITES = True @@ -29,6 +30,21 @@ def calc_angle(vec_a: list[float], vec_b: list[float]) -> float: return a.angle_to(b) +def grayscale(img): + arr = pygame.surfarray.pixels3d(img) + mean_arr = np.dot(arr[:, :, :], [0.216, 0.587, 0.144]) + mean_arr3d = mean_arr[..., np.newaxis] + new_arr = np.repeat(mean_arr3d[:, :, :], 3, axis=2) + new_arr = new_arr.astype(np.int8) + surface = pygame.Surface(new_arr.shape[0:2], pygame.SRCALPHA, 32) + + # Copy the rgb part of array to the new surface. + pygame.pixelcopy.array_to_surface(surface, new_arr) + surface_alpha = np.array(surface.get_view("A"), copy=False) + surface_alpha[:, :] = pygame.surfarray.pixels_alpha(img) + return surface + + def create_polygon(n, length): if n == 1: return np.array([0, 0]) @@ -62,6 +78,9 @@ class Visualizer: self.player_colors = [] self.config = config + self.fire_state = 0 + self.fire_time_steps = 8 + def create_player_colors(self, n) -> None: """Create different colors for the players. The color hues are sampled uniformly in HSV-Space, then the corresponding colors from the defined colors list are looked up. @@ -149,6 +168,7 @@ class Visualizer: size: float, pos: npt.NDArray, rot_angle=0, + burnt: bool = False, ): """Draws an image on the given screen. @@ -160,14 +180,24 @@ class Visualizer: rot_angle: Optional angle to rotate the image around. """ cache_entry = f"{img_path}" - if cache_entry in self.image_cache_dict.keys(): - image = self.image_cache_dict[cache_entry] + if cache_entry + ("-burnt" if burnt else "") in self.image_cache_dict: + image = self.image_cache_dict[cache_entry + ("-burnt" if burnt else "")] else: - image = pygame.image.load( - ROOT_DIR / "gui_2d_vis" / img_path - ).convert_alpha() - self.image_cache_dict[cache_entry] = image - + if burnt: + if cache_entry in self.image_cache_dict: + normal_image = self.image_cache_dict[cache_entry] + else: + normal_image = pygame.image.load( + ROOT_DIR / "gui_2d_vis" / img_path + ).convert_alpha() + self.image_cache_dict[cache_entry] = normal_image + image = grayscale(normal_image) + self.image_cache_dict[cache_entry + "-burnt"] = image + else: + image = pygame.image.load( + ROOT_DIR / "gui_2d_vis" / img_path + ).convert_alpha() + self.image_cache_dict[cache_entry] = image image = pygame.transform.scale(image, (size, size)) if rot_angle != 0: image = pygame.transform.rotate(image, rot_angle) @@ -279,6 +309,7 @@ class Visualizer: grid_size: float, parts: list[dict[str]], scale: float = 1.0, + burnt: bool = False, orientation: list[float] | None = None, ): """Draws an item, based on its visual parts specified in the visualization config. @@ -316,6 +347,7 @@ class Visualizer: part["path"], part["size"] * scale * grid_size, draw_pos, + burnt=burnt, rot_angle=angle, ) @@ -349,7 +381,7 @@ class Visualizer: self, pos: npt.NDArray[float] | list[float], grid_size: float, - item: ItemState | CookingEquipmentState, + item: ItemState | CookingEquipmentState | EffectState, scale: float = 1.0, plate=False, screen=None, @@ -367,34 +399,64 @@ class Visualizer: plate: item is on a plate (soup are is different on a plate and pot) """ - if not isinstance(item, list): # can we remove this check? - if item["type"] in self.config: + if not isinstance(item, list): # can we remove this check?w + if item["type"] in self.config or ( + item["type"].startswith("Burnt") + and item["type"].replace("Burnt", "") in self.config + ): item_key = item["type"] if "Soup" in item_key and plate: item_key += "Plate" + if item_key.startswith("Burnt"): + item_key = item_key.replace("Burnt", "") + + if item_key == "Fire": + item_key = ( + f"{item_key}{int(self.fire_state/self.fire_time_steps)+1}" + ) + self.draw_thing( pos=pos, parts=self.config[item_key]["parts"], scale=scale, screen=screen, grid_size=grid_size, + burnt=item["type"].startswith("Burnt"), ) if "progress_percentage" in item and item["progress_percentage"] > 0.0: + if item["inverse_progress"]: + percentage = 1 - item["progress_percentage"] + else: + percentage = item["progress_percentage"] self.draw_progress_bar( - screen, pos, item["progress_percentage"], grid_size=grid_size + screen, + pos, + percentage, + grid_size=grid_size, + attention=item["inverse_progress"], ) if ( "content_ready" in item and item["content_ready"] - and item["content_ready"]["type"] in self.config + and ( + item["content_ready"]["type"] in self.config + or ( + item["content_ready"]["type"].startswith("Burnt") + and item["content_ready"]["type"].replace("Burnt", "") + in self.config + ) + ) ): self.draw_thing( pos=pos, - parts=self.config[item["content_ready"]["type"]]["parts"], + parts=self.config[item["content_ready"]["type"].replace("Burnt", "")][ + "parts" + ], screen=screen, grid_size=grid_size, + burnt=item["type"].startswith("Burnt"), ) elif "content_list" in item and item["content_list"]: triangle_offsets = create_polygon(len(item["content_list"]), length=10) @@ -408,6 +470,9 @@ class Visualizer: screen=screen, grid_size=grid_size, ) + if "active_effects" in item and item["active_effects"]: + for effect in item["active_effects"]: + self.draw_item(pos=pos, item=effect, screen=screen, grid_size=grid_size) @staticmethod def draw_progress_bar( @@ -415,6 +480,7 @@ class Visualizer: pos: npt.NDArray[float], percent: float, grid_size: float, + attention: bool = False, ): """Visualize progress of progressing item as a green bar under the item. @@ -434,7 +500,7 @@ class Visualizer: progress_width, bar_height, ) - pygame.draw.rect(screen, colors["green1"], progress_bar) + pygame.draw.rect(screen, colors["red" if attention else "green1"], progress_bar) def draw_counter( self, screen: pygame.Surface, counter_dict: dict, grid_size: float @@ -526,6 +592,8 @@ class Visualizer: counters: The counter state returned by the environment. grid_size: Scaling of the object given in pixels. """ + global FIRE_STATE + for counter in counters: self.draw_counter(screen, counter, grid_size) @@ -558,6 +626,14 @@ class Visualizer: pos=item_pos * grid_size + (grid_size / 2), item_scale=item_scale, ) + if counter["active_effects"]: + for effect in counter["active_effects"]: + self.draw_item( + pos=np.array(counter["pos"]) * grid_size + (grid_size / 2), + grid_size=grid_size, + screen=screen, + item=effect, + ) if SHOW_COUNTER_CENTERS: pos = np.array(counter["pos"]) * grid_size @@ -580,6 +656,8 @@ class Visualizer: ), ) + self.fire_state = (self.fire_state + 1) % (3 * self.fire_time_steps) + def draw_orders( self, screen: pygame.surface, @@ -656,6 +734,7 @@ class Visualizer: percent=percentage, screen=order_screen, grid_size=grid_size, + attention=percentage < 0.25, ) orders_rect = order_screen.get_rect() diff --git a/overcooked_simulator/gui_2d_vis/images/fire.png b/overcooked_simulator/gui_2d_vis/images/fire.png new file mode 100644 index 0000000000000000000000000000000000000000..4d2e5237cbd6125eaeaa7781075a7dc382ab904e Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/fire2.png b/overcooked_simulator/gui_2d_vis/images/fire2.png new file mode 100644 index 0000000000000000000000000000000000000000..1f28ad6e87d7e5985a1dbf9d72d13643a7936b00 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire2.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/fire3.png b/overcooked_simulator/gui_2d_vis/images/fire3.png new file mode 100644 index 0000000000000000000000000000000000000000..65b883b7d5663f7c8f99032c9939e3f4479e03f0 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire3.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/fire_extinguisher.png b/overcooked_simulator/gui_2d_vis/images/fire_extinguisher.png new file mode 100644 index 0000000000000000000000000000000000000000..a03d2d39285c6e286df992826af27c4f8ffc8d16 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/fire_extinguisher.png differ diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index 7922bc2fedf5de237a620e01ef0ad2496e942da9..7d544fe2cb95b1e8f9e2421d80981b2d68774fec 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -453,7 +453,7 @@ class PyGameGUI: def setup_environment(self): environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml" layout_path = self.layout_file_paths[self.layout_selection.selected_option] - item_info_path = ROOT_DIR / "game_content" / "item_info_debug.yaml" + item_info_path = ROOT_DIR / "game_content" / "item_info.yaml" with open(item_info_path, "r") as file: item_info = file.read() with open(layout_path, "r") as file: diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml index afc3973770c62301432582d412450c5355f344ba..870fe279b7eabccf8b63ee98e6e2c5485eb11ef9 100644 --- a/overcooked_simulator/gui_2d_vis/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -138,6 +138,40 @@ SinkAddon: size: 0.75 center_offset: [ 0, -0.05 ] +# Tools +Extinguisher: + parts: + - type: image + path: images/fire_extinguisher.png + size: 0.85 + center_offset: [ 0, -0.05 ] + +# Effects +Fire: + parts: + - type: image + path: images/fire.png + size: 1 + +Fire1: + parts: + - type: image + path: images/fire.png + size: 1.0 + +Fire2: + parts: + - type: image + path: images/fire2.png + size: 1.0 + +Fire3: + parts: + - type: image + path: images/fire3.png + size: 1.0 + + # Items Tomato: parts: diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index c3e84b3b9b7c746c3d87799940f7b5b0f89fb11e..96b4949fc322887526d927564cc1543176fc35b0 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -9,7 +9,7 @@ import sys from datetime import timedelta, datetime from enum import Enum from pathlib import Path -from typing import Literal, TypedDict, Callable +from typing import Literal, TypedDict, Callable, Tuple import numpy as np import numpy.typing as npt @@ -20,6 +20,7 @@ from overcooked_simulator.counters import ( Counter, PlateConfig, ) +from overcooked_simulator.effect_manager import EffectManager from overcooked_simulator.game_items import ( ItemInfo, ItemType, @@ -107,6 +108,7 @@ class EnvironmentConfig(TypedDict): orders: OrderConfig player_config: PlayerConfig layout_chars: dict[str, str] + effect_manager: dict class Environment: @@ -197,6 +199,7 @@ class Environment: ) ), order_and_score=self.order_and_score, + effect_manager_config=self.environment_config["effect_manager"], hook=self.hook, ) @@ -240,6 +243,10 @@ class Environment: """The relative env time when it will stop/end""" log.debug(f"End time: {self.env_time_end}") + self.effect_manager: dict[ + str, EffectManager + ] = self.counter_factory.setup_effect_manger(self.counters) + self.hook(ENV_INITIALIZED) @property @@ -325,7 +332,9 @@ class Environment: # TODO add colors for ingredients, equipment and meals # plt.show() - def parse_layout_file(self): + def parse_layout_file( + self, + ) -> Tuple[list[Counter], list[npt.NDArray], list[npt.NDArray]]: """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 @@ -387,9 +396,6 @@ class Environment: return counters, designated_player_positions, free_positions def determine_counter_orientations(self, counters, grid, kitchen_center): - for l in grid: - print(l) - grid = np.array(grid).T grid_width = grid.shape[0] @@ -469,16 +475,14 @@ class Environment: self.hook(ACTION_PUT, action=action, counter=counter) elif action.action_type == ActionType.INTERACT: if action.action_data == InterActionData.START: - player.perform_interact_hold_start(counter) - player.last_interacted_counter = counter + player.perform_interact_start(counter) self.hook(ACTION_INTERACT_START, action=action, counter=counter) else: self.hook( ACTION_ON_NOT_REACHABLE_COUNTER, action=action, counter=counter ) if action.action_data == InterActionData.STOP: - if player.last_interacted_counter: - player.perform_interact_hold_stop(player.last_interacted_counter) + player.perform_interact_stop() self.hook(POST_PERFORM_ACTION, action=action) @@ -713,13 +717,15 @@ class Environment: self.hook(GAME_ENDED_STEP) else: for player in self.players.values(): + player.progress(passed_time, self.env_time) if self.env_time <= player.movement_until: self.perform_movement(player, passed_time) for counter in self.progressing_counters: counter.progress(passed_time=passed_time, now=self.env_time) self.order_and_score.progress(passed_time=passed_time, now=self.env_time) - + for effect_manager in self.effect_manager.values(): + effect_manager.progress(passed_time=passed_time, now=self.env_time) # self.hook(POST_STEP, passed_time=passed_time) def get_state(self): diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 0507e279ab80b765ead610623cd29991e37b0332..e2284ae0658f82064cfb690dc6bb0629fcb6ae21 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -7,16 +7,16 @@ holding object**. If so, it picks up the content and combines it on its hands. """ import dataclasses -import datetime import logging from collections import deque +from datetime import datetime, timedelta from typing import Optional import numpy as np import numpy.typing as npt from overcooked_simulator.counters import Counter -from overcooked_simulator.game_items import Item, Plate +from overcooked_simulator.game_items import Item, Plate, ItemType from overcooked_simulator.state_representation import PlayerState log = logging.getLogger(__name__) @@ -81,9 +81,11 @@ class Player: self.current_movement: npt.NDArray[float] = np.zeros(2, float) """The movement vector that will be used to calculate the movement in the next step call.""" - self.movement_until: datetime.datetime = datetime.datetime.min + self.movement_until: datetime = datetime.min """The env time until the player wants to move.""" + self.interacting: bool = False + def set_movement(self, move_vector, move_until): """Called by the `perform_action` method. Movements will be performed (pos will be updated) in the `step` function of the environment""" @@ -97,6 +99,8 @@ class Player: Args: movement: 2D-Vector of length 1 """ + if self.interacting and np.any(movement): + self.perform_interact_stop() self.pos += movement if np.linalg.norm(movement) != 0: self.turn(movement) @@ -164,25 +168,36 @@ class Player: if isinstance(self.holding, Plate): log.debug(self.holding.clean) - @staticmethod - def perform_interact_hold_start(counter: Counter): + def perform_interact_start(self, counter: Counter): """Starts an interaction with the counter. Should be called for a keydown event, for holding down a key on the keyboard. Args: counter: The counter to start the interaction with. """ - counter.interact_start() + self.interacting = True + self.last_interacted_counter = counter - @staticmethod - def perform_interact_hold_stop(counter: Counter): + def perform_interact_stop(self): """Stops an interaction with the counter. Should be called for a keyup event, for letting go of a keyboard key. Args: counter: The counter to stop the interaction with. """ - counter.interact_stop() + self.interacting = False + self.last_interacted_counter = None + + def progress(self, passed_time: timedelta, now: datetime): + if self.interacting and self.last_interacted_counter: + # TODO only interact on counter (Sink/CuttingBoard) if hands are free configure in config? + if self.holding: + if self.holding.item_info.type == ItemType.Tool: + self.last_interacted_counter.do_tool_interaction( + passed_time, self.holding + ) + else: + self.last_interacted_counter.do_hand_free_interaction(passed_time, now) def __repr__(self): return f"Player(name:{self.name},pos:{str(self.pos)},holds:{self.holding})" diff --git a/overcooked_simulator/state_representation.py b/overcooked_simulator/state_representation.py index c57c30c0812c003d8008b9347a03a2c96b00b39c..a35f8cd0577e0c231d63e55bcec080c46d39385b 100644 --- a/overcooked_simulator/state_representation.py +++ b/overcooked_simulator/state_representation.py @@ -15,11 +15,20 @@ class OrderState(TypedDict): max_duration: float +class EffectState(TypedDict): + id: str + type: str + progress_percentage: float | int + inverse_progress: bool + + class ItemState(TypedDict): id: str category: Literal["Item"] | Literal["ItemCookingEquipment"] type: str progress_percentage: float | int + inverse_progress: bool + active_effects: list[EffectState] # add ItemType Meal ? @@ -37,14 +46,10 @@ class CounterState(TypedDict): occupied_by: None | list[ ItemState | CookingEquipmentState ] | ItemState | CookingEquipmentState + active_effects: list[EffectState] # list[ItemState] -> type in ["Sink", "PlateDispenser"] -class CuttingBoardAndSinkState(TypedDict): - type: Literal["CuttingBoard"] | Literal["Sink"] - progressing: bool - - class PlayerState(TypedDict): id: str pos: list[float] diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py index 2754f08912d31ff513ed891646d29571800e8960..8f22d82daee96902a56799aad905d8996a0ea15f 100644 --- a/overcooked_simulator/utils.py +++ b/overcooked_simulator/utils.py @@ -1,20 +1,25 @@ """ Some utility functions. """ +from __future__ import annotations import logging import os import sys import uuid +from collections import deque from datetime import datetime from enum import Enum +from typing import TYPE_CHECKING import numpy as np import numpy.typing as npt from scipy.spatial import distance_matrix from overcooked_simulator import ROOT_DIR -from overcooked_simulator.counters import Counter + +if TYPE_CHECKING: + from overcooked_simulator.counters import Counter def create_init_env_time(): @@ -39,6 +44,26 @@ def get_closest(point: npt.NDArray[float], counters: list[Counter]): ] +def get_touching_counters(target: Counter, counters: list[Counter]) -> list[Counter]: + return list( + filter( + lambda counter: np.linalg.norm(counter.pos - target.pos) == 1.0, counters + ) + ) + + +def find_item_on_counters(item_uuid: str, counters: list[Counter]) -> Counter | None: + for counter in counters: + if counter.occupied_by: + if isinstance(counter.occupied_by, deque): + for item in counter.occupied_by: + if item.uuid == item_uuid: + return counter + else: + if item_uuid == counter.occupied_by.uuid: + return counter + + def custom_asdict_factory(data): """Convert enums to their value.""" diff --git a/tests/test_start.py b/tests/test_start.py index b3f0b7eaff95c49543dc592a482b409a0f7cfe00..86b0d10395229378b7bd4a31a6707ca2bda6e1bc 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -240,7 +240,6 @@ def test_processing(env_config, layout_config, item_info): }, ) env.counters.append(counter) - env.progressing_counters.append(counter) tomato = Item(name="Tomato", item_info=None) env.add_player("1", np.array([2, 3]))