mini-arcade-core 0.9.2__tar.gz → 0.9.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/PKG-INFO +1 -1
  2. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/pyproject.toml +1 -1
  3. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/__init__.py +10 -2
  4. mini_arcade_core-0.9.4/src/mini_arcade_core/autoreg.py +39 -0
  5. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/game.py +20 -5
  6. mini_arcade_core-0.9.4/src/mini_arcade_core/registry.py +112 -0
  7. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/ui/menu.py +3 -7
  8. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/LICENSE +0 -0
  9. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/README.md +0 -0
  10. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/backend.py +0 -0
  11. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/boundaries2d.py +0 -0
  12. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/collision2d.py +0 -0
  13. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/entity.py +0 -0
  14. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/geometry2d.py +0 -0
  15. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/keymaps/__init__.py +0 -0
  16. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/keymaps/sdl.py +0 -0
  17. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/keys.py +0 -0
  18. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/kinematics2d.py +0 -0
  19. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/physics2d.py +0 -0
  20. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/scene.py +0 -0
  21. {mini_arcade_core-0.9.2 → mini_arcade_core-0.9.4}/src/mini_arcade_core/ui/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 0.9.2
3
+ Version: 0.9.4
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "mini-arcade-core"
7
- version = "0.9.2"
7
+ version = "0.9.4"
8
8
  description = "Tiny scene-based game loop core for small arcade games."
