diff --git a/CHANGELOG.md b/CHANGELOG.md index 01fe2e8fc1478e88420e03f3568cfb9df451c09d..817d255463cdd268a6511bb907974164524de0a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,14 @@ ### Added +- More Docstrings + ### Changed +- No keyboard tutorial. Only the controller is described. In the future, add config option which to show. +- Study Doc is now a .jsonl file. +- Other default processing time. + ### Deprecated ### Removed diff --git a/cooperative_cuisine/__init__.py b/cooperative_cuisine/__init__.py index e30632759d68efe1389179f31f912fecd9ac26f7..ae22c4821557167170e293e37cdc9e7229e49bed 100644 --- a/cooperative_cuisine/__init__.py +++ b/cooperative_cuisine/__init__.py @@ -11,13 +11,12 @@ can be found [here](https://gitlab.ub.uni-bielefeld.de/scs/cocosy/godot-overcook # Background / Literature The overcooked/cooking domain is a well established cooperation domain/task. There exists environments designed for reinforcement learning agents as well as the game and adaptations of the game for human -players in a more "real-time"-like environment. They all mostly differ in the visual and graphics dimension. 2D versions -like overcooked-ai, ... are most well-known in the community. But more visually appealing 3D versions for cooperation with -humans are getting developed more frequently (cite,...). Besides, the general adaptations of the original overcooked -game. -CooperativeCuisine, we want to bring both worlds together: the reinforcement learning and real-time playable -environment with an appealing visualisation. Enable the potential of developing artificial agents that play with humans -like a "real", cooperative, human partner. +players in a more "real-time"-like environment. They all mostly differ in the visual and graphics dimension. 2D +versions like [overcooked-ai](https://github.com/HumanCompatibleAI/overcooked_ai), [gym-cooking]( +https://github.com/rosewang2008/gym-cooking) are most well-known in the community. Besides, the general +adaptations of the original overcooked game. CooperativeCuisine, we want to bring both worlds together: the +reinforcement learning and real-time playable environment with an appealing visualisation. Enable the potential of +developing artificial agents that play with humans like a "real", cooperative, human partner. # Installation @@ -200,7 +199,7 @@ python state_representation.py ``` Should look like - to have an interactive view, have a look at [jsonschemaviewer](https://navneethg.github.io/jsonschemaviewer/): ``` -{'$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'}, 'orientation': {'items': {'type': 'number'}, 'title': 'Orientation', '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'}, 'active_effects': {'items': {'$ref': '#/$defs/EffectState'}, 'title': 'Active Effects', 'type': 'array'}}, 'required': ['id', 'category', 'type', 'pos', 'orientation', 'occupied_by', 'active_effects'], 'title': 'CounterState', 'type': 'object'}, 'EffectState': {'properties': {'id': {'title': 'Id', 'type': 'string'}, 'type': {'title': 'Type', 'type': 'string'}, 'progress_percentage': {'anyOf': [{'type': 'number'}, {'type': 'integer'}], 'title': 'Progress Percentage'}, 'inverse_progress': {'title': 'Inverse Progress', 'type': 'boolean'}}, 'required': ['id', 'type', 'progress_percentage', 'inverse_progress'], 'title': 'EffectState', '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'}, 'inverse_progress': {'title': 'Inverse Progress', 'type': 'boolean'}, 'active_effects': {'items': {'$ref': '#/$defs/EffectState'}, 'title': 'Active Effects', 'type': 'array'}}, 'required': ['id', 'category', 'type', 'progress_percentage', 'inverse_progress', 'active_effects'], 'title': 'ItemState', 'type': 'object'}, 'KitchenInfo': {'description': 'Basic information of the kitchen.', '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'}, 'ViewRestriction': {'properties': {'direction': {'items': {'type': 'number'}, 'title': 'Direction', 'type': 'array'}, 'position': {'items': {'type': 'number'}, 'title': 'Position', 'type': 'array'}, 'angle': {'title': 'Angle', 'type': 'integer'}, 'counter_mask': {'anyOf': [{'items': {'type': 'boolean'}, 'type': 'array'}, {'type': 'null'}], 'title': 'Counter Mask'}, 'range': {'anyOf': [{'type': 'number'}, {'type': 'null'}], 'title': 'Range'}}, 'required': ['direction', 'position', 'angle', 'counter_mask', 'range'], 'title': 'ViewRestriction', 'type': 'object'}}, 'description': 'The format of the returned state representation.', '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'}, 'all_players_ready': {'title': 'All Players Ready', 'type': 'boolean'}, 'ended': {'title': 'Ended', 'type': 'boolean'}, 'env_time': {'format': 'date-time', 'title': 'Env Time', 'type': 'string'}, 'remaining_time': {'title': 'Remaining Time', 'type': 'number'}, 'view_restrictions': {'anyOf': [{'items': {'$ref': '#/$defs/ViewRestriction'}, 'type': 'array'}, {'type': 'null'}], 'title': 'View Restrictions'}, 'served_meals': {'items': {'maxItems': 2, 'minItems': 2, 'prefixItems': [{'type': 'string'}, {'type': 'string'}], 'type': 'array'}, 'title': 'Served Meals', 'type': 'array'}, 'info_msg': {'items': {'maxItems': 2, 'minItems': 2, 'prefixItems': [{'type': 'string'}, {'type': 'string'}], 'type': 'array'}, 'title': 'Info Msg', 'type': 'array'}}, 'required': ['players', 'counters', 'kitchen', 'score', 'orders', 'all_players_ready', 'ended', 'env_time', 'remaining_time', 'view_restrictions', 'served_meals', 'info_msg'], 'title': 'StateRepresentation', 'type': 'object'} +{"$defs": {"CookingEquipmentState": {"description": "Format of the state representation of cooking equipment.", "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"}, "inverse_progress": {"title": "Inverse Progress", "type": "boolean"}, "active_effects": {"items": {"$ref": "#/$defs/EffectState"}, "title": "Active Effects", "type": "array"}, "content_list": {"items": {"$ref": "#/$defs/ItemState"}, "title": "Content List", "type": "array"}, "content_ready": {"anyOf": [{"$ref": "#/$defs/ItemState"}, {"type": "null"}]}}, "required": ["id", "category", "type", "progress_percentage", "inverse_progress", "active_effects", "content_list", "content_ready"], "title": "CookingEquipmentState", "type": "object"}, "CounterState": {"description": "Format of the state representation of a counter.", "properties": {"id": {"title": "Id", "type": "string"}, "category": {"const": "Counter", "title": "Category"}, "type": {"title": "Type", "type": "string"}, "pos": {"items": {"type": "number"}, "title": "Pos", "type": "array"}, "orientation": {"items": {"type": "number"}, "title": "Orientation", "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"}, "active_effects": {"items": {"$ref": "#/$defs/EffectState"}, "title": "Active Effects", "type": "array"}}, "required": ["id", "category", "type", "pos", "orientation", "occupied_by", "active_effects"], "title": "CounterState", "type": "object"}, "EffectState": {"description": "Format of the state representation of an effect (fire).", "properties": {"id": {"title": "Id", "type": "string"}, "type": {"title": "Type", "type": "string"}, "progress_percentage": {"anyOf": [{"type": "number"}, {"type": "integer"}], "title": "Progress Percentage"}, "inverse_progress": {"title": "Inverse Progress", "type": "boolean"}}, "required": ["id", "type", "progress_percentage", "inverse_progress"], "title": "EffectState", "type": "object"}, "ItemState": {"description": "Format of the state representation of an item.", "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"}, "inverse_progress": {"title": "Inverse Progress", "type": "boolean"}, "active_effects": {"items": {"$ref": "#/$defs/EffectState"}, "title": "Active Effects", "type": "array"}}, "required": ["id", "category", "type", "progress_percentage", "inverse_progress", "active_effects"], "title": "ItemState", "type": "object"}, "KitchenInfo": {"description": "Format of the state representation of basic information of the kitchen.", "properties": {"width": {"title": "Width", "type": "number"}, "height": {"title": "Height", "type": "number"}}, "required": ["width", "height"], "title": "KitchenInfo", "type": "object"}, "OrderState": {"description": "Format of the state representation of an order.", "properties": {"id": {"title": "Id", "type": "string"}, "category": {"const": "Order", "title": "Category"}, "meal": {"title": "Meal", "type": "string"}, "start_time": {"anyOf": [{"format": "date-time", "type": "string"}, {"type": "string"}], "title": "Start Time"}, "max_duration": {"title": "Max Duration", "type": "number"}, "score": {"anyOf": [{"type": "number"}, {"type": "integer"}], "title": "Score"}}, "required": ["id", "category", "meal", "start_time", "max_duration", "score"], "title": "OrderState", "type": "object"}, "PlayerState": {"description": "Format of the state representation of a player.", "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"}, "ViewRestriction": {"description": "Format of the state representation of a view restriction from the players perspectives.\nCurrently, as a view cone, like a flashlight in the dark.", "properties": {"direction": {"items": {"type": "number"}, "title": "Direction", "type": "array"}, "position": {"items": {"type": "number"}, "title": "Position", "type": "array"}, "angle": {"title": "Angle", "type": "integer"}, "counter_mask": {"anyOf": [{"items": {"type": "boolean"}, "type": "array"}, {"type": "null"}], "title": "Counter Mask"}, "range": {"anyOf": [{"type": "number"}, {"type": "null"}], "title": "Range"}}, "required": ["direction", "position", "angle", "counter_mask", "range"], "title": "ViewRestriction", "type": "object"}}, "description": "The format of the returned state representation.", "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"}, "view_restrictions": {"anyOf": [{"items": {"$ref": "#/$defs/ViewRestriction"}, "type": "array"}, {"type": "null"}], "title": "View Restrictions"}, "served_meals": {"items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"type": "string"}, {"type": "string"}], "type": "array"}, "title": "Served Meals", "type": "array"}, "info_msg": {"items": {"maxItems": 2, "minItems": 2, "prefixItems": [{"type": "string"}, {"type": "string"}], "type": "array"}, "title": "Info Msg", "type": "array"}}, "required": ["players", "counters", "kitchen", "score", "orders", "ended", "env_time", "remaining_time", "view_restrictions", "served_meals", "info_msg"], "title": "StateRepresentation", "type": "object"} ``` @@ -295,10 +294,8 @@ plates: game: time_limit_seconds: 300 - undo_dispenser_pickup: true - -meals: - all: true + undo_dispenser_pickup: true + validate_recipes: true layout_chars: _: Free @@ -314,6 +311,8 @@ layout_chars: T: Tomato orders: # how to create orders + meals: + all: true ... player_config: diff --git a/cooperative_cuisine/argument_parser.py b/cooperative_cuisine/argument_parser.py index f6bc86eb06d4c011239af26b467f2ea42c3541a6..d5c6fd4660c47e70a359de5fb95a735d19d43235 100644 --- a/cooperative_cuisine/argument_parser.py +++ b/cooperative_cuisine/argument_parser.py @@ -77,7 +77,6 @@ def disable_websocket_logging_arguments(parser): Args: parser: The argument parser object (argparse.ArgumentParser) to which the "--enable-websocket-logging" argument will be added. - """ parser.add_argument( "--enable-websocket-logging" "", action="store_true", default=True @@ -89,9 +88,6 @@ def add_list_of_manager_ids_arguments(parser): Args: parser: An ArgumentParser object used to parse command line arguments. - - Returns: - None """ parser.add_argument( "-m", diff --git a/cooperative_cuisine/counters.py b/cooperative_cuisine/counters.py index eb39a753b95e938c077def8ebd36697bcef37eac..1b03174774dabac65690c83f4313ea07e3dc03ad 100644 --- a/cooperative_cuisine/counters.py +++ b/cooperative_cuisine/counters.py @@ -106,7 +106,7 @@ class Counter: pos: npt.NDArray[float], hook: Hooks, occupied_by: Item | None = None, - uid: hex = None, + uid: str = None, **kwargs, ): """Constructor setting the arguments as attributes. @@ -255,6 +255,24 @@ class Counter: ) def do_tool_interaction(self, passed_time: timedelta, tool: Item): + """Progress call for player that interact with a counter (optionally via a tool). + + Args: + passed_time: The amount of time that has passed between the environment step calls. + tool: The tool being used for interaction. + + This method performs tool interaction on the specified tool. It can handle cases where the location is + occupied by a single item or a deque of items. + + If the location is occupied by a deque of items, the method will iterate through each item and attempt to + perform the tool interaction. If succesful, it will set the 'successful' flag to True. + + If the location is occupied by a single item, the method will directly call the 'do_single_tool_interaction' + method to perform the interaction. + + If none of the attempts were successful, the method will call the 'do_single_tool_interaction' method on + itself to perform the interaction. + """ successful = False if self.occupied_by: if isinstance(self.occupied_by, deque): diff --git a/cooperative_cuisine/effects.py b/cooperative_cuisine/effects.py index 53989c0916dd9d8f2fe82b44ef2be4f971e1da29..5c7ff26e80b2c9123d7f3d024a1752b217ae40af 100644 --- a/cooperative_cuisine/effects.py +++ b/cooperative_cuisine/effects.py @@ -43,16 +43,16 @@ class EffectManager: hook: An instance of the Hooks class representing the hooks in the environment. random: An instance of the Random class representing the random number generator. """ - self.effects = [] + self.effects: list[ItemInfo] = [] """A list of ItemInfo objects representing the effects managed by the manager.""" - self.counters = [] + self.counters: list[Counter] = [] """A list of Counter objects representing the counters in the environment.""" - self.hook = hook + self.hook: Hooks = hook """An instance of the Hooks class representing the hooks in the environment.""" self.new_effects: list[Tuple[Effect, Item | Counter]] = [] """A list of tuples containing an Effect object and either an Item or Counter object representing the new active effects.""" - self.random = random + self.random: Random = random """An instance of the Random class representing the random number generator.""" def add_effect(self, effect: ItemInfo): diff --git a/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py index 90be31686eb64c7cd15a3a034cb5c2e2fc4edf34..8f8cc1b650be6a5275000a0a5571e908c6171560 100644 --- a/cooperative_cuisine/environment.py +++ b/cooperative_cuisine/environment.py @@ -17,7 +17,7 @@ from collections import defaultdict from datetime import timedelta, datetime from pathlib import Path from random import Random -from typing import Literal, TypedDict, Callable, Set +from typing import Literal, TypedDict, Callable, Set, Any import numpy as np import numpy.typing as npt @@ -94,7 +94,7 @@ class EnvironmentConfig(TypedDict): """Definition of which characters in the layout file correspond to which kitchen counter.""" hook_callbacks: dict[str, dict] """Configuration of callbacks via HookCallbackClass.""" - effect_manager: dict + effect_manager: dict[str, dict] """Config of different effects in the environment, which control for example fire behavior.""" @@ -136,7 +136,7 @@ class Environment: env_name: (optional) The name of the environment. Defaults to "overcooked_sim". seed: (optional) The seed for generating random numbers. Defaults to 56789223842348. """ - self.env_name = env_name + self.env_name: str = env_name """Reference to the run. E.g, the env id.""" self.env_time: datetime = create_init_env_time() """the internal time of the environment. An environment starts always with the time from @@ -305,7 +305,7 @@ class Environment: self.order_manager.create_init_orders(self.env_time) self.start_time: datetime = self.env_time """The relative env time when it started.""" - self.env_time_end = self.env_time + timedelta( + self.env_time_end: datetime = self.env_time + timedelta( seconds=self.environment_config["game"]["time_limit_seconds"] ) """The relative env time when it will stop/end""" @@ -314,7 +314,7 @@ class Environment: self.info_msgs_per_player: dict[str, list[InfoMsg]] = defaultdict(list) """Cache of info messages per player which should be showed in the visualization of each player.""" - self.additional_state_content = {} + self.additional_state_content: dict[str, Any] = {} """The environment will extend the content of each state with this dictionary. Adapt it with the setter function.""" @@ -587,5 +587,12 @@ class Environment: log.debug(f"Score: {self.score} ({score}) - {info}") def update_additional_state_content(self, **kwargs): + """ + Update the additional state content with the given key-value pairs. + + Args: + **kwargs: The key-value pairs to update the additional state content with. + + """ self.hook(ADDITIONAL_STATE_UPDATE, update=kwargs) self.additional_state_content.update(kwargs) diff --git a/cooperative_cuisine/game_server.py b/cooperative_cuisine/game_server.py index e565cbe91a1652af87d51cbc3b7de066e8903700..4d2fa765ee65156cfc28168febbd105a9d39ca65 100644 --- a/cooperative_cuisine/game_server.py +++ b/cooperative_cuisine/game_server.py @@ -132,11 +132,11 @@ class EnvironmentHandler: """A dictionary of player hashes and their respective data.""" self.manager_envs: dict[str, Set[str]] = defaultdict(set) """A dictionary of manager IDs and the environment IDs managed by each manager.""" - self.env_step_frequency = env_step_frequency + self.env_step_frequency: int = env_step_frequency """The frequency at which the environment steps.""" - self.preferred_sleep_time_ns = 1e9 / self.env_step_frequency + self.preferred_sleep_time_ns: float = 1e9 / self.env_step_frequency """The preferred sleep time between environment steps in nanoseconds based on the `env_step_frequency`.""" - self.client_ids_to_player_hashes = {} + self.client_ids_to_player_hashes: dict[str, str] = {} """A dictionary mapping client IDs to player hashes.""" self.allowed_manager: list[str] = [] """List of manager ids that are allowed to manage/create environments.""" @@ -567,9 +567,23 @@ class EnvironmentHandler: return False def extend_allowed_manager(self, manager: list[str]): + """ + Extends the list of allowed managers. + + Args: + manager: A list of strings representing the managers to be added to the allowed managers list. + + """ self.allowed_manager.extend(manager) - def set_host_and_port(self, host, port): + def set_host_and_port(self, host: str, port: int): + """Set the host and the port of the game server. + + Args: + host: The host value to set for the object. + port: The port value to set for the object. + + """ self.host = host self.port = port @@ -632,10 +646,13 @@ class PlayerConnectionManager: connection_manager = PlayerConnectionManager() +"""Manage the player connection for the game server.""" frequency = 200 +"""How often the step function is called for all environments in a second. (Hz).""" environment_handler: EnvironmentHandler = EnvironmentHandler( env_step_frequency=frequency ) +"""The environment handler for the game server. Manages the running environments (step calles, action passing, etc.).""" class PlayerRequestType(Enum): @@ -650,13 +667,20 @@ class PlayerRequestType(Enum): class WebsocketMessage(BaseModel): + """Type hint for a websocket response/request from the client.""" + type: str + """What kind/type of request is it? (get_state, action, ready)""" action: None | Action = None + """The data for an action (if type=="action").""" player_hash: str + """The provided player_hash from the game server for validation of the player.""" class Config: arbitrary_types_allowed = True + # maybe replace BaseModel with dataclass for better performance? + def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResult | str: """Manage WebSocket Message by validating the message and passing it to the environment. @@ -687,14 +711,6 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul "player_hash": None, } return state - case PlayerRequestType.READY: - accepted = environment_handler.set_player_ready(ws_message.player_hash) - return { - "request_type": request_type.value, - "msg": f"ready{' ' if accepted else ' not '}accepted", - "status": 200 if accepted else 400, - "player_hash": ws_message.player_hash, - } case PlayerRequestType.ACTION: assert ( ws_message.action is not None @@ -712,6 +728,14 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul "msg": f"action{' ' if accepted else ' not '}accepted", "player_hash": ws_message.player_hash, } + case PlayerRequestType.READY: + accepted = environment_handler.set_player_ready(ws_message.player_hash) + return { + "request_type": request_type.value, + "msg": f"ready{' ' if accepted else ' not '}accepted", + "status": 200 if accepted else 400, + "player_hash": ws_message.player_hash, + } return { "request_type": request_type.value, "status": 400, @@ -736,36 +760,60 @@ def manage_websocket_message(message: str, client_id: str) -> PlayerRequestResul @app.get("/") def read_root(): + """Simple response when calling the root of the url.""" return {"Cooperative": "Cuisine"} class CreateEnvironmentConfig(BaseModel): + """Datastructure for `create_env` post requests.""" + manager_id: str + """Check validity of the request by providing one of the allowed `manager_ids`""" number_players: int + """Number of players to generate player info for.""" same_websocket_player: list[list[str]] | None = None + """Future: allow players connect via one websocket.""" environment_settings: EnvironmentSettings + """Various other enviroment settings.""" item_info_config: str # file content + """The file content of the item info for the environment.""" environment_config: str # file content + """The file content of the environment_config.""" layout_config: str # file content + """The layout of the environment. Characters represent counter in grid. Rows split by newline character.""" seed: int + """The random seed for the environment. (Order generation, etc.)""" env_name: str + """The name of the environment / env_id.""" class ManageEnv(BaseModel): + """Datastructure for the post request to manage environments (stop_env).""" + manager_id: str + """For validation if the client is allowed to manage environments.""" env_id: str + """The reference to the environment to manage.""" reason: str + """The reason to manage the environment (why to stop it.).""" class AdditionalPlayer(BaseModel): + """Add a player to an environment / create player_info.""" + manager_id: str + """For validation if the client is allowed to manage environments.""" env_id: str + """The reference to the environment.""" number_players: int + """How many players to add.""" existing_websocket: str | None = None + """Future: allow players connect via one websocket that already exists.""" @app.post("/manage/create_env/") async def create_env(creation: CreateEnvironmentConfig) -> CreateEnvResult: + """Post request for creating an environment. See `CreateEnvironmentConfig` for the datastructure.""" result = environment_handler.create_env(creation) if result == 1: raise HTTPException(status_code=403, detail="Manager ID not known/registered.") @@ -774,12 +822,14 @@ async def create_env(creation: CreateEnvironmentConfig) -> CreateEnvResult: @app.post("/manage/additional_player/") async def additional_player(creation: AdditionalPlayer) -> dict[str, PlayerInfo]: + """Post request for adding additional players (not mentioned in create_env). See `AdditionalPlayer`.""" result = environment_handler.add_player(creation) return result @app.post("/manage/stop_env/") async def stop_env(manage_env: ManageEnv) -> str: + """Post request for stop an environment. See `ManageEnv`.""" accept = environment_handler.stop_env( manage_env.manager_id, manage_env.env_id, manage_env.reason ) @@ -829,6 +879,7 @@ async def websocket_player_endpoint(websocket: WebSocket, client_id: str): def main( host: str, port: int, manager_ids: list[str], enable_websocket_logging: bool = False ): + """Start a game server.""" print("Manager IDs:", manager_ids) setup_logging(enable_websocket_logging) loop = asyncio.new_event_loop() diff --git a/cooperative_cuisine/hooks.py b/cooperative_cuisine/hooks.py index ea6d6c59d46637d3ed2795793af63e4a3308c0bd..6b2dc39821a2ed08eabf630b744e7c1eae4c1470 100644 --- a/cooperative_cuisine/hooks.py +++ b/cooperative_cuisine/hooks.py @@ -537,8 +537,15 @@ class Hooks: """ def __init__(self, env: Environment): - self.hooks = defaultdict(list) - self.env = env + """Constructor for the Hooks object. + + Args: + env (Environment): The environment object to be referenced. + """ + self.hooks: dict[str, list[Callable]] = defaultdict(list) + """The hook callbacks per hook_ref.""" + self.env: Environment = env + """Reference to the environment object.""" def __call__(self, hook_ref, **kwargs): for callback in self.hooks[hook_ref]: diff --git a/cooperative_cuisine/orders.py b/cooperative_cuisine/orders.py index 9a98314a352324fb787b32e1a8121d6aeb11d193..1afe120e322259839545b8d3e1a65866f48e8d3a 100644 --- a/cooperative_cuisine/orders.py +++ b/cooperative_cuisine/orders.py @@ -121,7 +121,7 @@ class OrderGeneration: """Available meals restricted through the `environment_config.yml`.""" self.hook: Hooks = hook """Reference to the hook manager.""" - self.random = random + self.random: Random = random """Random instance.""" @abstractmethod @@ -156,7 +156,7 @@ class OrderManager: hook: An instance of the Hooks class. random: An instance of the Random class. """ - self.random = random + self.random: Random = random """Random instance.""" self.order_gen: OrderGeneration = order_config["order_gen_class"]( hook=hook, @@ -169,7 +169,7 @@ class OrderManager: ] = order_config["serving_not_ordered_meals"] """Function that decides if not ordered meals can be served and what score it gives""" - self.available_meals = None + self.available_meals: dict[str, ItemInfo] | None = None """The meals for that orders can be sampled from.""" self.open_orders: Deque[Order] = deque() """Current open orders. This attribute is used for the environment state.""" @@ -186,11 +186,11 @@ class OrderManager: self.hook: Hooks = hook """Reference to the hook manager.""" - self.score_callbacks = [] + self.score_callbacks: list[ScoreViaHooks] = [] """List of score callbacks.""" self.find_score_hook_callbacks() - def set_available_meals(self, available_meals): + def set_available_meals(self, available_meals: dict[str, ItemInfo]): """Set the available meals from which orders can be generated. Args: @@ -545,6 +545,13 @@ class RandomOrderGeneration(OrderGeneration): return orders def create_random_next_time_delta(self, now: datetime): + """ + Creates a random time delta for the next order based on order_duration_random_func in kwargs. + + Args: + now (datetime): The current datetime. + + """ if isinstance(self.kwargs.order_duration_random_func["func"], str): seconds = getattr( self.random, self.kwargs.sample_on_dur_random_func["func"] diff --git a/cooperative_cuisine/pygame_2d_vis/__init__.py b/cooperative_cuisine/pygame_2d_vis/__init__.py index c6af81b3aade3d2245e5e9e7da26abc028f5eeb2..ed6999e471c45593c051232617d9e2202481acd0 100644 --- a/cooperative_cuisine/pygame_2d_vis/__init__.py +++ b/cooperative_cuisine/pygame_2d_vis/__init__.py @@ -22,4 +22,5 @@ The keys for the control of the players are: - Interact: `O` - Swap Players (if configured): `P` +There is also a video game controller support. Just connect it to your PC (USB?) and it should work. """ diff --git a/cooperative_cuisine/scores.py b/cooperative_cuisine/scores.py index eb8529381456cbd4b5d438964d6425d713ef2c35..4e084d4985e693c87b0ba2649e778523cfa1cfd9 100644 --- a/cooperative_cuisine/scores.py +++ b/cooperative_cuisine/scores.py @@ -81,7 +81,7 @@ hook_callbacks: """ from __future__ import annotations -from typing import Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING, Callable import numpy as np @@ -176,7 +176,7 @@ class ScoreViaHooks(HookCallbackClass): static_score: float = 0, score_map: dict[str, float] = None, score_on_specific_kwarg: str = None, - time_dependence_func: callable = constant_score, + time_dependence_func: Callable = constant_score, time_dependence_kwargs: dict[str, Any] = None, kwarg_filter: dict[str, Any] = None, **kwargs, @@ -201,7 +201,7 @@ class ScoreViaHooks(HookCallbackClass): """Filtering condition for keyword arguments.""" self.score_on_specific_kwarg: str = score_on_specific_kwarg """The specific keyword argument to score on.""" - self.time_dependence_func: callable = time_dependence_func + self.time_dependence_func: Callable = time_dependence_func """The function to calculate the score based on time.""" self.time_dependence_kwargs: dict[str, Any] = ( time_dependence_kwargs if time_dependence_kwargs else {} diff --git a/cooperative_cuisine/study_server.py b/cooperative_cuisine/study_server.py index fbce4f2ecfa828b22110a6774336ca7bbdd4be7d..90e0534563e0d325ec91e004201cc05a3f98ef9d 100644 --- a/cooperative_cuisine/study_server.py +++ b/cooperative_cuisine/study_server.py @@ -143,14 +143,17 @@ class Study: @property def study_done(self) -> bool: + """Is the study done.""" return self.current_level_idx >= len(self.levels) @property def last_level(self) -> bool: + """Is it the last level that is played?""" return self.current_level_idx >= len(self.levels) - 1 @property def is_full(self) -> bool: + """Can the study start? No new players can join.""" return ( len(self.participant_id_to_player_info) == self.study_config["num_players"] ) @@ -299,6 +302,7 @@ class Study: Args: participant_id: The participant id which requests the connections. + participant_host: ip-address of the participant. Returns: The player info for the game server connections, level name and information if the level is the last @@ -523,6 +527,7 @@ class StudyManager: the fastapi requests act on top level of the python script. Args: + use_ssl: A boolean indicating whether to use SSL for the game server URL. game_host: The game server host address. game_port: The game server port. """ @@ -531,6 +536,11 @@ class StudyManager: self.create_game_server_url(use_ssl) def create_game_server_url(self, use_ssl: bool): + """Update `game_server_url` attribute. + Args: + use_ssl: A boolean indicating whether to use SSL for the game server URL. + + """ self.game_server_url = ( f"http{'s' if use_ssl else ''}://{self.game_host}:{self.game_port}" ) @@ -554,7 +564,14 @@ class StudyManager: # TODO validate study_config? self.study_config_path = study_config_path - def start_tutorial(self, participant_id: str): + @staticmethod + def start_tutorial(participant_id: str): + """Start the tutorial by instructing a game server to create a tutorial env. + + Args: + participant_id: The ID of the participant who wants to start the tutorial. + + """ environment_config_path = ROOT_DIR / "configs" / "tutorial_env_config.yaml" layout_path = ROOT_DIR / "configs" / "layouts" / "tutorial.layout" item_info_path = ROOT_DIR / "configs" / "item_info.yaml" @@ -596,7 +613,24 @@ class StudyManager: detail=f"Game server crashed: {env_info.json()['detail']}", ) - def end_tutorial(self, participant_id: str): + @staticmethod + def end_tutorial(participant_id: str): + """End a tutorial by doing clean up on the game server. + + This static method is used to end a tutorial for a specified participant. + It retrieves the environment information from the `running_tutorials` dictionary using the participant ID. + It then sends a request to the game server to stop the environment associated with the tutorial, + providing the manager ID, environment ID, and reason for ending the tutorial. + If the request is successful (status code 200), the tutorial is removed from the `running_tutorials` dictionary. + If the request fails, an HTTPException with status code 503 is raised. + + Args: + participant_id (str): The ID of the participant whose tutorial is ending. + + Raises: + HTTPException: If there is an error disconnecting from the tutorial. + + """ env = study_manager.running_tutorials[participant_id] answer = request_game_server( f"{study_manager.game_server_url}/manage/stop_env/", @@ -614,6 +648,7 @@ class StudyManager: study_manager = StudyManager() +"""Study manager instance for the study server.""" @app.post("/start_study/{participant_id}/{number_players}") @@ -649,6 +684,7 @@ async def get_game_connection( Args: participant_id: ID of the requesting participant. + request: Info about the post request, e.g, ip address. Returns: A dict containing the game server connection information and information about the current level. @@ -694,6 +730,7 @@ def main( study_config_path, use_ssl, ): + """Start a study server.""" study_manager.set_game_server_url( game_host=game_host, game_port=game_port, use_ssl=use_ssl ) @@ -701,7 +738,7 @@ def main( study_manager.set_study_config(study_config_path=study_config_path) print( - f"Use {study_manager.server_manager_id=} for game_server_url=http://{game_host}:{game_port}" + f"Use {study_manager.server_manager_id=} for game_server_url=http{'s' if use_ssl else ''}://{game_host}:{game_port}" ) loop = asyncio.new_event_loop() config = uvicorn.Config(app, host=study_host, port=study_port, loop=loop) diff --git a/cooperative_cuisine/utils.py b/cooperative_cuisine/utils.py index 601a1f3dc946b7901665e0d74f16cfc63a6df1df..ae8b3384eb3f6eace76f7b1120f0fd3f6218f04b 100644 --- a/cooperative_cuisine/utils.py +++ b/cooperative_cuisine/utils.py @@ -331,6 +331,13 @@ def create_layout_with_counters(w, h) -> str: def deep_update(d, u): + """Deep update of a nested dictionary. + + Args: + d: A dictionary to be updated. This dictionary will be modified in place. + u: A dictionary containing the updates to be applied to d. + + """ for k, v in u.items(): if isinstance(v, collections.abc.Mapping): d[k] = deep_update(d.get(k, {}), v)