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
@@ -0,0 +1,57 @@
1
+ """
2
+ Pipeline for managing and executing systems in order.
3
+ Defines the SystemPipeline dataclass that holds and runs systems.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Generic, Iterable, List
10
+
11
+ from mini_arcade_core.scenes.systems.base_system import (
12
+ BaseSystem,
13
+ TSystemContext,
14
+ )
15
+
16
+
17
+ @dataclass
18
+ class SystemPipeline(Generic[TSystemContext]):
19
+ """
20
+ Pipeline for managing and executing systems in order.
21
+
22
+ :ivar systems (List[BaseSystem[TSystemContext]]): List of systems in the pipeline.
23
+ """
24
+
25
+ systems: List[BaseSystem[TSystemContext]] = field(default_factory=list)
26
+
27
+ def add(self, system: BaseSystem[TSystemContext]):
28
+ """
29
+ Add a system to the pipeline and sort by order.
30
+
31
+ :param system: The system to add.
32
+ :type system: BaseSystem[TSystemContext]
33
+ """
34
+ self.systems.append(system)
35
+ self.systems.sort(key=lambda s: getattr(s, "order", 0))
36
+
37
+ def extend(self, systems: Iterable[BaseSystem[TSystemContext]]):
38
+ """
39
+ Extend the pipeline with multiple systems.
40
+
41
+ :param systems: An iterable of systems to add.
42
+ :type systems: Iterable[BaseSystem[TSystemContext]]
43
+ """
44
+ for s in systems:
45
+ self.add(s)
46
+
47
+ def step(self, ctx: TSystemContext):
48
+ """
49
+ Execute a step for each system in the pipeline.
50
+
51
+ :param ctx: The system context.
52
+ :type ctx: TSystemContext
53
+ """
54
+ for system in self.systems:
55
+ if hasattr(system, "enabled") and not system.enabled(ctx):
56
+ continue
57
+ system.step(ctx)
File without changes
@@ -0,0 +1,41 @@
1
+ """
2
+ Simulation scene protocol module.
3
+ Defines the SimScene protocol for simulation scenes.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from mini_arcade_core.engine.render.packet import RenderPacket
11
+ from mini_arcade_core.runtime.context import RuntimeContext
12
+ from mini_arcade_core.runtime.input_frame import InputFrame
13
+
14
+
15
+ @dataclass
16
+ class SimScene:
17
+ """
18
+ Simulation-first scene protocol.
19
+
20
+ tick() advances the simulation and returns a RenderPacket for this scene.
21
+ """
22
+
23
+ context: RuntimeContext
24
+
25
+ def on_enter(self):
26
+ """Called when the scene is entered."""
27
+
28
+ def on_exit(self):
29
+ """Called when the scene is exited."""
30
+
31
+ def tick(self, input_frame: InputFrame, dt: float) -> RenderPacket:
32
+ """
33
+ Advance the simulation by dt seconds, processing input_frame.
34
+
35
+ :param input_frame: InputFrame with input events for this frame.
36
+ :param dt: Time delta in seconds since the last tick.
37
+
38
+ :return: RenderPacket for this frame.
39
+ :rtype: RenderPacket
40
+ """
41
+ raise NotImplementedError()
@@ -0,0 +1,222 @@
1
+ """
2
+ Simulation runner module.
3
+ Defines the SimRunner class for running simulation scenes.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from time import perf_counter, sleep
10
+ from typing import Dict, Optional
11
+
12
+ from mini_arcade_core.backend import Backend
13
+ from mini_arcade_core.engine.render.packet import RenderPacket
14
+ from mini_arcade_core.engine.render.pipeline import RenderPipeline
15
+ from mini_arcade_core.runtime.input_frame import InputFrame
16
+ from mini_arcade_core.runtime.scene.scene_port import SceneEntry
17
+ from mini_arcade_core.runtime.services import RuntimeServices
18
+
19
+
20
+ def _neutral_input(frame_index: int, dt: float) -> InputFrame:
21
+ # InputFrame is frozen; create a clean snapshot for non-input scenes.
22
+ return InputFrame(frame_index=frame_index, dt=dt)
23
+
24
+
25
+ def _has_tick(scene: object) -> bool:
26
+ # Avoid isinstance(..., Protocol). Structural check.
27
+ return callable(getattr(scene, "tick", None))
28
+
29
+
30
+ def _has_draw(scene: object) -> bool:
31
+ return callable(getattr(scene, "draw", None))
32
+
33
+
34
+ def _has_update(scene: object) -> bool:
35
+ return callable(getattr(scene, "update", None))
36
+
37
+
38
+ def _has_handle_event(scene: object) -> bool:
39
+ return callable(getattr(scene, "handle_event", None))
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class SimRunnerConfig:
44
+ """
45
+ Config for sim runner.
46
+
47
+ - record: if True, capture a frame each tick using deterministic naming.
48
+ - run_id: required when record=True.
49
+ - max_frames: optional safety stop (useful for offline sims/tests).
50
+ """
51
+
52
+ fps: int = 60
53
+ record: bool = False
54
+ run_id: str = "run"
55
+ max_frames: Optional[int] = None
56
+ # If True, still forward raw events to the input scene's handle_event (legacy UI / text input).
57
+ forward_events_to_input_scene: bool = True
58
+
59
+
60
+ class SimRunner:
61
+ """
62
+ Simulation-first runner.
63
+
64
+ Uses:
65
+ - services.scenes.update_entries() for ticking (policy-aware)
66
+ - services.scenes.visible_entries() for rendering (opaque-aware)
67
+ - services.scenes.input_entry() for input focus
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ backend: Backend,
73
+ services: RuntimeServices,
74
+ *,
75
+ render_pipeline: Optional[RenderPipeline] = None,
76
+ ):
77
+ if services.scenes is None:
78
+ raise ValueError("RuntimeServices.scenes must be set")
79
+ if services.input is None:
80
+ raise ValueError("RuntimeServices.input must be set")
81
+ if services.capture is None:
82
+ # recording is optional, but capture port should exist in v1
83
+ raise ValueError("RuntimeServices.capture must be set")
84
+
85
+ self.backend = backend
86
+ self.services = services
87
+ self.pipeline = render_pipeline or RenderPipeline()
88
+
89
+ # cache: scene object id -> last RenderPacket
90
+ self._packets: Dict[int, RenderPacket] = {}
91
+
92
+ self._running: bool = False
93
+
94
+ def stop(self):
95
+ """
96
+ Stop the simulation loop.
97
+ """
98
+ self._running = False
99
+
100
+ # TODO: Solve too-many-statements, too-many-branches and too-many-locals
101
+ # warning later
102
+ # Justification: The run method orchestrates multiple complex steps in the
103
+ # simulation loop.
104
+ # pylint: disable=too-many-statements,too-many-branches,too-many-locals
105
+ def run(
106
+ self, initial_scene_id: str, *, cfg: Optional[SimRunnerConfig] = None
107
+ ):
108
+ """
109
+ Run the simulation loop starting from the initial scene.
110
+
111
+ :param initial_scene_id: ID of the initial scene to load.
112
+ :type initial_scene_id: str
113
+
114
+ :param cfg: Optional SimRunnerConfig instance.
115
+ :type cfg: Optional[SimRunnerConfig]
116
+ """
117
+ cfg = cfg or SimRunnerConfig()
118
+
119
+ scenes = self.services.scenes
120
+ assert scenes is not None
121
+
122
+ # start at initial scene
123
+ scenes.change(initial_scene_id)
124
+
125
+ self._running = True
126
+ target_dt = 1.0 / cfg.fps if cfg.fps > 0 else 0.0
127
+
128
+ last_time = perf_counter()
129
+ frame_index = 0
130
+
131
+ while self._running:
132
+ if cfg.max_frames is not None and frame_index >= cfg.max_frames:
133
+ break
134
+
135
+ now = perf_counter()
136
+ dt = now - last_time
137
+ last_time = now
138
+
139
+ # 1) poll events -> build InputFrame
140
+ events = list(self.backend.poll_events())
141
+ input_frame = self.services.input.build(events, frame_index, dt)
142
+
143
+ # 2) OS quit request is a hard stop
144
+ if input_frame.quit:
145
+ # use ScenePort.quit so Game.quit can be centralized there
146
+ scenes.quit()
147
+ break
148
+
149
+ # 3) input focus scene (top of visible stack)
150
+ input_entry: Optional[SceneEntry] = scenes.input_entry()
151
+ if input_entry is None:
152
+ break
153
+
154
+ # Optional legacy: forward raw events to focused scene
155
+ if cfg.forward_events_to_input_scene and _has_handle_event(
156
+ input_entry.scene
157
+ ):
158
+ for ev in events:
159
+ input_entry.scene.handle_event(ev)
160
+
161
+ # 4) tick/update policy-aware scenes
162
+ for entry in scenes.update_entries():
163
+ scene_obj = entry.scene
164
+ scene_key = id(scene_obj)
165
+
166
+ # Only the input-focused scene receives the actual input_frame
167
+ effective_input = (
168
+ input_frame
169
+ if entry is input_entry
170
+ else _neutral_input(frame_index, dt)
171
+ )
172
+
173
+ if _has_tick(scene_obj):
174
+ packet = scene_obj.tick(effective_input, dt) # SimScene
175
+ if not isinstance(packet, RenderPacket):
176
+ raise TypeError(
177
+ f"{entry.scene_id}.tick() must "
178
+ f"return RenderPacket, got {type(packet)!r}"
179
+ )
180
+ self._packets[scene_key] = packet
181
+ elif _has_update(scene_obj):
182
+ # legacy scene; keep packet cache if any
183
+ scene_obj.update(dt)
184
+
185
+ # 5) render visible stack (policy-aware)
186
+ self.backend.begin_frame()
187
+
188
+ for entry in scenes.visible_entries():
189
+ scene_obj = entry.scene
190
+ scene_key = id(scene_obj)
191
+
192
+ if _has_tick(scene_obj):
193
+ packet = self._packets.get(scene_key)
194
+ # If first frame and no packet exists yet, do a dt=0 tick to bootstrap
195
+ if packet is None:
196
+ packet = scene_obj.tick(
197
+ _neutral_input(frame_index, 0.0), 0.0
198
+ )
199
+ self._packets[scene_key] = packet
200
+ self.pipeline.draw_packet(self.backend, packet)
201
+
202
+ elif _has_draw(scene_obj):
203
+ # legacy scene draw path
204
+ scene_obj.draw(self.backend)
205
+
206
+ self.backend.end_frame()
207
+
208
+ # 6) deterministic capture (optional)
209
+ if cfg.record:
210
+ # label could be "frame" or something semantic later
211
+ self.services.capture.screenshot_sim(
212
+ cfg.run_id, frame_index, label="frame"
213
+ )
214
+
215
+ # 7) frame pacing
216
+ if target_dt > 0 and dt < target_dt:
217
+ sleep(target_dt - dt)
218
+
219
+ frame_index += 1
220
+
221
+ # cleanup scenes
222
+ scenes.clean()
File without changes
File without changes
@@ -4,45 +4,35 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ from abc import ABC, abstractmethod
7
8
  from dataclasses import dataclass
