pyguara 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. pyguara/__init__.py +0 -0
  2. pyguara/ai/ai_system.py +29 -0
  3. pyguara/ai/blackboard.py +32 -0
  4. pyguara/ai/components.py +76 -0
  5. pyguara/ai/fsm.py +82 -0
  6. pyguara/ai/pathfinding/astar.py +58 -0
  7. pyguara/ai/pathfinding/core.py +29 -0
  8. pyguara/ai/pathfinding/grid.py +72 -0
  9. pyguara/ai/steering.py +55 -0
  10. pyguara/app_scene.py +132 -0
  11. pyguara/application/__init__.py +6 -0
  12. pyguara/application/application.py +100 -0
  13. pyguara/application/bootstrap.py +132 -0
  14. pyguara/application/sandbox.py +116 -0
  15. pyguara/audio/__init__.py +1 -0
  16. pyguara/audio/audio_system.py +116 -0
  17. pyguara/audio/backends/pygame/loaders.py +30 -0
  18. pyguara/audio/backends/pygame/pygame_audio.py +185 -0
  19. pyguara/audio/backends/pygame/types.py +37 -0
  20. pyguara/audio/manager.py +248 -0
  21. pyguara/common/components.py +316 -0
  22. pyguara/common/constants.py +1 -0
  23. pyguara/common/palette.py +43 -0
  24. pyguara/common/types.py +311 -0
  25. pyguara/config/__init__.py +14 -0
  26. pyguara/config/events.py +39 -0
  27. pyguara/config/manager.py +128 -0
  28. pyguara/config/types.py +82 -0
  29. pyguara/config/validation.py +76 -0
  30. pyguara/di/__init__.py +34 -0
  31. pyguara/di/container.py +369 -0
  32. pyguara/di/decorators.py +56 -0
  33. pyguara/di/exceptions.py +33 -0
  34. pyguara/di/types.py +70 -0
  35. pyguara/ecs/__init__.py +14 -0
  36. pyguara/ecs/component.py +40 -0
  37. pyguara/ecs/entity.py +158 -0
  38. pyguara/ecs/manager.py +195 -0
  39. pyguara/ecs/query_cache.py +197 -0
  40. pyguara/editor/__init__.py +1 -0
  41. pyguara/editor/drawers.py +187 -0
  42. pyguara/editor/layer.py +165 -0
  43. pyguara/editor/panels/assets.py +163 -0
  44. pyguara/editor/panels/hierarchy.py +64 -0
  45. pyguara/editor/panels/inspector.py +81 -0
  46. pyguara/error/__init__.py +24 -0
  47. pyguara/error/exceptions.py +107 -0
  48. pyguara/error/handlers.py +110 -0
  49. pyguara/error/types.py +27 -0
  50. pyguara/events/__init__.py +1 -0
  51. pyguara/events/dispatcher.py +161 -0
  52. pyguara/events/input.py +50 -0
  53. pyguara/events/lifecycle.py +31 -0
  54. pyguara/events/protocols.py +92 -0
  55. pyguara/events/types.py +40 -0
  56. pyguara/events/window.py +20 -0
  57. pyguara/graphics/__init__.py +1 -0
  58. pyguara/graphics/animation_system.py +37 -0
  59. pyguara/graphics/atlas.py +110 -0
  60. pyguara/graphics/backends/headless_renderer.py +115 -0
  61. pyguara/graphics/backends/pygame/loaders.py +27 -0
  62. pyguara/graphics/backends/pygame/pygame_renderer.py +113 -0
  63. pyguara/graphics/backends/pygame/pygame_window.py +75 -0
  64. pyguara/graphics/backends/pygame/types.py +34 -0
  65. pyguara/graphics/backends/pygame/ui_renderer.py +115 -0
  66. pyguara/graphics/color.py +1 -0
  67. pyguara/graphics/components/__init__.py +27 -0
  68. pyguara/graphics/components/animation.py +312 -0
  69. pyguara/graphics/components/camera.py +407 -0
  70. pyguara/graphics/components/geometry.py +150 -0
  71. pyguara/graphics/components/particles.py +387 -0
  72. pyguara/graphics/components/sprite.py +32 -0
  73. pyguara/graphics/pipeline/batch.py +99 -0
  74. pyguara/graphics/pipeline/queue.py +37 -0
  75. pyguara/graphics/pipeline/render_system.py +95 -0
  76. pyguara/graphics/pipeline/viewport.py +147 -0
  77. pyguara/graphics/protocols.py +318 -0
  78. pyguara/graphics/spritesheet.py +79 -0
  79. pyguara/graphics/types.py +66 -0
  80. pyguara/graphics/window.py +80 -0
  81. pyguara/input/__init__.py +21 -0
  82. pyguara/input/binding.py +42 -0
  83. pyguara/input/events.py +63 -0
  84. pyguara/input/gamepad.py +358 -0
  85. pyguara/input/keys.py +19 -0
  86. pyguara/input/manager.py +196 -0
  87. pyguara/input/types.py +114 -0
  88. pyguara/log/__init__.py +19 -0
  89. pyguara/log/events.py +31 -0
  90. pyguara/log/handlers.py +75 -0
  91. pyguara/log/logger.py +173 -0
  92. pyguara/log/manager.py +68 -0
  93. pyguara/log/types.py +30 -0
  94. pyguara/persistence/manager.py +136 -0
  95. pyguara/persistence/serializer.py +110 -0
  96. pyguara/persistence/storage.py +99 -0
  97. pyguara/persistence/types.py +77 -0
  98. pyguara/physics/__init__.py +17 -0
  99. pyguara/physics/backends/pymunk_impl.py +448 -0
  100. pyguara/physics/collision_system.py +295 -0
  101. pyguara/physics/components.py +63 -0
  102. pyguara/physics/events.py +187 -0
  103. pyguara/physics/joints.py +349 -0
  104. pyguara/physics/materials.py +63 -0
  105. pyguara/physics/physics_system.py +109 -0
  106. pyguara/physics/protocols.py +138 -0
  107. pyguara/physics/types.py +103 -0
  108. pyguara/resources/data.py +69 -0
  109. pyguara/resources/loader.py +43 -0
  110. pyguara/resources/loaders/data_loader.py +40 -0
  111. pyguara/resources/manager.py +368 -0
  112. pyguara/resources/types.py +75 -0
  113. pyguara/scene/base.py +83 -0
  114. pyguara/scene/manager.py +55 -0
  115. pyguara/scene/serializer.py +93 -0
  116. pyguara/tools/base.py +80 -0
  117. pyguara/tools/debugger.py +80 -0
  118. pyguara/tools/event_monitor.py +83 -0
  119. pyguara/tools/inspector.py +166 -0
  120. pyguara/tools/manager.py +132 -0
  121. pyguara/tools/performance.py +59 -0
  122. pyguara/tools/shortcuts_panel.py +59 -0
  123. pyguara/ui/__init__.py +27 -0
  124. pyguara/ui/base.py +117 -0
  125. pyguara/ui/components/__init__.py +27 -0
  126. pyguara/ui/components/button.py +48 -0
  127. pyguara/ui/components/canvas.py +29 -0
  128. pyguara/ui/components/checkbox.py +60 -0
  129. pyguara/ui/components/image.py +27 -0
  130. pyguara/ui/components/label.py +35 -0
  131. pyguara/ui/components/navbar.py +33 -0
  132. pyguara/ui/components/panel.py +31 -0
  133. pyguara/ui/components/progress_bar.py +36 -0
  134. pyguara/ui/components/slider.py +84 -0
  135. pyguara/ui/components/text.py +46 -0
  136. pyguara/ui/components/text_input.py +77 -0
  137. pyguara/ui/components/widget.py +25 -0
  138. pyguara/ui/layout.py +87 -0
  139. pyguara/ui/manager.py +58 -0
  140. pyguara/ui/theme.py +37 -0
  141. pyguara/ui/types.py +113 -0
  142. pyguara-0.2.0.dist-info/METADATA +167 -0
  143. pyguara-0.2.0.dist-info/RECORD +145 -0
  144. pyguara-0.2.0.dist-info/WHEEL +4 -0
  145. pyguara-0.2.0.dist-info/licenses/LICENSE +8 -0
