mini-arcade-core 0.3.1__tar.gz → 0.8.0__tar.gz
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-0.3.1 → mini_arcade_core-0.8.0}/PKG-INFO +19 -18
- {mini_arcade_core-0.3.1 → mini_arcade_core-0.8.0}/README.md +17 -17
- {mini_arcade_core-0.3.1 → mini_arcade_core-0.8.0}/pyproject.toml +4 -2
- mini_arcade_core-0.8.0/src/mini_arcade_core/__init__.py +100 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/backend.py +173 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/boundaries2d.py +107 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/collision2d.py +71 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/entity.py +68 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/game.py +175 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/geometry2d.py +69 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/kinematics2d.py +78 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/physics2d.py +73 -0
- mini_arcade_core-0.8.0/src/mini_arcade_core/scene.py +141 -0
- mini_arcade_core-0.3.1/src/mini_arcade_core/__init__.py +0 -36
- mini_arcade_core-0.3.1/src/mini_arcade_core/backend.py +0 -75
- mini_arcade_core-0.3.1/src/mini_arcade_core/entity.py +0 -51
- mini_arcade_core-0.3.1/src/mini_arcade_core/game.py +0 -66
- mini_arcade_core-0.3.1/src/mini_arcade_core/scene.py +0 -40
- {mini_arcade_core-0.3.1 → mini_arcade_core-0.8.0}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mini-arcade-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Tiny scene-based game loop core for small arcade games.
|
|
5
5
|
License: Copyright (c) 2025 Santiago Rincón
|
|
6
6
|
|
|
@@ -34,6 +34,7 @@ Provides-Extra: dev
|
|
|
34
34
|
Requires-Dist: black (>=24.10,<25.0) ; extra == "dev"
|
|
35
35
|
Requires-Dist: isort (>=5.13,<6.0) ; extra == "dev"
|
|
36
36
|
Requires-Dist: mypy (>=1.5,<2.0) ; extra == "dev"
|
|
37
|
+
Requires-Dist: pillow (<12)
|
|
37
38
|
Requires-Dist: pylint (>=3.3,<4.0) ; extra == "dev"
|
|
38
39
|
Requires-Dist: pytest (>=8.3,<9.0) ; extra == "dev"
|
|
39
40
|
Requires-Dist: pytest-cov (>=6.0,<7.0) ; extra == "dev"
|
|
@@ -122,21 +123,21 @@ Represents one state of your game (menu, gameplay, pause, etc.):
|
|
|
122
123
|
from mini_arcade_core import Scene, Game
|
|
123
124
|
|
|
124
125
|
class MyScene(Scene):
|
|
125
|
-
def on_enter(self)
|
|
126
|
+
def on_enter(self):
|
|
126
127
|
print("Scene entered")
|
|
127
128
|
|
|
128
|
-
def on_exit(self)
|
|
129
|
+
def on_exit(self):
|
|
129
130
|
print("Scene exited")
|
|
130
131
|
|
|
131
|
-
def handle_event(self, event: object)
|
|
132
|
+
def handle_event(self, event: object):
|
|
132
133
|
# Handle input / events from your backend
|
|
133
134
|
pass
|
|
134
135
|
|
|
135
|
-
def update(self, dt: float)
|
|
136
|
+
def update(self, dt: float):
|
|
136
137
|
# Game logic
|
|
137
138
|
pass
|
|
138
139
|
|
|
139
|
-
def draw(self, surface: object)
|
|
140
|
+
def draw(self, surface: object):
|
|
140
141
|
# Rendering via your backend
|
|
141
142
|
pass
|
|
142
143
|
```
|
|
@@ -149,17 +150,17 @@ Lightweight game object primitives:
|
|
|
149
150
|
from mini_arcade_core import Entity, SpriteEntity
|
|
150
151
|
|
|
151
152
|
class Ball(Entity):
|
|
152
|
-
def __init__(self)
|
|
153
|
+
def __init__(self):
|
|
153
154
|
self.x = 100.0
|
|
154
155
|
self.y = 100.0
|
|
155
156
|
self.vx = 200.0
|
|
156
157
|
self.vy = 150.0
|
|
157
158
|
|
|
158
|
-
def update(self, dt: float)
|
|
159
|
+
def update(self, dt: float):
|
|
159
160
|
self.x += self.vx * dt
|
|
160
161
|
self.y += self.vy * dt
|
|
161
162
|
|
|
162
|
-
def draw(self, surface: object)
|
|
163
|
+
def draw(self, surface: object):
|
|
163
164
|
# Use your backend to draw the ball on `surface`
|
|
164
165
|
pass
|
|
165
166
|
|
|
@@ -181,7 +182,7 @@ from mini_arcade_core import Game, GameConfig, Scene
|
|
|
181
182
|
|
|
182
183
|
|
|
183
184
|
class PygameGame(Game):
|
|
184
|
-
def __init__(self, config: GameConfig)
|
|
185
|
+
def __init__(self, config: GameConfig):
|
|
185
186
|
super().__init__(config)
|
|
186
187
|
pygame.init()
|
|
187
188
|
self._screen = pygame.display.set_mode(
|
|
@@ -190,13 +191,13 @@ class PygameGame(Game):
|
|
|
190
191
|
pygame.display.set_caption(config.title)
|
|
191
192
|
self._clock = pygame.time.Clock()
|
|
192
193
|
|
|
193
|
-
def change_scene(self, scene: Scene)
|
|
194
|
+
def change_scene(self, scene: Scene):
|
|
194
195
|
if self._current_scene is not None:
|
|
195
196
|
self._current_scene.on_exit()
|
|
196
197
|
self._current_scene = scene
|
|
197
198
|
self._current_scene.on_enter()
|
|
198
199
|
|
|
199
|
-
def run(self, initial_scene: Scene)
|
|
200
|
+
def run(self, initial_scene: Scene):
|
|
200
201
|
self.change_scene(initial_scene)
|
|
201
202
|
self._running = True
|
|
202
203
|
|
|
@@ -219,7 +220,7 @@ class PygameGame(Game):
|
|
|
219
220
|
|
|
220
221
|
|
|
221
222
|
class PongScene(Scene):
|
|
222
|
-
def __init__(self, game: Game)
|
|
223
|
+
def __init__(self, game: Game):
|
|
223
224
|
super().__init__(game)
|
|
224
225
|
self.x = 100.0
|
|
225
226
|
self.y = 100.0
|
|
@@ -227,17 +228,17 @@ class PongScene(Scene):
|
|
|
227
228
|
self.vy = 150.0
|
|
228
229
|
self.radius = 10
|
|
229
230
|
|
|
230
|
-
def on_enter(self)
|
|
231
|
+
def on_enter(self):
|
|
231
232
|
print("Pong started")
|
|
232
233
|
|
|
233
|
-
def on_exit(self)
|
|
234
|
+
def on_exit(self):
|
|
234
235
|
print("Pong finished")
|
|
235
236
|
|
|
236
|
-
def handle_event(self, event: object)
|
|
237
|
+
def handle_event(self, event: object):
|
|
237
238
|
# no input yet
|
|
238
239
|
pass
|
|
239
240
|
|
|
240
|
-
def update(self, dt: float)
|
|
241
|
+
def update(self, dt: float):
|
|
241
242
|
self.x += self.vx * dt
|
|
242
243
|
self.y += self.vy * dt
|
|
243
244
|
|
|
@@ -249,7 +250,7 @@ class PongScene(Scene):
|
|
|
249
250
|
if self.y < self.radius or self.y > height - self.radius:
|
|
250
251
|
self.vy *= -1
|
|
251
252
|
|
|
252
|
-
def draw(self, surface: pygame.Surface)
|
|
253
|
+
def draw(self, surface: pygame.Surface): # type: ignore[override]
|
|
253
254
|
pygame.draw.circle(
|
|
254
255
|
surface, (255, 255, 255), (int(self.x), int(self.y)), self.radius
|
|
255
256
|
)
|
|
@@ -81,21 +81,21 @@ Represents one state of your game (menu, gameplay, pause, etc.):
|
|
|
81
81
|
from mini_arcade_core import Scene, Game
|
|
82
82
|
|
|
83
83
|
class MyScene(Scene):
|
|
84
|
-
def on_enter(self)
|
|
84
|
+
def on_enter(self):
|
|
85
85
|
print("Scene entered")
|
|
86
86
|
|
|
87
|
-
def on_exit(self)
|
|
87
|
+
def on_exit(self):
|
|
88
88
|
print("Scene exited")
|
|
89
89
|
|
|
90
|
-
def handle_event(self, event: object)
|
|
90
|
+
def handle_event(self, event: object):
|
|
91
91
|
# Handle input / events from your backend
|
|
92
92
|
pass
|
|
93
93
|
|
|
94
|
-
def update(self, dt: float)
|
|
94
|
+
def update(self, dt: float):
|
|
95
95
|
# Game logic
|
|
96
96
|
pass
|
|
97
97
|
|
|
98
|
-
def draw(self, surface: object)
|
|
98
|
+
def draw(self, surface: object):
|
|
99
99
|
# Rendering via your backend
|
|
100
100
|
pass
|
|
101
101
|
```
|
|
@@ -108,17 +108,17 @@ Lightweight game object primitives:
|
|
|
108
108
|
from mini_arcade_core import Entity, SpriteEntity
|
|
109
109
|
|
|
110
110
|
class Ball(Entity):
|
|
111
|
-
def __init__(self)
|
|
111
|
+
def __init__(self):
|
|
112
112
|
self.x = 100.0
|
|
113
113
|
self.y = 100.0
|
|
114
114
|
self.vx = 200.0
|
|
115
115
|
self.vy = 150.0
|
|
116
116
|
|
|
117
|
-
def update(self, dt: float)
|
|
117
|
+
def update(self, dt: float):
|
|
118
118
|
self.x += self.vx * dt
|
|
119
119
|
self.y += self.vy * dt
|
|
120
120
|
|
|
121
|
-
def draw(self, surface: object)
|
|
121
|
+
def draw(self, surface: object):
|
|
122
122
|
# Use your backend to draw the ball on `surface`
|
|
123
123
|
pass
|
|
124
124
|
|
|
@@ -140,7 +140,7 @@ from mini_arcade_core import Game, GameConfig, Scene
|
|
|
140
140
|
|
|
141
141
|
|
|
142
142
|
class PygameGame(Game):
|
|
143
|
-
def __init__(self, config: GameConfig)
|
|
143
|
+
def __init__(self, config: GameConfig):
|
|
144
144
|
super().__init__(config)
|
|
145
145
|
pygame.init()
|
|
146
146
|
self._screen = pygame.display.set_mode(
|
|
@@ -149,13 +149,13 @@ class PygameGame(Game):
|
|
|
149
149
|
pygame.display.set_caption(config.title)
|
|
150
150
|
self._clock = pygame.time.Clock()
|
|
151
151
|
|
|
152
|
-
def change_scene(self, scene: Scene)
|
|
152
|
+
def change_scene(self, scene: Scene):
|
|
153
153
|
if self._current_scene is not None:
|
|
154
154
|
self._current_scene.on_exit()
|
|
155
155
|
self._current_scene = scene
|
|
156
156
|
self._current_scene.on_enter()
|
|
157
157
|
|
|
158
|
-
def run(self, initial_scene: Scene)
|
|
158
|
+
def run(self, initial_scene: Scene):
|
|
159
159
|
self.change_scene(initial_scene)
|
|
160
160
|
self._running = True
|
|
161
161
|
|
|
@@ -178,7 +178,7 @@ class PygameGame(Game):
|
|
|
178
178
|
|
|
179
179
|
|
|
180
180
|
class PongScene(Scene):
|
|
181
|
-
def __init__(self, game: Game)
|
|
181
|
+
def __init__(self, game: Game):
|
|
182
182
|
super().__init__(game)
|
|
183
183
|
self.x = 100.0
|
|
184
184
|
self.y = 100.0
|
|
@@ -186,17 +186,17 @@ class PongScene(Scene):
|
|
|
186
186
|
self.vy = 150.0
|
|
187
187
|
self.radius = 10
|
|
188
188
|
|
|
189
|
-
def on_enter(self)
|
|
189
|
+
def on_enter(self):
|
|
190
190
|
print("Pong started")
|
|
191
191
|
|
|
192
|
-
def on_exit(self)
|
|
192
|
+
def on_exit(self):
|
|
193
193
|
print("Pong finished")
|
|
194
194
|
|
|
195
|
-
def handle_event(self, event: object)
|
|
195
|
+
def handle_event(self, event: object):
|
|
196
196
|
# no input yet
|
|
197
197
|
pass
|
|
198
198
|
|
|
199
|
-
def update(self, dt: float)
|
|
199
|
+
def update(self, dt: float):
|
|
200
200
|
self.x += self.vx * dt
|
|
201
201
|
self.y += self.vy * dt
|
|
202
202
|
|
|
@@ -208,7 +208,7 @@ class PongScene(Scene):
|
|
|
208
208
|
if self.y < self.radius or self.y > height - self.radius:
|
|
209
209
|
self.vy *= -1
|
|
210
210
|
|
|
211
|
-
def draw(self, surface: pygame.Surface)
|
|
211
|
+
def draw(self, surface: pygame.Surface): # type: ignore[override]
|
|
212
212
|
pygame.draw.circle(
|
|
213
213
|
surface, (255, 255, 255), (int(self.x), int(self.y)), self.radius
|
|
214
214
|
)
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mini-arcade-core"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Tiny scene-based game loop core for small arcade games."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Santiago Rincon", email = "rincores@gmail.com" },
|
|
@@ -12,7 +12,7 @@ authors = [
|
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = { file = "LICENSE" }
|
|
14
14
|
requires-python = ">=3.9,<3.12"
|
|
15
|
-
dependencies = []
|
|
15
|
+
dependencies = ["pillow (<12)"]
|
|
16
16
|
|
|
17
17
|
[project.optional-dependencies]
|
|
18
18
|
dev = [
|
|
@@ -57,6 +57,8 @@ strict = false
|
|
|
57
57
|
disable = [
|
|
58
58
|
# Justification: Allow classes with few public methods for simplicity.
|
|
59
59
|
"too-few-public-methods",
|
|
60
|
+
# Justification: f-strings are preferred for logging.
|
|
61
|
+
"logging-fstring-interpolation",
|
|
60
62
|
# Justification: TODOs are acceptable during development.
|
|
61
63
|
"fixme",
|
|
62
64
|
]
|
|
@@ -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,173 @@
|
|
|
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, 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
|
+
"""
|
|
24
|
+
|
|
25
|
+
UNKNOWN = auto()
|
|
26
|
+
QUIT = auto()
|
|
27
|
+
KEYDOWN = auto()
|
|
28
|
+
KEYUP = auto()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Event:
|
|
33
|
+
"""
|
|
34
|
+
Core event type.
|
|
35
|
+
|
|
36
|
+
For now we only care about:
|
|
37
|
+
- type: what happened
|
|
38
|
+
- key: integer key code (e.g. ESC = 27), or None if not applicable
|
|
39
|
+
|
|
40
|
+
:ivar type (EventType): The type of event.
|
|
41
|
+
:ivar key (int | None): The key code associated with the event, if any.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
type: EventType
|
|
45
|
+
key: int | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Backend(Protocol):
|
|
49
|
+
"""
|
|
50
|
+
Interface that any rendering/input backend must implement.
|
|
51
|
+
|
|
52
|
+
mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def init(self, width: int, height: int, title: str):
|
|
56
|
+
"""
|
|
57
|
+
Initialize the backend and open a window.
|
|
58
|
+
Should be called once before the main loop.
|
|
59
|
+
|
|
60
|
+
:param width: Width of the window in pixels.
|
|
61
|
+
:type width: int
|
|
62
|
+
|
|
63
|
+
:param height: Height of the window in pixels.
|
|
64
|
+
:type height: int
|
|
65
|
+
|
|
66
|
+
:param title: Title of the window.
|
|
67
|
+
:type title: str
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def poll_events(self) -> Iterable[Event]:
|
|
71
|
+
"""
|
|
72
|
+
Return all pending events since last call.
|
|
73
|
+
Concrete backends will translate their native events into core Event objects.
|
|
74
|
+
|
|
75
|
+
:return: An iterable of Event objects.
|
|
76
|
+
:rtype: Iterable[Event]
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def set_clear_color(self, r: int, g: int, b: int):
|
|
80
|
+
"""
|
|
81
|
+
Set the background/clear color used by begin_frame.
|
|
82
|
+
|
|
83
|
+
:param r: Red component (0-255).
|
|
84
|
+
:type r: int
|
|
85
|
+
|
|
86
|
+
:param g: Green component (0-255).
|
|
87
|
+
:type g: int
|
|
88
|
+
|
|
89
|
+
:param b: Blue component (0-255).
|
|
90
|
+
:type b: int
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def begin_frame(self):
|
|
94
|
+
"""
|
|
95
|
+
Prepare for drawing a new frame (e.g. clear screen).
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def end_frame(self):
|
|
99
|
+
"""
|
|
100
|
+
Present the frame to the user (swap buffers).
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
# Justification: Simple drawing API for now
|
|
104
|
+
# pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
105
|
+
def draw_rect(
|
|
106
|
+
self,
|
|
107
|
+
x: int,
|
|
108
|
+
y: int,
|
|
109
|
+
w: int,
|
|
110
|
+
h: int,
|
|
111
|
+
color: Color = (255, 255, 255),
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Draw a filled rectangle in some default color.
|
|
115
|
+
We'll keep this minimal for now; later we can extend with colors/sprites.
|
|
116
|
+
|
|
117
|
+
:param x: X position of the rectangle's top-left corner.
|
|
118
|
+
:type x: int
|
|
119
|
+
|
|
120
|
+
:param y: Y position of the rectangle's top-left corner.
|
|
121
|
+
:type y: int
|
|
122
|
+
|
|
123
|
+
:param w: Width of the rectangle.
|
|
124
|
+
:type w: int
|
|
125
|
+
|
|
126
|
+
:param h: Height of the rectangle.
|
|
127
|
+
:type h: int
|
|
128
|
+
|
|
129
|
+
:param color: RGB color tuple.
|
|
130
|
+
:type color: Color
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
# pylint: enable=too-many-arguments,too-many-positional-arguments
|
|
134
|
+
|
|
135
|
+
def draw_text(
|
|
136
|
+
self,
|
|
137
|
+
x: int,
|
|
138
|
+
y: int,
|
|
139
|
+
text: str,
|
|
140
|
+
color: Color = (255, 255, 255),
|
|
141
|
+
):
|
|
142
|
+
"""
|
|
143
|
+
Draw text at the given position in a default font and color.
|
|
144
|
+
|
|
145
|
+
Backends may ignore advanced styling for now; this is just to render
|
|
146
|
+
simple labels like menu items, scores, etc.
|
|
147
|
+
|
|
148
|
+
:param x: X position of the text's top-left corner.
|
|
149
|
+
:type x: int
|
|
150
|
+
|
|
151
|
+
:param y: Y position of the text's top-left corner.
|
|
152
|
+
:type y: int
|
|
153
|
+
|
|
154
|
+
:param text: The text string to draw.
|
|
155
|
+
:type text: str
|
|
156
|
+
|
|
157
|
+
:param color: RGB color tuple.
|
|
158
|
+
:type color: Color
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def capture_frame(self, path: str | None = None) -> bytes | None:
|
|
162
|
+
"""
|
|
163
|
+
Capture the current frame.
|
|
164
|
+
If `path` is provided, save to that file (e.g. PNG).
|
|
165
|
+
Returns raw bytes (PNG) or None if unsupported.
|
|
166
|
+
|
|
167
|
+
:param path: Optional file path to save the screenshot.
|
|
168
|
+
:type path: str | None
|
|
169
|
+
|
|
170
|
+
:return: Raw image bytes if no path given, else None.
|
|
171
|
+
:rtype: bytes | None
|
|
172
|
+
"""
|
|
173
|
+
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
|