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.
- mini_arcade_core/__init__.py +44 -80
- mini_arcade_core/backend/__init__.py +0 -5
- mini_arcade_core/backend/backend.py +9 -0
- mini_arcade_core/backend/events.py +1 -1
- mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
- mini_arcade_core/bus.py +57 -0
- mini_arcade_core/engine/__init__.py +0 -0
- mini_arcade_core/engine/commands.py +169 -0
- mini_arcade_core/engine/game.py +354 -0
- mini_arcade_core/engine/render/__init__.py +0 -0
- mini_arcade_core/engine/render/packet.py +56 -0
- mini_arcade_core/engine/render/pipeline.py +39 -0
- mini_arcade_core/managers/__init__.py +0 -14
- mini_arcade_core/managers/cheats.py +186 -0
- mini_arcade_core/managers/inputs.py +286 -0
- mini_arcade_core/runtime/__init__.py +0 -0
- mini_arcade_core/runtime/audio/__init__.py +0 -0
- mini_arcade_core/runtime/audio/audio_adapter.py +13 -0
- mini_arcade_core/runtime/audio/audio_port.py +17 -0
- mini_arcade_core/runtime/capture/__init__.py +0 -0
- mini_arcade_core/runtime/capture/capture_adapter.py +143 -0
- mini_arcade_core/runtime/capture/capture_port.py +32 -0
- mini_arcade_core/runtime/context.py +53 -0
- mini_arcade_core/runtime/file/__init__.py +0 -0
- mini_arcade_core/runtime/file/file_adapter.py +20 -0
- mini_arcade_core/runtime/file/file_port.py +31 -0
- mini_arcade_core/runtime/input/__init__.py +0 -0
- mini_arcade_core/runtime/input/input_adapter.py +49 -0
- mini_arcade_core/runtime/input/input_port.py +31 -0
- mini_arcade_core/runtime/input_frame.py +71 -0
- mini_arcade_core/runtime/scene/__init__.py +0 -0
- mini_arcade_core/runtime/scene/scene_adapter.py +97 -0
- mini_arcade_core/runtime/scene/scene_port.py +149 -0
- mini_arcade_core/runtime/services.py +35 -0
- mini_arcade_core/runtime/window/__init__.py +0 -0
- mini_arcade_core/runtime/window/window_adapter.py +26 -0
- mini_arcade_core/runtime/window/window_port.py +47 -0
- mini_arcade_core/scenes/__init__.py +0 -12
- mini_arcade_core/scenes/autoreg.py +1 -1
- mini_arcade_core/scenes/registry.py +21 -19
- mini_arcade_core/scenes/sim_scene.py +41 -0
- mini_arcade_core/scenes/systems/__init__.py +0 -0
- mini_arcade_core/scenes/systems/base_system.py +40 -0
- mini_arcade_core/scenes/systems/system_pipeline.py +57 -0
- mini_arcade_core/sim/__init__.py +0 -0
- mini_arcade_core/sim/protocols.py +41 -0
- mini_arcade_core/sim/runner.py +222 -0
- mini_arcade_core/spaces/__init__.py +0 -0
- mini_arcade_core/spaces/d2/__init__.py +0 -0
- mini_arcade_core/{two_d → spaces/d2}/collision2d.py +25 -28
- mini_arcade_core/{two_d → spaces/d2}/geometry2d.py +18 -0
- mini_arcade_core/{two_d → spaces/d2}/kinematics2d.py +5 -8
- mini_arcade_core/{two_d → spaces/d2}/physics2d.py +9 -0
- mini_arcade_core/ui/__init__.py +0 -14
- mini_arcade_core/ui/menu.py +415 -56
- mini_arcade_core/utils/__init__.py +10 -0
- mini_arcade_core/utils/deprecated_decorator.py +45 -0
- mini_arcade_core/utils/logging.py +174 -0
- {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/METADATA +1 -1
- mini_arcade_core-1.0.0.dist-info/RECORD +65 -0
- {mini_arcade_core-0.9.9.dist-info → mini_arcade_core-1.0.0.dist-info}/WHEEL +1 -1
- mini_arcade_core/cheats.py +0 -235
- mini_arcade_core/entity.py +0 -71
- mini_arcade_core/game.py +0 -287
- mini_arcade_core/keymaps/__init__.py +0 -15
- mini_arcade_core/managers/base.py +0 -91
- mini_arcade_core/managers/entity_manager.py +0 -38
- mini_arcade_core/managers/overlay_manager.py +0 -33
- mini_arcade_core/scenes/scene.py +0 -93
- mini_arcade_core/two_d/__init__.py +0 -30
- mini_arcade_core-0.9.9.dist-info/RECORD +0 -31
- /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
- /mini_arcade_core/{two_d → spaces/d2}/boundaries2d.py +0 -0
- {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
|
+
"""
|