mini-arcade-core 0.9.6__py3-none-any.whl → 0.9.8__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.
@@ -1,43 +1,16 @@
1
1
  """
2
2
  Entry point for the mini_arcade_core package.
3
3
  Provides access to core classes and a convenience function to run a game.
4
-
5
- mini_arcade_core/
6
- |-- __init__.py # main entry point
7
- |-- game.py # core game loop and management
8
- |-- entity.py # base entity classes
9
- |-- backend/ # backend abstraction layer
10
- | |-- __init__.py
11
- | |-- backend.py # abstract Backend class
12
- | |-- events.py # event definitions
13
- | |-- types.py # common types like Color
14
- |-- keymaps/ # key mapping utilities
15
- | |-- __init__.py
16
- | |-- keys.py # key definitions and keymaps
17
- | |-- sdl.py # SDL keycode mappings
18
- |-- scenes/ # scene management
19
- | |-- __init__.py
20
- | |-- autoreg.py # automatic scene registration
21
- | |-- registry.py # SceneRegistry class
22
- | |-- scene.py # base Scene class
23
- |-- two_d/ # 2D utilities and types
24
- | |-- __init__.py
25
- | |-- boundaries2d.py # boundary behaviors
26
- | |-- collision2d.py # collision detection
27
- | |-- geometry2d.py # geometric types like Position2D, Size2D
28
- | |-- kinematics2d.py # kinematic data structures
29
- | |-- physics2d.py # physics-related types like Velocity2D
30
- |-- ui/ # user interface components
31
- | |-- __init__.py
32
- | |-- menu.py # menu components
33
4
  """
34
5
 
35
6
  from __future__ import annotations
36
7
 
37
8
  import logging
38
9
  from importlib.metadata import PackageNotFoundError, version
10
+ from typing import Callable, Type, Union
39
11
 
40
12
  from mini_arcade_core.backend import Backend, Event, EventType
13
+ from mini_arcade_core.cheats import CheatCode, CheatManager
41
14
  from mini_arcade_core.entity import Entity, KinematicEntity, SpriteEntity
42
15
  from mini_arcade_core.game import Game, GameConfig
43
16
  from mini_arcade_core.keymaps.keys import Key, keymap
@@ -55,19 +28,27 @@ from mini_arcade_core.two_d import (
55
28
  VerticalWrap,
56
29
  )
57
30
 
31
+ SceneFactoryLike = Union[Type[Scene], Callable[[Game], Scene]]
32
+
58
33
  logger = logging.getLogger(__name__)
59
34
 
60
35
 
61
36
  def run_game(
62
- initial_scene_cls: type[Scene],
37
+ scene: SceneFactoryLike | None = None,
63
38
  config: GameConfig | None = None,
64
39
  registry: SceneRegistry | None = None,
40
+ initial_scene: str = "main",
65
41
  ):
66
42
  """
67
43
  Convenience helper to bootstrap and run a game with a single scene.
68
44
 
69
- :param initial_scene_cls: The Scene subclass to instantiate as the initial scene.
70
- :type initial_scene_cls: type[Scene]
45
+ Supports both:
46
+ - run_game(SceneClass, cfg) # legacy
47
+ - run_game(config=cfg, initial_scene="main", registry=...) # registry-based
48
+ - run_game(cfg) # config-only
49
+
50
+ :param initial_scene: The Scene ID to start the game with.
51
+ :type initial_scene: str
71
52
 
72
53
  :param config: Optional GameConfig to customize game settings.
73
54
  :type config: GameConfig | None
@@ -77,14 +58,27 @@ def run_game(
77
58
 
78
59
  :raises ValueError: If the provided config does not have a valid Backend.
79
60
  """
61
+ # Handle run_game(cfg) where the first arg is actually a GameConfig
62
+ if isinstance(scene, GameConfig) and config is None:
63
+ config = scene
64
+ scene = None
65
+
80
66
  cfg = config or GameConfig()
