From ad544b0a5bea2f477700f456112b43118007bae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6der?= <fschroeder@techfak.uni-bielefeld.de> Date: Thu, 14 Dec 2023 19:01:23 +0100 Subject: [PATCH] PlateReturn is now PlateDispenser (just a Dispenser), Changed path concatenation, Created dataclass for item information, pass item config path via argument of the environment, no tomato class anymore, no pot class anymore, no soup and pan class, now everything is either a meal, cuttableitem, item, cookingequipment or plate. maybe do the plate as a cooking equipment next. --- overcooked_simulator/counters.py | 81 ++------- .../game_content/item_combinations.yaml | 26 --- .../game_content/item_info.yaml | 37 ++++ overcooked_simulator/game_items.py | 167 +++++++++++------- overcooked_simulator/main.py | 3 +- .../overcooked_environment.py | 32 +++- overcooked_simulator/pygame_gui/pygame_gui.py | 41 ++--- .../pygame_gui/visualization.yaml | 4 +- overcooked_simulator/simulation_runner.py | 10 +- tests/test_start.py | 3 +- 10 files changed, 215 insertions(+), 189 deletions(-) delete mode 100644 overcooked_simulator/game_content/item_combinations.yaml create mode 100644 overcooked_simulator/game_content/item_info.yaml diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index ed6bab57..3f68bae6 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -12,11 +12,9 @@ from overcooked_simulator.game_items import ( CuttableItem, Item, Plate, - Pot, CookingEquipment, - Soup, + Meal, ) -from overcooked_simulator.game_items import item_loopkup class Counter: @@ -128,7 +126,7 @@ class ServingWindow(Counter): def can_score(self, item): if isinstance(item, Plate) and item.holds is not None: - if isinstance(item.holds, Soup): + if isinstance(item.holds, Meal): return item.holds.finished elif item.holds.name == "Salad": return True @@ -141,79 +139,31 @@ class ServingWindow(Counter): pass -class PlateReturn(Counter): - def __init__(self, pos): - super().__init__(pos) - self.occupied_by = [Plate()] - - def pick_up(self, on_hands: bool = True): - """Gets called upon a player performing the pickup action. Gives back a plate (possibly with ingredient. - - Returns: A plate possibly with an ingredient on it. - - """ - give_player = self.occupied_by.pop() - if not self.occupied_by: - self.occupied_by.append(Plate()) - return give_player - - def drop_off(self, item: Item) -> Item | None: - """Takes the ingredient dropped of by the player. - - Args: - item: The ingredient to be placed on the counter. - """ - if isinstance(item, Plate): - if self.occupied_by[-1].holds: - return item - self.occupied_by.append(item) - return None - if self.occupied_by[-1].can_combine(item): - return self.occupied_by[-1].combine(item) - return item - - def can_drop_off(self, item: Item) -> bool: - """Checks whether an ingredient by the player can be dropped of. - - Args: - item: The ingredient for which to check, if it can be placed on the counter. - - Returns: True if the ingredient can be placed on the counter, False if not. - - """ - # possibility to drop off empty plate on empty plate return - return ( - isinstance(self.occupied_by[-1], Plate) and isinstance(item, Plate) - ) or self.occupied_by[-1].can_combine(item) - - class Dispenser(Counter): def __init__(self, pos, dispensing): self.dispensing = dispensing super().__init__( pos, - CuttableItem( - name=self.dispensing, - finished_name=item_loopkup[self.dispensing]["finished_name"], - ), + self.dispensing.create_item(), ) def pick_up(self, on_hands: bool = True): - returned = CuttableItem( - name=self.dispensing, - finished_name=item_loopkup[self.dispensing]["finished_name"], - ) - print(self.occupied_by) - return returned + new_dispensing = self.dispensing.create_item() + if self.occupied_by != new_dispensing: + old_dispensing = self.occupied_by + self.occupied_by = new_dispensing + return old_dispensing + return new_dispensing def drop_off(self, item: Item) -> Item | None: - return None + if self.occupied_by.can_combine(item): + return self.occupied_by.combine(item) def can_drop_off(self, item: Item) -> bool: - return False + return self.occupied_by.can_combine(item) def __repr__(self): - return f"{self.dispensing}Dispenser" + return f"{self.dispensing.name}Dispenser" class Trash(Counter): @@ -234,11 +184,6 @@ class Trash(Counter): class Stove(Counter): - def __init__(self, pos: npt.NDArray[float], occupied_by: Optional[Item] = ...): - if occupied_by is ...: - occupied_by = Pot() - super().__init__(pos, occupied_by) - def progress(self): """Called by environment step function for time progression""" if ( diff --git a/overcooked_simulator/game_content/item_combinations.yaml b/overcooked_simulator/game_content/item_combinations.yaml deleted file mode 100644 index dd20f891..00000000 --- a/overcooked_simulator/game_content/item_combinations.yaml +++ /dev/null @@ -1,26 +0,0 @@ -Tomato: - type: Cuttable - needs: Tomato - finished_name: ChoppedTomato - -Lettuce: - type: Cuttable - needs: Lettuce - finished_name: ChoppedLettuce - -RawPatty: - type: Cuttable - needs: RawSteak - -Burger: - type: Meal - needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ] - -Salad: - type: Meal - needs: [ Lettuce, Tomato ] - -TomatoSoup: - type: Soup - needs: [ Tomato, Tomato, Tomato ] - diff --git a/overcooked_simulator/game_content/item_info.yaml b/overcooked_simulator/game_content/item_info.yaml new file mode 100644 index 00000000..a2df2608 --- /dev/null +++ b/overcooked_simulator/game_content/item_info.yaml @@ -0,0 +1,37 @@ +Tomato: + type: Ingredient + needs: Tomato + is_cuttable: True + steps_needed: 500 + +Lettuce: + type: Ingredient + needs: Lettuce + is_cuttable: True + steps_needed: 500 + +RawPatty: + type: Ingredient + needs: RawSteak + +Burger: + type: Meal + needs: [ Bun, ChoppedLettuce, ChoppedTomato, CookedPatty ] + +Salad: + type: Meal + needs: [ Lettuce, Tomato ] + equipment: Plate + +TomatoSoup: + type: Meal + finished_progress_name: TomatoSoup + steps_needed: 500 + needs: [ Tomato, Tomato, Tomato ] + equipment: Pot + +Plate: + type: Equipment + +Pot: + type: Equipment diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 822ce543..a0356ccc 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -1,25 +1,85 @@ from __future__ import annotations -import yaml - -from overcooked_simulator import ROOT_DIR - -with open(ROOT_DIR / "game_content/item_combinations.yaml", "r") as file: - item_loopkup = yaml.safe_load(file) +import dataclasses +from enum import Enum + + +class ItemType(Enum): + Ingredient = "Ingredient" + Meal = "Meal" + Equipment = "Equipment" + + +@dataclasses.dataclass +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*") + needs: list[ItemInfo] = dataclasses.field(compare=False, default_factory=list) + equipment: ItemInfo | None = dataclasses.field(compare=False, default=None) + + _start_items: list[ItemInfo] = dataclasses.field( + compare=False, default_factory=list + ) + + 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) -> 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 self.name == "Plate": + return Plate(item_info=self) + if self.name == "Pot": + 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, + ) + + def add_start_item_to_equipment(self, start_item: ItemInfo): + self._start_items.append(start_item) + + def can_start_meal(self, start_item: Item): + # TODO check specific order / only specific start items + return any( + [start_item.name == s for meal in self._start_items for s in meal.needs] + ) -combinables = {} -for key, item in item_loopkup.items(): - if item["type"] in ["Meal", "Soup"]: - combinables[key] = item["needs"] -print(combinables) + def start_meal(self, start_item: Item) -> Item: + for meal in self._start_items: + for s in meal.needs: + if s == start_item.name: + return meal.create_item() class Item: """Base class for game items which can be held by a player.""" - def __init__(self, name: str = None, *args, **kwargs): - super().__init__(*args, **kwargs) + 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 @@ -30,14 +90,17 @@ class Item: def __repr__(self): return f"{self.name}({self.extra_repr})" + def __eq__(self, other): + return other and self.name == other.name + @property def extra_repr(self): return "" class Plate(Item): - def __init__(self, holds: Item = None): - super().__init__() + def __init__(self, holds: Item = None, *args, **kwargs): + super().__init__(*args, name="Plate", **kwargs) self.clean = True self.holds = holds @@ -54,6 +117,14 @@ class Plate(Item): return other self.holds = other + def __eq__(self, other): + return ( + other + and self.name == other.name + and self.clean == other.clean + and self.holds == other.holds + ) + @property def extra_repr(self): return self.holds @@ -106,18 +177,6 @@ class ProgressibleItem(Item): class CuttableItem(ProgressibleItem): """Class of item which can be processed by the cutting board.""" - pass - - -class Tomato(CuttableItem, Item): - """Item class representing a tomato. Can be cut on the cutting board""" - - def can_combine(self, other): - return False - - def __init__(self): - super().__init__(steps_needed=500) - class CookingEquipment(Item): def __init__(self, content: Meal = None, *args, **kwargs): @@ -127,13 +186,13 @@ class CookingEquipment(Item): def can_combine(self, other): if self.content is None: # TODO check other is start of a meal, create meal - return True + return self.item_info.can_start_meal(other) return self.content.can_combine(other) def combine(self, other): if not self.content: # find starting meal for other - self.content = Soup() + self.content = self.item_info.start_meal(other) self.content.combine(other) def can_progress(self, counter_type="Stove") -> bool: @@ -163,46 +222,36 @@ class CookingEquipment(Item): return self.content -class Pot(CookingEquipment): - def __init__(self, holds: Meal = None): - super().__init__() - - -class Meal(Item): +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: - return ( - isinstance(other, Tomato) - and all([isinstance(o, other.__class__) for o in self.parts]) - and len(self.parts) < 3 - ) # rules + 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 def combine(self, other): self.parts.append(other) - @property - def extra_repr(self): - return self.parts - - -class Soup(ProgressibleItem, Meal): - def __init__(self): - super().__init__(finished_name="CookedSoup") - def can_progress(self) -> bool: - return len(self.parts) == 3 - - -class Pan(CookingEquipment): - def __init__(self): - super().__init__(steps_needed=500) + return self.item_info.steps_needed and len(self.item_info.needs) == len( + self.parts + ) - def can_combine(self, other): - return False + def finished_call(self): + super().finished_call() - def combine(self, other): - pass + @property + def extra_repr(self): + return self.parts diff --git a/overcooked_simulator/main.py b/overcooked_simulator/main.py index 9524338b..05d6f0c0 100644 --- a/overcooked_simulator/main.py +++ b/overcooked_simulator/main.py @@ -1,5 +1,4 @@ import sys -from pathlib import Path import numpy as np import pygame @@ -11,7 +10,7 @@ from overcooked_simulator.simulation_runner import Simulator def main(): - simulator = Simulator(Path(ROOT_DIR, "game_content/layouts", "basic.layout"), 600) + simulator = Simulator(ROOT_DIR / "game_content" / "layouts" / "basic.layout", 600) player_one_name = "p1" player_two_name = "p2" simulator.register_player(Player(player_one_name, np.array([350.0, 200.0]))) diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 4161e1d4..23ecb418 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -2,6 +2,10 @@ from __future__ import annotations from typing import TYPE_CHECKING +import yaml + +from overcooked_simulator.game_items import ItemInfo + if TYPE_CHECKING: from overcooked_simulator.player import Player from pathlib import Path @@ -14,7 +18,6 @@ from overcooked_simulator.counters import ( Trash, Dispenser, ServingWindow, - PlateReturn, Stove, ) @@ -55,10 +58,12 @@ class Environment: # TODO Abstract base class for different environments """ - def __init__(self, layout_path): + def __init__(self, layout_path, item_info_path): self.players: dict[str, Player] = {} self.counter_side_length: int = 40 self.layout_path: Path = layout_path + self.item_info_path: Path = item_info_path + self.item_info = self.load_item_info() self.game_score = GameScore() self.SYMBOL_TO_CHARACTER_MAP = { @@ -66,11 +71,14 @@ class Environment: "B": CuttingBoard, "X": Trash, "W": lambda pos: ServingWindow(pos, self.game_score), - "T": lambda pos: Dispenser(pos, "Tomato"), - "L": lambda pos: Dispenser(pos, "Lettuce"), - "P": PlateReturn, + "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]), + "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]), + "P": lambda pos: Dispenser(pos, self.item_info["Plate"]), "E": None, - "U": Stove, # Stove with pot: U because it looks like a pot + "U": lambda pos: Stove( + pos, + self.item_info["Pot"].create_item(), + ), # Stove with pot: U because it looks like a pot } self.counters: list[Counter] = self.create_counters(self.layout_path) @@ -78,6 +86,18 @@ class Environment: self.world_width: int = 800 self.world_height: int = 600 + def load_item_info(self) -> dict[str, ItemInfo]: + with open(self.item_info_path, "r") as file: + item_lookup = yaml.safe_load(file) + for item_name in item_lookup: + item_lookup[item_name] = ItemInfo(name=item_name, **item_lookup[item_name]) + + for item_name, item_info in item_lookup.items(): + if item_info.equipment: + item_info.equipment = item_lookup[item_info.equipment] + item_info.equipment.add_start_item_to_equipment(item_info) + return item_lookup + def create_counters(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/pygame_gui/pygame_gui.py b/overcooked_simulator/pygame_gui/pygame_gui.py index a086961c..1540d625 100644 --- a/overcooked_simulator/pygame_gui/pygame_gui.py +++ b/overcooked_simulator/pygame_gui/pygame_gui.py @@ -1,5 +1,4 @@ import math -from pathlib import Path import numpy as np import numpy.typing as npt @@ -13,7 +12,6 @@ from overcooked_simulator.game_items import ( Item, CookingEquipment, Meal, - Soup, ) from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.simulation_runner import Simulator @@ -84,10 +82,10 @@ class PyGameGUI: for player_name, keys in zip(self.player_names, self.player_keys) ] - with open(ROOT_DIR / "pygame_gui/visualization.yaml", "r") as file: + with open(ROOT_DIR / "pygame_gui" / "visualization.yaml", "r") as file: self.visualization_config = yaml.safe_load(file) - self.images_path = Path(ROOT_DIR, "pygame_gui", "images") + self.images_path = ROOT_DIR / "pygame_gui" / "images" def send_action(self, action: Action): """Sends an action to the game environment. @@ -179,7 +177,7 @@ class PyGameGUI: else: img_path = self.visualization_config["Cook"]["parts"][0]["path"] image = pygame.image.load( - ROOT_DIR / Path("pygame_gui") / img_path + ROOT_DIR / "pygame_gui" / img_path ).convert_alpha() rel_x, rel_y = player.facing_direction angle = -np.rad2deg(math.atan2(rel_y, rel_x)) + 90 @@ -206,7 +204,7 @@ class PyGameGUI: part_type = part["type"] if part_type == "image": image = pygame.image.load( - ROOT_DIR / Path("pygame_gui") / parts[0]["path"] + ROOT_DIR / "pygame_gui" / parts[0]["path"] ).convert_alpha() size = parts[0]["size"] @@ -267,25 +265,13 @@ class PyGameGUI: if isinstance(item, CookingEquipment) and item.content: self.draw_item(pos, item.content) - if isinstance(item, Meal) and item.parts and not isinstance(item, Soup): - for i, o in enumerate(item.parts): - self.draw_item(np.abs([pos[0], pos[1] - (i * 5)]), o) - - if isinstance(item, Soup) and item.parts: - if item.parts: - if not item.finished: - match len(item.parts): - case 1: - pygame.draw.circle(self.screen, RED, pos, 4) - case 2: - pygame.draw.circle(self.screen, RED, pos, 8) - case 3: - pygame.draw.circle(self.screen, RED, pos, 12) - else: + if isinstance(item, Meal): + if "Soup" in item.name: + if item.finished: if item.name in self.visualization_config: image = pygame.image.load( ROOT_DIR - / Path("pygame_gui") + / "pygame_gui" / self.visualization_config[item.name]["parts"][0]["path"] ).convert_alpha() @@ -295,6 +281,17 @@ class PyGameGUI: rect = image.get_rect() rect.center = pos self.screen.blit(image, rect) + elif item.parts: + match len(item.parts): + case 1: + pygame.draw.circle(self.screen, RED, pos, 4) + case 2: + pygame.draw.circle(self.screen, RED, pos, 8) + case 3: + pygame.draw.circle(self.screen, RED, pos, 12) + else: + for i, o in enumerate(item.parts): + self.draw_item(np.abs([pos[0], pos[1] - (i * 5)]), o) def draw_progress_bar(self, pos, current, needed): """Visualize progress of progressing item as a green bar under the item.""" diff --git a/overcooked_simulator/pygame_gui/visualization.yaml b/overcooked_simulator/pygame_gui/visualization.yaml index 758f7372..67c6f455 100644 --- a/overcooked_simulator/pygame_gui/visualization.yaml +++ b/overcooked_simulator/pygame_gui/visualization.yaml @@ -17,7 +17,7 @@ CuttingBoard: center_offset: [ +6, -8 ] color: [ 120, 120, 120 ] -PlateReturn: +PlateDispenser: parts: - type: "rect" height: 38 @@ -129,7 +129,7 @@ TomatoSoup3of3: radius: 12 color: [ 255, 0, 0 ] -CookedSoup: +TomatoSoup: parts: - type: "image" path: "images/tomato_soup.png" diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 31d9d377..6c9cf367 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -1,6 +1,7 @@ import time from threading import Thread +from overcooked_simulator import ROOT_DIR from overcooked_simulator.overcooked_environment import Environment, Action from overcooked_simulator.player import Player @@ -19,12 +20,17 @@ class Simulator(Thread): ``` """ - def __init__(self, env_layout_path, frequency: int): + def __init__( + self, + env_layout_path, + frequency: int, + item_info_path=ROOT_DIR / "game_content" / "item_info.yaml", + ): self.finished: bool = False self.step_frequency: int = frequency self.preferred_sleep_time_ns: float = 1e9 / self.step_frequency - self.env: Environment = Environment(env_layout_path) + self.env: Environment = Environment(env_layout_path, item_info_path) super().__init__() diff --git a/tests/test_start.py b/tests/test_start.py index e23635c2..729bd43e 100644 --- a/tests/test_start.py +++ b/tests/test_start.py @@ -1,5 +1,4 @@ import time -from pathlib import Path import numpy as np import pytest @@ -11,7 +10,7 @@ from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.player import Player from overcooked_simulator.simulation_runner import Simulator -layouts_folder = Path(ROOT_DIR / "game_content/layouts") +layouts_folder = ROOT_DIR / "game_content" / "layouts" @pytest.fixture(autouse=True) -- GitLab