pyguara/__init__.py ADDED
File without changes
@@ -0,0 +1,29 @@
1
+ """Systems for updating AI logic."""
2
+
3
+ from pyguara.ai.components import AIComponent
4
+ from pyguara.ecs.manager import EntityManager
5
+
6
+
7
+ class AISystem:
8
+ """Updates AI decision making (FSMs/Trees)."""
9
+
10
+ def __init__(self, entity_manager: EntityManager):
11
+ """Initialize the AI system for all Entities."""
12
+ self.manager = entity_manager
13
+
14
+ def update(self, dt: float) -> None:
15
+ """Update all AI components."""
16
+ # Query entities with AI
17
+ for entity in self.manager.get_entities_with(AIComponent):
18
+ ai: AIComponent = entity.ai_component # Using __getattr__ magic
19
+
20
+ if not ai.enabled:
21
+ continue
22
+
23
+ # Update FSM if present
24
+ if ai.fsm:
25
+ ai.fsm.update(dt)
26
+
27
+ # (Future) Update Behavior Tree if present
28
+ # if ai.behavior_tree:
29
+ # ai.behavior_tree.tick(dt)
@@ -0,0 +1,32 @@
1
+ """Blackboard system for sharing data between AI nodes and states."""
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ class Blackboard:
7
+ """
8
+ A shared memory container for AI decision making.
9
+
10
+ Allows different parts of an AI (e.g., nodes in a Behavior Tree or states
11
+ in an FSM) to read and write shared data without tight coupling.
12
+ """
13
+
14
+ def __init__(self) -> None:
15
+ """Initialize the blackboard."""
16
+ self._data: Dict[str, Any] = {}
17
+
18
+ def set(self, key: str, value: Any) -> None:
19
+ """Set a value in the blackboard."""
20
+ self._data[key] = value
21
+
22
+ def get(self, key: str, default: Any = None) -> Any:
23
+ """Retrieve a value from the blackboard."""
24
+ return self._data.get(key, default)
25
+
26
+ def has(self, key: str) -> bool:
27
+ """Check if a key exists."""
28
+ return key in self._data
29
+
30
+ def clear(self) -> None:
31
+ """Clear all data."""
32
+ self._data.clear()
@@ -0,0 +1,76 @@
1
+ """ECS components for AI."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Optional, List
5
+
6
+ from pyguara.common.types import Vector2
7
+ from pyguara.ai.blackboard import Blackboard
8
+ from pyguara.ai.fsm import StateMachine
9
+ from pyguara.ecs.component import BaseComponent
10
+
11
+
12
+ @dataclass
13
+ class AIComponent(BaseComponent):
14
+ """
15
+ Component that holds the AI brain (FSM or Behavior Tree).
16
+
17
+ Attributes:
18
+ blackboard: Shared memory for this agent.
19
+ fsm: Optional Finite State Machine.
20
+ enabled: Whether AI logic should run.
21
+ """
22
+
23
+ blackboard: Blackboard = field(default_factory=Blackboard)
24
+ fsm: Optional[StateMachine] = None
25
+ enabled: bool = True
26
+
27
+ def __post_init__(self) -> None:
28
+ """Call superclass init after initialization."""
29
+ super().__init__()
30
+
31
+
32
+ @dataclass
33
+ class SteeringAgent(BaseComponent):
34
+ """
35
+ Component that defines movement capabilities.
36
+
37
+ Attributes:
38
+ max_speed: Maximum movement speed.
39
+ max_force: Maximum steering force (turn speed/acceleration).
40
+ mass: Used to calculate acceleration (Force / Mass).
41
+ """
42
+
43
+ max_speed: float = 200.0
44
+ max_force: float = 500.0
45
+ mass: float = 1.0
46
+
47
+ def __post_init__(self) -> None:
48
+ """Call superclass init after initialization."""
49
+ super().__init__()
50
+
51
+
52
+ @dataclass
53
+ class Navigator(BaseComponent):
54
+ """
55
+ Component that handles pathfollowing.
56
+
57
+ Attributes:
58
+ path: Current list of waypoints.
59
+ current_index: Which waypoint we are moving toward.
60
+ reach_threshold: How close to get before switching to next waypoint.
61
+ """
62
+
63
+ path: List[Vector2] = field(default_factory=list)
64
+ current_index: int = 0
65
+ reach_threshold: float = 5.0
66
+
67
+ def set_path(self, path: List[Vector2]) -> None:
68
+ """Set the path defined by a list of vectors."""
69
+ self.path = path
70
+ self.current_index = 0
71
+
72
+ def get_current_target(self) -> Optional[Vector2]:
73
+ """Return the current imediate destination."""
74
+ if 0 <= self.current_index < len(self.path):
75
+ return self.path[self.current_index]
76
+ return None
pyguara/ai/fsm.py ADDED
@@ -0,0 +1,82 @@
1
+ """Finite State Machine (FSM) implementation."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Optional
5
+
6
+ from pyguara.ai.blackboard import Blackboard
7
+ from pyguara.ecs.entity import Entity
8
+
9
+
10
+ class State(ABC):
11
+ """Abstract base class for FSM states."""
12
+
13
+ def __init__(self, entity: Entity, blackboard: Blackboard):
14
+ """Initialize the state of an entity and a Blackboard."""
15
+ self.entity = entity
16
+ self.blackboard = blackboard
17
+
18
+ @abstractmethod
19
+ def on_enter(self) -> None:
20
+ """Call when entering this state."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def on_exit(self) -> None:
25
+ """Call when exiting this state."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def update(self, dt: float) -> Optional[str]:
30
+ """
31
+ Update the state logic.
32
+
33
+ Returns:
34
+ Name of the next state to transition to, or None to stay.
35
+ """
36
+ pass
37
+
38
+
39
+ class StateMachine:
40
+ """Manages states and transitions for an entity."""
41
+
42
+ def __init__(self, entity: Entity, blackboard: Blackboard):
43
+ """Initialize the State Machine of an entity and a Blackboard."""
44
+ self.entity = entity
45
+ self.blackboard = blackboard
46
+ self._states: Dict[str, State] = {}
47
+ self._current_state: Optional[State] = None
48
+ self._current_state_name: str = ""
49
+
50
+ def add_state(self, name: str, state: State) -> None:
51
+ """Register a state instance."""
52
+ self._states[name] = state
53
+
54
+ def set_initial_state(self, name: str) -> None:
55
+ """Set the starting state."""
56
+ if name in self._states:
57
+ self._current_state = self._states[name]
58
+ self._current_state_name = name
59
+ self._current_state.on_enter()
60
+
61
+ def update(self, dt: float) -> None:
62
+ """Update current state and handle transitions."""
63
+ if not self._current_state:
64
+ return
65
+
66
+ # Update returns potential transition
67
+ next_state_name = self._current_state.update(dt)
68
+
69
+ if next_state_name and next_state_name != self._current_state_name:
70
+ self._transition_to(next_state_name)
71
+
72
+ def _transition_to(self, name: str) -> None:
73
+ """Execute transition logic."""
74
+ if name not in self._states:
75
+ return
76
+
77
+ if self._current_state:
78
+ self._current_state.on_exit()
79
+
80
+ self._current_state = self._states[name]
81
+ self._current_state_name = name
82
+ self._current_state.on_enter()
@@ -0,0 +1,58 @@
1
+ """Generic A* pathfinding algorithm implementation."""
2
+
3
+ import heapq
4
+ from typing import Dict, List, Optional, Tuple
5
+
6
+ from pyguara.ai.pathfinding.core import Graph, Heuristic, Node
7
+
8
+
9
+ class AStarPathfinder:
10
+ """Generic A* solver for any graph type."""
11
+
12
+ def find_path(
13
+ self, graph: Graph[Node], start: Node, goal: Node, heuristic: Heuristic[Node]
14
+ ) -> Optional[List[Node]]:
15
+ """
16
+ Calculate the shortest path from start to goal.
17
+
18
+ Optimized to reduce heap operations.
19
+ """
20
+ frontier: List[Tuple[float, Node]] = []
21
+ heapq.heappush(frontier, (0, start))
22
+
23
+ came_from: Dict[Node, Optional[Node]] = {start: None}
24
+ cost_so_far: Dict[Node, float] = {start: 0.0}
25
+
26
+ while frontier:
27
+ _, current = heapq.heappop(frontier)
28
+
29
+ if current == goal:
30
+ break
31
+
32
+ for next_node in graph.get_neighbors(current):
33
+ new_cost = cost_so_far[current] + graph.cost(current, next_node)
34
+
35
+ if next_node not in cost_so_far or new_cost < cost_so_far[next_node]:
36
+ cost_so_far[next_node] = new_cost
37
+ priority = new_cost + heuristic.estimate(next_node, goal)
38
+ heapq.heappush(frontier, (priority, next_node))
39
+ came_from[next_node] = current
40
+
41
+ return self._reconstruct_path(came_from, start, goal)
42
+
43
+ def _reconstruct_path(
44
+ self, came_from: Dict[Node, Optional[Node]], start: Node, goal: Node
45
+ ) -> Optional[List[Node]]:
46
+ """Rebuild the path from the came_from map."""
47
+ if goal not in came_from:
48
+ return None
49
+
50
+ current: Optional[Node] = goal
51
+ path = []
52
+
53
+ while current is not None:
54
+ path.append(current)
55
+ current = came_from[current]
56
+
57
+ path.reverse()
58
+ return path
@@ -0,0 +1,29 @@
1
+ """Core abstractions for pathfinding protocols."""
2
+
3
+ from typing import Protocol, Iterator, TypeVar, Hashable
4
+
5
+ # Generic type for a Node (invariant for Graphs as they produce and consume)
6
+ Node = TypeVar("Node", bound=Hashable)
7
+
8
+ # Contravariant type for Heuristics (they only consume nodes)
9
+ NodeContra = TypeVar("NodeContra", bound=Hashable, contravariant=True)
10
+
11
+
12
+ class Graph(Protocol[Node]):
13
+ """Interface for a navigation graph."""
14
+
15
+ def get_neighbors(self, node: Node) -> Iterator[Node]:
16
+ """Yield the neighbors of the given node."""
17
+ ...
18
+
19
+ def cost(self, from_node: Node, to_node: Node) -> float:
20
+ """Calculate the movement cost between two adjacent nodes."""
21
+ ...
22
+
23
+
24
+ class Heuristic(Protocol[NodeContra]):
25
+ """Interface for heuristic functions."""
26
+
27
+ def estimate(self, current: NodeContra, goal: NodeContra) -> float:
28
+ """Estimate the remaining cost to the goal (h_score)."""
29
+ ...
@@ -0,0 +1,72 @@
1
+ """Grid-based graph implementation."""
2
+
3
+ import math
4
+ from typing import Iterator, Set, Tuple
5
+
6
+ from pyguara.ai.pathfinding.core import Graph, Heuristic
7
+
8
+ # GridNode is just an (x, y) tuple for efficiency
9
+ GridNode = Tuple[int, int]
10
+
11
+
12
+ class GridGraph(Graph[GridNode]):
13
+ """A 2D grid graph supporting 4 or 8 directional movement."""
14
+
15
+ def __init__(self, width: int, height: int, allow_diagonal: bool = True):
16
+ """Initialize the grid graph."""
17
+ self.width = width
18
+ self.height = height
19
+ self.allow_diagonal = allow_diagonal
20
+ self.walls: Set[GridNode] = set()
21
+ self.weights: dict[GridNode, float] = {} # For terrain costs
22
+
23
+ def in_bounds(self, node: GridNode) -> bool:
24
+ """Check if node is within grid limits."""
25
+ x, y = node
26
+ return 0 <= x < self.width and 0 <= y < self.height
27
+
28
+ def is_passable(self, node: GridNode) -> bool:
29
+ """Check if node is not a wall."""
30
+ return node not in self.walls
31
+
32
+ def get_neighbors(self, node: GridNode) -> Iterator[GridNode]:
33
+ """Yield valid neighbors."""
34
+ x, y = node
35
+ # Standard 4 directions
36
+ dirs = [(1, 0), (0, 1), (-1, 0), (0, -1)]
37
+
38
+ if self.allow_diagonal:
39
+ # Add diagonals
40
+ dirs.extend([(1, 1), (1, -1), (-1, 1), (-1, -1)])
41
+
42
+ for dx, dy in dirs:
43
+ next_node = (x + dx, y + dy)
44
+ if self.in_bounds(next_node) and self.is_passable(next_node):
45
+ yield next_node
46
+
47
+ def cost(self, from_node: GridNode, to_node: GridNode) -> float:
48
+ """Calculate movement cost, 1.0 orthogonal, 1.414 diagonal."""
49
+ base_cost = self.weights.get(to_node, 1.0)
50
+
51
+ # Heuristic check for diagonal movement
52
+ dx = abs(from_node[0] - to_node[0])
53
+ dy = abs(from_node[1] - to_node[1])
54
+
55
+ multiplier = 1.414 if (dx + dy) == 2 else 1.0
56
+ return base_cost * multiplier
57
+
58
+
59
+ class ManhattanDistance(Heuristic[GridNode]):
60
+ """Manhattan distance heuristic (better for 4-way movement)."""
61
+
62
+ def estimate(self, current: GridNode, goal: GridNode) -> float:
63
+ """Estimate the Manhattan distance."""
64
+ return abs(current[0] - goal[0]) + abs(current[1] - goal[1])
65
+
66
+
67
+ class EuclideanDistance(Heuristic[GridNode]):
68
+ """Euclidean distance heuristic (better for 8-way/any angle)."""
69
+
70
+ def estimate(self, current: GridNode, goal: GridNode) -> float:
71
+ """Estimate the Euclidean distance."""
72
+ return math.hypot(goal[0] - current[0], goal[1] - current[1])
pyguara/ai/steering.py ADDED
@@ -0,0 +1,55 @@
1
+ """Steering behaviors for autonomous movement."""
2
+
3
+ from typing import cast
4
+
5
+ from pyguara.common.components import Transform
6
+ from pyguara.common.types import Vector2
7
+
8
+
9
+ class SteeringBehavior:
10
+ """Calculates forces to move an entity in a natural way."""
11
+
12
+ @staticmethod
13
+ def seek(
14
+ transform: Transform,
15
+ target: Vector2,
16
+ max_speed: float,
17
+ current_velocity: Vector2,
18
+ ) -> Vector2:
19
+ """Calculate steering force to move toward a target."""
20
+ # FIX: Cast the result of vector math to Vector2
21
+ desired = cast(Vector2, (target - transform.position).normalized() * max_speed)
22
+ return desired - current_velocity
23
+
24
+ @staticmethod
25
+ def arrive(
26
+ transform: Transform,
27
+ target: Vector2,
28
+ max_speed: float,
29
+ current_velocity: Vector2,
30
+ slowing_radius: float = 100.0,
31
+ ) -> Vector2:
32
+ """
33
+ Calculate steering force to arrive at a target and stop.
34
+
35
+ Args:
36
+ transform: Entity transform.
37
+ target: Target position.
38
+ max_speed: Max movement speed.
39
+ current_velocity: Current entity velocity.
40
+ slowing_radius: Distance at which to start slowing down.
41
+ """
42
+ direction = target - transform.position
43
+ distance = direction.length
44
+
45
+ if distance < 0.1:
46
+ # Stop completely - return inverse of current velocity to cancel it out
47
+ return -current_velocity
48
+
49
+ if distance < slowing_radius:
50
+ target_speed = max_speed * (distance / slowing_radius)
51
+ else:
52
+ target_speed = max_speed
53
+
54
+ desired = cast(Vector2, direction.normalized() * target_speed)
55
+ return desired - current_velocity
pyguara/app_scene.py ADDED
@@ -0,0 +1,132 @@
1
+ """Main application entry point and engine integration."""
2
+
3
+ import pygame
4
+
5
+ from pyguara.common.components import Transform
6
+ from pyguara.common.types import Vector2
7
+ from pyguara.ecs.manager import EntityManager
8
+ from pyguara.events.dispatcher import EventDispatcher
9
+ from pyguara.graphics.backends.pygame.ui_renderer import PygameUIRenderer
10
+ from pyguara.graphics.backends.pygame.pygame_renderer import PygameBackend
11
+ from pyguara.graphics.window import Window, WindowConfig
12
+ from pyguara.graphics.backends.pygame.pygame_window import PygameWindow
13
+ from pyguara.input.manager import InputManager
14
+ from pyguara.physics.backends.pymunk_impl import PymunkEngine
15
+ from pyguara.physics.components import RigidBody, Collider
16
+ from pyguara.physics.physics_system import PhysicsSystem
17
+ from pyguara.physics.types import BodyType, ShapeType
18
+ from pyguara.scene.manager import SceneManager
19
+ from pyguara.game.scenes import GameplayScene
20
+ from pyguara.ui.components import Label, Panel
21
+ from pyguara.ui.manager import UIManager
22
+
23
+
24
+ class GameEngine:
25
+ """Core game engine class managing the main loop and subsystems."""
26
+
27
+ def __init__(self) -> None:
28
+ """Initialize the game engine, window, and subsystems."""
29
+ # 1. Window System
30
+ window_config = WindowConfig()
31
+ pygame_window = PygameWindow()
32
+ self.window = Window(window_config, pygame_window)
33
+ self.window.create()
34
+
35
+ self.clock = pygame.time.Clock()
36
+
37
+ # 2. Core Services
38
+ self.event_dispatcher = EventDispatcher()
39
+ self.input_manager = InputManager(self.event_dispatcher)
40
+
41
+ # 3. Subsystems
42
+ self.entity_manager = EntityManager()
43
+
44
+ self.physics_engine = PymunkEngine()
45
+ self.physics_system = PhysicsSystem(self.physics_engine, self.event_dispatcher)
46
+
47
+ # 4. Rendering & UI
48
+ self.world_renderer = PygameBackend(self.window.native_handle)
49
+ self.ui_renderer = PygameUIRenderer(self.window.native_handle)
50
+ self.ui_manager = UIManager(self.event_dispatcher)
51
+
52
+ # 5. Scene Management
53
+ self.scene_manager = SceneManager()
54
+
55
+ # Initialize Scene
56
+ game_scene = GameplayScene("game", self.event_dispatcher)
57
+ self.scene_manager.register(game_scene)
58
+ self.scene_manager.switch_to("game")
59
+
60
+ # Setup Test Entities (Fallback if scene not used)
61
+ self._setup_world()
62
+ self._setup_ui()
63
+
64
+ def _setup_world(self) -> None:
65
+ """Create initial game entities (Test)."""
66
+ player = self.entity_manager.create_entity("player_test")
67
+ player.add_component(Transform(position=Vector2(100, 100)))
68
+ player.add_component(RigidBody(body_type=BodyType.DYNAMIC, mass=1.0))
69
+ # Fixed dimensions
70
+ player.add_component(Collider(shape_type=ShapeType.BOX, dimensions=[32, 32]))
71
+
72
+ floor = self.entity_manager.create_entity("floor_test")
73
+ floor.add_component(Transform(position=Vector2(640, 600)))
74
+ floor.add_component(RigidBody(body_type=BodyType.STATIC))
75
+ floor.add_component(Collider(shape_type=ShapeType.BOX, dimensions=[1280, 50]))
76
+
77
+ def _setup_ui(self) -> None:
78
+ """Configure the initial UI layout (Test)."""
79
+ panel = Panel(position=Vector2(10, 200), size=Vector2(200, 50))
80
+ self.ui_manager.add_element(panel)
81
+
82
+ self.fps_label = Label("Engine FPS: 0", position=Vector2(10, 10))
83
+ panel.add_child(self.fps_label)
84
+
85
+ def run(self) -> None:
86
+ """Start the main game loop."""
87
+ while self.window.is_open:
88
+ dt = self.clock.tick(60) / 1000.0
89
+
90
+ # 1. Process Input
91
+ for event in self.window.poll_events():
92
+ self.input_manager.process_event(event)
93
+
94
+ # 2. Update Logic
95
+ self.update(dt)
96
+
97
+ # 3. Render
98
+ self.render()
99
+
100
+ self.window.close()
101
+
102
+ def update(self, dt: float) -> None:
103
+ """Update game state."""
104
+ # Update Global UI
105
+ self.ui_manager.update(dt)
106
+ self.fps_label.set_text(f"Engine FPS: {self.clock.get_fps():.0f}")
107
+
108
+ # Update Global Physics (Test entities)
109
+ physics_entities = list(
110
+ self.entity_manager.get_entities_with(Transform, RigidBody)
111
+ )
112
+ self.physics_system.update(physics_entities, dt)
113
+
114
+ # Update Scenes
115
+ self.scene_manager.update(dt)
116
+
117
+ def render(self) -> None:
118
+ """Render the current frame."""
119
+ # self.window.clear(Color(30, 30, 30))
120
+
121
+ # Render Scene
122
+ self.scene_manager.render(self.world_renderer, self.ui_renderer)
123
+
124
+ # Render Global UI
125
+ self.ui_manager.render(self.ui_renderer)
126
+
127
+ self.window.present()
128
+
129
+
130
+ if __name__ == "__main__":
131
+ app = GameEngine()
132
+ app.run()
@@ -0,0 +1,6 @@
1
+ """Application domain."""
2
+
3
+ from pyguara.application.application import Application
4
+ from pyguara.application.bootstrap import create_application, create_sandbox_application
5
+
6
+ __all__ = ["Application", "create_application", "create_sandbox_application"]
@@ -0,0 +1,100 @@
1
+ """Main application runtime."""
2
+
3
+ import pygame
4
+
5
+ from pyguara.config.manager import ConfigManager
6
+ from pyguara.di.container import DIContainer
7
+ from pyguara.events.dispatcher import EventDispatcher
8
+ from pyguara.graphics.protocols import UIRenderer, IRenderer
9
+ from pyguara.graphics.window import Window
10
+ from pyguara.input.manager import InputManager
11
+ from pyguara.scene.base import Scene
12
+ from pyguara.scene.manager import SceneManager
13
+ from pyguara.ui.manager import UIManager
14
+
15
+
16
+ class Application:
17
+ """The main runtime loop coordinator."""
18
+
19
+ def __init__(self, container: DIContainer) -> None:
20
+ """Initialize Application with a DI container."""
21
+ self._container = container
22
+ self._is_running = False
23
+
24
+ # Resolve Core Dependencies
25
+ self._window = container.get(Window)
26
+ self._event_dispatcher = container.get(EventDispatcher)
27
+ self._input_manager = container.get(InputManager)
28
+ self._scene_manager = container.get(SceneManager)
29
+ self._config_manager = container.get(ConfigManager)
30
+ self._ui_manager = container.get(UIManager)
31
+
32
+ # Retrieve Renderer
33
+ self._world_renderer = container.get(IRenderer) # type: ignore[type-abstract]
34
+ self._ui_renderer = container.get(UIRenderer) # type: ignore[type-abstract]
35
+
36
+ self._scene_manager.set_container(container)
37
+
38
+ self._clock = pygame.time.Clock()
39
+
40
+ def run(self, starting_scene: Scene) -> None:
41
+ """Execute the main game loop."""
42
+ print(f"[Application] Starting with scene: {starting_scene.name}")
43
+
44
+ self._scene_manager.register(starting_scene)
45
+ self._scene_manager.switch_to(starting_scene.name)
46
+
47
+ self._is_running = True
48
+ target_fps = self._config_manager.config.display.fps_target
49
+
50
+ # Force an initial event pump to show the window immediately
51
+ pygame.event.pump()
52
+
53
+ while self._is_running and self._window.is_open:
54
+ # 1. Time
55
+ dt = self._clock.tick(target_fps) / 1000.0
56
+
57
+ # 2. Input
58
+ self._process_input()
59
+
60
+ # 3. Update
61
+ self._update(dt)
62
+
63
+ # 4. Render
64
+ self._render()
65
+
66
+ self.shutdown()
67
+
68
+ def _process_input(self) -> None:
69
+ """Poll system events."""
70
+ # This call is CRITICAL. It keeps the OS window responsive.
71
+ for event in self._window.poll_events():
72
+ if hasattr(event, "type") and event.type == pygame.QUIT:
73
+ self._is_running = False
74
+
75
+ # Dispatch to input manager
76
+ self._input_manager.process_event(event)
77
+
78
+ def _update(self, dt: float) -> None:
79
+ """Update game logic."""
80
+ # 1. Process background thread events (CRITICAL FIX)
81
+ # We call the dispatcher to flush any pending queued events
82
+ self._event_dispatcher.process_queue()
83
+
84
+ # 2. Update UI
85
+ self._ui_manager.update(dt)
86
+
87
+ # 3. Update Scene (Physics, Logic)
88
+ self._scene_manager.update(dt)
89
+
90
+ def _render(self) -> None:
91
+ """Render frame."""
92
+ self._window.clear()
93
+ self._scene_manager.render(self._world_renderer, self._ui_renderer)
94
+ self._ui_manager.render(self._ui_renderer)
95
+ self._window.present()
96
+
97
+ def shutdown(self) -> None:
98
+ """Close Application."""
99
+ print("[Application] Shutting down...")
100
+ self._window.close()