From 1c8f60f15b0098f8df546ec1bbff21bb3c2fb0e9 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 14:39:39 -0700 Subject: [PATCH 01/12] feat: add singleton pattern to NyxEngine class to maintain a global state refactor: begin refactor to support new pattern ref #37 --- main.py | 2 +- nyx/moirai_ecs/system/base_systems.py | 2 - nyx/moirai_ecs/system/movement_system.py | 3 +- nyx/nyx_engine/nyx_engine.py | 53 +++++++++++++++++------- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/main.py b/main.py index 86a0859..3b7c369 100644 --- a/main.py +++ b/main.py @@ -211,7 +211,7 @@ def generate_planet(engine: NyxEngine): engine.render_frame() # Cull off-screen entities engine.kill_entities() - sleep_time = NyxEngine.sec_per_game_loop - (datetime.now() - start_time).seconds + sleep_time = engine.sec_per_game_loop - (datetime.now() - start_time).seconds time.sleep(sleep_time) diff --git a/nyx/moirai_ecs/system/base_systems.py b/nyx/moirai_ecs/system/base_systems.py index 35c3f18..c112c1b 100644 --- a/nyx/moirai_ecs/system/base_systems.py +++ b/nyx/moirai_ecs/system/base_systems.py @@ -7,8 +7,6 @@ from abc import ABC -from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager - class BaseSystem(ABC): """The base system class, which all systems in the ECS architechture inherit from.""" diff --git a/nyx/moirai_ecs/system/movement_system.py b/nyx/moirai_ecs/system/movement_system.py index 74f829d..d156ff9 100644 --- a/nyx/moirai_ecs/system/movement_system.py +++ b/nyx/moirai_ecs/system/movement_system.py @@ -35,7 +35,8 @@ def update(self): velocity_reg: Dict[int, VelocityComponent] = ( ComponentManager.component_registry["velocity"] ) - dt = NyxEngine.sec_per_game_loop + engine = NyxEngine() + dt = engine.sec_per_game_loop # Update the position of each entity for entity_id, velocity_component in velocity_reg.items(): diff --git a/nyx/nyx_engine/nyx_engine.py b/nyx/nyx_engine/nyx_engine.py index 2dd3f31..2c4b45a 100644 --- a/nyx/nyx_engine/nyx_engine.py +++ b/nyx/nyx_engine/nyx_engine.py @@ -40,10 +40,8 @@ class NyxEngine: render_frame(): Renders the current frame. """ - # fps_target = 5 - game_update_per_sec = 60 - sec_per_game_loop = 1 / game_update_per_sec - running_systems = [] + + entity_manager = MoiraiEntityManager() component_manager = ComponentManager() aether_bridge = AetherBridgeSystem() @@ -51,12 +49,35 @@ class NyxEngine: hemera_term_fx = HemeraTermFx() tilemap_manager = TilemapManager(dimensions=aether_renderer.dimensions) + # Singleton instance + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if not hasattr(self, "initialized"): + self.initialized = True + self.is_running = False + self.fps_target = 5 + self.game_update_per_sec = 60 + self.sec_per_game_loop = 1 / self.game_update_per_sec + self.running_systems = [] + self.entity_manager = MoiraiEntityManager() + self.component_manager = ComponentManager() + self.aether_bridge = AetherBridgeSystem() + self.aether_renderer = AetherRenderer() + self.hemera_term_fx = HemeraTermFx() + self.tilemap_manager = TilemapManager(dimensions=self.aether_renderer.dimensions) + def run_game(self): """The main game loop.""" while True: self.trigger_systems() self.render_frame() - time.sleep(NyxEngine.sec_per_game_loop) + time.sleep(self.sec_per_game_loop) def add_system(self, system: BaseSystem): """Adds a system to the running systems list. @@ -64,11 +85,11 @@ def add_system(self, system: BaseSystem): Args: system (BaseSystem): The system to add. """ - NyxEngine.running_systems.append(system) + self.running_systems.append(system) def trigger_systems(self): """Triggers an update call on all running systems.""" - for system in NyxEngine.running_systems: + for system in self.running_systems: system.update() def kill_entities(self, bounds: int = 10): @@ -79,21 +100,21 @@ def kill_entities(self, bounds: int = 10): """ cull_id_list = [] h, w = ( - NyxEngine.aether_renderer.dimensions.effective_window_h, - NyxEngine.aether_renderer.dimensions.effective_window_w, + self.aether_renderer.dimensions.effective_window_h, + self.aether_renderer.dimensions.effective_window_w, ) - for entity_id, comp in NyxEngine.component_manager.component_registry[ + for entity_id, comp in self.component_manager.component_registry[ "position" ].items(): if comp.render_x_pos >= w + bounds or comp.render_y_pos >= h + bounds: cull_id_list.append(entity_id) for entity_id in cull_id_list: - NyxEngine.entity_manager.destroy_entity(entity_id) + self.entity_manager.destroy_entity(entity_id) def render_frame(self): """Renders the current frame.""" - NyxEngine.aether_bridge.update() - renderable_entities = NyxEngine.aether_bridge.renderable_entities - NyxEngine.aether_renderer.accept_entities(renderable_entities) - new_frame = NyxEngine.aether_renderer.render() - NyxEngine.hemera_term_fx.print(new_frame) + self.aether_bridge.update() + renderable_entities = self.aether_bridge.renderable_entities + self.aether_renderer.accept_entities(renderable_entities) + new_frame = self.aether_renderer.render() + self.hemera_term_fx.print(new_frame) From c7ddf47e87ba85c9fadc6c417cd041f54acb5e54 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 14:44:08 -0700 Subject: [PATCH 02/12] refactor: replace class-level vars with instance vars in NyxEngine refactor: update references ref #37 --- nyx/aether_renderer/aether_renderer.py | 5 ++--- nyx/nyx_engine/nyx_engine.py | 9 --------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/nyx/aether_renderer/aether_renderer.py b/nyx/aether_renderer/aether_renderer.py index 61281f2..e84d183 100644 --- a/nyx/aether_renderer/aether_renderer.py +++ b/nyx/aether_renderer/aether_renderer.py @@ -167,11 +167,10 @@ def _process_tilemap_component(self): component (TilemapComponent): The component holding a tilemap array. """ from nyx.nyx_engine.nyx_engine import NyxEngine - + engine = NyxEngine() frame_w = self.dimensions.effective_window_w frame_h = self.dimensions.effective_window_h - # NyxEngine.tilemap_manager.render_tilemap() - self.layered_frames[0] = NyxEngine.tilemap_manager.rendered_tilemap[ + self.layered_frames[0] = engine.tilemap_manager.rendered_tilemap[ :frame_h, :frame_w ] diff --git a/nyx/nyx_engine/nyx_engine.py b/nyx/nyx_engine/nyx_engine.py index 2c4b45a..104dfc3 100644 --- a/nyx/nyx_engine/nyx_engine.py +++ b/nyx/nyx_engine/nyx_engine.py @@ -40,15 +40,6 @@ class NyxEngine: render_frame(): Renders the current frame. """ - - - entity_manager = MoiraiEntityManager() - component_manager = ComponentManager() - aether_bridge = AetherBridgeSystem() - aether_renderer = AetherRenderer() - hemera_term_fx = HemeraTermFx() - tilemap_manager = TilemapManager(dimensions=aether_renderer.dimensions) - # Singleton instance _instance = None From 63deb1b9cbc6341084bd353f428ea662a87d977a Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 17:07:41 -0700 Subject: [PATCH 03/12] refactor: remove `ComponentManager` class references ref #38 --- main.py | 2 +- nyx/moirai_ecs/component/component_manager.py | 46 +++++++++---------- .../entity/moirai_entity_manager.py | 14 ++++-- nyx/moirai_ecs/system/aether_bridge_system.py | 3 +- nyx/moirai_ecs/system/base_systems.py | 13 +++++- nyx/moirai_ecs/system/movement_system.py | 6 ++- nyx/nyx_engine/nyx_engine.py | 13 ++++-- 7 files changed, 61 insertions(+), 36 deletions(-) diff --git a/main.py b/main.py index 3b7c369..dab981d 100644 --- a/main.py +++ b/main.py @@ -97,6 +97,7 @@ def generate_planet(engine: NyxEngine): if __name__ == "__main__": + engine = NyxEngine() # Configs #Line profile string buffer printing: line_profiling = False @@ -109,7 +110,6 @@ def generate_planet(engine: NyxEngine): window_width = 480 # Start the engine - engine = NyxEngine() engine.hemera_term_fx.run_line_profile = line_profiling # Add required systems to loop engine.add_system(MovementSystem()) diff --git a/nyx/moirai_ecs/component/component_manager.py b/nyx/moirai_ecs/component/component_manager.py index c5a83a4..9013854 100644 --- a/nyx/moirai_ecs/component/component_manager.py +++ b/nyx/moirai_ecs/component/component_manager.py @@ -29,18 +29,19 @@ class ComponentManager: destroy_compon(): Remove the a component from from the registry. remove_entity(): Remove all components belonging to an entity. """ - - # Component Registry - component_registry: Dict[str, Dict[int, NyxComponent]] = { - "background-color": {}, - "dimensions": {}, - "position": {}, - "scene": {}, - "texture": {}, - "tilemap": {}, - "velocity": {}, - "z-index": {}, - } + + def __init__(self): + # Component Registry + self.component_registry: Dict[str, Dict[int, NyxComponent]] = { + "background-color": {}, + "dimensions": {}, + "position": {}, + "scene": {}, + "texture": {}, + "tilemap": {}, + "velocity": {}, + "z-index": {}, + } def add_component( self, entity_id: int, component_name: str, component: NyxComponent @@ -55,12 +56,12 @@ def add_component( Raises: ValueError: If the component type is already registered for that entity ID. """ - if entity_id in ComponentManager.component_registry[component_name]: + if entity_id in self.component_registry[component_name]: raise ValueError( f'Component="{component_name}" already exists for entity={entity_id}' ) - ComponentManager.component_registry[component_name][entity_id] = component + self.component_registry[component_name][entity_id] = component def get_component(self, entity_id: int, component_name: str) -> NyxComponent: """Get a component for an entity. @@ -76,8 +77,8 @@ def get_component(self, entity_id: int, component_name: str) -> NyxComponent: NyxComponent: The component retrieved. """ - if entity_id in ComponentManager.component_registry[component_name]: - return ComponentManager.component_registry[component_name][entity_id] + if entity_id in self.component_registry[component_name]: + return self.component_registry[component_name][entity_id] raise KeyError( f'Entity={entity_id} not found in "{component_name}" component registry.' ) @@ -95,11 +96,11 @@ def update_component( Raises: KeyError: If the entity_id is not found for that component type. """ - if entity_id not in ComponentManager.component_registry[component_name]: + if entity_id not in self.component_registry[component_name]: raise KeyError( f'Entity={entity_id} not found in "{component_name}" component registry.' ) - ComponentManager.component_registry[component_name][entity_id] = component + self.component_registry[component_name][entity_id] = component def destroy_component(self, entity_id: int, component_name: str): """Remove the a component from from the registry. @@ -111,19 +112,18 @@ def destroy_component(self, entity_id: int, component_name: str): Raises: KeyError: If the entity_id is not found for that component type. """ - if entity_id not in ComponentManager.component_registry[component_name]: + if entity_id not in self.component_registry[component_name]: raise KeyError( f'Entity={entity_id} not found in "{component_name}" component registry.' ) - del ComponentManager.component_registry[component_name][entity_id] + del self.component_registry[component_name][entity_id] - @staticmethod - def remove_entity(entity_id: int): + def remove_entity(self, entity_id: int): """Remove all components belonging to an entity. Args: entity_id (int): The entity ID of the entity to clear from the registry. """ - for sub_dict in ComponentManager.component_registry.values(): + for sub_dict in self.component_registry.values(): if entity_id in sub_dict.keys(): del sub_dict[entity_id] diff --git a/nyx/moirai_ecs/entity/moirai_entity_manager.py b/nyx/moirai_ecs/entity/moirai_entity_manager.py index ff9332b..b1e7ebf 100644 --- a/nyx/moirai_ecs/entity/moirai_entity_manager.py +++ b/nyx/moirai_ecs/entity/moirai_entity_manager.py @@ -14,10 +14,12 @@ the path that life will follow; and Atropos cuts the thread, ending that life's journey. """ -from typing import Dict -from nyx.moirai_ecs.component.component_manager import ComponentManager +from typing import Dict, TYPE_CHECKING from nyx.moirai_ecs.entity.nyx_entity import NyxEntity +if TYPE_CHECKING: + from nyx.nyx_engine.nyx_engine import NyxEngine + class MoiraiEntityManager: """Holds and performs CRUD operations on registries for entities and their entity ids. @@ -40,6 +42,12 @@ def reset_entity_registry(cls): """Clear the entity registry of all entities.""" cls.entity_registry.clear() + def __init__(self, engine: "NyxEngine"): + self.engine = engine + self.component_manager = self.engine.component_manager + self.component_registry = self.component_manager.component_registry + self.entity_registry: Dict[int, NyxEntity] = {} + def create_entity(self, friendly_name: str = "") -> NyxEntity: """Create a NyxEntity and add it to the entity registry. @@ -62,7 +70,7 @@ def destroy_entity(self, entity_id: int): """ if entity_id in MoiraiEntityManager.entity_registry: del MoiraiEntityManager.entity_registry[entity_id] - ComponentManager.remove_entity(entity_id=entity_id) + self.component_manager.remove_entity(entity_id=entity_id) return self def is_alive(self, entity_id: int) -> bool: diff --git a/nyx/moirai_ecs/system/aether_bridge_system.py b/nyx/moirai_ecs/system/aether_bridge_system.py index 0aa9f46..3c720f8 100644 --- a/nyx/moirai_ecs/system/aether_bridge_system.py +++ b/nyx/moirai_ecs/system/aether_bridge_system.py @@ -12,7 +12,6 @@ from typing import Dict, List, Tuple import numpy as np -from nyx.moirai_ecs.component.component_manager import ComponentManager from nyx.moirai_ecs.system.base_systems import BaseSystem @@ -32,7 +31,7 @@ def __init__(self): def update(self): """Gather all renderable entities and components, then pass them to AetherRenderer.""" - component_registry = ComponentManager.component_registry + component_registry = self.engine.component_registry renderable_entities = {} # scene_entities = {} diff --git a/nyx/moirai_ecs/system/base_systems.py b/nyx/moirai_ecs/system/base_systems.py index c112c1b..6f9aa91 100644 --- a/nyx/moirai_ecs/system/base_systems.py +++ b/nyx/moirai_ecs/system/base_systems.py @@ -6,7 +6,18 @@ """ from abc import ABC +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from nyx.nyx_engine.nyx_engine import NyxEngine class BaseSystem(ABC): - """The base system class, which all systems in the ECS architechture inherit from.""" + """The base system class, which all systems in the ECS architecture inherit from.""" + + @property + def engine(self) -> "NyxEngine": + """Return the engine instance.""" + from nyx.nyx_engine.nyx_engine import NyxEngine + + return NyxEngine() diff --git a/nyx/moirai_ecs/system/movement_system.py b/nyx/moirai_ecs/system/movement_system.py index d156ff9..4c0c3c1 100644 --- a/nyx/moirai_ecs/system/movement_system.py +++ b/nyx/moirai_ecs/system/movement_system.py @@ -28,12 +28,14 @@ def update(self): Note: Calculates both the actual position and the position to render on the screen. """ + engine = self.engine + component_registry = engine.component_registry entity_reg = MoiraiEntityManager.entity_registry position_reg: Dict[int, PositionComponent] = ( - ComponentManager.component_registry["position"] + component_registry["position"] ) velocity_reg: Dict[int, VelocityComponent] = ( - ComponentManager.component_registry["velocity"] + component_registry["velocity"] ) engine = NyxEngine() dt = engine.sec_per_game_loop diff --git a/nyx/nyx_engine/nyx_engine.py b/nyx/nyx_engine/nyx_engine.py index 104dfc3..bf54899 100644 --- a/nyx/nyx_engine/nyx_engine.py +++ b/nyx/nyx_engine/nyx_engine.py @@ -8,6 +8,7 @@ """ import time +from typing import TYPE_CHECKING from nyx.aether_renderer.aether_renderer import AetherRenderer from nyx.aether_renderer.tilemap_manager import TilemapManager @@ -15,7 +16,10 @@ from nyx.moirai_ecs.component.component_manager import ComponentManager from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager from nyx.moirai_ecs.system.aether_bridge_system import AetherBridgeSystem -from nyx.moirai_ecs.system.base_systems import BaseSystem + + +if TYPE_CHECKING: + from nyx.moirai_ecs.system.base_systems import BaseSystem class NyxEngine: @@ -56,8 +60,9 @@ def __init__(self): self.game_update_per_sec = 60 self.sec_per_game_loop = 1 / self.game_update_per_sec self.running_systems = [] - self.entity_manager = MoiraiEntityManager() self.component_manager = ComponentManager() + self.component_registry = self.component_manager.component_registry + self.entity_manager = MoiraiEntityManager(self) self.aether_bridge = AetherBridgeSystem() self.aether_renderer = AetherRenderer() self.hemera_term_fx = HemeraTermFx() @@ -70,11 +75,11 @@ def run_game(self): self.render_frame() time.sleep(self.sec_per_game_loop) - def add_system(self, system: BaseSystem): + def add_system(self, system: "BaseSystem"): """Adds a system to the running systems list. Args: - system (BaseSystem): The system to add. + system ("BaseSystem"): The system to add. """ self.running_systems.append(system) From 34faeb9d1cfcaf9c3c3c884269e443920dec7ffc Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 17:22:45 -0700 Subject: [PATCH 04/12] refactor: replace class-level vars in `MoraiEntityManager` ref #38 --- .../entity/moirai_entity_manager.py | 16 +-- nyx/moirai_ecs/system/movement_system.py | 4 +- .../test_component/test_component_manager.py | 8 +- .../test_entity/test_entity_manager.py | 134 +++++++++--------- 4 files changed, 81 insertions(+), 81 deletions(-) diff --git a/nyx/moirai_ecs/entity/moirai_entity_manager.py b/nyx/moirai_ecs/entity/moirai_entity_manager.py index b1e7ebf..87a9a42 100644 --- a/nyx/moirai_ecs/entity/moirai_entity_manager.py +++ b/nyx/moirai_ecs/entity/moirai_entity_manager.py @@ -45,7 +45,7 @@ def reset_entity_registry(cls): def __init__(self, engine: "NyxEngine"): self.engine = engine self.component_manager = self.engine.component_manager - self.component_registry = self.component_manager.component_registry + self.component_registry = self.engine.component_registry self.entity_registry: Dict[int, NyxEntity] = {} def create_entity(self, friendly_name: str = "") -> NyxEntity: @@ -59,7 +59,7 @@ def create_entity(self, friendly_name: str = "") -> NyxEntity: """ new_entity = NyxEntity(friendly_name=friendly_name.strip()) - MoiraiEntityManager.entity_registry[new_entity.entity_id] = new_entity + self.entity_registry[new_entity.entity_id] = new_entity return new_entity def destroy_entity(self, entity_id: int): @@ -68,8 +68,8 @@ def destroy_entity(self, entity_id: int): Args: entity_id (int): The entity ID to remove. """ - if entity_id in MoiraiEntityManager.entity_registry: - del MoiraiEntityManager.entity_registry[entity_id] + if entity_id in self.entity_registry: + del self.entity_registry[entity_id] self.component_manager.remove_entity(entity_id=entity_id) return self @@ -82,7 +82,7 @@ def is_alive(self, entity_id: int) -> bool: Returns: bool: If the entity is alive. """ - return entity_id in MoiraiEntityManager.entity_registry + return entity_id in self.entity_registry def get_entity(self, entity_id: int) -> NyxEntity: """Get an entity from the entity list @@ -93,8 +93,8 @@ def get_entity(self, entity_id: int) -> NyxEntity: Returns: NyxEntity: The entity with the specified entity ID. """ - if entity_id in MoiraiEntityManager.entity_registry: - return MoiraiEntityManager.entity_registry[entity_id] + if entity_id in self.entity_registry: + return self.entity_registry[entity_id] def get_all_entities(self) -> Dict[int, NyxEntity]: """Return a registry of all entities in this manager. @@ -102,4 +102,4 @@ def get_all_entities(self) -> Dict[int, NyxEntity]: Returns: Dict[int, NyxEntity]: The registry of NyxEntity objects. """ - return MoiraiEntityManager.entity_registry + return self.entity_registry diff --git a/nyx/moirai_ecs/system/movement_system.py b/nyx/moirai_ecs/system/movement_system.py index 4c0c3c1..c032b21 100644 --- a/nyx/moirai_ecs/system/movement_system.py +++ b/nyx/moirai_ecs/system/movement_system.py @@ -8,12 +8,10 @@ """ from typing import Dict -from nyx.moirai_ecs.component.component_manager import ComponentManager from nyx.moirai_ecs.component.transform_components import ( PositionComponent, VelocityComponent, ) -from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager from nyx.moirai_ecs.system.base_systems import BaseSystem from nyx.nyx_engine.nyx_engine import NyxEngine @@ -30,7 +28,7 @@ def update(self): """ engine = self.engine component_registry = engine.component_registry - entity_reg = MoiraiEntityManager.entity_registry + entity_reg = engine.entity_manager.entity_registry position_reg: Dict[int, PositionComponent] = ( component_registry["position"] ) diff --git a/tests/test_moirai_ecs/test_component/test_component_manager.py b/tests/test_moirai_ecs/test_component/test_component_manager.py index 29b6e7a..c05aaaa 100644 --- a/tests/test_moirai_ecs/test_component/test_component_manager.py +++ b/tests/test_moirai_ecs/test_component/test_component_manager.py @@ -20,7 +20,7 @@ def test_add_component(): component_manager.add_component( entity_id=entity_id, component_name="dimensions", component=component ) - assert component in ComponentManager.component_registry["dimensions"].values() + assert component in component_manager.component_registry["dimensions"].values() def test_get_component(): @@ -55,7 +55,7 @@ def test_update_component(): component_manager.update_component( entity_id=entity_id, component_name=component_name, component=component_2 ) - assert component_2 in ComponentManager.component_registry[component_name].values() + assert component_2 in component_manager.component_registry[component_name].values() def test_destroy_component(): @@ -72,7 +72,7 @@ def test_destroy_component(): entity_id=entity_id, component_name=component_name ) - assert component not in ComponentManager.component_registry[component_name].values() + assert component not in component_manager.component_registry[component_name].values() def test_remove_entity(): @@ -98,5 +98,5 @@ def test_remove_entity(): component_manager.remove_entity(entity_id=entity_id) - for sub_dict in ComponentManager.component_registry.values(): + for sub_dict in component_manager.component_registry.values(): assert entity_id not in sub_dict.keys() diff --git a/tests/test_moirai_ecs/test_entity/test_entity_manager.py b/tests/test_moirai_ecs/test_entity/test_entity_manager.py index 6a9a20d..127d144 100644 --- a/tests/test_moirai_ecs/test_entity/test_entity_manager.py +++ b/tests/test_moirai_ecs/test_entity/test_entity_manager.py @@ -1,92 +1,94 @@ -from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager -from nyx.moirai_ecs.entity.nyx_entity import NyxEntity +"""Need to mock the engine because AetherDimensions does not allow for setting a +terminal size within a pytest test.""" +# from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager +# from nyx.moirai_ecs.entity.nyx_entity import NyxEntity +# from nyx.nyx_engine.nyx_engine import NyxEngine +# def test_entity_manager_construction(): +# """Test that the entity manager can be initialized.""" +# assert isinstance(MoiraiEntityManager(NyxEngine()), MoiraiEntityManager) -def test_entity_manager_construction(): - """Test that the entity manager can be initialized.""" - assert isinstance(MoiraiEntityManager(), MoiraiEntityManager) +# def test_create_entities(): +# """Test the creation of entities (create, add to reg, return).""" +# entity_fate_manager = MoiraiEntityManager() +# entity_list = [] -def test_create_entities(): - """Test the creation of entities (create, add to reg, return).""" - entity_fate_manager = MoiraiEntityManager() - entity_list = [] +# for i in range(10): +# entity_list.append(entity_fate_manager.create_entity()) - for i in range(10): - entity_list.append(entity_fate_manager.create_entity()) +# for entity in entity_list: +# assert entity in entity_list - for entity in entity_list: - assert entity in entity_list +# def test_reset_entity_registry(): +# """Test clearing the entire entity registry""" +# entity_fate_manager = MoiraiEntityManager() +# entity_list = [] -def test_reset_entity_registry(): - """Test clearing the entire entity registry""" - entity_fate_manager = MoiraiEntityManager() - entity_list = [] +# # Create the entitiies +# for i in range(10): +# entity_list.append(entity_fate_manager.create_entity()) - # Create the entitiies - for i in range(10): - entity_list.append(entity_fate_manager.create_entity()) +# # Clear the registry +# entity_fate_manager.reset_entity_registry() - # Clear the registry - entity_fate_manager.reset_entity_registry() +# # Get the cleared registry +# entity_manager_list = MoiraiEntityManager.entity_registry - # Get the cleared registry - entity_manager_list = MoiraiEntityManager.entity_registry +# # Remove the entities +# for entity in entity_list: +# assert entity.entity_id not in entity_manager_list - # Remove the entities - for entity in entity_list: - assert entity.entity_id not in entity_manager_list +# def test_destroy_entity(): +# """Test the destruction of entities.""" +# entity_fate_manager = MoiraiEntityManager() +# entity_fate_manager.reset_entity_registry() +# entity_list = [] -def test_destroy_entity(): - """Test the destruction of entities.""" - entity_fate_manager = MoiraiEntityManager() - entity_fate_manager.reset_entity_registry() - entity_list = [] +# # Create the entities +# for i in range(10): +# entity_list.append(entity_fate_manager.create_entity()) - # Create the entities - for i in range(10): - entity_list.append(entity_fate_manager.create_entity()) +# # Get the registry +# entity_manager_list = MoiraiEntityManager.entity_registry - # Get the registry - entity_manager_list = MoiraiEntityManager.entity_registry +# # Remove the entities +# for entity in entity_list: +# entity_fate_manager.destroy_entity(entity.entity_id) +# assert entity.entity_id not in entity_manager_list - # Remove the entities - for entity in entity_list: - entity_fate_manager.destroy_entity(entity.entity_id) - assert entity.entity_id not in entity_manager_list +# def test_is_alive(): +# """Test if an entity is present in the entity list.""" +# entity_fate_manager = MoiraiEntityManager() +# entity_fate_manager.reset_entity_registry() +# entity_fate_manager.create_entity() -def test_is_alive(): - """Test if an entity is present in the entity list.""" - entity_fate_manager = MoiraiEntityManager() - entity_fate_manager.reset_entity_registry() - entity_fate_manager.create_entity() +# assert entity_fate_manager.is_alive(entity_fate_manager.create_entity().entity_id) +# assert not entity_fate_manager.is_alive(-1) - assert entity_fate_manager.is_alive(entity_fate_manager.create_entity().entity_id) - assert not entity_fate_manager.is_alive(-1) +# def test_get_entity(): +# """Test getting a single, existing entity""" +# entity_fate_manager = MoiraiEntityManager() +# entity_fate_manager.reset_entity_registry() -def test_get_entity(): - """Test getting a single, existing entity""" - entity_fate_manager = MoiraiEntityManager() - entity_fate_manager.reset_entity_registry() +# assert isinstance( +# entity_fate_manager.get_entity(entity_fate_manager.create_entity().entity_id), +# NyxEntity, +# ) - assert isinstance( - entity_fate_manager.get_entity(entity_fate_manager.create_entity().entity_id), - NyxEntity, - ) +# def test_get_all_entities(): +# """Test getting all entities from the manager""" +# entity_fate_manager = MoiraiEntityManager() +# entity_fate_manager.reset_entity_registry() +# entity_list = [] -def test_get_all_entities(): - """Test getting all entities from the manager""" - entity_fate_manager = MoiraiEntityManager() - entity_fate_manager.reset_entity_registry() - entity_list = [] +# # Create the entities +# for _ in range(10): +# entity_list.append(entity_fate_manager.create_entity()) - # Create the entities - for _ in range(10): - entity_list.append(entity_fate_manager.create_entity()) - - assert len(entity_fate_manager.get_all_entities()) == 10 +# assert len(entity_fate_manager.get_all_entities()) == 10 From fff92d10c799c5128fc917b53f0a2c2fe09b2748 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 17:39:01 -0700 Subject: [PATCH 05/12] refactor: replace `AetherRenderer` and `AetherDimensions` class-level calls ref # 38 --- nyx/moirai_ecs/system/base_systems.py | 1 - nyx/nyx_engine/nyx_engine.py | 16 ++++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/nyx/moirai_ecs/system/base_systems.py b/nyx/moirai_ecs/system/base_systems.py index 6f9aa91..7f93dd9 100644 --- a/nyx/moirai_ecs/system/base_systems.py +++ b/nyx/moirai_ecs/system/base_systems.py @@ -19,5 +19,4 @@ class BaseSystem(ABC): def engine(self) -> "NyxEngine": """Return the engine instance.""" from nyx.nyx_engine.nyx_engine import NyxEngine - return NyxEngine() diff --git a/nyx/nyx_engine/nyx_engine.py b/nyx/nyx_engine/nyx_engine.py index bf54899..edbdcb6 100644 --- a/nyx/nyx_engine/nyx_engine.py +++ b/nyx/nyx_engine/nyx_engine.py @@ -10,6 +10,7 @@ import time from typing import TYPE_CHECKING +from nyx.aether_renderer.aether_dimensions import AetherDimensions from nyx.aether_renderer.aether_renderer import AetherRenderer from nyx.aether_renderer.tilemap_manager import TilemapManager from nyx.hemera_term_fx.hemera_term_fx import HemeraTermFx @@ -60,13 +61,16 @@ def __init__(self): self.game_update_per_sec = 60 self.sec_per_game_loop = 1 / self.game_update_per_sec self.running_systems = [] - self.component_manager = ComponentManager() + self.component_manager: ComponentManager = ComponentManager() self.component_registry = self.component_manager.component_registry - self.entity_manager = MoiraiEntityManager(self) - self.aether_bridge = AetherBridgeSystem() - self.aether_renderer = AetherRenderer() - self.hemera_term_fx = HemeraTermFx() - self.tilemap_manager = TilemapManager(dimensions=self.aether_renderer.dimensions) + self.entity_manager: MoiraiEntityManager = MoiraiEntityManager(self) + self.aether_bridge: AetherBridgeSystem = AetherBridgeSystem() + self.aether_renderer: AetherRenderer = AetherRenderer() + self.aether_dimensions: AetherDimensions = self.aether_renderer.dimensions + self.hemera_term_fx: HemeraTermFx = HemeraTermFx() + self.tilemap_manager: TilemapManager = TilemapManager( + dimensions=self.aether_dimensions + ) def run_game(self): """The main game loop.""" From 326fbb6ce694bed0f8968fe3782b0d0421c1cf9e Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 17:49:49 -0700 Subject: [PATCH 06/12] feat: begin base class for manager subsystems ref #38 --- nyx/base_classes/base_manager.py | 14 ++++++++++++++ nyx/nyx_engine/nyx_engine.py | 7 ++++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 nyx/base_classes/base_manager.py diff --git a/nyx/base_classes/base_manager.py b/nyx/base_classes/base_manager.py new file mode 100644 index 0000000..b297c14 --- /dev/null +++ b/nyx/base_classes/base_manager.py @@ -0,0 +1,14 @@ +from abc import ABC + +from nyx.aether_renderer.aether_dimensions import AetherDimensions +from nyx.nyx_engine.nyx_engine import NyxEngine + + +class BaseManager: + @property + def engine(self) -> NyxEngine: + return NyxEngine() + + @property + def dimensions(self) -> AetherDimensions: + return self.engine.aether_dimensions diff --git a/nyx/nyx_engine/nyx_engine.py b/nyx/nyx_engine/nyx_engine.py index edbdcb6..4f0819c 100644 --- a/nyx/nyx_engine/nyx_engine.py +++ b/nyx/nyx_engine/nyx_engine.py @@ -8,12 +8,13 @@ """ import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List from nyx.aether_renderer.aether_dimensions import AetherDimensions from nyx.aether_renderer.aether_renderer import AetherRenderer from nyx.aether_renderer.tilemap_manager import TilemapManager from nyx.hemera_term_fx.hemera_term_fx import HemeraTermFx +from nyx.moirai_ecs.component.base_components import NyxComponent from nyx.moirai_ecs.component.component_manager import ComponentManager from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager from nyx.moirai_ecs.system.aether_bridge_system import AetherBridgeSystem @@ -60,9 +61,9 @@ def __init__(self): self.fps_target = 5 self.game_update_per_sec = 60 self.sec_per_game_loop = 1 / self.game_update_per_sec - self.running_systems = [] + self.running_systems: List[BaseSystem] = [] self.component_manager: ComponentManager = ComponentManager() - self.component_registry = self.component_manager.component_registry + self.component_registry: Dict[str, Dict[int, NyxComponent]] = self.component_manager.component_registry self.entity_manager: MoiraiEntityManager = MoiraiEntityManager(self) self.aether_bridge: AetherBridgeSystem = AetherBridgeSystem() self.aether_renderer: AetherRenderer = AetherRenderer() From eae0abbd50468dd1a6f25cf20b2cab9334080515 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 18:08:37 -0700 Subject: [PATCH 07/12] fix: catch`OSError` when trying to use in a non-term environment (such as testing or output to file, etc) - Specifically fixes the `MoiraiEntityManager` testing failures from the class attribute refactor rounds ref # 38 --- nyx/hemera_term_fx/term_utils.py | 10 +- nyx/nyx_engine/nyx_engine.py | 3 +- .../test_entity/test_entity_manager.py | 135 +++++++++--------- 3 files changed, 77 insertions(+), 71 deletions(-) diff --git a/nyx/hemera_term_fx/term_utils.py b/nyx/hemera_term_fx/term_utils.py index 4915f44..acdd806 100644 --- a/nyx/hemera_term_fx/term_utils.py +++ b/nyx/hemera_term_fx/term_utils.py @@ -92,5 +92,11 @@ def cursor_abs_move(pixel_x: int, pixel_y: int) -> str: @staticmethod def get_terminal_dimensions() -> Tuple[int, int]: """Return the terminal size as a tuple of integers in (h, w) format.""" - terminal_size = os.get_terminal_size() - return (terminal_size.lines, terminal_size.columns) + try: + terminal_size = os.get_terminal_size() + h, w = terminal_size.lines, terminal_size.columns + except OSError: + # Triggers when running in a non-terminal env., such as during testing + # 10, 10 was used simply because it is not 0, 0; which may cause bound issues + h, w = 10, 10 + return h, w diff --git a/nyx/nyx_engine/nyx_engine.py b/nyx/nyx_engine/nyx_engine.py index 4f0819c..f2bcc00 100644 --- a/nyx/nyx_engine/nyx_engine.py +++ b/nyx/nyx_engine/nyx_engine.py @@ -58,7 +58,8 @@ def __init__(self): if not hasattr(self, "initialized"): self.initialized = True self.is_running = False - self.fps_target = 5 + self.fps_target = 30 + self.sec_per_frame = 1 / self.fps_target self.game_update_per_sec = 60 self.sec_per_game_loop = 1 / self.game_update_per_sec self.running_systems: List[BaseSystem] = [] diff --git a/tests/test_moirai_ecs/test_entity/test_entity_manager.py b/tests/test_moirai_ecs/test_entity/test_entity_manager.py index 127d144..5a14261 100644 --- a/tests/test_moirai_ecs/test_entity/test_entity_manager.py +++ b/tests/test_moirai_ecs/test_entity/test_entity_manager.py @@ -1,94 +1,93 @@ -"""Need to mock the engine because AetherDimensions does not allow for setting a -terminal size within a pytest test.""" -# from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager -# from nyx.moirai_ecs.entity.nyx_entity import NyxEntity -# from nyx.nyx_engine.nyx_engine import NyxEngine -# def test_entity_manager_construction(): -# """Test that the entity manager can be initialized.""" -# assert isinstance(MoiraiEntityManager(NyxEngine()), MoiraiEntityManager) +from nyx.moirai_ecs.entity.moirai_entity_manager import MoiraiEntityManager +from nyx.moirai_ecs.entity.nyx_entity import NyxEntity +from nyx.nyx_engine.nyx_engine import NyxEngine +def test_entity_manager_construction(): + """Test that the entity manager can be initialized.""" + assert isinstance(MoiraiEntityManager(NyxEngine()), MoiraiEntityManager) -# def test_create_entities(): -# """Test the creation of entities (create, add to reg, return).""" -# entity_fate_manager = MoiraiEntityManager() -# entity_list = [] -# for i in range(10): -# entity_list.append(entity_fate_manager.create_entity()) +def test_create_entities(): + """Test the creation of entities (create, add to reg, return).""" + entity_fate_manager = MoiraiEntityManager(NyxEngine()) + entity_list = [] -# for entity in entity_list: -# assert entity in entity_list + for i in range(10): + entity_list.append(entity_fate_manager.create_entity()) + for entity in entity_list: + assert entity in entity_list -# def test_reset_entity_registry(): -# """Test clearing the entire entity registry""" -# entity_fate_manager = MoiraiEntityManager() -# entity_list = [] -# # Create the entitiies -# for i in range(10): -# entity_list.append(entity_fate_manager.create_entity()) +def test_reset_entity_registry(): + """Test clearing the entire entity registry""" + entity_fate_manager = MoiraiEntityManager(NyxEngine()) + entity_list = [] -# # Clear the registry -# entity_fate_manager.reset_entity_registry() + # Create the entitiies + for i in range(10): + entity_list.append(entity_fate_manager.create_entity()) -# # Get the cleared registry -# entity_manager_list = MoiraiEntityManager.entity_registry + # Clear the registry + entity_fate_manager.reset_entity_registry() -# # Remove the entities -# for entity in entity_list: -# assert entity.entity_id not in entity_manager_list + # Get the cleared registry + entity_manager_list = MoiraiEntityManager.entity_registry + # Remove the entities + for entity in entity_list: + assert entity.entity_id not in entity_manager_list -# def test_destroy_entity(): -# """Test the destruction of entities.""" -# entity_fate_manager = MoiraiEntityManager() -# entity_fate_manager.reset_entity_registry() -# entity_list = [] -# # Create the entities -# for i in range(10): -# entity_list.append(entity_fate_manager.create_entity()) +def test_destroy_entity(): + """Test the destruction of entities.""" + entity_fate_manager = MoiraiEntityManager(NyxEngine()) + entity_fate_manager.reset_entity_registry() + entity_list = [] -# # Get the registry -# entity_manager_list = MoiraiEntityManager.entity_registry + # Create the entities + for i in range(10): + entity_list.append(entity_fate_manager.create_entity()) -# # Remove the entities -# for entity in entity_list: -# entity_fate_manager.destroy_entity(entity.entity_id) -# assert entity.entity_id not in entity_manager_list + # Get the registry + entity_manager_list = MoiraiEntityManager.entity_registry + # Remove the entities + for entity in entity_list: + entity_fate_manager.destroy_entity(entity.entity_id) + assert entity.entity_id not in entity_manager_list -# def test_is_alive(): -# """Test if an entity is present in the entity list.""" -# entity_fate_manager = MoiraiEntityManager() -# entity_fate_manager.reset_entity_registry() -# entity_fate_manager.create_entity() -# assert entity_fate_manager.is_alive(entity_fate_manager.create_entity().entity_id) -# assert not entity_fate_manager.is_alive(-1) +def test_is_alive(): + """Test if an entity is present in the entity list.""" + entity_fate_manager = MoiraiEntityManager(NyxEngine()) + entity_fate_manager.reset_entity_registry() + entity_fate_manager.create_entity() + assert entity_fate_manager.is_alive(entity_fate_manager.create_entity().entity_id) + assert not entity_fate_manager.is_alive(-1) -# def test_get_entity(): -# """Test getting a single, existing entity""" -# entity_fate_manager = MoiraiEntityManager() -# entity_fate_manager.reset_entity_registry() -# assert isinstance( -# entity_fate_manager.get_entity(entity_fate_manager.create_entity().entity_id), -# NyxEntity, -# ) +def test_get_entity(): + """Test getting a single, existing entity""" + entity_fate_manager = MoiraiEntityManager(NyxEngine()) + entity_fate_manager.reset_entity_registry() + assert isinstance( + entity_fate_manager.get_entity(entity_fate_manager.create_entity().entity_id), + NyxEntity, + ) -# def test_get_all_entities(): -# """Test getting all entities from the manager""" -# entity_fate_manager = MoiraiEntityManager() -# entity_fate_manager.reset_entity_registry() -# entity_list = [] -# # Create the entities -# for _ in range(10): -# entity_list.append(entity_fate_manager.create_entity()) +def test_get_all_entities(): + """Test getting all entities from the manager""" + entity_fate_manager = MoiraiEntityManager(NyxEngine()) + entity_fate_manager.reset_entity_registry() + entity_list = [] -# assert len(entity_fate_manager.get_all_entities()) == 10 + # Create the entities + for _ in range(10): + entity_list.append(entity_fate_manager.create_entity()) + + assert len(entity_fate_manager.get_all_entities()) == 10 From edbf55bc68a6e09554203116c9f0a69aca4eb649 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 19:37:26 -0700 Subject: [PATCH 08/12] refactor: clarify method `remove_component` method name --- nyx/moirai_ecs/component/component_manager.py | 4 ++-- .../test_component/test_component_manager.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/nyx/moirai_ecs/component/component_manager.py b/nyx/moirai_ecs/component/component_manager.py index 9013854..99e62ba 100644 --- a/nyx/moirai_ecs/component/component_manager.py +++ b/nyx/moirai_ecs/component/component_manager.py @@ -29,7 +29,7 @@ class ComponentManager: destroy_compon(): Remove the a component from from the registry. remove_entity(): Remove all components belonging to an entity. """ - + def __init__(self): # Component Registry self.component_registry: Dict[str, Dict[int, NyxComponent]] = { @@ -102,7 +102,7 @@ def update_component( ) self.component_registry[component_name][entity_id] = component - def destroy_component(self, entity_id: int, component_name: str): + def remove_component(self, entity_id: int, component_name: str): """Remove the a component from from the registry. Args: diff --git a/tests/test_moirai_ecs/test_component/test_component_manager.py b/tests/test_moirai_ecs/test_component/test_component_manager.py index c05aaaa..35caca1 100644 --- a/tests/test_moirai_ecs/test_component/test_component_manager.py +++ b/tests/test_moirai_ecs/test_component/test_component_manager.py @@ -68,11 +68,13 @@ def test_destroy_component(): component_manager.add_component( entity_id=entity_id, component_name=component_name, component=component ) - component_manager.destroy_component( + component_manager.remove_component( entity_id=entity_id, component_name=component_name ) - assert component not in component_manager.component_registry[component_name].values() + assert ( + component not in component_manager.component_registry[component_name].values() + ) def test_remove_entity(): From f25ff3bcbcdb7bc6ccb4a0c9fd5c9c39a57fddb5 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 20:43:52 -0700 Subject: [PATCH 09/12] fix: update dependencies (numpy) and python (3.10.8) version in github action --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 737ea52..a39622a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,12 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.13' + python-version: '3.10.8' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black pytest + pip install black pytest numpy - name: Set up PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV From 05b0db99705212931d79890b70d2339389519196 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 21:40:49 -0700 Subject: [PATCH 10/12] fix: update action --- .github/workflows/ci.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a39622a..5cd3143 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI/CD Pipeline +name: Automated Tests on: push: @@ -22,13 +22,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install black pytest numpy + pip install pytest numpy - name: Set up PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV - - name: FORMATTING - Run black - run: black . - - name: TESTS - Run tests run: pytest --disable-warnings \ No newline at end of file From eb29d88d4715af36fefa5b3269193c8bd3b90027 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 21:42:01 -0700 Subject: [PATCH 11/12] fix: add `line_profiler` as dependency --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cd3143..22f6152 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest numpy + pip install pytest numpy line_profiler - name: Set up PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV From 503fc4a686e50bf89c7bf1431d61df8196f1ccf4 Mon Sep 17 00:00:00 2001 From: Charles Morman Date: Fri, 3 Jan 2025 21:54:24 -0700 Subject: [PATCH 12/12] docs: update changelog ref #37, #38 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d31a84..1eb2494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Changed - Refactor `HemeraTermFx` to further optimize terminal printing performance. +- Update `NyxEngine` as the main enforcer of game state consistency across the project. It now holds all instances of the major subclasses. --- @@ -22,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add an alient planet sprite to the current game demo in `main.py`. ### Changed -- Optimize terminal printing string generation for a 95% reduction in frame printing time. +- Optimize `HemeraTermFx` terminal printing string generation for a 95% reduction in frame printing time. ---