mini-arcade-core 1.1.1__tar.gz → 1.2.1__tar.gz
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-1.1.1 → mini_arcade_core-1.2.1}/PKG-INFO +1 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/pyproject.toml +1 -1
- mini_arcade_core-1.2.1/src/mini_arcade_core/__init__.py +86 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/__init__.py +1 -2
- mini_arcade_core-1.2.1/src/mini_arcade_core/backend/backend.py +322 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/backend/types.py +13 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/commands.py +8 -8
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/game.py +154 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/game_config.py +40 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/gameplay_settings.py +24 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/config.py +20 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/hooks.py +77 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/runner.py +272 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/state.py +32 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/managers.py +24 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/context.py +0 -2
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/base.py +2 -2
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/crt.py +4 -4
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/registry.py +1 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/vignette.py +8 -8
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/end_frame.py +1 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/postfx.py +1 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/ui.py +1 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/world.py +6 -6
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/pipeline.py +7 -6
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/viewport.py +10 -4
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/scenes/models.py +54 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/engine/scenes/scene_manager.py +213 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/audio/audio_port.py +0 -4
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/capture/capture_adapter.py +53 -31
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/capture/capture_port.py +0 -4
- mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/capture/capture_worker.py +174 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/context.py +8 -6
- mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
- mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/services.py +3 -2
- mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/window/window_adapter.py +92 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/window/window_port.py +3 -17
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/debug_overlay.py +5 -4
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/registry.py +11 -1
- mini_arcade_core-1.1.1/src/mini_arcade_core/sim/protocols.py → mini_arcade_core-1.2.1/src/mini_arcade_core/scenes/sim_scene.py +6 -6
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/ui/menu.py +54 -16
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/utils/__init__.py +2 -1
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/utils/logging.py +47 -18
- mini_arcade_core-1.2.1/src/mini_arcade_core/utils/profiler.py +283 -0
- mini_arcade_core-1.1.1/src/mini_arcade_core/__init__.py +0 -114
- mini_arcade_core-1.1.1/src/mini_arcade_core/backend/backend.py +0 -324
- mini_arcade_core-1.1.1/src/mini_arcade_core/backend/types.py +0 -9
- mini_arcade_core-1.1.1/src/mini_arcade_core/engine/game.py +0 -454
- mini_arcade_core-1.1.1/src/mini_arcade_core/managers/inputs.py +0 -284
- mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
- mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/scene/scene_port.py +0 -170
- mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/window/window_adapter.py +0 -90
- mini_arcade_core-1.1.1/src/mini_arcade_core/scenes/sim_scene.py +0 -41
- mini_arcade_core-1.1.1/src/mini_arcade_core/sim/runner.py +0 -222
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/LICENSE +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/README.md +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/events.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/keys.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/sdl_map.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/bus.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/managers → mini_arcade_core-1.2.1/src/mini_arcade_core/engine}/cheats.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render → mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/effects → mini_arcade_core-1.2.1/src/mini_arcade_core/engine/render}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/engine/render/passes → mini_arcade_core-1.2.1/src/mini_arcade_core/engine/render/effects}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/frame_packet.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/packet.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/managers → mini_arcade_core-1.2.1/src/mini_arcade_core/engine/render/passes}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/base.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/lighting.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/render_service.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime → mini_arcade_core-1.2.1/src/mini_arcade_core/engine/scenes}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/audio → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/capture → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/audio}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/file → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/capture}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/input → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/file}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/file/file_adapter.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/file/file_port.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/render → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/input}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/input/input_adapter.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/input/input_port.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/input_frame.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/scene → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/render}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/render/render_port.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/window → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/scene}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/scenes → mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/window}/__init__.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/scenes/systems → mini_arcade_core-1.2.1/src/mini_arcade_core/scenes}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/autoreg.py +0 -0
- {mini_arcade_core-1.1.1/src/mini_arcade_core/sim → mini_arcade_core-1.2.1/src/mini_arcade_core/scenes/systems}/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/systems/base_system.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/systems/system_pipeline.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/boundaries2d.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/collision2d.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/geometry2d.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/kinematics2d.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/physics2d.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/ui/__init__.py +0 -0
- {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/utils/deprecated_decorator.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mini-arcade-core"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.1"
|
|
8
8
|
description = "Tiny scene-based game loop core for small arcade games."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Santiago Rincon", email = "rincores@gmail.com" },
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry point for the mini_arcade_core package.
|
|
3
|
+
Provides access to core classes and a convenience function to run a game.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import traceback
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
from typing import Callable, Type, Union
|
|
11
|
+
|
|
12
|
+
from mini_arcade_core.engine.game import Game
|
|
13
|
+
from mini_arcade_core.engine.game_config import GameConfig
|
|
14
|
+
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
15
|
+
from mini_arcade_core.scenes.sim_scene import SimScene
|
|
16
|
+
from mini_arcade_core.utils import logger
|
|
17
|
+
|
|
18
|
+
SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# TODO: Improve exception handliers by usingng and logging in run_game
|
|
22
|
+
def run_game(
|
|
23
|
+
game_config: GameConfig | None = None,
|
|
24
|
+
scene_registry: SceneRegistry | None = None,
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Convenience helper to bootstrap and run a game with a single scene.
|
|
28
|
+
|
|
29
|
+
:param game_config: Optional GameConfig to customize game settings.
|
|
30
|
+
:type game_config: GameConfig | None
|
|
31
|
+
|
|
32
|
+
:param scene_registry: Optional SceneRegistry for scene management.
|
|
33
|
+
:type scene_registry: SceneRegistry | None
|
|
34
|
+
|
|
35
|
+
:raises ValueError: If the provided game_config does not have a valid Backend.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
cfg = game_config or GameConfig()
|
|
39
|
+
if cfg.backend is None:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"GameConfig.backend must be set to a Backend instance"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
game = Game(cfg, scene_registry=scene_registry)
|
|
45
|
+
game.run()
|
|
46
|
+
# Justification: We need to catch all exceptions while we improve error handling.
|
|
47
|
+
# pylint: disable=broad-exception-caught
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.exception(f"Unhandled exception in game loop: {e}")
|
|
50
|
+
logger.debug(traceback.format_exc())
|
|
51
|
+
# pylint: enable=broad-exception-caught
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
PACKAGE_NAME = "mini-arcade-core"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_version() -> str:
|
|
58
|
+
"""
|
|
59
|
+
Return the installed package version.
|
|
60
|
+
|
|
61
|
+
This is a thin helper around importlib.metadata.version so games can do:
|
|
62
|
+
|
|
63
|
+
from mini_arcade_core import get_version
|
|
64
|
+
print(get_version())
|
|
65
|
+
|
|
66
|
+
:return: The version string of the installed package.
|
|
67
|
+
:rtype: str
|
|
68
|
+
|
|
69
|
+
:raises PackageNotFoundError: If the package is not installed.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
return version(PACKAGE_NAME)
|
|
73
|
+
except PackageNotFoundError: # if running from source / editable
|
|
74
|
+
logger.warning(
|
|
75
|
+
f"Package '{PACKAGE_NAME}' not found. Returning default version '0.0.0'."
|
|
76
|
+
)
|
|
77
|
+
return "0.0.0"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"Game",
|
|
82
|
+
"GameConfig",
|
|
83
|
+
"run_game",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
__version__ = get_version()
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend interface for rendering and input.
|
|
3
|
+
This is the only part of the code that talks to SDL/pygame directly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Iterable, Protocol
|
|
9
|
+
|
|
10
|
+
from .events import Event
|
|
11
|
+
|
|
12
|
+
# Justification: Many positional and keyword arguments needed for some backend methods.
|
|
13
|
+
# Might be refactored later.
|
|
14
|
+
# pylint: disable=too-many-positional-arguments,too-many-arguments
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class WindowProtocol(Protocol):
|
|
18
|
+
"""
|
|
19
|
+
Represents a game window.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
width: int
|
|
23
|
+
height: int
|
|
24
|
+
|
|
25
|
+
def set_title(self, title: str):
|
|
26
|
+
"""
|
|
27
|
+
Set the window title.
|
|
28
|
+
|
|
29
|
+
:param title: New window title.
|
|
30
|
+
:type title: str
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def resize(self, width: int, height: int):
|
|
34
|
+
"""
|
|
35
|
+
Resize the window.
|
|
36
|
+
|
|
37
|
+
:param width: New width in pixels.
|
|
38
|
+
:type width: int
|
|
39
|
+
:param height: New height in pixels.
|
|
40
|
+
:type height: int
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def size(self) -> tuple[int, int]:
|
|
44
|
+
"""
|
|
45
|
+
Get the window size.
|
|
46
|
+
|
|
47
|
+
:return: Tuple of (width, height) in pixels.
|
|
48
|
+
:rtype: tuple[int, int]
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def drawable_size(self) -> tuple[int, int]:
|
|
52
|
+
"""
|
|
53
|
+
Get the drawable size of the window.
|
|
54
|
+
|
|
55
|
+
:return: Tuple of (width, height) in pixels.
|
|
56
|
+
:rtype: tuple[int, int]
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class InputProtocol(Protocol):
|
|
61
|
+
"""
|
|
62
|
+
Interface for input operations.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def poll(self) -> Iterable[Event]:
|
|
66
|
+
"""
|
|
67
|
+
Get the list of input events since the last call.
|
|
68
|
+
|
|
69
|
+
:return: Iterable of Event instances.
|
|
70
|
+
:rtype: Iterable[Event]
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RenderProtocol(Protocol):
|
|
75
|
+
"""
|
|
76
|
+
Interface for rendering operations.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def set_clear_color(self, r: int, g: int, b: int):
|
|
80
|
+
"""
|
|
81
|
+
Set the clear color for the renderer.
|
|
82
|
+
|
|
83
|
+
:param r: Red component (0-255).
|
|
84
|
+
:type r: int
|
|
85
|
+
:param g: Green component (0-255).
|
|
86
|
+
:type g: int
|
|
87
|
+
:param b: Blue component (0-255).
|
|
88
|
+
:type b: int
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def begin_frame(self):
|
|
92
|
+
"""Begin a new rendering frame."""
|
|
93
|
+
|
|
94
|
+
def end_frame(self):
|
|
95
|
+
"""End the current rendering frame."""
|
|
96
|
+
|
|
97
|
+
def draw_rect(self, x: int, y: int, w: int, h: int, color=(255, 255, 255)):
|
|
98
|
+
"""
|
|
99
|
+
Draw a filled rectangle.
|
|
100
|
+
|
|
101
|
+
:param x: The x-coordinate of the rectangle.
|
|
102
|
+
:type x: int
|
|
103
|
+
:param y: The y-coordinate of the rectangle.
|
|
104
|
+
:type y: int
|
|
105
|
+
:param w: The width of the rectangle.
|
|
106
|
+
:type w: int
|
|
107
|
+
:param h: The height of the rectangle.
|
|
108
|
+
:type h: int
|
|
109
|
+
:param color: The color of the rectangle as an (R, G, B) or (R, G, B, A) tuple.
|
|
110
|
+
:type color: tuple[int, int, int] | tuple[int, int, int, int]
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
def draw_line(
|
|
114
|
+
self, x1: int, y1: int, x2: int, y2: int, color=(255, 255, 255)
|
|
115
|
+
):
|
|
116
|
+
"""
|
|
117
|
+
Draw a line between two points.
|
|
118
|
+
|
|
119
|
+
:param x1: The x-coordinate of the start point.
|
|
120
|
+
:type x1: int
|
|
121
|
+
:param y1: The y-coordinate of the start point.
|
|
122
|
+
:type y1: int
|
|
123
|
+
:param x2: The x-coordinate of the end point.
|
|
124
|
+
:type x2: int
|
|
125
|
+
:param y2: The y-coordinate of the end point.
|
|
126
|
+
:type y2: int
|
|
127
|
+
:param color: The color of the line as an (R, G, B) or (R, G, B, A) tuple.
|
|
128
|
+
:type color: tuple[int, int, int] | tuple[int, int, int, int]
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def set_clip_rect(self, x: int, y: int, w: int, h: int):
|
|
132
|
+
"""
|
|
133
|
+
Set the clipping rectangle.
|
|
134
|
+
|
|
135
|
+
:param x: The x-coordinate of the clipping rectangle.
|
|
136
|
+
:type x: int
|
|
137
|
+
:param y: The y-coordinate of the clipping rectangle.
|
|
138
|
+
:type y: int
|
|
139
|
+
:param w: The width of the clipping rectangle.
|
|
140
|
+
:type w: int
|
|
141
|
+
:param h: The height of the clipping rectangle.
|
|
142
|
+
:type h: int
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def clear_clip_rect(self):
|
|
146
|
+
"""Clear the clipping rectangle."""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TextProtocol(Protocol):
|
|
150
|
+
"""
|
|
151
|
+
Interface for text rendering operations.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def measure(
|
|
155
|
+
self, text: str, font_size: int | None = None
|
|
156
|
+
) -> tuple[int, int]:
|
|
157
|
+
"""
|
|
158
|
+
Measure the width and height of the given text.
|
|
159
|
+
|
|
160
|
+
:param text: The text to measure.
|
|
161
|
+
:type text: str
|
|
162
|
+
:param font_size: The font size to use for measurement.
|
|
163
|
+
:type font_size: int | None
|
|
164
|
+
:return: A tuple containing the width and height of the text.
|
|
165
|
+
:rtype: tuple[int, int]
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def draw(
|
|
169
|
+
self,
|
|
170
|
+
x: int,
|
|
171
|
+
y: int,
|
|
172
|
+
text: str,
|
|
173
|
+
color=(255, 255, 255),
|
|
174
|
+
font_size: int | None = None,
|
|
175
|
+
):
|
|
176
|
+
"""
|
|
177
|
+
Draw the given text at the specified position.
|
|
178
|
+
|
|
179
|
+
:param x: The x-coordinate to draw the text.
|
|
180
|
+
:type x: int
|
|
181
|
+
:param y: The y-coordinate to draw the text.
|
|
182
|
+
:type y: int
|
|
183
|
+
:param text: The text to draw.
|
|
184
|
+
:type text: str
|
|
185
|
+
:param color: The color of the text as an (R, G, B) or (R, G, B, A) tuple.
|
|
186
|
+
:type color: tuple[int, int, int] | tuple[int, int, int, int]
|
|
187
|
+
:param font_size: The font size to use for drawing.
|
|
188
|
+
:type font_size: int | None
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class AudioProtocol(Protocol):
|
|
193
|
+
"""
|
|
194
|
+
Interface for audio operations.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def init(
|
|
198
|
+
self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Initialize audio subsystem.
|
|
202
|
+
|
|
203
|
+
:param frequency: Audio frequency in Hz.
|
|
204
|
+
:type frequency: int
|
|
205
|
+
:param channels: Number of audio channels (1=mono, 2=stereo).
|
|
206
|
+
:type channels: int
|
|
207
|
+
:param chunk_size: Size of audio chunks.
|
|
208
|
+
:type chunk_size: int
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
def shutdown(self):
|
|
212
|
+
"""
|
|
213
|
+
Shutdown the audio subsystem.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def load_sound(self, sound_id: str, path: str):
|
|
217
|
+
"""
|
|
218
|
+
Load a sound file.
|
|
219
|
+
|
|
220
|
+
:param sound_id: Unique identifier for the sound.
|
|
221
|
+
:type sound_id: str
|
|
222
|
+
:param path: File path to the sound.
|
|
223
|
+
:type path: str
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def play_sound(self, sound_id: str, loops: int = 0):
|
|
227
|
+
"""
|
|
228
|
+
Play a loaded sound.
|
|
229
|
+
|
|
230
|
+
:param sound_id: Unique identifier for the sound.
|
|
231
|
+
:type sound_id: str
|
|
232
|
+
:param loops: Number of times to loop the sound.
|
|
233
|
+
:type loops: int
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def set_master_volume(self, volume: int):
|
|
237
|
+
"""
|
|
238
|
+
Set the master volume.
|
|
239
|
+
|
|
240
|
+
:param volume: Volume level (0-128).
|
|
241
|
+
:type volume: int
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
def set_sound_volume(self, sound_id: str, volume: int):
|
|
245
|
+
"""
|
|
246
|
+
Set volume for a specific sound.
|
|
247
|
+
|
|
248
|
+
:param sound_id: Unique identifier for the sound.
|
|
249
|
+
:type sound_id: str
|
|
250
|
+
:param volume: Volume level (0-128).
|
|
251
|
+
:type volume: int
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
def stop_all(self):
|
|
255
|
+
"""
|
|
256
|
+
Stop all currently playing sounds.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class CaptureProtocol(Protocol):
|
|
261
|
+
"""
|
|
262
|
+
Interface for frame capture operations.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def bmp(self, path: str | None = None) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
Capture the current frame as a BMP file.
|
|
268
|
+
|
|
269
|
+
:param path: Optional file path to save the BMP. If None, returns bytes.
|
|
270
|
+
:type path: str | None
|
|
271
|
+
:return: Whether the capture was successful.
|
|
272
|
+
:rtype: bool
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# TODO: Refactor backend interface into smaller protocols?
|
|
277
|
+
# Justification: Many public methods needed for backend interface
|
|
278
|
+
# pylint: disable=too-many-public-methods
|
|
279
|
+
class Backend(Protocol):
|
|
280
|
+
"""
|
|
281
|
+
Interface that any rendering/input backend must implement.
|
|
282
|
+
mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
|
|
283
|
+
|
|
284
|
+
:ivar window (WindowProtocol): Window management interface.
|
|
285
|
+
:ivar audio (AudioProtocol): Audio management interface.
|
|
286
|
+
:ivar input (InputProtocol): Input management interface.
|
|
287
|
+
:ivar render (RenderProtocol): Rendering interface.
|
|
288
|
+
:ivar text (TextProtocol): Text rendering interface.
|
|
289
|
+
:ivar capture (CaptureProtocol): Frame capture interface.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
window: WindowProtocol
|
|
293
|
+
audio: AudioProtocol
|
|
294
|
+
input: InputProtocol
|
|
295
|
+
render: RenderProtocol
|
|
296
|
+
text: TextProtocol
|
|
297
|
+
capture: CaptureProtocol
|
|
298
|
+
|
|
299
|
+
def init(self):
|
|
300
|
+
"""
|
|
301
|
+
Initialize the backend and open a window.
|
|
302
|
+
Should be called once before the main loop.
|
|
303
|
+
"""
|
|
304
|
+
|
|
305
|
+
def set_viewport_transform(
|
|
306
|
+
self, offset_x: int, offset_y: int, scale: float
|
|
307
|
+
):
|
|
308
|
+
"""
|
|
309
|
+
Set the viewport transformation.
|
|
310
|
+
|
|
311
|
+
:param offset_x: Horizontal offset.
|
|
312
|
+
:type offset_x: int
|
|
313
|
+
:param offset_y: Vertical offset.
|
|
314
|
+
:type offset_y: int
|
|
315
|
+
:param scale: Scaling factor.
|
|
316
|
+
:type scale: float
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
def clear_viewport_transform(self):
|
|
320
|
+
"""
|
|
321
|
+
Clear the viewport transformation (reset to defaults).
|
|
322
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Types used in the backend module.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Tuple, Union
|
|
8
|
+
|
|
9
|
+
ColorRGB = Tuple[int, int, int]
|
|
10
|
+
ColorRGBA = Tuple[int, int, int, int]
|
|
11
|
+
|
|
12
|
+
Color = Union[ColorRGB, ColorRGBA]
|
|
13
|
+
Alpha = Union[float, int]
|
|
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
8
|
from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
|
|
9
9
|
|
|
10
|
-
from mini_arcade_core.
|
|
10
|
+
from mini_arcade_core.engine.scenes.models import ScenePolicy
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from mini_arcade_core.runtime.services import RuntimeServices
|
|
@@ -30,7 +30,7 @@ class CommandContext:
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
services: RuntimeServices
|
|
33
|
-
|
|
33
|
+
managers: object
|
|
34
34
|
settings: Optional[object] = None
|
|
35
35
|
world: Optional[object] = None
|
|
36
36
|
|
|
@@ -104,7 +104,7 @@ class QuitCommand(Command):
|
|
|
104
104
|
self,
|
|
105
105
|
context: CommandContext,
|
|
106
106
|
):
|
|
107
|
-
context.
|
|
107
|
+
context.managers.scenes.quit()
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
@dataclass(frozen=True)
|
|
@@ -140,7 +140,7 @@ class PushSceneCommand(Command):
|
|
|
140
140
|
self,
|
|
141
141
|
context: CommandContext,
|
|
142
142
|
):
|
|
143
|
-
context.
|
|
143
|
+
context.managers.scenes.push(self.scene_id, as_overlay=self.as_overlay)
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
@dataclass(frozen=True)
|
|
@@ -151,7 +151,7 @@ class PopSceneCommand(Command):
|
|
|
151
151
|
self,
|
|
152
152
|
context: CommandContext,
|
|
153
153
|
):
|
|
154
|
-
context.
|
|
154
|
+
context.managers.scenes.pop()
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
@dataclass(frozen=True)
|
|
@@ -168,7 +168,7 @@ class ChangeSceneCommand(Command):
|
|
|
168
168
|
self,
|
|
169
169
|
context: CommandContext,
|
|
170
170
|
):
|
|
171
|
-
context.
|
|
171
|
+
context.managers.scenes.change(self.scene_id)
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
@dataclass(frozen=True)
|
|
@@ -182,7 +182,7 @@ class ToggleDebugOverlayCommand(Command):
|
|
|
182
182
|
DEBUG_OVERLAY_ID = "debug_overlay"
|
|
183
183
|
|
|
184
184
|
def execute(self, context: CommandContext):
|
|
185
|
-
scenes = context.
|
|
185
|
+
scenes = context.managers.scenes
|
|
186
186
|
if scenes.has_scene(self.DEBUG_OVERLAY_ID):
|
|
187
187
|
scenes.remove_scene(self.DEBUG_OVERLAY_ID)
|
|
188
188
|
return
|
|
@@ -209,7 +209,7 @@ class ToggleEffectCommand(Command):
|
|
|
209
209
|
|
|
210
210
|
effect_id: str
|
|
211
211
|
|
|
212
|
-
def execute(self, context: CommandContext)
|
|
212
|
+
def execute(self, context: CommandContext):
|
|
213
213
|
# effects live in context.meta OR in a dedicated service/settings.
|
|
214
214
|
# v1 simplest: stash stack into context.settings or context.services.render
|
|
215
215
|
stack = getattr(context.settings, "effects_stack", None)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from mini_arcade_core.backend import Backend
|
|
8
|
+
from mini_arcade_core.engine.cheats import CheatManager
|
|
9
|
+
from mini_arcade_core.engine.commands import CommandQueue
|
|
10
|
+
from mini_arcade_core.engine.game_config import GameConfig
|
|
11
|
+
from mini_arcade_core.engine.gameplay_settings import GamePlaySettings
|
|
12
|
+
from mini_arcade_core.engine.loop.config import RunnerConfig
|
|
13
|
+
from mini_arcade_core.engine.loop.hooks import DefaultGameHooks
|
|
14
|
+
from mini_arcade_core.engine.loop.runner import EngineRunner
|
|
15
|
+
from mini_arcade_core.engine.managers import EngineManagers
|
|
16
|
+
from mini_arcade_core.engine.render.effects.base import (
|
|
17
|
+
EffectParams,
|
|
18
|
+
EffectStack,
|
|
19
|
+
)
|
|
20
|
+
from mini_arcade_core.engine.render.effects.crt import CRTEffect
|
|
21
|
+
from mini_arcade_core.engine.render.effects.registry import EffectRegistry
|
|
22
|
+
from mini_arcade_core.engine.render.effects.vignette import VignetteNoiseEffect
|
|
23
|
+
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
24
|
+
from mini_arcade_core.engine.render.render_service import RenderService
|
|
25
|
+
from mini_arcade_core.engine.scenes.scene_manager import SceneAdapter
|
|
26
|
+
from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
|
|
27
|
+
from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
|
|
28
|
+
from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
|
|
29
|
+
from mini_arcade_core.runtime.input.input_adapter import InputAdapter
|
|
30
|
+
from mini_arcade_core.runtime.scene.scene_query_adapter import (
|
|
31
|
+
SceneQueryAdapter,
|
|
32
|
+
)
|
|
33
|
+
from mini_arcade_core.runtime.services import RuntimeServices
|
|
34
|
+
from mini_arcade_core.runtime.window.window_adapter import WindowAdapter
|
|
35
|
+
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
36
|
+
from mini_arcade_core.utils import FrameTimer
|
|
37
|
+
from mini_arcade_core.utils.profiler import FrameTimerConfig
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Game:
|
|
41
|
+
"""Core game object responsible for managing the main loop and active scene."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self, config: GameConfig, scene_registry: SceneRegistry | None = None
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
:param config: Game configuration options.
|
|
48
|
+
:type config: GameConfig
|
|
49
|
+
|
|
50
|
+
:param scene_registry: Optional SceneRegistry for scene management.
|
|
51
|
+
:type scene_registry: SceneRegistry | None
|
|
52
|
+
|
|
53
|
+
:raises ValueError: If the provided config does not have a valid Backend.
|
|
54
|
+
"""
|
|
55
|
+
self.config = config
|
|
56
|
+
self._running: bool = False
|
|
57
|
+
|
|
58
|
+
if self.config.backend is None:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"GameConfig.backend must be set to a Backend instance"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
self.backend: Backend = self.config.backend
|
|
64
|
+
self.settings = GamePlaySettings()
|
|
65
|
+
self.managers = EngineManagers(
|
|
66
|
+
cheats=CheatManager(),
|
|
67
|
+
command_queue=CommandQueue(),
|
|
68
|
+
scenes=SceneAdapter(
|
|
69
|
+
scene_registry or SceneRegistry(_factories={}), self
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
self.services = RuntimeServices(
|
|
73
|
+
window=WindowAdapter(self.backend), # Turn into a manager?
|
|
74
|
+
audio=SDLAudioAdapter(self.backend),
|
|
75
|
+
files=LocalFilesAdapter(),
|
|
76
|
+
capture=CaptureAdapter(self.backend),
|
|
77
|
+
input=InputAdapter(),
|
|
78
|
+
render=RenderService(),
|
|
79
|
+
scenes=SceneQueryAdapter(self.managers.scenes),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def running(self) -> bool:
|
|
84
|
+
"""Check if the game is currently running."""
|
|
85
|
+
return self._running
|
|
86
|
+
|
|
87
|
+
def quit(self):
|
|
88
|
+
"""Request that the main loop stops."""
|
|
89
|
+
self._running = False
|
|
90
|
+
|
|
91
|
+
def run(self):
|
|
92
|
+
"""
|
|
93
|
+
Run the main loop starting with the given scene.
|
|
94
|
+
|
|
95
|
+
This is intentionally left abstract so you can plug pygame, pyglet,
|
|
96
|
+
or another backend.
|
|
97
|
+
|
|
98
|
+
:param initial_scene_id: The scene id to start the game with (must be registered).
|
|
99
|
+
:type initial_scene_id: str
|
|
100
|
+
"""
|
|
101
|
+
self.managers.scenes.change(self.config.initial_scene)
|
|
102
|
+
|
|
103
|
+
pipeline = RenderPipeline()
|
|
104
|
+
effects_registry = EffectRegistry()
|
|
105
|
+
effects_registry.register(CRTEffect())
|
|
106
|
+
effects_registry.register(VignetteNoiseEffect())
|
|
107
|
+
|
|
108
|
+
effects_stack = EffectStack(
|
|
109
|
+
enabled=self.config.postfx.enabled,
|
|
110
|
+
active=list(self.config.postfx.active),
|
|
111
|
+
params={
|
|
112
|
+
"crt": EffectParams(intensity=0.35, wobble_speed=1.0),
|
|
113
|
+
"vignette_noise": EffectParams(
|
|
114
|
+
intensity=0.25, wobble_speed=1.0
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
self.settings.effects_stack = effects_stack
|
|
119
|
+
|
|
120
|
+
for p in pipeline.passes:
|
|
121
|
+
if getattr(p, "name", "") == "PostFXPass":
|
|
122
|
+
p.registry = effects_registry
|
|
123
|
+
|
|
124
|
+
self._running = True
|
|
125
|
+
|
|
126
|
+
timer = FrameTimer(
|
|
127
|
+
config=FrameTimerConfig(enabled=self.config.enable_profiler)
|
|
128
|
+
)
|
|
129
|
+
hooks = DefaultGameHooks(self, effects_stack)
|
|
130
|
+
|
|
131
|
+
self.services.window.set_virtual_resolution(800, 600)
|
|
132
|
+
runner = EngineRunner(
|
|
133
|
+
self,
|
|
134
|
+
pipeline=pipeline,
|
|
135
|
+
effects_stack=effects_stack,
|
|
136
|
+
hooks=hooks,
|
|
137
|
+
)
|
|
138
|
+
runner.run(cfg=RunnerConfig(fps=self.config.fps), timer=timer)
|
|
139
|
+
|
|
140
|
+
def resolve_world(self) -> object | None:
|
|
141
|
+
"""
|
|
142
|
+
Resolve and return the current gameplay world.
|
|
143
|
+
|
|
144
|
+
:return: The current gameplay world, or None if not found.
|
|
145
|
+
:rtype: object | None
|
|
146
|
+
"""
|
|
147
|
+
# Prefer gameplay world underneath overlays:
|
|
148
|
+
# scan from top to bottom and pick the first scene that has .world
|
|
149
|
+
for entry in reversed(self.managers.scenes.visible_entries()):
|
|
150
|
+
scene = entry.scene
|
|
151
|
+
world = getattr(scene, "world", None)
|
|
152
|
+
if world is not None:
|
|
153
|
+
return world
|
|
154
|
+
return None
|