8
9
 
9
10
  from .geometry2d import Position2D, Size2D
10
11
 
11
12
 
12
- def _rects_intersect(
13
- pos_a: Position2D,
14
- size_a: Size2D,
15
- pos_b: Position2D,
16
- size_b: Size2D,
17
- ) -> bool:
13
+ class Collider2D(ABC):
14
+ """
15
+ Abstract base class for 2D colliders.
18
16
  """
19
- Low-level AABB check. Internal helper.
20
-
21
- :param pos_a: Top-left position of rectangle A.
22
- :type pos_a: Position2D
23
17
 
24
- :param size_a: Size of rectangle A.
25
- :type size_a: Size2D
18
+ position: Position2D
19
+ size: Size2D
26
20
 
27
- :param pos_b: Top-left position of rectangle B.
28
- :type pos_b: Position2D
21
+ @abstractmethod
22
+ def intersects(self, other: Collider2D) -> bool:
23
+ """
24
+ Check if this collider intersects with another collider.
29
25
 
30
- :param size_b: Size of rectangle B.
31
- :type size_b: Size2D
26
+ :param other: The other collider to check against.
27
+ :type other: Collider2D
32
28
 
33
- :return: True if the rectangles intersect.
34
- :rtype: bool
35
- """
36
- return not (
37
- pos_a.x + size_a.width < pos_b.x
38
- or pos_a.x > pos_b.x + size_b.width
39
- or pos_a.y + size_a.height < pos_b.y
40
- or pos_a.y > pos_b.y + size_b.height
41
- )
29
+ :return: True if the colliders intersect.
30
+ :rtype: bool
31
+ """
42
32
 
43
33
 
44
34
  @dataclass
45
- class RectCollider:
35
+ class RectCollider(Collider2D):
46
36
  """
