mini-arcade-core 1.1.1__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +182 -184
  4. mini_arcade_core/backend/types.py +5 -1
  5. mini_arcade_core/engine/commands.py +8 -8
  6. mini_arcade_core/engine/game.py +54 -354
  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 +2 -2
  16. mini_arcade_core/engine/render/effects/crt.py +4 -4
  17. mini_arcade_core/engine/render/effects/registry.py +1 -1
  18. mini_arcade_core/engine/render/effects/vignette.py +8 -8
  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 +1 -1
  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/models.py +54 -0
  27. mini_arcade_core/engine/scenes/scene_manager.py +213 -0
  28. mini_arcade_core/runtime/audio/audio_adapter.py +4 -3
  29. mini_arcade_core/runtime/audio/audio_port.py +0 -4
  30. mini_arcade_core/runtime/capture/capture_adapter.py +53 -31
  31. mini_arcade_core/runtime/capture/capture_port.py +0 -4
  32. mini_arcade_core/runtime/capture/capture_worker.py +174 -0
  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.1.dist-info → mini_arcade_core-1.2.1.dist-info}/METADATA +1 -1
  47. mini_arcade_core-1.2.1.dist-info/RECORD +93 -0
  48. {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.1.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.1.dist-info/RECORD +0 -85
  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/scenes}/__init__.py +0 -0
  58. {mini_arcade_core-1.1.1.dist-info → mini_arcade_core-1.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -4,21 +4,15 @@ Game core module defining the Game class and configuration.
4
4
 
5
5
  from __future__ import annotations
6
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, WindowSettings
12
- from mini_arcade_core.backend.events import EventType
13
- from mini_arcade_core.backend.keys import Key
14
- from mini_arcade_core.engine.commands import (
15
- CommandContext,
16
- CommandQueue,
17
- QuitCommand,
18
- ToggleDebugOverlayCommand,
19
- ToggleEffectCommand,
20
- )
21
- from mini_arcade_core.engine.render.context import RenderContext
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
22
16
  from mini_arcade_core.engine.render.effects.base import (
23
17
  EffectParams,
24
18
  EffectStack,
@@ -26,211 +20,75 @@ from mini_arcade_core.engine.render.effects.base import (
26
20
  from mini_arcade_core.engine.render.effects.crt import CRTEffect
27
21
  from mini_arcade_core.engine.render.effects.registry import EffectRegistry
28
22
  from mini_arcade_core.engine.render.effects.vignette import VignetteNoiseEffect
29
- from mini_arcade_core.engine.render.frame_packet import FramePacket
30
- from mini_arcade_core.engine.render.packet import RenderPacket
31
23
  from mini_arcade_core.engine.render.pipeline import RenderPipeline
32
24
  from mini_arcade_core.engine.render.render_service import RenderService
33
- from mini_arcade_core.managers.cheats import CheatManager
25
+ from mini_arcade_core.engine.scenes.scene_manager import SceneAdapter
34
26
  from mini_arcade_core.runtime.audio.audio_adapter import SDLAudioAdapter
35
27
  from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
36
28
  from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
37
29
  from mini_arcade_core.runtime.input.input_adapter import InputAdapter
38
- from mini_arcade_core.runtime.input_frame import InputFrame
39
- from mini_arcade_core.runtime.scene.scene_adapter import SceneAdapter
30
+ from mini_arcade_core.runtime.scene.scene_query_adapter import (
31
+ SceneQueryAdapter,
32
+ )
40
33
  from mini_arcade_core.runtime.services import RuntimeServices
41
34
  from mini_arcade_core.runtime.window.window_adapter import WindowAdapter
42
35
  from mini_arcade_core.scenes.registry import SceneRegistry
43
- from mini_arcade_core.utils import logger
44
-
45
-
46
- @dataclass
47
- class WindowConfig:
48
- """
49
- Configuration for a game window (not implemented).
50
-
51
- :ivar width (int): Width of the window in pixels.
52
- :ivar height (int): Height of the window in pixels.
53
- :ivar background_color (tuple[int, int, int]): RGB background color.
54
- :ivar title (str): Title of the window.
55
- """
56
-
57
- width: int
58
- height: int
59
- background_color: tuple[int, int, int]
60
- title: str
61
-
62
-
63
- @dataclass
64
- class PostFXConfig:
65
- """
66
- Configuration for post-processing effects.
67
-
68
- :ivar enabled (bool): Whether post effects are enabled by default.
69
- :ivar active (list[str]): List of active effect IDs by default.
70
- """
71
-
72
- enabled: bool = True
73
- active: list[str] = field(default_factory=list)
74
-
75
-
76
- @dataclass
77
- class GameConfig:
78
- """
79
- Configuration options for the Game.
80
-
81
- :ivar window (WindowConfig | None): Optional window configuration.
82
- :ivar fps (int): Target frames per second.
83
- :ivar backend (Backend | None): Optional Backend instance to use for rendering and input.
84
- """
85
-
86
- window: WindowConfig | None = None
87
- fps: int = 60
88
- backend: Backend | None = None
89
- postfx: PostFXConfig = field(default_factory=PostFXConfig)
90
-
91
-
92
- Difficulty = Literal["easy", "normal", "hard", "insane"]
93
-
94
-
95
- @dataclass
96
- class GameSettings:
97
- """
98
- Game settings that can be modified during gameplay.
99
-
100
- :ivar difficulty (Difficulty): Current game difficulty level.
101
- """
102
-
103
- difficulty: Difficulty = "normal"
104
- effects_stack: EffectStack | None = None
105
-
36
+ from mini_arcade_core.utils import FrameTimer
37
+ from mini_arcade_core.utils.profiler import FrameTimerConfig
106
38
 
107
- def _neutral_input(frame_index: int, dt: float) -> InputFrame:
108
- """Create a neutral InputFrame with no input events."""
109
- return InputFrame(frame_index=frame_index, dt=dt)
110
39
 
111
-
112
- @dataclass
113
- class FrameTimer:
114
- """
115
- Simple frame timer for marking and reporting time intervals.
116
-
117
- :ivar enabled (bool): Whether timing is enabled.
118
- :ivar marks (Dict[str, float]): Recorded time marks.
119
- """
120
-
121
- enabled: bool = False
122
- marks: Dict[str, float] = field(default_factory=dict)
123
-
124
- def mark(self, name: str):
125
- """
126
- Record a time mark with the given name.
127
-
128
- :param name: Name of the mark.
129
- :type name: str
130
- """
131
- if not self.enabled:
132
- return
133
- self.marks[name] = perf_counter()
134
-
135
- def diff_ms(self, start: str, end: str) -> float:
136
- """
137
- Get the time difference in milliseconds between two marks.
138
-
139
- :param start: Name of the start mark.
140
- :type start: str
141
-
142
- :param end: Name of the end mark.
143
- :type end: str
144
-
145
- :return: Time difference in milliseconds.
146
- :rtype: float
147
- """
148
- return (self.marks[end] - self.marks[start]) * 1000.0
149
-
150
- def report_ms(self) -> Dict[str, float]:
151
- """
152
- Returns diffs between consecutive marks in insertion order.
153
-
154
- :return: Dictionary mapping "start->end" to time difference in milliseconds.
155
- :rtype: Dict[str, float]
156
- """
157
- if not self.enabled:
158
- return {}
159
-
160
- keys = list(self.marks.keys())
161
- out: Dict[str, float] = {}
162
- for a, b in zip(keys, keys[1:]):
163
- out[f"{a}->{b}"] = self.diff_ms(a, b)
164
- return out
165
-
166
- def clear(self):
167
- """Clear all recorded marks."""
168
- if not self.enabled:
169
- return
170
- self.marks.clear()
171
-
172
-
173
- # TODO: Fix too-many-instance-attributes warning
174
- # Justification: Core game class with many dependencies.
175
- # pylint: disable=too-many-instance-attributes
176
40
  class Game:
177
41
  """Core game object responsible for managing the main loop and active scene."""
178
42
 
179
43
  def __init__(
180
- self, config: GameConfig, registry: SceneRegistry | None = None
44
+ self, config: GameConfig, scene_registry: SceneRegistry | None = None
181
45
  ):
182
46
  """
183
47
  :param config: Game configuration options.
184
48
  :type config: GameConfig
185
49
 
186
- :param registry: Optional SceneRegistry for scene management.
187
- :type registry: SceneRegistry | None
50
+ :param scene_registry: Optional SceneRegistry for scene management.
51
+ :type scene_registry: SceneRegistry | None
188
52
 
189
53
  :raises ValueError: If the provided config does not have a valid Backend.
190
54
  """
191
55
  self.config = config
192
56
  self._running: bool = False
193
57
 
194
- if config.backend is None:
58
+ if self.config.backend is None:
195
59
  raise ValueError(
196
60
  "GameConfig.backend must be set to a Backend instance"
197
61
  )
198
- if config.window is None:
199
- raise ValueError("GameConfig.window must be set")
200
62
 
201
- self.backend: Backend = config.backend
202
- self.registry = registry or SceneRegistry(_factories={})
203
- self.settings = GameSettings()
204
- self.services = RuntimeServices(
205
- window=WindowAdapter(
206
- self.backend,
207
- WindowSettings(
208
- width=self.config.window.width,
209
- height=self.config.window.height,
210
- ),
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
211
70
  ),
212
- scenes=SceneAdapter(self.registry, self),
71
+ )
72
+ self.services = RuntimeServices(
73
+ window=WindowAdapter(self.backend), # Turn into a manager?
213
74
  audio=SDLAudioAdapter(self.backend),
214
75
  files=LocalFilesAdapter(),
215
76
  capture=CaptureAdapter(self.backend),
216
77
  input=InputAdapter(),
217
78
  render=RenderService(),
79
+ scenes=SceneQueryAdapter(self.managers.scenes),
218
80
  )
219
81
 
220
- self.command_queue = CommandQueue()
221
- self.cheat_manager = CheatManager()
82
+ @property
83
+ def running(self) -> bool:
84
+ """Check if the game is currently running."""
85
+ return self._running
222
86
 
223
87
  def quit(self):
224
88
  """Request that the main loop stops."""
225
89
  self._running = False
226
90
 
227
- # TODO: Fix too-many-statements and too-many-locals warnings
228
- # Justification: Main game loop with multiple responsibilities.
229
- # pylint: disable=too-many-statements,too-many-locals
230
- # TODO: Fix too-many-branches warning
231
- # Justification: Complex control flow in main loop.
232
- # pylint: disable=too-many-branches
233
- def run(self, initial_scene_id: str):
91
+ def run(self):
234
92
  """
235
93
  Run the main loop starting with the given scene.
236
94
 
@@ -240,14 +98,9 @@ class Game:
240
98
  :param initial_scene_id: The scene id to start the game with (must be registered).
241
99
  :type initial_scene_id: str
242
100
  """
243
- backend = self.backend
244
-
245
- self._initialize_window()
246
-
247
- self.services.scenes.change(initial_scene_id)
101
+ self.managers.scenes.change(self.config.initial_scene)
248
102
 
249
103
  pipeline = RenderPipeline()
250
-
251
104
  effects_registry = EffectRegistry()
252
105
  effects_registry.register(CRTEffect())
253
106
  effects_registry.register(VignetteNoiseEffect())
@@ -269,186 +122,33 @@ class Game:
269
122
  p.registry = effects_registry
270
123
 
271
124
  self._running = True
272
- target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
273
- last_time = perf_counter()
274
- frame_index = 0
275
-
276
- # cache packets so blocked-update scenes still render their last frame
277
- packet_cache: dict[int, RenderPacket] = {}
278
-
279
- timer = FrameTimer(enabled=True)
280
- # report_every = 60 # print once per second at 60fps
281
-
282
- # TODO: Integrate SimRunner for simulation stepping
283
- # TODO: Fix assignment-from-no-return warning in self.services.input.build
284
- # & self.services.scenes.input_entry
285
- # Justification: These methods are expected to return values.
286
- # pylint: disable=assignment-from-no-return
287
- time_s = 0.0
288
-
289
- while self._running:
290
- timer.clear()
291
- timer.mark("frame_start")
292
-
293
- now = perf_counter()
294
- dt = now - last_time
295
- last_time = now
296
-
297
- events = list(backend.poll_events())
298
-
299
- for e in events:
300
- if e.type == EventType.WINDOWRESIZED and e.size:
301
- w, h = e.size
302
- logger.debug(f"Window resized event: {w}x{h}")
303
- self.services.window.on_window_resized(w, h)
304
- # if F1 pressed, toggle debug overlay
305
- if e.type == EventType.KEYDOWN:
306
- if e.key == Key.F1:
307
- self.command_queue.push(ToggleDebugOverlayCommand())
308
- elif e.key == Key.F2:
309
- self.command_queue.push(ToggleEffectCommand("crt"))
310
- elif e.key == Key.F3:
311
- self.command_queue.push(
312
- ToggleEffectCommand("vignette_noise")
313
- )
314
- elif e.key == Key.F4:
315
- effects_stack.enabled = not effects_stack.enabled
316
- timer.mark("events_polled")
317
-
318
- input_frame = self.services.input.build(events, frame_index, dt)
319
- timer.mark("input_built")
320
-
321
- # Window/OS quit (close button)
322
- if input_frame.quit:
323
- self.command_queue.push(QuitCommand())
324
-
325
- # who gets input?
326
- input_entry = self.services.scenes.input_entry()
327
- if input_entry is None:
328
- break
329
-
330
- # tick policy-aware scenes
331
- timer.mark("tick_start")
332
- for entry in self.services.scenes.update_entries():
333
- scene = entry.scene
334
- effective_input = (
335
- input_frame
336
- if entry is input_entry
337
- else _neutral_input(frame_index, dt)
338
- )
339
-
340
- packet = scene.tick(effective_input, dt)
341
- packet_cache[id(scene)] = packet
342
- timer.mark("tick_end")
343
-
344
- timer.mark("command_ctx_start")
345
- command_context = CommandContext(
346
- services=self.services,
347
- commands=self.command_queue,
348
- settings=self.settings,
349
- world=self._resolve_world(),
350
- )
351
- timer.mark("command_ctx_end")
352
-
353
- timer.mark("cheats_start")
354
- self.cheat_manager.process_frame(
355
- input_frame,
356
- context=command_context,
357
- queue=self.command_queue,
358
- )
359
- timer.mark("cheats_end")
360
125
 
361
- # Execute commands at the end of the frame (consistent write path)
362
- timer.mark("cmd_exec_start")
363
- for cmd in self.command_queue.drain():
364
- cmd.execute(command_context)
365
- timer.mark("cmd_exec_end")
366
-
367
- # ---------------- TO REPLACE WITH RENDERING PIPELINE ----------------
368
- timer.mark("render_start")
369
-
370
- vp = self.services.window.get_viewport()
371
-
372
- # gather visible packets
373
- frame_packets: list[RenderPacket] = []
374
- for entry in self.services.scenes.visible_entries():
375
- scene = entry.scene
376
- packet = packet_cache.get(id(scene))
377
- if packet is None:
378
- packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
379
- packet_cache[id(scene)] = packet
380
- frame_packets.append(
381
- FramePacket(
382
- scene_id=entry.scene_id,
383
- is_overlay=entry.is_overlay,
384
- packet=packet,
385
- )
386
- )
387
-
388
- render_ctx = RenderContext(
389
- viewport=vp,
390
- debug_overlay=getattr(self.settings, "debug_overlay", False),
391
- frame_ms=dt * 1000.0,
392
- )
393
- time_s += dt
394
- render_ctx.meta["frame_index"] = frame_index
395
- render_ctx.meta["time_s"] = time_s
396
- render_ctx.meta["effects_stack"] = effects_stack
397
-
398
- self.services.render.last_frame_ms = render_ctx.frame_ms
399
- self.services.render.last_stats = render_ctx.stats
400
- pipeline.render_frame(backend, render_ctx, frame_packets)
401
-
402
- timer.mark("render_done")
403
- # ---------------- END RENDERING PIPELINE ----------------------------
404
- timer.mark("end_frame_done")
405
-
406
- timer.mark("sleep_start")
407
- if target_dt > 0 and dt < target_dt:
408
- sleep(target_dt - dt)
409
- timer.mark("sleep_end")
410
-
411
- # --- report ---
412
- # if timer.enabled and (
413
- # frame_index % report_every == 0 and frame_index > 0
414
- # ):
415
- # ms = timer.report_ms()
416
- # total = (perf_counter() - timer.marks["frame_start"]) * 1000.0
417
- # logger.debug(
418
- # f"[Frame {frame_index}] total={total:.2f}ms | {ms}"
419
- # )
420
-
421
- frame_index += 1
422
-
423
- # pylint: enable=assignment-from-no-return
424
-
425
- # exit remaining scenes
426
- self.services.scenes.clean()
427
-
428
- # pylint: enable=too-many-statements,too-many-locals
429
-
430
- def _initialize_window(self):
431
- """Initialize the game window based on the configuration."""
432
- self.services.window.set_window_size(
433
- self.config.window.width, self.config.window.height
126
+ timer = FrameTimer(
127
+ config=FrameTimerConfig(enabled=self.config.enable_profiler)
434
128
  )
435
- self.services.window.set_title(self.config.window.title)
129
+ hooks = DefaultGameHooks(self, effects_stack)
436
130
 
437
- br, bg, bb = self.config.window.background_color
438
- self.services.window.set_clear_color(br, bg, bb)
439
-
440
- # the “authoring resolution”
441
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)
442
139
 
443
- def _resolve_world(self) -> object | None:
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
+ """
444
147
  # Prefer gameplay world underneath overlays:
445
148
  # scan from top to bottom and pick the first scene that has .world
446
- for entry in reversed(self.services.scenes.visible_entries()):
149
+ for entry in reversed(self.managers.scenes.visible_entries()):
447
150
  scene = entry.scene
448
151
  world = getattr(scene, "world", None)
449
152
  if world is not None:
450
153
  return world
451
154
  return None
452
-
453
-
454
- # pylint: enable=too-many-instance-attributes
@@ -0,0 +1,40 @@
1
+ """
2
+ Game configuration classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+
9
+ from mini_arcade_core.backend import Backend
10
+
11
+
12
+ @dataclass
13
+ class PostFXConfig:
14
+ """
15
+ Configuration for post-processing effects.
16
+
17
+ :ivar enabled (bool): Whether post effects are enabled by default.
18
+ :ivar active (list[str]): List of active effect IDs by default.
19
+ """
20
+
21
+ enabled: bool = True
22
+ active: list[str] = field(default_factory=list)
23
+
24
+
25
+ @dataclass
26
+ class GameConfig:
27
+ """
28
+ Configuration options for the Game.
29
+
30
+ :ivar initial_scene (str): Identifier of the initial scene to load.
31
+ :ivar fps (int): Target frames per second.
32
+ :ivar backend (Backend | None): Optional Backend instance to use for rendering and input.
33
+ :ivar postfx (PostFXConfig): Configuration for post-processing effects.
34
+ """
35
+
36
+ initial_scene: str = "main"
37
+ fps: int = 60
38
+ backend: Backend | None = None
39
+ postfx: PostFXConfig = field(default_factory=PostFXConfig)
40
+ enable_profiler: bool = False
@@ -0,0 +1,24 @@
1
+ """
2
+ Gameplay settings that can be modified during gameplay.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Literal
9
+
10
+ from mini_arcade_core.engine.render.effects.base import EffectStack
11
+
12
+ Difficulty = Literal["easy", "normal", "hard", "insane"]
13
+
14
+
15
+ @dataclass
16
+ class GamePlaySettings:
17
+ """
18
+ Game settings that can be modified during gameplay.
19
+
20
+ :ivar difficulty (Difficulty): Current game difficulty level.
21
+ """
22
+
23
+ difficulty: Difficulty = "normal"
24
+ effects_stack: EffectStack | None = None
@@ -0,0 +1,20 @@
1
+ """
2
+ Game core module defining the Game class and configuration.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RunnerConfig:
12
+ """
13
+ Configuration for the main loop runner.
14
+
15
+ :ivar fps (int): Target frames per second (0 for uncapped).
16
+ :ivar max_frames (int | None): Optional maximum number of frames to run (None for unlimited).
17
+ """
18
+
19
+ fps: int = 60
20
+ max_frames: int | None = None