mini-arcade-core 1.0.0__py3-none-any.whl → 1.0.2__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.
@@ -18,6 +18,8 @@ SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
18
18
 
19
19
 
20
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
21
23
  def run_game(
22
24
  scene: SceneFactoryLike | None = None,
23
25
  config: GameConfig | None = None,
@@ -28,9 +30,12 @@ def run_game(
28
30
  Convenience helper to bootstrap and run a game with a single scene.
29
31
 
30
32
  Supports both:
31
- - run_game(SceneClass, cfg) # legacy
32
- - run_game(config=cfg, initial_scene="main", registry=...) # registry-based
33
- - 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
34
39
 
35
40
  :param initial_scene: The SimScene ID to start the game with.
36
41
  :type initial_scene: str
@@ -6,8 +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
9
+ from .backend import Backend, WindowSettings
10
10
 
11
11
  __all__ = [
12
12
  "Backend",
13
+ "WindowSettings",
13
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,19 +35,13 @@ 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
28
-
29
- :param height: Height of the window in pixels.
30
- :type height: int
31
-
32
- :param title: Title of the window.
33
- :type title: str
43
+ :param window_settings: Settings for the backend window.
44
+ :type window_settings: WindowSettings
34
45
  """
35
46
 
36
47
  def set_window_title(self, title: str):
@@ -105,14 +116,13 @@ class Backend(Protocol):
105
116
  :type color: Color
106
117
  """
107
118
 
108
- # pylint: enable=too-many-arguments,too-many-positional-arguments
109
-
110
119
  def draw_text(
111
120
  self,
112
121
  x: int,
113
122
  y: int,
114
123
  text: str,
115
124
  color: Color = (255, 255, 255),
125
+ font_size: int | None = None,
116
126
  ):
117
127
  """
118
128
  Draw text at the given position in a default font and color.
@@ -133,6 +143,8 @@ class Backend(Protocol):
133
143
  :type color: Color
134
144
  """
135
145
 
146
+ # pylint: enable=too-many-arguments,too-many-positional-arguments
147
+
136
148
  def measure_text(self, text: str) -> tuple[int, int]:
137
149
  """
138
150
  Measure the width and height of the given text string in pixels.
@@ -158,3 +170,122 @@ class Backend(Protocol):
158
170
  :rtype: bytes | None
159
171
  """
160
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
@@ -7,6 +7,8 @@ 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
11
+
10
12
  if TYPE_CHECKING:
11
13
  from mini_arcade_core.runtime.services import RuntimeServices
12
14
 
@@ -167,3 +169,27 @@ class ChangeSceneCommand(Command):
167
169
  context: CommandContext,
168
170
  ):
169
171
  context.services.scenes.change(self.scene_id)
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class ToggleDebugOverlayCommand(Command):
176
+ """Toggle the debug overlay scene."""
177
+
178
+ DEBUG_OVERLAY_ID = "debug_overlay"
179
+
180
+ def execute(self, context: CommandContext) -> None:
181
+ scenes = context.services.scenes
182
+ if scenes.has_scene(self.DEBUG_OVERLAY_ID):
183
+ scenes.remove_scene(self.DEBUG_OVERLAY_ID)
184
+ return
185
+
186
+ scenes.push(
187
+ self.DEBUG_OVERLAY_ID,
188
+ as_overlay=True,
189
+ policy=ScenePolicy(
190
+ blocks_update=False,
191
+ blocks_input=False,
192
+ is_opaque=False,
193
+ receives_input=False,
194
+ ),
195
+ )
@@ -8,18 +8,19 @@ from dataclasses import dataclass, field
8
8
  from time import perf_counter, sleep
9
9
  from typing import Dict, Literal
10
10
 
11
- from mini_arcade_core.backend import Backend
11
+ from mini_arcade_core.backend import Backend, WindowSettings
12
+ from mini_arcade_core.backend.events import EventType
13
+ from mini_arcade_core.backend.keys import Key
12
14
  from mini_arcade_core.engine.commands import (
13
15
  CommandContext,
14
16
  CommandQueue,
15
17
  QuitCommand,
18
+ ToggleDebugOverlayCommand,
16
19
  )
17
-
18
- # from mini_arcade_core.sim.runner import SimRunner, SimRunnerConfig
19
20
  from mini_arcade_core.engine.render.packet import RenderPacket
20
21
  from mini_arcade_core.engine.render.pipeline import RenderPipeline
21
22
  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.audio.audio_adapter import SDLAudioAdapter
23
24
  from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
24
25
  from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
25
26
  from mini_arcade_core.runtime.input.input_adapter import InputAdapter
@@ -91,7 +92,7 @@ class FrameTimer:
91
92
  :ivar marks (Dict[str, float]): Recorded time marks.
92
93
  """
93
94
 
94
- enabled: bool = True
95
+ enabled: bool = False
95
96
  marks: Dict[str, float] = field(default_factory=dict)
96
97
 
97
98
  def mark(self, name: str):
@@ -177,9 +178,13 @@ class Game:
177
178
  self.services = RuntimeServices(
178
179
  window=WindowAdapter(
179
180
  self.backend,
181
+ WindowSettings(
182
+ width=self.config.window.width,
183
+ height=self.config.window.height,
184
+ ),
180
185
  ),
181
186
  scenes=SceneAdapter(self.registry, self),
182
- audio=NullAudioAdapter(),
187
+ audio=SDLAudioAdapter(self.backend),
183
188
  files=LocalFilesAdapter(),
184
189
  capture=CaptureAdapter(self.backend),
185
190
  input=InputAdapter(),
@@ -222,7 +227,7 @@ class Game:
222
227
  packet_cache: dict[int, RenderPacket] = {}
223
228
 
224
229
  timer = FrameTimer(enabled=True)
225
- report_every = 60 # print once per second at 60fps
230
+ # report_every = 60 # print once per second at 60fps
226
231
 
227
232
  # TODO: Integrate SimRunner for simulation stepping
228
233
  # TODO: Fix assignment-from-no-return warning in self.services.input.build
@@ -239,6 +244,15 @@ class Game:
239
244
  last_time = now
240
245
 
241
246
  events = list(backend.poll_events())
247
+
248
+ for e in events:
249
+ if e.type == EventType.WINDOWRESIZED and e.size:
250
+ w, h = e.size
251
+ logger.debug(f"Window resized event: {w}x{h}")
252
+ self.services.window.on_window_resized(w, h)
253
+ # if F1 pressed, toggle debug overlay
254
+ if e.type == EventType.KEYDOWN and e.key == Key.F1:
255
+ self.command_queue.push(ToggleDebugOverlayCommand())
242
256
  timer.mark("events_polled")
243
257
 
244
258
  input_frame = self.services.input.build(events, frame_index, dt)
@@ -294,6 +308,7 @@ class Game:
294
308
  backend.begin_frame()
295
309
  timer.mark("begin_frame_done")
296
310
 
311
+ vp = self.services.window.get_viewport()
297
312
  for entry in self.services.scenes.visible_entries():
298
313
  scene = entry.scene
299
314
  packet = packet_cache.get(id(scene))
@@ -302,7 +317,7 @@ class Game:
302
317
  packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
303
318
  packet_cache[id(scene)] = packet
304
319
 
305
- pipeline.draw_packet(backend, packet)
320
+ pipeline.draw_packet(backend, packet, vp)
306
321
 
307
322
  timer.mark("draw_done")
308
323
  backend.end_frame()
@@ -314,12 +329,14 @@ class Game:
314
329
  timer.mark("sleep_end")
315
330
 
316
331
  # --- 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
- )
332
+ # if timer.enabled and (
333
+ # frame_index % report_every == 0 and frame_index > 0
334
+ # ):
335
+ # ms = timer.report_ms()
336
+ # total = (perf_counter() - timer.marks["frame_start"]) * 1000.0
337
+ # logger.debug(
338
+ # f"[Frame {frame_index}] total={total:.2f}ms | {ms}"
339
+ # )
323
340
 
324
341
  frame_index += 1
325
342
 
@@ -340,6 +357,9 @@ class Game:
340
357
  br, bg, bb = self.config.window.background_color
341
358
  self.services.window.set_clear_color(br, bg, bb)
342
359
 
360
+ # the “authoring resolution”
361
+ self.services.window.set_virtual_resolution(800, 600)
362
+
343
363
  def _resolve_world(self) -> object | None:
344
364
  # Prefer gameplay world underneath overlays:
345
365
  # scan from top to bottom and pick the first scene that has .world
@@ -9,6 +9,7 @@ from dataclasses import dataclass
9
9
 
10
10
  from mini_arcade_core.backend import Backend
11
11
  from mini_arcade_core.engine.render.packet import RenderPacket
12
+ from mini_arcade_core.engine.render.viewport import ViewportState
12
13
 
13
14
 
14
15
  @dataclass
@@ -23,7 +24,12 @@ class RenderPipeline:
23
24
  - backend draw pass
24
25
  """
25
26
 
26
- def draw_packet(self, backend: Backend, packet: RenderPacket):
27
+ def draw_packet(
28
+ self,
29
+ backend: Backend,
30
+ packet: RenderPacket,
31
+ viewport_state: ViewportState,
32
+ ):
27
33
  """
28
34
  Draw the given RenderPacket using the provided Backend.
29
35
 
@@ -35,5 +41,23 @@ class RenderPipeline:
35
41
  """
36
42
  if not packet:
37
43
  return
38
- for op in packet.ops:
39
- op(backend)
44
+
45
+ backend.set_viewport_transform(
46
+ viewport_state.offset_x,
47
+ viewport_state.offset_y,
48
+ viewport_state.scale,
49
+ )
50
+
51
+ # backend.set_clip_rect(
52
+ # viewport_state.offset_x,
53
+ # viewport_state.offset_y,
54
+ # viewport_state.viewport_w,
55
+ # viewport_state.viewport_h,
56
+ # )
57
+
58
+ try:
59
+ for op in packet.ops:
60
+ op(backend)
61
+ finally:
62
+ backend.clear_clip_rect()
63
+ backend.clear_viewport_transform()
@@ -0,0 +1,203 @@
1
+ """
2
+ Viewport management for virtual to screen coordinate transformations.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+
10
+ from mini_arcade_core.utils import logger
11
+
12
+
13
+ class ViewportMode(str, Enum):
14
+ """
15
+ Viewport scaling modes.
16
+
17
+ :cvar FIT: Scale to fit within window, preserving aspect ratio (letterbox).
18
+ :cvar FILL: Scale to fill entire window, preserving aspect ratio (crop).
19
+ """
20
+
21
+ FIT = "fit" # letterbox
22
+ FILL = "fill" # crop
23
+
24
+
25
+ # Justification: Many attributes needed to describe viewport state
26
+ # pylint: disable=too-many-instance-attributes
27
+ @dataclass(frozen=True)
28
+ class ViewportState:
29
+ """
30
+ Current state of the viewport.
31
+
32
+ :ivar virtual_w (int): Virtual canvas width.
33
+ :ivar virtual_h (int): Virtual canvas height.
34
+ :ivar window_w (int): Current window width.
35
+ :ivar window_h (int): Current window height.
36
+ :ivar mode (ViewportMode): Current viewport mode.
37
+ :ivar scale (float): Current scale factor.
38
+ :ivar viewport_w (int): Width of the viewport rectangle on screen.
39
+ :ivar viewport_h (int): Height of the viewport rectangle on screen.
40
+ :ivar offset_x (int): X offset of the viewport rectangle on screen.
41
+ :ivar offset_y (int): Y offset of the viewport rectangle on screen.
42
+ """
43
+
44
+ virtual_w: int
45
+ virtual_h: int
46
+
47
+ window_w: int
48
+ window_h: int
49
+
50
+ mode: ViewportMode
51
+ scale: float
52
+
53
+ # viewport rect in screen pixels where the virtual canvas lands
54
+ # (can be larger than window in FILL mode -> offsets can be negative)
55
+ viewport_w: int
56
+ viewport_h: int
57
+ offset_x: int
58
+ offset_y: int
59
+
60
+
61
+ # pylint: enable=too-many-instance-attributes
62
+
63
+
64
+ class Viewport:
65
+ """
66
+ Manages viewport transformations between virtual and screen coordinates.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ virtual_w: int,
72
+ virtual_h: int,
73
+ mode: ViewportMode = ViewportMode.FIT,
74
+ ):
75
+ """
76
+ :param virtual_w: Virtual canvas width.
77
+ :type virtual_w: int
78
+
79
+ :param virtual_h: Virtual canvas height.
80
+ :type virtual_h: int
81
+
82
+ :param mode: Viewport scaling mode.
83
+ :type mode: ViewportMode
84
+ """
85
+ self._virtual_w = int(virtual_w)
86
+ self._virtual_h = int(virtual_h)
87
+ self._mode = mode
88
+ self._state: ViewportState | None = None
89
+
90
+ def set_virtual_resolution(self, w: int, h: int):
91
+ """
92
+ Set a new virtual resolution.
93
+
94
+ :param w: New virtual width.
95
+ :type w: int
96
+
97
+ :param h: New virtual height.
98
+ :type h: int
99
+ """
100
+ self._virtual_w = int(w)
101
+ self._virtual_h = int(h)
102
+ if self._state:
103
+ self.resize(self._state.window_w, self._state.window_h)
104
+
105
+ def set_mode(self, mode: ViewportMode):
106
+ """
107
+ Set a new viewport mode.
108
+
109
+ :param mode: New viewport mode.
110
+ :type mode: ViewportMode
111
+ """
112
+ self._mode = mode
113
+ if self._state:
114
+ self.resize(self._state.window_w, self._state.window_h)
115
+
116
+ def resize(self, window_w: int, window_h: int):
117
+ """
118
+ Resize the viewport based on the current window size.
119
+
120
+ :param window_w: Current window width.
121
+ :type window_w: int
122
+
123
+ :param window_h: Current window height.
124
+ :type window_h: int
125
+ """
126
+ window_w = int(window_w)
127
+ window_h = int(window_h)
128
+
129
+ sx = window_w / self._virtual_w
130
+ sy = window_h / self._virtual_h
131
+ scale = min(sx, sy) if self._mode == ViewportMode.FIT else max(sx, sy)
132
+
133
+ vw = int(round(self._virtual_w * scale))
134
+ vh = int(round(self._virtual_h * scale))
135
+ ox = int(round((window_w - vw) / 2))
136
+ oy = int(round((window_h - vh) / 2))
137
+
138
+ self._state = ViewportState(
139
+ virtual_w=self._virtual_w,
140
+ virtual_h=self._virtual_h,
141
+ window_w=window_w,
142
+ window_h=window_h,
143
+ mode=self._mode,
144
+ scale=float(scale),
145
+ viewport_w=vw,
146
+ viewport_h=vh,
147
+ offset_x=ox,
148
+ offset_y=oy,
149
+ )
150
+ logger.debug(
151
+ f"Viewport resized: window=({window_w}x{window_h}), "
152
+ f"virtual=({self._virtual_w}x{self._virtual_h}), "
153
+ f"mode={self._mode}, scale={scale:.3f}, "
154
+ f"viewport=({vw}x{vh})@({ox},{oy})"
155
+ )
156
+
157
+ @property
158
+ def state(self) -> ViewportState:
159
+ """
160
+ Get the current viewport state.
161
+
162
+ :return: Current ViewportState.
163
+ :rtype: ViewportState
164
+
165
+ :raises RuntimeError: If the viewport has not been initialized.
166
+ """
167
+ if self._state is None:
168
+ raise RuntimeError(
169
+ "Viewport not initialized. Call resize(window_w, window_h)."
170
+ )
171
+ return self._state
172
+
173
+ def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
174
+ """
175
+ Convert screen coordinates to virtual coordinates.
176
+
177
+ :param x: X coordinate on the screen.
178
+ :type x: float
179
+
180
+ :param y: Y coordinate on the screen.
181
+ :type y: float
182
+
183
+ :return: Corresponding virtual coordinates (x, y).
184
+ :rtype: tuple[float, float]
185
+ """
186
+ s = self.state
187
+ return ((x - s.offset_x) / s.scale, (y - s.offset_y) / s.scale)
188
+
189
+ def virtual_to_screen(self, x: float, y: float) -> tuple[float, float]:
190
+ """
191
+ Convert virtual coordinates to screen coordinates.
192
+
193
+ :param x: X coordinate in virtual space.
194
+ :type x: float
195
+
196
+ :param y: Y coordinate in virtual space.
197
+ :type y: float
198
+
199
+ :return: Corresponding screen coordinates (x, y).
200
+ :rtype: tuple[float, float]
201
+ """
202
+ s = self.state
203
+ return (s.offset_x + x * s.scale, s.offset_y + y * s.scale)
@@ -143,8 +143,6 @@ class InputManager:
143
143
  )
