Skip to content
Snippets Groups Projects
game_items.py 11.27 KiB
from __future__ import annotations

import collections
import dataclasses
import datetime
import logging
from enum import Enum
from typing import Optional

log = logging.getLogger(__name__)


class ItemType(Enum):
    Ingredient = "Ingredient"
    Meal = "Meal"
    Equipment = "Equipment"


@dataclasses.dataclass
class ItemInfo:
    type: ItemType = dataclasses.field(compare=False)
    name: str = dataclasses.field(compare=True)
    seconds: float = dataclasses.field(compare=False, default=0)
    needs: list[ItemInfo] = dataclasses.field(compare=False, default_factory=list)
    equipment: ItemInfo | None = dataclasses.field(compare=False, default=None)

    _start_meals: list[ItemInfo] = dataclasses.field(
        compare=False, default_factory=list
    )

    def __post_init__(self):
        self.type = ItemType(self.type)

    def add_start_meal_to_equipment(self, start_item: ItemInfo):
        self._start_meals.append(start_item)

    def sort_start_meals(self):
        self._start_meals.sort(key=lambda item_info: len(item_info.needs))

    # def can_start_meal(self, items: list[Item]):
    #     return items and self._return_start_meal(items) is not None

    # def start_meal(self, items: list[Item]) -> Item:
    #     return self._return_start_meal(items).create_item(parts=items)

    def _return_start_meal(self, items: list[Item]) -> ItemInfo | None:
        for meal in self._start_meals:
            satisfied = [False for _ in range(len(items))]
            for i, p in enumerate(items):
                for _, n in enumerate(meal.needs):
                    if not satisfied[i] and p.name == n:
                        satisfied[i] = True
                        break
            if all(satisfied):
                return meal


class Item:
    """Base class for game items which can be held by a player."""

    def __init__(self, name: str, item_info: ItemInfo, *args, **kwargs):
        self.name = self.__class__.__name__ if name is None else name
        self.item_info = item_info
        self.progress_equipment = None
        self.progress_percentage = 0.0

    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):
        return ""

    def can_combine(self, other):
        return False

    def combine(self, other) -> Item | None:
        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):
        self.progress_equipment = None
        self.progress_percentage = 0.0


class CookingEquipment(Item):
    def __init__(self, transitions: dict, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.transitions = transitions
        self.active_transition: Optional[dict] = None
        # self.content: Item | list[Item] = []  # list if cooking, meal item when done

        self.content_ready: Item | None = None
        self.content_list: list[Item] = []

        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):
        # 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 can_combine(self, other):
        if other is Non
        # TODO remove when content ready and content list is implemented
        if isinstance(self.content, Item):
            return False
e:
            return False
        if self.content is None:
            if isinstance(other, CookingEquipment):
                return other.can_release_content()
            # TODO check other is start of a meal, create meal
            if isinstance(other, Meal) and "Plate" in self.name:
                return not other.steps_needed or other.finished
            return self.item_info.can_start_meal([other])
        if self.content.can_combine(other):
            return True
        if isinstance(other, CookingEquipment) and other.content:
            other = other.content
        return self.item_info.can_start_meal(
            [other]
            + ([self.content] if self.content.progressed_steps else self.content.parts)
        )
    """

    def combine(self, other):
        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():
            recipe = transition["recipe"]
            if ingredients == recipe:
                if transition["seconds"] == 0:
                    self.content_ready = Item(name=result, item_info=transition["info"])
                else:
                    self.active_transition = {
                        "seconds": transition["seconds"],
                        "result": Item(name=result, item_info=transition["info"]),
                    }
                    print(f"{self.name} {self.active_transition}, {self.content_list}")
                break
        else:
            self.content_ready = None
        return return_value

    """
    def combine(self, other):
        if self.content is None:
            if isinstance(other, CookingEquipment):
                self.content = other.release()
                return other
            if isinstance(other, Meal) and "Plate" in self.name:
                self.content = other
                return
            # find starting meal for other
            self.content = self.item_info.start_meal([other])
            return
        if not self.content.can_combine(other):
            if isinstance(other, CookingEquipment) and other.content:
                content = other.release()
                self.content = self.item_info.start_meal(
                    [content]
                    + (
                        [self.content]
                        if self.content.progressed_steps
                        else self.content.parts
                    )
                )
                return other
            else:
                self.content = self.item_info.start_meal(
                    [other]
                    + (
                        [self.content]
                        if self.content.progressed_steps
                        else self.content.parts
                    )
                )
            return
        self.content.combine(other)
    """

    def can_progress(self) -> bool:
        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 can_release_content(self) -> bool:
    #     return (
    #         self.content
    #         and isinstance(self.content, ProgressibleItem)
    #         and self.content.finished
    #     )
    def reset_content(self):
        self.content_list = []
        self.content_ready = None

    def release(self):
        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


# class Meal(Item):
#     def __init__(self, parts=None, *args, **kwargs):
#         super().__init__(*args, **kwargs)
#         self.parts = [] if parts is None else parts
#         # self.rules ...
#
#     def can_combine(self, other) -> bool:
#         if other and not self.finished:
#             satisfied = [False for _ in range(len(self.parts))]
#             for n in self.item_info.needs:
#                 for i, p in enumerate(self.parts):
#                     if not satisfied[i] and p.name == n:
#                         satisfied[i] = True
#                         break
#                 else:
#                     if n == other.name:
#                         return True
#         return False
#
#     def combine(self, other):
#         self.parts.append(other)
#
#     def can_progress(self) -> bool:
#         return self.item_info.steps_needed and len(self.item_info.needs) == len(
#             self.parts
#         )
#
#     def finished_call(self):
#         super().finished_call()
#
#     @property
#     def extra_repr(self):
#         return self.parts


class Plate(CookingEquipment):
    def __init__(self, transitions, clean, content=None, *args, **kwargs):
        self.clean = clean
        super().__init__(
            name=self.create_name(), transitions=transitions, *args, **kwargs
        )

    def finished_call(self):
        self.clean = True
        self.name = self.create_name()

    def progress(self, equipment: str, percent: float):
        Item.progress(self, equipment, percent)

    # def progress(self, equipment: str, percent: float):
    #     """Progresses the item process as long as it is not finished."""
    #     if self.progressed_steps >= self.steps_needed:
    #         self.finished = True
    #         self.finished_call()
    #     if not self.finished:
    #         self.progressed_steps += 1

    # def can_progress(self, counter_type="Sink") -> bool:
    #     return not self.clean

    def create_name(self):
        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:
                return other.content_list[0].name in self.transitions
            return False
        else:
            return True


"""
Dominik 
TODOs für yaml issue:


Fix cooking equipment drawing with list as content
Pot / Pan cooking
Meal combination
Plate cleaning
Environment factory for counter and cooking equipment?
Yaml check
Plot recipe graph
Clean unused code & imports

"""