mini-arcade-core 1.1.0__py3-none-any.whl → 1.2.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 (58) hide show
  1. mini_arcade_core/__init__.py +14 -42
  2. mini_arcade_core/backend/__init__.py +1 -2
  3. mini_arcade_core/backend/backend.py +185 -154
  4. mini_arcade_core/backend/types.py +5 -1
  5. mini_arcade_core/engine/commands.py +26 -7
  6. mini_arcade_core/engine/game.py +79 -319
  7. mini_arcade_core/engine/game_config.py +40 -0
  8. mini_arcade_core/engine/gameplay_settings.py +24 -0
  9. mini_arcade_core/engine/loop/config.py +20 -0
  10. mini_arcade_core/engine/loop/hooks.py +77 -0
  11. mini_arcade_core/engine/loop/runner.py +272 -0
  12. mini_arcade_core/engine/loop/state.py +32 -0
  13. mini_arcade_core/engine/managers.py +24 -0
  14. mini_arcade_core/engine/render/context.py +0 -2
  15. mini_arcade_core/engine/render/effects/base.py +88 -0
  16. mini_arcade_core/engine/render/effects/crt.py +68 -0
  17. mini_arcade_core/engine/render/effects/registry.py +50 -0
  18. mini_arcade_core/engine/render/effects/vignette.py +79 -0
  19. mini_arcade_core/engine/render/passes/begin_frame.py +1 -1
  20. mini_arcade_core/engine/render/passes/end_frame.py +1 -1
  21. mini_arcade_core/engine/render/passes/postfx.py +25 -4
  22. mini_arcade_core/engine/render/passes/ui.py +1 -1
  23. mini_arcade_core/engine/render/passes/world.py +6 -6
  24. mini_arcade_core/engine/render/pipeline.py +7 -6
  25. mini_arcade_core/engine/render/viewport.py +10 -4
  26. mini_arcade_core/engine/scenes/__init__.py +0 -0
  27. mini_arcade_core/engine/scenes/models.py +54 -0
  28. mini_arcade_core/engine/scenes/scene_manager.py +213 -0
  29. mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
  30. mini_arcade_core/runtime/audio/audio_port.py +0 -4
  31. mini_arcade_core/runtime/capture/capture_adapter.py +13 -6
  32. mini_arcade_core/runtime/capture/capture_port.py +0 -4
  33. mini_arcade_core/runtime/context.py +8 -6
  34. mini_arcade_core/runtime/scene/scene_query_adapter.py +31 -0
  35. mini_arcade_core/runtime/scene/scene_query_port.py +38 -0
  36. mini_arcade_core/runtime/services.py +3 -2
  37. mini_arcade_core/runtime/window/window_adapter.py +43 -41
  38. mini_arcade_core/runtime/window/window_port.py +3 -17
  39. mini_arcade_core/scenes/debug_overlay.py +5 -4
  40. mini_arcade_core/scenes/registry.py +11 -1
  41. mini_arcade_core/scenes/sim_scene.py +14 -14
  42. mini_arcade_core/ui/menu.py +54 -16
  43. mini_arcade_core/utils/__init__.py +2 -1
  44. mini_arcade_core/utils/logging.py +47 -18
  45. mini_arcade_core/utils/profiler.py +283 -0
  46. {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/METADATA +1 -1
  47. mini_arcade_core-1.2.0.dist-info/RECORD +92 -0
  48. {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/WHEEL +1 -1
  49. mini_arcade_core/managers/inputs.py +0 -284
  50. mini_arcade_core/runtime/scene/scene_adapter.py +0 -125
  51. mini_arcade_core/runtime/scene/scene_port.py +0 -170
  52. mini_arcade_core/sim/protocols.py +0 -41
  53. mini_arcade_core/sim/runner.py +0 -222
  54. mini_arcade_core-1.1.0.dist-info/RECORD +0 -80
  55. /mini_arcade_core/{managers → engine}/cheats.py +0 -0
  56. /mini_arcade_core/{managers → engine/loop}/__init__.py +0 -0
  57. /mini_arcade_core/{sim → engine/render/effects}/__init__.py +0 -0
  58. {mini_arcade_core-1.1.0.dist-info → mini_arcade_core-1.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -24,7 +24,7 @@ class UIPass:
24
24
  """Run the UI render pass."""
25
25
  # UI overlays should be screen-space (no world transform / no clip unless you want it)
26
26
  backend.clear_viewport_transform()
27
- backend.clear_clip_rect()
27
+ backend.render.clear_clip_rect()
28
28
 
29
29
  for fp in packets:
30
30
  if not fp.is_overlay:
@@ -41,15 +41,15 @@ class WorldPass:
41
41
  backend.set_viewport_transform(
42
42
  ctx.viewport.offset_x, ctx.viewport.offset_y, ctx.viewport.scale
43
43
  )
44
- backend.set_clip_rect(
45
- ctx.viewport.offset_x,
46
- ctx.viewport.offset_y,
47
- ctx.viewport.viewport_w,
48
- ctx.viewport.viewport_h,
44
+ backend.render.set_clip_rect(
45
+ 0,
46
+ 0,
47
+ ctx.viewport.virtual_w,
48
+ ctx.viewport.virtual_h,
49
49
  )
50
50
  try:
51
51
  for op in packet.ops:
52
52
  op(backend)
53
53
  finally:
54
- backend.clear_clip_rect()
54
+ backend.render.clear_clip_rect()
55
55
  backend.clear_viewport_transform()
@@ -19,6 +19,7 @@ from dataclasses import dataclass, field
19
19
 
20
20
  from mini_arcade_core.backend import Backend
21
21
  from mini_arcade_core.engine.render.context import RenderContext
22
+ from mini_arcade_core.engine.render.frame_packet import FramePacket
22
23
  from mini_arcade_core.engine.render.packet import RenderPacket
23
24
  from mini_arcade_core.engine.render.passes.base import RenderPass
24
25
  from mini_arcade_core.engine.render.passes.begin_frame import BeginFramePass
@@ -56,10 +57,10 @@ class RenderPipeline:
56
57
  )
57
58
 
58
59
  def render_frame(
59
- self, backend: Backend, ctx: RenderContext, packets: list[RenderPacket]
60
+ self, backend: Backend, ctx: RenderContext, packets: list[FramePacket]
60
61
  ):
61
62
  """
62
- Render a frame using the provided Backend, RenderContext, and list of RenderPackets.
63
+ Render a frame using the provided Backend, RenderContext, and list of FramePackets.
63
64
 
64
65
  :param backend: Backend to use for rendering.
65
66
  :type backend: Backend
@@ -67,8 +68,8 @@ class RenderPipeline:
67
68
  :param ctx: RenderContext containing rendering state.
68
69
  :type ctx: RenderContext
69
70
 
70
- :param packets: List of RenderPackets to render.
71
- :type packets: list[RenderPacket]
71
+ :param packets: List of FramePackets to render.
72
+ :type packets: list[FramePacket]
72
73
  """
73
74
  for p in self.passes:
74
75
  p.run(backend, ctx, packets)
@@ -98,8 +99,8 @@ class RenderPipeline:
98
99
  )
99
100
 
100
101
  backend.set_clip_rect(
101
- viewport_state.offset_x,
102
- viewport_state.offset_y,
102
+ 0,
103
+ 0,
103
104
  viewport_state.viewport_w,
104
105
  viewport_state.viewport_h,
105
106
  )
@@ -4,6 +4,7 @@ Viewport management for virtual to screen coordinate transformations.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import math
7
8
  from dataclasses import dataclass
8
9
  from enum import Enum
9
10
 
@@ -130,10 +131,15 @@ class Viewport:
130
131
  sy = window_h / self._virtual_h
131
132
  scale = min(sx, sy) if self._mode == ViewportMode.FIT else max(sx, sy)
132
133
 
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))
134
+ if self._mode == ViewportMode.FIT:
135
+ vw = int(math.floor(self._virtual_w * scale))
136
+ vh = int(math.floor(self._virtual_h * scale))
137
+ else: # FILL
138
+ vw = int(math.ceil(self._virtual_w * scale))
139
+ vh = int(math.ceil(self._virtual_h * scale))
140
+
141
+ ox = (window_w - vw) // 2
142
+ oy = (window_h - vh) // 2
137
143
 