144
144
  binding.command.execute(to_inject)
145
145
 
146
- # --- Convenience API ------------------------------------------------------
147
-
148
146
  def on_quit(self, command: BaseCommand, action: str = "quit"):
149
147
  """
150
148
  Bind a command to the QUIT event.
@@ -7,7 +7,14 @@ from __future__ import annotations
7
7
  from mini_arcade_core.runtime.audio.audio_port import AudioPort
8
8
 
9
9
 
10
- class NullAudioAdapter(AudioPort):
10
+ class SDLAudioAdapter(AudioPort):
11
11
  """A no-op audio adapter."""
12
12
 
13
- def play(self, sound_id: str): ...
13
+ def __init__(self, backend):
14
+ self.backend = backend
15
+
16
+ def load_sound(self, sound_id: str, file_path: str):
17
+ self.backend.load_sound(sound_id, file_path)
18
+
19
+ def play(self, sound_id: str, loops: int = 0):
20
+ self.backend.play_sound(sound_id, loops)
@@ -4,14 +4,33 @@ Service interfaces for runtime components.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ from mini_arcade_core.backend.backend import Backend
8
+
7
9
 
8
10
  class AudioPort:
9
11
  """Interface for audio playback operations."""
10
12
 
11
- def play(self, sound_id: str):
13
+ backend: Backend
14
+
15
+ def load_sound(self, sound_id: str, file_path: str):
16
+ """
17
+ Load a sound from a file and associate it with an identifier.
18
+
19
+ :param sound_id: Identifier to associate with the sound.
20
+ :type sound_id: str
21
+
22
+ :param file_path: Path to the sound file.
23
+ :type file_path: str
24
+ """
25
+
26
+ def play(self, sound_id: str, loops: int = 0):
12
27
  """
