Skip to content
Snippets Groups Projects
  • Florian Schröder's avatar
    c5bc8943
    Improve `overcooked_simulator` comments and readability · c5bc8943
    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.
    c5bc8943
    History
    Improve `overcooked_simulator` comments and readability
    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.
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