mini-arcade-core 0.9.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.
@@ -0,0 +1,100 @@
1
+ """
2
+ Entry point for the mini_arcade_core package.
3
+ Provides access to core classes and a convenience function to run a game.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ from .backend import Backend, Event, EventType
12
+ from .boundaries2d import (
13
+ RectKinematic,
14
+ RectSprite,
15
+ VerticalBounce,
16
+ VerticalWrap,
17
+ )
18
+ from .collision2d import RectCollider
19
+ from .entity import Entity, KinematicEntity, SpriteEntity
20
+ from .game import Game, GameConfig
21
+ from .geometry2d import Bounds2D, Position2D, Size2D
22
+ from .kinematics2d import KinematicData
23
+ from .physics2d import Velocity2D
24
+ from .scene import Scene
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ def run_game(initial_scene_cls: type[Scene], config: GameConfig | None = None):
30
+ """
31
+ Convenience helper to bootstrap and run a game with a single scene.
32
+
33
+ :param initial_scene_cls: The Scene subclass to instantiate as the initial scene.
34
+ :type initial_scene_cls: type[Scene]
35
+
36
+ :param config: Optional GameConfig to customize game settings.
37
+ :type config: GameConfig | None
38
+
39
+ :raises ValueError: If the provided config does not have a valid Backend.
40
+ """
41
+ cfg = config or GameConfig()
42
+ if config.backend is None:
43
+ raise ValueError(
44
+ "GameConfig.backend must be set to a Backend instance"
45
+ )
46
+ game = Game(cfg)
47
+ scene = initial_scene_cls(game)
48
+ game.run(scene)
49
+
50
+
51
+ __all__ = [
52
+ "Game",
53
+ "GameConfig",
54
+ "Scene",
55
+ "Entity",
56
+ "SpriteEntity",
57
+ "run_game",
58
+ "Backend",
59
+ "Event",
60
+ "EventType",
61
+ "Velocity2D",
62
+ "Position2D",
63
+ "Size2D",
64
+ "KinematicEntity",
65
+ "KinematicData",
66
+ "RectCollider",
67
+ "VerticalBounce",
68
+ "Bounds2D",
69
+ "VerticalWrap",
70
+ "RectSprite",
71
+ "RectKinematic",
72
+ ]
73
+
74
+ PACKAGE_NAME = "mini-arcade-core" # or whatever is in your pyproject.toml
75
+
76
+
77
+ def get_version() -> str:
78
+ """
79
+ Return the installed package version.
80
+
81
+ This is a thin helper around importlib.metadata.version so games can do:
82
+
83
+ from mini_arcade_core import get_version
84
+ print(get_version())
85
+
86
+ :return: The version string of the installed package.
87
+ :rtype: str
88
+
89
+ :raises PackageNotFoundError: If the package is not installed.
90
+ """
91
+ try:
92
+ return version(PACKAGE_NAME)
93
+ except PackageNotFoundError: # if running from source / editable
94
+ logger.warning(
95
+ f"Package '{PACKAGE_NAME}' not found. Returning default version '0.0.0'."
96
+ )
97
+ return "0.0.0"
98
+
99
+
100
+ __version__ = get_version()
@@ -0,0 +1,225 @@
1
+ """
2
+ Backend interface for rendering and input.
3
+ This is the only part of the code that talks to SDL/pygame directly.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from enum import Enum, auto
10
+ from typing import Iterable, Optional, Protocol, Tuple, Union
11
+
12
+ Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
13
+
14
+
15
+ class EventType(Enum):
16
+ """
17
+ High-level event types understood by the core.
18
+
19
+ :cvar UNKNOWN: Unknown/unhandled event.
20
+ :cvar QUIT: User requested to quit the game.
21
+ :cvar KEYDOWN: A key was pressed.
22
+ :cvar KEYUP: A key was released.
23
+ :cvar MOUSEMOTION: The mouse was moved.
24
+ :cvar MOUSEBUTTONDOWN: A mouse button was pressed.
25
+ :cvar MOUSEBUTTONUP: A mouse button was released.
26
+ :cvar MOUSEWHEEL: The mouse wheel was scrolled.
27
+ :cvar WINDOWRESIZED: The window was resized.
28
+ :cvar TEXTINPUT: Text input event (for IME support).
29
+ """
30
+
31
+ UNKNOWN = auto()
32
+ QUIT = auto()
33
+
34
+ KEYDOWN = auto()
35
+ KEYUP = auto()
36
+
37
+ # Mouse
38
+ MOUSEMOTION = auto()
39
+ MOUSEBUTTONDOWN = auto()
40
+ MOUSEBUTTONUP = auto()
41
+ MOUSEWHEEL = auto()
42
+
43
+ # Window / text
44
+ WINDOWRESIZED = auto()
45
+ TEXTINPUT = auto()
46
+
47
+
48
+ # Justification: Simple data container for now
49
+ # pylint: disable=too-many-instance-attributes
50
+ @dataclass(frozen=True)
51
+ class Event:
52
+ """
53
+ Core event type.
54
+
55
+ For now we only care about:
56
+ - type: what happened
57
+ - key: integer key code (e.g. ESC = 27), or None if not applicable
58
+
59
+ :ivar type (EventType): The type of event.
60
+ :ivar key (int | None): The key code associated with the event, if any.
61
+ :ivar scancode (int | None): The hardware scancode of the key, if any.
62
+ :ivar mod (int | None): Modifier keys bitmask, if any.
63
+ :ivar repeat (bool | None): Whether this key event is a repeat, if any.
64
+ :ivar x (int | None): Mouse X position, if any.
65
+ :ivar y (int | None): Mouse Y position, if any.
66
+ :ivar dx (int | None): Mouse delta X, if any.
67
+ :ivar dy (int | None): Mouse delta Y, if any.
68
+ :ivar button (int | None): Mouse button number, if any.
69
+ :ivar wheel (Tuple[int, int] | None): Mouse wheel scroll (x, y), if any.
70
+ :ivar size (Tuple[int, int] | None): New window size (width, height), if any.
71
+ :ivar text (str | None): Text input, if any.
72
+ """
73
+
74
+ type: EventType
75
+ key: Optional[int] = None
76
+
77
+ # Keyboard extras (optional)
78
+ scancode: Optional[int] = None
79
+ mod: Optional[int] = None
80
+ repeat: Optional[bool] = None
81
+
82
+ # Mouse (optional)
83
+ x: Optional[int] = None
84
+ y: Optional[int] = None
85
+ dx: Optional[int] = None
86
+ dy: Optional[int] = None
87
+ button: Optional[int] = None
88
+ wheel: Optional[Tuple[int, int]] = None # (wheel_x, wheel_y)
89
+
90
+ # Window (optional)
91
+ size: Optional[Tuple[int, int]] = None # (width, height)
92
+
93
+ # Text input (optional)
94
+ text: Optional[str] = None
95
+
96
+
97
+ # pylint: enable=too-many-instance-attributes
98
+
99
+
100
+ class Backend(Protocol):
101
+ """
102
+ Interface that any rendering/input backend must implement.
103
+
104
+ mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
105
+ """
106
+
107
+ def init(self, width: int, height: int, title: str):
108
+ """
109
+ Initialize the backend and open a window.
110
+ Should be called once before the main loop.
111
+
112
+ :param width: Width of the window in pixels.
113
+ :type width: int
114
+
115
+ :param height: Height of the window in pixels.
116
+ :type height: int
117
+
118
+ :param title: Title of the window.
119
+ :type title: str
120
+ """
121
+
122
+ def poll_events(self) -> Iterable[Event]:
123
+ """
124
+ Return all pending events since last call.
125
+ Concrete backends will translate their native events into core Event objects.
126
+
127
+ :return: An iterable of Event objects.
128
+ :rtype: Iterable[Event]
129
+ """
130
+
131
+ def set_clear_color(self, r: int, g: int, b: int):
132
+ """
133
+ Set the background/clear color used by begin_frame.
134
+
135
+ :param r: Red component (0-255).
136
+ :type r: int
137
+
138
+ :param g: Green component (0-255).
139
+ :type g: int
140
+
141
+ :param b: Blue component (0-255).
142
+ :type b: int
143
+ """
144
+
145
+ def begin_frame(self):
146
+ """
147
+ Prepare for drawing a new frame (e.g. clear screen).
148
+ """
149
+
150
+ def end_frame(self):
151
+ """
152
+ Present the frame to the user (swap buffers).
153
+ """
154
+
155
+ # Justification: Simple drawing API for now
156
+ # pylint: disable=too-many-arguments,too-many-positional-arguments
157
+ def draw_rect(
158
+ self,
159
+ x: int,
160
+ y: int,
161
+ w: int,
162
+ h: int,
163
+ color: Color = (255, 255, 255),
164
+ ):
165
+ """
166
+ Draw a filled rectangle in some default color.
167
+ We'll keep this minimal for now; later we can extend with colors/sprites.
168
+
169
+ :param x: X position of the rectangle's top-left corner.
170
+ :type x: int
171
+
172
+ :param y: Y position of the rectangle's top-left corner.
173
+ :type y: int
174
+
175
+ :param w: Width of the rectangle.
176
+ :type w: int
177
+
178
+ :param h: Height of the rectangle.
179
+ :type h: int
180
+
181
+ :param color: RGB color tuple.
182
+ :type color: Color
183
+ """
184
+
185
+ # pylint: enable=too-many-arguments,too-many-positional-arguments
186
+
187
+ def draw_text(
188
+ self,
189
+ x: int,
190
+ y: int,
191
+ text: str,
192
+ color: Color = (255, 255, 255),
193
+ ):
194
+ """
195
+ Draw text at the given position in a default font and color.
196
+
197
+ Backends may ignore advanced styling for now; this is just to render
198
+ simple labels like menu items, scores, etc.
199
+
200
+ :param x: X position of the text's top-left corner.
201
+ :type x: int
202
+
203
+ :param y: Y position of the text's top-left corner.
204
+ :type y: int
205
+
206
+ :param text: The text string to draw.
207
+ :type text: str
208
+
209
+ :param color: RGB color tuple.
210
+ :type color: Color
211
+ """
212
+
213
+ def capture_frame(self, path: str | None = None) -> bytes | None:
214
+ """
215
+ Capture the current frame.
216
+ If `path` is provided, save to that file (e.g. PNG).
217
+ Returns raw bytes (PNG) or None if unsupported.
218
+
219
+ :param path: Optional file path to save the screenshot.
220
+ :type path: str | None
221
+
222
+ :return: Raw image bytes if no path given, else None.
223
+ :rtype: bytes | None
224
+ """
225
+ raise NotImplementedError
@@ -0,0 +1,107 @@
1
+ """
2
+ Boundary behaviors for 2D rectangular objects.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Protocol
9
+
10
+ from .geometry2d import Bounds2D, Position2D, Size2D
11
+ from .physics2d import Velocity2D
12
+
13
+
14
+ class RectKinematic(Protocol):
15
+ """
16
+ Minimal contract for something that can bounce:
17
+ - position: Position2D
18
+ - size: Size2D
19
+ - velocity: Velocity2D
20
+
21
+ :ivar position (Position2D): Top-left position of the object.
22
+ :ivar size (Size2D): Size of the object.
23
+ :ivar velocity (Velocity2D): Velocity of the object.
24
+ """
25
+
26
+ position: Position2D
27
+ size: Size2D
28
+ velocity: Velocity2D
29
+
30
+
31
+ @dataclass
32
+ class VerticalBounce:
33
+ """
34
+ Bounce an object off the top/bottom bounds by inverting vy
35
+ and clamping its position inside the bounds.
36
+
37
+ :ivar bounds (Bounds2D): The boundary rectangle.
38
+ """
39
+
40
+ bounds: Bounds2D
41
+
42
+ def apply(self, obj: RectKinematic):
43
+ """
44
+ Apply vertical bounce to the given object.
45
+
46
+ :param obj: The object to apply the bounce to.
47
+ :type obj: RectKinematic
48
+ """
49
+ top = self.bounds.top
50
+ bottom = self.bounds.bottom
51
+
52
+ # Top collision
53
+ if obj.position.y <= top:
54
+ obj.position.y = top
55
+ obj.velocity.vy *= -1
56
+
57
+ # Bottom collision
58
+ if obj.position.y + obj.size.height >= bottom:
59
+ obj.position.y = bottom - obj.size.height
60
+ obj.velocity.vy *= -1
61
+
62
+
63
+ class RectSprite(Protocol):
64
+ """
65
+ Minimal contract for something that can wrap:
66
+ - position: Position2D
67
+ - size: Size2D
68
+ (no velocity required)
69
+
70
+ :ivar position (Position2D): Top-left position of the object.
71
+ :ivar size (Size2D): Size of the object.
72
+ """
73
+
74
+ position: Position2D
75
+ size: Size2D
76
+
77
+
78
+ @dataclass
79
+ class VerticalWrap:
80
+ """
81
+ Wrap an object top <-> bottom.
82
+
83
+ If it leaves above the top, it reappears at the bottom.
84
+ If it leaves below the bottom, it reappears at the top.
85
+
86
+ :ivar bounds (Bounds2D): The boundary rectangle.
87
+ """
88
+
89
+ bounds: Bounds2D
90
+
91
+ def apply(self, obj: RectSprite):
92
+ """
93
+ Apply vertical wrap to the given object.
94
+
95
+ :param obj: The object to apply the wrap to.
96
+ :type obj: RectSprite
97
+ """
98
+ top = self.bounds.top
99
+ bottom = self.bounds.bottom
100
+
101
+ # Completely above top → appear at bottom
102
+ if obj.position.y + obj.size.height < top:
103
+ obj.position.y = bottom
104
+
105
+ # Completely below bottom → appear at top
106
+ elif obj.position.y > bottom:
107
+ obj.position.y = top - obj.size.height
@@ -0,0 +1,71 @@
1
+ """
2
+ 2D collision detection helpers.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from .geometry2d import Position2D, Size2D
10
+
11
+
12
+ def _rects_intersect(
13
+ pos_a: Position2D,
14
+ size_a: Size2D,
15
+ pos_b: Position2D,
16
+ size_b: Size2D,
17
+ ) -> bool:
18
+ """
19
+ Low-level AABB check. Internal helper.
20
+
21
+ :param pos_a: Top-left position of rectangle A.
22
+ :type pos_a: Position2D
23
+
24
+ :param size_a: Size of rectangle A.
25
+ :type size_a: Size2D
26
+
27
+ :param pos_b: Top-left position of rectangle B.
28
+ :type pos_b: Position2D
29
+
30
+ :param size_b: Size of rectangle B.
31
+ :type size_b: Size2D
32
+
33
+ :return: True if the rectangles intersect.
34
+ :rtype: bool
35
+ """
36
+ return not (
37
+ pos_a.x + size_a.width < pos_b.x
38
+ or pos_a.x > pos_b.x + size_b.width
39
+ or pos_a.y + size_a.height < pos_b.y
40
+ or pos_a.y > pos_b.y + size_b.height
41
+ )
42
+
43
+
44
+ @dataclass
45
+ class RectCollider:
46
+ """
47
+ OOP collision helper that wraps a Position2D + Size2D pair.
48
+
49
+ It does NOT own the data – it just points to them. If the
50
+ entity moves (position changes), the collider “sees” it.
51
+
52
+ :ivar position (Position2D): Top-left position of the rectangle.
53
+ :ivar size (Size2D): Size of the rectangle.
54
+ """
55
+
56
+ position: Position2D
57
+ size: Size2D
58
+
59
+ def intersects(self, other: "RectCollider") -> bool:
60
+ """
61
+ High-level OOP method to check collision with another collider.
62
+
63
+ ;param other: The other rectangle collider.
64
+ :type other: RectCollider
65
+
66
+ :return: True if the rectangles intersect.
67
+ :rtype: bool
68
+ """
69
+ return _rects_intersect(
70
+ self.position, self.size, other.position, other.size
71
+ )
@@ -0,0 +1,68 @@
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 .collision2d import RectCollider
10
+ from .geometry2d import Position2D, Size2D
11
+ from .kinematics2d import KinematicData
12
+
13
+
14
+ class Entity:
15
+ """Entity base class for game objects."""
16
+
17
+ def update(self, dt: float):
18
+ """
19
+ Advance the entity state by ``dt`` seconds.
20
+
21
+ :param dt: Time delta in seconds.
22
+ :type dt: float
23
+ """
24
+
25
+ def draw(self, surface: Any):
26
+ """
27
+ Render the entity to the given surface.
28
+
29
+ :param surface: The surface to draw on.
30
+ :type surface: Any
31
+ """
32
+
33
+
34
+ class SpriteEntity(Entity):
35
+ """Entity with position and size."""
36
+
37
+ def __init__(self, position: Position2D, size: Size2D):
38
+ """
39
+ :param position: Top-left position of the entity.
40
+ :type position: Position2D
41
+
42
+ :param size: Size of the entity.
43
+ :type size: Size2D
44
+ """
45
+ self.position = Position2D(float(position.x), float(position.y))
46
+ self.size = Size2D(int(size.width), int(size.height))
47
+ self.collider = RectCollider(self.position, self.size)
48
+
49
+
50
+ class KinematicEntity(SpriteEntity):
51
+ """SpriteEntity with velocity-based movement."""
52
+
53
+ def __init__(self, kinematic_data: KinematicData):
54
+ """
55
+ :param kinematic_data: Kinematic data for the entity.
56
+ :type kinematic_data: KinematicData
57
+ """
58
+ super().__init__(
59
+ position=kinematic_data.position,
60
+ size=kinematic_data.size,
61
+ )
62
+
63
+ self.velocity = kinematic_data.velocity
64
+
65
+ def update(self, dt: float):
66
+ self.position.x, self.position.y = self.velocity.advance(
67
+ self.position.x, self.position.y, dt
68
+ )