13
28
  Play the specified sound.
14
29
 
15
30
  :param sound_id: Identifier of the sound to play.
16
31
  :type sound_id: str
32
+
33
+ :param loops: Number of times to loop the sound.
34
+ 0 = play once, -1 = infinite loop, 1 = play twice, etc.
35
+ :type loops: int
17
36
  """
@@ -30,3 +30,22 @@ class CapturePort:
30
30
  :return: Screenshot data as bytes.
31
31
  :rtype: bytes | None
32
32
  """
33
+
34
+ def screenshot_sim(
35
+ self, run_id: str, frame_index: int, label: str = "frame"
36
+ ) -> str:
37
+ """
38
+ Capture the current frame in a simulation context.
39
+
40
+ :param run_id: Unique identifier for the simulation run.
41
+ :type run_id: str
42
+
43
+ :param frame_index: Index of the frame in the simulation.
44
+ :type frame_index: int
45
+
46
+ :param label: Optional label for the screenshot file.
47
+ :type label: str
48
+
49
+ :return: Screenshot file path.
50
+ :rtype: str
51
+ """
@@ -94,4 +94,32 @@ class SceneAdapter(ScenePort):
94
94
 
95
95
  def input_entry(self):
96
96
  vis = self.visible_entries()
97
- return vis[-1] if vis else None
97
+ if not vis:
98
+ return None
99
+
100
+ # If some scene blocks input, only scenes at/above it can receive.
101
+ start_idx = 0
102
+ for idx in range(len(vis) - 1, -1, -1):
103
+ if vis[idx].policy.blocks_input:
104
+ start_idx = idx
105
+ break
106
+
107
+ candidates = vis[start_idx:]
108
+
109
+ # Pick the top-most candidate that actually receives input.
110
+ for entry in reversed(candidates):
111
+ if entry.policy.receives_input:
112
+ return entry
113
+
114
+ return None
115
+
116
+ def has_scene(self, scene_id: str) -> bool:
117
+ return any(item.entry.scene_id == scene_id for item in self._stack)
118
+
119
+ def remove_scene(self, scene_id: str) -> None:
120
+ # remove first match from top (overlay is usually near top)
121
+ for i in range(len(self._stack) - 1, -1, -1):
122
+ if self._stack[i].entry.scene_id == scene_id:
123
+ item = self._stack.pop(i)
124
+ item.entry.scene.on_exit()
125
+ return
@@ -23,11 +23,13 @@ class ScenePolicy:
23
23
  blocks_update: if True, scenes below do not tick/update (pause modal)
