-
Florian Schröder authored
The changes made are focused on improving the readability of code comments and clarifying the purpose of sections within the 'overcooked_simulator'. Formatting changes were made to separate headers for better readability and the meaning of certain terms, like 'extra_repr' in 'game_items.py', was clarified further.
Florian Schröder authoredThe changes made are focused on improving the readability of code comments and clarifying the purpose of sections within the 'overcooked_simulator'. Formatting changes were made to separate headers for better readability and the meaning of certain terms, like 'extra_repr' in 'game_items.py', was clarified further.
game_items.py 12.16 KiB
"""
The game items that a player can hold.
They have methods that
- check if items can be combined (`Item.can_combine`): cooking equipment and ingredients, and so on,
- combine the items after a successful check (`Item.combine`),
- and a method to call the progress on the items (`Item.progress`)
All game items need to be specified in the `item_info.yml`.
The following classes are used for the base for all game items:
- `Item`: ingredients and meals.
- `CookingEquipment`: pots, pans, etc.
- `Plate`: clean and dirty plates.
The `ItemInfo` is the dataclass for the items in the `item_info.yml`.
## Code Documentation
"""
from __future__ import annotations
import collections
import dataclasses
import datetime
import logging
import uuid
from enum import Enum
from typing import Optional, TypedDict
log = logging.getLogger(__name__)
"""The logger for this module."""
ITEM_CATEGORY = "Item"
"""The string for the `category` value in the json state representation for all normal items."""
COOKING_EQUIPMENT_ITEM_CATEGORY = "ItemCookingEquipment"
"""The string for the `category` value in the json state representation for all cooking equipments."""
class ItemType(Enum):
Ingredient = "Ingredient"
"""All ingredients and process ingredients."""
Meal = "Meal"
"""All combined ingredients that can be served."""
Equipment = "Equipment"
"""All counters and cooking equipments."""
@dataclasses.dataclass
class ItemInfo:
"""Wrapper for the info in the `item_info.yml`.
Example:
A simple example for the tomato soup with 6 game items.
```yaml
CuttingBoard:
type: Equipment
Stove:
type: Equipment
Pot:
type: Equipment
equipment: Stove
Tomato:
type: Ingredient
ChoppedTomato:
type: Ingredient
needs: [ Tomato ]
seconds: 4.0
equipment: CuttingBoard
TomatoSoup:
type: Meal
needs: [ ChoppedTomato, ChoppedTomato, ChoppedTomato ]
seconds: 6.0
equipment: Pot
```
"""
type: ItemType = dataclasses.field(compare=False)
"""Type of the item. Either `Ingredient`, `Meal` or `Equipment`."""
name: str = dataclasses.field(compare=True)
"""The name of the item, is set automatically by the "group" name of the item."""
seconds: float = dataclasses.field(compare=False, default=0)
"""If progress is needed this argument defines how long it takes to complete the process in seconds."""
needs: list[str] = dataclasses.field(compare=False, default_factory=list)
"""The ingredients/items which are needed to create the item/start the progress."""
equipment: ItemInfo | None = dataclasses.field(compare=False, default=None)
"""On which the item can be created. `null`, `~` (None) converts to Plate."""
recipe: collections.Counter | None = None
"""Internally set in CookingEquipment"""
def __post_init__(self):
self.type = ItemType(self.type)
class ActiveTransitionTypedDict(TypedDict):
"""The values in the active transitions dicts of `CookingEquipment`."""
seconds: int | float
"""The needed seconds to progress for the transition."""
result: str
"""The new name of the item after the transition."""
class Item:
"""Base class for game items which can be held by a player."""
item_category = ITEM_CATEGORY
"""Class dependent category (is changed for the `CookingEquipment` class). """
def __init__(
self, name: str, item_info: ItemInfo, uid: str = None, *args, **kwargs
):
self.name: str = self.__class__.__name__ if name is None else name
"""The name of the item, e.g., `Tomato` or `ChoppedTomato`"""
self.item_info: ItemInfo = item_info
"""The information about the item from the `item_info.yml` config."""
self.progress_equipment: str | None = None
"""The equipment with which the item was last progressed."""
self.progress_percentage: float = 0.0
"""The current progress percentage of the item if it is progress-able."""
self.uuid: str = uuid.uuid4().hex if uid is None else uid
"""A unique identifier for the item. Useful for GUIs that handles specific asset instances."""
def __repr__(self):
if self.progress_equipment is None:
return f"{self.name}({self.extra_repr})"
else:
return f"{self.name}(progress={round(self.progress_percentage * 100, 2)}%,{self.extra_repr})"
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
def combine(self, other) -> Item | None:
"""Combine the item with another item based on possible transitions/needs."""
pass
def progress(self, equipment: str, percent: float):
"""Progresses the item process on the given equipment as long as it is not finished."""
if self.progress_equipment is None:
self.progress_equipment = equipment
if self.progress_equipment == equipment:
self.progress_percentage += percent
self.progress_percentage = min(self.progress_percentage, 1.0)
else:
log.warning(
f"{self.name} expected progress on {self.progress_equipment}, but got {percent * 100}% on {equipment}"
)
def reset(self):
"""Reset the progress."""
self.progress_equipment = None
self.progress_percentage = 0.0
def to_dict(self) -> dict:
"""For the state representation. Only the relevant attributes are put into the dict."""
return {
"id": self.uuid,
"category": self.item_category,
"type": self.name,
"progress_percentage": self.progress_percentage,
}
class CookingEquipment(Item):
"""Pot, Pan, ... that can hold items. It holds the progress of the content (e.g., the soup) in itself (
progress_percentage) and not in the items in the content list."""
item_category = COOKING_EQUIPMENT_ITEM_CATEGORY
def __init__(self, transitions: dict[str, ItemInfo], *args, **kwargs):
super().__init__(*args, **kwargs)
self.transitions: dict[str, ItemInfo] = transitions
"""What is needed to cook a meal / create another ingredient."""
self.active_transition: Optional[ActiveTransitionTypedDict] = None
"""The info how and when to convert the content_list to a new item."""
# TODO change content ready just to str (name of the item)?
self.content_ready: Item | None = None
"""Helper attribute that can have a ready meal which is also represented via it ingredients in the
content_list. But soups or other processed meals are not covered here. For a Burger or Salad, this attribute
is set."""
self.content_list: list[Item] = []
"""The items that the equipment holds."""
log.debug(f"Initialize {self.name}: {self.transitions}")
for transition in self.transitions.values():
transition.recipe = collections.Counter(transition.needs)
def can_combine(self, other) -> bool:
# already cooking or nothing to combine
if other is None:
return False
if isinstance(other, CookingEquipment):
other = other.content_list
else:
other = [other]
# other extends ingredients for meal
ingredients = collections.Counter(
item.name for item in self.content_list + other
)
return any(ingredients <= recipe.recipe for recipe in self.transitions.values())
def combine(self, other) -> Item | None:
return_value = None
if isinstance(other, CookingEquipment):
self.content_list.extend(other.content_list)
return_value = other
other.reset_content()
elif isinstance(other, list):
self.content_list.extend(other)
else:
self.content_list.append(other)
ingredients = collections.Counter(item.name for item in self.content_list)
for result, transition in self.transitions.items():
if ingredients == transition.recipe:
if transition.seconds == 0:
self.content_ready = Item(name=result, item_info=transition)
else:
self.active_transition = {
"seconds": transition.seconds,
"result": Item(name=result, item_info=transition),
}
break
else:
self.content_ready = None
return return_value
def can_progress(self) -> bool:
"""Check if the cooking equipment can progress items at all."""
return self.active_transition is not None
def progress(self, passed_time: datetime.timedelta, now: datetime.datetime):
percent = passed_time.total_seconds() / self.active_transition["seconds"]
super().progress(equipment=self.name, percent=percent)
if self.progress_percentage == 1.0:
self.content_list = [self.active_transition["result"]]
self.reset()
# todo set active transition for fire/burnt?
def reset_content(self):
"""Reset the content attributes after the content was picked up from the equipment."""
self.content_list = []
self.content_ready = None
def release(self):
"""Release the content when the player "picks up" the equipment with a plate in the hands"""
content = self.content_list
self.reset_content()
return content
@property
def extra_repr(self):
return f"{self.content_list}, {self.content_ready}"
def reset(self):
super().reset()
self.active_transition = None
def get_potential_meal(self) -> Item | None:
"""The meal that could be served depends on the attributes `content_ready` and `content_list`"""
if self.content_ready:
return self.content_ready
if len(self.content_list) == 1:
return self.content_list[0]
return None
def to_dict(self) -> dict:
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,
),
)
)
return d
class Plate(CookingEquipment):
"""The plate can have to states: clean and dirty. In the clean state it can hold content/other items."""
def __init__(self, transitions: dict[str, ItemInfo], clean: bool, *args, **kwargs):
self.clean: bool = clean
"""If the plate is clean or dirty."""
self.meals = set(transitions.keys())
"""All meals can be hold by a clean plate"""
super().__init__(
name=self.create_name(),
transitions={k: v for k, v in transitions.items() if not v.equipment},
*args,
**kwargs,
)
def progress(self, equipment: str, percent: float):
Item.progress(self, equipment, percent)
def create_name(self):
"""The name depends on the clean or dirty state of the plate. Clean plates are `Plate`, otherwise
`DirtyPlate`."""
return "Plate" if self.clean else "DirtyPlate"
def can_combine(self, other):
if not super().can_combine(other):
if (
isinstance(other, CookingEquipment)
and len(other.content_list) == 1
and not self.content_list
and self.clean
):
# additional check for meals in the content list of another equipment,
# e.g., Soups which is not covered by the normal transition checks.
return other.content_list[0].name in self.meals
return False
elif self.clean:
return True
return False