From fd162778cfe722b712375733f3aa30e0e82d9131 Mon Sep 17 00:00:00 2001 From: Dominik Battefeld <dbattefeld@techfak.de> Date: Tue, 16 Jan 2024 10:23:13 +0100 Subject: [PATCH] Add previous work on config exchange --- overcooked_simulator/counters.py | 30 ++- .../game_content/item_info_new.yaml | 30 ++- overcooked_simulator/game_items.py | 187 +++++++++--------- .../gui_2d_vis/overcooked_gui.py | 26 ++- .../overcooked_environment.py | 91 ++++++++- overcooked_simulator/simulation_runner.py | 6 +- 6 files changed, 235 insertions(+), 135 deletions(-) diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 616ad694..fdc94f2b 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -16,7 +16,6 @@ import numpy as np import numpy.typing as npt from overcooked_simulator.game_items import ( - CuttableItem, Item, CookingEquipment, Meal, @@ -34,6 +33,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 +98,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.""" @@ -305,7 +323,7 @@ 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): diff --git a/overcooked_simulator/game_content/item_info_new.yaml b/overcooked_simulator/game_content/item_info_new.yaml index 11f0ae05..440e8d46 100644 --- a/overcooked_simulator/game_content/item_info_new.yaml +++ b/overcooked_simulator/game_content/item_info_new.yaml @@ -1,12 +1,24 @@ CuttingBoard: type: Equipment +Sink: + type: Equipment + Pot: type: Equipment Pan: type: Equipment +DirtyPlate: + type: Equipment + +Plate: + type: Equipment + needs: [ DirtyPlate ] + seconds: 2.0 + equipment: Sink + # -------------------------------------------------------------------------------- Tomato: @@ -27,30 +39,30 @@ Bun: ChoppedTomato: type: Ingredient needs: [ Tomato ] - steps: 500 + seconds: 2.0 equipment: CuttingBoard ChoppedLettuce: type: Ingredient needs: [ Lettuce ] - steps: 700 + seconds: 4.0 equipment: CuttingBoard ChoppedOnion: type: Ingredient needs: [ Onion ] - steps: 500 + seconds: 2.0 equipment: CuttingBoard ChoppedMeat: type: Ingredient needs: [ Meat ] - steps: 700 + seconds: 0.1 equipment: CuttingBoard CookedPatty: type: Ingredient - steps: 500 + seconds: 10.0 needs: [ ChoppedMeat ] equipment: Pan @@ -59,21 +71,21 @@ CookedPatty: Burger: type: Meal needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ] - equipment: [ ] + equipment: ~ Salad: type: Meal needs: [ ChoppedLettuce, ChoppedTomato ] - equipment: [ ] + equipment: ~ TomatoSoup: type: Meal - steps: 500 needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] + seconds: 3.0 equipment: Pot OnionSoup: type: Meal - steps: 500 needs: [ ChoppedOnion, ChoppedOnion, ChoppedOnion ] + seconds: 3.0 equipment: Pot diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 937d0617..d9ebae0e 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,43 +31,23 @@ 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: + kwargs = { + "name": self.name, + "item_info": self, + "clean": clean_plate, + "parts": parts, + } 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) + return Item(**kwargs) 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, - ) + return Plate(**kwargs) else: - return CookingEquipment(name=self.name, item_info=self) + return CookingEquipment(**kwargs) 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, - ) + return Meal(**kwargs) def add_start_meal_to_equipment(self, start_item: ItemInfo): self._start_meals.append(start_item) @@ -99,15 +80,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,60 +96,51 @@ class Item: def extra_repr(self): return "" + def can_combine(self, other): + 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 can_progress(self) -> bool: - return True - - def finished_call(self): - self.name = self.finished_name + def combine(self, other) -> Item | None: + pass - def reset(self): - self.finished = False - self.progressed_steps = 0 + 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 __repr__(self): - if not self.steps_needed or self.finished: - return f"{self.name}({self.extra_repr})" + if self.progress_equipment == equipment: + self.progress_percentage += percent + self.progress_percentage = min(self.progress_percentage, 1.0) else: - return f"{self.name}(progress={int(self.progressed_steps / self.steps_needed * 100)}%,{self.extra_repr})" - + log.warning( + f"{self.name} expected progress on {self.progress_equipment}, but got {percent * 100}% on {equipment}" + ) -class CuttableItem(ProgressibleItem): - """Class of item which can be processed by the cutting board.""" + def reset(self): + self.progress_equipment = None + self.progress_percentage = 0.0 class CookingEquipment(Item): - def __init__(self, content: Meal = None, *args, **kwargs): + def __init__(self, transitions: dict, *args, **kwargs): super().__init__(*args, **kwargs) - self.content = content + self.transitions = transitions + self.active_transition: Optional[dict] = None + self.content: Item | list[Item] = [] # list if cooking, meal item when done + log.debug(f"Initialize {self.name}: {self.transitions}") + def can_combine(self, other): + # already cooking or nothing to combine + if self.active_transition is not None or other is None: + return False + + # other extends ingredients for meal + ingredients = collections.Counter(item.name for item in self.content + [other]) + return any( + ingredients <= collections.Counter(transition["needs"]) + for transition in self.transitions.values() + ) + + """ def can_combine(self, other): if other is None: return False @@ -188,7 +159,25 @@ class CookingEquipment(Item): [other] + ([self.content] if self.content.progressed_steps else self.content.parts) ) + """ + def combine(self, other): + if isinstance(other, CookingEquipment): + ... + else: + self.content.append(other) + + ingredients = collections.Counter(item.name for item in self.content) + for result, transition in self.transitions.items(): + recipe = collections.Counter(transition["needs"]) + if ingredients == recipe: + self.active_transition = { + "seconds": transition["seconds"], + "result": Item(name=result, item_info=transition["info"]) + } + break + + """ def combine(self, other): if self.content is None: if isinstance(other, CookingEquipment): @@ -223,16 +212,17 @@ class CookingEquipment(Item): ) 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() - ) + def can_progress(self) -> bool: + return self.active_transition is not None - def progress(self): - self.content.progress() + 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 = self.active_transition["result"] + self.reset() def can_release_content(self) -> bool: return ( @@ -250,8 +240,12 @@ class CookingEquipment(Item): def extra_repr(self): return self.content + def reset(self): + super().reset() + self.active_transition = None + -class Meal(ProgressibleItem): +class Meal(Item): def __init__(self, parts=None, *args, **kwargs): super().__init__(*args, **kwargs) self.parts = [] if parts is None else parts @@ -287,15 +281,10 @@ class Meal(ProgressibleItem): class Plate(CookingEquipment): - def __init__( - self, clean, steps_needed, finished, content: Meal = None, *args, **kwargs - ): + def __init__(self, clean, content: Meal = None, *args, **kwargs): super().__init__(content, *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 def finished_call(self): self.clean = True diff --git a/overcooked_simulator/gui_2d_vis/overcooked_gui.py b/overcooked_simulator/gui_2d_vis/overcooked_gui.py index 7c910c07..c2728c76 100644 --- a/overcooked_simulator/gui_2d_vis/overcooked_gui.py +++ b/overcooked_simulator/gui_2d_vis/overcooked_gui.py @@ -15,7 +15,6 @@ from scipy.spatial import KDTree from overcooked_simulator import ROOT_DIR from overcooked_simulator.counters import Counter from overcooked_simulator.game_items import ( - ProgressibleItem, Item, CookingEquipment, Meal, @@ -408,8 +407,8 @@ class PyGameGUI: pos, self.visualization_config[item.name]["parts"], scale=scale ) - if isinstance(item, (ProgressibleItem, Plate)) and not item.finished: - self.draw_progress_bar(pos, item.progressed_steps, item.steps_needed) + if isinstance(item, (Item, Plate)) and item.progress_percentage > 0.0: + self.draw_progress_bar(pos, item.progress_percentage) if isinstance(item, CookingEquipment) and item.content: self.draw_item(pos, item.content) @@ -423,18 +422,17 @@ class PyGameGUI: 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, current, needed): + def draw_progress_bar(self, pos, percent): """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, - ) - pygame.draw.rect(self.game_screen, colors["green1"], progress_bar) + 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, + ) + pygame.draw.rect(self.game_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. diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 8f5adf96..367e59bf 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -22,7 +22,7 @@ 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.player import Player from overcooked_simulator.utils import create_init_env_time @@ -88,12 +88,21 @@ class Environment: self.item_info_path: Path = item_info_path self.item_info = self.load_item_info() + self.validate_item_info() self.game_score = GameScore() 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), "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]), @@ -108,12 +117,28 @@ class Environment: "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), @@ -151,6 +176,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 diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 8ad41e28..38fe465d 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -30,7 +30,7 @@ class Simulator(Thread): env_config_path, layout_path, frequency: int, - item_info_path=ROOT_DIR / "game_content" / "item_info.yaml", + item_info_path=ROOT_DIR / "game_content" / "item_info_new.yaml", seed: int = 8654321, ): # TODO look at https://builtin.com/data-science/numpy-random-seed to change to other random @@ -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 - ( -- GitLab