24
24
  blocks_input: if True, scenes below do not receive input
25
25
  is_opaque: if True, scenes below are not rendered
26
+ receives_input: if True, scene can receive input
26
27
  """
27
28
 
28
29
  blocks_update: bool = False
29
30
  blocks_input: bool = False
30
31
  is_opaque: bool = False
32
+ receives_input: bool = True
31
33
 
32
34
 
33
35
  @dataclass(frozen=True)
@@ -147,3 +149,22 @@ class ScenePort:
147
149
  :return: The SceneEntry that receives input, or None if no scenes are active.
148
150
  :rtype: SceneEntry | None
149
151
  """
152
+
153
+ def has_scene(self, scene_id: str) -> bool:
154
+ """
155
+ Check if a scene with the given ID exists in the stack.
156
+
157
+ :param scene_id: Identifier of the scene to check.
158
+ :type scene_id: str
159
+
160
+ :return: True if the scene exists in the stack, False otherwise.
161
+ :rtype: bool
162
+ """
163
+
164
+ def remove_scene(self, scene_id: str) -> None:
165
+ """
166
+ Remove a scene with the given ID from the stack.
167
+
168
+ :param scene_id: Identifier of the scene to remove.
169
+ :type scene_id: str
170
+ """
@@ -4,6 +4,13 @@ Module providing runtime adapters for window and scene management.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ from venv import logger
8
+
9
+ from mini_arcade_core.engine.render.viewport import (
10
+ Viewport,
11
+ ViewportMode,
12
+ ViewportState,
13
+ )
7
14
  from mini_arcade_core.runtime.window.window_port import WindowPort
8
15
 
9
16
 
@@ -12,15 +19,72 @@ class WindowAdapter(WindowPort):
12
19
  Manages multiple game windows (not implemented).
13
20
  """
14
21
 
15
- def __init__(self, backend):
22
+ def __init__(self, backend, window_settings):
16
23
  self.backend = backend
24
+ self.window_settings = window_settings
25
+
26
+ self._initialized = False
27
+
28
+ # Default: virtual resolution == initial window resolution.
29
+ # You can override via set_virtual_resolution().
30
+ self._viewport = Viewport(
31
+ window_settings.width,
32
+ window_settings.height,
33
+ mode=ViewportMode.FIT,
34
+ )
35
+
36
+ # Cached current window size
37
+ self.size = (window_settings.width, window_settings.height)
17
38
 
18
39
  def set_window_size(self, width, height):
40
+ width = int(width)
41
+ height = int(height)
19
42
  self.size = (width, height)
20
- self.backend.init(width, height)
43
+
44
+ self.window_settings.width = width
45
+ self.window_settings.height = height
46
+
47
+ if not self._initialized:
48
+ self.backend.init(self.window_settings)
49
+ self._initialized = True
50
+ else:
51
+ self.backend.resize_window(width, height)
52
+
53
+ self._viewport.resize(width, height)
54
+
55
+ def set_virtual_resolution(self, width: int, height: int):
56
+ self._viewport.set_virtual_resolution(int(width), int(height))
57
+ # re-apply using current window size
58
+ w, h = self.size
59
+ self._viewport.resize(w, h)
60
+
61
+ def set_viewport_mode(self, mode: ViewportMode):
62
+ self._viewport.set_mode(mode)
63
+
64
+ def get_viewport(self) -> ViewportState:
65
+ return self._viewport.state
66
+
67
+ def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
68
+ return self._viewport.screen_to_virtual(x, y)
21
69
 
22
70
  def set_title(self, title):
23
71
  self.backend.set_window_title(title)
24
72
 
25
73
  def set_clear_color(self, r, g, b):
26
74
  self.backend.set_clear_color(r, g, b)
75
+
76
+ def on_window_resized(self, width: int, height: int):
77
+ logger.debug(f"Window resized event: {width}x{height}")
78
+ width = int(width)
79
+ height = int(height)
80
+
81
+ # Update cached size, but DO NOT call backend.resize_window here.
82
+ self.size = (width, height)
83
+ self.window_settings.width = width
84
+ self.window_settings.height = height
85
+
86
+ self._viewport.resize(width, height)
87
+
88
+ def get_virtual_size(self) -> tuple[int, int]:
89
+ s = self.get_viewport()
90
+ return (s.virtual_w, s.virtual_h)
@@ -4,7 +4,8 @@ Service interfaces for runtime components.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from mini_arcade_core.backend import Backend
7
+ from mini_arcade_core.backend import Backend, WindowSettings
8
+ from mini_arcade_core.engine.render.viewport import ViewportMode, ViewportState
8
9
 
9
10
 
10
11
  class WindowPort:
@@ -12,6 +13,7 @@ class WindowPort:
12
13
 
13
14
  backend: Backend
14
15
  size: tuple[int, int]
16
+ window_settings_cls: WindowSettings
15
17
 
16
18
  def set_window_size(self, width: int, height: int):
17
19
  """
