diff --git a/cooperative_cuisine/environment.py b/cooperative_cuisine/environment.py index 144d96c044903dc492d26f2969d12f2f09711192..186396d96ee6ca5652f70141eacd9cc04d82c32b 100644 --- a/cooperative_cuisine/environment.py +++ b/cooperative_cuisine/environment.py @@ -517,7 +517,7 @@ class Environment: state = self.get_state(player_id, additional_key_values) json_data = json.dumps(state) self.hook(JSON_STATE, json_data=json_data, player_id=player_id) - # assert additional_key_values is None or StateRepresentation.model_validate_json(json_data=json_data) + # assert StateRepresentation.model_validate_json(json_data=json_data) return json_data def reset_env_time(self): diff --git a/cooperative_cuisine/items.py b/cooperative_cuisine/items.py index 1de65506e50816d7d6ef17369a1c6e0ced65c1f3..1b563a152f9d1cf04e09dbffb9a9ecd0acc0d703 100644 --- a/cooperative_cuisine/items.py +++ b/cooperative_cuisine/items.py @@ -15,6 +15,8 @@ The following classes are used for the base for all game items: The `ItemInfo` is the dataclass for the items in the `item_info.yml`. +Additionally, the `Effect` class is defined for the `Fire` effect. + ## Code Documentation """ @@ -26,7 +28,13 @@ import datetime import logging import uuid from enum import Enum -from typing import Optional, TypedDict, TYPE_CHECKING +from typing import Optional, TypedDict, TYPE_CHECKING, Literal, Set + +from cooperative_cuisine.state_representation import ( + ItemState, + CookingEquipmentState, + EffectState, +) if TYPE_CHECKING: from cooperative_cuisine.effects import EffectManager @@ -136,7 +144,7 @@ class ActiveTransitionTypedDict(TypedDict): class Item: """Base class for game items which can be held by a player.""" - item_category = ITEM_CATEGORY + item_category: Literal["Item"] | Literal["ItemCookingEquipment"] = ITEM_CATEGORY """Class dependent category (is changed for the `CookingEquipment` class). """ def __init__( @@ -157,6 +165,11 @@ class Item: self.active_effects: list[Effect] = [] """The effects that affect the item.""" + @property + def extra_repr(self) -> str | Literal[""]: + """Additional string to add to the representation of the item (in __repr__).""" + return "" + def __repr__(self): if self.progress_equipment is None: return f"{self.name}({self.extra_repr})" @@ -166,11 +179,6 @@ class Item: def __eq__(self, other): return other and self.name == other.name - @property - def extra_repr(self): - """Additional string to add to the representation of the item (in __repr__).""" - 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 @@ -198,7 +206,7 @@ class Item: self.progress_percentage = 0.0 self.inverse_progress = False - def to_dict(self) -> dict: + def to_dict(self) -> CookingEquipmentState | ItemState: """For the state representation. Only the relevant attributes are put into the dict.""" return { "id": self.uuid, @@ -236,6 +244,10 @@ class CookingEquipment(Item): for transition in self.transitions.values(): transition.recipe = collections.Counter(transition.needs) + @property + def extra_repr(self) -> str: + return f"{self.content_list}, {self.content_ready}" + def can_combine(self, other) -> bool: # already cooking or nothing to combine if other is None or ( @@ -299,6 +311,11 @@ class CookingEquipment(Item): # todo set active transition for fire/burnt? def check_active_transition(self): + """Check if a new active transition is possible, if so, set a new active transition. + + Checks if there is an active transition based on the current state of the content list and transitions dictionary. + If an active transition is found, it updates the necessary attributes accordingly. + """ ingredients = collections.Counter(item.name for item in self.content_list) for result, transition in self.transitions.items(): if transition.type == ItemType.Effect: @@ -337,17 +354,13 @@ class CookingEquipment(Item): self.content_list = [] self.content_ready = None - def release(self): + def release(self) -> list[Item]: """Release the content when the player "picks up" the equipment with a plate in the hands""" content = self.content_list self.reset_content() self.reset() return content - @property - def extra_repr(self): - return f"{self.content_list}, {self.content_ready}" - def reset(self): super().reset() self.active_transition = None @@ -360,18 +373,15 @@ class CookingEquipment(Item): return self.content_list[0] return None - def to_dict(self) -> dict: + def to_dict(self) -> CookingEquipmentState: d = super().to_dict() d.update( - ( - ("content_list", [c.to_dict() for c in self.content_list]), - ( - "content_ready", - self.content_ready.to_dict() - if self.content_ready is not None - else None, - ), - ) + { + "content_list": [c.to_dict() for c in self.content_list], + "content_ready": self.content_ready.to_dict() + if self.content_ready is not None + else None, + } ) return d @@ -382,8 +392,8 @@ class Plate(CookingEquipment): 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""" + self.meals: Set[str] = set(transitions.keys()) + """All meals (meal names) can be hold by a clean plate,""" super().__init__( name=self.create_name(), transitions={k: v for k, v in transitions.items() if not v.equipment}, @@ -394,12 +404,12 @@ class Plate(CookingEquipment): def progress(self, equipment: str, percent: float): Item.progress(self, equipment, percent) - def create_name(self): + def create_name(self) -> str: """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): + def can_combine(self, other) -> bool: if not super().can_combine(other): if ( isinstance(other, CookingEquipment) @@ -418,13 +428,19 @@ class Plate(CookingEquipment): # this is here, due to a circular import if it would be in the effects.py class Effect: + """Effects on counters and items. Like Fire.""" + def __init__(self, name: str, item_info: ItemInfo, uid: str = None): self.uuid: str = uuid.uuid4().hex if uid is None else uid - self.name = name - self.item_info = item_info - self.progres_percentage = 0.0 + """ID of the effect instance.""" + self.name: str = name + """Name of the effect""" + self.item_info: ItemInfo = item_info + """Item info of the effect, effect manager, effect type, etc.""" + self.progres_percentage: float = 0.0 + """For fire: how much the player still has to extinguish.""" - def to_dict(self) -> dict: + def to_dict(self) -> EffectState: return { "id": self.uuid, "type": self.name, diff --git a/cooperative_cuisine/orders.py b/cooperative_cuisine/orders.py index 39c076ec38875ffb34b142c4a580d6003976c84e..416d78db399701892742e37221b17565cf2bee8c 100644 --- a/cooperative_cuisine/orders.py +++ b/cooperative_cuisine/orders.py @@ -21,7 +21,7 @@ class referenced. This file defines the following classes: - `Order` - `OrderGeneration` -- `OrderAndScoreManager` +- `OrderManager` Further, it defines same implementations for the basic order generation based on random sampling: - `RandomOrderGeneration` @@ -60,9 +60,6 @@ from cooperative_cuisine.state_representation import OrderState 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.""" - class OrderConfig(TypedDict): """The configuration of the order in the `environment_config`under the `order` key.""" @@ -283,13 +280,13 @@ class OrderManager: def order_state(self) -> list[OrderState]: """Similar to the `to_dict` in `Item` and `Counter`. Relevant for the state of the environment""" return [ - OrderState( - id=order.uuid, - category=ORDER_CATEGORY, - meal=order.meal.name, - start_time=order.start_time.isoformat(), - max_duration=order.max_duration.total_seconds(), - ) + { + "id": order.uuid, + "category": "Order", + "meal": order.meal.name, + "start_time": order.start_time.isoformat(), + "max_duration": order.max_duration.total_seconds(), + } for order in self.open_orders ] diff --git a/cooperative_cuisine/server_results.py b/cooperative_cuisine/server_results.py index 7b482242ef3758106a6a5070e5064b46d3a7f021..1134c8fc4c08a60426ac8857855385655be46ad6 100644 --- a/cooperative_cuisine/server_results.py +++ b/cooperative_cuisine/server_results.py @@ -13,9 +13,9 @@ class PlayerInfo(TypedDict): """Information about a player in an environment.""" client_id: str - """ID of the client that controls the player.""" + """ID of the client that controls the player. Used for the websocket url.""" player_hash: str - """Hash of the player.""" + """Hash of the player, for validation.""" player_id: str """ID of the player.""" diff --git a/cooperative_cuisine/state_representation.py b/cooperative_cuisine/state_representation.py index 5bc53ad418ee24ada3e4bfacbdb9098622d003b3..25c022fdabff52ade8c2b438a15392a02a65ec43 100644 --- a/cooperative_cuisine/state_representation.py +++ b/cooperative_cuisine/state_representation.py @@ -14,68 +14,70 @@ class OrderState(TypedDict): """Format of the state representation of an order.""" id: str - """ID of the order.""" + """UUID of the order.""" category: Literal["Order"] - """Category of the order.""" + """Object category of env output, should be "Order".""" meal: str """Name of the ordered meal.""" - start_time: datetime # isoformat str + start_time: datetime | str # isoformat str """Time of the creation of the order.""" max_duration: float - """Maximum duration of the order.""" + """Maximum duration of the order until it should be served.""" class EffectState(TypedDict): - """Format of the state representation of an effect.""" + """Format of the state representation of an effect (fire).""" id: str - """ID of th effect.""" + """UUID of th effect.""" type: str """Type of the effect.""" progress_percentage: float | int """Progressed percentage of the effect.""" inverse_progress: bool - """Progress in reverse.""" + """Display the "inverse" of the percentage: 1 - progress_percentage.""" class ItemState(TypedDict): """Format of the state representation of an item.""" id: str - """ID of the item.""" + """UUID of the item.""" category: Literal["Item"] | Literal["ItemCookingEquipment"] - """Category of the item.""" + """Object category of env output, should be "Item" or "ItemCookingEquipment".""" type: str - """Type of the item.""" + """Type of the item, "Tomato", ...""" progress_percentage: float | int """Progressed percentage of the item.""" inverse_progress: bool - """Progress in reverse.""" + """Display the "inverse" of the percentage: 1 - progress_percentage.""" active_effects: list[EffectState] - """Active effects of the item.""" + """Active effects of the item, e.g., fire.""" # add ItemType Meal ? -class CookingEquipmentState(TypedDict): +class CookingEquipmentState(ItemState): """Format of the state representation of cooking equipment.""" content_list: list[ItemState] - """List of contents of the cooking equipment.""" + """Items in/on the the cooking equipment.""" content_ready: None | ItemState - """Is the content ready to be released?""" + """Utility attribute, to show meals that would be ready to be served. But could also be transformed to other + meals. (Therefore, keeping the `content_list`.) If not None, you can display the `content_ready` meal instead of + the `content_list`""" class CounterState(TypedDict): """Format of the state representation of a counter.""" id: str - """ID of the counter.""" + """UUID of the counter.""" category: Literal["Counter"] - """Category of the counter.""" + """Object category of env output, should be "Counter".""" type: str """Type of the counter.""" pos: list[float] - """Position of the counter.""" + """2D Position of the counter.""" orientation: list[float] """Orientation / facing direction of the counter, currently only visual.""" occupied_by: None | list[ @@ -93,13 +95,13 @@ class PlayerState(TypedDict): id: str """ID of the player.""" pos: list[float] - """Position of the player.""" + """2D Position of the player.""" facing_direction: list[float] """The direction the player is facing.""" holding: ItemState | CookingEquipmentState | None """What the player is currently holding.""" current_nearest_counter_pos: list[float] | None - """Position of the currently nearest counter to the player.""" + """2D Position of the currently nearest counter to the player.""" current_nearest_counter_id: str | None """ID of the currently nearest counter to the player.""" @@ -115,12 +117,12 @@ class KitchenInfo(BaseModel): class ViewRestriction(BaseModel): """Format of the state representation of a view restriction from the players perspectives. - Currently as a view cone, like a flashlight in the dark.""" + Currently, as a view cone, like a flashlight in the dark.""" direction: list[float] - """Direction of what the player can see.""" + """2D Direction of what the player can see.""" position: list[float] - """Starting position of the view cone.""" + """2D Starting position of the view cone.""" angle: int # degrees """Angle of the view cone.""" counter_mask: None | list[bool] @@ -133,8 +135,11 @@ class InfoMsgLevel(Enum): """Level of importance of the messages displayed to the players.""" Normal = "Normal" + """Standard text, maybe black font color.""" Warning = "Warning" + """Warning text, should display emergency. Maybe red font color.""" Success = "Success" + """Something was good. Maybe green font color.""" class InfoMsg(TypedDict): @@ -158,17 +163,18 @@ class StateRepresentation(BaseModel): counters: list[CounterState] """Information about the counters in the environment.""" kitchen: KitchenInfo - """General information about the kitchen.""" + """General information about the kitchen: Width, Height.""" score: float | int - """The score achieved by the players.""" + """The total score achieved by the players.""" orders: list[OrderState] """Current orders in the environment.""" ended: bool """If the environment has ended.""" env_time: datetime # isoformat str - """Current time of the environment, relative to the start time of the env.""" + """Current time of the environment. Each environment start time is 2000-01-01T00:00:00. See + `cooperative_cuisine.utils.create_init_env_time`.""" remaining_time: float - """Remaining time for the players to act in the environment.""" + """Remaining seconds for the players to act in the environment.""" view_restrictions: None | list[ViewRestriction] """Restriction of the view for the players.""" served_meals: list[tuple[str, str]] @@ -176,7 +182,8 @@ class StateRepresentation(BaseModel): info_msg: list[tuple[str, str]] """Info messages for the players to be displayed.""" # is added: - # all_players_ready: bool + all_players_ready: bool + """Added by the game server, indicate if all players are ready and actions are passed to the environment.""" def create_json_schema() -> dict[str, Any]: