Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 1.3.0 #16

Merged
merged 9 commits into from
Mar 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[flake8]

ignore =
E203, # whitespace before :
E265, # block comment should start with #
F401, # module imported but unused
F403, # 'from module import *' unused; unable to detect undefined names
F405, # name may be undefined, or defined from star imports

max-line-length =
120
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Tests

on: [push]

jobs:
build:
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions pecs_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .engine import Engine
from .loader import Loader
from .events import EntityEvent
from .base_system import BaseSystem


__all__ = [
Expand All @@ -12,4 +13,5 @@
'Engine',
'Loader',
'EntityEvent',
'BaseSystem',
]
21 changes: 11 additions & 10 deletions pecs_framework/base_system.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations
from beartype.typing import *
from typing import TypeAlias
from abc import ABC, abstractmethod
from beartype.typing import TYPE_CHECKING

if TYPE_CHECKING:
from pecs_framework.query import Query, ComponentQuery
from pecs_framework.domain import Domain
from pecs_framework.engine import Engine
from pecs_framework.query import ComponentQuery
from pecs_framework.query import Query

from abc import ABC, abstractmethod


class Loop(ABC):
Expand All @@ -28,7 +29,7 @@ def teardown(self) -> None:
def pre_update(self) -> None:
raise NotImplementedError("Method has no implementation")

@abstractmethod
@abstractmethod
def update(self) -> None:
raise NotImplementedError("Method has no implementation")

Expand All @@ -45,18 +46,18 @@ def __init__(self, loop: Loop) -> None:
self.initialize()

def query(
self,
key: str,
self,
key: str,
all_of: ComponentQuery | None = None,
any_of: ComponentQuery | None = None,
any_of: ComponentQuery | None = None,
none_of: ComponentQuery | None = None,
) -> None:
all_of = all_of if all_of else []
any_of = any_of if any_of else []
none_of = none_of if none_of else []
self._queries[key] = self.loop.domain.create_query(
all_of,
any_of,
all_of,
any_of,
none_of,
)

Expand Down
17 changes: 9 additions & 8 deletions pecs_framework/component.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations
from beartype.typing import TypeVar
from beartype.typing import TypedDict
from beartype.typing import Any

from pecs_framework._types import Bases, Namespace
from pecs_framework.events import EntityEvent

import json
import sys
import traceback

Expand All @@ -14,19 +15,19 @@ class ComponentMeta(type):
comp_id: str
cbit: int
_entity_id: str

def __new__(
cls: type[ComponentMeta],
clsname: str,
bases: Bases,
cls: type[ComponentMeta],
clsname: str,
bases: Bases,
namespace: Namespace,
) -> ComponentMeta:
clsobj = super().__new__(cls, clsname, bases, namespace)
clsobj.comp_id = clsname.upper()
clsobj.cbit = 0
return clsobj


class Component(metaclass=ComponentMeta):
"""Root Component class that all components extend."""
_entity_id: str
Expand All @@ -49,7 +50,7 @@ def handle_event(self, evt: EntityEvent):

def on_event(self, evt: EntityEvent) -> EntityEvent | None:
pass


# Type variable ranging over Component instances
CT = TypeVar("CT", bound=Component)
148 changes: 129 additions & 19 deletions pecs_framework/domain.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
from __future__ import annotations
from beartype.typing import *
from beartype.typing import TYPE_CHECKING
from beartype.typing import Any
from beartype.typing import TypedDict

from uuid import uuid1
from collections import OrderedDict
import json

if TYPE_CHECKING:
from pecs_framework.component import Component
from pecs_framework.engine import Engine
from pecs_framework.query import ComponentQuery
from pecs_framework.prefab import EntityTemplate
from pecs_framework.query import ComponentQuery

from pecs_framework.entities import Entity
from pathlib import Path
from pecs_framework.entity import Entity
from pecs_framework.query import Query
from rich.console import Console
from rich import inspect


console = Console()

class ComponentDict(TypedDict):
comp_id: str
cbit: int
data: dict[str, Any]


class EntityDict(TypedDict):
alias: str | None
components: list[ComponentDict]


class EntityRegistry:

def __init__(self, domain: Domain) -> None:
self.domain = domain
self.aliases = OrderedDict()
self.alias_to_eid = OrderedDict()
self.eid_to_alias = OrderedDict()
self._map: dict[str, Entity] = OrderedDict()

def __getitem__(self, key: str) -> Entity:
Expand All @@ -31,12 +46,15 @@ def __getitem__(self, key: str) -> Entity:
def __setitem__(self, key: str, entity: Entity) -> None:
self._map[key] = entity

def __iter__(self):
return iter(self._map.values())

def keys(self):
return self._map.keys()

def values(self):
return self._map.values()

def get_entity_id(self, entity_or_alias: Entity | str) -> str:
"""
Get an Entity's identifier either by passing the entity itself or its
Expand All @@ -52,12 +70,17 @@ def get_entity_id(self, entity_or_alias: Entity | str) -> str:
The Entity's unique identifier
"""
if isinstance(entity_or_alias, str):
id_: str = self.aliases.get(entity_or_alias, '')
id_: str = self.alias_to_eid.get(entity_or_alias, '')
else:
id_: str = entity_or_alias.eid
return id_