@@ -24,6 +26,47 @@ class WindowPort:
24
26
  :type height: int
25
27
  """
26
28
 
29
+ def set_viewport_mode(self, mode: ViewportMode):
30
+ """
31
+ Set the viewport mode for rendering.
32
+
33
+ :param mode: The viewport mode to set.
34
+ :type mode: ViewportMode
35
+ """
36
+
37
+ def get_viewport(self) -> ViewportState:
38
+ """
39
+ Get the current viewport state.
40
+
41
+ :return: The current ViewportState.
42
+ :rtype: ViewportState
43
+ """
44
+
45
+ def screen_to_virtual(self, x: float, y: float) -> tuple[float, float]:
46
+ """
47
+ Convert screen coordinates to virtual coordinates.
48
+
49
+ :param x: X coordinate on the screen.
50
+ :type x: float
51
+
52
+ :param y: Y coordinate on the screen.
53
+ :type y: float
54
+
55
+ :return: Corresponding virtual coordinates (x, y).
56
+ :rtype: tuple[float, float]
57
+ """
58
+
59
+ def set_virtual_resolution(self, width: int, height: int):
60
+ """
61
+ Set the virtual resolution for rendering.
62
+
63
+ :param width: Virtual width in pixels.
64
+ :type width: int
65
+
66
+ :param height: Virtual height in pixels.
67
+ :type height: int
68
+ """
69
+
27
70
  def set_title(self, title: str):
28
71
  """
29
72
  Set the window title.
@@ -45,3 +88,22 @@ class WindowPort:
45
88
  :param b: Blue component (0-255).
46
89
  :type b: int
47
90
  """
91
+
92
+ def on_window_resized(self, width: int, height: int):
93
+ """
94
+ Handle window resized event.
95
+
96
+ :param width: New width of the window.
97
+ :type width: int
98
+
99
+ :param height: New height of the window.
100
+ :type height: int
101
+ """
102
+
103
+ def get_virtual_size(self) -> tuple[int, int]:
104
+ """
105
+ Get the current virtual resolution size.
106
+
107
+ :return: Tuple of (virtual_width, virtual_height).
108
+ :rtype: tuple[int, int]
109
+ """
@@ -0,0 +1,67 @@
1
+ """
2
+ Debug overlay scene that displays FPS, window size, and scene stack information.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from mini_arcade_core.engine.render.packet import RenderPacket
8
+ from mini_arcade_core.runtime.context import RuntimeContext
9
+ from mini_arcade_core.runtime.input_frame import InputFrame
10
+ from mini_arcade_core.scenes.autoreg import register_scene
11
+ from mini_arcade_core.sim.protocols import SimScene
12
+
13
+
14
+ @register_scene("debug_overlay")
15
+ class DebugOverlayScene(SimScene):
16
+ """
17
+ A debug overlay scene that displays FPS, window size, and scene stack information.
18
+ """
19
+
20
+ def __init__(self, ctx: RuntimeContext):
21
+ super().__init__(ctx)
22
+ self._accum = 0.0
23
+ self._frames = 0
24
+ self._fps = 0.0
25
+
26
+ def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
27
+ self._accum += dt
28
+ self._frames += 1
29
+ if self._accum >= 0.5:
30
+ self._fps = self._frames / self._accum
31
+ self._accum = 0.0
32
+ self._frames = 0
33
+
34
+ services = (
35
+ self.context.services
36
+ ) # or ctx.services (depends on your scene base)
37
+ # Justification: type checker can't infer type here
38
+ # pylint: disable=assignment-from-no-return
39
+ vp = services.window.get_viewport()
40
+ stack = services.scenes.visible_entries()
41
+ # pylint: enable=assignment-from-no-return
42
+
43
+ lines = [
44
+ f"FPS: {self._fps:5.1f}",
45
+ f"dt: {dt*1000.0:5.2f} ms",
46
+ f"virtual: {vp.virtual_w}x{vp.virtual_h}",
47
+ f"window: {vp.window_w}x{vp.window_h}",
48
+ f"scale: {vp.scale:.3f}",
49
+ f"offset: ({vp.offset_x},{vp.offset_y})",
50
+ "stack:",
51
+ ]
52
+ for e in stack:
53
+ lines.append(f" - {e.scene_id} overlay={e.is_overlay}")
54
+
55
+ def draw(backend):
56
+ # translucent background panel
57
+ backend.draw_rect(
58
+ 8, 8, 360, 18 * (len(lines) + 1), color=(0, 0, 0, 0.65)
59
+ )
60
+ y = 14
61
+ for line in lines:
62
+ backend.draw_text(
63
+ 16, y, line, color=(255, 255, 255), font_size=14
64
+ )
65
+ y += 18
66
+
67
+ return RenderPacket(ops=[draw])
@@ -93,22 +93,23 @@ class SceneRegistry:
93
93
  for scene_id, cls in catalog.items():
94
94
  self.register_cls(scene_id, cls)
95
95
 
96
- def discover(self, package: str) -> "SceneRegistry":
96
+ def discover(self, *packages: str) -> "SceneRegistry":
97
97
  """
98
98
  Import all modules in a package so @scene decorators run.
99
99
 
100
- :param package: The package name to scan for scene modules.
101
- :type package: str
100
+ :param packages: The package names to scan for scene modules.
101
+ :type packages: str
102
102
 
103
103
  :return: The SceneRegistry instance (for chaining).
104
104
  :rtype: SceneRegistry
105
105
  """
