mini-arcade-core 0.10.0__tar.gz → 1.0.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-0.10.0 → mini_arcade_core-1.0.1}/PKG-INFO +1 -1
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/pyproject.toml +1 -1
- mini_arcade_core-1.0.1/src/mini_arcade_core/__init__.py +114 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/backend/__init__.py +2 -6
- mini_arcade_core-1.0.1/src/mini_arcade_core/backend/backend.py +291 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/backend/events.py +1 -1
- mini_arcade_core-0.10.0/src/mini_arcade_core/keymaps/sdl.py → mini_arcade_core-1.0.1/src/mini_arcade_core/backend/sdl_map.py +1 -1
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/commands.py +169 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/game.py +369 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/packet.py +56 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/pipeline.py +63 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/viewport.py +203 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/managers/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/managers/cheats.py +186 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/managers/inputs.py +5 -3
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio/audio_adapter.py +20 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio/audio_port.py +36 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture/capture_port.py +51 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/context.py +53 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file/file_adapter.py +20 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file/file_port.py +31 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input/input_adapter.py +49 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input/input_port.py +31 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input_frame.py +71 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene/scene_port.py +149 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/services.py +35 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window/window_adapter.py +90 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window/window_port.py +109 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/__init__.py +0 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/scenes/autoreg.py +1 -1
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/scenes/registry.py +21 -19
- mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/sim_scene.py +41 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems/base_system.py +40 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/sim/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/sim/protocols.py +41 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/sim/runner.py +222 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/spaces/__init__.py +0 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/spaces/d2/__init__.py +0 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/collision2d.py +25 -28
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/geometry2d.py +18 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/physics2d.py +9 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/ui/__init__.py +0 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/ui/menu.py +271 -85
- mini_arcade_core-1.0.1/src/mini_arcade_core/utils/__init__.py +10 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/utils/deprecated_decorator.py +45 -0
- mini_arcade_core-1.0.1/src/mini_arcade_core/utils/logging.py +168 -0
- mini_arcade_core-0.10.0/src/mini_arcade_core/__init__.py +0 -126
- mini_arcade_core-0.10.0/src/mini_arcade_core/backend/backend.py +0 -151
- mini_arcade_core-0.10.0/src/mini_arcade_core/commands.py +0 -84
- mini_arcade_core-0.10.0/src/mini_arcade_core/entity.py +0 -72
- mini_arcade_core-0.10.0/src/mini_arcade_core/game.py +0 -287
- mini_arcade_core-0.10.0/src/mini_arcade_core/keymaps/__init__.py +0 -15
- mini_arcade_core-0.10.0/src/mini_arcade_core/managers/__init__.py +0 -22
- mini_arcade_core-0.10.0/src/mini_arcade_core/managers/base.py +0 -132
- mini_arcade_core-0.10.0/src/mini_arcade_core/managers/cheats.py +0 -355
- mini_arcade_core-0.10.0/src/mini_arcade_core/managers/entities.py +0 -38
- mini_arcade_core-0.10.0/src/mini_arcade_core/managers/overlays.py +0 -53
- mini_arcade_core-0.10.0/src/mini_arcade_core/managers/system.py +0 -26
- mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/__init__.py +0 -22
- mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/model.py +0 -34
- mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/runtime.py +0 -29
- mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/scene.py +0 -109
- mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/system.py +0 -69
- mini_arcade_core-0.10.0/src/mini_arcade_core/spaces/__init__.py +0 -12
- mini_arcade_core-0.10.0/src/mini_arcade_core/spaces/d2/__init__.py +0 -30
- mini_arcade_core-0.10.0/src/mini_arcade_core/ui/__init__.py +0 -26
- mini_arcade_core-0.10.0/src/mini_arcade_core/ui/overlays.py +0 -41
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/LICENSE +0 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/README.md +0 -0
- {mini_arcade_core-0.10.0/src/mini_arcade_core/keymaps → mini_arcade_core-1.0.1/src/mini_arcade_core/backend}/keys.py +0 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/backend/types.py +0 -0
- {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/bus.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 = "0.
|
|
7
|
+
version = "1.0.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,114 @@
|
|
|
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, GameConfig, WindowConfig
|
|
13
|
+
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
14
|
+
from mini_arcade_core.scenes.sim_scene import SimScene
|
|
15
|
+
from mini_arcade_core.utils import logger
|
|
16
|
+
|
|
17
|
+
SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# TODO: Improve exception handling and logging in run_game
|
|
21
|
+
# TODO: Consider reducing parameters by using a single config object
|
|
22
|
+
# TODO: Delegate responsibilities to Game class where appropriate
|
|
23
|
+
def run_game(
|
|
24
|
+
scene: SceneFactoryLike | None = None,
|
|
25
|
+
config: GameConfig | None = None,
|
|
26
|
+
registry: SceneRegistry | None = None,
|
|
27
|
+
initial_scene: str = "main",
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Convenience helper to bootstrap and run a game with a single scene.
|
|
31
|
+
|
|
32
|
+
Supports both:
|
|
33
|
+
- run_game(SceneClass, cfg) # legacy
|
|
34
|
+
- run_game(config=cfg, initial_scene="main", registry=...) # registry-based
|
|
35
|
+
- run_game(cfg) # config-only
|
|
36
|
+
|
|
37
|
+
:param scene: Optional SimScene factory/class to register
|
|
38
|
+
:type scene: SceneFactoryLike | None
|
|
39
|
+
|
|
40
|
+
:param initial_scene: The SimScene ID to start the game with.
|
|
41
|
+
:type initial_scene: str
|
|
42
|
+
|
|
43
|
+
:param config: Optional GameConfig to customize game settings.
|
|
44
|
+
:type config: GameConfig | None
|
|
45
|
+
|
|
46
|
+
:param registry: Optional SceneRegistry for scene management.
|
|
47
|
+
:type registry: SceneRegistry | None
|
|
48
|
+
|
|
49
|
+
:raises ValueError: If the provided config does not have a valid Backend.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
# Handle run_game(cfg) where the first arg is actually a GameConfig
|
|
53
|
+
if isinstance(scene, GameConfig) and config is None:
|
|
54
|
+
config = scene
|
|
55
|
+
scene = None
|
|
56
|
+
|
|
57
|
+
cfg = config or GameConfig()
|
|
58
|
+
if cfg.backend is None:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"GameConfig.backend must be set to a Backend instance"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# If user provided a SimScene factory/class, ensure it's registered
|
|
64
|
+
if scene is not None:
|
|
65
|
+
if registry is None:
|
|
66
|
+
registry = SceneRegistry(_factories={})
|
|
67
|
+
registry.register(
|
|
68
|
+
initial_scene, scene
|
|
69
|
+
) # SimScene class is callable(game) -> SimScene
|
|
70
|
+
|
|
71
|
+
game = Game(cfg, registry=registry)
|
|
72
|
+
game.run(initial_scene)
|
|
73
|
+
# Justification: We need to catch all exceptions while we improve error handling.
|
|
74
|
+
# pylint: disable=broad-exception-caught
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.exception(f"Unhandled exception in game loop: {e}")
|
|
77
|
+
logger.debug(traceback.format_exc())
|
|
78
|
+
# pylint: enable=broad-exception-caught
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_version() -> str:
|
|
85
|
+
"""
|
|
86
|
+
Return the installed package version.
|
|
87
|
+
|
|
88
|
+
This is a thin helper around importlib.metadata.version so games can do:
|
|
89
|
+
|
|
90
|
+
from mini_arcade_core import get_version
|
|
91
|
+
print(get_version())
|
|
92
|
+
|
|
93
|
+
:return: The version string of the installed package.
|
|
94
|
+
:rtype: str
|
|
95
|
+
|
|
96
|
+
:raises PackageNotFoundError: If the package is not installed.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
return version(PACKAGE_NAME)
|
|
100
|
+
except PackageNotFoundError: # if running from source / editable
|
|
101
|
+
logger.warning(
|
|
102
|
+
f"Package '{PACKAGE_NAME}' not found. Returning default version '0.0.0'."
|
|
103
|
+
)
|
|
104
|
+
return "0.0.0"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = [
|
|
108
|
+
"Game",
|
|
109
|
+
"GameConfig",
|
|
110
|
+
"WindowConfig",
|
|
111
|
+
"run_game",
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
__version__ = get_version()
|
|
@@ -6,13 +6,9 @@ This is the only part of the code that talks to SDL/pygame directly.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from .backend import Backend
|
|
10
|
-
from .events import Event, EventType
|
|
11
|
-
from .types import Color
|
|
9
|
+
from .backend import Backend, WindowSettings
|
|
12
10
|
|
|
13
11
|
__all__ = [
|
|
14
12
|
"Backend",
|
|
15
|
-
"
|
|
16
|
-
"EventType",
|
|
17
|
-
"Color",
|
|
13
|
+
"WindowSettings",
|
|
18
14
|
]
|
|
@@ -0,0 +1,291 @@
|
|
|
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 dataclasses import dataclass
|
|
9
|
+
from typing import Iterable, Protocol
|
|
10
|
+
|
|
11
|
+
from .events import Event
|
|
12
|
+
from .types import Color
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class WindowSettings:
|
|
17
|
+
"""
|
|
18
|
+
Settings for the backend window.
|
|
19
|
+
|
|
20
|
+
:ivar width (int): Width of the window in pixels.
|
|
21
|
+
:ivar height (int): Height of the window in pixels.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
width: int
|
|
25
|
+
height: int
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# TODO: Refactor backend interface into smaller protocols?
|
|
29
|
+
# Justification: Many public methods needed for backend interface
|
|
30
|
+
# pylint: disable=too-many-public-methods
|
|
31
|
+
class Backend(Protocol):
|
|
32
|
+
"""
|
|
33
|
+
Interface that any rendering/input backend must implement.
|
|
34
|
+
|
|
35
|
+
mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def init(self, window_settings: WindowSettings):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the backend and open a window.
|
|
41
|
+
Should be called once before the main loop.
|
|
42
|
+
|
|
43
|
+
:param window_settings: Settings for the backend window.
|
|
44
|
+
:type window_settings: WindowSettings
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def set_window_title(self, title: str):
|
|
48
|
+
"""
|
|
49
|
+
Set the window title.
|
|
50
|
+
|
|
51
|
+
:param title: The new title for the window.
|
|
52
|
+
:type title: str
|
|
53
|
+
"""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
def poll_events(self) -> Iterable[Event]:
|
|
57
|
+
"""
|
|
58
|
+
Return all pending events since last call.
|
|
59
|
+
Concrete backends will translate their native events into core Event objects.
|
|
60
|
+
|
|
61
|
+
:return: An iterable of Event objects.
|
|
62
|
+
:rtype: Iterable[Event]
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def set_clear_color(self, r: int, g: int, b: int):
|
|
66
|
+
"""
|
|
67
|
+
Set the background/clear color used by begin_frame.
|
|
68
|
+
|
|
69
|
+
:param r: Red component (0-255).
|
|
70
|
+
:type r: int
|
|
71
|
+
|
|
72
|
+
:param g: Green component (0-255).
|
|
73
|
+
:type g: int
|
|
74
|
+
|
|
75
|
+
:param b: Blue component (0-255).
|
|
76
|
+
:type b: int
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def begin_frame(self):
|
|
80
|
+
"""
|
|
81
|
+
Prepare for drawing a new frame (e.g. clear screen).
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def end_frame(self):
|
|
85
|
+
"""
|
|
86
|
+
Present the frame to the user (swap buffers).
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
# Justification: Simple drawing API for now
|
|
90
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
91
|
+
def draw_rect(
|
|
92
|
+
self,
|
|
93
|
+
x: int,
|
|
94
|
+
y: int,
|
|
95
|
+
w: int,
|
|
96
|
+
h: int,
|
|
97
|
+
color: Color = (255, 255, 255),
|
|
98
|
+
):
|
|
99
|
+
"""
|
|
100
|
+
Draw a filled rectangle in some default color.
|
|
101
|
+
We'll keep this minimal for now; later we can extend with colors/sprites.
|
|
102
|
+
|
|
103
|
+
:param x: X position of the rectangle's top-left corner.
|
|
104
|
+
:type x: int
|
|
105
|
+
|
|
106
|
+
:param y: Y position of the rectangle's top-left corner.
|
|
107
|
+
:type y: int
|
|
108
|
+
|
|
109
|
+
:param w: Width of the rectangle.
|
|
110
|
+
:type w: int
|
|
111
|
+
|
|
112
|
+
:param h: Height of the rectangle.
|
|
113
|
+
:type h: int
|
|
114
|
+
|
|
115
|
+
:param color: RGB color tuple.
|
|
116
|
+
:type color: Color
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def draw_text(
|
|
120
|
+
self,
|
|
121
|
+
x: int,
|
|
122
|
+
y: int,
|
|
123
|
+
text: str,
|
|
124
|
+
color: Color = (255, 255, 255),
|
|
125
|
+
font_size: int | None = None,
|
|
126
|
+
):
|
|
127
|
+
"""
|
|
128
|
+
Draw text at the given position in a default font and color.
|
|
129
|
+
|
|
130
|
+
Backends may ignore advanced styling for now; this is just to render
|
|
131
|
+
simple labels like menu items, scores, etc.
|
|
132
|
+
|
|
133
|
+
:param x: X position of the text's top-left corner.
|
|
134
|
+
:type x: int
|
|
135
|
+
|
|
136
|
+
:param y: Y position of the text's top-left corner.
|
|
137
|
+
:type y: int
|
|
138
|
+
|
|
139
|
+
:param text: The text string to draw.
|
|
140
|
+
:type text: str
|
|
141
|
+
|
|
142
|
+
:param color: RGB color tuple.
|
|
143
|
+
:type color: Color
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
147
|
+
|
|
148
|
+
def measure_text(self, text: str) -> tuple[int, int]:
|
|
149
|
+
"""
|
|
150
|
+
Measure the width and height of the given text string in pixels.
|
|
151
|
+
|
|
152
|
+
:param text: The text string to measure.
|
|
153
|
+
:type text: str
|
|
154
|
+
|
|
155
|
+
:return: A tuple (width, height) in pixels.
|
|
156
|
+
:rtype: tuple[int, int]
|
|
157
|
+
"""
|
|
158
|
+
raise NotImplementedError
|
|
159
|
+
|
|
160
|
+
def capture_frame(self, path: str | None = None) -> bytes | None:
|
|
161
|
+
"""
|
|
162
|
+
Capture the current frame.
|
|
163
|
+
If `path` is provided, save to that file (e.g. PNG).
|
|
164
|
+
Returns raw bytes (PNG) or None if unsupported.
|
|
165
|
+
|
|
166
|
+
:param path: Optional file path to save the screenshot.
|
|
167
|
+
:type path: str | None
|
|
168
|
+
|
|
169
|
+
:return: Raw image bytes if no path given, else None.
|
|
170
|
+
:rtype: bytes | None
|
|
171
|
+
"""
|
|
172
|
+
raise NotImplementedError
|
|
173
|
+
|
|
174
|
+
def init_audio(
|
|
175
|
+
self, frequency: int = 44100, channels: int = 2, chunk_size: int = 2048
|
|
176
|
+
):
|
|
177
|
+
"""
|
|
178
|
+
Initialize SDL_mixer audio.
|
|
179
|
+
|
|
180
|
+
:param frequency: Audio frequency in Hz.
|
|
181
|
+
:type frequency: int
|
|
182
|
+
|
|
183
|
+
:param channels: Number of audio channels (1=mono, 2=stereo).
|
|
184
|
+
:type channels: int
|
|
185
|
+
|
|
186
|
+
:param chunk_size: Size of audio chunks.
|
|
187
|
+
:type chunk_size: int
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def shutdown_audio(self):
|
|
191
|
+
"""Shutdown SDL_mixer audio and free loaded sounds."""
|
|
192
|
+
|
|
193
|
+
def load_sound(self, sound_id: str, path: str):
|
|
194
|
+
"""
|
|
195
|
+
Load a WAV sound and store it by ID.
|
|
196
|
+
Example: backend.load_sound("hit", "assets/sfx/hit.wav")
|
|
197
|
+
|
|
198
|
+
:param sound_id: Unique identifier for the sound.
|
|
199
|
+
:type sound_id: str
|
|
200
|
+
|
|
201
|
+
:param path: File path to the WAV sound.
|
|
202
|
+
:type path: str
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def play_sound(self, sound_id: str, loops: int = 0):
|
|
206
|
+
"""
|
|
207
|
+
Play a loaded sound.
|
|
208
|
+
loops=0 => play once
|
|
209
|
+
loops=-1 => infinite loop
|
|
210
|
+
loops=1 => play twice (SDL convention)
|
|
211
|
+
|
|
212
|
+
:param sound_id: Unique identifier for the sound.
|
|
213
|
+
:type sound_id: str
|
|
214
|
+
|
|
215
|
+
:param loops: Number of times to loop the sound.
|
|
216
|
+
:type loops: int
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def set_master_volume(self, volume: int):
|
|
220
|
+
"""
|
|
221
|
+
Master volume: 0..128
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
def set_sound_volume(self, sound_id: str, volume: int):
|
|
225
|
+
"""
|
|
226
|
+
Per-sound volume: 0..128
|
|
227
|
+
|
|
228
|
+
:param sound_id: Unique identifier for the sound.
|
|
229
|
+
:type sound_id: str
|
|
230
|
+
|
|
231
|
+
:param volume: Volume level (0-128).
|
|
232
|
+
:type volume: int
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def stop_all_sounds(self):
|
|
236
|
+
"""Stop all channels."""
|
|
237
|
+
|
|
238
|
+
def set_viewport_transform(
|
|
239
|
+
self, offset_x: int, offset_y: int, scale: float
|
|
240
|
+
):
|
|
241
|
+
"""
|
|
242
|
+
Apply a transform so draw_* receives VIRTUAL coords and backend maps to screen.
|
|
243
|
+
|
|
244
|
+
:param offset_x: X offset in pixels.
|
|
245
|
+
:type offset_x: int
|
|
246
|
+
|
|
247
|
+
:param offset_y: Y offset in pixels.
|
|
248
|
+
:type offset_y: int
|
|
249
|
+
|
|
250
|
+
:param scale: Scale factor.
|
|
251
|
+
:type scale: float
|
|
252
|
+
"""
|
|
253
|
+
raise NotImplementedError
|
|
254
|
+
|
|
255
|
+
def clear_viewport_transform(self):
|
|
256
|
+
"""Reset any viewport transform back to identity."""
|
|
257
|
+
raise NotImplementedError
|
|
258
|
+
|
|
259
|
+
def resize_window(self, width: int, height: int):
|
|
260
|
+
"""
|
|
261
|
+
Resize the actual OS window (SDL_SetWindowSize in native backend).
|
|
262
|
+
|
|
263
|
+
:param width: New width in pixels.
|
|
264
|
+
:type width: int
|
|
265
|
+
|
|
266
|
+
:param height: New height in pixels.
|
|
267
|
+
:type height: int
|
|
268
|
+
"""
|
|
269
|
+
raise NotImplementedError
|
|
270
|
+
|
|
271
|
+
def set_clip_rect(self, x: int, y: int, w: int, h: int):
|
|
272
|
+
"""
|
|
273
|
+
Set a clipping rectangle for rendering.
|
|
274
|
+
|
|
275
|
+
:param x: X position of the rectangle's top-left corner.
|
|
276
|
+
:type x: int
|
|
277
|
+
|
|
278
|
+
:param y: Y position of the rectangle's top-left corner.
|
|
279
|
+
:type y: int
|
|
280
|
+
|
|
281
|
+
:param w: Width of the rectangle.
|
|
282
|
+
:type w: int
|
|
283
|
+
|
|
284
|
+
:param h: Height of the rectangle.
|
|
285
|
+
:type h: int
|
|
286
|
+
"""
|
|
287
|
+
raise NotImplementedError
|
|
288
|
+
|
|
289
|
+
def clear_clip_rect(self):
|
|
290
|
+
"""Clear any clipping rectangle."""
|
|
291
|
+
raise NotImplementedError
|
|
File without changes
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command protocol for executing commands with a given context.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from mini_arcade_core.runtime.services import RuntimeServices
|
|
12
|
+
|
|
13
|
+
# Justification: Generic type for context
|
|
14
|
+
# pylint: disable=invalid-name
|
|
15
|
+
TContext = TypeVar("TContext")
|
|
16
|
+
# pylint: enable=invalid-name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CommandContext:
|
|
21
|
+
"""
|
|
22
|
+
Context for command execution.
|
|
23
|
+
|
|
24
|
+
:ivar services (RuntimeServices): The runtime services.
|
|
25
|
+
:ivar commands (CommandQueue | None): Optional command queue.
|
|
26
|
+
:ivar settings (object | None): Optional settings object.
|
|
27
|
+
:ivar world (object | None): The world object (can be any type).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
services: RuntimeServices
|
|
31
|
+
commands: Optional["CommandQueue"] = None
|
|
32
|
+
settings: Optional[object] = None
|
|
33
|
+
world: Optional[object] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Command(Protocol):
|
|
37
|
+
"""
|
|
38
|
+
A command is the only allowed "write path" from input/systems into:
|
|
39
|
+
- scene operations (push/pop/change/quit)
|
|
40
|
+
- capture
|
|
41
|
+
- global game lifecycle
|
|
42
|
+
- later: world mutations (if you pass a world reference)
|
|
43
|
+
|
|
44
|
+
For now we keep it simple: commands only need RuntimeServices.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def execute(
|
|
48
|
+
self,
|
|
49
|
+
context: CommandContext,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
Execute the command with the given world and runtime services.
|
|
53
|
+
|
|
54
|
+
:param services: Runtime services for command execution.
|
|
55
|
+
:type services: RuntimeServices
|
|
56
|
+
|
|
57
|
+
:param commands: Optional command queue for command execution.
|
|
58
|
+
:type commands: object | None
|
|
59
|
+
|
|
60
|
+
:param settings: Optional settings object for command execution.
|
|
61
|
+
:type settings: object | None
|
|
62
|
+
|
|
63
|
+
:param world: The world object (can be any type).
|
|
64
|
+
:type world: object | None
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class CommandQueue:
|
|
70
|
+
"""
|
|
71
|
+
Queue for storing and executing commands.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
_items: List[Command] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
def push(self, cmd: Command):
|
|
77
|
+
"""
|
|
78
|
+
Push a command onto the queue.
|
|
79
|
+
|
|
80
|
+
:param cmd: Command to be added to the queue.
|
|
81
|
+
:type cmd: Command
|
|
82
|
+
"""
|
|
83
|
+
self._items.append(cmd)
|
|
84
|
+
|
|
85
|
+
def drain(self) -> List[Command]:
|
|
86
|
+
"""
|
|
87
|
+
Drain and return all commands from the queue.
|
|
88
|
+
|
|
89
|
+
:return: List of commands that were in the queue.
|
|
90
|
+
:rtype: list[Command]
|
|
91
|
+
"""
|
|
92
|
+
items = self._items
|
|
93
|
+
self._items = []
|
|
94
|
+
return items
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class QuitCommand(Command):
|
|
99
|
+
"""Quit the game."""
|
|
100
|
+
|
|
101
|
+
def execute(
|
|
102
|
+
self,
|
|
103
|
+
context: CommandContext,
|
|
104
|
+
):
|
|
105
|
+
context.services.scenes.quit()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True)
|
|
109
|
+
class ScreenshotCommand(Command):
|
|
110
|
+
"""
|
|
111
|
+
Take a screenshot.
|
|
112
|
+
|
|
113
|
+
:ivar label (str | None): Optional label for the screenshot file.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
label: str | None = None
|
|
117
|
+
|
|
118
|
+
def execute(
|
|
119
|
+
self,
|
|
120
|
+
context: CommandContext,
|
|
121
|
+
):
|
|
122
|
+
context.services.capture.screenshot(label=self.label, mode="manual")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class PushSceneCommand(Command):
|
|
127
|
+
"""
|
|
128
|
+
Push a new scene onto the scene stack.
|
|
129
|
+
|
|
130
|
+
:ivar scene_id (str): Identifier of the scene to push.
|
|
131
|
+
:ivar as_overlay (bool): Whether to push the scene as an overlay.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
scene_id: str
|
|
135
|
+
as_overlay: bool = False
|
|
136
|
+
|
|
137
|
+
def execute(
|
|
138
|
+
self,
|
|
139
|
+
context: CommandContext,
|
|
140
|
+
):
|
|
141
|
+
context.services.scenes.push(self.scene_id, as_overlay=self.as_overlay)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class PopSceneCommand(Command):
|
|
146
|
+
"""Pop the current scene from the scene stack."""
|
|
147
|
+
|
|
148
|
+
def execute(
|
|
149
|
+
self,
|
|
150
|
+
context: CommandContext,
|
|
151
|
+
):
|
|
152
|
+
context.services.scenes.pop()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@dataclass(frozen=True)
|
|
156
|
+
class ChangeSceneCommand(Command):
|
|
157
|
+
"""
|
|
158
|
+
Change the current scene to the specified scene.
|
|
159
|
+
|
|
160
|
+
:ivar scene_id (str): Identifier of the scene to switch to.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
scene_id: str
|
|
164
|
+
|
|
165
|
+
def execute(
|
|
166
|
+
self,
|
|
167
|
+
context: CommandContext,
|
|
168
|
+
):
|
|
169
|
+
context.services.scenes.change(self.scene_id)
|