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.
Files changed (103) hide show
  1. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/PKG-INFO +1 -1
  2. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/pyproject.toml +1 -1
  3. mini_arcade_core-1.2.1/src/mini_arcade_core/__init__.py +86 -0
  4. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/__init__.py +1 -2
  5. mini_arcade_core-1.2.1/src/mini_arcade_core/backend/backend.py +322 -0
  6. mini_arcade_core-1.2.1/src/mini_arcade_core/backend/types.py +13 -0
  7. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/commands.py +8 -8
  8. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/game.py +154 -0
  9. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/game_config.py +40 -0
  10. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/gameplay_settings.py +24 -0
  11. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/config.py +20 -0
  12. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/hooks.py +77 -0
  13. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/runner.py +272 -0
  14. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/loop/state.py +32 -0
  15. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/managers.py +24 -0
  16. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/context.py +0 -2
  17. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/base.py +2 -2
  18. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/crt.py +4 -4
  19. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/registry.py +1 -1
  20. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/effects/vignette.py +8 -8
  21. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
  22. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/end_frame.py +1 -1
  23. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/postfx.py +1 -1
  24. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/ui.py +1 -1
  25. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/world.py +6 -6
  26. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/pipeline.py +7 -6
  27. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/viewport.py +10 -4
  28. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/scenes/models.py +54 -0
  29. mini_arcade_core-1.2.1/src/mini_arcade_core/engine/scenes/scene_manager.py +213 -0
  30. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
  31. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/audio/audio_port.py +0 -4
  32. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/capture/capture_adapter.py +53 -31
  33. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/capture/capture_port.py +0 -4
  34. mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/capture/capture_worker.py +174 -0
  35. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/context.py +8 -6
  36. mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
  37. mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
  38. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/services.py +3 -2
  39. mini_arcade_core-1.2.1/src/mini_arcade_core/runtime/window/window_adapter.py +92 -0
  40. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/window/window_port.py +3 -17
  41. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/debug_overlay.py +5 -4
  42. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/registry.py +11 -1
  43. 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
  44. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/ui/menu.py +54 -16
  45. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/utils/__init__.py +2 -1
  46. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/utils/logging.py +47 -18
  47. mini_arcade_core-1.2.1/src/mini_arcade_core/utils/profiler.py +283 -0
  48. mini_arcade_core-1.1.1/src/mini_arcade_core/__init__.py +0 -114
  49. mini_arcade_core-1.1.1/src/mini_arcade_core/backend/backend.py +0 -324
  50. mini_arcade_core-1.1.1/src/mini_arcade_core/backend/types.py +0 -9
  51. mini_arcade_core-1.1.1/src/mini_arcade_core/engine/game.py +0 -454
  52. mini_arcade_core-1.1.1/src/mini_arcade_core/managers/inputs.py +0 -284
  53. mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
  54. mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/scene/scene_port.py +0 -170
  55. mini_arcade_core-1.1.1/src/mini_arcade_core/runtime/window/window_adapter.py +0 -90
  56. mini_arcade_core-1.1.1/src/mini_arcade_core/scenes/sim_scene.py +0 -41
  57. mini_arcade_core-1.1.1/src/mini_arcade_core/sim/runner.py +0 -222
  58. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/LICENSE +0 -0
  59. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/README.md +0 -0
  60. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/events.py +0 -0
  61. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/keys.py +0 -0
  62. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/backend/sdl_map.py +0 -0
  63. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/bus.py +0 -0
  64. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/__init__.py +0 -0
  65. {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
  66. {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
  67. {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
  68. {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
  69. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/frame_packet.py +0 -0
  70. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/packet.py +0 -0
  71. {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
  72. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/base.py +0 -0
  73. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/passes/lighting.py +0 -0
  74. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/engine/render/render_service.py +0 -0
  75. {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
  76. {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
  77. {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
  78. {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
  79. {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
  80. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/file/file_adapter.py +0 -0
  81. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/file/file_port.py +0 -0
  82. {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
  83. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/input/input_adapter.py +0 -0
  84. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/input/input_port.py +0 -0
  85. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/input_frame.py +0 -0
  86. {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
  87. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/runtime/render/render_port.py +0 -0
  88. {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
  89. {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
  90. {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
  91. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/autoreg.py +0 -0
  92. {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
  93. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/systems/base_system.py +0 -0
  94. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/scenes/systems/system_pipeline.py +0 -0
  95. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/__init__.py +0 -0
  96. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/__init__.py +0 -0
  97. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/boundaries2d.py +0 -0
  98. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/collision2d.py +0 -0
  99. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/geometry2d.py +0 -0
  100. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/kinematics2d.py +0 -0
  101. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/spaces/d2/physics2d.py +0 -0
  102. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/ui/__init__.py +0 -0
  103. {mini_arcade_core-1.1.1 → mini_arcade_core-1.2.1}/src/mini_arcade_core/utils/deprecated_decorator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 1.1.1
3
+ Version: 1.2.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 = "1.1.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()
@@ -6,9 +6,8 @@ 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, WindowSettings
9
+ from .backend import Backend
10
10
 
11
11
  __all__ = [
12
12
  "Backend",
13
- "WindowSettings",
14
13
  ]
@@ -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.runtime.scene.scene_port import ScenePolicy
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
- commands: Optional["CommandQueue"] = None
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.services.scenes.quit()
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.services.scenes.push(self.scene_id, as_overlay=self.as_overlay)
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.services.scenes.pop()
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.services.scenes.change(self.scene_id)
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.services.scenes
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) -> None:
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