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.
Files changed (78) hide show
  1. mini_arcade_core/__init__.py +51 -63
  2. mini_arcade_core/backend/__init__.py +2 -6
  3. mini_arcade_core/backend/backend.py +148 -8
  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/engine/__init__.py +0 -0
  7. mini_arcade_core/engine/commands.py +169 -0
  8. mini_arcade_core/engine/game.py +369 -0
  9. mini_arcade_core/engine/render/__init__.py +0 -0
  10. mini_arcade_core/engine/render/packet.py +56 -0
  11. mini_arcade_core/engine/render/pipeline.py +63 -0
  12. mini_arcade_core/engine/render/viewport.py +203 -0
  13. mini_arcade_core/managers/__init__.py +0 -22
  14. mini_arcade_core/managers/cheats.py +71 -240
  15. mini_arcade_core/managers/inputs.py +5 -3
  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 +20 -0
  19. mini_arcade_core/runtime/audio/audio_port.py +36 -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 +51 -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 +90 -0
  37. mini_arcade_core/runtime/window/window_port.py +109 -0
  38. mini_arcade_core/scenes/__init__.py +0 -22
  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 -12
  49. mini_arcade_core/spaces/d2/__init__.py +0 -30
  50. mini_arcade_core/spaces/d2/boundaries2d.py +10 -1
  51. mini_arcade_core/spaces/d2/collision2d.py +25 -28
  52. mini_arcade_core/spaces/d2/geometry2d.py +18 -0
  53. mini_arcade_core/spaces/d2/kinematics2d.py +2 -8
  54. mini_arcade_core/spaces/d2/physics2d.py +9 -0
  55. mini_arcade_core/ui/__init__.py +0 -26
  56. mini_arcade_core/ui/menu.py +271 -85
  57. mini_arcade_core/utils/__init__.py +10 -0
  58. mini_arcade_core/utils/deprecated_decorator.py +45 -0
  59. mini_arcade_core/utils/logging.py +168 -0
  60. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/METADATA +1 -1
  61. mini_arcade_core-1.0.1.dist-info/RECORD +66 -0
  62. {mini_arcade_core-0.10.0.dist-info → mini_arcade_core-1.0.1.dist-info}/WHEEL +1 -1
  63. mini_arcade_core/commands.py +0 -84
  64. mini_arcade_core/entity.py +0 -72
  65. mini_arcade_core/game.py +0 -287
  66. mini_arcade_core/keymaps/__init__.py +0 -15
  67. mini_arcade_core/managers/base.py +0 -132
  68. mini_arcade_core/managers/entities.py +0 -38
  69. mini_arcade_core/managers/overlays.py +0 -53
  70. mini_arcade_core/managers/system.py +0 -26
  71. mini_arcade_core/scenes/model.py +0 -34
  72. mini_arcade_core/scenes/runtime.py +0 -29
  73. mini_arcade_core/scenes/scene.py +0 -109
  74. mini_arcade_core/scenes/system.py +0 -69
  75. mini_arcade_core/ui/overlays.py +0 -41
  76. mini_arcade_core-0.10.0.dist-info/RECORD +0 -40
  77. /mini_arcade_core/{keymaps → backend}/keys.py +0 -0
  78. {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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 0.10.0
3
+ Version: 1.0.1
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.2.1
2
+ Generator: poetry-core 2.3.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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()
@@ -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