diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 9ae5405596c1d93f9bbec103c3d2de25eece964e..6c0b658572ca9ba6a718083b07b7314153eb1feb 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -1,10 +1,13 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: - from overcooked_simulator.overcooked_environment import GameScore + from overcooked_simulator.overcooked_environment import ( + GameScore, + PlateManager, + SinkManager, + ) import numpy as np import numpy.typing as npt @@ -14,10 +17,10 @@ from overcooked_simulator.game_items import ( Item, CookingEquipment, Meal, + Plate, + ItemInfo, ) -log = logging.getLogger(__name__) - class Counter: """Simple class for a counter at a specified position (center of counter). Can hold things on top.""" @@ -115,15 +118,18 @@ class CuttingBoard(Counter): class ServingWindow(Counter): - def __init__(self, pos, game_score: GameScore): + def __init__(self, pos, game_score: GameScore, plate_manager: PlateManager): self.game_score = game_score + self.plate_manager = plate_manager super().__init__(pos) def drop_off(self, item) -> Item | None: reward = 5 - log.debug(f"Drop off {item}") + print(item) # TODO define rewards self.game_score.increment_score(reward) + self.plate_manager.number_returned_plates += 1 + self.plate_manager.update_plate_return() return None def can_score(self, item): @@ -171,6 +177,37 @@ class Dispenser(Counter): return f"{self.dispensing.name}Dispenser" +class DirtyPlateReturn(Counter): + def __init__(self, pos, dispensing, plate_manager: PlateManager): + self.plate_manager = plate_manager + self.dispensing = dispensing + super().__init__(pos) + self.occupied_by = [ + self.dispensing.create_item() + for _ in range(self.plate_manager.number_returned_plates) + ] + + def pick_up(self, on_hands: bool = True): + return_this = self.occupied_by.pop() + if self.plate_manager.number_returned_plates > 1: + self.plate_manager.number_returned_plates -= 1 + else: + self.occupied_by = [] + return return_this + + def can_drop_off(self, item: Item) -> bool: + return False + + def update(self): + if self.plate_manager.number_returned_plates == 0: + self.occupied_by = [] + else: + self.occupied_by.append(self.dispensing.create_item()) + + def __repr__(self): + return "PlateReturn" + + class Trash(Counter): def pick_up(self, on_hands: bool = True): pass @@ -200,3 +237,69 @@ class Stove(Counter): and self.occupied_by.can_progress() ): self.occupied_by.progress() + + +class Sink(Counter): + def __init__(self, pos, sink_manager): + super().__init__(pos) + self.progressing = False + self.sink_manager: SinkManager = sink_manager + self.occupied_by = None + + # TODO: can put multiple things in here and hold the button to do all. + + def progress(self): + """Called by environment step function for time progression""" + if self.progressing: + if isinstance(self.occupied_by, Plate): + self.occupied_by.progress() + if self.occupied_by.finished: + self.progressing = False + self.occupied_by = None + self.sink_manager.update_move_plate() + + def start_progress(self): + """Starts the cutting process.""" + self.progressing = True + + def pause_progress(self): + """Pauses the cutting process""" + self.progressing = False + + def interact_start(self): + """Handles player interaction, starting to hold key down.""" + print(self.occupied_by) + if isinstance(self.occupied_by, Plate): + print(self.occupied_by.can_progress()) + self.start_progress() + + def interact_stop(self): + """Handles player interaction, stopping to hold key down.""" + self.pause_progress() + + def can_drop_off(self, item: Item) -> bool: + return isinstance(item, Plate) and not item.clean + + +class SinkAddon(Counter): + def __init__(self, pos, dispensing: ItemInfo): + super().__init__(pos) + self.dispensing = dispensing + self.number_clean_plates = 0 + self.occupied_by = [ + self.dispensing.create_item() for _ in range(self.number_clean_plates) + ] + + def can_drop_off(self, item: Item) -> bool: + return False + + def add_clean_plate(self): + self.number_clean_plates += 1 + plate = self.dispensing.create_item() + if isinstance(plate, Plate): + plate.finished_call() + self.occupied_by.append(plate) + + def pick_up(self, on_hands: bool = True): + return_this = self.occupied_by.pop() + return return_this diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index d1df66da5d0e0c780ce91c3958061dc8edafe43a..ceaf3f69891ac34444392c8922b0d13f93a1d00b 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -1,3 +1,4 @@ counter_side_length: 40 world_width: 800 world_height: 600 +max_number_plates: 25 \ 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 4693e59d56ecc6eadc4c8086dcbb3b6f82782f77..f0af702310f807fbb6f36a26d834c70e4a27a8b5 100644 --- a/overcooked_simulator/game_content/item_info.yaml +++ b/overcooked_simulator/game_content/item_info.yaml @@ -22,14 +22,14 @@ Burger: Salad: type: Meal - needs: [ ChoppedLettuce, ChoppedTomato ] + needs: [ ChoppedLettuce, Tomato ] equipment: Plate TomatoSoup: type: Meal finished_progress_name: TomatoSoup steps_needed: 500 - needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] + needs: [ Tomato, Tomato, Tomato ] equipment: Pot OnionSoup: @@ -41,6 +41,8 @@ OnionSoup: Plate: type: Equipment + is_cuttable: True + steps_needed: 200 Pot: type: Equipment diff --git a/overcooked_simulator/game_content/layouts/basic.layout b/overcooked_simulator/game_content/layouts/basic.layout index 8d31dac9a5076fa7fccf32ce6f151a87fabd28b1..6c0ad9c2d10fd0bb45f85b65bf757e38a347743a 100644 --- a/overcooked_simulator/game_content/layouts/basic.layout +++ b/overcooked_simulator/game_content/layouts/basic.layout @@ -1,11 +1,11 @@ _________________ -_CCUCTNLCC_______ -_C_______C_______ -_C_______C_______ +_##U#TNL##_______ +_#_______#_______ +_#_______#_______ _W_______________ -_C__A__A_________ +_#__A__A_________ _P_______________ -_C_______C_______ -_C_______X_______ -_CCBBCCCCC_______ +_#_______#_______ +_#_______X_______ +_##BB##S+#_______ _________________ diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 55b6f745620b3a09095d691a5944fd2a5d5ef405..621819dc8209d14c49de37ba04fd866132e6df30 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -45,7 +45,16 @@ class ItemInfo: ) return Item(name=self.name, item_info=self) case ItemType.Equipment: - return CookingEquipment(name=self.name, item_info=self) + if "Plate" in self.name: + return Plate( + name=self.name, + steps_needed=self.steps_needed, + finished=False, + item_info=self, + clean="Clean" in self.name, + ) + else: + return CookingEquipment(name=self.name, item_info=self) case ItemType.Meal: return Meal( name=self.name, @@ -153,7 +162,7 @@ class CookingEquipment(Item): if isinstance(other, CookingEquipment): return other.can_release_content() # TODO check other is start of a meal, create meal - if isinstance(other, Meal) and self.name == "Plate": + 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) return self.content.can_combine(other) @@ -163,7 +172,7 @@ class CookingEquipment(Item): if isinstance(other, CookingEquipment): self.content = other.release() return other - if isinstance(other, Meal) and self.name == "Plate": + if isinstance(other, Meal) and "Plate" in self.name: self.content = other return # find starting meal for other @@ -230,3 +239,42 @@ class Meal(ProgressibleItem): @property def extra_repr(self): return self.parts + + +class Plate(CookingEquipment): + def __init__( + self, clean, steps_needed, finished, content: Meal = None, *args, **kwargs + ): + super().__init__(content, *args, **kwargs) + self.clean = clean + self.name = self.__repr__() + self.steps_needed = steps_needed + self.finished = finished + self.progressed_steps = steps_needed if finished else 0 + + def finished_call(self): + self.name = "CleanPlate" + self.clean = True + + 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, counter_type="Sink") -> bool: + return not self.clean + + def can_combine(self, other): + return super().can_combine(other) and self.clean + + def combine(self, other): + return super().combine(other) + + def __repr__(self): + if self.clean: + return "CleanPlate" + else: + return "DirtyPlate" diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 599b35d411082aff10444576665fd54b1cacde4f..7fb7f3fc93c66233a2f1a0e652d5757ab8e8352b 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import random from pathlib import Path +from typing import Optional import numpy as np import numpy.typing as npt @@ -16,6 +17,9 @@ from overcooked_simulator.counters import ( Dispenser, ServingWindow, Stove, + Sink, + DirtyPlateReturn, + SinkAddon, ) from overcooked_simulator.game_items import ItemInfo # if TYPE_CHECKING: @@ -24,18 +28,6 @@ from overcooked_simulator.player import Player 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.""" @@ -53,6 +45,48 @@ 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 PlateManager: + def __init__(self, max_number_plates): + self.max_number_plates = max_number_plates + self.number_returned_plates = max_number_plates + self.plates_out_of_kitchen = 0 + + self.dirty_plate_return = None + + def return_plate(self): + self.number_returned_plates += 1 + + def register_plate_return(self, plate_return: DirtyPlateReturn): + self.dirty_plate_return = plate_return + + def update_plate_return(self): + self.dirty_plate_return.update() + + +class SinkManager: + def __init__(self): + self.sink: Optional[Sink] = None + self.sink_addon: Optional[SinkAddon] = None + + def register_sink_addon(self, sink_addon: SinkAddon): + self.sink_addon = sink_addon + + def update_move_plate(self): + self.sink_addon.add_clean_plate() + + class Environment: """Environment class which handles the game logic for the overcooked-inspired environment. @@ -72,14 +106,19 @@ class Environment: self.item_info = self.load_item_info() self.game_score = GameScore() + self.plate_manager = PlateManager(environment_config["max_number_plates"]) + self.sink_manager = SinkManager() + self.SYMBOL_TO_CHARACTER_MAP = { - "C": Counter, + "#": Counter, # because # looks a bit like a counter "B": CuttingBoard, "X": Trash, - "W": lambda pos: ServingWindow(pos, self.game_score), + "W": lambda pos: ServingWindow(pos, self.game_score, self.plate_manager), "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"]), + "P": lambda pos: DirtyPlateReturn( + pos, self.item_info["Plate"], self.plate_manager + ), "N": lambda pos: Dispenser(pos, self.item_info["Onion"]), # N for oNioN "_": "Free", "A": "Agent", @@ -87,6 +126,8 @@ class Environment: pos, self.item_info["Pot"].create_item(), ), # Stove with pot: U because it looks like a pot + "S": lambda pos: Sink(pos, self.sink_manager), + "+": lambda pos: SinkAddon(pos, self.item_info["Plate"]), } ( @@ -126,6 +167,8 @@ class Environment: designated_player_positions: list[npt.NDArray] = [] free_positions: list[npt.NDArray] = [] + # TODO: Sink and sink addon have to be next to each other! + with open(layout_file, "r") as layout_file: lines = layout_file.readlines() for line in lines: @@ -137,7 +180,11 @@ class Environment: counter_class = self.SYMBOL_TO_CHARACTER_MAP[character] if not isinstance(counter_class, str): counter = counter_class(pos) + if isinstance(counter, DirtyPlateReturn): + self.plate_manager.register_plate_return(counter) counters.append(counter) + if isinstance(counter, SinkAddon): + self.sink_manager.register_sink_addon(counter) else: if counter_class == "Agent": designated_player_positions.append( @@ -396,7 +443,7 @@ class Environment: and time limits. """ for counter in self.counters: - if isinstance(counter, (CuttingBoard, Stove)): + if isinstance(counter, (CuttingBoard, Stove, Sink)): counter.progress() def get_state(self): diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 5e0fa68804c7513d1b6f081d89f9bec4cd47a806..49317e405e3e1e3688a85eb922af718c412fd4c1 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -107,7 +107,7 @@ class Player: elif counter.can_drop_off(self.holding): self.holding = counter.drop_off(self.holding) - elif self.holding.can_combine(counter.occupied_by): + elif not isinstance(counter.occupied_by, list) and self.holding.can_combine(counter.occupied_by): returned_by_counter = counter.pick_up(on_hands=False) self.holding.combine(returned_by_counter) diff --git a/overcooked_simulator/pygame_gui/images/plate_clean.png b/overcooked_simulator/pygame_gui/images/plate_clean.png new file mode 100644 index 0000000000000000000000000000000000000000..02c23be7bd183e8efa6222cdbccf11c6f7c2ea20 Binary files /dev/null and b/overcooked_simulator/pygame_gui/images/plate_clean.png differ diff --git a/overcooked_simulator/pygame_gui/images/plate_dirty.png b/overcooked_simulator/pygame_gui/images/plate_dirty.png new file mode 100644 index 0000000000000000000000000000000000000000..f19ce7e3448af63ca00c8d0f23c9ce5336a8d211 Binary files /dev/null and b/overcooked_simulator/pygame_gui/images/plate_dirty.png differ diff --git a/overcooked_simulator/pygame_gui/pygame_gui.py b/overcooked_simulator/pygame_gui/pygame_gui.py index 24c42107ae6a1064552d08d9b7c0300553586c72..5b0070c208324bec5c24cf8daec27ee0f8154ec6 100644 --- a/overcooked_simulator/pygame_gui/pygame_gui.py +++ b/overcooked_simulator/pygame_gui/pygame_gui.py @@ -15,13 +15,14 @@ from overcooked_simulator.game_items import ( Item, CookingEquipment, Meal, + Plate, ) from overcooked_simulator.overcooked_environment import Action from overcooked_simulator.pygame_gui.game_colors import BLUE from overcooked_simulator.pygame_gui.game_colors import colors, Color from overcooked_simulator.simulation_runner import Simulator -USE_PLAYER_COOK_SPRITES = False +USE_PLAYER_COOK_SPRITES = True SHOW_INTERACTION_RANGE = False @@ -324,7 +325,7 @@ class PyGameGUI: pos, self.visualization_config[item.name]["parts"], scale=scale ) - if isinstance(item, ProgressibleItem) and not item.finished: + 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: diff --git a/overcooked_simulator/pygame_gui/visualization.yaml b/overcooked_simulator/pygame_gui/visualization.yaml index df6f702c079771a958a53f8230782ee8dce8fbee..05d58ec703de0822559ec9a4e880c7f2e3af49c3 100644 --- a/overcooked_simulator/pygame_gui/visualization.yaml +++ b/overcooked_simulator/pygame_gui/visualization.yaml @@ -21,7 +21,7 @@ CuttingBoard: center_offset: [ +0.15, -0.2 ] color: silver -PlateDispenser: +DirtyPlateReturn: parts: - type: rect height: 0.95 @@ -76,6 +76,26 @@ Stove: type: circle radius: 0.25 +Sink: + parts: + - color: black + type: rect + height: 0.875 + width: 0.625 + - color: darkslategray1 + type: circle + radius: 0.4 + +SinkAddon: + parts: + - color: black + type: rect + height: 0.875 + width: 0.625 + - color: darkslategray1 + type: circle + radius: 0.4 + # Items Tomato: parts: @@ -175,11 +195,17 @@ Cook: path: images/pixel_cook.png size: 1 -Plate: +CleanPlate: parts: - type: image - path: images/plate.png - size: 1 + path: images/plate_clean.png + size: 0.8 + +DirtyPlate: + parts: + - type: image + path: images/plate_dirty.png + size: 0.8 Pot: parts: