From 5c100dd015be2973ba9e40a9700d641b99cea524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Schr=C3=B6der?= <fschroeder@techfak.uni-bielefeld.de> Date: Wed, 3 Apr 2024 17:12:35 +0200 Subject: [PATCH] Add type hints and docstrings across multiple modules Added type hints and docstrings throughout the project, notably in the effects, orders, study server, and game server modules. These additions provide better understanding and ease in navigating through the codebase. Minor code reorganization and syntax optimization were also performed. --- CHANGELOG.md | 6 ++ cooperative_cuisine/__init__.py | 23 +++--- cooperative_cuisine/argument_parser.py | 4 - cooperative_cuisine/counters.py | 20 ++++- cooperative_cuisine/effects.py | 8 +- cooperative_cuisine/environment.py | 17 +++-- cooperative_cuisine/game_server.py | 75 ++++++++++++++++--- cooperative_cuisine/hooks.py | 11 ++- cooperative_cuisine/orders.py | 17 +++-- cooperative_cuisine/pygame_2d_vis/__init__.py | 1 + cooperative_cuisine/scores.py | 6 +- cooperative_cuisine/study_server.py | 43 ++++++++++- cooperative_cuisine/utils.py | 7 ++ 13 files changed, 187 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01fe2e8f..817d2554 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 e3063275..ae22c482 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 f6bc86eb..d5c6fd46 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 eb39a753..1b031747 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 53989c09..5c7ff26e 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 90be3168..8f8cc1b6 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 e565cbe9..4d2fa765 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 ea6d6c59..6b2dc398 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 9a98314a..1afe120e 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 c6af81b3..ed6999e4 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 eb852938..4e084d49 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 fbce4f2e..90e05345 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 601a1f3d..ae8b3384 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) -- GitLab