mini-arcade-core 0.10.0__py3-none-any.whl → 1.0.0__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 (76) hide show
  1. mini_arcade_core/__init__.py +43 -60
  2. mini_arcade_core/backend/__init__.py +0 -5
  3. mini_arcade_core/backend/backend.py +9 -0
  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 +354 -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 +39 -0
  12. mini_arcade_core/managers/__init__.py +0 -22
  13. mini_arcade_core/managers/cheats.py +71 -240
  14. mini_arcade_core/managers/inputs.py +5 -1
  15. mini_arcade_core/runtime/__init__.py +0 -0
  16. mini_arcade_core/runtime/audio/__init__.py +0 -0
  17. mini_arcade_core/runtime/audio/audio_adapter.py +13 -0
  18. mini_arcade_core/runtime/audio/audio_port.py +17 -0
  19. mini_arcade_core/runtime/capture/__init__.py +0 -0
  20. mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
  21. mini_arcade_core/runtime/capture/capture_port.py +32 -0
  22. mini_arcade_core/runtime/context.py +53 -0
  23. mini_arcade_core/runtime/file/__init__.py +0 -0
  24. mini_arcade_core/runtime/file/file_adapter.py +20 -0
  25. mini_arcade_core/runtime/file/file_port.py +31 -0
  26. mini_arcade_core/runtime/input/__init__.py +0 -0
  27. mini_arcade_core/runtime/input/input_adapter.py +49 -0
  28. mini_arcade_core/runtime/input/input_port.py +31 -0
  29. mini_arcade_core/runtime/input_frame.py +71 -0
  30. mini_arcade_core/runtime/scene/__init__.py +0 -0
  31. mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
  32. mini_arcade_core/runtime/scene/scene_port.py +149 -0
  33. mini_arcade_core/runtime/services.py +35 -0
  34. mini_arcade_core/runtime/window/__init__.py +0 -0
  35. mini_arcade_core/runtime/window/window_adapter.py +26 -0
  36. mini_arcade_core/runtime/window/window_port.py +47 -0
  37. mini_arcade_core/scenes/__init__.py +0 -22
  38. mini_arcade_core/scenes/autoreg.py +1 -1
  39. mini_arcade_core/scenes/registry.py +21 -19
  40. mini_arcade_core/scenes/sim_scene.py +41 -0
  41. mini_arcade_core/scenes/systems/__init__.py +0 -0
  42. mini_arcade_core/scenes/systems/base_system.py +40 -0
  43. mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
  44. mini_arcade_core/sim/__init__.py +0 -0
  45. mini_arcade_core/sim/protocols.py +41 -0
  46. mini_arcade_core/sim/runner.py +222 -0
  47. mini_arcade_core/spaces/__init__.py +0 -12
  48. mini_arcade_core/spaces/d2/__init__.py +0 -30
  49. mini_arcade_core/spaces/d2/collision2d.py +25 -28
  50. mini_arcade_core/spaces/d2/geometry2d.py +18 -0
  51. mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
  52. mini_arcade_core/spaces/d2/physics2d.py +9 -0
  53. mini_arcade_core/ui/__init__.py +0 -26
  54. mini_arcade_core/ui/menu.py +265 -84
  55. mini_arcade_core/utils/__init__.py +10 -0
  56. mini_arcade_core/utils/deprecated_decorator.py +45 -0
  57. mini_arcade_core/utils/logging.py +174 -0
  58. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.0.dist-info}/METADATA +1 -1
  59. mini_arcade_core-1.0.0.dist-info/RECORD +65 -0
  60. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.0.dist-info}/WHEEL +1 -1
  61. mini_arcade_core/commands.py +0 -84
  62. mini_arcade_core/entity.py +0 -72
  63. mini_arcade_core/game.py +0 -287
  64. mini_arcade_core/keymaps/__init__.py +0 -15
  65. mini_arcade_core/managers/base.py +0 -132
  66. mini_arcade_core/managers/entities.py +0 -38
  67. mini_arcade_core/managers/overlays.py +0 -53
  68. mini_arcade_core/managers/system.py +0 -26
  69. mini_arcade_core/scenes/model.py +0 -34
  70. mini_arcade_core/scenes/runtime.py +0 -29
  71. mini_arcade_core/scenes/scene.py +0 -109
  72. mini_arcade_core/scenes/system.py +0 -69
  73. mini_arcade_core/ui/overlays.py +0 -41
  74. mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
  75. /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
  76. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,354 @@