138
144
  self._state = ViewportState(
139
145
  virtual_w=self._virtual_w,
File without changes
@@ -0,0 +1,54 @@
1
+ """
2
+ Models for scene management in the engine.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from mini_arcade_core.scenes.sim_scene import SimScene
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ScenePolicy:
14
+ """
15
+ Controls how a scene behaves in the scene stack.
16
+
17
+ blocks_update: if True, scenes below do not tick/update (pause modal)
18
+ blocks_input: if True, scenes below do not receive input
19
+ is_opaque: if True, scenes below are not rendered
20
+ receives_input: if True, scene can receive input
21
+ """
22
+
23
+ blocks_update: bool = False
24
+ blocks_input: bool = False
25
+ is_opaque: bool = False
26
+ receives_input: bool = True
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class SceneEntry:
31
+ """
32
+ An entry in the scene stack.
33
+
34
+ :ivar scene_id (str): Identifier of the scene.
35
+ :ivar scene (SimScene): The scene instance.
36
+ :ivar is_overlay (bool): Whether the scene is an overlay.
37
+ :ivar policy (ScenePolicy): The scene's policy.
38
+ """
39
+
40
+ scene_id: str
41
+ scene: SimScene
42
+ is_overlay: bool
43
+ policy: ScenePolicy
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class StackItem:
48
+ """
49
+ An item in the scene stack.
50
+
51
+ :ivar entry (SceneEntry): The scene entry.
52
+ """
53
+
54
+ entry: SceneEntry
@@ -0,0 +1,213 @@
1
+ """
2
+ Module providing runtime adapters for window and scene management.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, List
8
+
9
+ from mini_arcade_core.engine.scenes.models import (
10
+ SceneEntry,
11
+ ScenePolicy,
12
+ StackItem,
13
+ )
14
+ from mini_arcade_core.runtime.context import RuntimeContext
15
+ from mini_arcade_core.scenes.registry import SceneRegistry
16
+
17
+ if TYPE_CHECKING:
18
+ from mini_arcade_core.engine.game import Game
19
+ from mini_arcade_core.scenes.sim_scene import SimScene
20
+
21
+
22
+ class SceneAdapter:
23
+ """
24
+ Manages multiple scenes (not implemented).
25
+ """
26
+
27
+ def __init__(self, registry: SceneRegistry, game: Game):
28
+ self._registry = registry
29
+ self._stack: List[StackItem] = []
30
+ self._game = game
31
+
32
+ @property
33
+ def current_scene(self) -> SimScene | None:
34
+ """
35
+ Get the currently active scene.
36
+
37
+ :return: The active Scene instance, or None if no scene is active.
38
+ :rtype: SimScene | None
39
+ """
40
+ return self._stack[-1].entry.scene if self._stack else None
41
+
42
+ @property
43
+ def visible_stack(self) -> List[SimScene]:
44
+ """
45
+ Return the list of scenes that should be drawn (base + overlays).
46
+ We draw from the top-most non-overlay scene upward.
47
+
48
+ :return: List of visible Scene instances.
49
+ :rtype: List[SimScene]
50
+ """
51
+ return [e.scene for e in self.visible_entries()]
52
+
53
+ @property
54
+ def listed_scenes(self) -> List[SimScene]:
55
+ """
56
+ Return all scenes in the stack.
57
+
58
+ :return: List of all Scene instances in the stack.
59
+ :rtype: List[SimScene]
60
+ """
61
+ return [item.entry.scene for item in self._stack]
62
+
63
+ def change(self, scene_id: str):
64
+ """
65
+ Change the current scene to the specified scene.
66
+
67
+ :param scene_id: Identifier of the scene to switch to.
68
+ :type scene_id: str
69
+ """
70
+ self.clean()
71
+ self.push(scene_id, as_overlay=False)
72
+
73
+ def push(
74
+ self,
75
+ scene_id: str,
76
+ *,
77
+ as_overlay: bool = False,
78
+ policy: ScenePolicy | None = None,
79
+ ):
80
+ """
81
+ Push a new scene onto the scene stack.
82
+
83
+ :param scene_id: Identifier of the scene to push.
84
+ :type scene_id: str
85
+
86
+ :param as_overlay: Whether to push the scene as an overlay.
87
+ :type as_overlay: bool
88
+ """
89
+ # default policy based on overlay vs base
90
+ if policy is None:
91
+ # base scenes: do not block anything by default
92
+ policy = ScenePolicy()
93
+ runtime_context = RuntimeContext.from_game(self._game)
94
+ scene = self._registry.create(
95
+ scene_id, runtime_context
96
+ ) # or whatever your factory call is
97
+ scene.on_enter()
98
+
99
+ entry = SceneEntry(
100
+ scene_id=scene_id,
101
+ scene=scene,
102
+ is_overlay=as_overlay,
103
+ policy=policy,
104
+ )
105
+ self._stack.append(StackItem(entry=entry))
106
+
107
+ def pop(self) -> SimScene | None:
108
+ """
109
+ Pop the current scene from the scene stack.
110
+
111
+ :return: The popped Scene instance, or None if the stack was empty.
112
+ :rtype: SimScene | None
113
+ """
114
+ if not self._stack:
115
+ return
116
+ item = self._stack.pop()
117
+ item.entry.scene.on_exit()
118
+
119
+ def clean(self):
120
+ """Clean up all scenes from the scene stack."""
121
+ while self._stack:
122
+ self.pop()
123
+
124
+ def quit(self):
125
+ """Quit the game"""
126
+ self._game.quit()
127
+
128
+ def visible_entries(self) -> list[SceneEntry]:
129
+ """
130
+ Render from bottom->top unless an opaque entry exists; if so,
131
+ render only from that entry up.
132
+
133
+ :return: List of SceneEntry instances to render.
134
+ :rtype: list[SceneEntry]
135
+ """
136
+ entries = [i.entry for i in self._stack]
137
+ # find highest opaque from top down; render starting there
138
+ for idx in range(len(entries) - 1, -1, -1):
139
+ if entries[idx].policy.is_opaque:
140
+ return entries[idx:]
141
+ return entries
142
+
143
+ def update_entries(self) -> list[SceneEntry]:
144
+ """
145
+ Tick/update scenes considering blocks_update.
146
+ Typical: pause overlay blocks update below it.
147
+
148
+ :return: List of SceneEntry instances to update.
149
+ :rtype: list[SceneEntry]
150
+ """
151
+ vis = self.visible_entries()
152
+ if not vis:
153
+ return []
154
+ out = []
155
+ for entry in reversed(vis): # top->down
156
+ out.append(entry)
157
+ if entry.policy.blocks_update:
158
+ break
159
+ return list(reversed(out)) # bottom->top order
160
+
161
+ def input_entry(self) -> SceneEntry | None:
162
+ """
163
+ Who gets input this frame. If top blocks_input, only it receives input.
164
+ If not, top still gets input (v1 simple). Later you can allow fall-through.
165
+
166
+ :return: The SceneEntry that receives input, or None if no scenes are active.
167
+ :rtype: SceneEntry | None
168
+ """
169
+ vis = self.visible_entries()
170
+ if not vis:
171
+ return None
172
+
173
+ # If some scene blocks input, only scenes at/above it can receive.
174
+ start_idx = 0
175
+ for idx in range(len(vis) - 1, -1, -1):
176
+ if vis[idx].policy.blocks_input:
177
+ start_idx = idx
178
+ break
179
+
180
+ candidates = vis[start_idx:]
181
+
182
+ # Pick the top-most candidate that actually receives input.
183
+ for entry in reversed(candidates):
184
+ if entry.policy.receives_input:
185
+ return entry
186
+
187
+ return None
188
+
189
+ def has_scene(self, scene_id: str) -> bool:
190
+ """
191
+ Check if a scene with the given ID exists in the stack.
192
+
193
+ :param scene_id: Identifier of the scene to check.
194
+ :type scene_id: str
195
+
196
+ :return: True if the scene exists in the stack, False otherwise.
197
+ :rtype: bool
198
+ """
199
+ return any(item.entry.scene_id == scene_id for item in self._stack)
200
+
201
+ def remove_scene(self, scene_id: str):
202
+ """
203
+ Remove a scene with the given ID from the stack.
204
+
205
+ :param scene_id: Identifier of the scene to remove.
206
+ :type scene_id: str
207
+ """
208
+ # remove first match from top (overlay is usually near top)
209
+ for i in range(len(self._stack) - 1, -1, -1):
210
+ if self._stack[i].entry.scene_id == scene_id:
211
+ item = self._stack.pop(i)
212
+ item.entry.scene.on_exit()
213
+ return
@@ -4,17 +4,18 @@ Module providing runtime adapters for window and scene management.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ from mini_arcade_core.backend import Backend
7
8
  from mini_arcade_core.runtime.audio.audio_port import AudioPort
8
9
 
9
10
 
10
11
  class SDLAudioAdapter(AudioPort):
11
12
  """A no-op audio adapter."""
12
13
 
13
- def __init__(self, backend):
14
+ def __init__(self, backend: Backend):
14
15
  self.backend = backend
15
16
 
16
17
  def load_sound(self, sound_id: str, file_path: str):
17
- self.backend.load_sound(sound_id, file_path)
18
+ self.backend.audio.load_sound(sound_id, file_path)
18
19
 
19
20
  def play(self, sound_id: str, loops: int = 0):
20
- self.backend.play_sound(sound_id, loops)
21
+ self.backend.audio.play_sound(sound_id, loops)
@@ -4,14 +4,10 @@ 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
-
9
7
 
10
8
  class AudioPort:
11
9
  """Interface for audio playback operations."""
12
10
 
13
- backend: Backend
14
-
15
11
  def load_sound(self, sound_id: str, file_path: str):
16
12
  """
17
13
  Load a sound from a file and associate it with an identifier.
@@ -74,7 +74,14 @@ class CapturePathBuilder:
74
74
 
75
75
 
76
76
  class CaptureAdapter(CapturePort):
77
- """Adapter for capturing frames."""
77
+ """
78
+ Adapter for capturing frames.
79
+
80
+ :param backend: Backend instance to use for capturing frames.
81
+ :type backend: Backend
82
+ :param path_builder: Optional CapturePathBuilder for building file paths.
83
+ :type path_builder: CapturePathBuilder | None
84
+ """
78
85
 
79
86
  def __init__(
80
87
  self,
@@ -96,9 +103,9 @@ class CaptureAdapter(CapturePort):
96
103
  # If backend only saves BMP, capture to a temp bmp next to output
97
104
  bmp_path = out_path.with_suffix(".bmp")
98
105
 
99
- self.backend.capture_frame(str(bmp_path))
106
+ self.backend.capture.bmp(str(bmp_path))
100
107
  if not bmp_path.exists():
101
- raise RuntimeError("Backend capture_frame did not create BMP file")
108
+ raise RuntimeError("Backend capture.bmp did not create BMP file")
102
109
 
103
110
  self._bmp_to_image(str(bmp_path), str(out_path))
104
111
  try:
@@ -112,7 +119,7 @@ class CaptureAdapter(CapturePort):
112
119
  return str(out_path)
113
120
 
114
121
  def screenshot_bytes(self) -> bytes:
115
- data = self.backend.capture_frame(path=None)
122
+ data = self.backend.capture.bmp(path=None)
116
123
  if data is None:
117
124
  raise RuntimeError("Backend returned None for screenshot_bytes()")
118
125
  return data
@@ -125,10 +132,10 @@ class CaptureAdapter(CapturePort):
125
132
  out_path.parent.mkdir(parents=True, exist_ok=True)
126
133
 
127
134
  bmp_path = out_path.with_suffix(".bmp")
128
- self.backend.capture_frame(str(bmp_path))
135
+ self.backend.capture.bmp(str(bmp_path))
129
136
 
130
137
  if not bmp_path.exists():
131
- raise RuntimeError("Backend capture_frame did not create BMP file")
138
+ raise RuntimeError("Backend capture.bmp did not create BMP file")
132
139
 
133
140
  self._bmp_to_image(str(bmp_path), str(out_path))
134
141
 
@@ -4,14 +4,10 @@ Service interfaces for runtime components.
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
- from mini_arcade_core.backend import Backend
8
-
9
7
 
10
8
  class CapturePort:
11
9
  """Interface for frame capture operations."""
12
10
 
13
- backend: Backend
14
-
15
11
  def screenshot(self, label: str | None = None) -> str:
16
12
  """
17
13
  Capture the current frame.
@@ -9,12 +9,14 @@ from dataclasses import dataclass
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  if TYPE_CHECKING:
12
+ from mini_arcade_core.engine.cheats import CheatManager
12
13
  from mini_arcade_core.engine.commands import CommandQueue
13
- from mini_arcade_core.engine.game import Game, GameConfig, GameSettings
14
- from mini_arcade_core.managers.cheats import CheatManager
14
+ from mini_arcade_core.engine.game import Game, GameConfig
15
+ from mini_arcade_core.engine.gameplay_settings import GamePlaySettings
15
16
  from mini_arcade_core.runtime.services import RuntimeServices
16
17
 
17
18
 
19
+ # TODO: Remove cheats and command_queue from here later if unused.
18
20
  @dataclass(frozen=True)
19
21
  class RuntimeContext:
20
22
  """
@@ -22,14 +24,14 @@ class RuntimeContext:
22
24
 
23
25
  :ivar services (RuntimeServices): Runtime services.
24
26
  :ivar config (GameConfig): Game configuration.
25
- :ivar settings (GameSettings): Game settings.
27
+ :ivar settings (GamePlaySettings): Game settings.
26
28
  :ivar command_queue (CommandQueue | None): Optional command queue.
27
29
  :ivar cheats (CheatManager | None): Optional cheat manager.
28
30
  """
29
31
 
30
32
  services: RuntimeServices
31
33
  config: GameConfig
32
- settings: GameSettings
34
+ settings: GamePlaySettings
33
35
  command_queue: CommandQueue | None = None
34
36
  cheats: CheatManager | None = None
35
37
 
@@ -48,6 +50,6 @@ class RuntimeContext:
48
50
  services=game_entity.services,
49
51
  config=game_entity.config,
50
52
  settings=game_entity.settings,
51
- command_queue=game_entity.command_queue,
52
- cheats=game_entity.cheat_manager,
53
+ command_queue=game_entity.managers.command_queue,
54
+ cheats=game_entity.managers.cheats,
53
55
  )