def create(self, alias: str | None = None) -> Entity:

def create(
self,
alias: str | None = None,
*,
entity_id: str | None = None,
) -> Entity:
"""
Create a new Entity in the engine's Entity registry.

Expand All @@ -70,13 +93,17 @@ def create(self, alias: str | None = None) -> Entity:
-------
The newly created Entity instance
"""
entity = Entity(self.domain, self.domain.create_uid())
entity = Entity(
self.domain,
entity_id if entity_id else self.domain.create_uid(),
)
self._map[entity.eid] = entity

if alias:
if alias in self.aliases.keys():
if alias in self.alias_to_eid:
raise KeyError(f"Entity already exists with alias {alias}")
self.aliases[alias] = entity.eid
self.alias_to_eid[alias] = entity.eid
self.eid_to_alias[entity.eid] = alias

return entity

Expand Down Expand Up @@ -108,7 +135,7 @@ def create_from_prefab(
# props from the process above.
for name, props in comp_props.items():
self.domain.engine.components.attach(entity, name, props)

return entity

def get_by_alias(self, alias: str) -> Entity:
Expand All @@ -124,7 +151,7 @@ def get_by_alias(self, alias: str) -> Entity:
-------
The Entity corresponding to the passed alias.
"""
entity_id: str = self.aliases.get(alias, '')
entity_id: str = self.alias_to_eid.get(alias, '')
if entity_id:
return self.get_by_id(entity_id)
else:
Expand All @@ -134,25 +161,35 @@ def get_by_alias(self, alias: str) -> Entity:
def get_by_id(self, entity_id: str) -> Entity:
return self._map[entity_id]

def get_alias_for_entity(self, entity_or_eid: Entity | str) -> str | None:
if isinstance(entity_or_eid, str):
return self.eid_to_alias.get(entity_or_eid, None)
return self.eid_to_alias.get(entity_or_eid.eid, None)

def remove_entity_by_id(self, entity_id: str) -> None:
self._map[entity_id]._on_entity_destroyed()
del self._map[entity_id]

def remove_entity_by_alias(self, alias: str) -> None:
entity_id = self.get_entity_id(alias)
if alias in self.alias_to_eid:
del self.alias_to_eid[alias]
self.remove_entity_by_id(entity_id)


class Domain:

@staticmethod
def create_uid() -> str:
return str(uuid1())

def __init__(self, engine: Engine) -> None:
self.engine = engine
self.reset()

def reset(self) -> None:
self.entities = EntityRegistry(self)
self.queries: List[Query] = []
self.queries: list[Query] = []

def destroy_entity(self, entity: Entity | str) -> None:
if isinstance(entity, str):
Expand All @@ -161,18 +198,91 @@ def destroy_entity(self, entity: Entity | str) -> None:
else:
self.entities.remove_entity_by_alias(entity)
else:
# if entity.eid in self.entities.aliases.values():
for k, v in self.entities.alias_to_eid.items():
if v == entity.eid:
alias = k
self.entities.remove_entity_by_alias(alias)
return
self.entities.remove_entity_by_id(entity.eid)

def create_query(
self,
all_of: ComponentQuery | None = None,
any_of: ComponentQuery | None = None,
none_of: ComponentQuery | None = None,
) -> Query:
) -> Query:
query = Query(self, all_of, any_of, none_of)
self.queries.append(query)
return query

def candidate(self, entity: Entity) -> None:
for query in self.queries:
query.candidate(entity)

def save(self, directory: Path, filename: str) -> None:
output: dict[str, EntityDict] = {}

for entity in self.entities:
output[entity.eid] = {
"alias": self.entities.get_alias_for_entity(entity.eid),
"components": [],
}

for component in entity.components.values():
component_data = serialize_component(component)
output[entity.eid]["components"].append(component_data)

write_to_file(directory, filename, output)

def load(self, directory: Path, filename: str) -> None:
if loaded_data := load_from_file(directory, filename):
for eid, entity_data in loaded_data.items():
alias = entity_data["alias"]
component_data = entity_data["components"]

entity = self.entities.create(alias=alias, entity_id=eid)

for component_datum in component_data:
self.engine.components.attach(
entity,
component_datum["comp_id"],
{k: v for k, v in component_datum["data"].items()},
)


def write_to_file(
directory: Path,
filename: str,
data_dict: dict[str, EntityDict],
) -> None:
if not filename.endswith(".json"):
filename = filename + ".json"
with open(Path(directory, filename), "+w") as file:
file.write(json.dumps(data_dict, indent=4))


def load_from_file(directory: Path, filename: str) -> dict[str, EntityDict]:
if not (filename.endswith(".json")):
filename = filename + ".json"
with open(Path(directory, filename), "+r") as file:
return json.loads(file.read())


def serialize_component(component: Component) -> ComponentDict:
"""
Serialization should provide all of the necessary information needed to
rebuild what is set up in `setup_ecs` anew, with the state that the
components were in when they were serialized.
"""
comp_id = component.__class__.comp_id
cbit = component.__class__.cbit
instance_data = vars(component)

del instance_data["_entity_id"]

return {
"comp_id": comp_id,
"cbit": cbit,
"data": instance_data,
}
Loading
Loading