diff --git a/.gitignore b/.gitignore index 804c0d1..ad288ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .venv +.vscode *.pyc -**__pycache__/ \ No newline at end of file +**__pycache__/ diff --git a/components.py b/components.py new file mode 100644 index 0000000..de0ceb5 --- /dev/null +++ b/components.py @@ -0,0 +1,55 @@ +from dataclasses import dataclass, field +from enum import Enum, StrEnum + + +@dataclass +class Name: + value: str + + +@dataclass +class BaseStat: + value: int + + +@dataclass +class Health(BaseStat): + pass + + +@dataclass +class Satiety(BaseStat): + pass + + +@dataclass +class Hydration(BaseStat): + pass + + +@dataclass +class Hygiene(BaseStat): + pass + + +@dataclass +class Capacity(BaseStat): + pass + + +@dataclass +class Tags: + values: list = field(default_factory=list) + + +class LocationType(StrEnum): + NO_WHERE = "no_where" + PADDOCK = "paddock" + STORE = "store" + INVENTORY = "inventory" + + +@dataclass +class Location: + type: "LocationType" = LocationType.NO_WHERE + id: int | None = None diff --git a/ecs.py b/ecs.py new file mode 100644 index 0000000..6762fbb --- /dev/null +++ b/ecs.py @@ -0,0 +1,57 @@ +from dataclasses import dataclass, field +from typing import Any, Iterator, Protocol + +type Entity = int +type Archetype = frozenset[type] +type ComponentMap = dict[type, Any] +type EntityComponentStorage = dict[Entity, ComponentMap] + + +class System(Protocol): + @classmethod + def process(cls, world: "World") -> None: ... + + +@dataclass +class World: + registry: dict[Archetype, EntityComponentStorage] = field(default_factory=dict) + next_entity_index: int = 0 + systems: list[type[System]] = field(default_factory=list) + + def generate_id(self) -> int: + self.next_entity_index += 1 + return self.next_entity_index + + def spawn(self, *components) -> int: + archetype = frozenset([type(c) for c in components]) + + if archetype not in self.registry: + self.registry[archetype] = {} + + entity = self.generate_id() + self.registry[archetype][entity] = {type(c): c for c in components} + return entity + + def query( + self, *component_types, exact=False + ) -> Iterator[tuple[Entity, ComponentMap]]: + component_subset_signature = frozenset(component_types) + for archetype, entity_component_storage in self.registry.items(): + if (exact and component_subset_signature == archetype) or ( + not exact and component_subset_signature.issubset(archetype) + ): + for entity, component_storage in entity_component_storage.items(): + yield entity, component_storage + + def get_components(self, entity: int, pop=False): + for componentstorage in self.registry.values(): + if entity in componentstorage: + if pop: + return componentstorage.pop(entity, None) + else: + return componentstorage[entity] + return None + + def tick(self): + for system in self.systems: + system.process(self) diff --git a/entities.py b/entities.py deleted file mode 100644 index 7666afc..0000000 --- a/entities.py +++ /dev/null @@ -1,14 +0,0 @@ -from dataclasses import dataclass, field - -ENTITY_INDEX = 0 - - -def generate_id(): - global ENTITY_INDEX - ENTITY_INDEX += 1 - return ENTITY_INDEX - - -@dataclass -class BaseEntity: - id: int = field(default_factory=generate_id) diff --git a/imp.py b/imp.py new file mode 100644 index 0000000..e03e2c5 --- /dev/null +++ b/imp.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass, field + +from components import ( + Health, + Satiety, + Location, + LocationType, + Name, + Hydration, + Hygiene, + Tags, +) +from ecs import World + + +def create_pet(world: World, name: str): + return world.spawn( + Name(name), + Health(100), + Satiety(80), + Hydration(80), + Hygiene(80), + Tags(), + ) + + +def move_pet_to(world: World, pet_id, container_entity=None) -> bool: + component_map = world.get_components(pet_id, pop=True) + if component_map is None: + return False + component_map[Location].id = container_entity + component_map[Location].type = LocationType.PADDOCK + return True diff --git a/systems.py b/systems.py new file mode 100644 index 0000000..25fa783 --- /dev/null +++ b/systems.py @@ -0,0 +1,41 @@ +from components import Health, Hydration, Hygiene, Satiety +from ecs import World + + +class SatietySystem: + @classmethod + def process(cls, world: World): + for _, component_map in world.query(Satiety): + health: Health = component_map[Health] + hunger: Satiety = component_map[Satiety] + if hunger.value <= 0: + health.value -= 1 + hunger.value = 0 + else: + hunger.value -= 1 + + +class HydrationSystem: + @classmethod + def process(cls, world: World): + for _, component_map in world.query(Hydration): + health: Health = component_map[Health] + hydration: Hydration = component_map[Hydration] + if hydration.value <= 0: + health.value -= 1 + hydration.value = 0 + else: + hydration.value -= 1 + + +class HygieneSystem: + @classmethod + def process(cls, world: World): + for _, component_map in world.query(Hygiene): + health: Health = component_map[Health] + hygiene: Hygiene = component_map[Hygiene] + if hygiene.value <= 0: + health.value -= 1 + hygiene.value = 0 + else: + hygiene.value -= 1 diff --git a/test.py b/test.py index fbb6674..041cde2 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,26 @@ -from entities import BaseEntity +from imp import World, create_pet +from systems import SatietySystem, HydrationSystem, HygieneSystem -object = BaseEntity() + +def test_systems_tick(): + world = World() + world.systems.extend( + ( + HydrationSystem, + SatietySystem, + HygieneSystem, + ) + ) + + entity = create_pet(world, "karabas") + + for _ in range(81): + world.tick() + + components = world.get_components(entity) + + print(components) + + +if __name__ == "__main__": + test_systems_tick()