106
- pkg = importlib.import_module(package)
107
- if not hasattr(pkg, "__path__"):
108
- return self # not a package
106
+ for package in packages:
107
+ pkg = importlib.import_module(package)
108
+ if not hasattr(pkg, "__path__"):
109
+ continue
109
110
 
110
- for mod in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
111
- importlib.import_module(mod.name)
111
+ for mod in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
112
+ importlib.import_module(mod.name)
112
113
 
113
114
  self.load_catalog(snapshot())
114
115
  return self
@@ -39,25 +39,34 @@ class VerticalBounce:
39
39
 
40
40
  bounds: Bounds2D
41
41
 
42
- def apply(self, obj: RectKinematic):
42
+ def apply(self, obj: RectKinematic) -> bool:
43
43
  """
44
44
  Apply vertical bounce to the given object.
45
45
 
46
46
  :param obj: The object to apply the bounce to.
47
47
  :type obj: RectKinematic
48
+
49
+ :return: True if a bounce occurred, False otherwise.
50
+ :rtype: bool
48
51
  """
49
52
  top = self.bounds.top
50
53
  bottom = self.bounds.bottom
51
54
 
55
+ bounced = False
56
+
52
57
  # Top collision
53
58
  if obj.position.y <= top:
54
59
  obj.position.y = top
55
60
  obj.velocity.vy *= -1
61
+ bounced = True
56
62
 
57
63
  # Bottom collision
58
64
  if obj.position.y + obj.size.height >= bottom:
59
65
  obj.position.y = bottom - obj.size.height
60
66
  obj.velocity.vy *= -1
67
+ bounced = True
68
+
69
+ return bounced
61
70
 
62
71
 
63
72
  class RectSprite(Protocol):
@@ -128,6 +128,7 @@ class MenuStyle:
128
128
  class Menu:
129
129
  """A simple text-based menu system."""
130
130
 
131
+ # TODO: Solve too-many-arguments warning later
131
132
  # Justification: Multiple attributes for menu state
132
133
  # pylint: disable=too-many-arguments
133
134
  def __init__(
@@ -151,6 +152,9 @@ class Menu:
151
152
 
152
153
  :param style: Optional MenuStyle for customizing appearance.
153
154
  :type style: MenuStyle | None
155
+
156
+ :param on_select: Optional callback when an item is selected.
157
+ :type on_select: Optional[Callable[[MenuItem], None]]
154
158
  """
155
159
  self.items = list(items)
156
160
  self.viewport = viewport
@@ -250,6 +254,7 @@ class Menu:
250
254
 
251
255
  return False
252
256
 
257
+ # TODO: Delegate drawing to a renderer class later
253
258
  def draw(self, surface: Backend):
254
259
  """
255
260
  Draw the menu onto the given backend surface.
@@ -342,6 +347,7 @@ class Menu:
342
347
  font_size=self.style.item_font_size,
343
348
  )
344
349
 
350
+ # TODO: Solve too-many-locals warning later
345
351
  # Justification: Local variables for layout calculations
346
352
  # pylint: disable=too-many-locals
347
353
  def _draw_buttons(self, surface: Backend, x_center: int, cursor_y: int):
@@ -454,13 +460,14 @@ class Menu:
454
460
  + self.style.title_margin_bottom
455
461
  )
456
462
 
457
- # Sticky width (never shrink)
463
+ # Sticky width (never shrink)
458
464
  if self.stable_width:
459
465
  self._max_content_w_seen = max(self._max_content_w_seen, max_w)
460
466
  max_w = self._max_content_w_seen
461
467
 
462
468
  return max_w, content_h, title_h
463
469
 
470
+ # TODO: Solve too-many-arguments warning later
464
471
  # Justification: Many arguments for text drawing utility
465
472
  # pylint: disable=too-many-arguments
466
473
  @staticmethod
@@ -674,8 +681,6 @@ class BaseMenuScene(SimScene):
674
681
  super().__init__(ctx)
675
682
  self.model = MenuModel()
676
683
 
677
- # ---- hooks (same spirit as before) ----
678
-
679
684
  @property
680
685
  def menu_title(self) -> str | None:
681
686
  """
@@ -717,7 +722,7 @@ class BaseMenuScene(SimScene):
717
722
  def on_enter(self):
718
723
  self.menu = Menu(
719
724
  self._build_display_items(),
720
- viewport=self.context.services.window.size,
725
+ viewport=self.context.services.window.get_virtual_size(),
721
726
  title=self.menu_title,
722
727
  style=self.menu_style(),
723
728
  )
@@ -1,5 +1,6 @@
1
1
  """
2
2
  Logging utilities for Mini Arcade Core.