1
+ """
2
+ Game core module defining the Game class and configuration.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from time import perf_counter, sleep
9
+ from typing import Dict, Literal
10
+
11
+ from mini_arcade_core.backend import Backend
12
+ from mini_arcade_core.engine.commands import (
13
+ CommandContext,
14
+ CommandQueue,
15
+ QuitCommand,
16
+ )
17
+
18
+ # from mini_arcade_core.sim.runner import SimRunner, SimRunnerConfig
19
+ from mini_arcade_core.engine.render.packet import RenderPacket
20
+ from mini_arcade_core.engine.render.pipeline import RenderPipeline
21
+ from mini_arcade_core.managers.cheats import CheatManager
22
+ from mini_arcade_core.runtime.audio.audio_adapter import NullAudioAdapter
23
+ from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
24
+ from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
25
+ from mini_arcade_core.runtime.input.input_adapter import InputAdapter
26
+ from mini_arcade_core.runtime.input_frame import InputFrame
27
+ from mini_arcade_core.runtime.scene.scene_adapter import SceneAdapter
28
+ from mini_arcade_core.runtime.services import RuntimeServices
29
+ from mini_arcade_core.runtime.window.window_adapter import WindowAdapter
30
+ from mini_arcade_core.scenes.registry import SceneRegistry
31
+ from mini_arcade_core.utils import logger
32
+
33
+
34
+ @dataclass
35
+ class WindowConfig:
36
+ """
37
+ Configuration for a game window (not implemented).
38
+
39
+ :ivar width (int): Width of the window in pixels.
40
+ :ivar height (int): Height of the window in pixels.
41
+ :ivar background_color (tuple[int, int, int]): RGB background color.
42
+ :ivar title (str): Title of the window.
43
+ """
44
+
45
+ width: int
46
+ height: int
47
+ background_color: tuple[int, int, int]
48
+ title: str
49
+
50
+
51
+ @dataclass
52
+ class GameConfig:
53
+ """
54
+ Configuration options for the Game.
55
+
56
+ :ivar window (WindowConfig | None): Optional window configuration.
57
+ :ivar fps (int): Target frames per second.
58
+ :ivar backend (Backend | None): Optional Backend instance to use for rendering and input.
59
+ """
60
+
61
+ window: WindowConfig | None = None
62
+ fps: int = 60
63
+ backend: Backend | None = None
64
+
65
+
66
+ Difficulty = Literal["easy", "normal", "hard", "insane"]
67
+
68
+
69
+ @dataclass
70
+ class GameSettings:
71
+ """
72
+ Game settings that can be modified during gameplay.
73
+
74
+ :ivar difficulty (Difficulty): Current game difficulty level.
75
+ """
76
+
77
+ difficulty: Difficulty = "normal"
78
+
79
+
80
+ def _neutral_input(frame_index: int, dt: float) -> InputFrame:
81
+ """Create a neutral InputFrame with no input events."""
82
+ return InputFrame(frame_index=frame_index, dt=dt)
83
+
84
+
85
+ @dataclass
86
+ class FrameTimer:
87
+ """
88
+ Simple frame timer for marking and reporting time intervals.
89
+
90
+ :ivar enabled (bool): Whether timing is enabled.
91
+ :ivar marks (Dict[str, float]): Recorded time marks.
92
+ """
93
+
94
+ enabled: bool = True
95
+ marks: Dict[str, float] = field(default_factory=dict)
96
+
97
+ def mark(self, name: str):
98
+ """
99
+ Record a time mark with the given name.
100
+
101
+ :param name: Name of the mark.
102
+ :type name: str
103
+ """
104
+ if not self.enabled:
105
+ return
106
+ self.marks[name] = perf_counter()
107
+
108
+ def diff_ms(self, start: str, end: str) -> float:
109
+ """
110
+ Get the time difference in milliseconds between two marks.
111
+
112
+ :param start: Name of the start mark.
113
+ :type start: str
114
+
115
+ :param end: Name of the end mark.
116
+ :type end: str
117
+
118
+ :return: Time difference in milliseconds.
119
+ :rtype: float
120
+ """
121
+ return (self.marks[end] - self.marks[start]) * 1000.0
122
+
123
+ def report_ms(self) -> Dict[str, float]:
124
+ """
125
+ Returns diffs between consecutive marks in insertion order.
126
+
127
+ :return: Dictionary mapping "start->end" to time difference in milliseconds.
128
+ :rtype: Dict[str, float]
129
+ """
130
+ if not self.enabled:
131
+ return {}
132
+
133
+ keys = list(self.marks.keys())
134
+ out: Dict[str, float] = {}
135
+ for a, b in zip(keys, keys[1:]):
136
+ out[f"{a}->{b}"] = self.diff_ms(a, b)
137
+ return out
138
+
139
+ def clear(self):
140
+ """Clear all recorded marks."""
141
+ if not self.enabled:
142
+ return
143
+ self.marks.clear()
144
+
145
+
146
+ # TODO: Fix too-many-instance-attributes warning
147
+ # Justification: Core game class with many dependencies.
148
+ # pylint: disable=too-many-instance-attributes
149
+ class Game:
150
+ """Core game object responsible for managing the main loop and active scene."""
151
+
152
+ def __init__(
153
+ self, config: GameConfig, registry: SceneRegistry | None = None
154
+ ):
155
+ """
156
+ :param config: Game configuration options.
157
+ :type config: GameConfig
158
+
159
+ :param registry: Optional SceneRegistry for scene management.
160
+ :type registry: SceneRegistry | None
161
+
162
+ :raises ValueError: If the provided config does not have a valid Backend.
163
+ """
164
+ self.config = config
165
+ self._running: bool = False
166
+
167
+ if config.backend is None:
168
+ raise ValueError(
169
+ "GameConfig.backend must be set to a Backend instance"
170
+ )
171
+ if config.window is None:
172
+ raise ValueError("GameConfig.window must be set")
173
+
174
+ self.backend: Backend = config.backend
175
+ self.registry = registry or SceneRegistry(_factories={})
176
+ self.settings = GameSettings()
177
+ self.services = RuntimeServices(
178
+ window=WindowAdapter(
179
+ self.backend,
180
+ ),
181
+ scenes=SceneAdapter(self.registry, self),
182
+ audio=NullAudioAdapter(),
183
+ files=LocalFilesAdapter(),
184
+ capture=CaptureAdapter(self.backend),
185
+ input=InputAdapter(),
186
+ )
187
+
188
+ self.command_queue = CommandQueue()
189
+ self.cheat_manager = CheatManager()
190
+
191
+ def quit(self):
192
+ """Request that the main loop stops."""
193
+ self._running = False
194
+
195
+ # TODO: Fix too-many-statements and too-many-locals warnings
196
+ # Justification: Main game loop with multiple responsibilities.
197
+ # pylint: disable=too-many-statements,too-many-locals
198
+ def run(self, initial_scene_id: str):
199
+ """
200
+ Run the main loop starting with the given scene.
201
+
202
+ This is intentionally left abstract so you can plug pygame, pyglet,
203
+ or another backend.
204
+
205
+ :param initial_scene_id: The scene id to start the game with (must be registered).
206
+ :type initial_scene_id: str
207
+ """
208
+ backend = self.backend
209
+
210
+ self._initialize_window()
211
+
212
+ self.services.scenes.change(initial_scene_id)
213
+
214
+ pipeline = RenderPipeline()
215
+
216
+ self._running = True
217
+ target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
218
+ last_time = perf_counter()
219
+ frame_index = 0
220
+
221
+ # cache packets so blocked-update scenes still render their last frame
222
+ packet_cache: dict[int, RenderPacket] = {}
223
+
224
+ timer = FrameTimer(enabled=True)
225
+ report_every = 60 # print once per second at 60fps
226
+
227
+ # TODO: Integrate SimRunner for simulation stepping
228
+ # TODO: Fix assignment-from-no-return warning in self.services.input.build
229
+ # & self.services.scenes.input_entry
230
+ # Justification: These methods are expected to return values.
231
+ # pylint: disable=assignment-from-no-return
232
+
233
+ while self._running:
234
+ timer.clear()
235
+ timer.mark("frame_start")
236
+
237
+ now = perf_counter()
238
+ dt = now - last_time
239
+ last_time = now
240
+
241
+ events = list(backend.poll_events())
242
+ timer.mark("events_polled")
243
+
244
+ input_frame = self.services.input.build(events, frame_index, dt)
245
+ timer.mark("input_built")
246
+
247
+ # Window/OS quit (close button)
248
+ if input_frame.quit:
249
+ self.command_queue.push(QuitCommand())
250
+
251
+ # who gets input?
252
+ input_entry = self.services.scenes.input_entry()
253
+ if input_entry is None:
254
+ break
255
+
256
+ # tick policy-aware scenes
257
+ timer.mark("tick_start")
258
+ for entry in self.services.scenes.update_entries():
259
+ scene = entry.scene
260
+ effective_input = (
261
+ input_frame
262
+ if entry is input_entry
263
+ else _neutral_input(frame_index, dt)
264
+ )
265
+
266
+ packet = scene.tick(effective_input, dt)
267
+ packet_cache[id(scene)] = packet
268
+ timer.mark("tick_end")
269
+
270
+ timer.mark("command_ctx_start")
271
+ command_context = CommandContext(
272
+ services=self.services,
273
+ commands=self.command_queue,
274
+ settings=self.settings,
275
+ world=self._resolve_world(),
276
+ )
277
+ timer.mark("command_ctx_end")
278
+
279
+ timer.mark("cheats_start")
280
+ self.cheat_manager.process_frame(
281
+ input_frame,
282
+ context=command_context,
283
+ queue=self.command_queue,
284
+ )
285
+ timer.mark("cheats_end")
286
+
287
+ # Execute commands at the end of the frame (consistent write path)
288
+ timer.mark("cmd_exec_start")
289
+ for cmd in self.command_queue.drain():
290
+ cmd.execute(command_context)
291
+ timer.mark("cmd_exec_end")
292
+
293
+ timer.mark("render_start")
294
+ backend.begin_frame()
295
+ timer.mark("begin_frame_done")
296
+
297
+ for entry in self.services.scenes.visible_entries():
298
+ scene = entry.scene
299
+ packet = packet_cache.get(id(scene))
300
+ if packet is None:
301
+ # bootstrap (first frame visible but not updated)
302
+ packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
303
+ packet_cache[id(scene)] = packet
304
+
305
+ pipeline.draw_packet(backend, packet)
306
+
307
+ timer.mark("draw_done")
308
+ backend.end_frame()
309
+ timer.mark("end_frame_done")
310
+
311
+ timer.mark("sleep_start")
312
+ if target_dt > 0 and dt < target_dt:
313
+ sleep(target_dt - dt)
314
+ timer.mark("sleep_end")
315
+
316
+ # --- report ---
317
+ if frame_index % report_every == 0 and frame_index > 0:
318
+ ms = timer.report_ms()
319
+ total = (perf_counter() - timer.marks["frame_start"]) * 1000.0
320
+ logger.debug(
321
+ f"[Frame {frame_index}] total={total:.2f}ms | {ms}"
322
+ )
323
+
324
+ frame_index += 1
325
+
326
+ # pylint: enable=assignment-from-no-return
327
+
328
+ # exit remaining scenes
329
+ self.services.scenes.clean()
330
+
331
+ # pylint: enable=too-many-statements,too-many-locals
332
+
333
+ def _initialize_window(self):
334
+ """Initialize the game window based on the configuration."""
335
+ self.services.window.set_window_size(
336
+ self.config.window.width, self.config.window.height
337
+ )
338
+ self.services.window.set_title(self.config.window.title)
339
+
340
+ br, bg, bb = self.config.window.background_color
341
+ self.services.window.set_clear_color(br, bg, bb)
342
+
343
+ def _resolve_world(self) -> object | None:
344
+ # Prefer gameplay world underneath overlays:
345
+ # scan from top to bottom and pick the first scene that has .world
346
+ for entry in reversed(self.services.scenes.visible_entries()):
347
+ scene = entry.scene
348
+ world = getattr(scene, "world", None)
349
+ if world is not None:
350
+ return world
351
+ return None
352
+
353
+
354
+ # pylint: enable=too-many-instance-attributes
File without changes
@@ -0,0 +1,56 @@
1
+ """
2
+ Render packet module.
3
+ Defines the RenderPacket class and related types."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Callable, Iterable, Protocol, runtime_checkable
9
+
10
+ from mini_arcade_core.backend import Backend
11
+
12
+ DrawOp = Callable[[Backend], None]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class RenderPacket:
17
+ """
18
+ Minimal render packet for v1.
19
+
20
+ It is intentionally backend-agnostic: each op is a callable that knows
21
+ how to draw itself using the Backend instance.
22
+
23
+ Later you can replace DrawOp with typed primitives + passes.
24
+ """
25
+
26
+ ops: tuple[DrawOp, ...] = ()
27
+ meta: dict[str, object] = field(default_factory=dict)
28
+
29
+ @staticmethod
30
+ def from_ops(ops: Iterable[DrawOp], **meta: object) -> "RenderPacket":
31
+ """
32
+ Create a RenderPacket from an iterable of DrawOps and optional meta.
33
+
34
+ :param ops: Iterable of DrawOp callables.
35
+ :type ops: Iterable[DrawOp]
36
+
37
+ :return: RenderPacket instance.
38
+ :rtype: RenderPacket
39
+ """
40
+ return RenderPacket(ops=tuple(ops), meta=dict(meta))
41
+
42
+
43
+ # TODO: Implement later
44
+ @runtime_checkable
45
+ class Renderable(Protocol):
46
+ """
47
+ Optional convenience: any object that can produce a RenderPacket.
48
+ """
49
+
50
+ def render(self) -> RenderPacket:
51
+ """
52
+ Produce a RenderPacket for this object.
53
+
54
+ :return: RenderPacket instance.
55
+ :rtype: RenderPacket
56
+ """
@@ -0,0 +1,39 @@
1
+ """
2
+ Render pipeline module.
3
+ Defines the RenderPipeline class for rendering RenderPackets.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from mini_arcade_core.backend import Backend
11
+ from mini_arcade_core.engine.render.packet import RenderPacket
12
+
13
+
14
+ @dataclass
15
+ class RenderPipeline:
16
+ """
17
+ Minimal pipeline for v1.
18
+
19
+ Later you can expand this into passes:
20
+ - build draw list
21
+ - cull
22
+ - sort
23
+ - backend draw pass
24
+ """
25
+
26
+ def draw_packet(self, backend: Backend, packet: RenderPacket):
27
+ """
28
+ Draw the given RenderPacket using the provided Backend.
29
+
30
+ :param backend: Backend to use for drawing.
31
+ :type backend: Backend
32
+
33
+ :param packet: RenderPacket to draw.
34
+ :type packet: RenderPacket
35
+ """
36
+ if not packet:
37
+ return
38
+ for op in packet.ops:
39
+ op(backend)
@@ -1,22 +0,0 @@
1
- """
2
- Managers module for Mini Arcade Core.
3
- Provides various manager classes for handling game entities and resources.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from .cheats import BaseCheatCommand, CheatCode, CheatManager
9
- from .entities import EntityManager
10
- from .inputs import InputManager
11
- from .overlays import OverlayManager
12
- from .system import SystemManager
13
-
14
- __all__ = [
15
- "EntityManager",
16
- "OverlayManager",
17
- "CheatCode",
18
- "CheatManager",
19
- "BaseCheatCommand",
20
- "InputManager",
21
- "SystemManager",
22
- ]