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,286 @@
1
+ """
2
+ Input manager for handling input bindings and commands.
3
+ """
4
+
5
+ # TODO: Implement this manager into the new input system
6
+ # Justification: These module will be used later.
7
+ # pylint: disable=no-name-in-module,import-error,used-before-assignment
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from dataclasses import dataclass
13
+ from typing import TYPE_CHECKING, Callable, Dict, Optional
14
+
15
+ from mini_arcade_core.backend import Event, EventType
16
+ from mini_arcade_core.keymaps import Key
17
+
18
+ if TYPE_CHECKING:
19
+ from mini_arcade_core.engine.commands import BaseCommand, BaseSceneCommand
20
+ from mini_arcade_core.scenes.scene import Scene
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ Predicate = Callable[["Event"], bool]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class InputBinding:
30
+ """
31
+ Defines an input binding.
32
+
33
+ :ivar action (str): The action name.
34
+ :ivar command (BaseCommand): The command to execute.
35
+ :ivar predicate (Predicate): Predicate to match events.
36
+ """
37
+
38
+ action: str
39
+ command: BaseCommand
40
+ predicate: Predicate # decides whether this binding matches an event
41
+
42
+
43
+ class InputManager:
44
+ """
45
+ Manager for handling input bindings and commands.
46
+ """
47
+
48
+ def __init__(self):
49
+ # event_type -> key -> action -> command
50
+ self._bindings: Dict[EventType, Dict[Key, Dict[str, BaseCommand]]] = {}
51
+
52
+ # Justification: The method needs multiple optional parameters for flexibility.
53
+ # pylint: disable=too-many-arguments
54
+ def bind(
55
+ self,
56
+ event_type: EventType,
57
+ action: str,
58
+ command: BaseCommand,
59
+ *,
60
+ key: Optional[Key] = None,
61
+ button: Optional[int] = None,
62
+ predicate: Optional[Predicate] = None,
63
+ ):
64
+ """
65
+ Generic binding.
66
+
67
+ You can filter by:
68
+ - key: for KEYDOWN/KEYUP
69
+ - button: for MOUSEBUTTONDOWN/MOUSEBUTTONUP (if your Event exposes it)
70
+ - predicate: custom matcher (for anything)
71
+
72
+ :param event_type: The type of event to bind to.
73
+ :type event_type: EventType
74
+
75
+ :param action: The action name for the binding.
76
+ :type action: str
77
+
78
+ :param command: The command to execute when the binding is triggered.
79
+ :type command: BaseCommand
80
+
81
+ :param key: Optional key to filter KEYDOWN/KEYUP events.
82
+ :type key: Key | None
83
+
84
+ :param button: Optional button to filter MOUSEBUTTONDOWN/MOUSEBUTTONUP events.
85
+ :type button: int | None
86
+
87
+ :param predicate: Optional custom predicate to match events.
88
+ :type predicate: Predicate | None
89
+ """
90
+ logger.debug(
91
+ f"Binding {action} to {event_type} with key={key}, button={button}"
92
+ )
93
+
94
+ def default_predicate(ev: Event) -> bool:
95
+ if key is not None and getattr(ev, "key", None) != key:
96
+ return False
97
+ if button is not None and getattr(ev, "button", None) != button:
98
+ return False
99
+ return True
100
+
101
+ pred = predicate or default_predicate
102
+ self._bindings.setdefault(event_type, []).append(
103
+ InputBinding(action=action, command=command, predicate=pred)
104
+ )
105
+
106
+ # pylint: enable=too-many-arguments
107
+
108
+ def unbind(self, event_type: EventType, action: str):
109
+ """
110
+ Remove bindings by action for an event type.
111
+
112
+ :param event_type: The type of event to unbind from.
113
+ :type event_type: EventType
114
+
115
+ :param action: The action name of the binding to remove.
116
+ :type action: str
117
+ """
118
+ lst = self._bindings.get(event_type, [])
119
+ self._bindings[event_type] = [b for b in lst if b.action != action]
120
+
121
+ def clear(self):
122
+ """Clear all input bindings."""
123
+ self._bindings.clear()
124
+
125
+ def handle_event(self, event: Event, scene: Scene):
126
+ """
127
+ Handle an incoming event, executing any matching commands.
128
+
129
+ :param event: The event to handle.
130
+ :type event: Event
131
+
132
+ :param scene: The current scene context.
133
+ :type scene: Scene
134
+ """
135
+ et = event.type
136
+
137
+ for binding in self._bindings.get(et, []):
138
+ if binding.predicate(event):
139
+ to_inject = (
140
+ scene.model
141
+ if isinstance(binding.command, BaseSceneCommand)
142
+ else scene.game
143
+ )
144
+ binding.command.execute(to_inject)
145
+
146
+ # --- Convenience API ------------------------------------------------------
147
+
148
+ def on_quit(self, command: BaseCommand, action: str = "quit"):
149
+ """
150
+ Bind a command to the QUIT event.
151
+
152
+ :param command: The command to execute on quit.
153
+ :type command: BaseCommand
154
+
155
+ :param action: The action name for the binding.
156
+ :type action: str
157
+ """
158
+ self.bind(EventType.QUIT, action=action, command=command)
159
+
160
+ def on_key_down(self, key: Key, command: BaseCommand, action: str):
161
+ """
162
+ Bind a command to a key down event.
163
+
164
+ :param key: The key to bind to.
165
+ :type key: Key
166
+
167
+ :param command: The command to execute on key down.
168
+ :type command: BaseCommand
169
+
170
+ :param action: The action name for the binding.
171
+ :type action: str
172
+ """
173
+ self.bind(EventType.KEYDOWN, key=key, action=action, command=command)
174
+
175
+ def on_key_up(self, key: Key, command: BaseCommand, action: str):
176
+ """
177
+ Bind a command to a key up event.
178
+
179
+ :param key: The key to bind to.
180
+ :type key: Key
181
+
182
+ :param command: The command to execute on key up.
183
+ :type command: BaseCommand
184
+
185
+ :param action: The action name for the binding.
186
+ :type action: str
187
+ """
188
+ self.bind(EventType.KEYUP, key=key, action=action, command=command)
189
+
190
+ def on_mouse_button_down(
191
+ self, button: int, command: BaseCommand, action: str
192
+ ):
193
+ """
194
+ Bind a command to a mouse button down event.
195
+
196
+ :param button: The mouse button to bind to.
197
+ :type button: int
198
+
199
+ :param command: The command to execute on mouse button down.
200
+ :type command: BaseCommand
201
+
202
+ :param action: The action name for the binding.
203
+ :type action: str
204
+ """
205
+ self.bind(
206
+ EventType.MOUSEBUTTONDOWN,
207
+ button=button,
208
+ action=action,
209
+ command=command,
210
+ )
211
+
212
+ def on_mouse_button_up(
213
+ self, button: int, command: BaseCommand, action: str
214
+ ):
215
+ """
216
+ Bind a command to a mouse button up event.
217
+
218
+ :param button: The mouse button to bind to.
219
+ :type button: int
220
+
221
+ :param command: The command to execute on mouse button up.
222
+ :type command: BaseCommand
223
+
224
+ :param action: The action name for the binding.
225
+ :type action: str
226
+ """
227
+ self.bind(
228
+ EventType.MOUSEBUTTONUP,
229
+ button=button,
230
+ action=action,
231
+ command=command,
232
+ )
233
+
234
+ def on_mouse_motion(
235
+ self, command: BaseCommand, action: str = "mouse_motion"
236
+ ):
237
+ """
238
+ Bind a command to mouse motion events.
239
+
240
+ :param command: The command to execute on mouse motion.
241
+ :type command: BaseCommand
242
+
243
+ :param action: The action name for the binding.
244
+ :type action: str
245
+ """
246
+ self.bind(EventType.MOUSEMOTION, action=action, command=command)
247
+
248
+ def on_mouse_wheel(
249
+ self, command: BaseCommand, action: str = "mouse_wheel"
250
+ ):
251
+ """
252
+ Bind a command to mouse wheel events.
253
+
254
+ :param command: The command to execute on mouse wheel.
255
+ :type command: BaseCommand
256
+
257
+ :param action: The action name for the binding.
258
+ :type action: str
259
+ """
260
+ self.bind(EventType.MOUSEWHEEL, action=action, command=command)
261
+
262
+ def on_window_resized(
263
+ self, command: BaseCommand, action: str = "window_resized"
264
+ ):
265
+ """
266
+ Bind a command to window resized events.
267
+
268
+ :param command: The command to execute on window resize.
269
+ :type command: BaseCommand
270
+
271
+ :param action: The action name for the binding.
272
+ :type action: str
273
+ """
274
+ self.bind(EventType.WINDOWRESIZED, action=action, command=command)
275
+
276
+ def on_text_input(self, command: BaseCommand, action: str = "text_input"):
277
+ """
278
+ Bind a command to text input events.
279
+
280
+ :param command: The command to execute on text input.
281
+ :type command: BaseCommand
282
+
283
+ :param action: The action name for the binding.
284
+ :type action: str
285
+ """
286
+ self.bind(EventType.TEXTINPUT, action=action, command=command)
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ """
2
+ Module providing runtime adapters for window and scene management.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from mini_arcade_core.runtime.audio.audio_port import AudioPort
8
+
9
+
10
+ class NullAudioAdapter(AudioPort):
11
+ """A no-op audio adapter."""
12
+
13
+ def play(self, sound_id: str): ...
@@ -0,0 +1,17 @@
1
+ """
2
+ Service interfaces for runtime components.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+
8
+ class AudioPort:
9
+ """Interface for audio playback operations."""
10
+
11
+ def play(self, sound_id: str):
12
+ """
13
+ Play the specified sound.
14
+
15
+ :param sound_id: Identifier of the sound to play.
16
+ :type sound_id: str
17
+ """
File without changes
@@ -0,0 +1,143 @@
1
+ """
2
+ Module providing runtime adapters for window and scene management.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ from PIL import Image
13
+
14
+ from mini_arcade_core.backend import Backend
15
+ from mini_arcade_core.runtime.capture.capture_port import CapturePort
16
+ from mini_arcade_core.utils import logger
17
+
18
+
19
+ @dataclass
20
+ class CapturePathBuilder:
21
+ """
22
+ Helper to build file paths for captured screenshots.
23
+
24
+ :ivar directory (str): Directory to save screenshots in.
25
+ :ivar prefix (str): Prefix for screenshot filenames.
26
+ :ivar ext (str): File extension/format for screenshots.
27
+ """
28
+
29
+ directory: str = "screenshots"
30
+ prefix: str = ""
31
+ ext: str = "png" # final output format
32
+
33
+ def build(self, label: str) -> Path:
34
+ """
35
+ Build a file path for a screenshot with the given label.
36
+
37
+ :param label: Label to include in the filename.
38
+ :type label: str
39
+
40
+ :return: Full path for the screenshot file.
41
+ :rtype: Path
42
+ """
43
+ stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
44
+ safe_label = "".join(
45
+ c if c.isalnum() or c in ("-", "_") else "_" for c in label
46
+ )
47
+ name = f"{self.prefix}{stamp}_{safe_label}.{self.ext}"
48
+ return Path(self.directory) / name
49
+
50
+ def build_sim(self, run_id: str, frame_index: int, label: str) -> Path:
51
+ """
52
+ Build a file path for a simulation frame screenshot.
53
+
54
+ :param run_id: Unique identifier for the simulation run.
55
+ :type run_id: str
56
+
57
+ :param frame_index: Index of the frame in the simulation.
58
+ :type frame_index: int
59
+
60
+ :param label: Label to include in the filename.
61
+ :type label: str
62
+
63
+ :return: Full path for the screenshot file.
64
+ :rtype: Path
65
+ """
66
+ safe_label = "".join(
67
+ c if c.isalnum() or c in ("-", "_") else "_" for c in label
68
+ )
69
+ # deterministic: run_id + frame index
70
+ name = (
71
+ f"{self.prefix}{run_id}_f{frame_index:08d}_{safe_label}.{self.ext}"
72
+ )
73
+ return Path(self.directory) / run_id / name
74
+
75
+
76
+ class CaptureAdapter(CapturePort):
77
+ """Adapter for capturing frames."""
78
+
79
+ def __init__(
80
+ self,
81
+ backend: Backend,
82
+ path_builder: Optional[CapturePathBuilder] = None,
83
+ ):
84
+ self.backend = backend
85
+ self.path_builder = path_builder or CapturePathBuilder()
86
+
87
+ def _bmp_to_image(self, bmp_path: str, out_path: str):
88
+ img = Image.open(bmp_path)
89
+ img.save(out_path)
90
+
91
+ def screenshot(self, label: str | None = None) -> str:
92
+ label = label or "shot"
93
+ out_path = self.path_builder.build(label)
94
+ out_path.parent.mkdir(parents=True, exist_ok=True)
95
+
96
+ # If backend only saves BMP, capture to a temp bmp next to output
97
+ bmp_path = out_path.with_suffix(".bmp")
98
+
99
+ self.backend.capture_frame(str(bmp_path))
100
+ if not bmp_path.exists():
101
+ raise RuntimeError("Backend capture_frame did not create BMP file")
102
+
103
+ self._bmp_to_image(str(bmp_path), str(out_path))
104
+ try:
105
+ bmp_path.unlink(missing_ok=True)
106
+ # Justification: Various exceptions can occur on file deletion
107
+ # pylint: disable=broad-exception-caught
108
+ except Exception:
109
+ logger.warning(f"Failed to delete temporary BMP file: {bmp_path}")
110
+ # pylint: enable=broad-exception-caught
111
+
112
+ return str(out_path)
113
+
114
+ def screenshot_bytes(self) -> bytes:
115
+ data = self.backend.capture_frame(path=None)
116
+ if data is None:
117
+ raise RuntimeError("Backend returned None for screenshot_bytes()")
118
+ return data
119
+
120
+ def screenshot_sim(
121
+ self, run_id: str, frame_index: int, label: str = "frame"
122
+ ) -> str:
123
+ """Screenshot for simulation frames."""
124
+ out_path = self.path_builder.build_sim(run_id, frame_index, label)
125
+ out_path.parent.mkdir(parents=True, exist_ok=True)
126
+
127
+ bmp_path = out_path.with_suffix(".bmp")
128
+ self.backend.capture_frame(str(bmp_path))
129
+
130
+ if not bmp_path.exists():
131
+ raise RuntimeError("Backend capture_frame did not create BMP file")
132
+
133
+ self._bmp_to_image(str(bmp_path), str(out_path))
134
+
135
+ try:
136
+ bmp_path.unlink(missing_ok=True)
137
+ # Justification: Various exceptions can occur on file deletion
138
+ # pylint: disable=broad-exception-caught
139
+ except Exception:
140
+ logger.warning(f"Failed to delete temporary BMP file: {bmp_path}")
141
+ # pylint: enable=broad-exception-caught
142
+
143
+ return str(out_path)
@@ -0,0 +1,32 @@
1
+ """
2
+ Service interfaces for runtime components.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from mini_arcade_core.backend import Backend
8
+
9
+
10
+ class CapturePort:
11
+ """Interface for frame capture operations."""
12
+
13
+ backend: Backend
14
+
15
+ def screenshot(self, label: str | None = None) -> str:
16
+ """
17
+ Capture the current frame.
18
+
19
+ :param label: Optional label for the screenshot file.
20
+ :type label: str | None
21
+
22
+ :return: Screenshot file path.
23
+ :rtype: str
24
+ """
25
+
26
+ def screenshot_bytes(self) -> bytes | None:
27
+ """
28
+ Capture the current frame and return it as bytes.
29
+
30
+ :return: Screenshot data as bytes.
31
+ :rtype: bytes | None
32
+ """
@@ -0,0 +1,53 @@
1
+ """
2
+ Runtime context module.
3
+ Defines the RuntimeContext dataclass for game runtime context.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ 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
15
+ from mini_arcade_core.runtime.services import RuntimeServices
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class RuntimeContext:
20
+ """
21
+ Context for the game runtime.
22
+
23
+ :ivar services (RuntimeServices): Runtime services.
24
+ :ivar config (GameConfig): Game configuration.
25
+ :ivar settings (GameSettings): Game settings.
26
+ :ivar command_queue (CommandQueue | None): Optional command queue.
27
+ :ivar cheats (CheatManager | None): Optional cheat manager.
28
+ """
29
+
30
+ services: RuntimeServices
31
+ config: GameConfig
32
+ settings: GameSettings
33
+ command_queue: CommandQueue | None = None
34
+ cheats: CheatManager | None = None
35
+
36
+ @staticmethod
37
+ def from_game(game_entity: Game) -> "RuntimeContext":
38
+ """
39
+ Create a RuntimeContext from a Game entity.
40
+
41
+ :param game_entity: Game entity to extract context from.
42
+ :type game_entity: Game
43
+
44
+ :return: RuntimeContext instance.
45
+ :rtype: RuntimeContext
46
+ """
47
+ return RuntimeContext(
48
+ services=game_entity.services,
49
+ config=game_entity.config,
50
+ settings=game_entity.settings,
51
+ command_queue=game_entity.command_queue,
52
+ cheats=game_entity.cheat_manager,
53
+ )
File without changes
@@ -0,0 +1,20 @@
1
+ """
2
+ Module providing runtime adapters for window and scene management.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from mini_arcade_core.runtime.file.file_port import FilePort
8
+
9
+
10
+ class LocalFilesAdapter(FilePort):
11
+ """Adapter for local file operations."""
12
+
13
+ def write_text(self, path: str, text: str):
14
+ with open(path, "w", encoding="utf-8") as f:
15
+ f.write(text)
16
+
17
+ def write_bytes(self, path: str, data: bytes):
18
+ with open(path, "wb") as f:
19
+ f.write(data)
20
+ f.write(data)
@@ -0,0 +1,31 @@
1
+ """
2
+ Service interfaces for runtime components.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+
8
+ class FilePort:
9
+ """Interface for file operations."""
10
+
11
+ def write_bytes(self, path: str, data: bytes):
12
+ """
13
+ Write bytes to a file.
14
+
15
+ :param path: Path to the file.
16
+ :type path: str
17
+
18
+ :param data: Data to write.
19
+ :type data: bytes
20
+ """
21
+
22
+ def write_text(self, path: str, text: str):
23
+ """
24
+ Write text to a file.
25
+
26
+ :param path: Path to the file.
27
+ :type path: str
28
+
29
+ :param text: Text to write.
30
+ :type text: str
31
+ """
File without changes
@@ -0,0 +1,49 @@
1
+ """
2
+ Module providing runtime adapters for window and scene management.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass, field
8
+
9
+ from mini_arcade_core.backend.events import Event, EventType
10
+ from mini_arcade_core.backend.keys import Key
11
+ from mini_arcade_core.runtime.input.input_port import InputPort
12
+ from mini_arcade_core.runtime.input_frame import InputFrame
13
+
14
+
15
+ @dataclass
16
+ class InputAdapter(InputPort):
17
+ """Adapter for processing input events."""
18
+
19
+ _down: set[Key] = field(default_factory=set)
20
+
21
+ def build(
22
+ self, events: list[Event], frame_index: int, dt: float
23
+ ) -> InputFrame:
24
+ pressed: set[Key] = set()
25
+ released: set[Key] = set()
26
+ quit_req = False
27
+
28
+ for ev in events:
29
+ if ev.type == EventType.QUIT:
30
+ quit_req = True
31
+
32
+ elif ev.type == EventType.KEYDOWN and ev.key is not None:
33
+ if ev.key not in self._down:
34
+ pressed.add(ev.key)
35
+ self._down.add(ev.key)
36
+
37
+ elif ev.type == EventType.KEYUP and ev.key is not None:
38
+ if ev.key in self._down:
39
+ self._down.remove(ev.key)
40
+ released.add(ev.key)
41
+
42
+ return InputFrame(
43
+ frame_index=frame_index,
44
+ dt=dt,
45
+ keys_down=frozenset(self._down),
46
+ keys_pressed=frozenset(pressed),
47
+ keys_released=frozenset(released),
48
+ quit=quit_req,
49
+ )
@@ -0,0 +1,31 @@
1
+ """
2
+ Service interfaces for runtime components.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from mini_arcade_core.backend.events import Event
8
+ from mini_arcade_core.runtime.input_frame import InputFrame
9
+
10
+
11
+ class InputPort:
12
+ """Interface for input handling operations."""
13
+
14
+ def build(
15
+ self, events: list[Event], frame_index: int, dt: float
16
+ ) -> InputFrame:
17
+ """
18
+ Build an InputFrame from the given events.
19
+
20
+ :param events: List of input events.
21
+ :type events: list[Event]
22
+
23
+ :param frame_index: Current frame index.
24
+ :type frame_index: int
25
+
26
+ :param dt: Delta time since last frame.
27
+ :type dt: float
28
+
29
+ :return: Constructed InputFrame.
30
+ :rtype: InputFrame
31
+ """