mini-arcade-core 0.10.0__py3-none-any.whl → 1.0.1__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 +51 -63
- mini_arcade_core/backend/__init__.py +2 -6
- mini_arcade_core/backend/backend.py +148 -8
- mini_arcade_core/backend/events.py +1 -1
- mini_arcade_core/{keymaps/sdl.py → backend/sdl_map.py} +1 -1
- mini_arcade_core/engine/__init__.py +0 -0
- mini_arcade_core/engine/commands.py +169 -0
- mini_arcade_core/engine/game.py +369 -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 +63 -0
- mini_arcade_core/engine/render/viewport.py +203 -0
- mini_arcade_core/managers/__init__.py +0 -22
- mini_arcade_core/managers/cheats.py +71 -240
- mini_arcade_core/managers/inputs.py +5 -3
- 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 +20 -0
- mini_arcade_core/runtime/audio/audio_port.py +36 -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 +51 -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 +90 -0
- mini_arcade_core/runtime/window/window_port.py +109 -0
- mini_arcade_core/scenes/__init__.py +0 -22
- 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 -12
- mini_arcade_core/spaces/d2/__init__.py +0 -30
- mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
- mini_arcade_core/spaces/d2/collision2d.py +25 -28
- mini_arcade_core/spaces/d2/geometry2d.py +18 -0
- mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
- mini_arcade_core/spaces/d2/physics2d.py +9 -0
- mini_arcade_core/ui/__init__.py +0 -26
- mini_arcade_core/ui/menu.py +271 -85
- mini_arcade_core/utils/__init__.py +10 -0
- mini_arcade_core/utils/deprecated_decorator.py +45 -0
- mini_arcade_core/utils/logging.py +168 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/METADATA +1 -1
- mini_arcade_core-1.0.1.dist-info/RECORD +66 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/WHEEL +1 -1
- mini_arcade_core/commands.py +0 -84
- mini_arcade_core/entity.py +0 -72
- mini_arcade_core/game.py +0 -287
- mini_arcade_core/keymaps/__init__.py +0 -15
- mini_arcade_core/managers/base.py +0 -132
- mini_arcade_core/managers/entities.py +0 -38
- mini_arcade_core/managers/overlays.py +0 -53
- mini_arcade_core/managers/system.py +0 -26
- mini_arcade_core/scenes/model.py +0 -34
- mini_arcade_core/scenes/runtime.py +0 -29
- mini_arcade_core/scenes/scene.py +0 -109
- mini_arcade_core/scenes/system.py +0 -69
- mini_arcade_core/ui/overlays.py +0 -41
- mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
- /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
- {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging utilities for Mini Arcade Core.
|
|
3
|
+
Provides a console logger with colored output and class/function context.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _classname_from_locals(locals_: dict) -> Optional[str]:
|
|
15
|
+
"""Retrieve the class name from locals dict, if available."""
|
|
16
|
+
self_obj = locals_.get("self")
|
|
17
|
+
if self_obj is not None:
|
|
18
|
+
return type(self_obj).__name__
|
|
19
|
+
cls_obj = locals_.get("cls")
|
|
20
|
+
if isinstance(cls_obj, type):
|
|
21
|
+
return cls_obj.__name__
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EnsureClassName(logging.Filter):
|
|
26
|
+
"""
|
|
27
|
+
Populate record.classname by finding the *emitting* frame:
|
|
28
|
+
match by (pathname, funcName) and read self/cls from its locals.
|
|
29
|
+
Falls back to "-" when not in a class context.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
33
|
+
# record_factory ensures classname exists, but allow explicit override
|
|
34
|
+
if getattr(record, "classname", None) not in (None, "-"):
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
target_path = record.pathname
|
|
38
|
+
target_func = record.funcName
|
|
39
|
+
|
|
40
|
+
# Justification: Seems pretty obvious here.
|
|
41
|
+
# pylint: disable=protected-access
|
|
42
|
+
f = sys._getframe()
|
|
43
|
+
# pylint: enable=protected-access
|
|
44
|
+
|
|
45
|
+
for _ in range(200):
|
|
46
|
+
if f is None:
|
|
47
|
+
break
|
|
48
|
+
code = f.f_code
|
|
49
|
+
if code.co_filename == target_path and code.co_name == target_func:
|
|
50
|
+
record.classname = _classname_from_locals(f.f_locals) or "-"
|
|
51
|
+
return True
|
|
52
|
+
f = f.f_back
|
|
53
|
+
|
|
54
|
+
record.classname = "-"
|
|
55
|
+
return True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ConsoleColorFormatter(logging.Formatter):
|
|
59
|
+
"""
|
|
60
|
+
Console formatter with ANSI colors by log level.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
COLORS = {
|
|
64
|
+
logging.DEBUG: "\033[96m", # Cyan
|
|
65
|
+
logging.INFO: "\033[92m", # Green
|
|
66
|
+
logging.WARNING: "\033[93m", # Yellow
|
|
67
|
+
logging.ERROR: "\033[91m", # Red
|
|
68
|
+
logging.CRITICAL: "\033[95m", # Magenta
|
|
69
|
+
"RESET": "\033[0m",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
73
|
+
color = self.COLORS.get(record.levelno, self.COLORS["RESET"])
|
|
74
|
+
msg = super().format(record)
|
|
75
|
+
return f"{color}{msg}{self.COLORS['RESET']}"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
LOGGER_FORMAT = (
|
|
79
|
+
"%(asctime)s [%(levelname)-8.8s] [%(name)s] "
|
|
80
|
+
"%(module)s.%(classname)s.%(funcName)s: "
|
|
81
|
+
"%(message)s (%(filename)s:%(lineno)d)"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _enable_windows_ansi():
|
|
86
|
+
"""
|
|
87
|
+
Best-effort enable ANSI escape sequences on Windows terminals.
|
|
88
|
+
Newer Windows 10/11 terminals usually support this already.
|
|
89
|
+
"""
|
|
90
|
+
if os.name != "nt":
|
|
91
|
+
return
|
|
92
|
+
try:
|
|
93
|
+
# Enables VT100 sequences in some consoles; harmless if unsupported
|
|
94
|
+
# Justification: Importing ctypes only on Windows is acceptable.
|
|
95
|
+
# pylint: disable=import-outside-toplevel
|
|
96
|
+
import ctypes
|
|
97
|
+
|
|
98
|
+
# pylint: enable=import-outside-toplevel
|
|
99
|
+
|
|
100
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
101
|
+
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE = -11
|
|
102
|
+
mode = ctypes.c_uint32()
|
|
103
|
+
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
104
|
+
# ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
105
|
+
kernel32.SetConsoleMode(handle, mode.value | 0x0004)
|
|
106
|
+
# Justification: We want to catch all exceptions here.
|
|
107
|
+
# pylint: disable=broad-exception-caught
|
|
108
|
+
except Exception:
|
|
109
|
+
# If it fails, we just keep going without breaking logging.
|
|
110
|
+
pass
|
|
111
|
+
# pylint: enable=broad-exception-caught
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _install_record_factory_defaults():
|
|
115
|
+
"""
|
|
116
|
+
Ensure every LogRecord has `classname` so formatters never crash.
|
|
117
|
+
Safe to call multiple times; we keep the current factory chain.
|
|
118
|
+
"""
|
|
119
|
+
old_factory = logging.getLogRecordFactory()
|
|
120
|
+
|
|
121
|
+
def record_factory(*args, **kwargs):
|
|
122
|
+
record = old_factory(*args, **kwargs)
|
|
123
|
+
if not hasattr(record, "classname"):
|
|
124
|
+
record.classname = "-"
|
|
125
|
+
return record
|
|
126
|
+
|
|
127
|
+
logging.setLogRecordFactory(record_factory)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def configure_logging(level: int = logging.DEBUG):
|
|
131
|
+
"""
|
|
132
|
+
Configure logging once for the whole app (root logger).
|
|
133
|
+
Call this early (app entrypoint). Safe to call multiple times.
|
|
134
|
+
"""
|
|
135
|
+
_enable_windows_ansi()
|
|
136
|
+
_install_record_factory_defaults()
|
|
137
|
+
|
|
138
|
+
root = logging.getLogger()
|
|
139
|
+
root.setLevel(level)
|
|
140
|
+
|
|
141
|
+
# Avoid duplicate handlers if reloaded/imported multiple times
|
|
142
|
+
# We tag our handler so we can find it reliably.
|
|
143
|
+
handler_tag = "_mini_arcade_core_console_handler"
|
|
144
|
+
|
|
145
|
+
for h in list(root.handlers):
|
|
146
|
+
if getattr(h, handler_tag, False):
|
|
147
|
+
# Already configured
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
console = logging.StreamHandler(stream=sys.stdout)
|
|
151
|
+
setattr(console, handler_tag, True)
|
|
152
|
+
|
|
153
|
+
console.setFormatter(ConsoleColorFormatter(LOGGER_FORMAT))
|
|
154
|
+
console.addFilter(EnsureClassName())
|
|
155
|
+
|
|
156
|
+
# Important: don’t leave any basicConfig handlers around if someone called it earlier
|
|
157
|
+
# We remove only the plain StreamHandlers that don't have our tag.
|
|
158
|
+
for h in list(root.handlers):
|
|
159
|
+
if isinstance(h, logging.StreamHandler) and not getattr(
|
|
160
|
+
h, handler_tag, False
|
|
161
|
+
):
|
|
162
|
+
root.removeHandler(h)
|
|
163
|
+
|
|
164
|
+
root.addHandler(console)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
configure_logging()
|
|
168
|
+
logger = logging.getLogger("mini-arcade-core")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
mini_arcade_core/__init__.py,sha256=axwl7fiQ2Zu2vPOTMUxwnvR746gI9RhpBWpBCId_yqo,3686
|
|
2
|
+
mini_arcade_core/backend/__init__.py,sha256=E9uOCttCkXwdN_5MlcFHUmG3Bj6RYMatNNOno4C_6aI,312
|
|
3
|
+
mini_arcade_core/backend/backend.py,sha256=RpFclQFDOLSaqCACWR0sT_OQYjDIfYzQaFvnHzURLvI,7788
|
|
4
|
+
mini_arcade_core/backend/events.py,sha256=5Ohve3CQ6n2CztiOhbCoz6yFDY4z0j4v4R9FBKRDRjc,2929
|
|
5
|
+
mini_arcade_core/backend/keys.py,sha256=LTg20SwLBI3kpPIiTNpq2yBft_QUGj-iNFSNm9M-Fus,3010
|
|
6
|
+
mini_arcade_core/backend/sdl_map.py,sha256=_yBRtvaFUcQKy1kcoIf-SPhbbKEW7dzvzBcI6TLmKjc,2060
|
|
7
|
+
mini_arcade_core/backend/types.py,sha256=SuiwXGNmXCZxfPsww6zj3V_NK7k4jpoCuzMn19afS-g,175
|
|
8
|
+
mini_arcade_core/bus.py,sha256=2Etpoa-UWhk33xJjqDlY5YslPDJEjxNoIEVtF3C73vs,1558
|
|
9
|
+
mini_arcade_core/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
mini_arcade_core/engine/commands.py,sha256=1g7XBWmiVYxe2IUggPpUs5uaZP9PHZKfr6cQuhez8eg,4044
|
|
11
|
+
mini_arcade_core/engine/game.py,sha256=hq7cYdiQrPFNagc1BbARhzXYMBtFzCaTcTKQnNBDqy0,12137
|
|
12
|
+
mini_arcade_core/engine/render/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
mini_arcade_core/engine/render/packet.py,sha256=OiAPwGoVHo04OcUWMAoA_N1AFPUMyf8yxNgJthGj4-c,1440
|
|
14
|
+
mini_arcade_core/engine/render/pipeline.py,sha256=XdnzAXNle8_CJOPzYJaEy9Dp_UgoCgJwIh-GpW9-n5E,1516
|
|
15
|
+
mini_arcade_core/engine/render/viewport.py,sha256=fbzH3_rc27IGUtDalmxz5cukwHnpt1kopIeVqmTab20,5784
|
|
16
|
+
mini_arcade_core/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
mini_arcade_core/managers/cheats.py,sha256=jMx2a8YnaNCkCG5MPmIzz4uHuS7-_aYf0J45cv2-3v0,5569
|
|
18
|
+
mini_arcade_core/managers/inputs.py,sha256=9HZ0BnJyUX-elfGETPhhPZnTkz2bK83pEKj7GHPbPFU,8523
|
|
19
|
+
mini_arcade_core/runtime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
mini_arcade_core/runtime/audio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
mini_arcade_core/runtime/audio/audio_adapter.py,sha256=9ithbInYUB72ErwvlNrbIlI55uw0DT2QD33geYNXT3c,522
|
|
22
|
+
mini_arcade_core/runtime/audio/audio_port.py,sha256=3Mqv7TchEVkmd-RVjUpCD-EqA-yiL0Jf2Sj3rQwP678,907
|
|
23
|
+
mini_arcade_core/runtime/capture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
mini_arcade_core/runtime/capture/capture_adapter.py,sha256=qJ2JiOLJHbP00IesbAyyPGPBxSaxwPJRTMaMjMU4bXs,4660
|
|
25
|
+
mini_arcade_core/runtime/capture/capture_port.py,sha256=NVxMJrQJELiSYuUJ29tvsdIcCBq4f1dTT2rDLZs6gnI,1230
|
|
26
|
+
mini_arcade_core/runtime/context.py,sha256=IQpAhtbN0ZqJVXG6KXFq3FPk0sIGyPmyNgBYviqZl7A,1632
|
|
27
|
+
mini_arcade_core/runtime/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
mini_arcade_core/runtime/file/file_adapter.py,sha256=09q7G9Qijml9d4AAjo6HLC1yuoVTjE_7xaT8apT4mk0,523
|
|
29
|
+
mini_arcade_core/runtime/file/file_port.py,sha256=p1MouCSHXZw--rWNMw3aYBLU-of8mXaT_suopczPtM8,608
|
|
30
|
+
mini_arcade_core/runtime/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
mini_arcade_core/runtime/input/input_adapter.py,sha256=vExQiwFIWTI3zYD8lmnD9TvoQPZvJfI6IINPJUqAdQ0,1467
|
|
32
|
+
mini_arcade_core/runtime/input/input_port.py,sha256=d4ptftwf92_LJdyaUMFxIsLHXBINzQyJACHn4laNyxQ,746
|
|
33
|
+
mini_arcade_core/runtime/input_frame.py,sha256=34-RAfOD-YScVLyRQrarpm7byFTHjsWM77lIH0JsmT8,2384
|
|
34
|
+
mini_arcade_core/runtime/scene/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
mini_arcade_core/runtime/scene/scene_adapter.py,sha256=dYkqFWN3FcRmf9tsZ5wz1QZX67CUntndiNMOWrO578U,2523
|
|
36
|
+
mini_arcade_core/runtime/scene/scene_port.py,sha256=uDFrN7fudIldNkivEkagQd9-b1HVI71BpTpSv5Oh6JI,3854
|
|
37
|
+
mini_arcade_core/runtime/services.py,sha256=9LX7O-AYFTxKgBDewzu0B_D01EJ41WVAdl8U1ZcvEYg,1061
|
|
38
|
+
mini_arcade_core/runtime/window/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
+
mini_arcade_core/runtime/window/window_adapter.py,sha256=_yCcJwvHN0gEPh0rkgLEKtPZ50qRbONsUTbgtV1m7Y8,2689
|
|
40
|
+
mini_arcade_core/runtime/window/window_port.py,sha256=JbSH549De7fa4ifQ0EH5QQoq03Got1n9C4qViLgciUU,2682
|
|
41
|
+
mini_arcade_core/scenes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
|
+
mini_arcade_core/scenes/autoreg.py,sha256=wsuY7YUSZFmDyToKHFriAG78OU48-7J4BfL_X6T5GBg,1037
|
|
43
|
+
mini_arcade_core/scenes/registry.py,sha256=EF5qaAgoLwJa3IV_3wclvjHpE3jZt-KHB3GOJS9lQy8,3390
|
|
44
|
+
mini_arcade_core/scenes/sim_scene.py,sha256=b2JsOvPFkHCdCf8pMLJZ90qB0JJ6B8Ka3o5QK4cVshI,1055
|
|
45
|
+
mini_arcade_core/scenes/systems/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
mini_arcade_core/scenes/systems/base_system.py,sha256=GfMrXsO8ynW3xOxWeav7Ug5XUbRnbF0vo8VzmG7gpec,1075
|
|
47
|
+
mini_arcade_core/scenes/systems/system_pipeline.py,sha256=Cy9y1DclbMLZZ-yx7OKYe34ORoGLNa6dReQfOdiO8SY,1642
|
|
48
|
+
mini_arcade_core/sim/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
mini_arcade_core/sim/protocols.py,sha256=b7d2WAKRokTNbteNhUaWdGx9vc9Fnccxb-5rPwozDaA,1099
|
|
50
|
+
mini_arcade_core/sim/runner.py,sha256=ZF-BZJw-NcaFrg4zsUu1zOUUBZwZbRYflqcdF1jDcmM,7446
|
|
51
|
+
mini_arcade_core/spaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
+
mini_arcade_core/spaces/d2/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
|
+
mini_arcade_core/spaces/d2/boundaries2d.py,sha256=xeTnd0pW5DKfqaKsfSBXnufeb45aXNIspgHRyLXWejo,2804
|
|
54
|
+
mini_arcade_core/spaces/d2/collision2d.py,sha256=5IvgLnyVb8i0uzzZuum1noWsNhoxcvHOLaHkmrTMTxQ,1710
|
|
55
|
+
mini_arcade_core/spaces/d2/geometry2d.py,sha256=FuYzef-XdOyb1aeGLJbxINxr0WJHnqFFBgtbPi1WonY,1716
|
|
56
|
+
mini_arcade_core/spaces/d2/kinematics2d.py,sha256=AJ3DhPXNgm6wZYwCljMIE4_2BYx3E2rPcwhXTgQALkU,2030
|
|
57
|
+
mini_arcade_core/spaces/d2/physics2d.py,sha256=OQT7r-zMtmoKD2aWCSNmRAdI0OGIpxGX-pLR8LcAMbQ,1854
|
|
58
|
+
mini_arcade_core/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
|
+
mini_arcade_core/ui/menu.py,sha256=pUC6qfG3lNXa1Ga2Kz_cSgjiAfAWVMRevum61G_RE8k,24646
|
|
60
|
+
mini_arcade_core/utils/__init__.py,sha256=3Q9r6bTyqImYix8BnOGwWjAz25nbTQezGcRq3m5KEYE,189
|
|
61
|
+
mini_arcade_core/utils/deprecated_decorator.py,sha256=yrrW2ZqPskK-4MUTyIrMb465Wc54X2poV53ZQutZWqc,1140
|
|
62
|
+
mini_arcade_core/utils/logging.py,sha256=YyirsGRSpGtxegUl3HWz37mGNngK3QkYm2_aZjXJC84,5279
|
|
63
|
+
mini_arcade_core-1.0.1.dist-info/METADATA,sha256=zon5lZWKZ3_tLUoLgjItQRCTWBxLEzHSOp1IyhNGH14,8188
|
|
64
|
+
mini_arcade_core-1.0.1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
65
|
+
mini_arcade_core-1.0.1.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
|
|
66
|
+
mini_arcade_core-1.0.1.dist-info/RECORD,,
|
mini_arcade_core/commands.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Command protocol for executing commands with a given context.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar
|
|
8
|
-
|
|
9
|
-
from mini_arcade_core.game import Game
|
|
10
|
-
from mini_arcade_core.scenes.model import SceneModel
|
|
11
|
-
|
|
12
|
-
if TYPE_CHECKING:
|
|
13
|
-
from mini_arcade_core.scenes.scene import Scene
|
|
14
|
-
|
|
15
|
-
# Justification: Generic type for context
|
|
16
|
-
# pylint: disable=invalid-name
|
|
17
|
-
TContext = TypeVar("TContext")
|
|
18
|
-
# pylint: enable=invalid-name
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class BaseCommand(Protocol, Generic[TContext]):
|
|
22
|
-
"""
|
|
23
|
-
Protocol for a command that can be executed with a given context.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __call__(self, context: TContext) -> None:
|
|
27
|
-
"""
|
|
28
|
-
Execute the cheat code with the given context.
|
|
29
|
-
|
|
30
|
-
:param context: Context object for cheat execution.
|
|
31
|
-
:type context: TContext
|
|
32
|
-
"""
|
|
33
|
-
self.execute(context)
|
|
34
|
-
|
|
35
|
-
def execute(self, context: TContext):
|
|
36
|
-
"""
|
|
37
|
-
Execute the command with the given context.
|
|
38
|
-
|
|
39
|
-
:param context: Context object for command execution.
|
|
40
|
-
:type context: TContext
|
|
41
|
-
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class BaseGameCommand(BaseCommand[Game]):
|
|
45
|
-
"""
|
|
46
|
-
Base class for commands that operate on the Game context.
|
|
47
|
-
"""
|
|
48
|
-
|
|
49
|
-
def execute(self, context: Game) -> None:
|
|
50
|
-
"""
|
|
51
|
-
Execute the command with the given Game context.
|
|
52
|
-
|
|
53
|
-
:param context: Game context for command execution.
|
|
54
|
-
:type context: Game
|
|
55
|
-
"""
|
|
56
|
-
raise NotImplementedError(
|
|
57
|
-
"Execute method must be implemented by subclasses."
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class BaseSceneCommand(BaseCommand[SceneModel]):
|
|
62
|
-
"""
|
|
63
|
-
Base class for commands that operate on the Scene SceneModel context within a scene.
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
def execute(self, context: SceneModel) -> None:
|
|
67
|
-
"""
|
|
68
|
-
Execute the command with the given Scene Model context.
|
|
69
|
-
|
|
70
|
-
:param context: Scene Model context for command execution.
|
|
71
|
-
:type context: SceneModel
|
|
72
|
-
"""
|
|
73
|
-
raise NotImplementedError(
|
|
74
|
-
"Execute method must be implemented by subclasses."
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class QuitGameCommand(BaseGameCommand):
|
|
79
|
-
"""
|
|
80
|
-
Command to quit the game.
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
def execute(self, context: Game) -> None:
|
|
84
|
-
context.quit()
|
mini_arcade_core/entity.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Entity base classes for mini_arcade_core.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from mini_arcade_core.spaces.d2 import (
|
|
10
|
-
KinematicData,
|
|
11
|
-
Position2D,
|
|
12
|
-
RectCollider,
|
|
13
|
-
Size2D,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class Entity:
|
|
18
|
-
"""Entity base class for game objects."""
|
|
19
|
-
|
|
20
|
-
def update(self, dt: float):
|
|
21
|
-
"""
|
|
22
|
-
Advance the entity state by ``dt`` seconds.
|
|
23
|
-
|
|
24
|
-
:param dt: Time delta in seconds.
|
|
25
|
-
:type dt: float
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def draw(self, surface: Any):
|
|
29
|
-
"""
|
|
30
|
-
Render the entity to the given surface.
|
|
31
|
-
|
|
32
|
-
:param surface: The surface to draw on.
|
|
33
|
-
:type surface: Any
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class SpriteEntity(Entity):
|
|
38
|
-
"""Entity with position and size."""
|
|
39
|
-
|
|
40
|
-
def __init__(self, position: Position2D, size: Size2D):
|
|
41
|
-
"""
|
|
42
|
-
:param position: Top-left position of the entity.
|
|
43
|
-
:type position: Position2D
|
|
44
|
-
|
|
45
|
-
:param size: Size of the entity.
|
|
46
|
-
:type size: Size2D
|
|
47
|
-
"""
|
|
48
|
-
self.position = Position2D(float(position.x), float(position.y))
|
|
49
|
-
self.size = Size2D(int(size.width), int(size.height))
|
|
50
|
-
self.collider = RectCollider(self.position, self.size)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
class KinematicEntity(SpriteEntity):
|
|
54
|
-
"""SpriteEntity with velocity-based movement."""
|
|
55
|
-
|
|
56
|
-
def __init__(self, kinematic_data: KinematicData):
|
|
57
|
-
"""
|
|
58
|
-
:param kinematic_data: Kinematic data for the entity.
|
|
59
|
-
:type kinematic_data: KinematicData
|
|
60
|
-
"""
|
|
61
|
-
super().__init__(
|
|
62
|
-
position=kinematic_data.position,
|
|
63
|
-
size=kinematic_data.size,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
self.time_scale = kinematic_data.time_scale
|
|
67
|
-
self.velocity = kinematic_data.velocity
|
|
68
|
-
|
|
69
|
-
def update(self, dt: float):
|
|
70
|
-
self.position.x, self.position.y = self.velocity.advance(
|
|
71
|
-
self.position.x, self.position.y, dt * self.time_scale
|
|
72
|
-
)
|
mini_arcade_core/game.py
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Game core module defining the Game class and configuration.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from __future__ import annotations
|
|
6
|
-
|
|
7
|
-
import os
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from time import perf_counter, sleep
|
|
12
|
-
from typing import TYPE_CHECKING, Literal, Union
|
|
13
|
-
|
|
14
|
-
from PIL import Image # type: ignore[import]
|
|
15
|
-
|
|
16
|
-
from mini_arcade_core.backend import Backend
|
|
17
|
-
from mini_arcade_core.scenes.registry import SceneRegistry
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING: # avoid runtime circular import
|
|
20
|
-
from mini_arcade_core.scenes import Scene
|
|
21
|
-
|
|
22
|
-
SceneOrId = Union["Scene", str]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@dataclass
|
|
26
|
-
class GameConfig:
|
|
27
|
-
"""
|
|
28
|
-
Configuration options for the Game.
|
|
29
|
-
|
|
30
|
-
:ivar width: Width of the game window in pixels.
|
|
31
|
-
:ivar height: Height of the game window in pixels.
|
|
32
|
-
:ivar title: Title of the game window.
|
|
33
|
-
:ivar fps: Target frames per second.
|
|
34
|
-
:ivar background_color: RGB background color.
|
|
35
|
-
:ivar backend: Optional Backend instance to use for rendering and input.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
width: int = 800
|
|
39
|
-
height: int = 600
|
|
40
|
-
title: str = "Mini Arcade Game"
|
|
41
|
-
fps: int = 60
|
|
42
|
-
background_color: tuple[int, int, int] = (0, 0, 0)
|
|
43
|
-
backend: Backend | None = None
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Difficulty = Literal["easy", "normal", "hard", "insane"]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@dataclass
|
|
50
|
-
class GameSettings:
|
|
51
|
-
"""
|
|
52
|
-
Game settings that can be modified during gameplay.
|
|
53
|
-
|
|
54
|
-
:ivar difficulty: Current game difficulty level.
|
|
55
|
-
"""
|
|
56
|
-
|
|
57
|
-
difficulty: Difficulty = "normal"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
@dataclass
|
|
61
|
-
class _StackEntry:
|
|
62
|
-
scene: "Scene"
|
|
63
|
-
as_overlay: bool = False
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class Game:
|
|
67
|
-
"""Core game object responsible for managing the main loop and active scene."""
|
|
68
|
-
|
|
69
|
-
def __init__(
|
|
70
|
-
self, config: GameConfig, registry: SceneRegistry | None = None
|
|
71
|
-
):
|
|
72
|
-
"""
|
|
73
|
-
:param config: Game configuration options.
|
|
74
|
-
:type config: GameConfig
|
|
75
|
-
|
|
76
|
-
:param registry: Optional SceneRegistry for scene management.
|
|
77
|
-
:type registry: SceneRegistry | None
|
|
78
|
-
|
|
79
|
-
:raises ValueError: If the provided config does not have a valid Backend.
|
|
80
|
-
"""
|
|
81
|
-
self.config = config
|
|
82
|
-
self._current_scene: Scene | None = None
|
|
83
|
-
self._running: bool = False
|
|
84
|
-
|
|
85
|
-
if config.backend is None:
|
|
86
|
-
raise ValueError(
|
|
87
|
-
"GameConfig.backend must be set to a Backend instance"
|
|
88
|
-
)
|
|
89
|
-
self.backend: Backend = config.backend
|
|
90
|
-
self.registry = registry or SceneRegistry(_factories={})
|
|
91
|
-
self._scene_stack: list[_StackEntry] = []
|
|
92
|
-
self.settings = GameSettings()
|
|
93
|
-
|
|
94
|
-
def current_scene(self) -> "Scene | None":
|
|
95
|
-
"""
|
|
96
|
-
Get the currently active scene.
|
|
97
|
-
|
|
98
|
-
:return: The active Scene instance, or None if no scene is active.
|
|
99
|
-
:rtype: Scene | None
|
|
100
|
-
"""
|
|
101
|
-
return self._scene_stack[-1].scene if self._scene_stack else None
|
|
102
|
-
|
|
103
|
-
def change_scene(self, scene: SceneOrId):
|
|
104
|
-
"""
|
|
105
|
-
Swap the active scene. Concrete implementations should call
|
|
106
|
-
``on_exit``/``on_enter`` appropriately.
|
|
107
|
-
|
|
108
|
-
:param scene: The new scene to activate.
|
|
109
|
-
:type scene: SceneOrId
|
|
110
|
-
"""
|
|
111
|
-
scene = self._resolve_scene(scene)
|
|
112
|
-
|
|
113
|
-
while self._scene_stack:
|
|
114
|
-
entry = self._scene_stack.pop()
|
|
115
|
-
entry.scene.on_exit()
|
|
116
|
-
|
|
117
|
-
self._scene_stack.append(_StackEntry(scene=scene, as_overlay=False))
|
|
118
|
-
scene.on_enter()
|
|
119
|
-
|
|
120
|
-
def push_scene(self, scene: SceneOrId, as_overlay: bool = False):
|
|
121
|
-
"""
|
|
122
|
-
Push a scene on top of the current one.
|
|
123
|
-
If as_overlay=True, underlying scene(s) may still be drawn but never updated.
|
|
124
|
-
|
|
125
|
-
:param scene: The scene to push onto the stack.
|
|
126
|
-
:type scene: SceneOrId
|
|
127
|
-
|
|
128
|
-
:param as_overlay: Whether to treat the scene as an overlay.
|
|
129
|
-
:type as_overlay: bool
|
|
130
|
-
"""
|
|
131
|
-
scene = self._resolve_scene(scene)
|
|
132
|
-
|
|
133
|
-
top = self.current_scene()
|
|
134
|
-
if top is not None:
|
|
135
|
-
top.on_pause()
|
|
136
|
-
|
|
137
|
-
self._scene_stack.append(
|
|
138
|
-
_StackEntry(scene=scene, as_overlay=as_overlay)
|
|
139
|
-
)
|
|
140
|
-
scene.on_enter()
|
|
141
|
-
|
|
142
|
-
def pop_scene(self) -> "Scene | None":
|
|
143
|
-
"""
|
|
144
|
-
Pop the top scene. If stack becomes empty, quit.
|
|
145
|
-
|
|
146
|
-
:return: The popped Scene instance, or None if the stack is now empty.
|
|
147
|
-
:rtype: Scene | None
|
|
148
|
-
"""
|
|
149
|
-
if not self._scene_stack:
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
popped = self._scene_stack.pop()
|
|
153
|
-
popped.scene.on_exit()
|
|
154
|
-
|
|
155
|
-
top = self.current_scene()
|
|
156
|
-
if top is None:
|
|
157
|
-
self.quit()
|
|
158
|
-
return popped.scene
|
|
159
|
-
|
|
160
|
-
top.on_resume()
|
|
161
|
-
return popped.scene
|
|
162
|
-
|
|
163
|
-
def _visible_stack(self) -> list["Scene"]:
|
|
164
|
-
"""
|
|
165
|
-
Return the list of scenes that should be drawn (base + overlays).
|
|
166
|
-
We draw from the top-most non-overlay scene upward.
|
|
167
|
-
"""
|
|
168
|
-
if not self._scene_stack:
|
|
169
|
-
return []
|
|
170
|
-
|
|
171
|
-
# find top-most base scene (as_overlay=False)
|
|
172
|
-
base_idx = 0
|
|
173
|
-
for i in range(len(self._scene_stack) - 1, -1, -1):
|
|
174
|
-
if not self._scene_stack[i].as_overlay:
|
|
175
|
-
base_idx = i
|
|
176
|
-
break
|
|
177
|
-
|
|
178
|
-
return [e.scene for e in self._scene_stack[base_idx:]]
|
|
179
|
-
|
|
180
|
-
def quit(self):
|
|
181
|
-
"""Request that the main loop stops."""
|
|
182
|
-
self._running = False
|
|
183
|
-
|
|
184
|
-
def run(self, initial_scene: SceneOrId):
|
|
185
|
-
"""
|
|
186
|
-
Run the main loop starting with the given scene.
|
|
187
|
-
|
|
188
|
-
This is intentionally left abstract so you can plug pygame, pyglet,
|
|
189
|
-
or another backend.
|
|
190
|
-
|
|
191
|
-
:param initial_scene: The scene to start the game with.
|
|
192
|
-
:type initial_scene: SceneOrId
|
|
193
|
-
"""
|
|
194
|
-
backend = self.backend
|
|
195
|
-
backend.init(self.config.width, self.config.height, self.config.title)
|
|
196
|
-
|
|
197
|
-
br, bg, bb = self.config.background_color
|
|
198
|
-
backend.set_clear_color(br, bg, bb)
|
|
199
|
-
|
|
200
|
-
self.change_scene(initial_scene)
|
|
201
|
-
|
|
202
|
-
self._running = True
|
|
203
|
-
target_dt = 1.0 / self.config.fps if self.config.fps > 0 else 0.0
|
|
204
|
-
last_time = perf_counter()
|
|
205
|
-
|
|
206
|
-
while self._running:
|
|
207
|
-
now = perf_counter()
|
|
208
|
-
dt = now - last_time
|
|
209
|
-
last_time = now
|
|
210
|
-
|
|
211
|
-
top = self.current_scene()
|
|
212
|
-
if top is None:
|
|
213
|
-
break
|
|
214
|
-
|
|
215
|
-
for ev in backend.poll_events():
|
|
216
|
-
top.handle_event(ev)
|
|
217
|
-
|
|
218
|
-
top.update(dt)
|
|
219
|
-
|
|
220
|
-
backend.begin_frame()
|
|
221
|
-
for scene in self._visible_stack():
|
|
222
|
-
scene.draw(backend)
|
|
223
|
-
backend.end_frame()
|
|
224
|
-
|
|
225
|
-
if target_dt > 0 and dt < target_dt:
|
|
226
|
-
sleep(target_dt - dt)
|
|
227
|
-
|
|
228
|
-
# exit remaining scenes
|
|
229
|
-
while self._scene_stack:
|
|
230
|
-
entry = self._scene_stack.pop()
|
|
231
|
-
entry.scene.on_exit()
|
|
232
|
-
|
|
233
|
-
@staticmethod
|
|
234
|
-
def _convert_bmp_to_image(bmp_path: str, out_path: str) -> bool:
|
|
235
|
-
"""
|
|
236
|
-
Convert a BMP file to another image format using Pillow.
|
|
237
|
-
|
|
238
|
-
:param bmp_path: Path to the input BMP file.
|
|
239
|
-
:type bmp_path: str
|
|
240
|
-
|
|
241
|
-
:param out_path: Path to the output image file.
|
|
242
|
-
:type out_path: str
|
|
243
|
-
|
|
244
|
-
:return: True if conversion was successful, False otherwise.
|
|
245
|
-
:rtype: bool
|
|
246
|
-
"""
|
|
247
|
-
try:
|
|
248
|
-
img = Image.open(bmp_path)
|
|
249
|
-
img.save(out_path) # Pillow chooses format from extension
|
|
250
|
-
return True
|
|
251
|
-
# Justification: Pillow can raise various exceptions on failure
|
|
252
|
-
# pylint: disable=broad-exception-caught
|
|
253
|
-
except Exception:
|
|
254
|
-
return False
|
|
255
|
-
# pylint: enable=broad-exception-caught
|
|
256
|
-
|
|
257
|
-
def screenshot(
|
|
258
|
-
self, label: str | None = None, directory: str = "screenshots"
|
|
259
|
-
) -> str | None:
|
|
260
|
-
"""
|
|
261
|
-
Ask backend to save a screenshot. Returns the file path or None.
|
|
262
|
-
|
|
263
|
-
:param label: Optional label to include in the filename.
|
|
264
|
-
:type label: str | None
|
|
265
|
-
|
|
266
|
-
:param directory: Directory to save screenshots in.
|
|
267
|
-
:type directory: str
|
|
268
|
-
|
|
269
|
-
:return: The file path of the saved screenshot, or None on failure.
|
|
270
|
-
:rtype: str | None
|
|
271
|
-
"""
|
|
272
|
-
os.makedirs(directory, exist_ok=True)
|
|
273
|
-
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
274
|
-
label = label or "shot"
|
|
275
|
-
filename = f"{stamp}_{label}"
|
|
276
|
-
bmp_path = os.path.join(directory, f"{filename}.bmp")
|
|
277
|
-
|
|
278
|
-
if self.backend.capture_frame(bmp_path):
|
|
279
|
-
out_path = Path(directory) / f"{filename}.png"
|
|
280
|
-
self._convert_bmp_to_image(bmp_path, str(out_path))
|
|
281
|
-
return str(out_path)
|
|
282
|
-
return None
|
|
283
|
-
|
|
284
|
-
def _resolve_scene(self, scene: SceneOrId) -> "Scene":
|
|
285
|
-
if isinstance(scene, str):
|
|
286
|
-
return self.registry.create(scene, self)
|
|
287
|
-
return scene
|