diff --git a/README.md b/README.md index a70fae97dd3f3f0d4424d1ecfc2576bda5e0745b..7222709f10f94cc0b0ab6343079ab35dfd8f970e 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,21 @@ # Overcooked Simulator -[API Docs](https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator) +[Documentation](https://scs.pages.ub.uni-bielefeld.de/cocosy/overcooked-simulator) The real-time overcooked simulation for a cognitive cooperative system. **The name ist still work in progress and we will probably change it.** ## Installation -You have two options to install the environment. Either clone it and install it locally or install it in your site-packages. + +You have two options to install the environment. Either clone it and install it locally or install it in your +site-packages. You need a Python 3.10 or higher environment. Either conda or PyEnv. ### Local Editable Installation -In your `repo`, `PyCharmProjects` or similiar directory with the correct environment active: + +In your `repo`, `PyCharmProjects` or similar directory with the correct environment active: + ```bash git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git cd overcooked_simulator @@ -25,29 +29,41 @@ python3 overcooked_simulator/main.py ``` ### Library Installation + The correct environment needs to be active: + ```bash pip install overcooked-environment@git+https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator@main ``` #### Run -You can now use the environment and/or simulator in your python code. Just by importing it `import overcooked_environment` + +You can now use the environment and/or simulator in your python code. Just by importing +it `import overcooked_environment` ## Configuration + The environment configuration is currently done with 3 config files + GUI configuration. ### Item Config -The item config defines which ingredients, cooking equipment and meals can exist and how meals and processed ingredients can be cooked/created. + +The item config defines which ingredients, cooking equipment and meals can exist and how meals and processed ingredients +can be cooked/created. ### Layout Config -You can define the layout of the kitchen via a layout file. The position of counters are based on a grid system, even when the playere do not move grid steps but continous steps. Each character defines a different type of counter. + +You can define the layout of the kitchen via a layout file. The position of counters are based on a grid system, even +when the players do not move grid steps but continuous steps. Each character defines a different type of counter. ### Environment Config -The environment config defines how a level/environment is defined. Here, the available plates, meals, order and player configraation is done. + +The environment config defines how a level/environment is defined. Here, the available plates, meals, order and player +configuration is done. ### PyGame Visualization Config -Here the visualisation for all objects is defined. Reference the images or define a list of base shapes that represent the counters, ingrredients, meals and players. +Here the visualisation for all objects is defined. Reference the images or define a list of base shapes that represent +the counters, ingredients, meals and players. ## Troubleshooting diff --git a/overcooked_simulator/__init__.py b/overcooked_simulator/__init__.py index 7d983330e5810899308a2e0b5b06990a754af5f6..c5d7d61bd0acf0412b5affe0069c97bbab98bf2e 100644 --- a/overcooked_simulator/__init__.py +++ b/overcooked_simulator/__init__.py @@ -1,19 +1,62 @@ """ -This is the documentation of Overcooked Simulator. -It contains of +This is the documentation of the Overcooked Simulator. # About the package +The package contains of an environment for cooperation between players/agents. +A PyGameGUI visualizes the game to human or visual agents in 2D. +A 3D web-enabled version (for example for online studies) can be found in the future [here](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/godot-overcooked-3d-visualization) + # Background / Literature +The overcooked/cooking domain is a well established cooperation domain/task. +There exists environments designed for reinforcement learning agents as well as the game and adaptations of the game for human players in a more "real-time" environment. +They all mostly differ in the visual and graphics dimension. 2D versions like overcooked-ai, ... are most known in the community. +But more visual appealing 3D versions for cooperation with humans are getting developed more frequently (cite,...). +Besides, the general adaptations of the original overcooked game. + # Usage / Examples +Our overcooked simulator is designed for real time interaction but also with reinforcement learning in mind (gym environment). +It focuses on configurability, extensibility and appealing visualization options. + +## Human Player +Start `main.py` in your python/conda environment: +```bash +python overcooked_simulator/main.py +``` + +## Connect with player and receive game state +... + +## Direct integration into your code. +Initialize an environment.... + # Citation +# Structure of the Documentation +The API documentation follows the file and content structure in the repo. +On the left you can find the navigation panel that brings you to the implementation of +- the **counters**, including the kitchen utility objects like dispenser, stove, sink, etc., +- the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals, +- in **main**, you find an example how to start a simulation, +- the **orders**, how to sample incoming orders and their attributes, +- the **environment**, handles the incoming actions and provides the state, +- the **player**, +- a **simulation runner**, that calls the step function of the environment for a real-time interaction, +- **util**ity code. + """ import os from pathlib import Path ROOT_DIR = Path(os.path.dirname(os.path.abspath(__file__))) # This is your Project Root +"""A path variable to get access to the layouts coming with the package. For example, +```python +from overcooked_simulator import ROOT_DIR + +environment_config_path = ROOT_DIR / "game_content" / "environment_config.yaml" +``` +""" diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index 41bd67eeb5c10e23ea15538dd90fe9ca1afce515..2cbbeb4d048a7d113c784ea1792103cdda1bdba1 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -1,3 +1,22 @@ +""" +All counters are derived from the `Counter` class. +Counters implement the `Counter.pick_up` method, which defines what should happen when the agent wants to pick something up from the counter. +On the other side, the `Counter.drop_off` method receives the item what should be put on the counter. Before that the `Counter.can_drop_off` method checked if the item can be put on the counter. +The progress on Counters or on objects on the counters are handled via the Counters. They have the task to delegate the progress call via the `Counter.progress` method. +On which type of counter the progress method is called is currently defined in the environment class. + +Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the interaction/requirements for the interaction. +```yaml +CuttingBoard: + type: Equipment + +Sink: + type: Equipment + +Stove: + type: Equipment +``` +""" from __future__ import annotations import logging @@ -96,13 +115,35 @@ class Counter: class CuttingBoard(Counter): + """Cutting ingredients on. The requirement in a new object could look like + + ```yaml + ChoppedTomato: + type: Ingredient + needs: [ Tomato ] + seconds: 4.0 + equipment: CuttingBoard + ``` + + + """ + 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""" + """Called by environment step function for time progression. + + Args: + passed_time: the time passed since the last progress call + now: the current env time. Not the same as `datetime.now` + + Checks if the item on the board is in the allowed transitions via a Cutting board. Pass the progress call to + the item on the board. If the progress on the item reaches 100% it changes the name of the item based on the + "goal" name in the transition definition. + """ if ( self.occupied and self.progressing @@ -139,6 +180,14 @@ class CuttingBoard(Counter): class ServingWindow(Counter): + """The orders and scores are updated based on completed and dropped off meals. The plate dispenser is pinged for the info about a plate outside of the kitchen. + + All items in the `item_info.yml` with the type meal are considered to be servable, if they are ordered. Not + ordered meals can also be served, if a `serving_not_ordered_meals` function is set in the `environment_config.yml`. + + The plate dispenser will put after some time a dirty plate on itself after a meal was served. + """ + def __init__( self, pos, @@ -175,6 +224,24 @@ class ServingWindow(Counter): class Dispenser(Counter): + """The class for all dispenser except plate dispenser. Here ingredients can be graped from the player/agent. + + At the moment all ingredients have an unlimited stock. + + The character for each dispenser in the `layout` file is currently hard coded in the environment class: + ```yaml + T: Tomato + L: Lettuce + N: Onion # N for oNioN + B: Bun + M: Meat + ``` + The plan is to put the info also in the config. + + In the implementation, an instance of the item to dispense is always on top of the dispenser. + Which also is easier for the visualization of the dispenser. + """ + def __init__(self, pos, dispensing: ItemInfo): self.dispensing = dispensing super().__init__( @@ -206,6 +273,21 @@ class Dispenser(Counter): class PlateDispenser(Counter): + """At the moment, one and only one plate dispenser must exist in an environment, because only at one place the dirty + plates should arrive. + + How many plates should exist at the start of the level on the plate dispenser is defined in the `environment_config.yml`: + ```yaml + plates: + clean_plates: 1 + dirty_plates: 2 + plate_delay: [ 5, 10 ] + # seconds until the dirty plate arrives. + ``` + + The character `P` in the `layout` file represents the PlateDispenser. + """ + def __init__( self, pos, dispensing, plate_config, plate_transitions, **kwargs ) -> None: @@ -227,14 +309,8 @@ class PlateDispenser(Counter): return not self.occupied_by or self.occupied_by[-1].can_combine(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?) - - """ + """At the moment items can be put on the top of the plate dispenser or the top plate if it is clean and can + be put on a plate.""" if not self.occupied_by: self.occupied_by.append(item) elif self.occupied_by[-1].can_combine(item): @@ -301,7 +377,12 @@ class PlateDispenser(Counter): return Plate(**kwargs) -class Trash(Counter): +class Trashcan(Counter): + """Ingredients and content on a cooking equipment can be removed from the environment via the trash. + + The character `X` in the `layout` file represents the Trashcan. + """ + def pick_up(self, on_hands: bool = True): pass @@ -316,6 +397,16 @@ class Trash(Counter): class Stove(Counter): + """Cooking machine. Currently, the stove which can have a pot and pan on top. In the future one class for stove, + deep fryer, and oven. + + The character depends on the cooking equipment on top of it: + ```yaml + U: Stove with a pot + Q: Stove with a pan + ``` + """ + def can_drop_off(self, item: Item) -> bool: if self.occupied_by is None: return isinstance(item, CookingEquipment) and item.name in ["Pot", "Pan"] @@ -333,11 +424,25 @@ class Stove(Counter): class Sink(Counter): + """The counter in which the dirty plates are in. + + Needs a `SinkAddon`. The closest is calculated during initialisation, should not be seperated by each other (needs + to touch the sink). + + The logic is similar to the CuttingBoard because there is no additional cooking equipment between the object to + progress and the counter. When the progress on the dirty plate is done, it is set to clean and is passed to the + `SinkAddon`. + + The character `S` in the `layout` file represents the Sink. + """ + def __init__(self, pos, transitions, sink_addon=None): super().__init__(pos) self.progressing = False self.sink_addon: SinkAddon = sink_addon + """The connected sink addon which will receive the clean plates""" self.occupied_by = deque() + """The queue of dirty plates. Only the one on the top is progressed.""" self.transitions = transitions @property @@ -399,6 +504,13 @@ class Sink(Counter): class SinkAddon(Counter): + """The counter on which the clean plates appear after cleaning them in the `Sink` + + It needs to be set close to/touching the `Sink`. + + The character `+` in the `layout` file represents the SinkAddon. + """ + def __init__(self, pos, occupied_by=None): super().__init__(pos) self.occupied_by = deque([occupied_by]) if occupied_by else deque() diff --git a/overcooked_simulator/game_content/environment_config.yaml b/overcooked_simulator/game_content/environment_config.yaml index b6b7c579ec950edd189c41e2ec09a5cbda2a239f..1dbb4f5632d2c95d07cdba95970c3990fc768aec 100644 --- a/overcooked_simulator/game_content/environment_config.yaml +++ b/overcooked_simulator/game_content/environment_config.yaml @@ -2,7 +2,7 @@ plates: clean_plates: 1 dirty_plates: 2 plate_delay: [ 5, 10 ] - # seconds until the dirty plate arrives. + # range of seconds until the dirty plate arrives. game: time_limit_seconds: 180 @@ -17,8 +17,10 @@ meals: - Salad orders: - kwargs: - duration_sample: + order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' + # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py + order_gen_kwargs: + order_duration_random_func: # how long should the orders be alive # 'random' library call with getattr, kwargs are passed to the function func: uniform @@ -29,10 +31,7 @@ orders: # maximum number of active orders at the same time num_start_meals: 3 # number of orders generated at the start of the environment - sample_on_dur: true - # if true, the next order is generated based on the sample_on_dur_func method in seconds - # if sample_on_serving is also true, the value is sampled after a meal was served, otherwise it is sampled directly after an order generation. - sample_on_dur_func: + sample_on_dur_random_func: # 'random' library call with getattr, kwargs are passed to the function func: uniform kwargs: @@ -54,8 +53,6 @@ orders: default: -5 serving_not_ordered_meals: null # a func that calcs a store for not ordered but served meals. Input: meal - order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' - # the class to that receives the kwargs. Should be a child class of OrderGeneration in order.py player_config: radius: 0.4 diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index 4592e68bbe4057719b3618fbf7b34513fb4d232b..1699481abb4a8611798b080ce2a173b940bec5b5 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -1,3 +1,14 @@ +"""" +The game items that a player can hold. + +They have methods that +- check if items can be combined (`Item.can_combine`): cooking equipment and ingredients, and so on +- combine the items after a successful check (`Item.combine`), +- and a method to call the progress on the items (`Item.progress`) + +All game items need to be specified in the `item_info.yml`. +""" + from __future__ import annotations import collections @@ -18,42 +29,52 @@ class ItemType(Enum): @dataclasses.dataclass class ItemInfo: + """Wrapper for the info in the `item_info.yml`. + + Example: + A simple example for the tomato soup with 6 game items. + ```yaml + CuttingBoard: + type: Equipment + + Stove: + type: Equipment + + Pot: + type: Equipment + equipment: Stove + + Tomato: + type: Ingredient + + ChoppedTomato: + type: Ingredient + needs: [ Tomato ] + seconds: 4.0 + equipment: CuttingBoard + + TomatoSoup: + type: Meal + needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ] + seconds: 6.0 + equipment: Pot + ``` + """ + type: ItemType = dataclasses.field(compare=False) + """Type of the item. Either `Ingredient`, `Meal` or `Equipment`.""" name: str = dataclasses.field(compare=True) + """The name of the item, is set automatically by the "group" name of the item.""" seconds: float = dataclasses.field(compare=False, default=0) + """If a progress is needed this defines how long it takes to complete the process in seconds.""" needs: list[ItemInfo] = dataclasses.field(compare=False, default_factory=list) + """The ingredients/items which are needed to create the item/start the progress.""" equipment: ItemInfo | None = dataclasses.field(compare=False, default=None) - - _start_meals: list[ItemInfo] = dataclasses.field( - compare=False, default_factory=list - ) + """On which the item can be created. `null`, `~` (None) converts to Plate.""" def __post_init__(self): self.type = ItemType(self.type) - def add_start_meal_to_equipment(self, start_item: ItemInfo): - self._start_meals.append(start_item) - - def sort_start_meals(self): - self._start_meals.sort(key=lambda item_info: len(item_info.needs)) - - # def can_start_meal(self, items: list[Item]): - # return items and self._return_start_meal(items) is not None - - # def start_meal(self, items: list[Item]) -> Item: - # return self._return_start_meal(items).create_item(parts=items) - - def _return_start_meal(self, items: list[Item]) -> ItemInfo | None: - for meal in self._start_meals: - satisfied = [False for _ in range(len(items))] - for i, p in enumerate(items): - for _, n in enumerate(meal.needs): - if not satisfied[i] and p.name == n: - satisfied[i] = True - break - if all(satisfied): - return meal - class Item: """Base class for game items which can be held by a player.""" @@ -102,13 +123,21 @@ class Item: class CookingEquipment(Item): + """Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself ( + progress_percentage) and not in the items in the content list.""" + def __init__(self, transitions: dict, *args, **kwargs): super().__init__(*args, **kwargs) self.transitions = transitions self.active_transition: Optional[dict] = None + """The info how and when to convert the content_list to a new item.""" self.content_ready: Item | None = None + """Helper attribute that can have a ready meal which is also represented via it ingredients in the + content_list. But soups or other processed meals are not covered here. For a Burger or Salad, this attribute + is set.""" self.content_list: list[Item] = [] + """The items that the equipment holds.""" log.debug(f"Initialize {self.name}: {self.transitions}") @@ -174,12 +203,6 @@ class CookingEquipment(Item): # todo set active transition for fire/burnt? - # def can_release_content(self) -> bool: - # return ( - # self.content - # and isinstance(self.content, ProgressibleItem) - # and self.content.finished - # ) def reset_content(self): self.content_list = [] self.content_ready = None @@ -206,9 +229,12 @@ class CookingEquipment(Item): class Plate(CookingEquipment): + """The plate can have to states: clean and dirty. In the clean state it can hold content/other items.""" + def __init__(self, transitions, clean, *args, **kwargs): self.clean = clean self.meals = set(transitions.keys()) + """All meals can be hold by a clean plate""" super().__init__( name=self.create_name(), transitions={ @@ -236,6 +262,8 @@ class Plate(CookingEquipment): and not self.content_list and self.clean ): + # additional check for meals in the content list of another equipment, + # e.g., Soups which is not covered by the normal transition checks. return other.content_list[0].name in self.meals return False elif self.clean: diff --git a/overcooked_simulator/gui_2d_vis/visualization.yaml b/overcooked_simulator/gui_2d_vis/visualization.yaml index 8d4d52deb8f111a56dda65c44501aca3414fe979..a4ba4f7ec354aa825fe8eb09b44ee19995015eaa 100644 --- a/overcooked_simulator/gui_2d_vis/visualization.yaml +++ b/overcooked_simulator/gui_2d_vis/visualization.yaml @@ -39,7 +39,7 @@ PlateDispenser: width: 0.95 color: cadetblue1 -Trash: +Trashcan: parts: - type: image path: images/trash3.png diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py index 5d7abbf72cdbbd1a6d2fe110934b8561d342ca44..54c7ef1e7a8b9948df220fe28fa17376c992e7cf 100644 --- a/overcooked_simulator/order.py +++ b/overcooked_simulator/order.py @@ -1,10 +1,13 @@ +"""""" +from __future__ import annotations + import dataclasses import logging import random from abc import abstractmethod from collections import deque from datetime import datetime, timedelta -from typing import Callable, Tuple, Any, Deque +from typing import Callable, Tuple, Any, Deque, Protocol, TypedDict from overcooked_simulator.game_items import Item, Plate, ItemInfo @@ -13,19 +16,29 @@ log = logging.getLogger(__name__) @dataclasses.dataclass class Order: + """Datawrapper for Orders""" + meal: ItemInfo + """The meal to serve and that should be cooked.""" start_time: datetime + """The start time relative to the env_time. On which the order is returned from the get_orders func.""" max_duration: timedelta - score_calc: Callable[[timedelta, ...], float] + """The duration after which the order expires.""" + score_calc: ScoreCalcFuncType + """The function that calculates the score of the served meal/fulfilled order.""" timed_penalties: list[ Tuple[timedelta, float] | Tuple[timedelta, float, int, timedelta] ] + """list of timed penalties when the order is not fulfilled.""" expired_penalty: float + """the penalty to the score if the order expires""" finished_info: dict[str, Any] = dataclasses.field(default_factory=dict) + """Is set after the order is completed.""" _timed_penalties: list[Tuple[datetime, float]] = dataclasses.field( default_factory=list ) + """Converted penalties the env is working with from the `timed_penalties`""" def order_time(self, env_time: datetime) -> timedelta: return self.start_time - env_time @@ -45,11 +58,24 @@ class Order: class OrderGeneration: + """Base class for generating orders. + + You can set your child class via the `environment_config.yml`. + Example: + ```yaml + orders: + order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' + kwargs: + ... + ``` + """ + def __init__(self, available_meals: dict[str, ItemInfo], **kwargs): self.available_meals: list[ItemInfo] = list(available_meals.values()) @abstractmethod def init_orders(self, now) -> list[Order]: + """Get the orders the environment starts with.""" ... @abstractmethod @@ -60,30 +86,141 @@ class OrderGeneration: new_finished_orders: list[Order], expired_orders: list[Order], ) -> list[Order]: + """Orders for each progress call. Should often be the empty list.""" + ... + + +class ScoreCalcFuncType(Protocol): + """Type with kwargs of the expected `Order.score_calc` function and returned function for the + `RandomOrderKwarg.score_calc_gen_func`.""" + + def __call__(self, relative_order_time: timedelta, order: Order) -> float: + ... + + +class ScoreCalcGenFuncType(Protocol): + """Type with kwargs of the expected function for the `RandomOrderKwarg.score_calc_gen_func`.""" + + def __call__( + self, + meal: ItemInfo, + duration: timedelta, + now: datetime, + kwargs: dict, + **other_kwargs, + ) -> ScoreCalcFuncType: + ... + + +class ExpiredPenaltyFuncType(Protocol): + """Type with kwargs of the expected function for the `RandomOrderKwarg.expired_penalty_func`. + + An example is the `zero` function.""" + + def __call__(self, item: ItemInfo, **kwargs) -> float: ... def zero(item: ItemInfo, **kwargs) -> float: + """Example and default for the `RandomOrderKwarg.expired_penalty_func` function. + + Just no penalty for expired orders.""" return 0.0 +class RandomFuncConfig(TypedDict): + """Types of the dict for sampling with different random functions from the `random` library. + + Example: + Sampling uniformly between `10` and `20`. + ```yaml + func: uniform + kwargs: + a: 10 + b: 20 + ``` + Or in pyton: + ```python + random_func: RandomFuncConfig = {'func': 'uniform', 'kwargs': {'a': 10, 'b': 20}} + ``` + """ + + func: Callable + """the name of a functions in the `random` library.""" + kwargs: dict + """the kwargs of the functions in the `random` library.""" + + @dataclasses.dataclass class RandomOrderKwarg: num_start_meals: int + """Number of meals sampled at the start.""" sample_on_serving: bool - sample_on_dur: bool - sample_on_dur_func: dict + """Only sample the delay for the next order after a meal was served.""" + sample_on_dur_random_func: RandomFuncConfig + """How to sample the delay of the next incoming order. Either after a new meal was served or the last order was + generated (based on the `sample_on_serving` attribute).""" max_orders: int - duration_sample: dict - score_calc_gen_func: Callable[ - [ItemInfo, timedelta, datetime, Any], Callable[[timedelta, Order], float] - ] + """How many orders can maximally be active at the same time.""" + order_duration_random_func: RandomFuncConfig + """How long the order is alive until it expires. If `sample_on_serving` is `true` all orders have no expire time.""" + score_calc_gen_func: ScoreCalcGenFuncType + """The function that generates the `Order.score_calc` for each order.""" score_calc_gen_kwargs: dict + """The additional static kwargs for `score_calc_gen_func`.""" expired_penalty_func: Callable[[ItemInfo], float] = zero + """The function that calculates the penalty for a meal that was not served.""" expired_penalty_kwargs: dict = dataclasses.field(default_factory=dict) + """The additional static kwargs for the `expired_penalty_func`.""" class RandomOrderGeneration(OrderGeneration): + """A simple order generation based on random sampling with two options. + + Either sample the delay when a new order should come in after the last order comes in or after a meals was served + (and an order got removed). + + To configure it align your kwargs with the `RandomOrderKwarg` class. + + You can set this order generation in your `environment_config.yml` with + ```yaml + orders: + order_gen_class: !!python/name:overcooked_simulator.order.RandomOrderGeneration '' + kwargs: + order_duration_random_func: + # how long should the orders be alive + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 40 + b: 60 + max_orders: 6 + # maximum number of active orders at the same time + num_start_meals: 3 + # number of orders generated at the start of the environment + sample_on_dur_random_func: + # 'random' library call with getattr, kwargs are passed to the function + func: uniform + kwargs: + a: 10 + b: 20 + sample_on_serving: false + # The sample time for a new incoming order is only generated after a meal was served. + score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func '' + score_calc_gen_kwargs: + # the kwargs for the score_calc_gen_func + other: 0 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty '' + expired_penalty_kwargs: + default: -5 + ``` + """ + def __init__(self, available_meals: dict[str, ItemInfo], **kwargs): super().__init__(available_meals, **kwargs) self.kwargs: RandomOrderKwarg = RandomOrderKwarg(**kwargs["kwargs"]) @@ -94,7 +231,7 @@ class RandomOrderGeneration(OrderGeneration): def init_orders(self, now) -> list[Order]: self.number_cur_orders = self.kwargs.num_start_meals - if self.kwargs.sample_on_dur: + if not self.kwargs.sample_on_serving: self.create_random_next_time_delta(now) return self.create_orders_for_meals( random.choices(self.available_meals, k=self.kwargs.num_start_meals), @@ -127,7 +264,7 @@ class RandomOrderGeneration(OrderGeneration): if self.number_cur_orders >= self.kwargs.max_orders: self.needed_orders += 1 else: - if self.kwargs.sample_on_dur: + if not self.kwargs.sample_on_serving: self.create_random_next_time_delta(now) else: self.next_order_time = datetime.max @@ -147,9 +284,9 @@ class RandomOrderGeneration(OrderGeneration): duration = datetime.max - now else: duration = timedelta( - seconds=getattr(random, self.kwargs.duration_sample["func"])( - **self.kwargs.duration_sample["kwargs"] - ) + seconds=getattr( + random, self.kwargs.order_duration_random_func["func"] + )(**self.kwargs.order_duration_random_func["kwargs"]) ) log.info(f"Create order for meal {meal} with duration {duration}") orders.append( @@ -174,8 +311,8 @@ class RandomOrderGeneration(OrderGeneration): def create_random_next_time_delta(self, now: datetime): self.next_order_time = now + timedelta( - seconds=getattr(random, self.kwargs.sample_on_dur_func["func"])( - **self.kwargs.sample_on_dur_func["kwargs"] + seconds=getattr(random, self.kwargs.sample_on_dur_random_func["func"])( + **self.kwargs.sample_on_dur_random_func["kwargs"] ) ) log.info(f"Next order in {self.next_order_time}") @@ -184,6 +321,21 @@ class RandomOrderGeneration(OrderGeneration): def simple_score_calc_gen_func( meal: Item, duration: timedelta, now: datetime, kwargs: dict, **other_kwargs ) -> Callable: + """An example for the `RandomOrderKwarg.score_calc_gen_func` that selects the score for an order based on its meal from a list. + + Example: + ```yaml + score_calc_gen_func: !!python/name:overcooked_simulator.order.simple_score_calc_gen_func '' + score_calc_gen_kwargs: + # the kwargs for the score_calc_gen_func + other: 0 + scores: + Burger: 15 + OnionSoup: 10 + Salad: 5 + TomatoSoup: 10 + ``` + """ scores = kwargs["scores"] other = kwargs["other"] @@ -196,14 +348,27 @@ def simple_score_calc_gen_func( def simple_expired_penalty(item: ItemInfo, default: float, **kwargs) -> float: + """Example for the `RandomOrderKwarg.expired_penalty_func` function. + + A static default. + + Example: + ```yaml + expired_penalty_func: !!python/name:overcooked_simulator.order.simple_expired_penalty '' + expired_penalty_kwargs: + default: -5 + ``` + """ return default class OrderAndScoreManager: + """The Order and Score Manager that is called from the serving window.""" + def __init__(self, order_config, available_meals: dict[str, ItemInfo]): self.score = 0 self.order_gen: OrderGeneration = order_config["order_gen_class"]( - available_meals=available_meals, kwargs=order_config["kwargs"] + available_meals=available_meals, kwargs=order_config["order_gen_kwargs"] ) self.kwargs_for_func = order_config["kwargs"] self.serving_not_ordered_meals = order_config["serving_not_ordered_meals"] @@ -240,7 +405,7 @@ class OrderAndScoreManager: log.info( f"Serving meal without order {meal.name} with score {score}" ) - self.score += score + self.increment_score(score) self.served_meals.append((meal, env_time)) return accept log.info( @@ -252,7 +417,7 @@ class OrderAndScoreManager: relative_order_time=env_time - order.start_time, order=order, ) - self.score += score + self.increment_score(score) order.finished_info = { "end_time": env_time, "score": score, @@ -265,15 +430,17 @@ class OrderAndScoreManager: log.info(f"Do not serve item {item}") return False - def increment_score(self, score: int): + def increment_score(self, score: int | float): self.score += score log.debug(f"Score: {self.score}") def create_init_orders(self, env_time): + """Create the initial orders in an environment.""" init_orders = self.order_gen.init_orders(env_time) self.open_orders.extend(init_orders) def progress(self, passed_time: timedelta, now: datetime): + """Check expired orders and check order generation.""" new_orders = self.order_gen.get_orders( passed_time=passed_time, now=now, @@ -287,7 +454,7 @@ class OrderAndScoreManager: remove_orders = [] for index, order in enumerate(self.open_orders): if now >= order.start_time + order.max_duration: - self.score += order.expired_penalty + self.increment_score(order.expired_penalty) remove_orders.append(index) remove_penalties = [] for i, (penalty_time, penalty) in enumerate(order.timed_penalties): @@ -314,40 +481,3 @@ class OrderAndScoreManager: def setup_penalties(self, new_orders: list[Order], env_time: datetime): for order in new_orders: order.create_penalties(env_time) - - -if __name__ == "__main__": - import yaml - - order_config = yaml.safe_load( - """orders: - kwargs: - duration_sample: - func: uniform - kwargs: - a: 30 - b: 50 - max_orders: 5 - num_start_meals: 3 - sample_on_dur: false - sample_on_dur_func: - func: uniform - kwargs: - a: 30 - b: 50 - sample_on_serving: true - score_calc_gen_func: null - score_calc_gen_kwargs: - other: 0 - scores: - Burger: 15 - OnionSoup: 10 - Salad: 5 - TomatoSoup: 10 - score_calc_gen_func: ~'' - order_gen_class: ~ - serving_not_ordered_meals: null""" - ) - order_config["orders"]["order_gen_class"] = RandomOrderGeneration - order_config["orders"]["kwargs"]["score_calc_gen_func"] = simple_score_calc_gen_func - print(yaml.dump(order_config)) diff --git a/overcooked_simulator/overcooked_environment.py b/overcooked_simulator/overcooked_environment.py index 3eaf84e14abfee0b6bc2931b00c54a1b0a5c6be5..f4bd59de451d5f02b9d1f99ab24c13490f7b7b40 100644 --- a/overcooked_simulator/overcooked_environment.py +++ b/overcooked_simulator/overcooked_environment.py @@ -14,7 +14,7 @@ from scipy.spatial import distance_matrix from overcooked_simulator.counters import ( Counter, CuttingBoard, - Trash, + Trashcan, Dispenser, ServingWindow, Stove, @@ -110,7 +110,7 @@ class Environment: and info.equipment.name == "CuttingBoard" }, ), - "X": Trash, + "X": Trashcan, "W": lambda pos: ServingWindow( pos, self.order_and_score, @@ -210,11 +210,6 @@ class Environment: 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_meal_to_equipment(item_info) - for item_name, item_info in item_lookup.items(): - if item_info.type == ItemType.Equipment: - # first select meals with smaller needs / ingredients - item_info.sort_start_meals() return item_lookup def validate_item_info(self):