mini-arcade-core 0.3.1__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/LICENSE +19 -0
- mini_arcade_core-0.3.1/PKG-INFO +287 -0
- mini_arcade_core-0.3.1/README.md +245 -0
- mini_arcade_core-0.3.1/pyproject.toml +76 -0
- mini_arcade_core-0.3.1/src/mini_arcade_core/__init__.py +36 -0
- mini_arcade_core-0.3.1/src/mini_arcade_core/backend.py +75 -0
- mini_arcade_core-0.3.1/src/mini_arcade_core/entity.py +51 -0
- mini_arcade_core-0.3.1/src/mini_arcade_core/game.py +66 -0
- mini_arcade_core-0.3.1/src/mini_arcade_core/scene.py +40 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2025 Santiago Rincón
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mini-arcade-core
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Tiny scene-based game loop core for small arcade games.
|
|
5
|
+
License: Copyright (c) 2025 Santiago Rincón
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Author: Santiago Rincon
|
|
26
|
+
Author-email: rincores@gmail.com
|
|
27
|
+
Requires-Python: >=3.9,<3.12
|
|
28
|
+
Classifier: License :: Other/Proprietary License
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: black (>=24.10,<25.0) ; extra == "dev"
|
|
35
|
+
Requires-Dist: isort (>=5.13,<6.0) ; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy (>=1.5,<2.0) ; extra == "dev"
|
|
37
|
+
Requires-Dist: pylint (>=3.3,<4.0) ; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest (>=8.3,<9.0) ; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-cov (>=6.0,<7.0) ; extra == "dev"
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# mini-arcade-core 🎮
|
|
43
|
+
|
|
44
|
+
Tiny Python game core for building simple scene-based arcade games
|
|
45
|
+
(Pong, Breakout, Space Invaders, etc.).
|
|
46
|
+
|
|
47
|
+
> Minimal, opinionated abstractions: **Game**, **Scene**, and **Entity** – nothing else.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- 🎯 **Tiny API surface**
|
|
54
|
+
- `GameConfig` – basic window & FPS configuration
|
|
55
|
+
- `Game` – abstract game core to plug your own backend (e.g. pygame)
|
|
56
|
+
- `Scene` – base class for screens/states (menus, gameplay, pause)
|
|
57
|
+
- `Entity` / `SpriteEntity` – simple game object primitives
|
|
58
|
+
- `run_game()` – convenience helper once a concrete `Game` backend is wired
|
|
59
|
+
|
|
60
|
+
- 🧩 **Backend-agnostic**
|
|
61
|
+
- The core doesn’t depend on any specific rendering/input library.
|
|
62
|
+
- You can build backends using `pygame`, `pyglet`, or something custom.
|
|
63
|
+
|
|
64
|
+
- 🕹️ **Perfect for small arcade projects**
|
|
65
|
+
- Pong, Breakout, Snake, Asteroids-likes, runners, flappy-likes, etc.
|
|
66
|
+
- Great for learning, experiments, and portfolio-friendly mini games.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
> **Note:** Adjust this once it’s on PyPI.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# From a local checkout
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or, once published:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install mini-arcade-core
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Requires Python 3.9–3.11.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Core Concepts
|
|
90
|
+
|
|
91
|
+
### ``GameConfig``
|
|
92
|
+
|
|
93
|
+
Basic configuration for your game:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from mini_arcade_core import GameConfig
|
|
97
|
+
|
|
98
|
+
config = GameConfig(
|
|
99
|
+
width=800,
|
|
100
|
+
height=600,
|
|
101
|
+
title="My Mini Arcade Game",
|
|
102
|
+
fps=60,
|
|
103
|
+
background_color=(0, 0, 0), # RGB
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### ``Game``
|
|
108
|
+
|
|
109
|
+
Abstract base class that owns:
|
|
110
|
+
|
|
111
|
+
- the main loop
|
|
112
|
+
- the active ``Scene``
|
|
113
|
+
- high-level control like ``run()`` and ``change_scene()``
|
|
114
|
+
|
|
115
|
+
You subclass ``Game`` to plug in your rendering/input backend.
|
|
116
|
+
|
|
117
|
+
### ``Scene``
|
|
118
|
+
|
|
119
|
+
Represents one state of your game (menu, gameplay, pause, etc.):
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from mini_arcade_core import Scene, Game
|
|
123
|
+
|
|
124
|
+
class MyScene(Scene):
|
|
125
|
+
def on_enter(self) -> None:
|
|
126
|
+
print("Scene entered")
|
|
127
|
+
|
|
128
|
+
def on_exit(self) -> None:
|
|
129
|
+
print("Scene exited")
|
|
130
|
+
|
|
131
|
+
def handle_event(self, event: object) -> None:
|
|
132
|
+
# Handle input / events from your backend
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def update(self, dt: float) -> None:
|
|
136
|
+
# Game logic
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
def draw(self, surface: object) -> None:
|
|
140
|
+
# Rendering via your backend
|
|
141
|
+
pass
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### ``Entity`` & ``SpriteEntity``
|
|
145
|
+
|
|
146
|
+
Lightweight game object primitives:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from mini_arcade_core import Entity, SpriteEntity
|
|
150
|
+
|
|
151
|
+
class Ball(Entity):
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
self.x = 100.0
|
|
154
|
+
self.y = 100.0
|
|
155
|
+
self.vx = 200.0
|
|
156
|
+
self.vy = 150.0
|
|
157
|
+
|
|
158
|
+
def update(self, dt: float) -> None:
|
|
159
|
+
self.x += self.vx * dt
|
|
160
|
+
self.y += self.vy * dt
|
|
161
|
+
|
|
162
|
+
def draw(self, surface: object) -> None:
|
|
163
|
+
# Use your backend to draw the ball on `surface`
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
paddle = SpriteEntity(x=50.0, y=300.0, width=80, height=16)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Example: Minimal pygame Backend
|
|
172
|
+
|
|
173
|
+
``mini-arcade-core`` doesn’t force any backend.
|
|
174
|
+
Here’s a minimal example using pygame as a backend:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# example_pygame_game.py
|
|
178
|
+
|
|
179
|
+
import pygame
|
|
180
|
+
from mini_arcade_core import Game, GameConfig, Scene
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class PygameGame(Game):
|
|
184
|
+
def __init__(self, config: GameConfig) -> None:
|
|
185
|
+
super().__init__(config)
|
|
186
|
+
pygame.init()
|
|
187
|
+
self._screen = pygame.display.set_mode(
|
|
188
|
+
(config.width, config.height)
|
|
189
|
+
)
|
|
190
|
+
pygame.display.set_caption(config.title)
|
|
191
|
+
self._clock = pygame.time.Clock()
|
|
192
|
+
|
|
193
|
+
def change_scene(self, scene: Scene) -> None:
|
|
194
|
+
if self._current_scene is not None:
|
|
195
|
+
self._current_scene.on_exit()
|
|
196
|
+
self._current_scene = scene
|
|
197
|
+
self._current_scene.on_enter()
|
|
198
|
+
|
|
199
|
+
def run(self, initial_scene: Scene) -> None:
|
|
200
|
+
self.change_scene(initial_scene)
|
|
201
|
+
self._running = True
|
|
202
|
+
|
|
203
|
+
while self._running:
|
|
204
|
+
dt = self._clock.tick(self.config.fps) / 1000.0
|
|
205
|
+
|
|
206
|
+
for event in pygame.event.get():
|
|
207
|
+
if event.type == pygame.QUIT:
|
|
208
|
+
self._running = False
|
|
209
|
+
elif self._current_scene is not None:
|
|
210
|
+
self._current_scene.handle_event(event)
|
|
211
|
+
|
|
212
|
+
if self._current_scene is not None:
|
|
213
|
+
self._current_scene.update(dt)
|
|
214
|
+
self._screen.fill(self.config.background_color)
|
|
215
|
+
self._current_scene.draw(self._screen)
|
|
216
|
+
pygame.display.flip()
|
|
217
|
+
|
|
218
|
+
pygame.quit()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class PongScene(Scene):
|
|
222
|
+
def __init__(self, game: Game) -> None:
|
|
223
|
+
super().__init__(game)
|
|
224
|
+
self.x = 100.0
|
|
225
|
+
self.y = 100.0
|
|
226
|
+
self.vx = 200.0
|
|
227
|
+
self.vy = 150.0
|
|
228
|
+
self.radius = 10
|
|
229
|
+
|
|
230
|
+
def on_enter(self) -> None:
|
|
231
|
+
print("Pong started")
|
|
232
|
+
|
|
233
|
+
def on_exit(self) -> None:
|
|
234
|
+
print("Pong finished")
|
|
235
|
+
|
|
236
|
+
def handle_event(self, event: object) -> None:
|
|
237
|
+
# no input yet
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
def update(self, dt: float) -> None:
|
|
241
|
+
self.x += self.vx * dt
|
|
242
|
+
self.y += self.vy * dt
|
|
243
|
+
|
|
244
|
+
width = self.game.config.width
|
|
245
|
+
height = self.game.config.height
|
|
246
|
+
|
|
247
|
+
if self.x < self.radius or self.x > width - self.radius:
|
|
248
|
+
self.vx *= -1
|
|
249
|
+
if self.y < self.radius or self.y > height - self.radius:
|
|
250
|
+
self.vy *= -1
|
|
251
|
+
|
|
252
|
+
def draw(self, surface: pygame.Surface) -> None: # type: ignore[override]
|
|
253
|
+
pygame.draw.circle(
|
|
254
|
+
surface, (255, 255, 255), (int(self.x), int(self.y)), self.radius
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
cfg = GameConfig(width=640, height=360, title="Mini Arcade - Pong")
|
|
260
|
+
game = PygameGame(cfg)
|
|
261
|
+
scene = PongScene(game)
|
|
262
|
+
game.run(scene)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Once you have a shared backend like PygameGame in its own package (or inside your game repo), you can also wire run_game() to use it instead of the abstract Game.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Testing
|
|
270
|
+
|
|
271
|
+
This project uses pytest for tests.
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
pip install -e ".[dev]"
|
|
275
|
+
pytest
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Roadmap
|
|
279
|
+
|
|
280
|
+
[ ] First concrete backend (e.g. ``mini-arcade-pygame``)
|
|
281
|
+
[ ] Example games: Pong, Breakout, Snake, Asteroids-lite, Endless Runner
|
|
282
|
+
[ ] Packaging the example games as separate repos using this core
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
 — feel free to use this as a learning tool, or as a base for your own mini arcade projects.
|
|
287
|
+
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# mini-arcade-core 🎮
|
|
2
|
+
|
|
3
|
+
Tiny Python game core for building simple scene-based arcade games
|
|
4
|
+
(Pong, Breakout, Space Invaders, etc.).
|
|
5
|
+
|
|
6
|
+
> Minimal, opinionated abstractions: **Game**, **Scene**, and **Entity** – nothing else.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- 🎯 **Tiny API surface**
|
|
13
|
+
- `GameConfig` – basic window & FPS configuration
|
|
14
|
+
- `Game` – abstract game core to plug your own backend (e.g. pygame)
|
|
15
|
+
- `Scene` – base class for screens/states (menus, gameplay, pause)
|
|
16
|
+
- `Entity` / `SpriteEntity` – simple game object primitives
|
|
17
|
+
- `run_game()` – convenience helper once a concrete `Game` backend is wired
|
|
18
|
+
|
|
19
|
+
- 🧩 **Backend-agnostic**
|
|
20
|
+
- The core doesn’t depend on any specific rendering/input library.
|
|
21
|
+
- You can build backends using `pygame`, `pyglet`, or something custom.
|
|
22
|
+
|
|
23
|
+
- 🕹️ **Perfect for small arcade projects**
|
|
24
|
+
- Pong, Breakout, Snake, Asteroids-likes, runners, flappy-likes, etc.
|
|
25
|
+
- Great for learning, experiments, and portfolio-friendly mini games.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
> **Note:** Adjust this once it’s on PyPI.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# From a local checkout
|
|
35
|
+
pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or, once published:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install mini-arcade-core
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.9–3.11.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Core Concepts
|
|
49
|
+
|
|
50
|
+
### ``GameConfig``
|
|
51
|
+
|
|
52
|
+
Basic configuration for your game:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from mini_arcade_core import GameConfig
|
|
56
|
+
|
|
57
|
+
config = GameConfig(
|
|
58
|
+
width=800,
|
|
59
|
+
height=600,
|
|
60
|
+
title="My Mini Arcade Game",
|
|
61
|
+
fps=60,
|
|
62
|
+
background_color=(0, 0, 0), # RGB
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### ``Game``
|
|
67
|
+
|
|
68
|
+
Abstract base class that owns:
|
|
69
|
+
|
|
70
|
+
- the main loop
|
|
71
|
+
- the active ``Scene``
|
|
72
|
+
- high-level control like ``run()`` and ``change_scene()``
|
|
73
|
+
|
|
74
|
+
You subclass ``Game`` to plug in your rendering/input backend.
|
|
75
|
+
|
|
76
|
+
### ``Scene``
|
|
77
|
+
|
|
78
|
+
Represents one state of your game (menu, gameplay, pause, etc.):
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from mini_arcade_core import Scene, Game
|
|
82
|
+
|
|
83
|
+
class MyScene(Scene):
|
|
84
|
+
def on_enter(self) -> None:
|
|
85
|
+
print("Scene entered")
|
|
86
|
+
|
|
87
|
+
def on_exit(self) -> None:
|
|
88
|
+
print("Scene exited")
|
|
89
|
+
|
|
90
|
+
def handle_event(self, event: object) -> None:
|
|
91
|
+
# Handle input / events from your backend
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def update(self, dt: float) -> None:
|
|
95
|
+
# Game logic
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
def draw(self, surface: object) -> None:
|
|
99
|
+
# Rendering via your backend
|
|
100
|
+
pass
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### ``Entity`` & ``SpriteEntity``
|
|
104
|
+
|
|
105
|
+
Lightweight game object primitives:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from mini_arcade_core import Entity, SpriteEntity
|
|
109
|
+
|
|
110
|
+
class Ball(Entity):
|
|
111
|
+
def __init__(self) -> None:
|
|
112
|
+
self.x = 100.0
|
|
113
|
+
self.y = 100.0
|
|
114
|
+
self.vx = 200.0
|
|
115
|
+
self.vy = 150.0
|
|
116
|
+
|
|
117
|
+
def update(self, dt: float) -> None:
|
|
118
|
+
self.x += self.vx * dt
|
|
119
|
+
self.y += self.vy * dt
|
|
120
|
+
|
|
121
|
+
def draw(self, surface: object) -> None:
|
|
122
|
+
# Use your backend to draw the ball on `surface`
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
paddle = SpriteEntity(x=50.0, y=300.0, width=80, height=16)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### Example: Minimal pygame Backend
|
|
131
|
+
|
|
132
|
+
``mini-arcade-core`` doesn’t force any backend.
|
|
133
|
+
Here’s a minimal example using pygame as a backend:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# example_pygame_game.py
|
|
137
|
+
|
|
138
|
+
import pygame
|
|
139
|
+
from mini_arcade_core import Game, GameConfig, Scene
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class PygameGame(Game):
|
|
143
|
+
def __init__(self, config: GameConfig) -> None:
|
|
144
|
+
super().__init__(config)
|
|
145
|
+
pygame.init()
|
|
146
|
+
self._screen = pygame.display.set_mode(
|
|
147
|
+
(config.width, config.height)
|
|
148
|
+
)
|
|
149
|
+
pygame.display.set_caption(config.title)
|
|
150
|
+
self._clock = pygame.time.Clock()
|
|
151
|
+
|
|
152
|
+
def change_scene(self, scene: Scene) -> None:
|
|
153
|
+
if self._current_scene is not None:
|
|
154
|
+
self._current_scene.on_exit()
|
|
155
|
+
self._current_scene = scene
|
|
156
|
+
self._current_scene.on_enter()
|
|
157
|
+
|
|
158
|
+
def run(self, initial_scene: Scene) -> None:
|
|
159
|
+
self.change_scene(initial_scene)
|
|
160
|
+
self._running = True
|
|
161
|
+
|
|
162
|
+
while self._running:
|
|
163
|
+
dt = self._clock.tick(self.config.fps) / 1000.0
|
|
164
|
+
|
|
165
|
+
for event in pygame.event.get():
|
|
166
|
+
if event.type == pygame.QUIT:
|
|
167
|
+
self._running = False
|
|
168
|
+
elif self._current_scene is not None:
|
|
169
|
+
self._current_scene.handle_event(event)
|
|
170
|
+
|
|
171
|
+
if self._current_scene is not None:
|
|
172
|
+
self._current_scene.update(dt)
|
|
173
|
+
self._screen.fill(self.config.background_color)
|
|
174
|
+
self._current_scene.draw(self._screen)
|
|
175
|
+
pygame.display.flip()
|
|
176
|
+
|
|
177
|
+
pygame.quit()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PongScene(Scene):
|
|
181
|
+
def __init__(self, game: Game) -> None:
|
|
182
|
+
super().__init__(game)
|
|
183
|
+
self.x = 100.0
|
|
184
|
+
self.y = 100.0
|
|
185
|
+
self.vx = 200.0
|
|
186
|
+
self.vy = 150.0
|
|
187
|
+
self.radius = 10
|
|
188
|
+
|
|
189
|
+
def on_enter(self) -> None:
|
|
190
|
+
print("Pong started")
|
|
191
|
+
|
|
192
|
+
def on_exit(self) -> None:
|
|
193
|
+
print("Pong finished")
|
|
194
|
+
|
|
195
|
+
def handle_event(self, event: object) -> None:
|
|
196
|
+
# no input yet
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
def update(self, dt: float) -> None:
|
|
200
|
+
self.x += self.vx * dt
|
|
201
|
+
self.y += self.vy * dt
|
|
202
|
+
|
|
203
|
+
width = self.game.config.width
|
|
204
|
+
height = self.game.config.height
|
|
205
|
+
|
|
206
|
+
if self.x < self.radius or self.x > width - self.radius:
|
|
207
|
+
self.vx *= -1
|
|
208
|
+
if self.y < self.radius or self.y > height - self.radius:
|
|
209
|
+
self.vy *= -1
|
|
210
|
+
|
|
211
|
+
def draw(self, surface: pygame.Surface) -> None: # type: ignore[override]
|
|
212
|
+
pygame.draw.circle(
|
|
213
|
+
surface, (255, 255, 255), (int(self.x), int(self.y)), self.radius
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
cfg = GameConfig(width=640, height=360, title="Mini Arcade - Pong")
|
|
219
|
+
game = PygameGame(cfg)
|
|
220
|
+
scene = PongScene(game)
|
|
221
|
+
game.run(scene)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Once you have a shared backend like PygameGame in its own package (or inside your game repo), you can also wire run_game() to use it instead of the abstract Game.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Testing
|
|
229
|
+
|
|
230
|
+
This project uses pytest for tests.
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
pip install -e ".[dev]"
|
|
234
|
+
pytest
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Roadmap
|
|
238
|
+
|
|
239
|
+
[ ] First concrete backend (e.g. ``mini-arcade-pygame``)
|
|
240
|
+
[ ] Example games: Pong, Breakout, Snake, Asteroids-lite, Endless Runner
|
|
241
|
+
[ ] Packaging the example games as separate repos using this core
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
 — feel free to use this as a learning tool, or as a base for your own mini arcade projects.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mini-arcade-core"
|
|
7
|
+
version = "0.3.1"
|
|
8
|
+
description = "Tiny scene-based game loop core for small arcade games."
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Santiago Rincon", email = "rincores@gmail.com" },
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
requires-python = ">=3.9,<3.12"
|
|
15
|
+
dependencies = []
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest~=8.3",
|
|
20
|
+
"pytest-cov~=6.0",
|
|
21
|
+
"black~=24.10",
|
|
22
|
+
"isort~=5.13",
|
|
23
|
+
"mypy~=1.5",
|
|
24
|
+
"pylint~=3.3",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.black]
|
|
28
|
+
line-length = 79
|
|
29
|
+
target-version = ["py39"]
|
|
30
|
+
force-exclude = '''(
|
|
31
|
+
^\.venv
|
|
32
|
+
|^venv
|
|
33
|
+
|^env
|
|
34
|
+
|^\.env
|
|
35
|
+
|^build
|
|
36
|
+
|^dist
|
|
37
|
+
|^docs
|
|
38
|
+
)'''
|
|
39
|
+
|
|
40
|
+
[tool.isort]
|
|
41
|
+
profile = "black"
|
|
42
|
+
line_length = 79
|
|
43
|
+
known_first_party = ["mini_arcade_core"]
|
|
44
|
+
skip = [".venv", "venv", "env", "build", "dist", "docs"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
addopts = "-v --color=yes"
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
python_version = "3.9"
|
|
52
|
+
packages = ["mini_arcade_core"]
|
|
53
|
+
ignore_missing_imports = true
|
|
54
|
+
strict = false
|
|
55
|
+
|
|
56
|
+
[tool.pylint.messages_control]
|
|
57
|
+
disable = [
|
|
58
|
+
# Justification: Allow classes with few public methods for simplicity.
|
|
59
|
+
"too-few-public-methods",
|
|
60
|
+
# Justification: TODOs are acceptable during development.
|
|
61
|
+
"fixme",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pylint.MAIN]
|
|
65
|
+
ignore-paths = [
|
|
66
|
+
"^\\.venv",
|
|
67
|
+
"^venv",
|
|
68
|
+
"^tests",
|
|
69
|
+
"^env",
|
|
70
|
+
"^build",
|
|
71
|
+
"^dist",
|
|
72
|
+
"^docs",
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.poetry]
|
|
76
|
+
packages = [{ include = "mini_arcade_core", from = "src" }]
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
from .backend import Backend, Event, EventType
|
|
9
|
+
from .entity import Entity, SpriteEntity
|
|
10
|
+
from .game import Game, GameConfig
|
|
11
|
+
from .scene import Scene
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_game(initial_scene_cls: type[Scene], config: GameConfig | None = None):
|
|
15
|
+
"""
|
|
16
|
+
Convenience helper to bootstrap and run a game with a single scene.
|
|
17
|
+
|
|
18
|
+
:param initial_scene_cls: The Scene subclass to instantiate as the initial scene.
|
|
19
|
+
:param config: Optional GameConfig to customize game settings.
|
|
20
|
+
"""
|
|
21
|
+
game = Game(config or GameConfig())
|
|
22
|
+
scene = initial_scene_cls(game)
|
|
23
|
+
game.run(scene)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Game",
|
|
28
|
+
"GameConfig",
|
|
29
|
+
"Scene",
|
|
30
|
+
"Entity",
|
|
31
|
+
"SpriteEntity",
|
|
32
|
+
"run_game",
|
|
33
|
+
"Backend",
|
|
34
|
+
"Event",
|
|
35
|
+
"EventType",
|
|
36
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventType(Enum):
|
|
14
|
+
"""High-level event types understood by the core."""
|
|
15
|
+
|
|
16
|
+
UNKNOWN = auto()
|
|
17
|
+
QUIT = auto()
|
|
18
|
+
KEYDOWN = auto()
|
|
19
|
+
KEYUP = auto()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Event:
|
|
24
|
+
"""
|
|
25
|
+
Core event type.
|
|
26
|
+
|
|
27
|
+
For now we only care about:
|
|
28
|
+
- type: what happened
|
|
29
|
+
- key: integer key code (e.g. ESC = 27), or None if not applicable
|
|
30
|
+
|
|
31
|
+
:ivar type (EventType): The type of event.
|
|
32
|
+
:ivar key (int | None): The key code associated with the event, if any.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
type: EventType
|
|
36
|
+
key: int | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Backend(Protocol):
|
|
40
|
+
"""
|
|
41
|
+
Interface that any rendering/input backend must implement.
|
|
42
|
+
|
|
43
|
+
mini-arcade-core only talks to this protocol, never to SDL/pygame directly.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def init(self, width: int, height: int, title: str) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Initialize the backend and open a window.
|
|
49
|
+
|
|
50
|
+
Should be called once before the main loop.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def poll_events(self) -> Iterable[Event]:
|
|
54
|
+
"""
|
|
55
|
+
Return all pending events since last call.
|
|
56
|
+
|
|
57
|
+
Concrete backends will translate their native events into core Event objects.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def begin_frame(self) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Prepare for drawing a new frame (e.g. clear screen).
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def end_frame(self) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Present the frame to the user (swap buffers).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def draw_rect(self, x: int, y: int, w: int, h: int) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Draw a filled rectangle in some default color.
|
|
73
|
+
|
|
74
|
+
We'll keep this minimal for now; later we can extend with colors/sprites.
|
|
75
|
+
"""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entity base classes for mini_arcade_core.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Entity:
|
|
11
|
+
"""Entity base class for game objects."""
|
|
12
|
+
|
|
13
|
+
def update(self, dt: float):
|
|
14
|
+
"""
|
|
15
|
+
Advance the entity state by ``dt`` seconds.
|
|
16
|
+
|
|
17
|
+
:param dt: Time delta in seconds.
|
|
18
|
+
:type dt: float
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def draw(self, surface: Any):
|
|
22
|
+
"""
|
|
23
|
+
Render the entity to the given surface.
|
|
24
|
+
|
|
25
|
+
:param surface: The surface to draw on.
|
|
26
|
+
:type surface: Any
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SpriteEntity(Entity):
|
|
31
|
+
"""Entity with position and size."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, x: float, y: float, width: int, height: int):
|
|
34
|
+
"""
|
|
35
|
+
:param x: X position.
|
|
36
|
+
:type x: float
|
|
37
|
+
|
|
38
|
+
:param y: Y position.
|
|
39
|
+
:type y: float
|
|
40
|
+
|
|
41
|
+
:param width: Width of the entity.
|
|
42
|
+
:type width: int
|
|
43
|
+
|
|
44
|
+
:param height: Height of the entity.
|
|
45
|
+
:type height: int
|
|
46
|
+
"""
|
|
47
|
+
self.x = x
|
|
48
|
+
self.y = y
|
|
49
|
+
self.width = width
|
|
50
|
+
self.height = height
|
|
51
|
+
# TODO: velocity, color, etc.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game core module defining the Game class and configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING: # avoid runtime circular import
|
|
11
|
+
from .scene import Scene
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GameConfig:
|
|
16
|
+
"""
|
|
17
|
+
Configuration options for the Game.
|
|
18
|
+
|
|
19
|
+
:ivar width: Width of the game window in pixels.
|
|
20
|
+
:ivar height: Height of the game window in pixels.
|
|
21
|
+
:ivar title: Title of the game window.
|
|
22
|
+
:ivar fps: Target frames per second.
|
|
23
|
+
:ivar background_color: RGB background color.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
width: int = 800
|
|
27
|
+
height: int = 600
|
|
28
|
+
title: str = "Mini Arcade Game"
|
|
29
|
+
fps: int = 60
|
|
30
|
+
background_color: tuple[int, int, int] = (0, 0, 0)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Game:
|
|
34
|
+
"""Core game object responsible for managing the main loop and active scene."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, config: GameConfig):
|
|
37
|
+
"""
|
|
38
|
+
:param config: Game configuration options.
|
|
39
|
+
"""
|
|
40
|
+
self.config = config
|
|
41
|
+
self._current_scene: Scene | None = None
|
|
42
|
+
self._running: bool = False
|
|
43
|
+
|
|
44
|
+
def change_scene(self, scene: Scene):
|
|
45
|
+
"""
|
|
46
|
+
Swap the active scene. Concrete implementations should call
|
|
47
|
+
``on_exit``/``on_enter`` appropriately.
|
|
48
|
+
|
|
49
|
+
:param scene: The new scene to activate.
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError(
|
|
52
|
+
"Game.change_scene must be implemented by a concrete backend."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def run(self, initial_scene: Scene):
|
|
56
|
+
"""
|
|
57
|
+
Run the main loop starting with the given scene.
|
|
58
|
+
|
|
59
|
+
This is intentionally left abstract so you can plug pygame, pyglet,
|
|
60
|
+
or another backend.
|
|
61
|
+
|
|
62
|
+
:param initial_scene: The scene to start the game with.
|
|
63
|
+
"""
|
|
64
|
+
raise NotImplementedError(
|
|
65
|
+
"Game.run must be implemented by a concrete backend."
|
|
66
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for game scenes (states/screens).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
|
|
9
|
+
from .game import Game
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Scene(ABC):
|
|
13
|
+
"""Base class for game scenes (states/screens)."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, game: Game):
|
|
16
|
+
"""
|
|
17
|
+
:param game: Reference to the main Game object.
|
|
18
|
+
:type game: Game
|
|
19
|
+
"""
|
|
20
|
+
self.game = game
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def on_enter(self):
|
|
24
|
+
"""Called when the scene becomes active."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def on_exit(self):
|
|
28
|
+
"""Called when the scene is replaced."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def handle_event(self, event: object):
|
|
32
|
+
"""Handle input / events (e.g. pygame.Event)."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def update(self, dt: float):
|
|
36
|
+
"""Update game logic. ``dt`` is the delta time in seconds."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def draw(self, surface: object):
|
|
40
|
+
"""Render to the main surface."""
|