81
- if config.backend is None:
67
+ if cfg.backend is None:
82
68
  raise ValueError(
83
69
  "GameConfig.backend must be set to a Backend instance"
84
70
  )
71
+
72
+ # If user provided a Scene factory/class, ensure it's registered
73
+ if scene is not None:
74
+ if registry is None:
75
+ registry = SceneRegistry(_factories={})
76
+ registry.register(
77
+ initial_scene, scene
78
+ ) # Scene class is callable(game) -> Scene
79
+
85
80
  game = Game(cfg, registry=registry)
86
- scene = initial_scene_cls(game)
87
- game.run(scene)
81
+ game.run(initial_scene)
88
82
 
89
83
 
90
84
  __all__ = [
@@ -112,6 +106,8 @@ __all__ = [
112
106
  "keymap",
113
107
  "SceneRegistry",
114
108
  "register_scene",
109
+ "CheatManager",
110
+ "CheatCode",
115
111
  ]
116
112
 
117
113
  PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
@@ -141,6 +137,3 @@ def get_version() -> str:
141
137
 
142
138
 
143
139
  __version__ = get_version()
144
- __version__ = get_version()
145
- __version__ = get_version()
146
- __version__ = get_version()
@@ -0,0 +1,235 @@
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
10
+ from enum import Enum
11
+ from typing import Callable, Deque, Dict, Optional, Sequence, TypeVar
12
+
13
+ # Justification: We want to keep the type variable name simple here.
14
+ # pylint: disable=invalid-name
15
+ TContext = TypeVar("TContext")
16
+ # pylint: enable=invalid-name
17
+
18
+ CheatCallback = Callable[[TContext], None]
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 callback (CheatCallback): Function 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
+ callback: CheatCallback
36
+ clear_buffer_on_match: bool = False
37
+ enabled: bool = True
38
+
39
+
40
+ class CheatManager:
41
+ """
42
+ Reusable cheat code matcher.
43
+ Keeps a rolling buffer of recent keys and triggers callbacks on sequence match.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ buffer_size: int = 16,
49
+ *,
50
+ enabled: bool = True,
51
+ normalize: Optional[Callable[[str], str]] = None,
52
+ key_getter: Optional[Callable[[object], Optional[str]]] = None,
53
+ ):
54
+ """
55
+ :param buffer_size: Maximum size of the input buffer.
56
+ :type buffer_size: int
57
+
58
+ :param enabled: Whether the cheat manager is enabled.
59
+ :type enabled: bool
60
+
61
+ :param normalize: Optional function to normalize key strings.
62
+ :type normalize: Callable[[str], str] | None
63
+
64
+ :param key_getter: Optional function to extract key string from event object.
65
+ :type key_getter: Callable[[object], Optional[str]] | None
66
+ """
67
+ self.enabled = enabled
68
+ self._buffer: Deque[str] = deque(maxlen=buffer_size)
69
+ self._codes: Dict[str, CheatCode] = {}
70
+
71
+ self._normalize = normalize or (lambda s: s.strip().upper())
72
+ # key_getter: how to extract a key from an engine/backend event object
73
+ self._key_getter = key_getter or self._default_key_getter
74
+
75
+ # Justification: We want to keep the number of arguments manageable here.
76
+ # pylint: disable=too-many-arguments
77
+ def register_code(
78
+ self,
79
+ name: str,
80
+ sequence: Sequence[str],
81
+ callback: CheatCallback,
82
+ *,
83
+ clear_buffer_on_match: bool = False,
84
+ enabled: bool = True,
85
+ ):
86
+ """
87
+ Register a new cheat code.
88
+
89
+ :param name: Unique name for the cheat code.
90
+ :type name: str
91
+
92
+ :param sequence: Sequence of key strings that trigger the cheat.
93
+ :type sequence: Sequence[str]
94
+
95
+ :param callback: Function to call when the cheat is activated.
96
+ :type callback: CheatCallback
97
+
98
+ :param clear_buffer_on_match: Whether to clear the input buffer after a match.
99
+ :type clear_buffer_on_match: bool
100
+
101
+ :param enabled: Whether the cheat code is enabled.
102
+ :type enabled: bool
103
+ """
104
+ if not name:
105
+ raise ValueError("Cheat code name must be non-empty.")
106
+ if not sequence:
107
+ raise ValueError(
108
+ f"Cheat code '{name}' sequence must be non-empty."
109
+ )
110
+
111
+ norm_seq = tuple(self._normalize(k) for k in sequence)
112
+ self._codes[name] = CheatCode(
113
+ name=name,
114
+ sequence=norm_seq,
115
+ callback=callback,
116
+ clear_buffer_on_match=clear_buffer_on_match,
117
+ enabled=enabled,
118
+ )
119
+
120
+ # pylint: enable=too-many-arguments
121
+
122
+ def unregister_code(self, name: str):
123
+ """
124
+ Unregister a cheat code by name.
125
+
126
+ :param name: Name of the cheat code to unregister.
127
+ :type name: str
128
+ """
129
+ self._codes.pop(name, None)
130
+
131
+ def clear_buffer(self):
132
+ """Clear the input buffer."""
133
+ self._buffer.clear()
134
+
135
+ def process_event(self, event: object, context: TContext) -> list[str]:
136
+ """
137
+ Call from Scene when a key is pressed.
138
+ Returns list of cheat names matched (often 0 or 1).
139
+
140
+ :param event: The event object from the backend/engine.
141
+ :type event: object
142
+
143
+ :param context: Context object passed to cheat callbacks.
144
+ :type context: TContext
145
+ """
146
+ if not self.enabled:
147
+ return []
148
+ key = self._key_getter(event)
149
+ if not key:
150
+ return []
151
+ return self.process_key(key, context)
152
+
153
+ def process_key(self, key: str, context: TContext) -> list[str]:
154
+ """
155
+ Pure method for tests: feed a key string directly.
156
+
157
+ :param key: The key string to process.
158
+ :type key: str
159
+
160
+ :param context: Context object passed to cheat callbacks.
161
+ :type context: TContext
162
+
163
+ :return: List of cheat names matched.
164
+ :rtype: list[str]
165
+ """
166
+ if not self.enabled:
167
+ return []
168
+
169
+ k = self._normalize(key)
170
+ if not k:
171
+ return []
172
+
173
+ self._buffer.append(k)
174
+ buf = tuple(self._buffer)
175
+
176
+ matched: list[str] = []
177
+ # Check if buffer ends with any cheat sequence
178
+ for code in self._codes.values():
179
+ if not code.enabled:
180
+ continue
181
+ seq = code.sequence
182
+ if len(seq) > len(buf):
183
+ continue
184
+ if buf[-len(seq) :] == seq:
185
+ code.callback(context)
186
+ matched.append(code.name)
187
+ if code.clear_buffer_on_match:
188
+ self.clear_buffer()
189
+ break # buffer changed; stop early
190
+
191
+ return matched
192
+
193
+ @staticmethod
194
+ def _default_key_getter(event: object) -> Optional[str]:
195
+ """
196
+ Best-effort extraction:
197
+ - event.key
198
+ - event.key_code
199
+ - event.code
200
+ - event.scancode
201
+ - dict-like {"key": "..."}
202
+ Adjust/override with key_getter in your engine if needed.
203
+
204
+ :param event: The event object.
205
+ :type event: object
206
+
207
+ :return: Extracted key string, or None if not found.
208
+ :rtype: Optional[str]
209
+ """
210
+ if event is None:
211
+ return None
212
+
213
+ # dict-like
214
+ if isinstance(event, dict):
215
+ v = (
216
+ event.get("key")
217
+ or event.get("key_code")
218
+ or event.get("code")
219
+ or event.get("scancode")
220
+ )
221
+ if isinstance(v, Enum):
222
+ return v.name
223
+ return str(v) if v is not None else None
224
+
225
+ # object-like
226
+ for attr in ("key", "key_code", "code", "scancode"):
227
+ if hasattr(event, attr):
228
+ v = getattr(event, attr)
229
+ if v is None:
230
+ continue
231
+ if isinstance(v, Enum):
232
+ return v.name # <-- THIS is the important bit
233
+ return str(v)
234
+
235
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mini-arcade-core
3
- Version: 0.9.6
3
+ Version: 0.9.8
4
4
  Summary: Tiny scene-based game loop core for small arcade games.
5
5
  License: Copyright (c) 2025 Santiago Rincón
6
6
 
@@ -1,8 +1,9 @@
1
- mini_arcade_core/__init__.py,sha256=yAtLsSrBZD3778nStf0XQjKn5hTP-daYkddk7_dAGe4,4029
1
+ mini_arcade_core/__init__.py,sha256=3gb_Q9j5jGlBljwmpExrsXDKZX9FLbCxms9KxhrCOA0,3747
2
2
  mini_arcade_core/backend/__init__.py,sha256=w-6QTUngdIYZuvEU3B8zL-vXyKbyLDVucbt7yKZdLlc,379
3
3
  mini_arcade_core/backend/backend.py,sha256=dLXTtLn5qURftWOWOVQd2ouuZmJpbiOl11KMIDRk2us,4026
4
4
  mini_arcade_core/backend/events.py,sha256=usn2HTk5-5ZiaU2IjbrNRpEj_4uoaiqfY3qufQLYy0w,2929
5
5
  mini_arcade_core/backend/types.py,sha256=SuiwXGNmXCZxfPsww6zj3V_NK7k4jpoCuzMn19afS-g,175
6
+ mini_arcade_core/cheats.py,sha256=J0HPLUC6sARXNl_CtUaWhWoo9A0vEjQcIHP6XJUPrNQ,7133
6
7
  mini_arcade_core/entity.py,sha256=vDe2v7GajyGaqckBYeD3cjs--pOTFrDNhMIdf7PZsJQ,1759
7
8
  mini_arcade_core/game.py,sha256=C8flMXn_DMHWDmr5FVrTey_nUHabwja8wSaialNMcLg,8526
8
9
  mini_arcade_core/keymaps/__init__.py,sha256=_5Y5_61XT5TsOhbb6p2EPzvjCNN9hCPoxTgj_TAfBvA,258
@@ -20,7 +21,7 @@ mini_arcade_core/two_d/kinematics2d.py,sha256=NFMiIzDYqDquyg_EhD7EQBJ_Sz4RncmkEj
20
21
  mini_arcade_core/two_d/physics2d.py,sha256=qIq86qWVuvcnLIMEPH6xx7XQO4tQhfiMZY6FseuVOR8,1636
21
22
  mini_arcade_core/ui/__init__.py,sha256=RmcZXfBFFmL09j_Bo1LHagRjiKYajuySPilIUNjc3KQ,248
22
23
  mini_arcade_core/ui/menu.py,sha256=J3hYY46xOHLN65WSet4-cnQU7L5pu48scjLJUW9IkEM,13681
23
- mini_arcade_core-0.9.6.dist-info/METADATA,sha256=TXCP-kgqZegagNZ6_Zoa_lLU0nhNiC6yVGnHpv4iRjM,8188
24
- mini_arcade_core-0.9.6.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
25
- mini_arcade_core-0.9.6.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
26
- mini_arcade_core-0.9.6.dist-info/RECORD,,
24
+ mini_arcade_core-0.9.8.dist-info/METADATA,sha256=YkzK5IIHDjJN4hTAu9dAUiOOAiOFLEm15zMwJMxDcTs,8188
25
+ mini_arcade_core-0.9.8.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
26
+ mini_arcade_core-0.9.8.dist-info/licenses/LICENSE,sha256=3lHAuV0584cVS5vAqi2uC6GcsVgxUijvwvtZckyvaZ4,1096
27
+ mini_arcade_core-0.9.8.dist-info/RECORD,,