47
37
  OOP collision helper that wraps a Position2D + Size2D pair.
48
38
 
@@ -66,6 +56,13 @@ class RectCollider:
66
56
  :return: True if the rectangles intersect.
67
57
  :rtype: bool
68
58
  """
69
- return _rects_intersect(
70
- self.position, self.size, other.position, other.size
59
+ pos_a = self.position
60
+ size_a = self.size
61
+ pos_b = other.position
62
+ size_b = other.size
63
+ return not (
64
+ pos_a.x + size_a.width < pos_b.x
65
+ or pos_a.x > pos_b.x + size_b.width
66
+ or pos_a.y + size_a.height < pos_b.y
67
+ or pos_a.y > pos_b.y + size_b.height
71
68
  )
@@ -19,6 +19,15 @@ class Position2D:
19
19
  x: float
20
20
  y: float
21
21
 
22
+ def to_tuple(self) -> tuple[float, float]:
23
+ """
24
+ Convert Position2D to a tuple.
25
+
26
+ :return: Tuple of (x, y).
27
+ :rtype: tuple[float, float]
28
+ """
29
+ return (self.x, self.y)
30
+
22
31
 
23
32
  @dataclass
24
33
  class Size2D:
@@ -32,6 +41,15 @@ class Size2D:
32
41
  width: int
33
42
  height: int
34
43
 
44
+ def to_tuple(self) -> tuple[int, int]:
45
+ """
46
+ Convert Size2D to a tuple.
47
+
48
+ :return: Tuple of (width, height).
49
+ :rtype: tuple[int, int]
50
+ """
51
+ return (self.width, self.height)
52
+
35
53
 
36
54
  @dataclass
37
55
  class Bounds2D:
@@ -7,8 +7,7 @@ from __future__ import annotations
7
7
  from dataclasses import dataclass
8
8
  from typing import Optional
9
9
 
10
- from mini_arcade_core.backend import Color
11
-
10
+ from .collision2d import Collider2D
12
11
  from .geometry2d import Position2D, Size2D
13
12
  from .physics2d import Velocity2D
14
13
 
@@ -27,7 +26,8 @@ class KinematicData:
27
26
  position: Position2D
28
27
  size: Size2D
29
28
  velocity: Velocity2D
30
- color: Optional[Color] = None # future use
29
+ time_scale: float = 1.0
30
+ collider: Optional[Collider2D] = None
31
31
 
32
32
  # Justification: Convenience factory with many params.
33
33
  # pylint: disable=too-many-arguments,too-many-positional-arguments
@@ -40,7 +40,7 @@ class KinematicData:
40
40
  height: int,
41
41
  vx: float = 0.0,
42
42
  vy: float = 0.0,
43
- color: Optional[Color] = None,
43
+ time_scale: float = 1.0,
44
44
  ) -> "KinematicData":
45
45
  """
