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,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from time import perf_counter, sleep
|
|
9
|
+
from typing import Dict, Literal
|
|
10
|
+
|
|
11
|
+
from mini_arcade_core.backend import Backend
|
|
12
|
+
from mini_arcade_core.engine.commands import (
|
|
13
|
+
CommandContext,
|
|
14
|
+
CommandQueue,
|
|
15
|
+
QuitCommand,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# from mini_arcade_core.sim.runner import SimRunner, SimRunnerConfig
|
|
19
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
20
|
+
from mini_arcade_core.engine.render.pipeline import RenderPipeline
|
|
21
|
+
from mini_arcade_core.managers.cheats import CheatManager
|
|
22
|
+
from mini_arcade_core.runtime.audio.audio_adapter import NullAudioAdapter
|
|
23
|
+
from mini_arcade_core.runtime.capture.capture_adapter import CaptureAdapter
|
|
24
|
+
from mini_arcade_core.runtime.file.file_adapter import LocalFilesAdapter
|
|
25
|
+
from mini_arcade_core.runtime.input.input_adapter import InputAdapter
|
|
26
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
27
|
+
from mini_arcade_core.runtime.scene.scene_adapter import SceneAdapter
|
|
28
|
+
from mini_arcade_core.runtime.services import RuntimeServices
|
|
29
|
+
from mini_arcade_core.runtime.window.window_adapter import WindowAdapter
|
|
30
|
+
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
31
|
+
from mini_arcade_core.utils import logger
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class WindowConfig:
|
|
36
|
+
"""
|
|
37
|
+
Configuration for a game window (not implemented).
|
|
38
|
+
|
|
39
|
+
:ivar width (int): Width of the window in pixels.
|
|
40
|
+
:ivar height (int): Height of the window in pixels.
|
|
41
|
+
:ivar background_color (tuple[int, int, int]): RGB background color.
|
|
42
|
+
:ivar title (str): Title of the window.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
width: int
|
|
46
|
+
height: int
|
|
47
|
+
background_color: tuple[int, int, int]
|
|
48
|
+
title: str
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class GameConfig:
|
|
53
|
+
"""
|
|
54
|
+
Configuration options for the Game.
|
|
55
|
+
|
|
56
|
+
:ivar window (WindowConfig | None): Optional window configuration.
|
|
57
|
+
:ivar fps (int): Target frames per second.
|
|
58
|
+
:ivar backend (Backend | None): Optional Backend instance to use for rendering and input.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
window: WindowConfig | None = None
|
|
62
|
+
fps: int = 60
|
|
63
|
+
backend: Backend | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Difficulty = Literal["easy", "normal", "hard", "insane"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class GameSettings:
|
|
71
|
+
"""
|
|
72
|
+
Game settings that can be modified during gameplay.
|
|
73
|
+
|
|
74
|
+
:ivar difficulty (Difficulty): Current game difficulty level.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
difficulty: Difficulty = "normal"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _neutral_input(frame_index: int, dt: float) -> InputFrame:
|
|
81
|
+
"""Create a neutral InputFrame with no input events."""
|
|
82
|
+
return InputFrame(frame_index=frame_index, dt=dt)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class FrameTimer:
|
|
87
|
+
"""
|
|
88
|
+
Simple frame timer for marking and reporting time intervals.
|
|
89
|
+
|
|
90
|
+
:ivar enabled (bool): Whether timing is enabled.
|
|
91
|
+
:ivar marks (Dict[str, float]): Recorded time marks.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
enabled: bool = True
|
|
95
|
+
marks: Dict[str, float] = field(default_factory=dict)
|
|
96
|
+
|
|
97
|
+
def mark(self, name: str):
|
|
98
|
+
"""
|
|
99
|
+
Record a time mark with the given name.
|
|
100
|
+
|
|
101
|
+
:param name: Name of the mark.
|
|
102
|
+
:type name: str
|
|
103
|
+
"""
|
|
104
|
+
if not self.enabled:
|
|
105
|
+
return
|
|
106
|
+
self.marks[name] = perf_counter()
|
|
107
|
+
|
|
108
|
+
def diff_ms(self, start: str, end: str) -> float:
|
|
109
|
+
"""
|
|
110
|
+
Get the time difference in milliseconds between two marks.
|
|
111
|
+
|
|
112
|
+
:param start: Name of the start mark.
|
|
113
|
+
:type start: str
|
|
114
|
+
|
|
115
|
+
:param end: Name of the end mark.
|
|
116
|
+
:type end: str
|
|
117
|
+
|
|
118
|
+
:return: Time difference in milliseconds.
|
|
119
|
+
:rtype: float
|
|
120
|
+
"""
|
|
121
|
+
return (self.marks[end] - self.marks[start]) * 1000.0
|
|
122
|
+
|
|
123
|
+
def report_ms(self) -> Dict[str, float]:
|
|
124
|
+
"""
|
|
125
|
+
Returns diffs between consecutive marks in insertion order.
|
|
126
|
+
|
|
127
|
+
:return: Dictionary mapping "start->end" to time difference in milliseconds.
|
|
128
|
+
:rtype: Dict[str, float]
|
|
129
|
+
"""
|
|
130
|
+
if not self.enabled:
|
|
131
|
+
return {}
|
|
132
|
+
|
|
133
|
+
keys = list(self.marks.keys())
|
|
134
|
+
out: Dict[str, float] = {}
|
|
135
|
+
for a, b in zip(keys, keys[1:]):
|
|
136
|
+
out[f"{a}->{b}"] = self.diff_ms(a, b)
|
|
137
|
+
return out
|
|
138
|
+
|
|
139
|
+
def clear(self):
|
|
140
|
+
"""Clear all recorded marks."""
|
|
141
|
+
if not self.enabled:
|
|
142
|
+
return
|
|
143
|
+
self.marks.clear()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# TODO: Fix too-many-instance-attributes warning
|
|
147
|
+
# Justification: Core game class with many dependencies.
|
|
148
|
+
# pylint: disable=too-many-instance-attributes
|
|
149
|
+
class Game:
|
|
150
|
+
"""Core game object responsible for managing the main loop and active scene."""
|
|
151
|
+
|
|
152
|
+
def __init__(
|
|
153
|
+
self, config: GameConfig, registry: SceneRegistry | None = None
|
|
154
|
+
):
|
|
155
|
+
"""
|
|
156
|
+
:param config: Game configuration options.
|
|
157
|
+
:type config: GameConfig
|
|
158
|
+
|
|
159
|
+
:param registry: Optional SceneRegistry for scene management.
|
|
160
|
+
:type registry: SceneRegistry | None
|
|
161
|
+
|
|
162
|
+
:raises ValueError: If the provided config does not have a valid Backend.
|
|
163
|
+
"""
|
|
164
|
+
self.config = config
|
|
165
|
+
self._running: bool = False
|
|
166
|
+
|
|
167
|
+
if config.backend is None:
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"GameConfig.backend must be set to a Backend instance"
|
|
170
|
+
)
|
|
171
|
+
if config.window is None:
|
|
172
|
+
raise ValueError("GameConfig.window must be set")
|
|
173
|
+
|
|
174
|
+
self.backend: Backend = config.backend
|
|
175
|
+
self.registry = registry or SceneRegistry(_factories={})
|
|
176
|
+
self.settings = GameSettings()
|
|
177
|
+
self.services = RuntimeServices(
|
|
178
|
+
window=WindowAdapter(
|
|
179
|
+
self.backend,
|
|
180
|
+
),
|
|
181
|
+
scenes=SceneAdapter(self.registry, self),
|
|
182
|
+
audio=NullAudioAdapter(),
|
|
183
|
+
files=LocalFilesAdapter(),
|
|
184
|
+
capture=CaptureAdapter(self.backend),
|
|
185
|
+
input=InputAdapter(),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self.command_queue = CommandQueue()
|
|
189
|
+
self.cheat_manager = CheatManager()
|
|
190
|
+
|
|
191
|
+
def quit(self):
|
|
192
|
+
"""Request that the main loop stops."""
|
|
193
|
+
self._running = False
|
|
194
|
+
|
|
195
|
+
# TODO: Fix too-many-statements and too-many-locals warnings
|
|
196
|
+
# Justification: Main game loop with multiple responsibilities.
|
|
197
|
+
# pylint: disable=too-many-statements,too-many-locals
|
|
198
|
+
def run(self, initial_scene_id: str):
|
|
199
|
+
"""
|
|
200
|
+
Run the main loop starting with the given scene.
|
|
201
|
+
|
|
202
|
+
This is intentionally left abstract so you can plug pygame, pyglet,
|
|
203
|
+
or another backend.
|
|
204
|
+
|
|
205
|
+
:param initial_scene_id: The scene id to start the game with (must be registered).
|
|
206
|
+
:type initial_scene_id: str
|
|
207
|
+
"""
|
|
208
|
+
backend = self.backend
|
|
209
|
+
|
|
210
|
+
self._initialize_window()
|
|
211
|
+
|
|
212
|
+
self.services.scenes.change(initial_scene_id)
|
|
213
|
+
|
|
214
|
+
pipeline = RenderPipeline()
|
|
215
|
+
|
|
216
|
+
self._running = True
|
|
217
|
+
target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
|
|
218
|
+
last_time = perf_counter()
|
|
219
|
+
frame_index = 0
|
|
220
|
+
|
|
221
|
+
# cache packets so blocked-update scenes still render their last frame
|
|
222
|
+
packet_cache: dict[int, RenderPacket] = {}
|
|
223
|
+
|
|
224
|
+
timer = FrameTimer(enabled=True)
|
|
225
|
+
report_every = 60 # print once per second at 60fps
|
|
226
|
+
|
|
227
|
+
# TODO: Integrate SimRunner for simulation stepping
|
|
228
|
+
# TODO: Fix assignment-from-no-return warning in self.services.input.build
|
|
229
|
+
# & self.services.scenes.input_entry
|
|
230
|
+
# Justification: These methods are expected to return values.
|
|
231
|
+
# pylint: disable=assignment-from-no-return
|
|
232
|
+
|
|
233
|
+
while self._running:
|
|
234
|
+
timer.clear()
|
|
235
|
+
timer.mark("frame_start")
|
|
236
|
+
|
|
237
|
+
now = perf_counter()
|
|
238
|
+
dt = now - last_time
|
|
239
|
+
last_time = now
|
|
240
|
+
|
|
241
|
+
events = list(backend.poll_events())
|
|
242
|
+
timer.mark("events_polled")
|
|
243
|
+
|
|
244
|
+
input_frame = self.services.input.build(events, frame_index, dt)
|
|
245
|
+
timer.mark("input_built")
|
|
246
|
+
|
|
247
|
+
# Window/OS quit (close button)
|
|
248
|
+
if input_frame.quit:
|
|
249
|
+
self.command_queue.push(QuitCommand())
|
|
250
|
+
|
|
251
|
+
# who gets input?
|
|
252
|
+
input_entry = self.services.scenes.input_entry()
|
|
253
|
+
if input_entry is None:
|
|
254
|
+
break
|
|
255
|
+
|
|
256
|
+
# tick policy-aware scenes
|
|
257
|
+
timer.mark("tick_start")
|
|
258
|
+
for entry in self.services.scenes.update_entries():
|
|
259
|
+
scene = entry.scene
|
|
260
|
+
effective_input = (
|
|
261
|
+
input_frame
|
|
262
|
+
if entry is input_entry
|
|
263
|
+
else _neutral_input(frame_index, dt)
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
packet = scene.tick(effective_input, dt)
|
|
267
|
+
packet_cache[id(scene)] = packet
|
|
268
|
+
timer.mark("tick_end")
|
|
269
|
+
|
|
270
|
+
timer.mark("command_ctx_start")
|
|
271
|
+
command_context = CommandContext(
|
|
272
|
+
services=self.services,
|
|
273
|
+
commands=self.command_queue,
|
|
274
|
+
settings=self.settings,
|
|
275
|
+
world=self._resolve_world(),
|
|
276
|
+
)
|
|
277
|
+
timer.mark("command_ctx_end")
|
|
278
|
+
|
|
279
|
+
timer.mark("cheats_start")
|
|
280
|
+
self.cheat_manager.process_frame(
|
|
281
|
+
input_frame,
|
|
282
|
+
context=command_context,
|
|
283
|
+
queue=self.command_queue,
|
|
284
|
+
)
|
|
285
|
+
timer.mark("cheats_end")
|
|
286
|
+
|
|
287
|
+
# Execute commands at the end of the frame (consistent write path)
|
|
288
|
+
timer.mark("cmd_exec_start")
|
|
289
|
+
for cmd in self.command_queue.drain():
|
|
290
|
+
cmd.execute(command_context)
|
|
291
|
+
timer.mark("cmd_exec_end")
|
|
292
|
+
|
|
293
|
+
timer.mark("render_start")
|
|
294
|
+
backend.begin_frame()
|
|
295
|
+
timer.mark("begin_frame_done")
|
|
296
|
+
|
|
297
|
+
for entry in self.services.scenes.visible_entries():
|
|
298
|
+
scene = entry.scene
|
|
299
|
+
packet = packet_cache.get(id(scene))
|
|
300
|
+
if packet is None:
|
|
301
|
+
# bootstrap (first frame visible but not updated)
|
|
302
|
+
packet = scene.tick(_neutral_input(frame_index, 0.0), 0.0)
|
|
303
|
+
packet_cache[id(scene)] = packet
|
|
304
|
+
|
|
305
|
+
pipeline.draw_packet(backend, packet)
|
|
306
|
+
|
|
307
|
+
timer.mark("draw_done")
|
|
308
|
+
backend.end_frame()
|
|
309
|
+
timer.mark("end_frame_done")
|
|
310
|
+
|
|
311
|
+
timer.mark("sleep_start")
|
|
312
|
+
if target_dt > 0 and dt < target_dt:
|
|
313
|
+
sleep(target_dt - dt)
|
|
314
|
+
timer.mark("sleep_end")
|
|
315
|
+
|
|
316
|
+
# --- report ---
|
|
317
|
+
if frame_index % report_every == 0 and frame_index > 0:
|
|
318
|
+
ms = timer.report_ms()
|
|
319
|
+
total = (perf_counter() - timer.marks["frame_start"]) * 1000.0
|
|
320
|
+
logger.debug(
|
|
321
|
+
f"[Frame {frame_index}] total={total:.2f}ms | {ms}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
frame_index += 1
|
|
325
|
+
|
|
326
|
+
# pylint: enable=assignment-from-no-return
|
|
327
|
+
|
|
328
|
+
# exit remaining scenes
|
|
329
|
+
self.services.scenes.clean()
|
|
330
|
+
|
|
331
|
+
# pylint: enable=too-many-statements,too-many-locals
|
|
332
|
+
|
|
333
|
+
def _initialize_window(self):
|
|
334
|
+
"""Initialize the game window based on the configuration."""
|
|
335
|
+
self.services.window.set_window_size(
|
|
336
|
+
self.config.window.width, self.config.window.height
|
|
337
|
+
)
|
|
338
|
+
self.services.window.set_title(self.config.window.title)
|
|
339
|
+
|
|
340
|
+
br, bg, bb = self.config.window.background_color
|
|
341
|
+
self.services.window.set_clear_color(br, bg, bb)
|
|
342
|
+
|
|
343
|
+
def _resolve_world(self) -> object | None:
|
|
344
|
+
# Prefer gameplay world underneath overlays:
|
|
345
|
+
# scan from top to bottom and pick the first scene that has .world
|
|
346
|
+
for entry in reversed(self.services.scenes.visible_entries()):
|
|
347
|
+
scene = entry.scene
|
|
348
|
+
world = getattr(scene, "world", None)
|
|
349
|
+
if world is not None:
|
|
350
|
+
return world
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# pylint: enable=too-many-instance-attributes
|
|
File without changes
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render packet module.
|
|
3
|
+
Defines the RenderPacket class and related types."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Callable, Iterable, Protocol, runtime_checkable
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.backend import Backend
|
|
11
|
+
|
|
12
|
+
DrawOp = Callable[[Backend], None]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class RenderPacket:
|
|
17
|
+
"""
|
|
18
|
+
Minimal render packet for v1.
|
|
19
|
+
|
|
20
|
+
It is intentionally backend-agnostic: each op is a callable that knows
|
|
21
|
+
how to draw itself using the Backend instance.
|
|
22
|
+
|
|
23
|
+
Later you can replace DrawOp with typed primitives + passes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
ops: tuple[DrawOp, ...] = ()
|
|
27
|
+
meta: dict[str, object] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def from_ops(ops: Iterable[DrawOp], **meta: object) -> "RenderPacket":
|
|
31
|
+
"""
|
|
32
|
+
Create a RenderPacket from an iterable of DrawOps and optional meta.
|
|
33
|
+
|
|
34
|
+
:param ops: Iterable of DrawOp callables.
|
|
35
|
+
:type ops: Iterable[DrawOp]
|
|
36
|
+
|
|
37
|
+
:return: RenderPacket instance.
|
|
38
|
+
:rtype: RenderPacket
|
|
39
|
+
"""
|
|
40
|
+
return RenderPacket(ops=tuple(ops), meta=dict(meta))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# TODO: Implement later
|
|
44
|
+
@runtime_checkable
|
|
45
|
+
class Renderable(Protocol):
|
|
46
|
+
"""
|
|
47
|
+
Optional convenience: any object that can produce a RenderPacket.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def render(self) -> RenderPacket:
|
|
51
|
+
"""
|
|
52
|
+
Produce a RenderPacket for this object.
|
|
53
|
+
|
|
54
|
+
:return: RenderPacket instance.
|
|
55
|
+
:rtype: RenderPacket
|
|
56
|
+
"""
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Render pipeline module.
|
|
3
|
+
Defines the RenderPipeline class for rendering RenderPackets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from mini_arcade_core.backend import Backend
|
|
11
|
+
from mini_arcade_core.engine.render.packet import RenderPacket
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class RenderPipeline:
|
|
16
|
+
"""
|
|
17
|
+
Minimal pipeline for v1.
|
|
18
|
+
|
|
19
|
+
Later you can expand this into passes:
|
|
20
|
+
- build draw list
|
|
21
|
+
- cull
|
|
22
|
+
- sort
|
|
23
|
+
- backend draw pass
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def draw_packet(self, backend: Backend, packet: RenderPacket):
|
|
27
|
+
"""
|
|
28
|
+
Draw the given RenderPacket using the provided Backend.
|
|
29
|
+
|
|
30
|
+
:param backend: Backend to use for drawing.
|
|
31
|
+
:type backend: Backend
|
|
32
|
+
|
|
33
|
+
:param packet: RenderPacket to draw.
|
|
34
|
+
:type packet: RenderPacket
|
|
35
|
+
"""
|
|
36
|
+
if not packet:
|
|
37
|
+
return
|
|
38
|
+
for op in packet.ops:
|
|
39
|
+
op(backend)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Managers module for Mini Arcade Core.
|
|
3
|
-
Provides various manager classes for handling game entities and resources.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
from mini_arcade_core.managers.entity_manager import EntityManager
|
|
9
|
-
from mini_arcade_core.managers.overlay_manager import OverlayManager
|
|
10
|
-
|
|
11
|
-
__all__ = [
|
|
12
|
-
"EntityManager",
|
|
13
|
-
"OverlayManager",
|
|
14
|
-
]
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cheats module for Mini Arcade Core.
|
|
3
|
+
Provides cheat codes and related functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import deque
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Callable, Deque, Dict, Optional, Sequence, TypeVar
|
|
11
|
+
|
|
12
|
+
from mini_arcade_core.engine.commands import Command, CommandQueue
|
|
13
|
+
from mini_arcade_core.runtime.input_frame import InputFrame
|
|
14
|
+
|
|
15
|
+
# Justification: We want to keep the type variable name simple here.
|
|
16
|
+
# pylint: disable=invalid-name
|
|
17
|
+
TContext = TypeVar("TContext")
|
|
18
|
+
# pylint: enable=invalid-name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class CheatCode:
|
|
23
|
+
"""
|
|
24
|
+
Represents a registered cheat code.
|
|
25
|
+
|
|
26
|
+
:ivar name (str): Unique name of the cheat code.
|
|
27
|
+
:ivar sequence (tuple[str, ...]): Sequence of key strings that trigger the cheat.
|
|
28
|
+
:ivar action (CheatAction): BaseCheatCommand to call when the cheat is activated.
|
|
29
|
+
:ivar clear_buffer_on_match (bool): Whether to clear the input buffer after a match.
|
|
30
|
+
:ivar enabled (bool): Whether the cheat code is enabled.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
sequence: tuple[str, ...]
|
|
35
|
+
command_factory: Optional[Callable[[TContext], Command]] = None
|
|
36
|
+
clear_buffer_on_match: bool = False
|
|
37
|
+
enabled: bool = True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CheatManager:
|
|
42
|
+
"""
|
|
43
|
+
Reusable cheat code matcher.
|
|
44
|
+
Keeps a rolling buffer of recent keys and triggers callbacks on sequence match.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
buffer_size: int = 16
|
|
48
|
+
enabled: bool = True
|
|
49
|
+
_buffer: Deque[str] = field(default_factory=lambda: deque(maxlen=16))
|
|
50
|
+
_codes: Dict[str, CheatCode[TContext]] = field(default_factory=dict)
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
# ensure deque maxlen matches buffer_size
|
|
54
|
+
self._buffer = deque(maxlen=self.buffer_size)
|
|
55
|
+
|
|
56
|
+
# TODO: ISolve too-many-arguments warning later
|
|
57
|
+
# Justification: The method needs multiple optional parameters for flexibility.
|
|
58
|
+
# pylint: disable=too-many-arguments
|
|
59
|
+
def register(
|
|
60
|
+
self,
|
|
61
|
+
name: str,
|
|
62
|
+
*,
|
|
63
|
+
sequence: Sequence[str],
|
|
64
|
+
command_factory: Callable[[TContext], Command],
|
|
65
|
+
clear_buffer_on_match: bool = False,
|
|
66
|
+
enabled: bool = True,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Register a new cheat code.
|
|
70
|
+
|
|
71
|
+
:param name: Unique name of the cheat code.
|
|
72
|
+
:type name: str
|
|
73
|
+
|
|
74
|
+
:param sequence: Sequence of key strings that trigger the cheat.
|
|
75
|
+
:type sequence: Sequence[str]
|
|
76
|
+
|
|
77
|
+
:param command_factory: Factory function to create the Command when the cheat is activated.
|
|
78
|
+
:type command_factory: Callable[[TContext], Command]
|
|
79
|
+
|
|
80
|
+
:param clear_buffer_on_match: Whether to clear the input buffer after a match.
|
|
81
|
+
:type clear_buffer_on_match: bool
|
|
82
|
+
|
|
83
|
+
:param enabled: Whether the cheat code is enabled.
|
|
84
|
+
:type enabled: bool
|
|
85
|
+
|
|
86
|
+
:raises ValueError: If name is empty or sequence is empty.
|
|
87
|
+
"""
|
|
88
|
+
if not name:
|
|
89
|
+
raise ValueError("Cheat name must be non-empty.")
|
|
90
|
+
if not sequence:
|
|
91
|
+
raise ValueError(f"Cheat '{name}' sequence must be non-empty.")
|
|
92
|
+
|
|
93
|
+
norm_seq = tuple(self._norm(s) for s in sequence)
|
|
94
|
+
self._codes[name] = CheatCode(
|
|
95
|
+
name=name,
|
|
96
|
+
sequence=norm_seq,
|
|
97
|
+
command_factory=command_factory,
|
|
98
|
+
clear_buffer_on_match=clear_buffer_on_match,
|
|
99
|
+
enabled=enabled,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# pylint: enable=too-many-arguments
|
|
103
|
+
|
|
104
|
+
def process_frame(
|
|
105
|
+
self,
|
|
106
|
+
input_frame: InputFrame,
|
|
107
|
+
*,
|
|
108
|
+
context: TContext,
|
|
109
|
+
queue: CommandQueue,
|
|
110
|
+
) -> list[str]:
|
|
111
|
+
"""
|
|
112
|
+
Process an InputFrame for cheat code matches.
|
|
113
|
+
|
|
114
|
+
:param input_frame: InputFrame containing current inputs.
|
|
115
|
+
:type input_frame: InputFrame
|
|
116
|
+
|
|
117
|
+
:param context: Context to pass to command factories.
|
|
118
|
+
:type context: TContext
|
|
119
|
+
|
|
120
|
+
:param queue: CommandQueue to push matched commands into.
|
|
121
|
+
:type queue: CommandQueue
|
|
122
|
+
|
|
123
|
+
:return: List of names of matched cheat codes.
|
|
124
|
+
:rtype: list[str]
|
|
125
|
+
"""
|
|
126
|
+
if not self.enabled:
|
|
127
|
+
return []
|
|
128
|
+
|
|
129
|
+
matched: list[str] = []
|
|
130
|
+
for key in input_frame.keys_pressed:
|
|
131
|
+
key_name = getattr(key, "name", str(key))
|
|
132
|
+
matched.extend(
|
|
133
|
+
self.process_key(key_name, context=context, queue=queue)
|
|
134
|
+
)
|
|
135
|
+
return matched
|
|
136
|
+
|
|
137
|
+
def process_key(
|
|
138
|
+
self, key: str, *, context: TContext, queue: CommandQueue
|
|
139
|
+
) -> list[str]:
|
|
140
|
+
"""
|
|
141
|
+
Process a single key input.
|
|
142
|
+
|
|
143
|
+
:param key: The key string to process.
|
|
144
|
+
:type key: str
|
|
145
|
+
|
|
146
|
+
:param context: Context to pass to command factories.
|
|
147
|
+
:type context: TContext
|
|
148
|
+
|
|
149
|
+
:param queue: CommandQueue to push matched commands into.
|
|
150
|
+
:type queue: CommandQueue
|
|
151
|
+
|
|
152
|
+
:return: List of names of matched cheat codes.
|
|
153
|
+
:rtype: list[str]
|
|
154
|
+
"""
|
|
155
|
+
if not self.enabled:
|
|
156
|
+
return []
|
|
157
|
+
|
|
158
|
+
k = self._norm(key)
|
|
159
|
+
if not k:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
self._buffer.append(k)
|
|
163
|
+
buf = tuple(self._buffer)
|
|
164
|
+
|
|
165
|
+
matched: list[str] = []
|
|
166
|
+
for code in self._codes.values():
|
|
167
|
+
if not code.enabled:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
seq = code.sequence
|
|
171
|
+
if len(seq) > len(buf):
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
if buf[-len(seq) :] == seq:
|
|
175
|
+
queue.push(code.command_factory(context))
|
|
176
|
+
matched.append(code.name)
|
|
177
|
+
|
|
178
|
+
if code.clear_buffer_on_match:
|
|
179
|
+
self._buffer.clear()
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
return matched
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _norm(s: str) -> str:
|
|
186
|
+
return s.strip().upper()
|