Skip to content
Snippets Groups Projects
Commit 201d90fe authored by Florian Schröder's avatar Florian Schröder
Browse files

update doc strings

parent b2fe3491
No related branches found
No related tags found
1 merge request!29Resolve "docstrings"
Pipeline #44135 failed
# 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
......
"""
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"
```
"""
"""
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()
......
......@@ -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
......
""""
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:
......
......@@ -39,7 +39,7 @@ PlateDispenser:
width: 0.95
color: cadetblue1
Trash:
Trashcan:
parts:
- type: image
path: images/trash3.png
......
""""""
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))
......@@ -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):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment