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.
- pyguara/__init__.py +0 -0
- pyguara/ai/ai_system.py +29 -0
- pyguara/ai/blackboard.py +32 -0
- pyguara/ai/components.py +76 -0
- pyguara/ai/fsm.py +82 -0
- pyguara/ai/pathfinding/astar.py +58 -0
- pyguara/ai/pathfinding/core.py +29 -0
- pyguara/ai/pathfinding/grid.py +72 -0
- pyguara/ai/steering.py +55 -0
- pyguara/app_scene.py +132 -0
- pyguara/application/__init__.py +6 -0
- pyguara/application/application.py +100 -0
- pyguara/application/bootstrap.py +132 -0
- pyguara/application/sandbox.py +116 -0
- pyguara/audio/__init__.py +1 -0
- pyguara/audio/audio_system.py +116 -0
- pyguara/audio/backends/pygame/loaders.py +30 -0
- pyguara/audio/backends/pygame/pygame_audio.py +185 -0
- pyguara/audio/backends/pygame/types.py +37 -0
- pyguara/audio/manager.py +248 -0
- pyguara/common/components.py +316 -0
- pyguara/common/constants.py +1 -0
- pyguara/common/palette.py +43 -0
- pyguara/common/types.py +311 -0
- pyguara/config/__init__.py +14 -0
- pyguara/config/events.py +39 -0
- pyguara/config/manager.py +128 -0
- pyguara/config/types.py +82 -0
- pyguara/config/validation.py +76 -0
- pyguara/di/__init__.py +34 -0
- pyguara/di/container.py +369 -0
- pyguara/di/decorators.py +56 -0
- pyguara/di/exceptions.py +33 -0
- pyguara/di/types.py +70 -0
- pyguara/ecs/__init__.py +14 -0
- pyguara/ecs/component.py +40 -0
- pyguara/ecs/entity.py +158 -0
- pyguara/ecs/manager.py +195 -0
- pyguara/ecs/query_cache.py +197 -0
- pyguara/editor/__init__.py +1 -0
- pyguara/editor/drawers.py +187 -0
- pyguara/editor/layer.py +165 -0
- pyguara/editor/panels/assets.py +163 -0
- pyguara/editor/panels/hierarchy.py +64 -0
- pyguara/editor/panels/inspector.py +81 -0
- pyguara/error/__init__.py +24 -0
- pyguara/error/exceptions.py +107 -0
- pyguara/error/handlers.py +110 -0
- pyguara/error/types.py +27 -0
- pyguara/events/__init__.py +1 -0
- pyguara/events/dispatcher.py +161 -0
- pyguara/events/input.py +50 -0
- pyguara/events/lifecycle.py +31 -0
- pyguara/events/protocols.py +92 -0
- pyguara/events/types.py +40 -0
- pyguara/events/window.py +20 -0
- pyguara/graphics/__init__.py +1 -0
- pyguara/graphics/animation_system.py +37 -0
- pyguara/graphics/atlas.py +110 -0
- pyguara/graphics/backends/headless_renderer.py +115 -0
- pyguara/graphics/backends/pygame/loaders.py +27 -0
- pyguara/graphics/backends/pygame/pygame_renderer.py +113 -0
- pyguara/graphics/backends/pygame/pygame_window.py +75 -0
- pyguara/graphics/backends/pygame/types.py +34 -0
- pyguara/graphics/backends/pygame/ui_renderer.py +115 -0
- pyguara/graphics/color.py +1 -0
- pyguara/graphics/components/__init__.py +27 -0
- pyguara/graphics/components/animation.py +312 -0
- pyguara/graphics/components/camera.py +407 -0
- pyguara/graphics/components/geometry.py +150 -0
- pyguara/graphics/components/particles.py +387 -0
- pyguara/graphics/components/sprite.py +32 -0
- pyguara/graphics/pipeline/batch.py +99 -0
- pyguara/graphics/pipeline/queue.py +37 -0
- pyguara/graphics/pipeline/render_system.py +95 -0
- pyguara/graphics/pipeline/viewport.py +147 -0
- pyguara/graphics/protocols.py +318 -0
- pyguara/graphics/spritesheet.py +79 -0
- pyguara/graphics/types.py +66 -0
- pyguara/graphics/window.py +80 -0
- pyguara/input/__init__.py +21 -0
- pyguara/input/binding.py +42 -0
- pyguara/input/events.py +63 -0
- pyguara/input/gamepad.py +358 -0
- pyguara/input/keys.py +19 -0
- pyguara/input/manager.py +196 -0
- pyguara/input/types.py +114 -0
- pyguara/log/__init__.py +19 -0
- pyguara/log/events.py +31 -0
- pyguara/log/handlers.py +75 -0
- pyguara/log/logger.py +173 -0
- pyguara/log/manager.py +68 -0
- pyguara/log/types.py +30 -0
- pyguara/persistence/manager.py +136 -0
- pyguara/persistence/serializer.py +110 -0
- pyguara/persistence/storage.py +99 -0
- pyguara/persistence/types.py +77 -0
- pyguara/physics/__init__.py +17 -0
- pyguara/physics/backends/pymunk_impl.py +448 -0
- pyguara/physics/collision_system.py +295 -0
- pyguara/physics/components.py +63 -0
- pyguara/physics/events.py +187 -0
- pyguara/physics/joints.py +349 -0
- pyguara/physics/materials.py +63 -0
- pyguara/physics/physics_system.py +109 -0
- pyguara/physics/protocols.py +138 -0
- pyguara/physics/types.py +103 -0
- pyguara/resources/data.py +69 -0
- pyguara/resources/loader.py +43 -0
- pyguara/resources/loaders/data_loader.py +40 -0
- pyguara/resources/manager.py +368 -0
- pyguara/resources/types.py +75 -0
- pyguara/scene/base.py +83 -0
- pyguara/scene/manager.py +55 -0
- pyguara/scene/serializer.py +93 -0
- pyguara/tools/base.py +80 -0
- pyguara/tools/debugger.py +80 -0
- pyguara/tools/event_monitor.py +83 -0
- pyguara/tools/inspector.py +166 -0
- pyguara/tools/manager.py +132 -0
- pyguara/tools/performance.py +59 -0
- pyguara/tools/shortcuts_panel.py +59 -0
- pyguara/ui/__init__.py +27 -0
- pyguara/ui/base.py +117 -0
- pyguara/ui/components/__init__.py +27 -0
- pyguara/ui/components/button.py +48 -0
- pyguara/ui/components/canvas.py +29 -0
- pyguara/ui/components/checkbox.py +60 -0
- pyguara/ui/components/image.py +27 -0
- pyguara/ui/components/label.py +35 -0
- pyguara/ui/components/navbar.py +33 -0
- pyguara/ui/components/panel.py +31 -0
- pyguara/ui/components/progress_bar.py +36 -0
- pyguara/ui/components/slider.py +84 -0
- pyguara/ui/components/text.py +46 -0
- pyguara/ui/components/text_input.py +77 -0
- pyguara/ui/components/widget.py +25 -0
- pyguara/ui/layout.py +87 -0
- pyguara/ui/manager.py +58 -0
- pyguara/ui/theme.py +37 -0
- pyguara/ui/types.py +113 -0
- pyguara-0.2.0.dist-info/METADATA +167 -0
- pyguara-0.2.0.dist-info/RECORD +145 -0
- pyguara-0.2.0.dist-info/WHEEL +4 -0
- pyguara-0.2.0.dist-info/licenses/LICENSE +8 -0
pyguara/__init__.py
ADDED
|
File without changes
|
pyguara/ai/ai_system.py
ADDED
|
@@ -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)
|
pyguara/ai/blackboard.py
ADDED
|
@@ -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()
|
pyguara/ai/components.py
ADDED
|
@@ -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,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()
|