mini-arcade-core 0.9.9__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. mini_arcade_core/__init__.py +44 -80
  2. mini_arcade_core/backend/__init__.py +0 -5
  3. mini_arcade_core/backend/backend.py +9 -0
  4. mini_arcade_core/backend/events.py +1 -1
  5. mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
  6. mini_arcade_core/bus.py +57 -0
  7. mini_arcade_core/engine/__init__.py +0 -0
  8. mini_arcade_core/engine/commands.py +169 -0
  9. mini_arcade_core/engine/game.py +354 -0
  10. mini_arcade_core/engine/render/__init__.py +0 -0
  11. mini_arcade_core/engine/render/packet.py +56 -0
  12. mini_arcade_core/engine/render/pipeline.py +39 -0
  13. mini_arcade_core/managers/__init__.py +0 -14
  14. mini_arcade_core/managers/cheats.py +186 -0
  15. mini_arcade_core/managers/inputs.py +286 -0
  16. mini_arcade_core/runtime/__init__.py +0 -0
  17. mini_arcade_core/runtime/audio/__init__.py +0 -0
  18. mini_arcade_core/runtime/audio/audio_adapter.py +13 -0
  19. mini_arcade_core/runtime/audio/audio_port.py +17 -0
  20. mini_arcade_core/runtime/capture/__init__.py +0 -0
  21. mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
  22. mini_arcade_core/runtime/capture/capture_port.py +32 -0
  23. mini_arcade_core/runtime/context.py +53 -0
  24. mini_arcade_core/runtime/file/__init__.py +0 -0
  25. mini_arcade_core/runtime/file/file_adapter.py +20 -0
  26. mini_arcade_core/runtime/file/file_port.py +31 -0
  27. mini_arcade_core/runtime/input/__init__.py +0 -0
  28. mini_arcade_core/runtime/input/input_adapter.py +49 -0
  29. mini_arcade_core/runtime/input/input_port.py +31 -0
  30. mini_arcade_core/runtime/input_frame.py +71 -0
  31. mini_arcade_core/runtime/scene/__init__.py +0 -0
  32. mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
  33. mini_arcade_core/runtime/scene/scene_port.py +149 -0
  34. mini_arcade_core/runtime/services.py +35 -0
  35. mini_arcade_core/runtime/window/__init__.py +0 -0
  36. mini_arcade_core/runtime/window/window_adapter.py +26 -0
  37. mini_arcade_core/runtime/window/window_port.py +47 -0
  38. mini_arcade_core/scenes/__init__.py +0 -12
  39. mini_arcade_core/scenes/autoreg.py +1 -1
  40. mini_arcade_core/scenes/registry.py +21 -19
  41. mini_arcade_core/scenes/sim_scene.py +41 -0
  42. mini_arcade_core/scenes/systems/__init__.py +0 -0
  43. mini_arcade_core/scenes/systems/base_system.py +40 -0
  44. mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
  45. mini_arcade_core/sim/__init__.py +0 -0
  46. mini_arcade_core/sim/protocols.py +41 -0
  47. mini_arcade_core/sim/runner.py +222 -0
  48. mini_arcade_core/spaces/__init__.py +0 -0
  49. mini_arcade_core/spaces/d2/__init__.py +0 -0
  50. mini_arcade_core/{two_d → spaces/d2}/collision2d.py +25 -28
  51. mini_arcade_core/{two_d → spaces/d2}/geometry2d.py +18 -0
  52. mini_arcade_core/{two_d → spaces/d2}/kinematics2d.py +5 -8
  53. mini_arcade_core/{two_d → spaces/d2}/physics2d.py +9 -0
  54. mini_arcade_core/ui/__init__.py +0 -14
  55. mini_arcade_core/ui/menu.py +415 -56
  56. mini_arcade_core/utils/__init__.py +10 -0
  57. mini_arcade_core/utils/deprecated_decorator.py +45 -0
  58. mini_arcade_core/utils/logging.py +174 -0
  59. {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/METADATA +1 -1
  60. mini_arcade_core-1.0.0.dist-info/RECORD +65 -0
  61. {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/WHEEL +1 -1
  62. mini_arcade_core/cheats.py +0 -235
  63. mini_arcade_core/entity.py +0 -71
  64. mini_arcade_core/game.py +0 -287
  65. mini_arcade_core/keymaps/__init__.py +0 -15
  66. mini_arcade_core/managers/base.py +0 -91
  67. mini_arcade_core/managers/entity_manager.py +0 -38
  68. mini_arcade_core/managers/overlay_manager.py +0 -33
  69. mini_arcade_core/scenes/scene.py +0 -93
  70. mini_arcade_core/two_d/__init__.py +0 -30
  71. mini_arcade_core-0.9.9.dist-info/RECORD +0 -31
  72. /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
  73. /mini_arcade_core/{two_d → spaces/d2}/boundaries2d.py +0 -0
  74. {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,39 +5,19 @@ Provides access to core classes and a convenience function to run a game.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import logging
8
+ import traceback
9
9
  from importlib.metadata import PackageNotFoundError, version
10
10
  from typing import Callable, Type, Union
11
11
 
12
- from mini_arcade_core.backend import Backend, Event, EventType
13
- from mini_arcade_core.cheats import CheatCode, CheatManager
14
- from mini_arcade_core.entity import Entity, KinematicEntity, SpriteEntity
15
- from mini_arcade_core.game import Game, GameConfig
16
- from mini_arcade_core.keymaps.keys import Key, keymap
17
- from mini_arcade_core.scenes import (
18
- Scene,
19
- SceneRegistry,
20
- SceneServices,
21
- register_scene,
22
- )
23
- from mini_arcade_core.two_d import (
24
- Bounds2D,
25
- KinematicData,
26
- Position2D,
27
- RectCollider,
28
- RectKinematic,
29
- RectSprite,
30
- Size2D,
31
- Velocity2D,
32
- VerticalBounce,
33
- VerticalWrap,
34
- )
35
-
36
- SceneFactoryLike = Union[Type[Scene], Callable[[Game], Scene]]
37
-
38
- logger = logging.getLogger(__name__)
12
+ from mini_arcade_core.engine.game import Game, GameConfig, WindowConfig
13
+ from mini_arcade_core.scenes.registry import SceneRegistry
14
+ from mini_arcade_core.scenes.sim_scene import SimScene
15
+ from mini_arcade_core.utils import logger
39
16
 
17
+ SceneFactoryLike = Union[Type[SimScene], Callable[[Game], SimScene]]
40
18
 
19
+
20
+ # TODO: Improve exception handling and logging in run_game
41
21
  def run_game(
42
22
  scene: SceneFactoryLike | None = None,
43
23
  config: GameConfig | None = None,
@@ -52,7 +32,7 @@ def run_game(
52
32
  - run_game(config=cfg, initial_scene="main", registry=...) # registry-based
53
33
  - run_game(cfg) # config-only
54
34
 
55
- :param initial_scene: The Scene ID to start the game with.
35
+ :param initial_scene: The SimScene ID to start the game with.
56
36
  :type initial_scene: str
57
37
 
58
38
  :param config: Optional GameConfig to customize game settings.
@@ -63,59 +43,36 @@ def run_game(
63
43
 
64
44
  :raises ValueError: If the provided config does not have a valid Backend.
65
45
  """
66
- # Handle run_game(cfg) where the first arg is actually a GameConfig
67
- if isinstance(scene, GameConfig) and config is None:
68
- config = scene
69
- scene = None
70
-
71
- cfg = config or GameConfig()
72
- if cfg.backend is None:
73
- raise ValueError(
74
- "GameConfig.backend must be set to a Backend instance"
75
- )
76
-
77
- # If user provided a Scene factory/class, ensure it's registered
78
- if scene is not None:
79
- if registry is None:
80
- registry = SceneRegistry(_factories={})
81
- registry.register(
82
- initial_scene, scene
83
- ) # Scene class is callable(game) -> Scene
84
-
85
- game = Game(cfg, registry=registry)
86
- game.run(initial_scene)
46
+ try:
47
+ # Handle run_game(cfg) where the first arg is actually a GameConfig
48
+ if isinstance(scene, GameConfig) and config is None:
49
+ config = scene
50
+ scene = None
51
+
52
+ cfg = config or GameConfig()
53
+ if cfg.backend is None:
54
+ raise ValueError(
55
+ "GameConfig.backend must be set to a Backend instance"
56
+ )
57
+
58
+ # If user provided a SimScene factory/class, ensure it's registered
59
+ if scene is not None:
60
+ if registry is None:
61
+ registry = SceneRegistry(_factories={})
62
+ registry.register(
63
+ initial_scene, scene
64
+ ) # SimScene class is callable(game) -> SimScene
65
+
66
+ game = Game(cfg, registry=registry)
67
+ game.run(initial_scene)
68
+ # Justification: We need to catch all exceptions while we improve error handling.
69
+ # pylint: disable=broad-exception-caught
70
+ except Exception as e:
71
+ logger.exception(f"Unhandled exception in game loop: {e}")
72
+ logger.debug(traceback.format_exc())
73
+ # pylint: enable=broad-exception-caught
87
74
 
88
75
 
89
- __all__ = [
90
- "Game",
91
- "GameConfig",
92
- "Scene",
93
- "Entity",
94
- "SpriteEntity",
95
- "run_game",
96
- "Backend",
97
- "Event",
98
- "EventType",
99
- "Velocity2D",
100
- "Position2D",
101
- "Size2D",
102
- "KinematicEntity",
103
- "KinematicData",
104
- "RectCollider",
105
- "VerticalBounce",
106
- "Bounds2D",
107
- "VerticalWrap",
108
- "RectSprite",
109
- "RectKinematic",
110
- "Key",
111
- "keymap",
112
- "SceneRegistry",
113
- "register_scene",
114
- "CheatManager",
115
- "CheatCode",
116
- "SceneServices",
117
- ]
118
-
119
76
  PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
120
77
 
121
78
 
@@ -142,4 +99,11 @@ def get_version() -> str:
142
99
  return "0.0.0"
143
100
 
144
101
 
102
+ __all__ = [
103
+ "Game",
104
+ "GameConfig",
105
+ "WindowConfig",
106
+ "run_game",
107
+ ]
108
+
145
109
  __version__ = get_version()
@@ -7,12 +7,7 @@ This is the only part of the code that talks to SDL/pygame directly.
7
7
  from __future__ import annotations
8
8
 
9
9
  from .backend import Backend
10
- from .events import Event, EventType
11
- from .types import Color
12
10
 
13
11
  __all__ = [
14
12
  "Backend",
15
- "Event",
16
- "EventType",
17
- "Color",
18
13
  ]
@@ -33,6 +33,15 @@ class Backend(Protocol):
33
33
  :type title: str
34
34
  """
35
35
 
36
+ def set_window_title(self, title: str):
37
+ """
38
+ Set the window title.
39
+
40
+ :param title: The new title for the window.
41
+ :type title: str
42
+ """
43
+ raise NotImplementedError
44
+
36
45
  def poll_events(self) -> Iterable[Event]:
37
46
  """
38
47
  Return all pending events since last call.
@@ -8,7 +8,7 @@ from dataclasses import dataclass
8
8
  from enum import Enum, auto
9
9
  from typing import Optional, Tuple
10
10
 
11
- from mini_arcade_core.keymaps.keys import Key
11
+ from mini_arcade_core.backend.keys import Key
12
12
 
13
13
 
14
14
  class EventType(Enum):
@@ -8,7 +8,7 @@ Maps SDL keycodes to mini arcade core Key enums.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from mini_arcade_core.keymaps.keys import Key
11
+ from mini_arcade_core.backend.keys import Key
12
12
 
13
13
  # SDL keycodes you need (minimal set)
14
14
  _SDLK_ESCAPE = 27
@@ -0,0 +1,57 @@
1
+ """
2
+ Event bus module.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Callable, ClassVar, Dict, List, Optional
8
+
9
+
10
+ class EventBus:
11
+ """
12
+ Simple event bus for managing event subscriptions and emissions.
13
+ """
14
+
15
+ _instance: ClassVar[Optional["EventBus"]] = None
16
+ _subscribers: Dict[str, List[Callable]]
17
+
18
+ def __new__(cls):
19
+ if cls._instance is None:
20
+ inst = super().__new__(cls)
21
+ inst._subscribers = {}
22
+ cls._instance = inst
23
+ return cls._instance
24
+
25
+ def on(self, event_type: str, handler: Callable):
26
+ """
27
+ Subscribe a handler to an event type.
28
+
29
+ :param event_type: The type of event to subscribe to.
30
+ :type event_type: str
31
+
32
+ :param handler: The handler function to call when the event is emitted.
33
+ :type handler: Callable
34
+ """
35
+ if event_type not in self._subscribers:
36
+ self._subscribers[event_type] = []
37
+ self._subscribers[event_type].append(handler)
38
+
39
+ def emit(self, event_type: str, **kwargs):
40
+ """
41
+ Emit an event of a given type, calling all subscribed handlers.
42
+
43
+ :param event_type: The type of event to emit.
44
+ :type event_type: str
45
+
46
+ :param kwargs: Additional keyword arguments to pass to handlers.
47
+ """
48
+ handlers = self._subscribers.get(event_type, [])
49
+ for handler in handlers:
50
+ handler(**kwargs)
51
+
52
+ def clear(self):
53
+ """Clear all subscribers from the event bus."""
54
+ self._subscribers.clear()
55
+
56
+
57
+ event_bus = EventBus()
File without changes
@@ -0,0 +1,169 @@
1
+ """
2
+ Command protocol for executing commands with a given context.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, List, Optional, Protocol, TypeVar
9
+
10
+ if TYPE_CHECKING:
11
+ from mini_arcade_core.runtime.services import RuntimeServices
12
+
13
+ # Justification: Generic type for context
14
+ # pylint: disable=invalid-name
15
+ TContext = TypeVar("TContext")
16
+ # pylint: enable=invalid-name
17
+
18
+
19
+ @dataclass
20
+ class CommandContext:
21
+ """
22
+ Context for command execution.
23
+
24
+ :ivar services (RuntimeServices): The runtime services.
25
+ :ivar commands (CommandQueue | None): Optional command queue.
26
+ :ivar settings (object | None): Optional settings object.
27
+ :ivar world (object | None): The world object (can be any type).
28
+ """
29
+
30
+ services: RuntimeServices
31
+ commands: Optional["CommandQueue"] = None
32
+ settings: Optional[object] = None
33
+ world: Optional[object] = None
34
+
35
+
36
+ class Command(Protocol):
37
+ """
38
+ A command is the only allowed "write path" from input/systems into:
39
+ - scene operations (push/pop/change/quit)
40
+ - capture
41
+ - global game lifecycle
42
+ - later: world mutations (if you pass a world reference)
43
+
44
+ For now we keep it simple: commands only need RuntimeServices.
45
+ """
46
+
47
+ def execute(
48
+ self,
49
+ context: CommandContext,
50
+ ):
51
+ """
52
+ Execute the command with the given world and runtime services.
53
+
54
+ :param services: Runtime services for command execution.
55
+ :type services: RuntimeServices
56
+
57
+ :param commands: Optional command queue for command execution.
58
+ :type commands: object | None
59
+
60
+ :param settings: Optional settings object for command execution.
61
+ :type settings: object | None
62
+
63
+ :param world: The world object (can be any type).
64
+ :type world: object | None
65
+ """
66
+
67
+
68
+ @dataclass
69
+ class CommandQueue:
70
+ """
71
+ Queue for storing and executing commands.
72
+ """
73
+
74
+ _items: List[Command] = field(default_factory=list)
75
+
76
+ def push(self, cmd: Command):
77
+ """
78
+ Push a command onto the queue.
79
+
80
+ :param cmd: Command to be added to the queue.
81
+ :type cmd: Command
82
+ """
83
+ self._items.append(cmd)
84
+
85
+ def drain(self) -> List[Command]:
86
+ """
87
+ Drain and return all commands from the queue.
88
+
89
+ :return: List of commands that were in the queue.
90
+ :rtype: list[Command]
91
+ """
92
+ items = self._items
93
+ self._items = []
94
+ return items
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class QuitCommand(Command):
99
+ """Quit the game."""
100
+
101
+ def execute(
102
+ self,
103
+ context: CommandContext,
104
+ ):
105
+ context.services.scenes.quit()
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class ScreenshotCommand(Command):
110
+ """
111
+ Take a screenshot.
112
+
113
+ :ivar label (str | None): Optional label for the screenshot file.
114
+ """
115
+
116
+ label: str | None = None
117
+
118
+ def execute(
119
+ self,
120
+ context: CommandContext,
121
+ ):
122
+ context.services.capture.screenshot(label=self.label, mode="manual")
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class PushSceneCommand(Command):
127
+ """
128
+ Push a new scene onto the scene stack.
129
+
130
+ :ivar scene_id (str): Identifier of the scene to push.
131
+ :ivar as_overlay (bool): Whether to push the scene as an overlay.
132
+ """
133
+
134
+ scene_id: str
135
+ as_overlay: bool = False
136
+
137
+ def execute(
138
+ self,
139
+ context: CommandContext,
140
+ ):
141
+ context.services.scenes.push(self.scene_id, as_overlay=self.as_overlay)
142
+
143
+
144
+ @dataclass(frozen=True)
145
+ class PopSceneCommand(Command):
146
+ """Pop the current scene from the scene stack."""
147
+
148
+ def execute(
149
+ self,
150
+ context: CommandContext,
151
+ ):
152
+ context.services.scenes.pop()
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class ChangeSceneCommand(Command):
157
+ """
158
+ Change the current scene to the specified scene.
159
+
160
+ :ivar scene_id (str): Identifier of the scene to switch to.
161
+ """
162
+
163
+ scene_id: str
164
+
165
+ def execute(
166
+ self,
167
+ context: CommandContext,
168
+ ):
169
+ context.services.scenes.change(self.scene_id)