From a8fd5a5dd14b6ba7eecbb65b392c5d26efbed88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6der?= <fschroeder@techfak.uni-bielefeld.de> Date: Wed, 20 Dec 2023 11:18:33 +0100 Subject: [PATCH] plate dispenser without manager. plates disappear for a time and then appear on the plate dispenser. You can now add stuff on the plates on the dispenser. New lock for gui and environment. --- overcooked_simulator/counters.py | 124 ++++++++++++++---- .../game_content/environment_config.yaml | 5 +- overcooked_simulator/game_items.py | 20 +-- .../overcooked_environment.py | 62 +++++---- overcooked_simulator/player.py | 5 +- overcooked_simulator/pygame_gui/pygame_gui.py | 12 +- .../pygame_gui/visualization.yaml | 2 +- overcooked_simulator/simulation_runner.py | 9 +- 8 files changed, 168 insertions(+), 71 deletions(-) diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 6c0b6585..79272eb5 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -1,11 +1,13 @@ from __future__ import annotations +import logging +from collections import deque +from datetime import datetime, timedelta from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from overcooked_simulator.overcooked_environment import ( GameScore, - PlateManager, SinkManager, ) @@ -22,6 +24,9 @@ from overcooked_simulator.game_items import ( ) +log = logging.getLogger(__name__) + + class Counter: """Simple class for a counter at a specified position (center of counter). Can hold things on top.""" @@ -118,9 +123,11 @@ class CuttingBoard(Counter): class ServingWindow(Counter): - def __init__(self, pos, game_score: GameScore, plate_manager: PlateManager): + def __init__( + self, pos, game_score: GameScore, plate_dispenser: PlateDispenser = None + ): self.game_score = game_score - self.plate_manager = plate_manager + self.plate_dispenser = plate_dispenser super().__init__(pos) def drop_off(self, item) -> Item | None: @@ -128,8 +135,8 @@ class ServingWindow(Counter): print(item) # TODO define rewards self.game_score.increment_score(reward) - self.plate_manager.number_returned_plates += 1 - self.plate_manager.update_plate_return() + if self.plate_dispenser is not None: + self.plate_dispenser.update_plate_out_of_kitchen() return None def can_score(self, item): @@ -152,6 +159,9 @@ class ServingWindow(Counter): def pick_up(self, on_hands: bool = True): pass + def add_plate_dispenser(self, plate_dispenser): + self.plate_dispenser = plate_dispenser + class Dispenser(Counter): def __init__(self, pos, dispensing): @@ -177,32 +187,98 @@ 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 +class PlateDispenser(Counter): + def __init__(self, pos, dispensing, plate_config): self.dispensing = dispensing super().__init__(pos) - self.occupied_by = [ - self.dispensing.create_item() - for _ in range(self.plate_manager.number_returned_plates) - ] + 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.setup_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 + if self.occupied_by: + return self.occupied_by.pop() + + # def can_drop_off(self, item: Item) -> bool: + # return False def can_drop_off(self, item: Item) -> bool: - return False + return ( + not self.occupied_by + or isinstance(item, Plate) + or self.occupied_by[-1].can_combine(item) + ) - def update(self): - if self.plate_manager.number_returned_plates == 0: - self.occupied_by = [] - else: - self.occupied_by.append(self.dispensing.create_item()) + def drop_off(self, item: Item) -> Item | None: + """Takes the thing dropped of by the player. + + Args: + item: The item to be placed on the counter. + + Returns: TODO Return information, whether the score is affected (Serving Window?) + + """ + if not self.occupied_by: + self.occupied_by.append(item) + elif isinstance(item, Plate): + self.occupied_by.append(item) + elif self.occupied_by[-1].can_combine(item): + return self.occupied_by[-1].combine(item) + return None + + def add_dirty_plate(self): + self.occupied_by.appendleft(self.dispensing.create_item()) + + def update_plate_out_of_kitchen(self): + """Is called from the serving window to add a plate out of kitchen.""" + time_plate_to_add = datetime.now() + timedelta( + seconds=np.random.uniform( + low=self.plate_config["plate_delay"][0], + high=self.plate_config["plate_delay"][1], + ) + ) + log.debug(f"New plate out of kitchen until {time_plate_to_add}") + self.out_of_kitchen_timer.append(time_plate_to_add) + if time_plate_to_add < self.next_plate_time: + self.next_plate_time = time_plate_to_add + + def setup_plates(self): + """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"]) + ] + ) + if "clean_plates" in self.plate_config: + self.occupied_by.extend( + [ + self.dispensing.create_item(clean_plate=True) + for _ in range(self.plate_config["clean_plates"]) + ] + ) + + def progress(self): + """Check if plates arrive from outside the kitchen and add a dirty plate accordingly""" + now = datetime.now() + if self.next_plate_time < now: + idx_delete = [] + for i, times in enumerate(self.out_of_kitchen_timer): + if times < now: + idx_delete.append(i) + log.debug("Add dirty plate") + self.add_dirty_plate() + for idx in reversed(idx_delete): + self.out_of_kitchen_timer.pop(idx) + self.next_plate_time = ( + min(self.out_of_kitchen_timer) + if self.out_of_kitchen_timer + else datetime.max + ) def __repr__(self): return "PlateReturn" diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index ceaf3f69..39e273e0 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -1,4 +1,7 @@ counter_side_length: 40 world_width: 800 world_height: 600 -max_number_plates: 25 \ No newline at end of file +plates: + clean_plates: 1 + dirty_plates: 0 + plate_delay: [ 5, 10 ] \ No newline at end of file diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 621819dc..fb983d7d 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -1,8 +1,11 @@ from __future__ import annotations import dataclasses +import logging from enum import Enum +log = logging.getLogger(__name__) + class ItemType(Enum): Ingredient = "Ingredient" @@ -32,7 +35,7 @@ class ItemInfo: return self.finished_progress_name.replace("*", self.name) return self.name - def create_item(self) -> Item: + def create_item(self, clean_plate=False) -> Item: match self.type: case ItemType.Ingredient: if self.is_cuttable: @@ -51,7 +54,7 @@ class ItemInfo: steps_needed=self.steps_needed, finished=False, item_info=self, - clean="Clean" in self.name, + clean=clean_plate, ) else: return CookingEquipment(name=self.name, item_info=self) @@ -247,14 +250,14 @@ class Plate(CookingEquipment): ): super().__init__(content, *args, **kwargs) self.clean = clean - self.name = self.__repr__() + 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.name = "CleanPlate" self.clean = True + self.name = self.create_name() def progress(self): """Progresses the item process as long as it is not finished.""" @@ -268,13 +271,10 @@ class Plate(CookingEquipment): return not self.clean def can_combine(self, other): - return super().can_combine(other) and self.clean + return self.clean and super().can_combine(other) def combine(self, other): return super().combine(other) - def __repr__(self): - if self.clean: - return "CleanPlate" - else: - return "DirtyPlate" + def create_name(self): + return "CleanPlate" if self.clean else "DirtyPlate" diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 7fb7f3fc..5c9e2419 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 threading import Lock from typing import Optional import numpy as np @@ -18,7 +19,7 @@ from overcooked_simulator.counters import ( ServingWindow, Stove, Sink, - DirtyPlateReturn, + PlateDispenser, SinkAddon, ) from overcooked_simulator.game_items import ItemInfo @@ -57,24 +58,6 @@ class GameScore: 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 @@ -95,6 +78,7 @@ class Environment: """ def __init__(self, env_config_path: Path, layout_path, item_info_path: Path): + self.lock = Lock() self.players: dict[str, Player] = {} with open(env_config_path, "r") as file: @@ -106,18 +90,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 = { "#": Counter, # because # looks a bit like a counter "B": CuttingBoard, "X": Trash, - "W": lambda pos: ServingWindow(pos, self.game_score, self.plate_manager), + "W": lambda pos: ServingWindow(pos, self.game_score), "T": lambda pos: Dispenser(pos, self.item_info["Tomato"]), "L": lambda pos: Dispenser(pos, self.item_info["Lettuce"]), - "P": lambda pos: DirtyPlateReturn( - pos, self.item_info["Plate"], self.plate_manager + "P": lambda pos: PlateDispenser( + pos, + self.item_info["Plate"], + environment_config["plates"] if "plates" in environment_config else {}, ), "N": lambda pos: Dispenser(pos, self.item_info["Onion"]), # N for oNioN "_": "Free", @@ -136,6 +121,8 @@ class Environment: self.free_positions, ) = self.parse_layout_file(self.layout_path) + self.init_counters() + self.score: int = 0 self.world_width: int = environment_config["world_width"] @@ -180,8 +167,6 @@ 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) @@ -442,9 +427,10 @@ class Environment: """Performs a step of the environment. Affects time based events such as cooking or cutting things, orders and time limits. """ - for counter in self.counters: - if isinstance(counter, (CuttingBoard, Stove, Sink)): - counter.progress() + with self.lock: + for counter in self.counters: + if isinstance(counter, (CuttingBoard, Stove, Sink, PlateDispenser)): + counter.progress() def get_state(self): """Get the current state of the game environment. The state here is accessible by the current python objects. @@ -461,3 +447,23 @@ class Environment: """ pass + + def init_counters(self): + plate_dispenser = self.get_counter_of_type(PlateDispenser) + assert len(plate_dispenser) > 0, "No Plate Return in the environment" + + for counter in self.counters: + match counter: + case ServingWindow(): + counter.add_plate_dispenser(plate_dispenser[0]) + case Sink(): + ... + case SinkAddon(): + ... + + pass + + def get_counter_of_type(self, counter_type) -> list[Counter]: + return list( + filter(lambda counter: isinstance(counter, counter_type), self.counters) + ) diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 49317e40..00f90bce 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -1,4 +1,5 @@ import logging +from collections import deque from pathlib import Path from typing import Optional @@ -107,7 +108,9 @@ class Player: elif counter.can_drop_off(self.holding): self.holding = counter.drop_off(self.holding) - elif not isinstance(counter.occupied_by, list) and self.holding.can_combine(counter.occupied_by): + elif not isinstance( + counter.occupied_by, (list, deque) + ) 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/pygame_gui.py b/overcooked_simulator/pygame_gui/pygame_gui.py index 5b0070c2..e4ec3b06 100644 --- a/overcooked_simulator/pygame_gui/pygame_gui.py +++ b/overcooked_simulator/pygame_gui/pygame_gui.py @@ -2,6 +2,7 @@ import colorsys import logging import math import sys +from collections import deque import numpy as np import numpy.typing as npt @@ -375,11 +376,12 @@ class PyGameGUI: if counter.occupied_by is not None: # Multiple plates on plate return: - if isinstance(counter.occupied_by, list): - for i, o in enumerate(counter.occupied_by): - self.draw_item( - np.abs([counter.pos[0], counter.pos[1] - (i * 3)]), o - ) + if isinstance(counter.occupied_by, (list, deque)): + with self.simulator.env.lock: + for i, o in enumerate(counter.occupied_by): + self.draw_item( + np.abs([counter.pos[0], counter.pos[1] - (i * 3)]), o + ) # All other items: else: self.draw_item(counter.pos, counter.occupied_by) diff --git a/overcooked_simulator/pygame_gui/visualization.yaml b/overcooked_simulator/pygame_gui/visualization.yaml index 05d58ec7..62a327a4 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 -DirtyPlateReturn: +PlateDispenser: parts: - type: rect height: 0.95 diff --git a/overcooked_simulator/simulation_runner.py b/overcooked_simulator/simulation_runner.py index 4d38ac34..795b61e6 100644 --- a/overcooked_simulator/simulation_runner.py +++ b/overcooked_simulator/simulation_runner.py @@ -2,6 +2,8 @@ import logging import time from threading import Thread +import numpy as np + from overcooked_simulator import ROOT_DIR from overcooked_simulator.overcooked_environment import Environment, Action @@ -28,12 +30,17 @@ class Simulator(Thread): layout_path, frequency: int, item_info_path=ROOT_DIR / "game_content" / "item_info.yaml", + seed: int = 8654321, ): + # TODO look at https://builtin.com/data-science/numpy-random-seed to change to other random + np.random.seed(seed) 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, layout_path, item_info_path) + self.env: Environment = Environment( + env_layout_path, layout_path, item_info_path + ) super().__init__() -- GitLab