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

Refactor code and update documentation

The code changes include renaming the 'category' field from "Cooking Equipment" to "ItemCookingEquipment" in sample_state.json. The documentation has also been updated with more detailed explanations on how the system works, including how to install and use it. A few function signatures were changed and additional explanation comments were included in the counters.py and utils.py files for better code clarity.
parent ff409005
No related branches found
No related tags found
No related merge requests found
Pipeline #45188 passed
...@@ -19,9 +19,22 @@ With this overcooked-simulator, we want to bring both worlds together: the reinf ...@@ -19,9 +19,22 @@ With this overcooked-simulator, we want to bring both worlds together: the reinf
environment with an appealing visualisation. Enable the potential of developing artificial agents that play with humans environment with an appealing visualisation. Enable the potential of developing artificial agents that play with humans
like a "real" cooperative / human partner. like a "real" cooperative / human partner.
# Usage / Examples # Installation
Our overcooked simulator is designed for real time interaction but also with reinforcement learning in mind (gymnasium environment).
It focuses on configurability, extensibility and appealing visualization options. You need a Python **3.10** or higher environment.
```bash
pip install overcooked-environment@git+https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator@main
```
Or clone it and install it as an editable library which allows you to use all the scripts directly.
```bash
git clone https://gitlab.ub.uni-bielefeld.de/scs/cocosy/overcooked-simulator.git
cd overcooked_simulator
pip install -e .
```
# Usage / Examples Our overcooked simulator is designed for real time interaction but also with reinforcement
learning in mind (gymnasium environment). It focuses on configurability, extensibility and appealing visualization
options.
## Human Player ## Human Player
Run it via the command line (in your pyenv/conda environment): Run it via the command line (in your pyenv/conda environment):
...@@ -75,7 +88,7 @@ env_info: CreateEnvResult = env_info.json() ...@@ -75,7 +88,7 @@ env_info: CreateEnvResult = env_info.json()
env_info = requests.post("http://localhost:8000/manage/create_env", json=create_env) env_info = requests.post("http://localhost:8000/manage/create_env", json=create_env)
``` ```
Connect for each player to a websocket (threaded or async). Connect for each player a websocket (threaded or async).
```python ```python
import json import json
import dataclasses import dataclasses
...@@ -133,7 +146,7 @@ websocket.recv() ...@@ -133,7 +146,7 @@ websocket.recv()
Stop the environment if you want the game to end before the time is up. Stop the environment if you want the game to end before the time is up.
```python ```python
requests.post( requests.post(
f"http://localhost:8000/manage/stop_env", "http://localhost:8000/manage/stop_env",
json={ json={
"manager_id": "SECRETKEY1", "manager_id": "SECRETKEY1",
"env_id": env_info["env_id"], "env_id": env_info["env_id"],
...@@ -142,12 +155,13 @@ requests.post( ...@@ -142,12 +155,13 @@ requests.post(
) )
``` ```
## Direct integration into your code. ## Direct integration into your code. You can use the `overcooked_simulator.overcooked_environment.Environment` class
You can use the [overcooked_simulator.overcooked_environment.Environment] class and call the `step`, `get_json_state`, and `perform_action` methods directly. and call the `step`, `get_json_state`, and `perform_action` methods directly.
```python ```python
from datetime import timedelta from datetime import timedelta
from overcooked_simulator import ROOT_DIR
from overcooked_simulator.overcooked_environment import Action, Environment from overcooked_simulator.overcooked_environment import Action, Environment
env = Environment( env = Environment(
...@@ -177,25 +191,39 @@ The JSON Schema for the state of the environment for a player can be generated b ...@@ -177,25 +191,39 @@ The JSON Schema for the state of the environment for a player can be generated b
python state_representation.py python state_representation.py
``` ```
Should look like 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'} {'$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. 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.
# Generate images from JSON states
You might have stored some json states and now you want to visualize them via the
pygame-2d visualization. You can do that by running the `drawing.py` script and referencing a json file.
```bash
python3 overcooked_simulator/gui_2d_vis/drawing.py --state my_state.json
```
- You can specify a different visualization config with `-v` or `--visualization_config`.
- You can specify the name of the output file with `-o` or `--output_file`. The default is `screenshot.jpg`.
# Citation # Citation
# Structure of the Documentation # Structure of the Documentation
The API documentation follows the file and content structure in the repo. 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 On the left you can find the navigation panel that brings you to the implementation of
- the **counter factory** converts the characters in the layout file to counter instances,
- the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven), - the **counters**, including the kitchen utility objects like dispenser, cooking counter (stove, deep fryer, oven),
sink, etc., sink, etc.,
- the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals, - the **game items**, the holdable ingredients, cooking equipment, composed ingredients, and meals,
- in **main**, you find an example how to start a simulation, - the **game server**, which can manage several running environments and can communicates via FastAPI post requests and
websockets,
- the **orders**, how to sample incoming orders and their attributes, - the **orders**, how to sample incoming orders and their attributes,
- the **environment**, handles the incoming actions and provides the state, - the **environment**, handles the incoming actions and provides the state,
- the **player**/agent, that interacts in the environment, - the **player**/agent, that interacts in the environment,
- a **simulation runner**, that calls the step function of the environment for a real-time interaction, and - type hints are defined in **state representation** for the json state and **server results** for the data returned by
the game server in post requests.
- **util**ity code. - **util**ity code.
......
...@@ -126,27 +126,36 @@ class CounterFactory: ...@@ -126,27 +126,36 @@ class CounterFactory:
serving_window_additional_kwargs: Additional keyword arguments for serving window configuration. serving_window_additional_kwargs: Additional keyword arguments for serving window configuration.
plate_config: The configuration for plate usage. plate_config: The configuration for plate usage.
""" """
self.layout_chars_config = convert_words_to_chars(layout_chars_config) self.layout_chars_config: dict[str, str] = convert_words_to_chars(
self.item_info = item_info layout_chars_config
self.serving_window_additional_kwargs = serving_window_additional_kwargs )
self.plate_config = plate_config """Layout chars to the counter names."""
self.order_and_score = order_and_score self.item_info: dict[str, ItemInfo] = item_info
"""All item infos from the `item_info` config."""
self.no_counter_chars = set( self.serving_window_additional_kwargs: dict[
str, Any
] = serving_window_additional_kwargs
"""The additional keyword arguments for the serving window."""
self.plate_config: PlateConfig = plate_config
"""The plate config from the `environment_config`"""
self.order_and_score: OrderAndScoreManager = order_and_score
"""The order and score manager to pass to `ServingWindow` and the `Tashcan` which can affect the scores."""
self.no_counter_chars: set[str] = set(
c c
for c, name in self.layout_chars_config.items() for c, name in self.layout_chars_config.items()
if name in ["Agent", "Free"] if name in ["Agent", "Free"]
) )
"""A set of characters that represent counters for agents or free spaces.""" """A set of characters that represent counters for agents or free spaces."""
self.counter_classes = dict( self.counter_classes: dict[str, Type] = dict(
inspect.getmembers( inspect.getmembers(
sys.modules["overcooked_simulator.counters"], inspect.isclass sys.modules["overcooked_simulator.counters"], inspect.isclass
) )
) )
"""A dictionary of counter classes imported from the 'overcooked_simulator.counters' module.""" """A dictionary of counter classes imported from the 'overcooked_simulator.counters' module."""
self.cooking_counter_equipments = { self.cooking_counter_equipments: dict[str, set[str]] = {
cooking_counter: { cooking_counter: {
equipment equipment
for equipment, e_info in self.item_info.items() for equipment, e_info in self.item_info.items()
......
...@@ -9,16 +9,17 @@ method, e.g., the `CuttingBoard.progress`. The environment class detects which c ...@@ -9,16 +9,17 @@ method, e.g., the `CuttingBoard.progress`. The environment class detects which c
Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the Inside the item_info.yaml, equipment needs to be defined. It includes counters that are part of the
interaction/requirements for the interaction. interaction/requirements for the interaction.
```yaml Example:
CuttingBoard:
type: Equipment CuttingBoard:
type: Equipment
Sink: Sink:
type: Equipment type: Equipment
Stove:
type: Equipment
Stove:
type: Equipment
```
The defined counter classes are: The defined counter classes are:
- `Counter` - `Counter`
...@@ -431,6 +432,7 @@ class PlateDispenser(Counter): ...@@ -431,6 +432,7 @@ class PlateDispenser(Counter):
return None return None
def add_dirty_plate(self): def add_dirty_plate(self):
"""Add a dirty plate after a timer is completed."""
self.occupied_by.appendleft(self.create_item()) self.occupied_by.appendleft(self.create_item())
def update_plate_out_of_kitchen(self, env_time: datetime): def update_plate_out_of_kitchen(self, env_time: datetime):
...@@ -658,7 +660,7 @@ class Sink(Counter): ...@@ -658,7 +660,7 @@ class Sink(Counter):
def can_drop_off(self, item: Item) -> bool: def can_drop_off(self, item: Item) -> bool:
return isinstance(item, Plate) and not item.clean return isinstance(item, Plate) and not item.clean
def drop_off(self, item: Item) -> Item | None: def drop_off(self, item: Plate) -> Item | None:
self.occupied_by.appendleft(item) self.occupied_by.appendleft(item)
return None return None
......
...@@ -112,6 +112,7 @@ class Item: ...@@ -112,6 +112,7 @@ class Item:
"""Base class for game items which can be held by a player.""" """Base class for game items which can be held by a player."""
item_category = ITEM_CATEGORY item_category = ITEM_CATEGORY
"""Class dependent category (is changed for the `CookingEquipment` class). """
def __init__( def __init__(
self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs
...@@ -181,7 +182,7 @@ class CookingEquipment(Item): ...@@ -181,7 +182,7 @@ class CookingEquipment(Item):
"""Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself ( """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.""" progress_percentage) and not in the items in the content list."""
item_category = "Cooking Equipment" item_category = COOKING_EQUIPMENT_ITEM_CATEGORY
def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs): def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
......
...@@ -541,7 +541,7 @@ if __name__ == "__main__": ...@@ -541,7 +541,7 @@ if __name__ == "__main__":
) )
parser.add_argument( parser.add_argument(
"-o", "-o",
"--output_filename", "--output_file",
type=str, type=str,
default="screenshot.jpg", default="screenshot.jpg",
) )
...@@ -550,4 +550,4 @@ if __name__ == "__main__": ...@@ -550,4 +550,4 @@ if __name__ == "__main__":
viz_config = yaml.safe_load(f) viz_config = yaml.safe_load(f)
with open(args.state, "r") as f: with open(args.state, "r") as f:
state = json.load(f) state = json.load(f)
save_screenshot(state, viz_config, args.output_filename) save_screenshot(state, viz_config, args.output_file)
...@@ -50,7 +50,7 @@ ...@@ -50,7 +50,7 @@
], ],
"occupied_by": { "occupied_by": {
"id": "240eb5d4e4dd4215a0ab9fbaa93f02f5", "id": "240eb5d4e4dd4215a0ab9fbaa93f02f5",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "Pan", "type": "Pan",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
...@@ -67,7 +67,7 @@ ...@@ -67,7 +67,7 @@
], ],
"occupied_by": { "occupied_by": {
"id": "54124ff47f9a4b2abb6e2df96a9db919", "id": "54124ff47f9a4b2abb6e2df96a9db919",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "Pot", "type": "Pot",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
], ],
"occupied_by": { "occupied_by": {
"id": "987ea1da780c44cabc06883f5c721414", "id": "987ea1da780c44cabc06883f5c721414",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "Basket", "type": "Basket",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
], ],
"occupied_by": { "occupied_by": {
"id": "a8609b417c0a4c1f8a0d10ef042f2df6", "id": "a8609b417c0a4c1f8a0d10ef042f2df6",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "Peel", "type": "Peel",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
...@@ -391,7 +391,7 @@ ...@@ -391,7 +391,7 @@
"occupied_by": [ "occupied_by": [
{ {
"id": "db41a69e880d4d64b535b265199d4148", "id": "db41a69e880d4d64b535b265199d4148",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "DirtyPlate", "type": "DirtyPlate",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
...@@ -399,7 +399,7 @@ ...@@ -399,7 +399,7 @@
}, },
{ {
"id": "13fd4df8399d48c490091bb9ed3e1302", "id": "13fd4df8399d48c490091bb9ed3e1302",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "DirtyPlate", "type": "DirtyPlate",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
...@@ -407,7 +407,7 @@ ...@@ -407,7 +407,7 @@
}, },
{ {
"id": "5075019e20ed45c1aa7aa01ca5198c50", "id": "5075019e20ed45c1aa7aa01ca5198c50",
"category": "Cooking Equipment", "category": "ItemCookingEquipment",
"type": "Plate", "type": "Plate",
"progress_percentage": 0.0, "progress_percentage": 0.0,
"content_list": [], "content_list": [],
......
...@@ -40,6 +40,8 @@ def get_closest(point: npt.NDArray[float], counters: list[Counter]): ...@@ -40,6 +40,8 @@ def get_closest(point: npt.NDArray[float], counters: list[Counter]):
def custom_asdict_factory(data): def custom_asdict_factory(data):
"""Convert enums to their value."""
def convert_value(obj): def convert_value(obj):
if isinstance(obj, Enum): if isinstance(obj, Enum):
return obj.value return obj.value
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment