mini-arcade-core 0.10.0__py3-none-any.whl → 1.0.1__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.
Files changed (78) hide show
  1. mini_arcade_core/__init__.py +51 -63
  2. mini_arcade_core/backend/__init__.py +2 -6
  3. mini_arcade_core/backend/backend.py +148 -8
  4. mini_arcade_core/backend/events.py +1 -1
  5. mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
  6. mini_arcade_core/engine/__init__.py +0 -0
  7. mini_arcade_core/engine/commands.py +169 -0
  8. mini_arcade_core/engine/game.py +369 -0
  9. mini_arcade_core/engine/render/__init__.py +0 -0
  10. mini_arcade_core/engine/render/packet.py +56 -0
  11. mini_arcade_core/engine/render/pipeline.py +63 -0
  12. mini_arcade_core/engine/render/viewport.py +203 -0
  13. mini_arcade_core/managers/__init__.py +0 -22
  14. mini_arcade_core/managers/cheats.py +71 -240
  15. mini_arcade_core/managers/inputs.py +5 -3
  16. mini_arcade_core/runtime/__init__.py +0 -0
  17. mini_arcade_core/runtime/audio/__init__.py +0 -0
  18. mini_arcade_core/runtime/audio/audio_adapter.py +20 -0
  19. mini_arcade_core/runtime/audio/audio_port.py +36 -0
  20. mini_arcade_core/runtime/capture/__init__.py +0 -0
  21. mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
  22. mini_arcade_core/runtime/capture/capture_port.py +51 -0
  23. mini_arcade_core/runtime/context.py +53 -0
  24. mini_arcade_core/runtime/file/__init__.py +0 -0
  25. mini_arcade_core/runtime/file/file_adapter.py +20 -0
  26. mini_arcade_core/runtime/file/file_port.py +31 -0
  27. mini_arcade_core/runtime/input/__init__.py +0 -0
  28. mini_arcade_core/runtime/input/input_adapter.py +49 -0
  29. mini_arcade_core/runtime/input/input_port.py +31 -0
  30. mini_arcade_core/runtime/input_frame.py +71 -0
  31. mini_arcade_core/runtime/scene/__init__.py +0 -0
  32. mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
  33. mini_arcade_core/runtime/scene/scene_port.py +149 -0
  34. mini_arcade_core/runtime/services.py +35 -0
  35. mini_arcade_core/runtime/window/__init__.py +0 -0
  36. mini_arcade_core/runtime/window/window_adapter.py +90 -0
  37. mini_arcade_core/runtime/window/window_port.py +109 -0
  38. mini_arcade_core/scenes/__init__.py +0 -22
  39. mini_arcade_core/scenes/autoreg.py +1 -1
  40. mini_arcade_core/scenes/registry.py +21 -19
  41. mini_arcade_core/scenes/sim_scene.py +41 -0
  42. mini_arcade_core/scenes/systems/__init__.py +0 -0
  43. mini_arcade_core/scenes/systems/base_system.py +40 -0
  44. mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
  45. mini_arcade_core/sim/__init__.py +0 -0
  46. mini_arcade_core/sim/protocols.py +41 -0
  47. mini_arcade_core/sim/runner.py +222 -0
  48. mini_arcade_core/spaces/__init__.py +0 -12
  49. mini_arcade_core/spaces/d2/__init__.py +0 -30
  50. mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
  51. mini_arcade_core/spaces/d2/collision2d.py +25 -28
  52. mini_arcade_core/spaces/d2/geometry2d.py +18 -0
  53. mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
  54. mini_arcade_core/spaces/d2/physics2d.py +9 -0
  55. mini_arcade_core/ui/__init__.py +0 -26
  56. mini_arcade_core/ui/menu.py +271 -85
  57. mini_arcade_core/utils/__init__.py +10 -0
  58. mini_arcade_core/utils/deprecated_decorator.py +45 -0
  59. mini_arcade_core/utils/logging.py +168 -0
  60. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/METADATA +1 -1
  61. mini_arcade_core-1.0.1.dist-info/RECORD +66 -0
  62. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/WHEEL +1 -1
  63. mini_arcade_core/commands.py +0 -84
  64. mini_arcade_core/entity.py +0 -72
  65. mini_arcade_core/game.py +0 -287
  66. mini_arcade_core/keymaps/__init__.py +0 -15
  67. mini_arcade_core/managers/base.py +0 -132
  68. mini_arcade_core/managers/entities.py +0 -38
  69. mini_arcade_core/managers/overlays.py +0 -53
  70. mini_arcade_core/managers/system.py +0 -26
  71. mini_arcade_core/scenes/model.py +0 -34
  72. mini_arcade_core/scenes/runtime.py +0 -29
  73. mini_arcade_core/scenes/scene.py +0 -109
  74. mini_arcade_core/scenes/system.py +0 -69
  75. mini_arcade_core/ui/overlays.py +0 -41
  76. mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
  77. /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
  78. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -5,31 +5,21 @@ Provides access to core classes and a convenience function to run a game.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import logging
8
+ import traceback
9
9
  from importlib.metadata import PackageNotFoundError, version
10
10
  from typing import Callable, Type, Union
11
11
 
12
- from mini_arcade_core import backend # noqa: F401
13
- from mini_arcade_core import keymaps # noqa: F401
14
- from mini_arcade_core import managers # noqa: F401
15
- from mini_arcade_core import spaces # noqa: F401
16
- from mini_arcade_core import ui # noqa: F401
17
- from mini_arcade_core.bus import event_bus
18
- from mini_arcade_core.commands import (
19
- BaseCommand,
20
- BaseGameCommand,
21
- BaseSceneCommand,
22
- QuitGameCommand,
23
- )
24
- from mini_arcade_core.entity import Entity, KinematicEntity, SpriteEntity
25
- from mini_arcade_core.game import Game, GameConfig
26
- from mini_arcade_core.scenes import Scene, SceneRegistry
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
27
16
 
28
- SceneFactoryLike = Union[Type[Scene], Callable[[Game], Scene]]
29
-
30
- logger = logging.getLogger(__name__)
17
+ SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
31
18
 
32
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
33
23
  def run_game(
34
24
  scene: SceneFactoryLike | None = None,
35
25
  config: GameConfig | None = None,
@@ -40,11 +30,14 @@ def run_game(
40
30
  Convenience helper to bootstrap and run a game with a single scene.
41
31
 
42
32
  Supports both:
43
- - run_game(SceneClass, cfg) # legacy
44
- - run_game(config=cfg, initial_scene="main", registry=...) # registry-based
45
- - run_game(cfg) # config-only
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
46
39
 
47
- :param initial_scene: The Scene ID to start the game with.
40
+ :param initial_scene: The SimScene ID to start the game with.
48
41
  :type initial_scene: str
49
42
 
50
43
  :param config: Optional GameConfig to customize game settings.
@@ -55,47 +48,35 @@ def run_game(
55
48
 
56
49
  :raises ValueError: If the provided config does not have a valid Backend.
57
50
  """
58
- # Handle run_game(cfg) where the first arg is actually a GameConfig
59
- if isinstance(scene, GameConfig) and config is None:
60
- config = scene
61
- scene = None
62
-
63
- cfg = config or GameConfig()
64
- if cfg.backend is None:
65
- raise ValueError(
66
- "GameConfig.backend must be set to a Backend instance"
67
- )
68
-
69
- # If user provided a Scene factory/class, ensure it's registered
70
- if scene is not None:
71
- if registry is None:
72
- registry = SceneRegistry(_factories={})
73
- registry.register(
74
- initial_scene, scene
75
- ) # Scene class is callable(game) -> Scene
76
-
77
- game = Game(cfg, registry=registry)
78
- game.run(initial_scene)
79
-
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
80
79
 
81
- __all__ = [
82
- "Game",
83
- "GameConfig",
84
- "Entity",
85
- "SpriteEntity",
86
- "KinematicEntity",
87
- "BaseCommand",
88
- "BaseGameCommand",
89
- "BaseSceneCommand",
90
- "QuitGameCommand",
91
- "event_bus",
92
- "run_game",
93
- "backend",
94
- "keymaps",
95
- "managers",
96
- "spaces",
97
- "ui",
98
- ]
99
80
 
100
81
  PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
101
82
 
@@ -123,4 +104,11 @@ def get_version() -> str:
123
104
  return "0.0.0"
124
105
 
125
106
 
107
+ __all__ = [
108
+ "Game",
109
+ "GameConfig",
110
+ "WindowConfig",
111
+ "run_game",
112
+ ]
113
+
126
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
  ]
@@ -5,12 +5,29 @@ This is the only part of the code that talks to SDL/pygame directly.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from dataclasses import dataclass
8
9
  from typing import Iterable, Protocol
9
10
 
10
11
  from .events import Event
11
12
  from .types import Color
12
13
 
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
14
31
  class Backend(Protocol):
15
32
  """
16
33
  Interface that any rendering/input backend must implement.
@@ -18,20 +35,23 @@ class Backend(Protocol):
18
35
  mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
19
36
  """
20
37
 
21
- def init(self, width: int, height: int, title: str):
38
+ def init(self, window_settings: WindowSettings):
22
39
  """
23
40
  Initialize the backend and open a window.
24
41
  Should be called once before the main loop.
25
42
 
26
- :param width: Width of the window in pixels.
27
- :type width: int
43
+ :param window_settings: Settings for the backend window.
44
+ :type window_settings: WindowSettings
45
+ """
28
46
 
29
- :param height: Height of the window in pixels.
30
- :type height: int
47
+ def set_window_title(self, title: str):
48
+ """
49
+ Set the window title.
31
50
 
32
- :param title: Title of the window.
51
+ :param title: The new title for the window.
33
52
  :type title: str
34
53
  """
54
+ raise NotImplementedError
35
55
 
36
56
  def poll_events(self) -> Iterable[Event]:
37
57
  """
@@ -96,14 +116,13 @@ class Backend(Protocol):
96
116
  :type color: Color
97
117
  """
98
118
 
99
- # pylint: enable=too-many-arguments,too-many-positional-arguments
100
-
101
119
  def draw_text(
102
120
  self,
103
121
  x: int,
104
122
  y: int,
105
123
  text: str,
106
124
  color: Color = (255, 255, 255),
125
+ font_size: int | None = None,
107
126
  ):
108
127
  """
109
128
  Draw text at the given position in a default font and color.
@@ -124,6 +143,8 @@ class Backend(Protocol):
124
143
  :type color: Color
125
144
  """
126
145
 
146
+ # pylint: enable=too-many-arguments,too-many-positional-arguments
147
+
127
148
  def measure_text(self, text: str) -> tuple[int, int]:
128
149
  """
129
150
  Measure the width and height of the given text string in pixels.
@@ -149,3 +170,122 @@ class Backend(Protocol):
149
170
  :rtype: bytes | None
150
171
  """
151
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
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)