diff --git a/overcooked_simulator/__init__.py b/overcooked_simulator/__init__.py index cfd15d482adac935144d6b73af50657fec6b67c4..6ddcbf220d2e9c738d430a3ac112f29602985217 100644 --- a/overcooked_simulator/__init__.py +++ b/overcooked_simulator/__init__.py @@ -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 diff --git a/overcooked_simulator/counter_factory.py b/overcooked_simulator/counter_factory.py index 8f36b974384b8c78bbf92b004e830dc6769630d1..8e9bfbbffe085bb9d5b71de00964961977495ed8 100644 --- a/overcooked_simulator/counter_factory.py +++ b/overcooked_simulator/counter_factory.py @@ -1,3 +1,36 @@ +""" +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, diff --git a/overcooked_simulator/counters.py b/overcooked_simulator/counters.py index c3e33392d38b4331273061c8c21a1bdd5687eb1a..bcdf48ca8156f137cf3e680bdf028b6c3da0ae67 100644 --- a/overcooked_simulator/counters.py +++ b/overcooked_simulator/counters.py @@ -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: diff --git a/overcooked_simulator/game_items.py b/overcooked_simulator/game_items.py index a77f8d78159ee6e6542209230e7bc50a6c0e76df..85f19a4256460ba1bee695068daba67dee282613 100644 --- a/overcooked_simulator/game_items.py +++ b/overcooked_simulator/game_items.py @@ -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): diff --git a/overcooked_simulator/game_server.py b/overcooked_simulator/game_server.py index 95e8e18fb6733f319d85fc58dfcef18471e66521..1848a5329552dc8e0f733fcc3b330fd78ada83dd 100644 --- a/overcooked_simulator/game_server.py +++ b/overcooked_simulator/game_server.py @@ -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() diff --git a/overcooked_simulator/gui_2d_vis/drawing.py b/overcooked_simulator/gui_2d_vis/drawing.py index d2f44c42de154e0f98b59bb02c572518422c7a63..0a88be41c0832cef603b2bfa4abc1b4846149e95 100644 --- a/overcooked_simulator/gui_2d_vis/drawing.py +++ b/overcooked_simulator/gui_2d_vis/drawing.py @@ -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 diff --git a/overcooked_simulator/order.py b/overcooked_simulator/order.py index b44d014d24a1df78cefcc1b5cf55b536f166b465..b61c9218f7adce6b059933eaf27d66c9bb9e8ee1 100644 --- a/overcooked_simulator/order.py +++ b/overcooked_simulator/order.py @@ -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.""" diff --git a/overcooked_simulator/player.py b/overcooked_simulator/player.py index 4c7257ddb13a1d7e2ca4f28ca8358659ce10643b..5f87b87f7032e54f12cace9fecda1d882aac5c54 100644 --- a/overcooked_simulator/player.py +++ b/overcooked_simulator/player.py @@ -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, diff --git a/overcooked_simulator/state_representation.py b/overcooked_simulator/state_representation.py index 4fc0a137a097a81047c790c1a8741399c2ad220d..47ddf2878977b394b35855732bcdd52a4bc0d0da 100644 --- a/overcooked_simulator/state_representation.py +++ b/overcooked_simulator/state_representation.py @@ -1,3 +1,6 @@ +""" +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 diff --git a/overcooked_simulator/utils.py b/overcooked_simulator/utils.py index edf11d72e8b4a5cbaa8672d9da727d6e4d3c312f..3b944b3c6ebe3fc118d75990991ef16d3e504192 100644 --- a/overcooked_simulator/utils.py +++ b/overcooked_simulator/utils.py @@ -1,3 +1,7 @@ +""" +Some utility functions. +""" + import logging import os import sys