9
9
  authors = [
10
10
  { name = "Santiago Rincon", email = "rincores@gmail.com" },
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  import logging
9
9
  from importlib.metadata import PackageNotFoundError, version
10
10
 
11
+ from .autoreg import register_scene
11
12
  from .backend import Backend, Event, EventType
12
13
  from .boundaries2d import (
13
14
  RectKinematic,
@@ -22,12 +23,17 @@ from .geometry2d import Bounds2D, Position2D, Size2D
22
23
  from .keys import Key, keymap
23
24
  from .kinematics2d import KinematicData
24
25
  from .physics2d import Velocity2D
26
+ from .registry import SceneRegistry
25
27
  from .scene import Scene
26
28
 
27
29
  logger = logging.getLogger(__name__)
28
30
 
29
31
 
30
- def run_game(initial_scene_cls: type[Scene], config: GameConfig | None = None):
32
+ def run_game(
33
+ initial_scene_cls: type[Scene],
34
+ config: GameConfig | None = None,
35
+ registry: SceneRegistry | None = None,
36
+ ):
31
37
  """
32
38
  Convenience helper to bootstrap and run a game with a single scene.
33
39
 
@@ -44,7 +50,7 @@ def run_game(initial_scene_cls: type[Scene], config: GameConfig | None = None):
44
50
  raise ValueError(
45
51
  "GameConfig.backend must be set to a Backend instance"
46
52
  )
47
- game = Game(cfg)
53
+ game = Game(cfg, registry=registry)
48
54
  scene = initial_scene_cls(game)
49
55
  game.run(scene)
50
56
 
@@ -72,6 +78,8 @@ __all__ = [
72
78
  "RectKinematic",
73
79
  "Key",
74
80
  "keymap",
81
+ "SceneRegistry",
82
+ "register_scene",
75
83
  ]
76
84
 
77
85
  PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
@@ -0,0 +1,39 @@
1
+ """
2
+ Autoregistration utilities for mini arcade core.
3
+ Allows automatic registration of Scene classes via decorators.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING, Dict, Type
9
+
10
+ if TYPE_CHECKING:
11
+ from .scene import Scene
12
+
13
+ _AUTO: Dict[str, Type["Scene"]] = {}
14
+
15
+
16
+ def register_scene(scene_id: str):
17
+ """Decorator to mark and register a Scene class under an id."""
18
+
19
+ def deco(cls: Type["Scene"]):
20
+ _AUTO[scene_id] = cls
21
+ setattr(cls, "__scene_id__", scene_id)
22
+ return cls
23
+
24
+ return deco
25
+
26
+
27
+ def snapshot() -> Dict[str, Type["Scene"]]:
28
+ """
29
+ Copy of current catalog (useful for tests).
30
+
31
+ :return: A copy of the current scene catalog.
32
+ :rtype: Dict[str, Type["Scene"]]
33
+ """
34
+ return dict(_AUTO)
35
+
36
+
37
+ def clear():
38
+ """Clear the current catalog (useful for tests)."""
39
+ _AUTO.clear()
@@ -9,15 +9,18 @@ from dataclasses import dataclass
9
9
  from datetime import datetime
10
10
  from pathlib import Path
11
11
  from time import perf_counter, sleep
12
- from typing import TYPE_CHECKING
12
+ from typing import TYPE_CHECKING, Union
13
13
 
14
14
  from PIL import Image # type: ignore[import]
15
15
 
16
16
  from .backend import Backend
17
+ from .registry import SceneRegistry
17
18
 
18
19
  if TYPE_CHECKING: # avoid runtime circular import
19
20
  from .scene import Scene
20
21
 
22
+ SceneOrId = Union["Scene", str]
23
+
21
24
 
22
25
  @dataclass
23
26
  class GameConfig:
@@ -49,7 +52,9 @@ class _StackEntry:
49
52
  class Game:
50
53
  """Core game object responsible for managing the main loop and active scene."""
51
54
 
52
- def __init__(self, config: GameConfig):
55
+ def __init__(
56
+ self, config: GameConfig, registry: SceneRegistry | None = None
57
+ ):
53
58
  """
54
59
  :param config: Game configuration options.
55
60
  :type config: GameConfig
@@ -65,6 +70,7 @@ class Game:
65
70
  "GameConfig.backend must be set to a Backend instance"
66
71
  )
67
72
  self.backend: Backend = config.backend
73
+ self.registry = registry or SceneRegistry(_factories={})
68
74
  self._scene_stack: list[_StackEntry] = []
69
75
 
70
76
  def current_scene(self) -> "Scene | None":
@@ -76,7 +82,7 @@ class Game:
76
82
  """
77
83
  return self._scene_stack[-1].scene if self._scene_stack else None
78
84
 
79
- def change_scene(self, scene: Scene):
85
+ def change_scene(self, scene: SceneOrId):
80
86
  """
81
87
  Swap the active scene. Concrete implementations should call
82
88
  ``on_exit``/``on_enter`` appropriately.
@@ -84,6 +90,8 @@ class Game:
84
90
  :param scene: The new scene to activate.
85
91
  :type scene: Scene
86
92
  """
93
+ scene = self._resolve_scene(scene)
94
+
87
95
  while self._scene_stack:
88
96
  entry = self._scene_stack.pop()
89
97
  entry.scene.on_exit()
@@ -91,11 +99,13 @@ class Game:
91
99
  self._scene_stack.append(_StackEntry(scene=scene, as_overlay=False))
92
100
  scene.on_enter()
93
101
 
94
- def push_scene(self, scene: "Scene", as_overlay: bool = False):
102
+ def push_scene(self, scene: SceneOrId, as_overlay: bool = False):
95
103
  """
96
104
  Push a scene on top of the current one.
97
105
  If as_overlay=True, underlying scene(s) may still be drawn but never updated.
98
106
  """
107
+ scene = self._resolve_scene(scene)
108
+
99
109
  top = self.current_scene()
100
110
  if top is not None:
101
111
  top.on_pause()
@@ -142,7 +152,7 @@ class Game:
142
152
  """Request that the main loop stops."""
143
153
  self._running = False
144
154
 
145
- def run(self, initial_scene: Scene):
155
+ def run(self, initial_scene: SceneOrId):
146
156
  """
147
157
  Run the main loop starting with the given scene.
148
158
 
@@ -241,3 +251,8 @@ class Game:
241
251
  self._convert_bmp_to_image(bmp_path, str(out_path))
242
252
  return str(out_path)
243
253
  return None
254
+
255
+ def _resolve_scene(self, scene: SceneOrId) -> "Scene":
256
+ if isinstance(scene, str):
257
+ return self.registry.create(scene, self)
258
+ return scene
@@ -0,0 +1,112 @@
1
+ """
2
+ Scene registry for mini arcade core.
3
+ Allows registering and creating scenes by string IDs.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import importlib
9
+ import pkgutil
10
+ from dataclasses import dataclass
11
+ from typing import TYPE_CHECKING, Dict, Protocol
12
+
13
+ from .autoreg import snapshot
14
+
15
+ if TYPE_CHECKING:
16
+ from mini_arcade_core.game import Game
17
+ from mini_arcade_core.scene import Scene
18
+
19
+
20
+ class SceneFactory(Protocol):
21
+ """
22
+ Protocol for scene factory callables.
23
+ """
24
+
25
+ def __call__(self, game: "Game") -> "Scene": ...
26
+
27
+
28
+ @dataclass
29
+ class SceneRegistry:
30
+ """
31
+ Registry for scene factories, allowing registration and creation of scenes by string IDs.
32
+ """
33
+
34
+ _factories: Dict[str, SceneFactory]
35
+
36
+ def register(self, scene_id: str, factory: SceneFactory):
37
+ """
38
+ Register a scene factory under a given scene ID.
39
+
40
+ :param scene_id: The string ID for the scene.
41
+ :type scene_id: str
42
+
43
+ :param factory: A callable that creates a Scene instance.
44
+ :type factory: SceneFactory
45
+ """
46
+ self._factories[scene_id] = factory
47
+
48
+ def register_cls(self, scene_id: str, scene_cls: type["Scene"]):
49
+ """
50
+ Register a Scene class under a given scene ID.
51
+
52
+ :param scene_id: The string ID for the scene.
53
+ :type scene_id: str
54
+
55
+ :param scene_cls: The Scene class to register.
56
+ :type scene_cls: type["Scene"]
57
+ """
58
+
59
+ def return_factory(game: "Game") -> "Scene":
60
+ return scene_cls(game)
61
+
62
+ self.register(scene_id, return_factory)
63
+
64
+ def create(self, scene_id: str, game: "Game") -> "Scene":
65
+ """
66
+ Create a scene instance using the registered factory for the given scene ID.
67
+
68
+ :param scene_id: The string ID of the scene to create.
69
+ :type scene_id: str
70
+
71
+ :param game: The Game instance to pass to the scene factory.
72
+ :type game: Game
73
+
74
+ :return: A new Scene instance.
75
+ :rtype: Scene
76
+
77
+ :raises KeyError: If no factory is registered for the given scene ID.
78
+ """
79
+ try:
80
+ return self._factories[scene_id](game)
81
+ except KeyError as e:
82
+ raise KeyError(f"Unknown scene_id={scene_id!r}") from e
83
+
84
+ def load_catalog(self, catalog: Dict[str, type["Scene"]]):
85
+ """
86
+ Load a catalog of Scene classes into the registry.
87
+
88
+ :param catalog: A dictionary mapping scene IDs to Scene classes.
89
+ :type catalog: Dict[str, type["Scene"]]
90
+ """
91
+ for scene_id, cls in catalog.items():
92
+ self.register_cls(scene_id, cls)
93
+
94
+ def discover(self, package: str) -> "SceneRegistry":
95
+ """
96
+ Import all modules in a package so @scene decorators run.
97
+
98
+ :param package: The package name to scan for scene modules.
99
+ :type package: str
100
+
101
+ :return: The SceneRegistry instance (for chaining).
102
+ :rtype: SceneRegistry
103
+ """
104
+ pkg = importlib.import_module(package)
105
+ if not hasattr(pkg, "__path__"):
106
+ return self # not a package
107
+
108
+ for mod in pkgutil.walk_packages(pkg.__path__, pkg.__name__ + "."):
109
+ importlib.import_module(mod.name)
110
+
111
+ self.load_catalog(snapshot())
112
+ return self
@@ -253,9 +253,7 @@ class Menu:
253
253
  color=self.style.hint_color,
254
254
  )
255
255
 
256
- def _draw_text_items(
257
- self, surface: Backend, x_center: int, cursor_y: int
258
- ) -> None:
256
+ def _draw_text_items(self, surface: Backend, x_center: int, cursor_y: int):
259
257
  for i, item in enumerate(self.items):
260
258
  color = (
261
259
  self.style.selected
@@ -272,9 +270,7 @@ class Menu:
272
270
 
273
271
  # Justification: Local variables for layout calculations
274
272
  # pylint: disable=too-many-locals
275
- def _draw_buttons(
276
- self, surface: Backend, x_center: int, cursor_y: int
277
- ) -> None:
273
+ def _draw_buttons(self, surface: Backend, x_center: int, cursor_y: int):
278
274
  # Determine button width: fixed or auto-fit
279
275
  if self.style.button_width is not None:
280
276
  bw = self.style.button_width
@@ -366,6 +362,6 @@ class Menu:
366
362
  text: str,
367
363
  *,
368
364
  color: Color,
369
- ) -> None:
365
+ ):
370
366
  w, _ = surface.measure_text(text)
371
367
  surface.draw_text(x_center - (w // 2), y, text, color=color)