@@ -0,0 +1,31 @@
1
+ """
2
+ Scene query adapter implementation.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Sequence
9
+
10
+ from mini_arcade_core.engine.scenes.models import SceneEntry
11
+ from mini_arcade_core.engine.scenes.scene_manager import SceneAdapter
12
+ from mini_arcade_core.runtime.scene.scene_query_port import SceneQueryPort
13
+
14
+
15
+ @dataclass
16
+ class SceneQueryAdapter(SceneQueryPort):
17
+ """Adapter that exposes a read-only view of the SceneAdapter manager."""
18
+
19
+ _scenes: SceneAdapter
20
+
21
+ def visible_entries(self) -> Sequence[SceneEntry]:
22
+ return list(self._scenes.visible_entries())
23
+
24
+ def input_entry(self) -> SceneEntry | None:
25
+ return self._scenes.input_entry()
26
+
27
+ def stack_summary(self) -> list[str]:
28
+ out: list[str] = []
29
+ for e in self._scenes.visible_entries():
30
+ out.append(f"{e.scene_id} overlay={e.is_overlay}")
31
+ return out
@@ -0,0 +1,38 @@
1
+ """
2
+ Scene query port protocol.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Protocol, Sequence, runtime_checkable
8
+
9
+ from mini_arcade_core.engine.scenes.models import SceneEntry
10
+
11
+
12
+ @runtime_checkable
13
+ class SceneQueryPort(Protocol):
14
+ """Read-only queries over the engine scene stack."""
15
+
16
+ def visible_entries(self) -> Sequence[SceneEntry]:
17
+ """
18
+ Scenes that should be rendered (policy-aware).
19
+
20
+ :return: Sequence of SceneEntry instances that are visible.
21
+ :rtype: Sequence[SceneEntry]
22
+ """
23
+
24
+ def input_entry(self) -> SceneEntry | None:
25
+ """
26
+ The scene that currently receives input (top-most eligible).
27
+
28
+ :return: SceneEntry that receives input, or None if stack is empty.
29
+ :rtype: SceneEntry | None
30
+ """
31
+
32
+ def stack_summary(self) -> list[str]:
33
+ """
34
+ Convenience: human-readable stack lines for debug overlays.
35
+
36
+ :return: List of strings summarizing the scene stack.
37
+ :rtype: list[str]
38
+ """
@@ -11,7 +11,7 @@ from mini_arcade_core.runtime.capture.capture_port import CapturePort
11
11
  from mini_arcade_core.runtime.file.file_port import FilePort
12
12
  from mini_arcade_core.runtime.input.input_port import InputPort
13
13
  from mini_arcade_core.runtime.render.render_port import RenderServicePort
14
- from mini_arcade_core.runtime.scene.scene_port import ScenePort
14
+ from mini_arcade_core.runtime.scene.scene_query_port import SceneQueryPort
15
15
  from mini_arcade_core.runtime.window.window_port import WindowPort
16
16
 
17
17
 
@@ -26,12 +26,13 @@ class RuntimeServices:
26
26
  :ivar files (FilePort): File service port.
27
27
  :ivar capture (CapturePort): Capture service port.
28
28
  :ivar input (InputPort): Input handling service port.
29
+ :ivar render (RenderServicePort): Rendering service port.
29
30
  """
30
31
 
31
32
  window: WindowPort
32
- scenes: ScenePort
33
33
  audio: AudioPort
34
34
  files: FilePort
35
35
  capture: CapturePort
36
36
  input: InputPort
37
37
  render: RenderServicePort
38
+ scenes: SceneQueryPort