diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 616ad6949e266bf9a00207525ab8334054751c95..41bd67eeb5c10e23ea15538dd90fe9ca1afce515 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -3,24 +3,21 @@ from __future__ import annotations import logging from collections import deque from datetime import datetime, timedelta -from typing import TYPE_CHECKING, Optional - -from overcooked_simulator.utils import create_init_env_time +from typing import TYPE_CHECKING, Optional, Callable if TYPE_CHECKING: from overcooked_simulator.overcooked_environment import ( - GameScore, + OrderAndScoreManager, ) import numpy as np import numpy.typing as npt from overcooked_simulator.game_items import ( - CuttableItem, Item, CookingEquipment, - Meal, Plate, + ItemInfo, ) @@ -34,6 +31,10 @@ class Counter: self.pos: npt.NDArray[float] = pos self.occupied_by: Optional[Item] = occupied_by + @property + def occupied(self): + return self.occupied_by is not None + def pick_up(self, on_hands: bool = True): """Gets called upon a player performing the pickup action. If the counter can give something to the player, it does so. In the standard counter this is when an item is on the counter. @@ -95,15 +96,30 @@ class Counter: class CuttingBoard(Counter): - def __init__(self, pos: np.ndarray): + def __init__(self, pos: np.ndarray, transitions: dict): self.progressing = False + self.transitions = transitions super().__init__(pos) def progress(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression""" - if self.progressing: - if isinstance(self.occupied_by, CuttableItem): - self.occupied_by.progress() + if ( + self.occupied + and self.progressing + and self.occupied_by.name in self.transitions + ): + percent = ( + passed_time.total_seconds() + / self.transitions[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" + ] def start_progress(self): """Starts the cutting process.""" @@ -124,37 +140,32 @@ class CuttingBoard(Counter): class ServingWindow(Counter): def __init__( - self, pos, game_score: GameScore, plate_dispenser: PlateDispenser = None + self, + pos, + order_and_score: OrderAndScoreManager, + meals: set[str], + env_time_func: Callable[[], datetime], + plate_dispenser: PlateDispenser = None, ): - self.game_score = game_score + self.order_and_score = order_and_score self.plate_dispenser = plate_dispenser + self.meals = meals + self.env_time_func = env_time_func super().__init__(pos) def drop_off(self, item) -> Item | None: - reward = 5 - log.debug(f"Drop off item {item}") - # TODO define rewards - self.game_score.increment_score(reward) - if self.plate_dispenser is not None: - self.plate_dispenser.update_plate_out_of_kitchen() - return None - - def can_score(self, item): - if ( - isinstance(item, CookingEquipment) - and "Plate" in item.name - and item.content is not None - ): - if isinstance(item.content, Meal) and item.content.progressed_steps: - return item.content.finished - if not item.content.item_info.steps_needed and len( - item.content.item_info.needs - ) == len(item.content.parts): - return True - return False + env_time = self.env_time_func() + if self.order_and_score.serve_meal(item=item, env_time=env_time): + if self.plate_dispenser is not None: + self.plate_dispenser.update_plate_out_of_kitchen(env_time=env_time) + return None + return item def can_drop_off(self, item: Item) -> bool: - return self.can_score(item) + 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) + ) def pick_up(self, on_hands: bool = True): pass @@ -164,16 +175,16 @@ class ServingWindow(Counter): class Dispenser(Counter): - def __init__(self, pos, dispensing): + def __init__(self, pos, dispensing: ItemInfo): self.dispensing = dispensing super().__init__( pos, - self.dispensing.create_item(), + self.create_item(), ) def pick_up(self, on_hands: bool = True): return_this = self.occupied_by - self.occupied_by = self.dispensing.create_item() + self.occupied_by = self.create_item() return return_this def drop_off(self, item: Item) -> Item | None: @@ -186,17 +197,26 @@ class Dispenser(Counter): def __repr__(self): return f"{self.dispensing.name}Dispenser" + def create_item(self): + kwargs = { + "name": self.dispensing.name, + "item_info": self.dispensing, + } + return Item(**kwargs) + class PlateDispenser(Counter): - def __init__(self, pos, dispensing, plate_config): + def __init__( + self, pos, dispensing, plate_config, plate_transitions, **kwargs + ) -> None: + super().__init__(pos, **kwargs) self.dispensing = dispensing - super().__init__(pos) self.occupied_by = deque() self.out_of_kitchen_timer = [] self.plate_config = {"plate_delay": [5, 10]} self.plate_config.update(plate_config) self.next_plate_time = datetime.max - self.env_time = create_init_env_time() # is overwritten in progress anyway + self.plate_transitions = plate_transitions self.setup_plates() def pick_up(self, on_hands: bool = True): @@ -222,12 +242,12 @@ class PlateDispenser(Counter): return None def add_dirty_plate(self): - self.occupied_by.appendleft(self.dispensing.create_item()) + self.occupied_by.appendleft(self.create_item()) - def update_plate_out_of_kitchen(self): + def update_plate_out_of_kitchen(self, env_time: datetime): """Is called from the serving window to add a plate out of kitchen.""" # not perfect identical to datetime.now but based on framerate enough. - time_plate_to_add = self.env_time + timedelta( + time_plate_to_add = env_time + timedelta( seconds=np.random.uniform( low=self.plate_config["plate_delay"][0], high=self.plate_config["plate_delay"][1], @@ -242,22 +262,18 @@ class PlateDispenser(Counter): """Create plates based on the config. Clean and dirty ones.""" if "dirty_plates" in self.plate_config: self.occupied_by.extend( - [ - self.dispensing.create_item() - for _ in range(self.plate_config["dirty_plates"]) - ] + [self.create_item() for _ in range(self.plate_config["dirty_plates"])] ) if "clean_plates" in self.plate_config: self.occupied_by.extend( [ - self.dispensing.create_item(clean_plate=True) + self.create_item(clean=True) for _ in range(self.plate_config["clean_plates"]) ] ) def progress(self, passed_time: timedelta, now: datetime): """Check if plates arrive from outside the kitchen and add a dirty plate accordingly""" - self.env_time = now if self.next_plate_time < now: idx_delete = [] for i, times in enumerate(self.out_of_kitchen_timer): @@ -276,6 +292,14 @@ class PlateDispenser(Counter): def __repr__(self): return "PlateReturn" + def create_item(self, clean: bool = False): + kwargs = { + "clean": clean, + "transitions": self.plate_transitions, + "item_info": self.dispensing, + } + return Plate(**kwargs) + class Trash(Counter): def pick_up(self, on_hands: bool = True): @@ -283,7 +307,7 @@ class Trash(Counter): def drop_off(self, item: Item) -> Item | None: if isinstance(item, CookingEquipment): - item.content = None + item.reset_content() return item return None @@ -305,27 +329,44 @@ class Stove(Counter): and isinstance(self.occupied_by, CookingEquipment) and self.occupied_by.can_progress() ): - self.occupied_by.progress() + self.occupied_by.progress(passed_time, now) class Sink(Counter): - def __init__(self, pos, sink_addon=None): + def __init__(self, pos, transitions, sink_addon=None): super().__init__(pos) self.progressing = False self.sink_addon: SinkAddon = sink_addon self.occupied_by = deque() + self.transitions = transitions + + @property + def occupied(self): + return len(self.occupied_by) != 0 def progress(self, passed_time: timedelta, now: datetime): """Called by environment step function for time progression""" - if self.progressing: - if self.occupied_by: - self.occupied_by[-1].progress() - if self.occupied_by[-1].finished: - plate = self.occupied_by.pop() - if not self.occupied_by: - self.pause_progress() - plate.finished_call() - self.sink_addon.add_clean_plate(plate) + if ( + self.occupied + and self.progressing + and self.occupied_by[-1].name in self.transitions + ): + 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) def start_progress(self): """Starts the cutting process.""" @@ -360,7 +401,7 @@ class Sink(Counter): class SinkAddon(Counter): def __init__(self, pos, occupied_by=None): super().__init__(pos) - self.occupied_by = deque(occupied_by) if occupied_by else deque() + self.occupied_by = deque([occupied_by]) if occupied_by else deque() def can_drop_off(self, item: Item) -> bool: return self.occupied_by and self.occupied_by[-1].can_combine(item) diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index 158a22290759fa0415806ee59ce968972e07029c..b6b7c579ec950edd189c41e2ec09a5cbda2a239f 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -1,4 +1,63 @@ plates: - clean_plates: 3 + clean_plates: 1 dirty_plates: 2 - plate_delay: [ 5, 10 ] \ No newline at end of file + plate_delay: [ 5, 10 ] + # seconds until the dirty plate arrives. + +game: + time_limit_seconds: 180 + +meals: + all: false + # if all: false -> only orders for these meals are generated + # TODO: what if this list is empty? + list: + - TomatoSoup + - OnionSoup + - Salad + +orders: + kwargs: + duration_sample: + # how long should the orders be alive + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 40 + b: 60 + max_orders: 6 + # maximum number of active orders at the same time + num_start_meals: 3 + # number of orders generated at the start of the environment + sample_on_dur: true + # if true, the next order is generated based on the sample_on_dur_func method in seconds + # if sample_on_serving is also true, the value is sampled after a meal was served, otherwise it is sampled directly after an order generation. + sample_on_dur_func: + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 10 + b: 20 + sample_on_serving: false + # The sample time for a new incoming order is only generated after a meal was served. + score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func '' + score_calc_gen_kwargs: + # the kwargs for the score_calc_gen_func + other: 0 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty '' + expired_penalty_kwargs: + default: -5 + serving_not_ordered_meals: null + # a func that calcs a store for not ordered but served meals. Input: meal + 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 + +player_config: + radius: 0.4 + move_dist: 0.15 + interaction_range: 1.6 \ 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 f3c79c5f87c03c4ec0a6fa4501f857bcca179d50..777e5f28e106587285ecf8eb6d1e77e23911914e 100644 --- a/overcooked_simulator/game_content/item_info.yaml +++ b/overcooked_simulator/game_content/item_info.yaml @@ -1,64 +1,96 @@ +CuttingBoard: + type: Equipment + +Sink: + type: Equipment + +Stove: + type: Equipment + +Pot: + type: Equipment + equipment: Stove + +Pan: + type: Equipment + equipment: Stove + +DirtyPlate: + type: Equipment + +Plate: + type: Equipment + needs: [ DirtyPlate ] + seconds: 2.0 + equipment: Sink + +# -------------------------------------------------------------------------------- + Tomato: type: Ingredient - is_cuttable: True - steps_needed: 500 Lettuce: type: Ingredient - is_cuttable: True - steps_needed: 500 Onion: type: Ingredient - is_cuttable: True - steps_needed: 700 Meat: type: Ingredient - is_cuttable: True - steps_needed: 500 + +Bun: + type: Ingredient + +ChoppedTomato: + type: Ingredient + needs: [ Tomato ] + seconds: 4.0 + equipment: CuttingBoard + +ChoppedLettuce: + type: Ingredient + needs: [ Lettuce ] + seconds: 3.0 + equipment: CuttingBoard + +ChoppedOnion: + type: Ingredient + needs: [ Onion ] + seconds: 5.0 + equipment: CuttingBoard + +ChoppedMeat: + type: Ingredient + needs: [ Meat ] + seconds: 4.0 + equipment: CuttingBoard CookedPatty: - type: Meal - steps_needed: 500 + type: Ingredient + seconds: 5.0 needs: [ ChoppedMeat ] - finished_progress_name: CookedPatty equipment: Pan -Bun: - type: Ingredient +# -------------------------------------------------------------------------------- Burger: type: Meal needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ] - equipment: Plate + equipment: ~ Salad: type: Meal - needs: [ ChoppedLettuce, Tomato ] - equipment: Plate + needs: [ ChoppedLettuce, ChoppedTomato ] + equipment: ~ TomatoSoup: type: Meal - finished_progress_name: TomatoSoup - steps_needed: 500 - needs: [ Tomato, Tomato, Tomato ] + needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] + seconds: 6.0 equipment: Pot OnionSoup: type: Meal - finished_progress_name: OnionSoup - steps_needed: 500 needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ] + seconds: 6.0 equipment: Pot - -Plate: - type: Equipment - is_cuttable: True - steps_needed: 200 - -Pot: - type: Equipment - -Pan: - type: Equipment \ No newline at end of file diff --git a/overcooked_simulator/game_content/item_info_debug.yaml b/overcooked_simulator/game_content/item_info_debug.yaml new file mode 100644 index 0000000000000000000000000000000000000000..32c8696610ab0e1b3f059071da01d7c182e6b80f --- /dev/null +++ b/overcooked_simulator/game_content/item_info_debug.yaml @@ -0,0 +1,96 @@ +CuttingBoard: + type: Equipment + +Sink: + type: Equipment + +Stove: + type: Equipment + +Pot: + type: Equipment + equipment: Stove + +Pan: + type: Equipment + equipment: Stove + +DirtyPlate: + type: Equipment + +Plate: + type: Equipment + needs: [ DirtyPlate ] + seconds: 1.0 + equipment: Sink + +# -------------------------------------------------------------------------------- + +Tomato: + type: Ingredient + +Lettuce: + type: Ingredient + +Onion: + type: Ingredient + +Meat: + type: Ingredient + +Bun: + type: Ingredient + +ChoppedTomato: + type: Ingredient + needs: [ Tomato ] + seconds: 0.1 + equipment: CuttingBoard + +ChoppedLettuce: + type: Ingredient + needs: [ Lettuce ] + seconds: 0.1 + equipment: CuttingBoard + +ChoppedOnion: + type: Ingredient + needs: [ Onion ] + seconds: 0.1 + equipment: CuttingBoard + +ChoppedMeat: + type: Ingredient + needs: [ Meat ] + seconds: 0.1 + equipment: CuttingBoard + +CookedPatty: + type: Ingredient + seconds: 2.0 + needs: [ ChoppedMeat ] + equipment: Pan + +# -------------------------------------------------------------------------------- + +Burger: + type: Meal + needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ] + equipment: ~ + +Salad: + type: Meal + needs: [ ChoppedLettuce, ChoppedTomato ] + equipment: ~ + +TomatoSoup: + type: Meal + needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] + seconds: 3.0 + equipment: Pot + +OnionSoup: + type: Meal + needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ] + seconds: 3.0 + equipment: Pot diff --git a/overcooked_simulator/game_content/player_config.yaml b/overcooked_simulator/game_content/player_config.yaml index 8a26daf15cf4ddecb91d09f191bd70e0a571e456..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 --- a/overcooked_simulator/game_content/player_config.yaml +++ b/overcooked_simulator/game_content/player_config.yaml @@ -1,3 +0,0 @@ -radius: 0.4 -move_speed_units_per_second: 0.1 -interaction_range: 1.6 \ No newline at end of file diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 937d061742e2084e2851a294c793953ab5a4bebf..4592e68bbe4057719b3618fbf7b34513fb4d232b 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -1,8 +1,11 @@ from __future__ import annotations +import collections import dataclasses +import datetime import logging from enum import Enum +from typing import Optional log = logging.getLogger(__name__) @@ -17,9 +20,7 @@ class ItemType(Enum): class ItemInfo: type: ItemType = dataclasses.field(compare=False) name: str = dataclasses.field(compare=True) - is_cuttable: bool = dataclasses.field(compare=False, default=False) - steps_needed: int = dataclasses.field(compare=False, default=0) - finished_progress_name: str = dataclasses.field(compare=False, default="Chopped*") + seconds: float = dataclasses.field(compare=False, default=0) needs: list[ItemInfo] = dataclasses.field(compare=False, default_factory=list) equipment: ItemInfo | None = dataclasses.field(compare=False, default=None) @@ -30,56 +31,17 @@ class ItemInfo: def __post_init__(self): self.type = ItemType(self.type) - def get_finished_name(self): - if "*" in self.finished_progress_name: - return self.finished_progress_name.replace("*", self.name) - return self.name - - def create_item(self, clean_plate=False, parts=None) -> Item: - match self.type: - case ItemType.Ingredient: - if self.is_cuttable: - return CuttableItem( - name=self.name, - finished=False, - steps_needed=self.steps_needed, - finished_name=self.get_finished_name(), - item_info=self, - ) - return Item(name=self.name, item_info=self) - case ItemType.Equipment: - if "Plate" in self.name: - return Plate( - name=self.name, - steps_needed=self.steps_needed, - finished=False, - item_info=self, - clean=clean_plate, - ) - else: - return CookingEquipment(name=self.name, item_info=self) - case ItemType.Meal: - return Meal( - name=self.name, - finished=False, - steps_needed=self.steps_needed, - finished_name=self.get_finished_name(), - item_info=self, - parts=parts, - ) - def add_start_meal_to_equipment(self, start_item: ItemInfo): self._start_meals.append(start_item) def sort_start_meals(self): self._start_meals.sort(key=lambda item_info: len(item_info.needs)) - def can_start_meal(self, items: list[Item]): - # TODO check specific order / only specific start items - return items and self._return_start_meal(items) is not None + # def can_start_meal(self, items: list[Item]): + # return items and self._return_start_meal(items) is not None - def start_meal(self, items: list[Item]) -> Item: - return self._return_start_meal(items).create_item(parts=items) + # def start_meal(self, items: list[Item]) -> Item: + # return self._return_start_meal(items).create_item(parts=items) def _return_start_meal(self, items: list[Item]) -> ItemInfo | None: for meal in self._start_meals: @@ -99,15 +61,14 @@ class Item: def __init__(self, name: str, item_info: ItemInfo, *args, **kwargs): self.name = self.__class__.__name__ if name is None else name self.item_info = item_info - - def can_combine(self, other): - return False - - def combine(self, other) -> Item | None: - pass + self.progress_equipment = None + self.progress_percentage = 0.0 def __repr__(self): - return f"{self.name}({self.extra_repr})" + if self.progress_equipment is None: + return f"{self.name}({self.extra_repr})" + else: + return f"{self.name}(progress={round(self.progress_percentage * 100, 2)}%,{self.extra_repr})" def __eq__(self, other): return other and self.name == other.name @@ -116,207 +77,167 @@ class Item: def extra_repr(self): return "" + def can_combine(self, other) -> bool: + return False -class ProgressibleItem(Item): - """Class for items which need to be processed (cut, cooked, ...)""" - - def __init__( - self, - finished: bool = False, - steps_needed: int = 500, - finished_name: str = None, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - self.progressed_steps = steps_needed if finished else 0 - self.steps_needed = steps_needed - self.finished = finished - self.finished_name = ( - f"Chopped{self.name}" if finished_name is None else finished_name - ) - - def progress(self): - """Progresses the item process as long as it is not finished.""" - if self.progressed_steps >= self.steps_needed: - self.finished = True - self.finished_call() - if not self.finished: - self.progressed_steps += 1 + def combine(self, other) -> Item | None: + pass - def can_progress(self) -> bool: - return True + def progress(self, equipment: str, percent: float): + """Progresses the item process on the given equipment as long as it is not finished.""" + if self.progress_equipment is None: + self.progress_equipment = equipment - def finished_call(self): - self.name = self.finished_name + if self.progress_equipment == equipment: + self.progress_percentage += percent + self.progress_percentage = min(self.progress_percentage, 1.0) + else: + log.warning( + f"{self.name} expected progress on {self.progress_equipment}, but got {percent * 100}% on {equipment}" + ) def reset(self): - self.finished = False - self.progressed_steps = 0 + self.progress_equipment = None + self.progress_percentage = 0.0 - def __repr__(self): - if not self.steps_needed or self.finished: - return f"{self.name}({self.extra_repr})" - else: - return f"{self.name}(progress={int(self.progressed_steps / self.steps_needed * 100)}%,{self.extra_repr})" +class CookingEquipment(Item): + def __init__(self, transitions: dict, *args, **kwargs): + super().__init__(*args, **kwargs) + self.transitions = transitions + self.active_transition: Optional[dict] = None -class CuttableItem(ProgressibleItem): - """Class of item which can be processed by the cutting board.""" + self.content_ready: Item | None = None + self.content_list: list[Item] = [] + log.debug(f"Initialize {self.name}: {self.transitions}") -class CookingEquipment(Item): - def __init__(self, content: Meal = None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.content = content + for transition in self.transitions.values(): + transition["recipe"] = collections.Counter(transition["needs"]) - def can_combine(self, other): + def can_combine(self, other) -> bool: + # already cooking or nothing to combine if other is None: return False - if self.content is None: - if isinstance(other, CookingEquipment): - return other.can_release_content() - # TODO check other is start of a meal, create meal - if isinstance(other, Meal) and "Plate" in self.name: - return not other.steps_needed or other.finished - return self.item_info.can_start_meal([other]) - if self.content.can_combine(other): - return True - if isinstance(other, CookingEquipment) and other.content: - other = other.content - return self.item_info.can_start_meal( - [other] - + ([self.content] if self.content.progressed_steps else self.content.parts) - ) - def combine(self, other): - if self.content is None: - if isinstance(other, CookingEquipment): - self.content = other.release() - return other - if isinstance(other, Meal) and "Plate" in self.name: - self.content = other - return - # find starting meal for other - self.content = self.item_info.start_meal([other]) - return - if not self.content.can_combine(other): - if isinstance(other, CookingEquipment) and other.content: - content = other.release() - self.content = self.item_info.start_meal( - [content] - + ( - [self.content] - if self.content.progressed_steps - else self.content.parts - ) - ) - return other - else: - self.content = self.item_info.start_meal( - [other] - + ( - [self.content] - if self.content.progressed_steps - else self.content.parts - ) - ) - return - self.content.combine(other) - - def can_progress(self, counter_type="Stove") -> bool: - return ( - self.content - and isinstance(self.content, ProgressibleItem) - and self.content.can_progress() + if isinstance(other, CookingEquipment): + other = other.content_list + else: + other = [other] + + # other extends ingredients for meal + ingredients = collections.Counter( + item.name for item in self.content_list + other + ) + print(ingredients) + return any( + ingredients <= recipe["recipe"] for recipe in self.transitions.values() ) - def progress(self): - self.content.progress() + def combine(self, other) -> Item | None: + return_value = None + if isinstance(other, CookingEquipment): + self.content_list.extend(other.content_list) + return_value = other + other.reset_content() + elif isinstance(other, list): + self.content_list.extend(other) + else: + self.content_list.append(other) + + 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"]) + else: + self.active_transition = { + "seconds": transition["seconds"], + "result": Item(name=result, item_info=transition["info"]), + } + print(f"{self.name} {self.active_transition}, {self.content_list}") + break + else: + self.content_ready = None + return return_value - def can_release_content(self) -> bool: - return ( - self.content - and isinstance(self.content, ProgressibleItem) - and self.content.finished - ) + def can_progress(self) -> bool: + return self.active_transition is not None + + 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"]] + self.reset() + + # todo set active transition for fire/burnt? + + # def can_release_content(self) -> bool: + # return ( + # self.content + # and isinstance(self.content, ProgressibleItem) + # and self.content.finished + # ) + def reset_content(self): + self.content_list = [] + self.content_ready = None def release(self): - content = self.content - self.content = None + content = self.content_list + self.reset_content() return content @property def extra_repr(self): - return self.content - - -class Meal(ProgressibleItem): - def __init__(self, parts=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.parts = [] if parts is None else parts - # self.rules ... - - def can_combine(self, other) -> bool: - if other and not self.finished: - satisfied = [False for _ in range(len(self.parts))] - for n in self.item_info.needs: - for i, p in enumerate(self.parts): - if not satisfied[i] and p.name == n: - satisfied[i] = True - break - else: - if n == other.name: - return True - return False + return f"{self.content_list}, {self.content_ready}" - def combine(self, other): - self.parts.append(other) - - def can_progress(self) -> bool: - return self.item_info.steps_needed and len(self.item_info.needs) == len( - self.parts - ) - - def finished_call(self): - super().finished_call() + def reset(self): + super().reset() + self.active_transition = None - @property - def extra_repr(self): - return self.parts + def get_potential_meal(self) -> Item | None: + if self.content_ready: + return self.content_ready + if len(self.content_list) == 1: + return self.content_list[0] + return None class Plate(CookingEquipment): - def __init__( - self, clean, steps_needed, finished, content: Meal = None, *args, **kwargs - ): - super().__init__(content, *args, **kwargs) + def __init__(self, transitions, clean, *args, **kwargs): self.clean = clean - self.name = self.create_name() - self.steps_needed = steps_needed - self.finished = finished - self.progressed_steps = steps_needed if finished else 0 + self.meals = set(transitions.keys()) + super().__init__( + name=self.create_name(), + transitions={ + k: v for k, v in transitions.items() if not v["info"].equipment + }, + *args, + **kwargs, + ) def finished_call(self): self.clean = True self.name = self.create_name() - def progress(self): - """Progresses the item process as long as it is not finished.""" - if self.progressed_steps >= self.steps_needed: - self.finished = True - self.finished_call() - if not self.finished: - self.progressed_steps += 1 + def progress(self, equipment: str, percent: float): + Item.progress(self, equipment, percent) - def can_progress(self, counter_type="Sink") -> bool: - return not self.clean + def create_name(self): + return "Plate" if self.clean else "DirtyPlate" def can_combine(self, other): - return self.clean and super().can_combine(other) - - def combine(self, other): - return super().combine(other) - - def create_name(self): - return "CleanPlate" if self.clean else "DirtyPlate" + if not super().can_combine(other): + if ( + isinstance(other, CookingEquipment) + and len(other.content_list) == 1 + and not self.content_list + and self.clean + ): + return other.content_list[0].name in self.meals + return False + elif self.clean: + return True + return False diff --git a/overcooked_simulator/gui_2d_vis/gui_theme.json b/overcooked_simulator/gui_2d_vis/gui_theme.json index 3ca11b55479e8d69bbbf8ab0e3fc4b46cad713e7..862d3d963c63ae0abe29c2c37516be2bc750b1cf 100644 --- a/overcooked_simulator/gui_2d_vis/gui_theme.json +++ b/overcooked_simulator/gui_2d_vis/gui_theme.json @@ -44,6 +44,44 @@ }, "misc": { "tool_tip_delay": "1.5" + }, + "font": { + "size": 15, + "bold": 1 + } + }, + "#timer_label": { + "colours": { + "normal_text": "#000000" + }, + "font": { + "size": 20, + "bold": 0 + } + }, + "#score_label": { + "colours": { + "normal_text": "#000000" + }, + "font": { + "size": 20, + "bold": 1 + } + }, + "#orders_label": { + "colours": { + "normal_text": "#000000" + }, + "font": { + "size": 20, + "bold": 0 + } + }, + "#quit_button": { + "colours": { + "normal_bg": "#f71b29", + "hovered_bg": "#bf0310", + "normal_border": "#DDDDDD" } } } \ No newline at end of file diff --git a/overcooked_simulator/gui_2d_vis/images/arrow_right.png b/overcooked_simulator/gui_2d_vis/images/arrow_right.png new file mode 100644 index 0000000000000000000000000000000000000000..522ec051e8f1ad938c8e53cd0e8b563f1e383cb1 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/arrow_right.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/bun.png b/overcooked_simulator/gui_2d_vis/images/bun.png new file mode 100644 index 0000000000000000000000000000000000000000..2c2f180610dbfd664acb61d2d733d7a3df75e2e5 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/bun.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/burger.png b/overcooked_simulator/gui_2d_vis/images/burger.png new file mode 100644 index 0000000000000000000000000000000000000000..df84a65fcca2ff5edcc9e258fe80a4005dc0716e Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/burger.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/cooked_patty.png b/overcooked_simulator/gui_2d_vis/images/cooked_patty.png new file mode 100644 index 0000000000000000000000000000000000000000..3d5b9270e569211eca7e971b21e8e9db944ac9bc Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/cooked_patty.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/cutting_board_large.png b/overcooked_simulator/gui_2d_vis/images/cutting_board_large.png new file mode 100644 index 0000000000000000000000000000000000000000..8f50132ce6b8646ec1f35f1599c7210f7d0e1c4f Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/cutting_board_large.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/lettuce_cut_smaller.png b/overcooked_simulator/gui_2d_vis/images/lettuce_cut_smaller.png new file mode 100644 index 0000000000000000000000000000000000000000..0fc897f72a779a998a264fce5ed4363bd8813d32 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/lettuce_cut_smaller.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/lettuce_smaller.png b/overcooked_simulator/gui_2d_vis/images/lettuce_smaller.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0abbda27f4ac859d8965d8c0828d0685277c08 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/lettuce_smaller.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/meat.png b/overcooked_simulator/gui_2d_vis/images/meat.png new file mode 100644 index 0000000000000000000000000000000000000000..ff750e1c4858d0bab5dd2434966ee38c7f88e085 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/meat.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/onion_cut.png b/overcooked_simulator/gui_2d_vis/images/onion_cut.png new file mode 100644 index 0000000000000000000000000000000000000000..f33ec9ab41c1aea7fbe6553891db86032346d29e Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/onion_cut.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/onion_large.png b/overcooked_simulator/gui_2d_vis/images/onion_large.png new file mode 100644 index 0000000000000000000000000000000000000000..dd70c1a5dd8a271c353909e4717ebe3629e80d54 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/onion_large.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/onion_soup_plate.png b/overcooked_simulator/gui_2d_vis/images/onion_soup_plate.png new file mode 100644 index 0000000000000000000000000000000000000000..89a81f7b27509ce34005d3752949e1c4a1dd3084 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/onion_soup_plate.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/onion_soup_pot.png b/overcooked_simulator/gui_2d_vis/images/onion_soup_pot.png new file mode 100644 index 0000000000000000000000000000000000000000..35ee354b302c94fe9c70a3502a1ebbfd2616cb54 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/onion_soup_pot.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/pan.png b/overcooked_simulator/gui_2d_vis/images/pan.png new file mode 100644 index 0000000000000000000000000000000000000000..afb288156faee0fdbb6917de0b18e0e1b698862d Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pan.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/pot_large.png b/overcooked_simulator/gui_2d_vis/images/pot_large.png new file mode 100644 index 0000000000000000000000000000000000000000..6c702693d5234eb32b148d68cce3024ce17d7aef Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pot_large.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/pot_smaller.png b/overcooked_simulator/gui_2d_vis/images/pot_smaller.png new file mode 100644 index 0000000000000000000000000000000000000000..5f8d29c3f66beba620e518b09e210783ff43ebe1 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/pot_smaller.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/raw_patty.png b/overcooked_simulator/gui_2d_vis/images/raw_patty.png new file mode 100644 index 0000000000000000000000000000000000000000..85a42e2a04efbddf49c99e23705f192e7d089e3b Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/raw_patty.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/salad.png b/overcooked_simulator/gui_2d_vis/images/salad.png new file mode 100644 index 0000000000000000000000000000000000000000..e227f75660c7c89e4ed7604dc38983bfc084f769 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/salad.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/sink_large.png b/overcooked_simulator/gui_2d_vis/images/sink_large.png new file mode 100644 index 0000000000000000000000000000000000000000..0758d02bc6f4b2c80cfd4c984f8914b74bb0ff83 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/sink_large.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/tomato3_cut_smaller.png b/overcooked_simulator/gui_2d_vis/images/tomato3_cut_smaller.png new file mode 100644 index 0000000000000000000000000000000000000000..f9aabe95db07b439e5557f2811d83ff516d0c4aa Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato3_cut_smaller.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/tomato3_smaller.png b/overcooked_simulator/gui_2d_vis/images/tomato3_smaller.png new file mode 100644 index 0000000000000000000000000000000000000000..64bc2f9a2a48e3ef85ddd5bd990fe38553b1f106 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato3_smaller.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_soup_plate.png b/overcooked_simulator/gui_2d_vis/images/tomato_soup_plate.png new file mode 100644 index 0000000000000000000000000000000000000000..006070021fc1c9ca3087309025bffe5d48083132 Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato_soup_plate.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/tomato_soup_pot.png b/overcooked_simulator/gui_2d_vis/images/tomato_soup_pot.png new file mode 100644 index 0000000000000000000000000000000000000000..70ea72b49ea7fc5916d4e1b11f81810f2f7bcfab Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/tomato_soup_pot.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/trash2.png b/overcooked_simulator/gui_2d_vis/images/trash2.png new file mode 100644 index 0000000000000000000000000000000000000000..76c76c47209ab25636a3952ab77e605fd3a7dd8f Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/trash2.png differ diff --git a/overcooked_simulator/gui_2d_vis/images/trash3.png b/overcooked_simulator/gui_2d_vis/images/trash3.png new file mode 100644 index 0000000000000000000000000000000000000000..5a391dc446197664636119fd360eeace6cb17b3c Binary files /dev/null and b/overcooked_simulator/gui_2d_vis/images/trash3.png differ diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index b94b950bd0078eea10f64addafcce755ed41daa5..3353d900b3774c9ad504709dc809fde3a7dbe832 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -4,6 +4,7 @@ import logging import math import sys from collections import deque +from datetime import timedelta from enum import Enum import numpy as np @@ -17,20 +18,19 @@ from websockets.sync.client import connect from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter from overcooked_simulator.game_items import ( - ProgressibleItem, Item, CookingEquipment, - Meal, Plate, ) from overcooked_simulator.gui_2d_vis.game_colors import BLUE from overcooked_simulator.gui_2d_vis.game_colors import colors, Color +from overcooked_simulator.order import Order from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.simulation_runner import Simulator -USE_PLAYER_COOK_SPRITES = False +USE_PLAYER_COOK_SPRITES = True SHOW_INTERACTION_RANGE = False -SHOW_COUNTER_CENTERS = True +SHOW_COUNTER_CENTERS = False class MenuStates(Enum): @@ -40,7 +40,7 @@ class MenuStates(Enum): def create_polygon(n, length): - if n == 0: + if n == 1: return np.array([0, 0]) vector = np.array([length, 0]) @@ -116,17 +116,24 @@ class PyGameGUI: self.visualization_config = yaml.safe_load(file) self.screen_margin = self.visualization_config["GameWindow"]["screen_margin"] - self.window_width = self.visualization_config["GameWindow"]["start_width"] - self.window_height = self.visualization_config["GameWindow"]["start_height"] + self.min_width = self.visualization_config["GameWindow"]["min_width"] + self.min_height = self.visualization_config["GameWindow"]["min_height"] + + self.buttons_width = self.visualization_config["GameWindow"]["buttons_width"] + self.buttons_height = self.visualization_config["GameWindow"]["buttons_height"] + + self.order_bar_height = self.visualization_config["GameWindow"][ + "order_bar_height" + ] + + self.window_width = self.min_width + self.window_height = self.min_height self.main_window = pygame.display.set_mode( - ( - self.window_width, - self.window_height, - ) + (self.window_width, self.window_height) ) - self.game_width, self.game_height = 0, 0 + # self.game_width, self.game_height = 0, 0 self.images_path = ROOT_DIR / "pygame_gui" / "images" @@ -135,46 +142,50 @@ class PyGameGUI: self.menu_state = MenuStates.Start self.manager: pygame_gui.UIManager - def init_window_sizes(self): - state = self.request_state() - print("THIS:", state["counters"]) - counter_positions = np.array([c["pos"] for c in state["counters"]]) - print(counter_positions) - kitchen_width = np.max(counter_positions[:, 0]) + 0.5 - kitchen_height = np.max(counter_positions[:, 1]) + 0.5 - - print(kitchen_width, kitchen_height) - - # if self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_width": - # game_width = self.visualization_config["GameWindow"]["size"] - # kitchen_aspect_ratio = ( - # self.simulator.env.kitchen_height / self.simulator.env.kitchen_width - # ) - # game_height = int(game_width * kitchen_aspect_ratio) - # grid_size = int(game_width / self.simulator.env.kitchen_width) - # elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "grid": - # grid_size = self.visualization_config["GameWindow"]["size"] - # game_width, game_height = ( - # self.simulator.env.kitchen_width * grid_size, - # self.simulator.env.kitchen_height * grid_size, - # ) - # else: - # game_width, game_height = 0, 0 - # grid_size = 0 - - grid_size = 40 - game_width = kitchen_width * grid_size - game_height = kitchen_height * grid_size + def get_window_sizes(self, state: dir): + counter_positions = np.array([c.pos for c in state["counters"]]) + kitchen_width = counter_positions[:, 0].max() + 0.5 + kitchen_height = counter_positions[:, 1].max() + 0.5 + if self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_width": + game_width = self.visualization_config["GameWindow"]["size"] + kitchen_aspect_ratio = kitchen_height / kitchen_width + game_height = int(game_width * kitchen_aspect_ratio) + grid_size = int(game_width / self.simulator.env.kitchen_width) + + elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "window_height": + game_height = self.visualization_config["GameWindow"]["size"] + kitchen_aspect_ratio = kitchen_width / kitchen_height + game_width = int(game_height * kitchen_aspect_ratio) + grid_size = int(game_width / self.simulator.env.kitchen_width) + + elif self.visualization_config["GameWindow"]["WhatIsFixed"] == "grid": + grid_size = self.visualization_config["GameWindow"]["size"] + game_width, game_height = ( + kitchen_width * grid_size, + kitchen_height * grid_size, + ) + + else: + game_width, game_height = 0, 0 + grid_size = 0 window_width, window_height = ( game_width + (2 * self.screen_margin), - game_height + (2 * self.screen_margin), + game_height + (2 * self.screen_margin), # bar with orders ) - return window_width, window_height, game_width, game_height, grid_size + window_width = max(window_width, self.min_width) + window_height = max(window_height, self.min_height) + return ( + int(window_width), + int(window_height), + game_width, + game_height, + grid_size, + ) def create_player_colors(self) -> list[Color]: - number_player = len(self.player_keys) + number_player = len(self.simulator.env.players) hue_values = np.linspace(0, 1, number_player + 1) colors_vec = np.array([col for col in colors.values()]) @@ -238,8 +249,8 @@ class PyGameGUI: self.game_screen.fill( colors[self.visualization_config["Kitchen"]["ground_tiles_color"]] ) - for x in range(0, int(self.window_width), block_size): - for y in range(0, int(self.window_height), block_size): + for x in range(0, self.window_width, block_size): + for y in range(0, self.window_height, block_size): rect = pygame.Rect(x, y, block_size, block_size) pygame.draw.rect( self.game_screen, @@ -248,7 +259,9 @@ class PyGameGUI: 1, ) - def draw_image(self, img_path, size, pos, rot_angle=0): + def draw_image( + self, img_path, size, pos, rot_angle=0, screen: pygame.Surface = None + ): cache_entry = f"{img_path}" if cache_entry in self.image_cache_dict.keys(): image = self.image_cache_dict[cache_entry] @@ -263,7 +276,11 @@ class PyGameGUI: image = pygame.transform.rotate(image, rot_angle) rect = image.get_rect() rect.center = pos - self.game_screen.blit(image, rect) + + if screen is None: + self.game_screen.blit(image, rect) + else: + screen.blit(image, rect) def draw_players(self, state, state_dict): """Visualizes the players as circles with a triangle for the facing direction. @@ -354,7 +371,11 @@ class PyGameGUI: ) def draw_thing( - self, pos: npt.NDArray[float], parts: list[dict[str]], scale: float = 1.0 + self, + pos: npt.NDArray[float], + parts: list[dict[str]], + scale: float = 1.0, + screen: pygame.Surface = None, ): """Draws an item, based on its visual parts specified in the visualization config. @@ -364,14 +385,22 @@ class PyGameGUI: scale: Rescale the item by this factor. """ + if screen is None: + screen = self.game_screen + for part in parts: part_type = part["type"] match part_type: case "image": + if "center_offset" in part: + d = np.array(part["center_offset"]) * self.grid_size + pos += d + self.draw_image( - parts[0]["path"], - parts[0]["size"] * scale * self.grid_size, + part["path"], + part["size"] * scale * self.grid_size, pos, + screen=screen, ) case "rect": height = part["height"] * self.grid_size @@ -380,7 +409,7 @@ class PyGameGUI: if "center_offset" in part: dx, dy = np.array(part["center_offset"]) * self.grid_size rect = pygame.Rect(pos[0] + dx, pos[1] + dy, height, width) - pygame.draw.rect(self.game_screen, color, rect) + pygame.draw.rect(screen, color, rect) else: rect = pygame.Rect( pos[0] - (height / 2), @@ -388,7 +417,7 @@ class PyGameGUI: height, width, ) - pygame.draw.rect(self.game_screen, color, rect) + pygame.draw.rect(screen, color, rect) case "circle": radius = part["radius"] * self.grid_size color = colors[part["color"]] @@ -400,9 +429,16 @@ class PyGameGUI: radius, ) else: - pygame.draw.circle(self.game_screen, color, pos, radius) + pygame.draw.circle(screen, color, pos, radius) - def draw_item(self, pos: npt.NDArray[float], item: Item, scale: float = 1.0): + def draw_item( + self, + pos: npt.NDArray[float], + item: Item, + scale: float = 1.0, + plate=False, + screen=None, + ): """Visualization of an item at the specified position. On a counter or in the hands of the player. The visual composition of the item is read in from visualization.yaml file, where it is specified as different parts to be drawn. @@ -411,41 +447,70 @@ class PyGameGUI: pos: The position of the item to draw. item: The item do be drawn in the game. scale: Rescale the item by this factor. + screen: the pygame screen to draw on. + plate: item is on a plate (soup are is different on a plate and pot) """ - if not isinstance(item, Meal): + if not isinstance(item, list): if item.name in self.visualization_config: + item_key = item.name + if "Soup" in item.name and plate: + item_key += "Plate" self.draw_thing( - pos, self.visualization_config[item.name]["parts"], scale=scale + pos, + self.visualization_config[item_key]["parts"], + scale=scale, + screen=screen, ) - if isinstance(item, (ProgressibleItem, Plate)) and not item.finished: - self.draw_progress_bar(pos, item.progressed_steps, item.steps_needed) - - if isinstance(item, CookingEquipment) and item.content: - self.draw_item(pos, item.content) + if isinstance(item, (Item, Plate)) and item.progress_percentage > 0.0: + self.draw_progress_bar(pos, item.progress_percentage) - if isinstance(item, Meal): - if item.finished: - if item.name in self.visualization_config: - self.draw_thing(pos, self.visualization_config[item.name]["parts"]) + if isinstance(item, CookingEquipment) and item.content_list: + if ( + item.content_ready + and item.content_ready.name in self.visualization_config + ): + self.draw_thing( + pos, + self.visualization_config[item.content_ready.name]["parts"], + screen=screen, + ) else: - for idx, o in enumerate(item.parts): - triangle_offsets = create_polygon(len(item.parts), length=10) - self.draw_item(pos + triangle_offsets[idx], o, scale=0.6) + triangle_offsets = create_polygon(len(item.content_list), length=10) + scale = 1 if len(item.content_list) == 1 else 0.6 + for idx, o in enumerate(item.content_list): + self.draw_item( + pos + triangle_offsets[idx], + o, + scale=scale, + plate=isinstance(item, Plate), + screen=screen, + ) - def draw_progress_bar(self, pos, current, needed): + # if isinstance(item, Meal): + # if item.finished: + # if item.name in self.visualization_config: + # self.draw_thing(pos, self.visualization_config[item.name]["parts"]) + # else: + # for idx, o in enumerate(item.parts): + # triangle_offsets = create_polygon(len(item.parts), length=10) + # self.draw_item(pos + triangle_offsets[idx], o, scale=0.6) + + def draw_progress_bar(self, pos, percent, screen=None): """Visualize progress of progressing item as a green bar under the item.""" - if current != 0: - bar_height = self.grid_size * 0.2 - progress_width = (current / needed) * self.grid_size - progress_bar = pygame.Rect( - pos[0] - (self.grid_size / 2), - pos[1] - (self.grid_size / 2) + self.grid_size - bar_height, - progress_width, - bar_height, - ) + bar_height = self.grid_size * 0.2 + progress_width = percent * self.grid_size + progress_bar = pygame.Rect( + pos[0] - (self.grid_size / 2), + pos[1] - (self.grid_size / 2) + self.grid_size - bar_height, + progress_width, + bar_height, + ) + if screen is None: pygame.draw.rect(self.game_screen, colors["green1"], progress_bar) + else: + pygame.draw.rect(screen, colors["green1"], progress_bar) def draw_counter(self, counter): """Visualization of a counter at its position. If it is occupied by an item, it is also shown. @@ -457,7 +522,6 @@ class PyGameGUI: """ pos = counter.pos * self.grid_size - self.draw_thing(pos, self.visualization_config["Counter"]["parts"]) if str(counter) in self.visualization_config: self.draw_thing(pos, self.visualization_config[str(counter)]["parts"]) @@ -486,11 +550,81 @@ class PyGameGUI: for counter in state["counters"]: self.draw_counter(counter) if SHOW_COUNTER_CENTERS: - pygame.draw.circle( - self.game_screen, colors["green1"], counter.pos * self.grid_size, 3 + pygame.draw.circle(self.game_screen, colors["green1"], counter.pos, 3) + + def update_score_label(self, state): + score = state["score"] + self.score_label.set_text(f"Score {score}") + + def update_conclusion_label(self, state): + score = state["score"] + self.conclusion_label.set_text(f"Your final score is {score}. Hurray!") + + def update_remaining_time(self, remaining_time: timedelta): + hours, rem = divmod(remaining_time.seconds, 3600) + minutes, seconds = divmod(rem, 60) + display_time = f"{minutes}:{'%02d' % seconds}" + self.timer_label.set_text(f"Time remaining: {display_time}") + + def draw_orders(self, state): + orders_width = self.game_width - 100 + orders_height = self.screen_margin + + order_screen = pygame.Surface( + (orders_width, orders_height), + ) + + bg_color = colors[self.visualization_config["GameWindow"]["background_color"]] + pygame.draw.rect(order_screen, bg_color, order_screen.get_rect()) + + order_rects_start = (orders_height // 2) - (self.grid_size // 2) + with self.simulator.env.lock: + for idx, order in enumerate(state["orders"]): + order: Order + order_upper_left = [ + order_rects_start + idx * self.grid_size * 1.2, + order_rects_start, + ] + pygame.draw.rect( + order_screen, + colors["red"], + pygame.Rect( + order_upper_left[0], + order_upper_left[1], + self.grid_size, + self.grid_size, + ), + width=2, + ) + center = np.array(order_upper_left) + np.array( + [self.grid_size / 2, self.grid_size / 2] ) + self.draw_thing( + center, + self.visualization_config["Plate"]["parts"], + screen=order_screen, + ) + self.draw_item( + center, + order.meal, + plate=True, + screen=order_screen, + ) + order_done_seconds = ( + (order.start_time + order.max_duration) - state["env_time"] + ).total_seconds() + + percentage = order_done_seconds / order.max_duration.total_seconds() + self.draw_progress_bar(center, percentage, screen=order_screen) - def draw(self, state, state_dict): + orders_rect = order_screen.get_rect() + orders_rect.center = [ + self.screen_margin + (orders_width // 2), + orders_height // 2, + ] + self.main_window.blit(order_screen, orders_rect) + + def draw(self, state): """Main visualization function. Args: @@ -500,21 +634,24 @@ class PyGameGUI: self.draw_background() self.draw_counters(state) - self.draw_players(state, state_dict) - self.manager.draw_ui(self.game_screen) + self.draw_players(state) + self.manager.draw_ui(self.main_window) + self.update_remaining_time(state["remaining_time"]) + + self.draw_orders(state) + self.update_score_label(state) def init_ui_elements(self): self.manager = pygame_gui.UIManager((self.window_width, self.window_height)) self.manager.get_theme().load_theme(ROOT_DIR / "gui_2d_vis" / "gui_theme.json") - button_width, button_height = 200, 60 self.start_button = pygame_gui.elements.UIButton( relative_rect=pygame.Rect( ( - (self.window_width // 2) - button_width // 2, - (self.window_height / 2) - button_height // 2, + (self.window_width // 2) - self.buttons_width // 2, + (self.window_height / 2) - self.buttons_height // 2, ), - (button_width, button_height), + (self.buttons_width, self.buttons_height), ), text="Start Game", manager=self.manager, @@ -524,25 +661,26 @@ class PyGameGUI: self.quit_button = pygame_gui.elements.UIButton( relative_rect=pygame.Rect( ( - (self.window_width - button_width), + (self.window_width - self.buttons_width), 0, ), - (button_width, button_height), + (self.buttons_width, self.buttons_height), ), text="Quit Game", manager=self.manager, + object_id="#quit_button", ) self.quit_button.can_hover() self.finished_button = pygame_gui.elements.UIButton( relative_rect=pygame.Rect( ( - (self.window_width - button_width), - (self.window_height - button_height), + (self.window_width - self.buttons_width), + (self.window_height - self.buttons_height), ), - (button_width, button_height), + (self.buttons_width, self.buttons_height), ), - text="End screen", + text="Finish round", manager=self.manager, ) self.finished_button.can_hover() @@ -551,26 +689,24 @@ class PyGameGUI: relative_rect=pygame.Rect( ( (0), - (self.window_height - button_height), + (self.window_height - self.buttons_height), ), - (button_width, button_height), + (self.buttons_width, self.buttons_height), ), - text="Back to Start", + text="Back to menu", manager=self.manager, ) self.back_button.can_hover() - self.score_rect = pygame.Rect( - ( - (self.window_width // 2) - button_width // 2, - (self.window_height / 2) - button_height // 2, - ), - (button_width, button_height), - ) - self.score_label = pygame_gui.elements.UILabel( - text=f"Your score: _", - relative_rect=self.score_rect, + text=f"Score: _", + relative_rect=pygame.Rect( + ( + (0), + self.window_height - self.screen_margin, + ), + (self.screen_margin * 2, self.screen_margin), + ), manager=self.manager, object_id="#score_label", ) @@ -593,30 +729,51 @@ class PyGameGUI: options_list=layout_file_paths, starting_option=layout_file_paths[-1], ) + self.timer_label = pygame_gui.elements.UILabel( + text="GAMETIME", + relative_rect=pygame.Rect( + (self.screen_margin, self.window_height - self.screen_margin), + (self.game_width, self.screen_margin), + ), + manager=self.manager, + object_id="#timer_label", + ) - def setup_windows(self): - ( - self.window_width, - self.window_height, - self.game_width, - self.game_height, - self.grid_size, - ) = self.init_window_sizes() + self.orders_label = pygame_gui.elements.UILabel( + text="Orders:", + relative_rect=pygame.Rect(0, 0, self.screen_margin, self.screen_margin), + manager=self.manager, + object_id="#orders_label", + ) + self.conclusion_label = pygame_gui.elements.UILabel( + text="Your final score was _", + relative_rect=pygame.Rect(0, 0, self.window_width, self.window_height), + manager=self.manager, + object_id="#score_label", + ) + + def set_window_size(self, window_width, window_height, game_width, game_height): self.game_screen = pygame.Surface( ( - self.game_width, - self.game_height, + game_width, + game_height, ), ) - self.main_window = pygame.display.set_mode( ( - self.window_width, - self.window_height, + window_width, + window_height, ) ) - self.player_colors = self.create_player_colors() + + def reset_window_size(self): + self.window_width = self.min_width + self.window_height = self.min_height + self.game_width = 0 + self.game_height = 0 + self.set_window_size(self.min_width, self.min_height, 0, 0) + self.init_ui_elements() def setup_simulation(self, config_path, layout_path): self.simulator = Simulator(config_path, layout_path, 600) @@ -625,6 +782,14 @@ class PyGameGUI: player_name = f"p{i}" self.simulator.register_player(player_name) self.simulator.start() + ( + self.window_width, + self.window_height, + self.game_width, + self.game_height, + self.grid_size, + ) = self.get_window_sizes(self.simulator.get_state()) + self.player_colors = self.create_player_colors() def manage_button_visibility(self): match self.menu_state: @@ -635,21 +800,28 @@ class PyGameGUI: self.score_label.hide() self.finished_button.hide() self.layout_selection.show() + self.timer_label.hide() + self.orders_label.hide() + self.conclusion_label.hide() case MenuStates.Game: self.start_button.hide() - self.back_button.show() - self.score_label.hide() + self.back_button.hide() + self.score_label.show() + self.score_label.show() self.finished_button.show() self.layout_selection.hide() + self.timer_label.show() + self.orders_label.show() + self.conclusion_label.hide() case MenuStates.End: self.start_button.hide() self.back_button.show() - self.score_label.show() - self.score_label.set_text( - f"Your Score is {self.simulator.env.game_score.score}" - ) + self.score_label.hide() self.finished_button.hide() self.layout_selection.hide() + self.timer_label.hide() + self.orders_label.hide() + self.conclusion_label.show() def start_button_press(self): self.menu_state = MenuStates.Game @@ -663,15 +835,18 @@ class PyGameGUI: config_path = ROOT_DIR / "game_content" / "environment_config.yaml" self.setup_simulation(config_path, layout_path) - self.setup_windows() + + self.set_window_size(*(self.get_window_sizes(self.simulator.get_state()))[:-1]) + self.init_ui_elements() log.debug("Pressed start button") # self.api.set_sim(self.simulator) def back_button_press(self): - self.simulator.stop() self.menu_state = MenuStates.Start + self.reset_window_size() + self.simulator.stop() log.debug("Pressed back button") def quit_button_press(self): @@ -680,8 +855,9 @@ class PyGameGUI: log.debug("Pressed quit button") def finished_button_press(self): - self.simulator.stop() self.menu_state = MenuStates.End + self.reset_window_size() + self.simulator.stop() log.debug("Pressed finished button") def send_action(self, action: Action): @@ -718,13 +894,12 @@ class PyGameGUI: pygame.init() pygame.font.init() - self.setup_windows() - self.init_ui_elements() - pygame.display.set_caption("Simple Overcooked Simulator") clock = pygame.time.Clock() + self.reset_window_size() + self.init_ui_elements() self.manage_button_visibility() # Game loop @@ -736,6 +911,8 @@ class PyGameGUI: for event in pygame.event.get(): if event.type == pygame.QUIT: self.running = False + + # UI Buttons: if event.type == pygame_gui.UI_BUTTON_PRESSED: match event.ui_element: case self.start_button: @@ -759,12 +936,17 @@ class PyGameGUI: # drawing: - self.main_window.fill(colors["lemonchiffon1"]) + state = self.simulator.get_state() + + self.main_window.fill( + colors[self.visualization_config["GameWindow"]["background_color"]] + ) self.manager.draw_ui(self.main_window) match self.menu_state: case MenuStates.Start: pass + case MenuStates.Game: self.draw_background() @@ -774,18 +956,23 @@ class PyGameGUI: state = self.simulator.get_state() self.draw(state, state_dict) - game_screen_rect = self.game_screen.get_rect() - game_screen_rect.center = [ - self.window_width // 2, - self.window_height // 2, - ] + if state["ended"]: + self.finished_button_press() + self.manage_button_visibility() + else: + self.draw(state) - self.main_window.blit(self.game_screen, game_screen_rect) + game_screen_rect = self.game_screen.get_rect() + game_screen_rect.center = [ + self.window_width // 2, + self.window_height // 2, + ] + + self.main_window.blit(self.game_screen, game_screen_rect) case MenuStates.End: - pygame.draw.rect( - self.game_screen, colors["cornsilk1"], self.score_rect - ) + self.update_conclusion_label(state) + self.manager.update(time_delta) pygame.display.flip() diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml index 021761424694300f516cd77cfd63e907ac60fa23..8d4d52deb8f111a56dda65c44501aca3414fe979 100644 --- a/overcooked_simulator/gui_2d_vis/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -1,11 +1,18 @@ # colors: https://www.webucator.com/article/python-color-constants-module/ GameWindow: - WhatIsFixed: window_width # entweder grid oder window_width - size: 600 + WhatIsFixed: grid # grid or window_width or window_height + size: 50 screen_margin: 100 - start_width: 600 - start_height: 600 + min_width: 700 + min_height: 600 + buttons_width: 180 + buttons_height: 60 + + order_bar_height: 100 + order_size: 50 + + background_color: lemonchiffon1 Kitchen: ground_tiles_color: sgigray76 @@ -20,15 +27,10 @@ Counter: CuttingBoard: parts: - - type: rect - height: 0.75 - width: 0.75 - color: burlywood1 - - type: rect - height: 0.125 - width: 0.5 - center_offset: [ +0.15, -0.2 ] - color: silver + - type: image + path: images/cutting_board_large.png + size: 0.9 + PlateDispenser: parts: @@ -39,12 +41,10 @@ PlateDispenser: Trash: parts: - - type: circle - radius: 0.4 - color: black - - type: circle - radius: 0.375 - color: gray33 + - type: image + path: images/trash3.png + size: 0.9 + center_offset: [ 0, 0 ] TomatoDispenser: parts: @@ -84,10 +84,10 @@ BunDispenser: ServingWindow: parts: - - color: darkgoldenrod1 - type: rect - height: 0.85 - width: 0.85 + - type: image + path: images/arrow_right.png + size: 1 + center_offset: [ 0, 0 ] Stove: parts: @@ -101,13 +101,9 @@ Stove: Sink: parts: - - color: black - type: rect - height: 0.875 - width: 0.625 - - color: darkslategray1 - type: circle - radius: 0.45 + - type: image + path: images/sink_large.png + size: 0.9 SinkAddon: parts: @@ -134,139 +130,99 @@ SinkAddon: Tomato: parts: - type: image - path: images/tomato.png + path: images/tomato3_smaller.png size: 1 Onion: parts: - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: deeppink4 + - type: image + path: images/onion_large.png + size: 0.8 Bun: parts: - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: navajowhite2 + - type: image + path: images/bun.png + size: 0.9 Lettuce: parts: - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: emeraldgreen + - type: image + path: images/lettuce_smaller.png + size: 0.8 Meat: parts: - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: hotpink - + - type: image + path: images/meat.png + size: 1 ChoppedLettuce: parts: - - type: circle - radius: 0.3 - color: black - center_offset: [ -0.125, 0 ] - - type: circle - radius: 0.25 - color: emeraldgreen - center_offset: [ -0.125, 0 ] - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: emeraldgreen - - type: circle - radius: 0.3 - color: black - center_offset: [ 0.125, 0 ] - - type: circle - radius: 0.25 - color: emeraldgreen - center_offset: [ 0.125, 0 ] + - type: image + path: images/lettuce_cut_smaller.png + size: 0.8 ChoppedTomato: parts: - type: image - path: images/tomato_cut.png + path: images/tomato3_cut_smaller.png size: 1 ChoppedOnion: parts: - - type: circle - radius: 0.3 - color: black - center_offset: [ -0.125, 0 ] - - type: circle - radius: 0.25 - color: deeppink4 - center_offset: [ -0.125, 0 ] - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: deeppink4 - - type: circle - radius: 0.3 - color: black - center_offset: [ 0.125, 0 ] - - type: circle - radius: 0.25 - color: deeppink4 - center_offset: [ 0.125, 0 ] + - type: image + path: images/onion_cut.png + size: 0.95 ChoppedMeat: parts: - - type: circle - radius: 0.3 - color: black - center_offset: [ -0.125, 0 ] - - type: circle - radius: 0.25 - color: indianred1 - center_offset: [ -0.125, 0 ] - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: indianred1 + - type: image + path: images/raw_patty.png + size: 0.9 CookedPatty: parts: - - type: circle - radius: 0.3 - color: black - - type: circle - radius: 0.25 - color: salmon4 + - type: image + path: images/cooked_patty.png + size: 0.9 +Burger: + parts: + - type: image + path: images/burger.png + size: 0.8 + +Salad: + parts: + - type: image + path: images/salad.png + size: 0.8 TomatoSoup: parts: - type: image - path: images/tomato_soup.png + path: images/tomato_soup_pot.png + size: 1.05 + center_offset: [ 0, 0 ] + +TomatoSoupPlate: + parts: + - type: image + path: images/tomato_soup_plate.png size: 0.6 OnionSoup: parts: - type: image - path: images/tomato_soup.png + path: images/onion_soup_pot.png + size: 1.05 + center_offset: [ 0, 0 ] + +OnionSoupPlate: + parts: + - type: image + path: images/onion_soup_plate.png size: 0.6 Cook: @@ -275,7 +231,7 @@ Cook: path: images/pixel_cook.png size: 1 -CleanPlate: +Plate: parts: - type: image path: images/plate_clean.png @@ -290,19 +246,12 @@ DirtyPlate: Pot: parts: - type: image - path: images/pot.png - size: 0.8 + path: images/pot_smaller.png + size: 1.05 + center_offset: [ -0.02, -0.1 ] Pan: parts: - - type: circle - radius: 0.4 - color: black - - type: circle - radius: 0.35 - color: sgigray16 - - color: orange4 - type: rect - height: 0.5 - width: 0.1 - center_offset: [ -0.8, -0.05 ] \ No newline at end of file + - type: image + path: images/pan.png + size: 1.1 \ No newline at end of file diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py index 81a5d8f029c82707cb5720a6a8d08c8b16309d8c..dc790c40c467caacdff47c5b2d26a65a4b59dd09 100644 --- a/overcooked_simulator/main.py +++ b/overcooked_simulator/main.py @@ -27,6 +27,7 @@ def setup_logging(): logging.StreamHandler(sys.stdout), ], ) + logging.getLogger("matplotlib").setLevel(logging.WARNING) def main(): diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py new file mode 100644 index 0000000000000000000000000000000000000000..5d7abbf72cdbbd1a6d2fe110934b8561d342ca44 --- /dev/null +++ b/overcooked_simulator/order.py @@ -0,0 +1,353 @@ +import dataclasses +import logging +import random +from abc import abstractmethod +from collections import deque +from datetime import datetime, timedelta +from typing import Callable, Tuple, Any, Deque + +from overcooked_simulator.game_items import Item, Plate, ItemInfo + +log = logging.getLogger(__name__) + + +@dataclasses.dataclass +class Order: + meal: ItemInfo + start_time: datetime + max_duration: timedelta + score_calc: Callable[[timedelta, ...], float] + timed_penalties: list[ + Tuple[timedelta, float] | Tuple[timedelta, float, int, timedelta] + ] + expired_penalty: float + + finished_info: dict[str, Any] = dataclasses.field(default_factory=dict) + _timed_penalties: list[Tuple[datetime, float]] = dataclasses.field( + default_factory=list + ) + + def order_time(self, env_time: datetime) -> timedelta: + return self.start_time - env_time + + def create_penalties(self, env_time: datetime): + for penalty_info in self.timed_penalties: + match penalty_info: + case (offset, penalty): + self._timed_penalties.append((env_time + offset, penalty)) + case (duration, penalty, number_repeat, offset): + self._timed_penalties.extend( + [ + (env_time + offset + (duration * i), penalty) + for i in range(number_repeat) + ] + ) + + +class OrderGeneration: + def __init__(self, available_meals: dict[str, ItemInfo], **kwargs): + self.available_meals: list[ItemInfo] = list(available_meals.values()) + + @abstractmethod + def init_orders(self, now) -> list[Order]: + ... + + @abstractmethod + def get_orders( + self, + passed_time: timedelta, + now: datetime, + new_finished_orders: list[Order], + expired_orders: list[Order], + ) -> list[Order]: + ... + + +def zero(item: ItemInfo, **kwargs) -> float: + return 0.0 + + +@dataclasses.dataclass +class RandomOrderKwarg: + num_start_meals: int + sample_on_serving: bool + sample_on_dur: bool + sample_on_dur_func: dict + max_orders: int + duration_sample: dict + score_calc_gen_func: Callable[ + [ItemInfo, timedelta, datetime, Any], Callable[[timedelta, Order], float] + ] + score_calc_gen_kwargs: dict + expired_penalty_func: Callable[[ItemInfo], float] = zero + expired_penalty_kwargs: dict = dataclasses.field(default_factory=dict) + + +class RandomOrderGeneration(OrderGeneration): + def __init__(self, available_meals: dict[str, ItemInfo], **kwargs): + super().__init__(available_meals, **kwargs) + self.kwargs: RandomOrderKwarg = RandomOrderKwarg(**kwargs["kwargs"]) + self.next_order_time: datetime | None = datetime.max + self.number_cur_orders = 0 + self.needed_orders: int = 0 + """For the sample on dur but when it was restricted due to max order number.""" + + def init_orders(self, now) -> list[Order]: + self.number_cur_orders = self.kwargs.num_start_meals + if self.kwargs.sample_on_dur: + self.create_random_next_time_delta(now) + return self.create_orders_for_meals( + random.choices(self.available_meals, k=self.kwargs.num_start_meals), + now, + self.kwargs.sample_on_serving, + ) + + def get_orders( + self, + passed_time: timedelta, + now: datetime, + new_finished_orders: list[Order], + expired_orders: list[Order], + ) -> list[Order]: + self.number_cur_orders -= len(new_finished_orders) + self.number_cur_orders -= len(expired_orders) + if self.kwargs.sample_on_serving: + if new_finished_orders: + self.create_random_next_time_delta(now) + return [] + if self.needed_orders: + self.needed_orders -= len(new_finished_orders) + self.needed_orders = max(self.needed_orders, 0) + self.number_cur_orders += len(new_finished_orders) + return self.create_orders_for_meals( + random.choices(self.available_meals, k=len(new_finished_orders)), + now, + ) + if self.next_order_time <= now: + if self.number_cur_orders >= self.kwargs.max_orders: + self.needed_orders += 1 + else: + if self.kwargs.sample_on_dur: + self.create_random_next_time_delta(now) + else: + self.next_order_time = datetime.max + self.number_cur_orders += 1 + return self.create_orders_for_meals( + [random.choice(self.available_meals)], + now, + ) + return [] + + def create_orders_for_meals( + self, meals: list[ItemInfo], now: datetime, no_time_limit: bool = False + ) -> list[Order]: + orders = [] + for meal in meals: + if no_time_limit: + duration = datetime.max - now + else: + duration = timedelta( + seconds=getattr(random, self.kwargs.duration_sample["func"])( + **self.kwargs.duration_sample["kwargs"] + ) + ) + log.info(f"Create order for meal {meal} with duration {duration}") + orders.append( + Order( + meal=meal, + start_time=now, + max_duration=duration, + score_calc=self.kwargs.score_calc_gen_func( + meal=meal, + duration=duration, + now=now, + kwargs=self.kwargs.score_calc_gen_kwargs, + ), + timed_penalties=[], + expired_penalty=self.kwargs.expired_penalty_func( + meal, **self.kwargs.expired_penalty_kwargs + ), + ) + ) + + return orders + + def create_random_next_time_delta(self, now: datetime): + self.next_order_time = now + timedelta( + seconds=getattr(random, self.kwargs.sample_on_dur_func["func"])( + **self.kwargs.sample_on_dur_func["kwargs"] + ) + ) + log.info(f"Next order in {self.next_order_time}") + + +def simple_score_calc_gen_func( + meal: Item, duration: timedelta, now: datetime, kwargs: dict, **other_kwargs +) -> Callable: + scores = kwargs["scores"] + other = kwargs["other"] + + def score_calc(relative_order_time: timedelta, order: Order) -> float: + if order.meal.name in scores: + return scores[order.meal.name] + return other + + return score_calc + + +def simple_expired_penalty(item: ItemInfo, default: float, **kwargs) -> float: + return default + + +class OrderAndScoreManager: + def __init__(self, order_config, available_meals: dict[str, ItemInfo]): + self.score = 0 + self.order_gen: OrderGeneration = order_config["order_gen_class"]( + available_meals=available_meals, kwargs=order_config["kwargs"] + ) + self.kwargs_for_func = order_config["kwargs"] + self.serving_not_ordered_meals = order_config["serving_not_ordered_meals"] + self.available_meals = available_meals + self.open_orders: Deque[Order] = deque() + + # for logs or history in the future + # TODO log who / which player served which meal -> for split scores + self.served_meals: list[Tuple[Item, datetime]] = [] + self.last_finished = [] + self.next_relevant_time = datetime.max + self.last_expired = [] + + def update_next_relevant_time(self): + next_relevant_time = datetime.max + for order in self.open_orders: + next_relevant_time = min( + next_relevant_time, order.start_time + order.max_duration + ) + for penalty in order._timed_penalties: + next_relevant_time = min(next_relevant_time, penalty[0]) + self.next_relevant_time = next_relevant_time + + def serve_meal(self, item: Item, env_time: datetime) -> bool: + if isinstance(item, Plate): + meal = item.get_potential_meal() + if meal is not None: + if meal.name in self.available_meals: + order = self.find_order_for_meal(meal) + if order is None: + if self.serving_not_ordered_meals: + accept, score = self.serving_not_ordered_meals(meal) + if accept: + log.info( + f"Serving meal without order {meal.name} with score {score}" + ) + self.score += score + self.served_meals.append((meal, env_time)) + return accept + log.info( + f"Do not serve meal {meal.name} because it is not ordered" + ) + return False + order, index = order + score = order.score_calc( + relative_order_time=env_time - order.start_time, + order=order, + ) + self.score += score + order.finished_info = { + "end_time": env_time, + "score": score, + } + log.info(f"Serving meal {meal.name} with order with score {score}") + self.last_finished.append(order) + del self.open_orders[index] + self.served_meals.append((meal, env_time)) + return True + log.info(f"Do not serve item {item}") + return False + + def increment_score(self, score: int): + self.score += score + log.debug(f"Score: {self.score}") + + def create_init_orders(self, env_time): + init_orders = self.order_gen.init_orders(env_time) + self.open_orders.extend(init_orders) + + def progress(self, passed_time: timedelta, now: datetime): + new_orders = self.order_gen.get_orders( + passed_time=passed_time, + now=now, + new_finished_orders=self.last_finished, + expired_orders=self.last_expired, + ) + self.open_orders.extend(new_orders) + self.last_finished = [] + self.last_expired = [] + if new_orders or self.next_relevant_time <= now: + remove_orders = [] + for index, order in enumerate(self.open_orders): + if now >= order.start_time + order.max_duration: + self.score += order.expired_penalty + remove_orders.append(index) + remove_penalties = [] + for i, (penalty_time, penalty) in enumerate(order.timed_penalties): + if penalty_time < now: + self.score -= penalty + remove_penalties.append(i) + + for i in reversed(remove_penalties): + # or del order.timed_penalties[index] + order.timed_penalties.pop(i) + expired_orders = [] + for remove_order in reversed(remove_orders): + expired_orders.append(self.open_orders[remove_order]) + del self.open_orders[remove_order] + self.last_expired = expired_orders + + self.update_next_relevant_time() + + def find_order_for_meal(self, meal) -> Tuple[Order, int] | None: + for index, order in enumerate(self.open_orders): + if order.meal.name == meal.name: + return order, index + + def setup_penalties(self, new_orders: list[Order], env_time: datetime): + for order in new_orders: + order.create_penalties(env_time) + + +if __name__ == "__main__": + import yaml + + order_config = yaml.safe_load( + """orders: + kwargs: + duration_sample: + func: uniform + kwargs: + a: 30 + b: 50 + max_orders: 5 + num_start_meals: 3 + sample_on_dur: false + sample_on_dur_func: + func: uniform + kwargs: + a: 30 + b: 50 + sample_on_serving: true + score_calc_gen_func: null + score_calc_gen_kwargs: + other: 0 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + score_calc_gen_func: ~'' + order_gen_class: ~ + serving_not_ordered_meals: null""" + ) + order_config["orders"]["order_gen_class"] = RandomOrderGeneration + order_config["orders"]["kwargs"]["score_calc_gen_func"] = simple_score_calc_gen_func + print(yaml.dump(order_config)) diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index c3bf550256bc7f40abbd14bf6362a05c3865cc33..eff35464f09aff983d3e6bbf56e2b0ea506d54de 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -23,25 +23,18 @@ from overcooked_simulator.counters import ( PlateDispenser, SinkAddon, ) -from overcooked_simulator.game_items import ItemInfo, ItemType +from overcooked_simulator.game_items import ( + ItemInfo, + ItemType, + CookingEquipment, +) +from overcooked_simulator.order import OrderAndScoreManager from overcooked_simulator.player import Player from overcooked_simulator.utils import create_init_env_time log = logging.getLogger(__name__) -class GameScore: - def __init__(self): - self.score = 0 - - def increment_score(self, score: int): - self.score += score - log.debug(f"Score: {self.score}") - - def read_score(self): - return self.score - - class Action: """Action class, specifies player, action type and action itself.""" @@ -59,18 +52,6 @@ class Action: return f"Action({self.player},{self.act_type},{self.action})" -class GameScore: - def __init__(self): - self.score = 0 - - def increment_score(self, score: int): - self.score += score - log.debug(f"Score: {self.score}") - - def read_score(self): - return self.score - - class Environment: """Environment class which handles the game logic for the overcooked-inspired environment. @@ -83,41 +64,115 @@ class Environment: self.players: dict[str, Player] = {} with open(env_config_path, "r") as file: - environment_config = yaml.safe_load(file) + self.environment_config = yaml.load(file, Loader=yaml.Loader) self.layout_path: Path = layout_path # self.counter_side_length = 1 # -> this changed! is 1 now self.item_info_path: Path = item_info_path self.item_info = self.load_item_info() - - self.game_score = GameScore() + self.validate_item_info() + if self.environment_config["meals"]["all"]: + self.allowed_meal_names = set( + [ + item + for item, info in self.item_info.items() + if info.type == ItemType.Meal + ] + ) + else: + self.allowed_meal_names = set(self.environment_config["meals"]["list"]) + + self.order_and_score = OrderAndScoreManager( + order_config=self.environment_config["orders"], + available_meals={ + item: info + for item, info in self.item_info.items() + if info.type == ItemType.Meal and item in self.allowed_meal_names + }, + ) + 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": CuttingBoard, + "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" + }, + ), "X": Trash, - "W": lambda pos: ServingWindow(pos, self.game_score), + "W": lambda pos: ServingWindow( + pos, + self.order_and_score, + meals=self.allowed_meal_names, + env_time_func=self.get_env_time, + ), "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]), "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]), "P": lambda pos: PlateDispenser( - pos, - self.item_info["Plate"], - environment_config["plates"] if "plates" in environment_config else {}, + plate_transitions=plate_transitions, + pos=pos, + dispensing=self.item_info["Plate"], + plate_config=self.environment_config["plates"] + if "plates" in self.environment_config + else {}, ), "N": lambda pos: Dispenser(pos, self.item_info["Onion"]), # N for oNioN "_": "Free", "A": "Agent", "U": lambda pos: Stove( pos, - self.item_info["Pot"].create_item(), + 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" + }, + ), ), # Stove with pot: U because it looks like a pot "Q": lambda pos: Stove( pos, - self.item_info["Pan"].create_item(), - ), # Stove with pot: U because it looks like a pot + 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" + }, + ), + ), # 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), + "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" + }, + ), "+": SinkAddon, } @@ -132,9 +187,20 @@ class Environment: self.init_counters() - self.score: int = 0 - self.env_time = create_init_env_time() + self.order_and_score.create_init_orders(self.env_time) + self.beginning_time = self.env_time + self.env_time_end = self.env_time + timedelta( + seconds=self.environment_config["game"]["time_limit_seconds"] + ) + log.debug(f"End time: {self.env_time_end}") + + def get_env_time(self): + return self.env_time + + @property + def game_ended(self) -> bool: + return self.env_time >= self.env_time_end def load_item_info(self) -> dict[str, ItemInfo]: with open(self.item_info_path, "r") as file: @@ -152,6 +218,62 @@ class Environment: item_info.sort_start_meals() return item_lookup + def validate_item_info(self): + pass + # infos = {t: [] for t in ItemType} + # graph = nx.DiGraph() + # for info in self.item_info.values(): + # infos[info.type].append(info) + # graph.add_node(info.name) + # match info.type: + # case ItemType.Ingredient: + # if info.is_cuttable: + # graph.add_edge( + # info.name, info.finished_progress_name[:-1] + info.name + # ) + # case ItemType.Equipment: + # ... + # case ItemType.Meal: + # if info.equipment is not None: + # graph.add_edge(info.equipment.name, info.name) + # for ingredient in info.needs: + # graph.add_edge(ingredient, info.name) + + # graph = nx.DiGraph() + # for item_name, item_info in self.item_info.items(): + # graph.add_node(item_name, type=item_info.type.name) + # if len(item_info.equipment) == 0: + # for item in item_info.needs: + # graph.add_edge(item, item_name) + # else: + # for item in item_info.needs: + # for equipment in item_info.equipment: + # graph.add_edge(item, equipment) + # graph.add_edge(equipment, item_name) + + # plt.figure(figsize=(10, 10)) + # pos = nx.nx_agraph.graphviz_layout(graph, prog="twopi", args="") + # nx.draw(graph, pos=pos, with_labels=True, node_color="white", node_size=500) + # print(nx.multipartite_layout(graph, subset_key="type", align="vertical")) + + # pos = { + # node: ( + # len(nx.ancestors(graph, node)) - len(nx.descendants(graph, node)), + # y, + # ) + # for y, node in enumerate(graph) + # } + # nx.draw( + # graph, + # pos=pos, + # with_labels=True, + # node_shape="s", + # node_size=500, + # node_color="white", + # ) + # TODO add colors for ingredients, equipment and meals + # plt.show() + def parse_layout_file(self, layout_file: Path): """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 @@ -207,18 +329,19 @@ class Environment: Args: action: The action to be performed """ - assert action.player in self.players.keys(), "Unknown player." player = self.players[action.player] if action.act_type == "movement": - self.perform_movement(player, action.action) + with self.lock: + self.perform_movement(player, action.action) else: counter = self.get_facing_counter(player) if player.can_reach(counter): if action.act_type == "pickup": - player.pick_action(counter) + with self.lock: + player.pick_action(counter) elif action.act_type == "interact": if action.action == "keydown": @@ -226,7 +349,10 @@ class Environment: player.last_interacted_counter = counter if action.action == "keyup": if player.last_interacted_counter: - player.perform_interact_hold_stop(player.last_interacted_counter) + with self.lock: + player.perform_interact_hold_stop( + player.last_interacted_counter + ) def get_closest_counter(self, point: np.ndarray): """Determines the closest counter for a given 2d-coordinate point in the env. @@ -405,7 +531,9 @@ class Environment: def add_player(self, player_name: str, pos: npt.NDArray = None): log.debug(f"Add player {player_name} to the game") - player = Player(player_name, pos) + player = Player( + player_name, player_config=self.environment_config["player_config"], pos=pos + ) self.players[player.name] = player if player.pos is None: if len(self.designated_player_positions) > 0: @@ -439,10 +567,12 @@ class Environment: and time limits. """ self.env_time += passed_time + with self.lock: for counter in self.counters: if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)): counter.progress(passed_time=passed_time, now=self.env_time) + self.order_and_score.progress(passed_time=passed_time, now=self.env_time) def get_state(self): """Get the current state of the game environment. The state here is accessible by the current python objects. @@ -450,7 +580,15 @@ class Environment: Returns: Dict of lists of the current relevant game objects. """ - return {"players": self.players, "counters": self.counters, "score": self.score} + return { + "players": self.players, + "counters": self.counters, + "score": self.order_and_score.score, + "orders": self.order_and_score.open_orders, + "ended": self.game_ended, + "env_time": self.env_time, + "remaining_time": max(self.env_time_end - self.env_time, timedelta(0)), + } def get_state_simple_json(self): """Get the current state of the game environment as a json-like nested dictionary. diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 4f85046ff9a100c8b0b95a43246a9acccaff4b04..63f803c86a1c2575c2cf2a8b8248e8c0a62d116a 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -1,15 +1,12 @@ import logging from collections import deque -from pathlib import Path -from typing import Optional +from typing import Optional, Any import numpy as np import numpy.typing as npt -import yaml -from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter -from overcooked_simulator.game_items import Item +from overcooked_simulator.game_items import Item, Plate log = logging.getLogger(__name__) @@ -24,9 +21,11 @@ class Player: def __init__( self, name: str, + player_config: dict[str, Any], pos: Optional[npt.NDArray[float]] = None, ): self.name: str = name + self.player_config = player_config if pos is not None: self.pos: npt.NDArray[float] = np.array(pos, dtype=float) else: @@ -34,12 +33,6 @@ class Player: self.holding: Optional[Item] = None - self.player_config_path: Path = Path( - ROOT_DIR / "game_content" / "player_config.yaml" - ) - with open(self.player_config_path, "r") as file: - self.player_config = yaml.safe_load(file) - self.radius: float = self.player_config["radius"] self.move_dist: int = self.player_config["move_dist"] self.interaction_range: int = self.player_config["interaction_range"] @@ -120,6 +113,8 @@ class Player: log.debug( f"Self: {self.holding}, {counter.__class__.__name__}: {counter.occupied_by}" ) + if isinstance(self.holding, Plate): + log.debug(self.holding.clean) def perform_interact_hold_start(self, counter: Counter): """Starts an interaction with the counter. Should be called for a diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index f004ecdd68d52be1e5d596c0bef5434948aa610b..5c5754d0745b24396ef0487a47da9ba42ee69703 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -98,9 +98,11 @@ class Simulator(Thread): overslept_in_ns = 0 self.env.reset_env_time() + last_step_start = time.time_ns() while not self.finished: step_start = time.time_ns() - self.step(timedelta(seconds=overslept_in_ns / 1_000_000_000)) + self.step(timedelta(seconds=(step_start - last_step_start) / 1_000_000_000)) + last_step_start = step_start step_duration = time.time_ns() - step_start time_to_sleep_ns = self.preferred_sleep_time_ns - ( diff --git a/setup.py b/setup.py index f03249639eb04f12e8b9bbd12d70ac9d792a9e19..df6cec6fdeed8e45f88a8e987cfb04f951c6f514 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( license="MIT license", long_description=readme + "\n\n" + history, include_package_data=True, - keywords=["aaambos", "overcooked_simulator"], + keywords=["overcooked_simulator"], name="overcooked_simulator", packages=find_packages(include=["overcooked_simulator", "overcooked_simulator.*"]), test_suite="tests", diff --git a/tests/test_start.py b/tests/test_start.py index 64fce11daf8fb14381605e96180e55b2db40e827..d6010a9e0e373c12673601c8f6a99c55651296d0 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 CuttableItem +from overcooked_simulator.game_items import Item from overcooked_simulator.overcooked_environment import Action, Environment from overcooked_simulator.simulation_runner import Simulator from overcooked_simulator.utils import create_init_env_time @@ -118,25 +118,27 @@ def test_collision_detection(): player2 = sim.env.players["p2"] sim.start() - assert not sim.env.detect_collision_counters(player1), "Should not collide" - assert not sim.env.detect_player_collision(player1), "Should not collide yet." - - assert not sim.env.detect_collision(player1), "Does not collide yet." - - player1.move_abs(counter_pos) - assert sim.env.detect_collision_counters( - player1 - ), "Player and counter at same pos. Not detected." - player2.move_abs(counter_pos) - assert sim.env.detect_player_collision( - player1 - ), "Players at same pos. Not detected." - - player1.move_abs(np.array([0, 0])) - assert sim.env.detect_collision_world_bounds( - player1 - ), "Player collides with world bounds." - sim.stop() + try: + assert not sim.env.detect_collision_counters(player1), "Should not collide" + assert not sim.env.detect_player_collision(player1), "Should not collide yet." + + assert not sim.env.detect_collision(player1), "Does not collide yet." + + player1.move_abs(counter_pos) + assert sim.env.detect_collision_counters( + player1 + ), "Player and counter at same pos. Not detected." + player2.move_abs(counter_pos) + assert sim.env.detect_player_collision( + player1 + ), "Players at same pos. Not detected." + + player1.move_abs(np.array([0, 0])) + assert sim.env.detect_collision_world_bounds( + player1 + ), "Player collides with world bounds." + finally: + sim.stop() def test_player_reach(): @@ -169,7 +171,7 @@ def test_pickup(): counter_pos = np.array([2, 2]) counter = Counter(counter_pos) - counter.occupied_by = CuttableItem(name="Tomato", item_info=None) + counter.occupied_by = Item(name="Tomato", item_info=None) sim.env.counters = [counter] sim.register_player("p1", np.array([2, 3])) @@ -217,36 +219,38 @@ def test_processing(): sim_frequency, ) sim.start() + try: + counter_pos = np.array([2, 2]) + counter = CuttingBoard( + counter_pos, + transitions={"Tomato": {"seconds": 1, "result": "ChoppedTomato"}}, + ) + sim.env.counters.append(counter) - counter_pos = np.array([2, 2]) - counter = CuttingBoard(counter_pos) - sim.env.counters.append(counter) + tomato = Item(name="Tomato", item_info=None) + sim.register_player("p1", np.array([2, 3])) + player = sim.env.players["p1"] + player.holding = tomato - tomato = CuttableItem(name="Tomato", item_info=None) - sim.register_player("p1", np.array([2, 3])) - player = sim.env.players["p1"] - player.holding = tomato + move = Action("p1", "movement", np.array([0, -1])) + pick = Action("p1", "pickup", "pickup") - move = Action("p1", "movement", np.array([0, -1])) - pick = Action("p1", "pickup", "pickup") + sim.enter_action(move) + sim.enter_action(pick) - sim.enter_action(move) - sim.enter_action(pick) - - hold_down = Action("p1", "interact", "keydown") - sim.enter_action(hold_down) + hold_down = Action("p1", "interact", "keydown") + sim.enter_action(hold_down) - assert not tomato.finished, "Tomato is not finished yet." + assert tomato.name != "ChoppedTomato", "Tomato is not finished yet." - sleep_time = (tomato.steps_needed / sim_frequency) + 0.1 - time.sleep(sleep_time) + time.sleep(1) - assert tomato.finished, "Tomato should be finished." + assert tomato.name == "ChoppedTomato", "Tomato should be finished." - button_up = Action("p1", "interact", "keyup") - sim.enter_action(button_up) - - sim.stop() + button_up = Action("p1", "interact", "keyup") + sim.enter_action(button_up) + finally: + sim.stop() def test_time_passed(): @@ -268,3 +272,27 @@ def test_time_passed(): assert ( env.env_time == create_init_env_time() + passed_time + passed_time_2 ), "Env time needs to be updated via the step function" + + +def test_time_limit(): + np.random.seed(42) + env = Environment( + ROOT_DIR / "game_content" / "environment_config.yaml", + layouts_folder / "empty.layout", + ROOT_DIR / "game_content" / "item_info.yaml", + ) + env.reset_env_time() + + assert not env.game_ended, "Game has not ended yet" + + passed_time = timedelta(seconds=10) + env.step(passed_time) + + assert not env.game_ended, "Game has not ended yet" + + passed_time_2 = timedelta( + seconds=(env.env_time_end - env.beginning_time).total_seconds() + ) + env.step(passed_time_2) + + assert env.game_ended, "Game has ended now."