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

Add docstrings and type hinting

The updates include adding docstrings and type hinting to several classes and methods in multiple Python files. This commit also includes fixes for inconsistent code formatting and minor bugs. The docstrings provide essential details about the classes and methods, improving readability and understanding for other developers. Furthermore, the added type hinting will enable better IDE assistance, static analysis, and clarity on expected input and output types. Lastly, the code formatting fixes and bug fixes enhance the overall code quality and maintainability.
parent 274592dd
No related branches found
No related tags found
No related merge requests found
Pipeline #45184 passed
......@@ -6,7 +6,7 @@ This is the documentation of the Overcooked Simulator.
The package contains 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, currently under development)
can be found [here](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/godot-overcooked-3d-visualization)
can be found [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
......@@ -30,7 +30,7 @@ Run it via the command line (in your pyenv/conda environment):
overcooked-sim --url "localhost" --port 8000
```
_The arguments are the defaults. Therefore, they are optional._
*The arguments are the defaults. Therefore, they are optional.*
You can also start the **Game Server** and the **PyGame GUI** individually in different terminals.
......@@ -41,13 +41,147 @@ python3 overcooked_simulator/gui_2d_vis/overcooked_gui.py --url "localhost" --po
```
## Connect with agent and receive game state
...
Or you start a game server, create an environment and connect each player/agent via a websocket connection.
To start a game server see above. Your own manager needs to create an environment.
```python
import requests
from overcooked_simulator import ROOT_DIR
from overcooked_simulator.game_server import CreateEnvironmentConfig
from overcooked_simulator.server_results import CreateEnvResult
with open(ROOT_DIR / "game_content" / "item_info.yaml", "r") as file:
item_info = file.read()
with open(ROOT_DIR / "game_content" / "layouts" / "basic.layout", "r") as file:
layout = file.read()
with open(ROOT_DIR / "game_content" / "environment_config.yaml", "r") as file:
environment_config = file.read()
create_env = CreateEnvironmentConfig(
manager_id="SECRETKEY1",
number_players=2,
environment_settings={"all_player_can_pause_game": False},
item_info_config=item_info,
environment_config=environment_config,
layout_config=layout,
).model_dump(mode="json")
if env_info.status_code == 403:
raise ValueError(f"Forbidden Request: {env_info.json()['detail']}")
env_info: CreateEnvResult = env_info.json()
env_info = requests.post("http://localhost:8000/manage/create_env", json=create_env)
```
Connect for each player to a websocket (threaded or async).
```python
import json
import dataclasses
from websockets import connect
from overcooked_simulator.overcooked_environment import Action, ActionType, InterActionData
from overcooked_simulator.utils import custom_asdict_factory
p1_websocket = connect("ws://localhost:8000/ws/player/" + env_info["player_info"]["0"]["client_id"])
# set player "0" as ready
p1_websocket.send(json.dumps({"type": "ready", "player_hash": env_info["player_info"]["0"]["player_hash"]}))
assert json.loads(websocket.recv())["status"] == 200, "not accepted player"
# get the state for player "0", call every frame/step
p1_websocket.send(json.dumps({"type": "get_state", "player_hash": env_info["player_info"]["0"]["player_hash"]}))
state = json.loads(websocket.recv())
# send an action for player "0"
# movement
action = Action(
player="0",
action_type=ActionType.MOVEMENT,
action_data=[0.0, 1.0], # direction (here straight up)
duration=0.5 # seconds
)
# pickup/drop off
action = Action(
player="0",
action_type=ActionType.PUT,
action_data="pickup",
)
# interact
action = Action(
player="0",
action_type=ActionType.INTERACT,
action_data=InterActionData.START # InterActionData.STOP when to stop the interaction
)
p1_websocket.send(json.dumps({
"type": "action",
"player_hash": env_info["player_info"]["0"]["player_hash"],
"action": dataclasses.asdict(
action, dict_factory=custom_asdict_factory
),
}))
websocket.recv()
```
Stop the environment if you want the game to end before the time is up.
```python
requests.post(
f"http://localhost:8000/manage/stop_env",
json={
"manager_id": "SECRETKEY1",
"env_id": env_info["env_id"],
"reason": "closed environment",
},
)
```
## Direct integration into your code.
Initialize an environment....
You can use the [overcooked_simulator.overcooked_environment.Environment] class and call the `step`, `get_json_state`, and `perform_action` methods directly.
```python
from datetime import timedelta
**TODO** JSON State description.
from overcooked_simulator.overcooked_environment import Action, Environment
env = Environment(
env_config=ROOT_DIR / "game_content" / "environment_config.yaml",
layout_config=ROOT_DIR / "game_content" / "layouts" / "basic.layout",
item_info=ROOT_DIR / "game_content" / "item_info.yaml"
)
env.add_player("0")
env.add_player("1")
while True:
# adapt this to real time if needed with time.sleep etc.
env.step(timedelta(seconds=0.1))
player_0_state = json.loads(env.get_json_state("0"))
if player_0_state["ended"]:
break
action = ... # See above but use np.array instead of list for the movement direction vector
env.perform_action(action)
```
# JSON State
The JSON Schema for the state of the environment for a player can be generated by running the `state_representation.py`
```bash
python state_representation.py
```
Should look like
```json
{'$defs': {'CookingEquipmentState': {'properties': {'content_list': {'items': {'$ref': '#/$defs/ItemState'}, 'title': 'Content List', 'type': 'array'}, 'content_ready': {'anyOf': [{'$ref': '#/$defs/ItemState'}, {'type': 'null'}]}}, 'required': ['content_list', 'content_ready'], 'title': 'CookingEquipmentState', 'type': 'object'}, 'CounterState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'category': {'const': 'Counter', 'title': 'Category'}, 'type': {'title': 'Type', 'type': 'string'}, 'pos': {'items': {'type': 'number'}, 'title': 'Pos', 'type': 'array'}, 'occupied_by': {'anyOf': [{'items': {'anyOf': [{'$ref': '#/$defs/ItemState'}, {'$ref': '#/$defs/CookingEquipmentState'}]}, 'type': 'array'}, {'$ref': '#/$defs/ItemState'}, {'$ref': '#/$defs/CookingEquipmentState'}, {'type': 'null'}], 'title': 'Occupied By'}}, 'required': ['id', 'category', 'type', 'pos', 'occupied_by'], 'title': 'CounterState', 'type': 'object'}, 'ItemState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'category': {'anyOf': [{'const': 'Item'}, {'const': 'ItemCookingEquipment'}], 'title': 'Category'}, 'type': {'title': 'Type', 'type': 'string'}, 'progress_percentage': {'anyOf': [{'type': 'number'}, {'type': 'integer'}], 'title': 'Progress Percentage'}}, 'required': ['id', 'category', 'type', 'progress_percentage'], 'title': 'ItemState', 'type': 'object'}, 'KitchenInfo': {'properties': {'width': {'title': 'Width', 'type': 'number'}, 'height': {'title': 'Height', 'type': 'number'}}, 'required': ['width', 'height'], 'title': 'KitchenInfo', 'type': 'object'}, 'OrderState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'category': {'const': 'Order', 'title': 'Category'}, 'meal': {'title': 'Meal', 'type': 'string'}, 'start_time': {'format': 'date-time', 'title': 'Start Time', 'type': 'string'}, 'max_duration': {'title': 'Max Duration', 'type': 'number'}}, 'required': ['id', 'category', 'meal', 'start_time', 'max_duration'], 'title': 'OrderState', 'type': 'object'}, 'PlayerState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'pos': {'items': {'type': 'number'}, 'title': 'Pos', 'type': 'array'}, 'facing_direction': {'items': {'type': 'number'}, 'title': 'Facing Direction', 'type': 'array'}, 'holding': {'anyOf': [{'$ref': '#/$defs/ItemState'}, {'$ref': '#/$defs/CookingEquipmentState'}, {'type': 'null'}], 'title': 'Holding'}, 'current_nearest_counter_pos': {'anyOf': [{'items': {'type': 'number'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Current Nearest Counter Pos'}, 'current_nearest_counter_id': {'anyOf': [{'type': 'string'}, {'type': 'null'}], 'title': 'Current Nearest Counter Id'}}, 'required': ['id', 'pos', 'facing_direction', 'holding', 'current_nearest_counter_pos', 'current_nearest_counter_id'], 'title': 'PlayerState', 'type': 'object'}}, 'properties': {'players': {'items': {'$ref': '#/$defs/PlayerState'}, 'title': 'Players', 'type': 'array'}, 'counters': {'items': {'$ref': '#/$defs/CounterState'}, 'title': 'Counters', 'type': 'array'}, 'kitchen': {'$ref': '#/$defs/KitchenInfo'}, 'score': {'anyOf': [{'type': 'number'}, {'type': 'integer'}], 'title': 'Score'}, 'orders': {'items': {'$ref': '#/$defs/OrderState'}, 'title': 'Orders', 'type': 'array'}, 'ended': {'title': 'Ended', 'type': 'boolean'}, 'env_time': {'format': 'date-time', 'title': 'Env Time', 'type': 'string'}, 'remaining_time': {'title': 'Remaining Time', 'type': 'number'}}, 'required': ['players', 'counters', 'kitchen', 'score', 'orders', 'ended', 'env_time', 'remaining_time'], 'title': 'StateRepresentation', 'type': 'object'}
```
The BaseModel and TypedDicts can be found in `overcooked_simulator.state_representation`. The `overcooked_simulator.state_representation.StateRepresentation` represents the json state that the `get_json_state` returns.
# Citation
......
"""
The `CounterFactory` initializes the `Counter` classes from the characters in the layout config.
The mapping depends on the definition in the `environment_config.yml` in the `layout_chars` section.
```yaml
layout_chars:
_: Free
hash: Counter
A: Agent
P: PlateDispenser
C: CuttingBoard
X: Trashcan
W: ServingWindow
S: Sink
+: SinkAddon
U: Pot # with Stove
Q: Pan # with Stove
O: Peel # with Oven
F: Basket # with DeepFryer
T: Tomato
N: Onion # oNioN
L: Lettuce
K: Potato # Kartoffel
I: Fish # fIIIsh
D: Dough
E: Cheese # chEEEse
G: Sausage # sausaGe
B: Bun
M: Meat
```
# Code Documentation
"""
import inspect
import sys
from typing import Any, Type, TypeVar
......@@ -27,12 +60,15 @@ T = TypeVar("T")
def convert_words_to_chars(layout_chars_config: dict[str, str]) -> dict[str, str]:
"""Converts words in a given layout chars configuration dictionary to their corresponding characters.
This is useful for characters that can not be keys in a yaml file. For example, `#` produces a comment.
Therefore, you can specify `hash` as a key (`hash: Counter`). `word_refs` defines the conversions. *Click on `▶ View Source`.*
Args:
layout_chars_config (dict[str, str]): A dictionary containing layout character configurations, where the keys are words
layout_chars_config: A dictionary containing layout character configurations, where the keys are words
representing layout characters and the values are the corresponding character representations.
Returns:
dict[str, str]: A dictionary where the keys are the layout characters and the values are their corresponding words.
A dictionary where the keys are the layout characters and the values are their corresponding words.
"""
word_refs = {
"hash": "#",
......@@ -78,25 +114,17 @@ class CounterFactory:
) -> None:
"""Constructor for the `CounterFactory` class. Set up the attributes necessary to instantiate the counters.
Args:
layout_chars_config (dict[str, str]): A dictionary mapping layout characters to their corresponding names.
item_info (dict[str, ItemInfo]): A dictionary containing information about different items.
serving_window_additional_kwargs (dict[str, Any]): Additional keyword arguments for serving window configuration.
plate_config (PlateConfig): The configuration for plate usage.
Returns:
None
Initializes the object with the provided parameters. It performs the following tasks:
- Converts the layout character configuration from words to characters.
- Sets the item information dictionary.
- Sets the additional keyword arguments for serving window configuration.
- Sets the plate configuration.
It also sets the following attributes:
- `no_counter_chars`: A set of characters that represent counters for agents or free spaces.
- `counter_classes`: A dictionary of counter classes imported from the 'overcooked_simulator.counters' module.
- `cooking_counter_equipments`: A dictionary mapping cooking counters to the list of equipment items associated with them.
Args:
layout_chars_config: A dictionary mapping layout characters to their corresponding names.
item_info: A dictionary containing information about different items.
serving_window_additional_kwargs: Additional keyword arguments for serving window configuration.
plate_config: The configuration for plate usage.
"""
self.layout_chars_config = convert_words_to_chars(layout_chars_config)
self.item_info = item_info
......@@ -109,22 +137,25 @@ class CounterFactory:
for c, name in self.layout_chars_config.items()
if name in ["Agent", "Free"]
)
"""A set of characters that represent counters for agents or free spaces."""
self.counter_classes = dict(
inspect.getmembers(
sys.modules["overcooked_simulator.counters"], inspect.isclass
)
)
"""A dictionary of counter classes imported from the 'overcooked_simulator.counters' module."""
self.cooking_counter_equipments = {
cooking_counter: [
cooking_counter: {
equipment
for equipment, e_info in self.item_info.items()
if e_info.equipment and e_info.equipment.name == cooking_counter
]
}
for cooking_counter, info in self.item_info.items()
if info.type == ItemType.Equipment and info.equipment is None
}
"""A dictionary mapping cooking counters to the list of equipment items associated with them."""
def get_counter_object(self, c: str, pos: npt.NDArray[float]) -> Counter:
"""Create and returns a counter object based on the provided character and position."""
......@@ -137,7 +168,9 @@ class CounterFactory:
if item_info.equipment.name not in self.counter_classes:
return CookingCounter(
name=item_info.equipment.name,
cooking_counter_equipments=self.cooking_counter_equipments,
equipments=self.cooking_counter_equipments[
item_info.equipment.name
],
pos=pos,
occupied_by=CookingEquipment(
name=item_info.name,
......
......@@ -3,20 +3,22 @@ what should happen when the agent wants to pick something up from the counter. O
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 `progress`
method, e.g., the `CuttingBoard.progress`. On which type of counter the progress method is called is currently
defined in the environment class.
method, e.g., the `CuttingBoard.progress`. The environment class detects which classes in this module have the
`progress` method defined and on instances of these classes the progress will be delegated.
Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the
interaction/requirements for the interaction.
CuttingBoard:
type: Equipment
```yaml
CuttingBoard:
type: Equipment
Sink:
type: Equipment
Sink:
type: Equipment
Stove:
type: Equipment
Stove:
type: Equipment
```
The defined counter classes are:
- `Counter`
......@@ -58,8 +60,10 @@ from overcooked_simulator.game_items import (
log = logging.getLogger(__name__)
"""The logger for this module."""
COUNTER_CATEGORY = "Counter"
"""The string for the `category` value in the json state representation for all counters."""
class Counter:
......@@ -81,12 +85,16 @@ class Counter:
pos: Position of the counter in the environment. 2-element vector.
occupied_by: The item on top of the counter.
"""
self.uuid = uuid.uuid4().hex if uid is None else None
self.uuid: str = uuid.uuid4().hex if uid is None else None
"""A unique id for better tracking in GUIs with assets which instance moved or changed."""
self.pos: npt.NDArray[float] = pos
"""The position of the counter."""
self.occupied_by: Optional[Item] = occupied_by
"""What is on top of the counter, e.g., `Item`s."""
@property
def occupied(self):
def occupied(self) -> bool:
"""Is something on top of the counter."""
return self.occupied_by is not None
def pick_up(self, on_hands: bool = True) -> Item | None:
......@@ -151,6 +159,7 @@ class Counter:
)
def to_dict(self) -> dict:
"""For the state representation. Only the relevant attributes are put into the dict."""
return {
"id": self.uuid,
"category": COUNTER_CATEGORY,
......@@ -180,11 +189,16 @@ class CuttingBoard(Counter):
"""
def __init__(self, pos: np.ndarray, transitions: dict[str, ItemInfo], **kwargs):
self.progressing = False
self.transitions = transitions
self.inverted_transition_dict = {
self.progressing: bool = False
"""Is a player progressing/cutting on the board."""
self.transitions: dict[str, ItemInfo] = transitions
"""The allowed transitions to a new item. Keys are the resulting items and the `ItemInfo` (value) contains
the needed items in the `need` attribute."""
self.inverted_transition_dict: dict[str, ItemInfo] = {
info.needs[0]: info for name, info in self.transitions.items()
}
"""For faster accessing the needed item. Keys are the ingredients that the player can put and chop on the
board."""
super().__init__(pos=pos, **kwargs)
def progress(self, passed_time: timedelta, now: datetime):
......@@ -259,10 +273,16 @@ class ServingWindow(Counter):
plate_dispenser: PlateDispenser = None,
**kwargs,
):
self.order_and_score = order_and_score
self.plate_dispenser = plate_dispenser
self.meals = meals
self.env_time_func = env_time_func
self.order_and_score: OrderAndScoreManager = order_and_score
"""Reference to the OrderAndScoreManager class. It determines which meals can be served and it manages the
score."""
self.plate_dispenser: PlateDispenser = plate_dispenser
"""Served meals are mentioned on the plate dispenser. So that the plate dispenser can add a dirty plate after
some time."""
self.meals: set[str] = meals
"""All allowed meals by the `environment_config.yml`."""
self.env_time_func: Callable[[], datetime] = env_time_func
"""Reference to get the current env time by calling the `env_time_func`."""
super().__init__(pos=pos, **kwargs)
def drop_off(self, item) -> Item | None:
......@@ -306,7 +326,8 @@ class Dispenser(Counter):
"""
def __init__(self, pos: npt.NDArray[float], dispensing: ItemInfo, **kwargs):
self.dispensing = dispensing
self.dispensing: ItemInfo = dispensing
"""`ItemInfo` what the the Dispenser is dispensing. One ready object always is on top of the counter."""
super().__init__(
pos=pos,
occupied_by=self.create_item(),
......@@ -329,6 +350,7 @@ class Dispenser(Counter):
return f"{self.dispensing.name}Dispenser"
def create_item(self) -> Item:
"""Create a new item to put on the dispenser after the previous one was picked up."""
kwargs = {
"name": self.dispensing.name,
"item_info": self.dispensing,
......@@ -378,12 +400,20 @@ class PlateDispenser(Counter):
**kwargs,
) -> None:
super().__init__(pos=pos, **kwargs)
self.dispensing = dispensing
self.occupied_by = deque()
self.out_of_kitchen_timer = []
self.plate_config = plate_config
self.next_plate_time = datetime.max
self.plate_transitions = plate_transitions
self.dispensing: ItemInfo = dispensing
"""Plate ItemInfo."""
self.occupied_by: deque = deque()
"""The queue of plates. New dirty ones are put at the end and therefore under the current plates."""
self.out_of_kitchen_timer: list[datetime] = []
"""Internal timer for how many plates are out of kitchen and how long."""
self.plate_config: PlateConfig = plate_config
"""The config how many plates are present in the kitchen at the beginning (and in total) and the config for
the random "out of kitchen" timer."""
self.next_plate_time: datetime = datetime.max
"""For efficient checking if dirty plates should be created, instead of looping through the
`out_of_kitchen_timer` list every frame."""
self.plate_transitions: dict[str, ItemInfo] = plate_transitions
"""Transitions for the plates. Relevant for the sink, because a plate can become a clean one there."""
self.setup_plates()
def pick_up(self, on_hands: bool = True) -> Item | None:
......@@ -394,8 +424,6 @@ class PlateDispenser(Counter):
return not self.occupied_by or self.occupied_by[-1].can_combine(item)
def drop_off(self, item: Item) -> Item | None:
"""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):
......@@ -456,6 +484,11 @@ class PlateDispenser(Counter):
return "PlateReturn"
def create_item(self, clean: bool = False) -> Plate:
"""Create a plate.
Args:
clean: Whether the plate is clean or dirty.
"""
kwargs = {
"clean": clean,
"transitions": self.plate_transitions,
......@@ -474,7 +507,8 @@ class Trashcan(Counter):
self, order_and_score: OrderAndScoreManager, pos: npt.NDArray[float], **kwargs
):
super().__init__(pos, **kwargs)
self.order_and_score = order_and_score
self.order_and_score: OrderAndScoreManager = order_and_score
"""Reference to the `OrderAndScoreManager`, because unnecessary removed items can affect the score."""
def pick_up(self, on_hands: bool = True) -> Item | None:
pass
......@@ -507,19 +541,18 @@ class CookingCounter(Counter):
def __init__(
self,
name: str,
cooking_counter_equipments: dict[str, list[str]],
equipments: set[str],
**kwargs,
):
self.name = name
self.cooking_counter_equipments = cooking_counter_equipments
self.name: str = name
"""The type/name of the cooking counter, e.g., Stove, DeepFryer, Oven."""
self.equipments: set[str] = equipments
"""The valid equipment for the cooking counter, e.g., for a Stove {'Pot','Pan'}."""
super().__init__(**kwargs)
def can_drop_off(self, item: Item) -> bool:
if self.occupied_by is None:
return (
isinstance(item, CookingEquipment)
and item.name in self.cooking_counter_equipments[self.name]
)
return isinstance(item, CookingEquipment) and item.name in self.equipments
else:
return self.occupied_by.can_combine(item)
......@@ -528,7 +561,7 @@ class CookingCounter(Counter):
if (
self.occupied_by
and isinstance(self.occupied_by, CookingEquipment)
and self.occupied_by.name in self.cooking_counter_equipments[self.name]
and self.occupied_by.name in self.equipments
and self.occupied_by.can_progress()
):
self.occupied_by.progress(passed_time, now)
......@@ -563,7 +596,8 @@ class Sink(Counter):
**kwargs,
):
super().__init__(pos=pos, **kwargs)
self.progressing = False
self.progressing: bool = False
"""If a player currently cleans a plate."""
self.sink_addon: SinkAddon = sink_addon
"""The connected sink addon which will receive the clean plates"""
self.occupied_by: deque[Plate] = deque()
......@@ -580,7 +614,8 @@ class Sink(Counter):
self.transition_needs.update([info.needs[0]])
@property
def occupied(self):
def occupied(self) -> bool:
"""If there is a plate in the sink."""
return len(self.occupied_by) != 0
def progress(self, passed_time: timedelta, now: datetime):
......@@ -631,6 +666,7 @@ class Sink(Counter):
return None
def set_addon(self, sink_addon: SinkAddon):
"""Set the closest addon in post_setup."""
self.sink_addon = sink_addon
def to_dict(self) -> dict:
......@@ -650,7 +686,8 @@ class SinkAddon(Counter):
def __init__(self, pos: npt.NDArray[float], occupied_by=None):
super().__init__(pos=pos)
# maybe check if occupied by is already a list or deque?
self.occupied_by = deque([occupied_by]) if occupied_by else deque()
self.occupied_by: deque = deque([occupied_by]) if occupied_by else deque()
"""The stack of clean plates."""
def can_drop_off(self, item: Item) -> bool:
return self.occupied_by and self.occupied_by[-1].can_combine(item)
......@@ -659,6 +696,7 @@ class SinkAddon(Counter):
return self.occupied_by[-1].combine(item)
def add_clean_plate(self, plate: Plate):
"""Called from the `Sink` after a plate is cleaned / the progress is complete."""
self.occupied_by.appendleft(plate)
def pick_up(self, on_hands: bool = True) -> Item | None:
......
......@@ -29,15 +29,22 @@ from enum import Enum
from typing import Optional, TypedDict
log = logging.getLogger(__name__)
"""The logger for this module."""
ITEM_CATEGORY = "Item"
"""The string for the `category` value in the json state representation for all normal items."""
COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment"
"""The string for the `category` value in the json state representation for all cooking equipments."""
class ItemType(Enum):
Ingredient = "Ingredient"
"""All ingredients and process ingredients."""
Meal = "Meal"
"""All combined ingredients that can be served."""
Equipment = "Equipment"
"""All counters and cooking equipments."""
@dataclasses.dataclass
......@@ -109,11 +116,16 @@ class Item:
def __init__(
self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs
):
self.name = self.__class__.__name__ if name is None else name
self.item_info = item_info
self.progress_equipment = None
self.progress_percentage = 0.0
self.uuid = uuid.uuid4().hex if uid is None else uid
self.name: str = self.__class__.__name__ if name is None else name
"""The name of the item, e.g., `Tomato` or `ChoppedTomato`"""
self.item_info: ItemInfo = item_info
"""The information about the item from the `item_info.yml` config."""
self.progress_equipment: str | None = None
"""The equipment with which the item was last progressed."""
self.progress_percentage: float = 0.0
"""The current progress percentage of the item if it is progress-able."""
self.uuid: str = uuid.uuid4().hex if uid is None else uid
"""A unique identifier for the item. Useful for GUIs that handles specific asset instances."""
def __repr__(self):
if self.progress_equipment is None:
......@@ -126,12 +138,15 @@ class Item:
@property
def extra_repr(self):
"""Stuff to add to the representation of the item in subclasses."""
return ""
def can_combine(self, other) -> bool:
"""Check if the item can be combined with the other. After it returned True the `combine` method is called."""
return False
def combine(self, other) -> Item | None:
"""Combine the item with another item based on possible transitions/needs."""
pass
def progress(self, equipment: str, percent: float):
......@@ -148,10 +163,12 @@ class Item:
)
def reset(self):
"""Reset the progress."""
self.progress_equipment = None
self.progress_percentage = 0.0
def to_dict(self) -> dict:
"""For the state representation. Only the relevant attributes are put into the dict."""
return {
"id": self.uuid,
"category": self.item_category,
......@@ -163,11 +180,13 @@ 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."""
item_category = "Cooking Equipment"
def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs):
super().__init__(*args, **kwargs)
self.transitions = transitions
self.transitions: dict[str, ItemInfo] = transitions
"""What is needed to cook a meal / create another ingredient."""
self.active_transition: Optional[ActiveTransitionTypedDict] = None
"""The info how and when to convert the content_list to a new item."""
......@@ -227,6 +246,7 @@ class CookingEquipment(Item):
return return_value
def can_progress(self) -> bool:
"""Check if the cooking equipment can progress items at all."""
return self.active_transition is not None
def progress(self, passed_time: datetime.timedelta, now: datetime.datetime):
......@@ -239,10 +259,12 @@ class CookingEquipment(Item):
# todo set active transition for fire/burnt?
def reset_content(self):
"""Reset the content attributes after the content was picked up from the equipment."""
self.content_list = []
self.content_ready = None
def release(self):
"""Release the content when the player "picks up" the equipment with a plate in the hands"""
content = self.content_list
self.reset_content()
return content
......@@ -256,6 +278,7 @@ class CookingEquipment(Item):
self.active_transition = None
def get_potential_meal(self) -> Item | None:
"""The meal that could be served depends on the attributes `content_ready` and `content_list`"""
if self.content_ready:
return self.content_ready
if len(self.content_list) == 1:
......@@ -281,8 +304,9 @@ 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
def __init__(self, transitions: dict[str, ItemInfo], clean: bool, *args, **kwargs):
self.clean: bool = clean
"""If the plate is clean or dirty."""
self.meals = set(transitions.keys())
"""All meals can be hold by a clean plate"""
super().__init__(
......@@ -292,14 +316,12 @@ class Plate(CookingEquipment):
**kwargs,
)
def finished_call(self):
self.clean = True
self.name = self.create_name()
def progress(self, equipment: str, percent: float):
Item.progress(self, equipment, percent)
def create_name(self):
"""The name depends on the clean or dirty state of the plate. Clean plates are `Plate`, otherwise
`DirtyPlate`."""
return "Plate" if self.clean else "DirtyPlate"
def can_combine(self, other):
......
......@@ -250,7 +250,7 @@ class EnvironmentHandler:
manager_id in self.manager_envs
and env_id in self.manager_envs[manager_id]
and self.envs[env_id].status
not in [EnvironmentStatus.STOPPED, Environment.PAUSED]
not in [EnvironmentStatus.STOPPED, EnvironmentStatus.PAUSED]
):
self.envs[env_id].status = EnvironmentStatus.PAUSED
......@@ -266,7 +266,7 @@ class EnvironmentHandler:
manager_id in self.manager_envs
and env_id in self.manager_envs[manager_id]
and self.envs[env_id].status
not in [EnvironmentStatus.STOPPED, Environment.PAUSED]
not in [EnvironmentStatus.STOPPED, EnvironmentStatus.PAUSED]
):
self.envs[env_id].status = EnvironmentStatus.PAUSED
self.envs[env_id].last_step_time = time.time_ns()
......
......@@ -91,8 +91,6 @@ class Visualizer:
grid_size,
)
print(state)
def draw_background(self, surface, width, height, grid_size):
"""Visualizes a game background."""
block_size = grid_size // 2 # Set the size of the grid block
......
......@@ -36,6 +36,9 @@ For an easier usage of the random orders, also some classes for type hints and d
- `ScoreCalcGenFuncType`
- `ExpiredPenaltyFuncType`
For the scoring of using the trashcan the `penalty_for_each_item` example function is defined. You can set/replace it
in the `environment_config`.
## Code Documentation
"""
......@@ -53,8 +56,10 @@ from typing import Callable, Tuple, Any, Deque, Protocol, TypedDict
from overcooked_simulator.game_items import Item, Plate, ItemInfo
log = logging.getLogger(__name__)
"""The logger for this module."""
ORDER_CATEGORY = "Order"
"""The string for the `category` value in the json state representation for all orders."""
@dataclasses.dataclass
......@@ -76,7 +81,7 @@ class Order:
expired_penalty: float
"""The penalty to the score if the order expires"""
uuid: str = dataclasses.field(default_factory=lambda: uuid.uuid4().hex)
"""The unique identifier for the order."""
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(
......@@ -84,10 +89,9 @@ class Order:
)
"""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
def create_penalties(self, env_time: datetime):
"""Create the general timed penalties list to check for penalties after some time the order is still not
fulfilled."""
for penalty_info in self.timed_penalties:
match penalty_info:
case (offset, penalty):
......@@ -116,6 +120,7 @@ class OrderGeneration:
def __init__(self, available_meals: dict[str, ItemInfo], **kwargs):
self.available_meals: list[ItemInfo] = list(available_meals.values())
"""Available meals restricted through the `environment_config.yml`."""
@abstractmethod
def init_orders(self, now) -> list[Order]:
......@@ -138,10 +143,12 @@ 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.score: float = 0.0
"""The current score of the environment."""
self.order_gen: OrderGeneration = order_config["order_gen_class"](
available_meals=available_meals, kwargs=order_config["order_gen_kwargs"]
)
"""The order generation."""
self.serving_not_ordered_meals: Callable[
[Item], Tuple[bool, float]
] = order_config["serving_not_ordered_meals"]
......@@ -170,6 +177,7 @@ class OrderAndScoreManager:
"""Cache last expired orders for `OrderGeneration.get_orders` call."""
def update_next_relevant_time(self):
"""For more efficient checking when to do something in the progress call."""
next_relevant_time = datetime.max
for order in self.open_orders:
next_relevant_time = min(
......@@ -180,6 +188,8 @@ class OrderAndScoreManager:
self.next_relevant_time = next_relevant_time
def serve_meal(self, item: Item, env_time: datetime) -> bool:
"""Is called by the ServingWindow to serve a meal. Returns True if the meal can be served and should be
"deleted" from the hands of the player."""
if isinstance(item, Plate):
meal = item.get_potential_meal()
if meal is not None:
......@@ -220,6 +230,7 @@ class OrderAndScoreManager:
return False
def increment_score(self, score: int | float):
"""Add a value to the current score and log it."""
self.score += score
log.debug(f"Score: {self.score}")
......@@ -271,6 +282,8 @@ class OrderAndScoreManager:
self.update_next_relevant_time()
def find_order_for_meal(self, meal) -> Tuple[Order, int] | None:
"""Get the order that will be fulfilled for a meal. At the moment the oldest order in the list that has the
same meal (name)."""
for index, order in enumerate(self.open_orders):
if order.meal.name == meal.name:
return order, index
......@@ -282,6 +295,7 @@ class OrderAndScoreManager:
order.create_penalties(env_time)
def order_state(self) -> list[dict]:
"""Similar to the `to_dict` in `Item` and `Counter`. Relevant for the state of the environment"""
return [
{
"id": order.uuid,
......@@ -294,6 +308,7 @@ class OrderAndScoreManager:
]
def apply_penalty_for_using_trash(self, remove: Item | list[Item]):
"""Is called if a item is put into the trashcan."""
self.increment_score(self.penalty_for_trash(remove))
......@@ -463,7 +478,7 @@ class RandomOrderGeneration(OrderGeneration):
super().__init__(available_meals, **kwargs)
self.kwargs: RandomOrderKwarg = RandomOrderKwarg(**kwargs["kwargs"])
self.next_order_time: datetime | None = datetime.max
self.number_cur_orders = 0
self.number_cur_orders: int = 0
self.needed_orders: int = 0
"""For the sample on dur but when it was restricted due to max order number."""
......
......@@ -20,6 +20,7 @@ from overcooked_simulator.game_items import Item, Plate
from overcooked_simulator.state_representation import PlayerState
log = logging.getLogger(__name__)
"""The logger for this module."""
@dataclasses.dataclass
......@@ -79,9 +80,13 @@ class Player:
calculated with."""
self.current_movement: npt.NDArray[float] = np.zeros(2, float)
"""The movement vector that will be used to calculate the movement in the next step call."""
self.movement_until: datetime.datetime = datetime.datetime.min
"""The env time until the player wants to move."""
def set_movement(self, move_vector, move_until):
"""Called by the `perform_action` method. Movements will be performed (pos will be updated) in the `step`
function of the environment"""
self.current_movement = move_vector
self.movement_until = move_until
......@@ -127,7 +132,8 @@ class Player:
Args:
counter: The counter, can the player reach it?
Returns: True if the counter is in range of the player, False if not.
Returns:
True if the counter is in range of the player, False if not.
"""
return np.linalg.norm(counter.pos - self.facing_point) <= self.interaction_range
......@@ -182,6 +188,7 @@ class Player:
return f"Player(name:{self.name},pos:{str(self.pos)},holds:{self.holding})"
def to_dict(self) -> PlayerState:
"""For the state representation. Only the relevant attributes are put into the dict."""
# TODO add color to player class for vis independent player color
return {
"id": self.name,
......
"""
Type hint classes for the representation of the json state.
"""
from datetime import datetime
from pydantic import BaseModel
......@@ -51,11 +54,15 @@ class PlayerState(TypedDict):
class KitchenInfo(BaseModel):
"""Basic information of the kitchen."""
width: float
height: float
class StateRepresentation(BaseModel):
"""The format of the returned state representation."""
players: list[PlayerState]
counters: list[CounterState]
kitchen: KitchenInfo
......
"""
Some utility functions.
"""
import logging
import os
import sys
......
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