diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index ed6bab5736ea6859db335d8cd49288baa0f85c26..3f68bae69948eba8f059f73e68bbec1554f94c90 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 dd20f8914e520dce0148565b825dc83bbfefaf6e..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..a2df260836a62fed83a32d3e35d84fd78f74a96e --- /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 822ce543bcb5a21e41d8bd7d099ff914820fd931..a0356cccbf381077270518ed5c418cc3f3b64b6c 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 9524338b1a38d513a42ddfd60d82ef74d9093a67..05d6f0c0c62a9c2ba0df540c5a8b68c15d185a11 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 4161e1d47e3559f2c265d22ddeb5a34c1b75be61..23ecb41865b64bc16b88061e13f47abf028912c1 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 a086961c2a3a3cb0e131ed4d23799386dd6b5315..1540d625229a6b262baa886f9c20ccda11d34397 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 758f7372a2bc73e12d5c3e88f34fc1ca70d282d5..67c6f455e8e089ab03a173898b71fa5649ebc7a9 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 31d9d3774dd5425a6579ba8ea4ded7e1ac8dfa26..6c9cf367ca96478711ec0aa717bc16c22a88f0d9 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 e23635c29b84f756973474fedaf8b15309f6517b..729bd43e31fa644dea3cf52504c3e068e60035c4 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)