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.
Files changed (87) hide show
  1. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/PKG-INFO +1 -1
  2. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/pyproject.toml +1 -1
  3. mini_arcade_core-1.0.1/src/mini_arcade_core/__init__.py +114 -0
  4. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/backend/__init__.py +2 -6
  5. mini_arcade_core-1.0.1/src/mini_arcade_core/backend/backend.py +291 -0
  6. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/backend/events.py +1 -1
  7. 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
  8. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/__init__.py +0 -0
  9. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/commands.py +169 -0
  10. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/game.py +369 -0
  11. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/__init__.py +0 -0
  12. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/packet.py +56 -0
  13. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/pipeline.py +63 -0
  14. mini_arcade_core-1.0.1/src/mini_arcade_core/engine/render/viewport.py +203 -0
  15. mini_arcade_core-1.0.1/src/mini_arcade_core/managers/__init__.py +0 -0
  16. mini_arcade_core-1.0.1/src/mini_arcade_core/managers/cheats.py +186 -0
  17. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/managers/inputs.py +5 -3
  18. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/__init__.py +0 -0
  19. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio/__init__.py +0 -0
  20. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio/audio_adapter.py +20 -0
  21. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/audio/audio_port.py +36 -0
  22. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture/__init__.py +0 -0
  23. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
  24. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/capture/capture_port.py +51 -0
  25. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/context.py +53 -0
  26. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file/__init__.py +0 -0
  27. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file/file_adapter.py +20 -0
  28. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/file/file_port.py +31 -0
  29. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input/__init__.py +0 -0
  30. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input/input_adapter.py +49 -0
  31. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input/input_port.py +31 -0
  32. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/input_frame.py +71 -0
  33. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene/__init__.py +0 -0
  34. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
  35. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/scene/scene_port.py +149 -0
  36. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/services.py +35 -0
  37. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window/__init__.py +0 -0
  38. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window/window_adapter.py +90 -0
  39. mini_arcade_core-1.0.1/src/mini_arcade_core/runtime/window/window_port.py +109 -0
  40. mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/__init__.py +0 -0
  41. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/scenes/autoreg.py +1 -1
  42. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/scenes/registry.py +21 -19
  43. mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/sim_scene.py +41 -0
  44. mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems/__init__.py +0 -0
  45. mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems/base_system.py +40 -0
  46. mini_arcade_core-1.0.1/src/mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
  47. mini_arcade_core-1.0.1/src/mini_arcade_core/sim/__init__.py +0 -0
  48. mini_arcade_core-1.0.1/src/mini_arcade_core/sim/protocols.py +41 -0
  49. mini_arcade_core-1.0.1/src/mini_arcade_core/sim/runner.py +222 -0
  50. mini_arcade_core-1.0.1/src/mini_arcade_core/spaces/__init__.py +0 -0
  51. mini_arcade_core-1.0.1/src/mini_arcade_core/spaces/d2/__init__.py +0 -0
  52. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
  53. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/collision2d.py +25 -28
  54. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/geometry2d.py +18 -0
  55. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
  56. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/spaces/d2/physics2d.py +9 -0
  57. mini_arcade_core-1.0.1/src/mini_arcade_core/ui/__init__.py +0 -0
  58. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/ui/menu.py +271 -85
  59. mini_arcade_core-1.0.1/src/mini_arcade_core/utils/__init__.py +10 -0
  60. mini_arcade_core-1.0.1/src/mini_arcade_core/utils/deprecated_decorator.py +45 -0
  61. mini_arcade_core-1.0.1/src/mini_arcade_core/utils/logging.py +168 -0
  62. mini_arcade_core-0.10.0/src/mini_arcade_core/__init__.py +0 -126
  63. mini_arcade_core-0.10.0/src/mini_arcade_core/backend/backend.py +0 -151
  64. mini_arcade_core-0.10.0/src/mini_arcade_core/commands.py +0 -84
  65. mini_arcade_core-0.10.0/src/mini_arcade_core/entity.py +0 -72
  66. mini_arcade_core-0.10.0/src/mini_arcade_core/game.py +0 -287
  67. mini_arcade_core-0.10.0/src/mini_arcade_core/keymaps/__init__.py +0 -15
  68. mini_arcade_core-0.10.0/src/mini_arcade_core/managers/__init__.py +0 -22
  69. mini_arcade_core-0.10.0/src/mini_arcade_core/managers/base.py +0 -132
  70. mini_arcade_core-0.10.0/src/mini_arcade_core/managers/cheats.py +0 -355
  71. mini_arcade_core-0.10.0/src/mini_arcade_core/managers/entities.py +0 -38
  72. mini_arcade_core-0.10.0/src/mini_arcade_core/managers/overlays.py +0 -53
  73. mini_arcade_core-0.10.0/src/mini_arcade_core/managers/system.py +0 -26
  74. mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/__init__.py +0 -22
  75. mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/model.py +0 -34
  76. mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/runtime.py +0 -29
  77. mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/scene.py +0 -109
  78. mini_arcade_core-0.10.0/src/mini_arcade_core/scenes/system.py +0 -69
  79. mini_arcade_core-0.10.0/src/mini_arcade_core/spaces/__init__.py +0 -12
  80. mini_arcade_core-0.10.0/src/mini_arcade_core/spaces/d2/__init__.py +0 -30
  81. mini_arcade_core-0.10.0/src/mini_arcade_core/ui/__init__.py +0 -26
  82. mini_arcade_core-0.10.0/src/mini_arcade_core/ui/overlays.py +0 -41
  83. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/LICENSE +0 -0
  84. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/README.md +0 -0
  85. {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
  86. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/backend/types.py +0 -0
  87. {mini_arcade_core-0.10.0 → mini_arcade_core-1.0.1}/src/mini_arcade_core/bus.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 0.10.0
3
+ Version: 1.0.1
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "mini-arcade-core"
7
- version = "0.10.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
- "Event",
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
@@ -8,7 +8,7 @@ from dataclasses import dataclass
8
8
  from enum import Enum, auto
9
9
  from typing import Optional, Tuple
10
10
 
11
- from mini_arcade_core.keymaps.keys import Key
11
+ from mini_arcade_core.backend.keys import Key
12
12
 
13
13
 
14
14
  class EventType(Enum):
@@ -8,7 +8,7 @@ Maps SDL keycodes to mini arcade core Key enums.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from mini_arcade_core.keymaps.keys import Key
11
+ from mini_arcade_core.backend.keys import Key
12
12
 
13
13
  # SDL keycodes you need (minimal set)
14
14
  _SDLK_ESCAPE = 27
@@ -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)