3
+ Provides a console logger with colored output and class/function context.
3
4
  """
4
5
 
5
6
  from __future__ import annotations
@@ -11,6 +12,7 @@ from typing import Optional
11
12
 
12
13
 
13
14
  def _classname_from_locals(locals_: dict) -> Optional[str]:
15
+ """Retrieve the class name from locals dict, if available."""
14
16
  self_obj = locals_.get("self")
15
17
  if self_obj is not None:
16
18
  return type(self_obj).__name__
@@ -73,10 +75,6 @@ class ConsoleColorFormatter(logging.Formatter):
73
75
  return f"{color}{msg}{self.COLORS['RESET']}"
74
76
 
75
77
 
76
- # --------------------------------------------------------------------------------------
77
- # Global logging setup (single source of truth)
78
- # --------------------------------------------------------------------------------------
79
-
80
78
  LOGGER_FORMAT = (
81
79
  "%(asctime)s [%(levelname)-8.8s] [%(name)s] "
82
80
  "%(module)s.%(classname)s.%(funcName)s: "
@@ -166,9 +164,5 @@ def configure_logging(level: int = logging.DEBUG):
166
164
  root.addHandler(console)
167
165
 
168
166
 
169
- # --------------------------------------------------------------------------------------
170
- # Public logger for DejaBounce
171
- # --------------------------------------------------------------------------------------
172
-
173
167
  configure_logging()
174
168
  logger = logging.getLogger("mini-arcade-core")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -1,27 +1,28 @@
1
- mini_arcade_core/__init__.py,sha256=Cyp7bCkNW018_AlhUGXRH29iPytyW03dE5cvetezxLg,3441
2
- mini_arcade_core/backend/__init__.py,sha256=J1wZBHX-aqmBP3zh_ey9PK2b_gnWp72zxMfKcs3iwSw,274
3
- mini_arcade_core/backend/backend.py,sha256=9CYcI7llbUhyO1p3Rjl-jmsw2c3KsQUtPAx0ys-WnW4,4237
1
+ mini_arcade_core/__init__.py,sha256=axwl7fiQ2Zu2vPOTMUxwnvR746gI9RhpBWpBCId_yqo,3686
2
+ mini_arcade_core/backend/__init__.py,sha256=E9uOCttCkXwdN_5MlcFHUmG3Bj6RYMatNNOno4C_6aI,312
3
+ mini_arcade_core/backend/backend.py,sha256=RpFclQFDOLSaqCACWR0sT_OQYjDIfYzQaFvnHzURLvI,7788
4
4
  mini_arcade_core/backend/events.py,sha256=5Ohve3CQ6n2CztiOhbCoz6yFDY4z0j4v4R9FBKRDRjc,2929
5
5
  mini_arcade_core/backend/keys.py,sha256=LTg20SwLBI3kpPIiTNpq2yBft_QUGj-iNFSNm9M-Fus,3010
6
6
  mini_arcade_core/backend/sdl_map.py,sha256=_yBRtvaFUcQKy1kcoIf-SPhbbKEW7dzvzBcI6TLmKjc,2060
7
7
  mini_arcade_core/backend/types.py,sha256=SuiwXGNmXCZxfPsww6zj3V_NK7k4jpoCuzMn19afS-g,175
8
8
  mini_arcade_core/bus.py,sha256=2Etpoa-UWhk33xJjqDlY5YslPDJEjxNoIEVtF3C73vs,1558
9
9
  mini_arcade_core/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- mini_arcade_core/engine/commands.py,sha256=1g7XBWmiVYxe2IUggPpUs5uaZP9PHZKfr6cQuhez8eg,4044
11
- mini_arcade_core/engine/game.py,sha256=ovfSCNczMN8Tu-rj83reHKl32OKEa649PwMbshlLpac,11479
10
+ mini_arcade_core/engine/commands.py,sha256=5caisiwWhLWO5w18rOvEaeYStOUT8mvrTP0O5gpgeoA,4772
11
+ mini_arcade_core/engine/game.py,sha256=stb75hlqY-DsOqXz7zOFpfSR-jtF8rD0Hpmy1qPuSzE,12409
12
12
  mini_arcade_core/engine/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  mini_arcade_core/engine/render/packet.py,sha256=OiAPwGoVHo04OcUWMAoA_N1AFPUMyf8yxNgJthGj4-c,1440
14
- mini_arcade_core/engine/render/pipeline.py,sha256=4Pdwwt-fo4dK3uqPb3M_IiRIDxymilAK8fOaiojsl10,891
14
+ mini_arcade_core/engine/render/pipeline.py,sha256=XdnzAXNle8_CJOPzYJaEy9Dp_UgoCgJwIh-GpW9-n5E,1516
15
+ mini_arcade_core/engine/render/viewport.py,sha256=fbzH3_rc27IGUtDalmxz5cukwHnpt1kopIeVqmTab20,5784
15
16
  mini_arcade_core/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  mini_arcade_core/managers/cheats.py,sha256=jMx2a8YnaNCkCG5MPmIzz4uHuS7-_aYf0J45cv2-3v0,5569
17
- mini_arcade_core/managers/inputs.py,sha256=LL0Q_QYUF3K1orpLIrmYXOadBUascyJVpd2KYvpTNDU,8605
18
+ mini_arcade_core/managers/inputs.py,sha256=9HZ0BnJyUX-elfGETPhhPZnTkz2bK83pEKj7GHPbPFU,8523
18
19
  mini_arcade_core/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  mini_arcade_core/runtime/audio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- mini_arcade_core/runtime/audio/audio_adapter.py,sha256=CEMYbAhCT1MoIsH6PTqu2kE6iOCcB7SwnFjBqk3hLlc,286
21
- mini_arcade_core/runtime/audio/audio_port.py,sha256=idUxRIr_qga46gL31_GMwQ3j-zfNyuyYC-Ygkl_tIO4,338
21
+ mini_arcade_core/runtime/audio/audio_adapter.py,sha256=9ithbInYUB72ErwvlNrbIlI55uw0DT2QD33geYNXT3c,522
22
+ mini_arcade_core/runtime/audio/audio_port.py,sha256=3Mqv7TchEVkmd-RVjUpCD-EqA-yiL0Jf2Sj3rQwP678,907
22
23
  mini_arcade_core/runtime/capture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
24
  mini_arcade_core/runtime/capture/capture_adapter.py,sha256=qJ2JiOLJHbP00IesbAyyPGPBxSaxwPJRTMaMjMU4bXs,4660
24
- mini_arcade_core/runtime/capture/capture_port.py,sha256=lZme02Cs9aeb_qpZnAz3sClZieG-lFc05wbFPCyB4_I,704
25
+ mini_arcade_core/runtime/capture/capture_port.py,sha256=NVxMJrQJELiSYuUJ29tvsdIcCBq4f1dTT2rDLZs6gnI,1230
25
26
  mini_arcade_core/runtime/context.py,sha256=IQpAhtbN0ZqJVXG6KXFq3FPk0sIGyPmyNgBYviqZl7A,1632
26
27
  mini_arcade_core/runtime/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
28
  mini_arcade_core/runtime/file/file_adapter.py,sha256=09q7G9Qijml9d4AAjo6HLC1yuoVTjE_7xaT8apT4mk0,523
@@ -31,15 +32,16 @@ mini_arcade_core/runtime/input/input_adapter.py,sha256=vExQiwFIWTI3zYD8lmnD9TvoQ
31
32
  mini_arcade_core/runtime/input/input_port.py,sha256=d4ptftwf92_LJdyaUMFxIsLHXBINzQyJACHn4laNyxQ,746
32
33
  mini_arcade_core/runtime/input_frame.py,sha256=34-RAfOD-YScVLyRQrarpm7byFTHjsWM77lIH0JsmT8,2384
33
34
  mini_arcade_core/runtime/scene/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- mini_arcade_core/runtime/scene/scene_adapter.py,sha256=dYkqFWN3FcRmf9tsZ5wz1QZX67CUntndiNMOWrO578U,2523
35
- mini_arcade_core/runtime/scene/scene_port.py,sha256=uDFrN7fudIldNkivEkagQd9-b1HVI71BpTpSv5Oh6JI,3854
35
+ mini_arcade_core/runtime/scene/scene_adapter.py,sha256=I2kWc76fU05pNpt2bO-1_aeR6KwkkmZLD2-9fr_UAn8,3481
36
+ mini_arcade_core/runtime/scene/scene_port.py,sha256=F_KtHO-N5O3EgVAE3HaYYHqoc7zpUnCwn6wQdsl1iTA,4481
36
37
  mini_arcade_core/runtime/services.py,sha256=9LX7O-AYFTxKgBDewzu0B_D01EJ41WVAdl8U1ZcvEYg,1061
37
38
  mini_arcade_core/runtime/window/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
- mini_arcade_core/runtime/window/window_adapter.py,sha256=EMSWE72aWWzCN0uRuVJvry3g2PQTBmEa_zEfgK6weKY,637
39
- mini_arcade_core/runtime/window/window_port.py,sha256=HwY8ilhZLLK5cZbEA93R5crjOA4i8Gn2MB8y4BbI4o4,988
39
+ mini_arcade_core/runtime/window/window_adapter.py,sha256=_yCcJwvHN0gEPh0rkgLEKtPZ50qRbONsUTbgtV1m7Y8,2689
40
+ mini_arcade_core/runtime/window/window_port.py,sha256=JbSH549De7fa4ifQ0EH5QQoq03Got1n9C4qViLgciUU,2682
40
41
  mini_arcade_core/scenes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
42
  mini_arcade_core/scenes/autoreg.py,sha256=wsuY7YUSZFmDyToKHFriAG78OU48-7J4BfL_X6T5GBg,1037
42
- mini_arcade_core/scenes/registry.py,sha256=EF5qaAgoLwJa3IV_3wclvjHpE3jZt-KHB3GOJS9lQy8,3390
43
+ mini_arcade_core/scenes/debug_overlay.py,sha256=N4sGQ_3pMz-ZvJZX63kqtylpS9U1PTpWeImrkfmVnus,2230
44
+ mini_arcade_core/scenes/registry.py,sha256=taWyLUDLRe7RJN0kqF_qpqRSiGCcbzEeCpcKlmPUFnE,3428
43
45
  mini_arcade_core/scenes/sim_scene.py,sha256=b2JsOvPFkHCdCf8pMLJZ90qB0JJ6B8Ka3o5QK4cVshI,1055
44
46
  mini_arcade_core/scenes/systems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
47
  mini_arcade_core/scenes/systems/base_system.py,sha256=GfMrXsO8ynW3xOxWeav7Ug5XUbRnbF0vo8VzmG7gpec,1075
@@ -49,17 +51,17 @@ mini_arcade_core/sim/protocols.py,sha256=b7d2WAKRokTNbteNhUaWdGx9vc9Fnccxb-5rPwo
49
51
  mini_arcade_core/sim/runner.py,sha256=ZF-BZJw-NcaFrg4zsUu1zOUUBZwZbRYflqcdF1jDcmM,7446
50
52
  mini_arcade_core/spaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
53
  mini_arcade_core/spaces/d2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
- mini_arcade_core/spaces/d2/boundaries2d.py,sha256=H1HkCR1422MkQIEve2DFKvnav4RpvtLx-qTMxzmdDMQ,2610
54
+ mini_arcade_core/spaces/d2/boundaries2d.py,sha256=xeTnd0pW5DKfqaKsfSBXnufeb45aXNIspgHRyLXWejo,2804
53
55
  mini_arcade_core/spaces/d2/collision2d.py,sha256=5IvgLnyVb8i0uzzZuum1noWsNhoxcvHOLaHkmrTMTxQ,1710
54
56
  mini_arcade_core/spaces/d2/geometry2d.py,sha256=FuYzef-XdOyb1aeGLJbxINxr0WJHnqFFBgtbPi1WonY,1716
55
57
  mini_arcade_core/spaces/d2/kinematics2d.py,sha256=AJ3DhPXNgm6wZYwCljMIE4_2BYx3E2rPcwhXTgQALkU,2030
56
58
  mini_arcade_core/spaces/d2/physics2d.py,sha256=OQT7r-zMtmoKD2aWCSNmRAdI0OGIpxGX-pLR8LcAMbQ,1854
57
59
  mini_arcade_core/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- mini_arcade_core/ui/menu.py,sha256=94W9lFI0qVB-v-f0b5U05p-MTmzaa5H1RGxy-1S01go,24345
60
+ mini_arcade_core/ui/menu.py,sha256=pUC6qfG3lNXa1Ga2Kz_cSgjiAfAWVMRevum61G_RE8k,24646
59
61
  mini_arcade_core/utils/__init__.py,sha256=3Q9r6bTyqImYix8BnOGwWjAz25nbTQezGcRq3m5KEYE,189
60
62
  mini_arcade_core/utils/deprecated_decorator.py,sha256=yrrW2ZqPskK-4MUTyIrMb465Wc54X2poV53ZQutZWqc,1140
61
- mini_arcade_core/utils/logging.py,sha256=IqkM1C5yezb3qnmqlftROlHO6mBCW0iWQmyIR9mwd-4,5576
62
- mini_arcade_core-1.0.0.dist-info/METADATA,sha256=hMAIBh7Kk29GpAzrP4LziqDT3fzHc0SoiNmhz8A70YY,8188
63
- mini_arcade_core-1.0.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
64
- mini_arcade_core-1.0.0.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
65
- mini_arcade_core-1.0.0.dist-info/RECORD,,
63
+ mini_arcade_core/utils/logging.py,sha256=YyirsGRSpGtxegUl3HWz37mGNngK3QkYm2_aZjXJC84,5279
64
+ mini_arcade_core-1.0.2.dist-info/METADATA,sha256=CtAYloxC_GLDTJT53Ap5Sz8JSF3b_2iTPJuo_Kc4vSg,8188
65
+ mini_arcade_core-1.0.2.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
66
+ mini_arcade_core-1.0.2.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
67
+ mini_arcade_core-1.0.2.dist-info/RECORD,,