mini-arcade-core 0.10.0__py3-none-any.whl → 1.0.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.
- mini_arcade_core/__init__.py +43 -60
- mini_arcade_core/backend/__init__.py +0 -5
- mini_arcade_core/backend/backend.py +9 -0
- mini_arcade_core/backend/events.py +1 -1
- mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
- mini_arcade_core/engine/__init__.py +0 -0
- mini_arcade_core/engine/commands.py +169 -0
- mini_arcade_core/engine/game.py +354 -0
- mini_arcade_core/engine/render/__init__.py +0 -0
- mini_arcade_core/engine/render/packet.py +56 -0
- mini_arcade_core/engine/render/pipeline.py +39 -0
- mini_arcade_core/managers/__init__.py +0 -22
- mini_arcade_core/managers/cheats.py +71 -240
- mini_arcade_core/managers/inputs.py +5 -1
- mini_arcade_core/runtime/__init__.py +0 -0
- mini_arcade_core/runtime/audio/__init__.py +0 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +13 -0
- mini_arcade_core/runtime/audio/audio_port.py +17 -0
- mini_arcade_core/runtime/capture/__init__.py +0 -0
- mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
- mini_arcade_core/runtime/capture/capture_port.py +32 -0
- mini_arcade_core/runtime/context.py +53 -0
- mini_arcade_core/runtime/file/__init__.py +0 -0
- mini_arcade_core/runtime/file/file_adapter.py +20 -0
- mini_arcade_core/runtime/file/file_port.py +31 -0
- mini_arcade_core/runtime/input/__init__.py +0 -0
- mini_arcade_core/runtime/input/input_adapter.py +49 -0
- mini_arcade_core/runtime/input/input_port.py +31 -0
- mini_arcade_core/runtime/input_frame.py +71 -0
- mini_arcade_core/runtime/scene/__init__.py +0 -0
- mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
- mini_arcade_core/runtime/scene/scene_port.py +149 -0
- mini_arcade_core/runtime/services.py +35 -0
- mini_arcade_core/runtime/window/__init__.py +0 -0
- mini_arcade_core/runtime/window/window_adapter.py +26 -0
- mini_arcade_core/runtime/window/window_port.py +47 -0
- mini_arcade_core/scenes/__init__.py +0 -22
- mini_arcade_core/scenes/autoreg.py +1 -1
- mini_arcade_core/scenes/registry.py +21 -19
- mini_arcade_core/scenes/sim_scene.py +41 -0
- mini_arcade_core/scenes/systems/__init__.py +0 -0
- mini_arcade_core/scenes/systems/base_system.py +40 -0
- mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
- mini_arcade_core/sim/__init__.py +0 -0
- mini_arcade_core/sim/protocols.py +41 -0
- mini_arcade_core/sim/runner.py +222 -0
- mini_arcade_core/spaces/__init__.py +0 -12
- mini_arcade_core/spaces/d2/__init__.py +0 -30
- mini_arcade_core/spaces/d2/collision2d.py +25 -28
- mini_arcade_core/spaces/d2/geometry2d.py +18 -0
- mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
- mini_arcade_core/spaces/d2/physics2d.py +9 -0
- mini_arcade_core/ui/__init__.py +0 -26
- mini_arcade_core/ui/menu.py +265 -84
- mini_arcade_core/utils/__init__.py +10 -0
- mini_arcade_core/utils/deprecated_decorator.py +45 -0
- mini_arcade_core/utils/logging.py +174 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.0.dist-info}/METADATA +1 -1
- mini_arcade_core-1.0.0.dist-info/RECORD +65 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.0.dist-info}/WHEEL +1 -1
- mini_arcade_core/commands.py +0 -84
- mini_arcade_core/entity.py +0 -72
- mini_arcade_core/game.py +0 -287
- mini_arcade_core/keymaps/__init__.py +0 -15
- mini_arcade_core/managers/base.py +0 -132
- mini_arcade_core/managers/entities.py +0 -38
- mini_arcade_core/managers/overlays.py +0 -53
- mini_arcade_core/managers/system.py +0 -26
- mini_arcade_core/scenes/model.py +0 -34
- mini_arcade_core/scenes/runtime.py +0 -29
- mini_arcade_core/scenes/scene.py +0 -109
- mini_arcade_core/scenes/system.py +0 -69
- mini_arcade_core/ui/overlays.py +0 -41
- mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
- /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
SimScene registry for mini arcade core.
|
|
3
3
|
Allows registering and creating scenes by string IDs.
|
|
4
4
|
"""
|
|
5
5
|
|
|
@@ -10,11 +10,13 @@ import pkgutil
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from typing import TYPE_CHECKING, Dict, Protocol
|
|
12
12
|
|
|
13
|
+
from mini_arcade_core.runtime.context import RuntimeContext
|
|
14
|
+
|
|
13
15
|
from .autoreg import snapshot
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
|
-
from mini_arcade_core.
|
|
17
|
-
from mini_arcade_core.
|
|
18
|
+
from mini_arcade_core.engine.commands import CommandQueue
|
|
19
|
+
from mini_arcade_core.sim import SimScene
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class SceneFactory(Protocol):
|
|
@@ -22,7 +24,7 @@ class SceneFactory(Protocol):
|
|
|
22
24
|
Protocol for scene factory callables.
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
|
-
def __call__(self,
|
|
27
|
+
def __call__(self, context: RuntimeContext) -> "SimScene": ...
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
@dataclass
|
|
@@ -40,28 +42,28 @@ class SceneRegistry:
|
|
|
40
42
|
:param scene_id: The string ID for the scene.
|
|
41
43
|
:type scene_id: str
|
|
42
44
|
|
|
43
|
-
:param factory: A callable that creates a
|
|
45
|
+
:param factory: A callable that creates a SimScene instance.
|
|
44
46
|
:type factory: SceneFactory
|
|
45
47
|
"""
|
|
46
48
|
self._factories[scene_id] = factory
|
|
47
49
|
|
|
48
|
-
def register_cls(self, scene_id: str, scene_cls: type["
|
|
50
|
+
def register_cls(self, scene_id: str, scene_cls: type["SimScene"]):
|
|
49
51
|
"""
|
|
50
|
-
Register a
|
|
52
|
+
Register a SimScene class under a given scene ID.
|
|
51
53
|
|
|
52
54
|
:param scene_id: The string ID for the scene.
|
|
53
55
|
:type scene_id: str
|
|
54
56
|
|
|
55
|
-
:param scene_cls: The
|
|
56
|
-
:type scene_cls: type["
|
|
57
|
+
:param scene_cls: The SimScene class to register.
|
|
58
|
+
:type scene_cls: type["SimScene"]
|
|
57
59
|
"""
|
|
58
60
|
|
|
59
|
-
def return_factory(
|
|
60
|
-
return scene_cls(
|
|
61
|
+
def return_factory(context: RuntimeContext) -> "SimScene":
|
|
62
|
+
return scene_cls(context)
|
|
61
63
|
|
|
62
64
|
self.register(scene_id, return_factory)
|
|
63
65
|
|
|
64
|
-
def create(self, scene_id: str,
|
|
66
|
+
def create(self, scene_id: str, context: RuntimeContext) -> "SimScene":
|
|
65
67
|
"""
|
|
66
68
|
Create a scene instance using the registered factory for the given scene ID.
|
|
67
69
|
|
|
@@ -71,22 +73,22 @@ class SceneRegistry:
|
|
|
71
73
|
:param game: The Game instance to pass to the scene factory.
|
|
72
74
|
:type game: Game
|
|
73
75
|
|
|
74
|
-
:return: A new
|
|
75
|
-
:rtype:
|
|
76
|
+
:return: A new SimScene instance.
|
|
77
|
+
:rtype: SimScene
|
|
76
78
|
|
|
77
79
|
:raises KeyError: If no factory is registered for the given scene ID.
|
|
78
80
|
"""
|
|
79
81
|
try:
|
|
80
|
-
return self._factories[scene_id](
|
|
82
|
+
return self._factories[scene_id](context)
|
|
81
83
|
except KeyError as e:
|
|
82
84
|
raise KeyError(f"Unknown scene_id={scene_id!r}") from e
|
|
83
85
|
|
|
84
|
-
def load_catalog(self, catalog: Dict[str, type["
|
|
86
|
+
def load_catalog(self, catalog: Dict[str, type["SimScene"]]):
|
|
85
87
|
"""
|
|
86
|
-
Load a catalog of
|
|
88
|
+
Load a catalog of SimScene classes into the registry.
|
|
87
89
|
|
|
88
|
-
:param catalog: A dictionary mapping scene IDs to
|
|
89
|
-
:type catalog: Dict[str, type["
|
|
90
|
+
:param catalog: A dictionary mapping scene IDs to SimScene classes.
|
|
91
|
+
:type catalog: Dict[str, type["SimScene"]]
|
|
90
92
|
"""
|
|
91
93
|
for scene_id, cls in catalog.items():
|
|
92
94
|
self.register_cls(scene_id, cls)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation scene protocol module.
|
|
3
|
+
Defines the SimScene protocol for simulation scenes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
11
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class SimScene(Protocol):
|
|
16
|
+
""" "Protocol for a simulation scene in the mini arcade core."""
|
|
17
|
+
|
|
18
|
+
def on_enter(self):
|
|
19
|
+
"""Called when the scene is entered."""
|
|
20
|
+
|
|
21
|
+
def on_exit(self):
|
|
22
|
+
"""Called when the scene is exited."""
|
|
23
|
+
|
|
24
|
+
def tick(self, input_frame: InputFrame, dt: float):
|
|
25
|
+
"""
|
|
26
|
+
Advance the simulation by one tick.
|
|
27
|
+
|
|
28
|
+
:param input_frame: Current input frame.
|
|
29
|
+
:type input_frame: InputFrame
|
|
30
|
+
|
|
31
|
+
:param dt: Delta time since last tick.
|
|
32
|
+
:type dt: float
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def build_render_packet(self) -> RenderPacket:
|
|
36
|
+
"""
|
|
37
|
+
Build the render packet for the current scene state.
|
|
38
|
+
|
|
39
|
+
:return: RenderPacket instance.
|
|
40
|
+
:rtype: RenderPacket
|
|
41
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocol for base systems in the mini arcade core.
|
|
3
|
+
Defines the BaseSystem protocol that all systems should implement.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Generic, Protocol, TypeVar, runtime_checkable
|
|
9
|
+
|
|
10
|
+
# Justification: Type variable name is conventional.
|
|
11
|
+
# pylint: disable=invalid-name
|
|
12
|
+
TSystemContext = TypeVar("TSystemContext")
|
|
13
|
+
# pylint: enable=invalid-name
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@runtime_checkable
|
|
17
|
+
class BaseSystem(Protocol, Generic[TSystemContext]):
|
|
18
|
+
"""Protocol for a system that operates within a given context."""
|
|
19
|
+
|
|
20
|
+
name: str
|
|
21
|
+
order: int
|
|
22
|
+
|
|
23
|
+
def enabled(self, ctx: TSystemContext) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Determine if the system is enabled in the given context.
|
|
26
|
+
|
|
27
|
+
:param ctx: The system context.
|
|
28
|
+
:type ctx: TSystemContext
|
|
29
|
+
|
|
30
|
+
:return: True if the system is enabled, False otherwise.
|
|
31
|
+
:rtype: bool
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def step(self, ctx: TSystemContext):
|
|
35
|
+
"""
|
|
36
|
+
Perform a single step of the system within the given context.
|
|
37
|
+
|
|
38
|
+
:param ctx: The system context.
|
|
39
|
+
:type ctx: TSystemContext
|
|
40
|
+
"""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pipeline for managing and executing systems in order.
|
|
3
|
+
Defines the SystemPipeline dataclass that holds and runs systems.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Generic, Iterable, List
|
|
10
|
+
|
|
11
|
+
from mini_arcade_core.scenes.systems.base_system import (
|
|
12
|
+
BaseSystem,
|
|
13
|
+
TSystemContext,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SystemPipeline(Generic[TSystemContext]):
|
|
19
|
+
"""
|
|
20
|
+
Pipeline for managing and executing systems in order.
|
|
21
|
+
|
|
22
|
+
:ivar systems (List[BaseSystem[TSystemContext]]): List of systems in the pipeline.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
systems: List[BaseSystem[TSystemContext]] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
def add(self, system: BaseSystem[TSystemContext]):
|
|
28
|
+
"""
|
|
29
|
+
Add a system to the pipeline and sort by order.
|
|
30
|
+
|
|
31
|
+
:param system: The system to add.
|
|
32
|
+
:type system: BaseSystem[TSystemContext]
|
|
33
|
+
"""
|
|
34
|
+
self.systems.append(system)
|
|
35
|
+
self.systems.sort(key=lambda s: getattr(s, "order", 0))
|
|
36
|
+
|
|
37
|
+
def extend(self, systems: Iterable[BaseSystem[TSystemContext]]):
|
|
38
|
+
"""
|
|
39
|
+
Extend the pipeline with multiple systems.
|
|
40
|
+
|
|
41
|
+
:param systems: An iterable of systems to add.
|
|
42
|
+
:type systems: Iterable[BaseSystem[TSystemContext]]
|
|
43
|
+
"""
|
|
44
|
+
for s in systems:
|
|
45
|
+
self.add(s)
|
|
46
|
+
|
|
47
|
+
def step(self, ctx: TSystemContext):
|
|
48
|
+
"""
|
|
49
|
+
Execute a step for each system in the pipeline.
|
|
50
|
+
|
|
51
|
+
:param ctx: The system context.
|
|
52
|
+
:type ctx: TSystemContext
|
|
53
|
+
"""
|
|
54
|
+
for system in self.systems:
|
|
55
|
+
if hasattr(system, "enabled") and not system.enabled(ctx):
|
|
56
|
+
continue
|
|
57
|
+
system.step(ctx)
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation scene protocol module.
|
|
3
|
+
Defines the SimScene protocol for simulation scenes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
11
|
+
from mini_arcade_core.runtime.context import RuntimeContext
|
|
12
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class SimScene:
|
|
17
|
+
"""
|
|
18
|
+
Simulation-first scene protocol.
|
|
19
|
+
|
|
20
|
+
tick() advances the simulation and returns a RenderPacket for this scene.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
context: RuntimeContext
|
|
24
|
+
|
|
25
|
+
def on_enter(self):
|
|
26
|
+
"""Called when the scene is entered."""
|
|
27
|
+
|
|
28
|
+
def on_exit(self):
|
|
29
|
+
"""Called when the scene is exited."""
|
|
30
|
+
|
|
31
|
+
def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
|
|
32
|
+
"""
|
|
33
|
+
Advance the simulation by dt seconds, processing input_frame.
|
|
34
|
+
|
|
35
|
+
:param input_frame: InputFrame with input events for this frame.
|
|
36
|
+
:param dt: Time delta in seconds since the last tick.
|
|
37
|
+
|
|
38
|
+
:return: RenderPacket for this frame.
|
|
39
|
+
:rtype: RenderPacket
|
|
40
|
+
"""
|
|
41
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation runner module.
|
|
3
|
+
Defines the SimRunner class for running simulation scenes.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from time import perf_counter, sleep
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
from mini_arcade_core.backend import Backend
|
|
13
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
14
|
+
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
15
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
16
|
+
from mini_arcade_core.runtime.scene.scene_port import SceneEntry
|
|
17
|
+
from mini_arcade_core.runtime.services import RuntimeServices
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _neutral_input(frame_index: int, dt: float) -> InputFrame:
|
|
21
|
+
# InputFrame is frozen; create a clean snapshot for non-input scenes.
|
|
22
|
+
return InputFrame(frame_index=frame_index, dt=dt)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _has_tick(scene: object) -> bool:
|
|
26
|
+
# Avoid isinstance(..., Protocol). Structural check.
|
|
27
|
+
return callable(getattr(scene, "tick", None))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _has_draw(scene: object) -> bool:
|
|
31
|
+
return callable(getattr(scene, "draw", None))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _has_update(scene: object) -> bool:
|
|
35
|
+
return callable(getattr(scene, "update", None))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _has_handle_event(scene: object) -> bool:
|
|
39
|
+
return callable(getattr(scene, "handle_event", None))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class SimRunnerConfig:
|
|
44
|
+
"""
|
|
45
|
+
Config for sim runner.
|
|
46
|
+
|
|
47
|
+
- record: if True, capture a frame each tick using deterministic naming.
|
|
48
|
+
- run_id: required when record=True.
|
|
49
|
+
- max_frames: optional safety stop (useful for offline sims/tests).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
fps: int = 60
|
|
53
|
+
record: bool = False
|
|
54
|
+
run_id: str = "run"
|
|
55
|
+
max_frames: Optional[int] = None
|
|
56
|
+
# If True, still forward raw events to the input scene's handle_event (legacy UI / text input).
|
|
57
|
+
forward_events_to_input_scene: bool = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SimRunner:
|
|
61
|
+
"""
|
|
62
|
+
Simulation-first runner.
|
|
63
|
+
|
|
64
|
+
Uses:
|
|
65
|
+
- services.scenes.update_entries() for ticking (policy-aware)
|
|
66
|
+
- services.scenes.visible_entries() for rendering (opaque-aware)
|
|
67
|
+
- services.scenes.input_entry() for input focus
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
backend: Backend,
|
|
73
|
+
services: RuntimeServices,
|
|
74
|
+
*,
|
|
75
|
+
render_pipeline: Optional[RenderPipeline] = None,
|
|
76
|
+
):
|
|
77
|
+
if services.scenes is None:
|
|
78
|
+
raise ValueError("RuntimeServices.scenes must be set")
|
|
79
|
+
if services.input is None:
|
|
80
|
+
raise ValueError("RuntimeServices.input must be set")
|
|
81
|
+
if services.capture is None:
|
|
82
|
+
# recording is optional, but capture port should exist in v1
|
|
83
|
+
raise ValueError("RuntimeServices.capture must be set")
|
|
84
|
+
|
|
85
|
+
self.backend = backend
|
|
86
|
+
self.services = services
|
|
87
|
+
self.pipeline = render_pipeline or RenderPipeline()
|
|
88
|
+
|
|
89
|
+
# cache: scene object id -> last RenderPacket
|
|
90
|
+
self._packets: Dict[int, RenderPacket] = {}
|
|
91
|
+
|
|
92
|
+
self._running: bool = False
|
|
93
|
+
|
|
94
|
+
def stop(self):
|
|
95
|
+
"""
|
|
96
|
+
Stop the simulation loop.
|
|
97
|
+
"""
|
|
98
|
+
self._running = False
|
|
99
|
+
|
|
100
|
+
# TODO: Solve too-many-statements, too-many-branches and too-many-locals
|
|
101
|
+
# warning later
|
|
102
|
+
# Justification: The run method orchestrates multiple complex steps in the
|
|
103
|
+
# simulation loop.
|
|
104
|
+
# pylint: disable=too-many-statements,too-many-branches,too-many-locals
|
|
105
|
+
def run(
|
|
106
|
+
self, initial_scene_id: str, *, cfg: Optional[SimRunnerConfig] = None
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Run the simulation loop starting from the initial scene.
|
|
110
|
+
|
|
111
|
+
:param initial_scene_id: ID of the initial scene to load.
|
|
112
|
+
:type initial_scene_id: str
|
|
113
|
+
|
|
114
|
+
:param cfg: Optional SimRunnerConfig instance.
|
|
115
|
+
:type cfg: Optional[SimRunnerConfig]
|
|
116
|
+
"""
|
|
117
|
+
cfg = cfg or SimRunnerConfig()
|
|
118
|
+
|
|
119
|
+
scenes = self.services.scenes
|
|
120
|
+
assert scenes is not None
|
|
121
|
+
|
|
122
|
+
# start at initial scene
|
|
123
|
+
scenes.change(initial_scene_id)
|
|
124
|
+
|
|
125
|
+
self._running = True
|
|
126
|
+
target_dt = 1.0 / cfg.fps if cfg.fps > 0 else 0.0
|
|
127
|
+
|
|
128
|
+
last_time = perf_counter()
|
|
129
|
+
frame_index = 0
|
|
130
|
+
|
|
131
|
+
while self._running:
|
|
132
|
+
if cfg.max_frames is not None and frame_index >= cfg.max_frames:
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
now = perf_counter()
|
|
136
|
+
dt = now - last_time
|
|
137
|
+
last_time = now
|
|
138
|
+
|
|
139
|
+
# 1) poll events -> build InputFrame
|
|
140
|
+
events = list(self.backend.poll_events())
|
|
141
|
+
input_frame = self.services.input.build(events, frame_index, dt)
|
|
142
|
+
|
|
143
|
+
# 2) OS quit request is a hard stop
|
|
144
|
+
if input_frame.quit:
|
|
145
|
+
# use ScenePort.quit so Game.quit can be centralized there
|
|
146
|
+
scenes.quit()
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
# 3) input focus scene (top of visible stack)
|
|
150
|
+
input_entry: Optional[SceneEntry] = scenes.input_entry()
|
|
151
|
+
if input_entry is None:
|
|
152
|
+
break
|
|
153
|
+
|
|
154
|
+
# Optional legacy: forward raw events to focused scene
|
|
155
|
+
if cfg.forward_events_to_input_scene and _has_handle_event(
|
|
156
|
+
input_entry.scene
|
|
157
|
+
):
|
|
158
|
+
for ev in events:
|
|
159
|
+
input_entry.scene.handle_event(ev)
|
|
160
|
+
|
|
161
|
+
# 4) tick/update policy-aware scenes
|
|
162
|
+
for entry in scenes.update_entries():
|
|
163
|
+
scene_obj = entry.scene
|
|
164
|
+
scene_key = id(scene_obj)
|
|
165
|
+
|
|
166
|
+
# Only the input-focused scene receives the actual input_frame
|
|
167
|
+
effective_input = (
|
|
168
|
+
input_frame
|
|
169
|
+
if entry is input_entry
|
|
170
|
+
else _neutral_input(frame_index, dt)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if _has_tick(scene_obj):
|
|
174
|
+
packet = scene_obj.tick(effective_input, dt) # SimScene
|
|
175
|
+
if not isinstance(packet, RenderPacket):
|
|
176
|
+
raise TypeError(
|
|
177
|
+
f"{entry.scene_id}.tick() must "
|
|
178
|
+
f"return RenderPacket, got {type(packet)!r}"
|
|
179
|
+
)
|
|
180
|
+
self._packets[scene_key] = packet
|
|
181
|
+
elif _has_update(scene_obj):
|
|
182
|
+
# legacy scene; keep packet cache if any
|
|
183
|
+
scene_obj.update(dt)
|
|
184
|
+
|
|
185
|
+
# 5) render visible stack (policy-aware)
|
|
186
|
+
self.backend.begin_frame()
|
|
187
|
+
|
|
188
|
+
for entry in scenes.visible_entries():
|
|
189
|
+
scene_obj = entry.scene
|
|
190
|
+
scene_key = id(scene_obj)
|
|
191
|
+
|
|
192
|
+
if _has_tick(scene_obj):
|
|
193
|
+
packet = self._packets.get(scene_key)
|
|
194
|
+
# If first frame and no packet exists yet, do a dt=0 tick to bootstrap
|
|
195
|
+
if packet is None:
|
|
196
|
+
packet = scene_obj.tick(
|
|
197
|
+
_neutral_input(frame_index, 0.0), 0.0
|
|
198
|
+
)
|
|
199
|
+
self._packets[scene_key] = packet
|
|
200
|
+
self.pipeline.draw_packet(self.backend, packet)
|
|
201
|
+
|
|
202
|
+
elif _has_draw(scene_obj):
|
|
203
|
+
# legacy scene draw path
|
|
204
|
+
scene_obj.draw(self.backend)
|
|
205
|
+
|
|
206
|
+
self.backend.end_frame()
|
|
207
|
+
|
|
208
|
+
# 6) deterministic capture (optional)
|
|
209
|
+
if cfg.record:
|
|
210
|
+
# label could be "frame" or something semantic later
|
|
211
|
+
self.services.capture.screenshot_sim(
|
|
212
|
+
cfg.run_id, frame_index, label="frame"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# 7) frame pacing
|
|
216
|
+
if target_dt > 0 and dt < target_dt:
|
|
217
|
+
sleep(target_dt - dt)
|
|
218
|
+
|
|
219
|
+
frame_index += 1
|
|
220
|
+
|
|
221
|
+
# cleanup scenes
|
|
222
|
+
scenes.clean()
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Two-dimensional utilities and components for Mini Arcade Core.
|
|
3
|
-
Includes 2D entities, boundaries, and physics.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from .boundaries2d import (
|
|
9
|
-
RectKinematic,
|
|
10
|
-
RectSprite,
|
|
11
|
-
VerticalBounce,
|
|
12
|
-
VerticalWrap,
|
|
13
|
-
)
|
|
14
|
-
from .collision2d import RectCollider
|
|
15
|
-
from .geometry2d import Bounds2D, Position2D, Size2D
|
|
16
|
-
from .kinematics2d import KinematicData
|
|
17
|
-
from .physics2d import Velocity2D
|
|
18
|
-
|
|
19
|
-
__all__ = [
|
|
20
|
-
"Bounds2D",
|
|
21
|
-
"Position2D",
|
|
22
|
-
"Size2D",
|
|
23
|
-
"KinematicData",
|
|
24
|
-
"Velocity2D",
|
|
25
|
-
"RectCollider",
|
|
26
|
-
"RectKinematic",
|
|
27
|
-
"RectSprite",
|
|
28
|
-
"VerticalBounce",
|
|
29
|
-
"VerticalWrap",
|
|
30
|
-
]
|
|
@@ -4,45 +4,35 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
|
|
9
10
|
from .geometry2d import Position2D, Size2D
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
pos_b: Position2D,
|
|
16
|
-
size_b: Size2D,
|
|
17
|
-
) -> bool:
|
|
13
|
+
class Collider2D(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Abstract base class for 2D colliders.
|
|
18
16
|
"""
|
|
19
|
-
Low-level AABB check. Internal helper.
|
|
20
|
-
|
|
21
|
-
:param pos_a: Top-left position of rectangle A.
|
|
22
|
-
:type pos_a: Position2D
|
|
23
17
|
|
|
24
|
-
:
|
|
25
|
-
:
|
|
18
|
+
position: Position2D
|
|
19
|
+
size: Size2D
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
:
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def intersects(self, other: Collider2D) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Check if this collider intersects with another collider.
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
:param other: The other collider to check against.
|
|
27
|
+
:type other: Collider2D
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return not (
|
|
37
|
-
pos_a.x + size_a.width < pos_b.x
|
|
38
|
-
or pos_a.x > pos_b.x + size_b.width
|
|
39
|
-
or pos_a.y + size_a.height < pos_b.y
|
|
40
|
-
or pos_a.y > pos_b.y + size_b.height
|
|
41
|
-
)
|
|
29
|
+
:return: True if the colliders intersect.
|
|
30
|
+
:rtype: bool
|
|
31
|
+
"""
|
|
42
32
|
|
|
43
33
|
|
|
44
34
|
@dataclass
|
|
45
|
-
class RectCollider:
|
|
35
|
+
class RectCollider(Collider2D):
|
|
46
36
|
"""
|
|
47
37
|
OOP collision helper that wraps a Position2D + Size2D pair.
|
|
48
38
|
|
|
@@ -66,6 +56,13 @@ class RectCollider:
|
|
|
66
56
|
:return: True if the rectangles intersect.
|
|
67
57
|
:rtype: bool
|
|
68
58
|
"""
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
pos_a = self.position
|
|
60
|
+
size_a = self.size
|
|
61
|
+
pos_b = other.position
|
|
62
|
+
size_b = other.size
|
|
63
|
+
return not (
|
|
64
|
+
pos_a.x + size_a.width < pos_b.x
|
|
65
|
+
or pos_a.x > pos_b.x + size_b.width
|
|
66
|
+
or pos_a.y + size_a.height < pos_b.y
|
|
67
|
+
or pos_a.y > pos_b.y + size_b.height
|
|
71
68
|
)
|
|
@@ -19,6 +19,15 @@ class Position2D:
|
|
|
19
19
|
x: float
|
|
20
20
|
y: float
|
|
21
21
|
|
|
22
|
+
def to_tuple(self) -> tuple[float, float]:
|
|
23
|
+
"""
|
|
24
|
+
Convert Position2D to a tuple.
|
|
25
|
+
|
|
26
|
+
:return: Tuple of (x, y).
|
|
27
|
+
:rtype: tuple[float, float]
|
|
28
|
+
"""
|
|
29
|
+
return (self.x, self.y)
|
|
30
|
+
|
|
22
31
|
|
|
23
32
|
@dataclass
|
|
24
33
|
class Size2D:
|
|
@@ -32,6 +41,15 @@ class Size2D:
|
|
|
32
41
|
width: int
|
|
33
42
|
height: int
|
|
34
43
|
|
|
44
|
+
def to_tuple(self) -> tuple[int, int]:
|
|
45
|
+
"""
|
|
46
|
+
Convert Size2D to a tuple.
|
|
47
|
+
|
|
48
|
+
:return: Tuple of (width, height).
|
|
49
|
+
:rtype: tuple[int, int]
|
|
50
|
+
"""
|
|
51
|
+
return (self.width, self.height)
|
|
52
|
+
|
|
35
53
|
|
|
36
54
|
@dataclass
|
|
37
55
|
class Bounds2D:
|