46
46
  Convenience factory for rectangular kinematic data.
@@ -63,9 +63,6 @@ class KinematicData:
63
63
  :param vy: Velocity in the Y direction.
64
64
  :type vy: float
65
65
 
66
- :param color: Optional color for rendering.
67
- :type color: Optional[Color]
68
-
69
66
  :return: KinematicData instance with the specified parameters.
70
67
  :rtype: KinematicData
71
68
  """
@@ -73,7 +70,7 @@ class KinematicData:
73
70
  position=Position2D(float(x), float(y)),
74
71
  size=Size2D(int(width), int(height)),
75
72
  velocity=Velocity2D(float(vx), float(vy)),
76
- color=color,
73
+ time_scale=time_scale,
77
74
  )
78
75
 
79
76
  # pylint: enable=too-many-arguments,too-many-positional-arguments
@@ -19,6 +19,15 @@ class Velocity2D:
19
19
  vx: float = 0.0
20
20
  vy: float = 0.0
21
21
 
22
+ def to_tuple(self) -> tuple[float, float]:
23
+ """
24
+ Convert Velocity2D to a tuple.
25
+
26
+ :return: Tuple of (vx, vy).
27
+ :rtype: tuple[float, float]
28
+ """
29
+ return (self.vx, self.vy)
30
+
22
31
  def advance(self, x: float, y: float, dt: float) -> tuple[float, float]:
23
32
  """Return new (x, y) after dt seconds."""
24
33
  return x + self.vx * dt, y + self.vy * dt
@@ -1,14 +0,0 @@
1
- """
2
- UI utilities and components for Mini Arcade Core.
3
- Includes buttons, labels, and layout management.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from .menu import Menu, MenuItem, MenuStyle
9
-
10
- __all__ = [
11
- "Menu",
12
- "MenuItem",
13
- "MenuStyle",
14
- ]