system-b 1.1.4__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.
Files changed (36) hide show
  1. system_b-1.1.4/LICENSE +21 -0
  2. system_b-1.1.4/PKG-INFO +41 -0
  3. system_b-1.1.4/README.md +25 -0
  4. system_b-1.1.4/pyproject.toml +25 -0
  5. system_b-1.1.4/setup.cfg +4 -0
  6. system_b-1.1.4/src/system_b/__init__.py +0 -0
  7. system_b-1.1.4/src/system_b/controller/__init__.py +0 -0
  8. system_b-1.1.4/src/system_b/controller/controller.py +74 -0
  9. system_b-1.1.4/src/system_b/demo/__init__.py +0 -0
  10. system_b-1.1.4/src/system_b/demo/__main__.py +4 -0
  11. system_b-1.1.4/src/system_b/demo/demo.py +268 -0
  12. system_b-1.1.4/src/system_b/exceptions/__init__.py +0 -0
  13. system_b-1.1.4/src/system_b/exceptions/game_exception.py +4 -0
  14. system_b-1.1.4/src/system_b/game/__init__.py +4 -0
  15. system_b-1.1.4/src/system_b/game/game.py +82 -0
  16. system_b-1.1.4/src/system_b/images/__init__.py +5 -0
  17. system_b-1.1.4/src/system_b/images/images.py +100 -0
  18. system_b-1.1.4/src/system_b/math/__init__.py +0 -0
  19. system_b-1.1.4/src/system_b/math/vector_utils.py +23 -0
  20. system_b-1.1.4/src/system_b/physics/__init__.py +1 -0
  21. system_b-1.1.4/src/system_b/physics/physics.py +113 -0
  22. system_b-1.1.4/src/system_b/screen/__init__.py +1 -0
  23. system_b-1.1.4/src/system_b/screen/screen.py +46 -0
  24. system_b-1.1.4/src/system_b/sprites/__init__.py +4 -0
  25. system_b-1.1.4/src/system_b/sprites/sprites.py +110 -0
  26. system_b-1.1.4/src/system_b/toolkit/__init__.py +1 -0
  27. system_b-1.1.4/src/system_b/toolkit/__main__.py +6 -0
  28. system_b-1.1.4/src/system_b/toolkit/toolkit.py +19 -0
  29. system_b-1.1.4/src/system_b.egg-info/PKG-INFO +41 -0
  30. system_b-1.1.4/src/system_b.egg-info/SOURCES.txt +34 -0
  31. system_b-1.1.4/src/system_b.egg-info/dependency_links.txt +1 -0
  32. system_b-1.1.4/src/system_b.egg-info/requires.txt +2 -0
  33. system_b-1.1.4/src/system_b.egg-info/top_level.txt +1 -0
  34. system_b-1.1.4/tests/test_game.py +37 -0
  35. system_b-1.1.4/tests/test_images.py +35 -0
  36. system_b-1.1.4/tests/test_physics.py +77 -0
system_b-1.1.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Bee Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: system-b
3
+ Version: 1.1.4
4
+ Summary: A pygame engine for common game operations like vector math and maintaining events in the game loop
5
+ Author-email: Bee Hall <bhall1224@gmail.com>
6
+ Project-URL: Homepage, https://gitlab.com/bhall/game-utils
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: <=3.13
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pygame
14
+ Requires-Dist: numpy
15
+ Dynamic: license-file
16
+
17
+ # Game Utils
18
+
19
+ >1.1.4
20
+
21
+ ## `game-utils` is a pygame engine. The engine includes modules to facilitate boilerplate game operations.
22
+
23
+ ### Requirements
24
+
25
+ **Dependences:**
26
+ - pygame
27
+ - numpy
28
+
29
+ **Demo Game**
30
+
31
+ Run: `python -m game_utils.demo` or `python -m game_utils.toolkit --demo` to play the demo bouncy ball game!
32
+
33
+ Run: `python -m game_utils.toolkit --copy` to copy the sample project to your directory!
34
+
35
+ ---
36
+
37
+ ## See Also...
38
+
39
+ <p><b>My ongoing game projects!</b></p>
40
+
41
+ - [GitLab](https://gitlab.com/bhall)
@@ -0,0 +1,25 @@
1
+ # Game Utils
2
+
3
+ >1.1.4
4
+
5
+ ## `game-utils` is a pygame engine. The engine includes modules to facilitate boilerplate game operations.
6
+
7
+ ### Requirements
8
+
9
+ **Dependences:**
10
+ - pygame
11
+ - numpy
12
+
13
+ **Demo Game**
14
+
15
+ Run: `python -m game_utils.demo` or `python -m game_utils.toolkit --demo` to play the demo bouncy ball game!
16
+
17
+ Run: `python -m game_utils.toolkit --copy` to copy the sample project to your directory!
18
+
19
+ ---
20
+
21
+ ## See Also...
22
+
23
+ <p><b>My ongoing game projects!</b></p>
24
+
25
+ - [GitLab](https://gitlab.com/bhall)
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "system-b"
3
+ version = "1.1.4"
4
+ authors = [
5
+ { name="Bee Hall", email="bhall1224@gmail.com" },
6
+ ]
7
+ description = "A pygame engine for common game operations like vector math and maintaining events in the game loop"
8
+ readme = "README.md"
9
+ requires-python = "<=3.13"
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ dependencies = [
16
+ "pygame",
17
+ "numpy"
18
+ ]
19
+
20
+ [project.urls]
21
+ "Homepage" = "https://gitlab.com/bhall/game-utils"
22
+
23
+ [build-system]
24
+ requires = ["setuptools"]
25
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
File without changes
@@ -0,0 +1,74 @@
1
+ from abc import abstractmethod
2
+
3
+
4
+ from pygame import Vector2, key, joystick
5
+
6
+
7
+ class Controller:
8
+ def __init__(self, directions: list[int]):
9
+ self._directions = directions
10
+
11
+ @abstractmethod
12
+ def direction(self, scalar) -> Vector2:
13
+ raise NotImplementedError
14
+
15
+ @abstractmethod
16
+ def action(self, index) -> bool:
17
+ raise NotImplementedError
18
+
19
+
20
+ class KeyboardController(Controller):
21
+ def __init__(self, directions, actions):
22
+ """Provide directions up, down, left, and right, in that order; and provide actions
23
+
24
+ Args:
25
+ directions (list[int]): ex. [pygame.K_w, pygame.K_s ...]
26
+ actions (list[int]): ex. [pygame.K_SPACE, pygame.K_ESCAPE ...]
27
+ """
28
+ super.__init__(directions)
29
+ self._actions = actions
30
+
31
+
32
+ class DefaultKeyboardController(KeyboardController):
33
+ def __init__(self, directions, actions):
34
+ super().__init__(directions, actions)
35
+
36
+ def direction(self, scalar=1):
37
+ u, d, l, r = (key.get_pressed()[i] for i in self._directions[:4])
38
+
39
+ return Vector2(r - l, u - d) * scalar
40
+
41
+ def action(self, index):
42
+ return self._actions[index]
43
+
44
+
45
+ class JoystickController(Controller):
46
+ def __init__(self, id: int, directions):
47
+ """Provide directions up, down, left, and right, in that order; and provide actions
48
+
49
+ Args:
50
+ directions (list[int]): ex. [0, 1 ...]
51
+ actions (list[int]): ex. [0, 1 ...]
52
+ """
53
+ super.__init__(directions)
54
+ if not joystick.get_init():
55
+ joystick.init()
56
+
57
+ self._joystick = joystick.Joystick(id)
58
+
59
+
60
+ class DefaultJoystickController(JoystickController):
61
+ def __init__(self, id, directions):
62
+ super().__init__(id, directions)
63
+
64
+ def direction(self, scalar):
65
+ x = self._joystick.get_axis(self._directions[0])
66
+ y = self._joystick.get_axis(self._directions[1])
67
+
68
+ return Vector2(x, y) * scalar
69
+
70
+ def rotation(self):
71
+ return self._joystick.get_axis(2)
72
+
73
+ def action(self, index):
74
+ return self._joystick.get_button(index)
File without changes
@@ -0,0 +1,4 @@
1
+ from .demo import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from pygame import Rect, Surface, Vector2
4
+ from pygame import display, draw, sprite, key
5
+ from pygame import K_DOWN, K_RIGHT, K_ESCAPE, K_UP, KEYDOWN, K_LEFT
6
+ from system_b.game import SpriteGame
7
+ from system_b.physics import PhysicsBody
8
+ from system_b.screen import ScreenReference
9
+ from system_b.sprites import PhysicsSprite
10
+ from system_b.math.vector_utils import apply_vector_actions, get_random_vector
11
+
12
+
13
+ class BouncyBallPlayer(PhysicsSprite):
14
+ """A PlayerSprite is a subclass of PhysicsSprite. It features
15
+ a PhysicsBody and also a Controller reference. Here you define logic
16
+ for what the controller actions do to the player sprite over time"""
17
+
18
+ def __init__(
19
+ self,
20
+ image: Surface,
21
+ position: Vector2,
22
+ physics_body: PhysicsBody,
23
+ color: str = "indigo",
24
+ boundaries: Rect | None = None,
25
+ id: int = 0,
26
+ ):
27
+ super().__init__(image, position, physics_body, boundaries, id)
28
+ self.color = color
29
+ # lock for collision logic - freezes if calculated at every loop
30
+ self.collided = False
31
+
32
+ def update(self, dt: float):
33
+ # sync position and physics body
34
+ self.physics_body.move(dt)
35
+
36
+ # bound position with physics body
37
+ self._update_pos(
38
+ self.physics_body.position,
39
+ xbounds=self._bounds_x,
40
+ ybounds=self._bounds_y,
41
+ )
42
+
43
+ def _bounds_x(self):
44
+ self.physics_body.velociy = apply_vector_actions(
45
+ self.physics_body.velociy,
46
+ lambda v: Vector2(0, v.y),
47
+ )
48
+
49
+ def _bounds_y(self):
50
+ self.physics_body.velociy = apply_vector_actions(
51
+ self.physics_body.velociy,
52
+ lambda v: Vector2(v.x, 0),
53
+ )
54
+
55
+
56
+ class BouncyBallPuck(PhysicsSprite):
57
+ """This bit probably looks complicated, and it is :)
58
+ PhysicsSprite is subclass of GameSprite that features a PhysicsBody.
59
+ Here we define the logic for what the sprite will do over time"""
60
+
61
+ def __init__(
62
+ self,
63
+ image: Surface,
64
+ position: Vector2,
65
+ physics_body: PhysicsBody,
66
+ color: str = "blue",
67
+ boundaries: Rect | None = None,
68
+ id: int = 0,
69
+ speed: float = 50,
70
+ ):
71
+ super().__init__(image, position, physics_body, boundaries, id)
72
+ self.color = color
73
+ self.speed = speed
74
+ self.physics_body.velociy = get_random_vector(speed)
75
+ self.start_position = Vector2.copy(position)
76
+
77
+ def update(self, dt: float):
78
+ # sync position and physics body
79
+ self.physics_body.move(dt)
80
+ # bound positions
81
+ self._update_pos(
82
+ self.physics_body.position,
83
+ xbounds=self.__bounds_x,
84
+ ybounds=self.__bounds_y,
85
+ )
86
+
87
+ def __bounds_x(self):
88
+ self.physics_body.velociy = (
89
+ apply_vector_actions(
90
+ self.physics_body.velociy, lambda v: Vector2(-v.x, v.y)
91
+ )
92
+ * self.physics_body.elasticity
93
+ )
94
+
95
+ def __bounds_y(self):
96
+ self.physics_body.velociy = (
97
+ apply_vector_actions(
98
+ self.physics_body.velociy,
99
+ lambda v: Vector2(v.x, -v.y),
100
+ )
101
+ * self.physics_body.elasticity
102
+ )
103
+
104
+
105
+ class BouncyBallScreen(ScreenReference):
106
+ """Anything to do with screen activity"""
107
+
108
+ def __init__(
109
+ self,
110
+ player: BouncyBallPlayer,
111
+ puck: BouncyBallPuck,
112
+ width=0,
113
+ height=0,
114
+ frames_per_second=60,
115
+ bg_color=None,
116
+ bg_image=None,
117
+ ):
118
+ super().__init__(
119
+ width=width,
120
+ height=height,
121
+ frames_per_second=frames_per_second,
122
+ bg_color=bg_color,
123
+ bg_image=bg_image,
124
+ )
125
+ self.puck = puck
126
+ self.player = player
127
+
128
+ def update_screen(self):
129
+ # DRAW THE TABLE
130
+ draw.rect(
131
+ surface=self.screen,
132
+ color=self.bg_color or "black",
133
+ rect=(0, 0, self.width, self.height),
134
+ border_radius=15,
135
+ )
136
+ # DRAW PLAYER
137
+ draw.circle(
138
+ self.screen,
139
+ self.player.color,
140
+ self.player.position,
141
+ self.player.rect.h,
142
+ )
143
+
144
+ # DRAW PUCK
145
+ draw.circle(
146
+ self.screen,
147
+ self.puck.color,
148
+ self.puck.position,
149
+ self.puck.rect.h,
150
+ )
151
+
152
+
153
+ class BouncyBall(SpriteGame):
154
+ """This is where high-level game logic goes
155
+ Think of this as the game-loop layer of the
156
+ application
157
+ """
158
+
159
+ def __init__(self, ctrl_speed, screen_ref, player_sprites, other_sprites):
160
+ # configure sprites
161
+ super().__init__(screen_ref, player_sprites, other_sprites)
162
+ display.set_caption("Game Utils Demo Game")
163
+ self.puck: BouncyBallPuck = self.other_sprites_group.sprites()[0]
164
+ self.player: BouncyBallPlayer = self.player_sprites[0]
165
+ self.ctrl_speed = ctrl_speed
166
+
167
+ def events(self, event):
168
+ if event.type == KEYDOWN:
169
+ # print(f"Key pressed: {event.key}")
170
+ if key.get_pressed()[K_ESCAPE]:
171
+ self._running = False
172
+
173
+ accel = (
174
+ Vector2(
175
+ (key.get_pressed()[K_RIGHT] - key.get_pressed()[K_LEFT]),
176
+ (key.get_pressed()[K_DOWN] - key.get_pressed()[K_UP]),
177
+ )
178
+ * self.ctrl_speed
179
+ )
180
+
181
+ self.player.physics_body.force(accel, self._screen_ref.get_delta_time())
182
+
183
+ def update(self):
184
+ """This method is required in a Game implementation.
185
+ Here is where the game loop logic will go. This will
186
+ be called once per game loop"""
187
+ dt = self._screen_ref.get_delta_time()
188
+
189
+ self.player.update(dt)
190
+ self.puck.update(dt)
191
+
192
+ if sprite.collide_circle(self.player, self.puck):
193
+ # lock out of collision logic so it doesn't freeze lol
194
+ if not self.player.collided:
195
+ self.player.physics_body.on_collide(self.puck.physics_body)
196
+ self.puck.physics_body.on_collide(self.player.physics_body)
197
+ self.player.collided = True
198
+ else:
199
+ self.player.collided = False
200
+
201
+
202
+ def main():
203
+ """configurations kept in a separate file
204
+ can make it easy to change things at this
205
+ orchestration level.
206
+ Play around with these input paramters and see what you can get it to do!"""
207
+ input_params = {
208
+ "player": {
209
+ "color": "darkred",
210
+ "physics_body": {"mass": 4, "friction": 0.002, "elasticity": 0},
211
+ },
212
+ "puck": {
213
+ "color": "midnightblue",
214
+ "physics_body": {"mass": 0.5, "friction": 0, "elasticity": 0},
215
+ },
216
+ "controller": {"speed": 1500.0},
217
+ "table": {"color": "firebrick"},
218
+ "mode": "DEBUG",
219
+ "screen": {"width": 1280},
220
+ }
221
+
222
+ # properties
223
+ table_color = input_params["table"]["color"]
224
+ ctrl_speed = input_params["controller"]["speed"]
225
+ player_physics = input_params["player"]["physics_body"]
226
+ player_color = input_params["player"]["color"]
227
+ puck_physics = input_params["puck"]["physics_body"]
228
+ puck_color = input_params["puck"]["color"]
229
+ screen_w = input_params["screen"]["width"]
230
+ screen_h = screen_w / 16 * 9 # screen["height"]
231
+ puck_size = screen_h / 9 / 2
232
+
233
+ # configure sprites
234
+ player = BouncyBallPlayer(
235
+ image=Surface((puck_size, puck_size)),
236
+ position=Vector2(screen_w / 2, screen_h / 2),
237
+ physics_body=PhysicsBody(
238
+ mass=player_physics["mass"],
239
+ friction=player_physics["friction"],
240
+ elasticity=player_physics["elasticity"],
241
+ ),
242
+ boundaries=Rect(screen_w, 0, screen_h, 0),
243
+ color=player_color,
244
+ )
245
+
246
+ puck = BouncyBallPuck(
247
+ image=Surface((puck_size, puck_size)),
248
+ position=Vector2(screen_w / 4, screen_h / 4),
249
+ boundaries=Rect(screen_w, 0, screen_h, 0),
250
+ physics_body=PhysicsBody(
251
+ mass=puck_physics["mass"],
252
+ friction=puck_physics["friction"],
253
+ elasticity=puck_physics["elasticity"],
254
+ ),
255
+ color=puck_color,
256
+ )
257
+
258
+ # screen settings and configurations
259
+ settings = BouncyBallScreen(
260
+ player, puck, width=screen_w, height=screen_h, bg_color=table_color
261
+ )
262
+
263
+ # load sprites and configurations
264
+ game = BouncyBall(ctrl_speed, settings, [player], [puck])
265
+
266
+ print("Starting game...")
267
+ game.run()
268
+ print("Game quit")
File without changes
@@ -0,0 +1,4 @@
1
+
2
+
3
+ # to see when an exception is caused by my foolery
4
+ GameError = Exception
@@ -0,0 +1,4 @@
1
+ from .game import (
2
+ Game,
3
+ SpriteGame
4
+ )
@@ -0,0 +1,82 @@
1
+ from abc import abstractmethod
2
+
3
+ import pygame
4
+
5
+ from system_b.screen import ScreenReference
6
+ from system_b.sprites import GameSprite
7
+
8
+
9
+ class Game:
10
+ """Game class defines high-level game logic. This class includes a run method, which controls the game loop.
11
+ User is required to implement abstract methods self.update() and self.events(), which is invoked within self.run()
12
+ """
13
+
14
+ def __init__(self, screen_ref: ScreenReference | None = None):
15
+ """High level game orchestrations
16
+
17
+ Args:
18
+ screen_ref (ScreenReference): Any screen settings
19
+ """
20
+ self._running = False
21
+ self._screen_ref = screen_ref
22
+
23
+ def run(self):
24
+ """Runs and maintains the game loop and clock. Updates the screen and invokes any handlers
25
+ Invokes pygame.init(), pygame.quit() when pygame.QUIT event is reached
26
+ """
27
+ pygame.init()
28
+ self._running = True
29
+ while self._running:
30
+ self.update()
31
+ if self._screen_ref is not None:
32
+ self._screen_ref.update_screen()
33
+ pygame.display.flip()
34
+ for event in pygame.event.get():
35
+ if event.type == pygame.QUIT:
36
+ self._running = False
37
+ self.events(event)
38
+
39
+ pygame.quit()
40
+
41
+ @abstractmethod
42
+ def events(self, event: pygame.event.Event):
43
+ """Define custom event handlers. Optional. Called once per game loop
44
+
45
+ Args:
46
+ event (pygame.event.Event): The event passed from run() method
47
+ *args/**kwargs
48
+ Raises:
49
+ NotImplementedError: Required implementation
50
+ """
51
+
52
+ raise NotImplementedError()
53
+
54
+ @abstractmethod
55
+ def update(self):
56
+ """Required implentation of game logic. Called once per game loop.
57
+
58
+ Raises:
59
+ NotImplementedError: Required implementation
60
+ """
61
+ raise NotImplementedError()
62
+
63
+
64
+ class SpriteGame(Game):
65
+ def __init__(
66
+ self,
67
+ screen_ref: ScreenReference,
68
+ player_sprites: list[GameSprite],
69
+ other_sprites: list[GameSprite],
70
+ ):
71
+ """Create new instance of SpriteGame object. Invokes pygame.init(). Must
72
+ implement update_sprites()
73
+
74
+ Args:
75
+ settings (ScreenSettings): Information needed to update the screen
76
+ player_sprite (GameSprite): The sprite to be used for the player
77
+ *other_sprites (GameSprite): Any other sprites needed for the game
78
+ """
79
+ super().__init__(screen_ref=screen_ref)
80
+ self.player_sprites: list[GameSprite] = player_sprites
81
+ self.player_group = pygame.sprite.Group(player_sprites)
82
+ self.other_sprites_group = pygame.sprite.Group(other_sprites)
@@ -0,0 +1,5 @@
1
+ from .images import (
2
+ SpriteSheet,
3
+ SpriteSheetList,
4
+ SpriteSheetMap
5
+ )
@@ -0,0 +1,100 @@
1
+ from pygame import Rect, Surface
2
+
3
+
4
+ class SpriteSheet:
5
+ """Allows user to access segements of a sprite sheet"""
6
+
7
+ def __init__(self, spritesheet: Surface, bg_color: str | None = None) -> None:
8
+ self.spritesheet = spritesheet
9
+ self.rect = self.spritesheet.get_rect()
10
+ self.bg_color = bg_color
11
+
12
+ def get_spritesheet_segment(self, rect: Rect) -> Surface:
13
+ """Get a segment of the spritesheet using the given coordinates
14
+
15
+ Args:
16
+ rect (Rect): The coordinates of the segment you want me to find in the spritesheet
17
+
18
+ Returns:
19
+ Surface: The sprite from the spritesheet using the given coordinates
20
+ """
21
+ image = Surface(rect.size)
22
+ if self.bg_color is not None:
23
+ image.fill(self.bg_color)
24
+ image.blit(self.spritesheet, (0, 0), rect)
25
+ return image
26
+
27
+
28
+ class SpriteSheetDataStruct(SpriteSheet):
29
+ """Base Sprite Sheet structure for sprite sheet data structures"""
30
+
31
+ def __init__(
32
+ self,
33
+ spritesheet: Surface,
34
+ n_sprites: int = 1,
35
+ n_lists: int = 1,
36
+ bg_color: str | None = None,
37
+ ) -> None:
38
+ super().__init__(spritesheet, bg_color)
39
+ self.n_lists = n_lists
40
+ self.n_sprites = n_sprites
41
+ self.sprite_size = Rect((0, 0), (self.rect.height, self.rect.width / n_sprites))
42
+ self.sprite_rects = self._get_sprite_rects()
43
+
44
+ def _get_sprite_rects(self) -> list[Rect]:
45
+ return [
46
+ # add a new rectangle for n sprites in this sheet (by m lists)
47
+ Rect(
48
+ (self.sprite_size.x + i * self.sprite_size.w, self.sprite_size.y),
49
+ self.sprite_size.size,
50
+ )
51
+ for i in range(self.n_sprites)
52
+ for _ in range(self.n_lists)
53
+ ]
54
+
55
+
56
+ class SpriteSheetList(SpriteSheetDataStruct):
57
+ """implements sprite sheet as a list of sprites. sprites are indexed"""
58
+
59
+ def __getitem__(self, index: int) -> Surface:
60
+ if index >= len(self.sprite_rects):
61
+ msg = f"index {index} out of bounds"
62
+ raise IndexError(msg)
63
+
64
+ return self.get_spritesheet_segment(self.sprite_rects[index])
65
+
66
+
67
+ class SpriteSheetMap(SpriteSheetDataStruct):
68
+ """Implements a sprite sheet as a dictionary of sprites"""
69
+
70
+ def __init__(
71
+ self,
72
+ spritesheet: Surface,
73
+ keys: list[str] = ["0"],
74
+ n_sprites: int = 1,
75
+ n_lists: int = 1,
76
+ bg_color: str | None = None,
77
+ ) -> None:
78
+ super().__init__(spritesheet, n_sprites, n_lists, bg_color)
79
+ if len(keys) < len(self.sprite_rects):
80
+ raise Exception("Not enough keys for sprites determined")
81
+
82
+ self.keys = keys
83
+ self.sprite_map = {
84
+ self.keys[i]: self.sprite_rects[i] for i in range(len(self.keys))
85
+ }
86
+
87
+ def get(self, name) -> Surface | None:
88
+ s = self.sprite_map.get(name)
89
+
90
+ if s is not None:
91
+ return self.get_spritesheet_segment(s)
92
+
93
+ def __getitem__(self, name: str) -> Surface | None:
94
+ s = self.sprite_map.get(name)
95
+
96
+ if s is None:
97
+ msg = f"No key {name}"
98
+ raise KeyError(msg)
99
+
100
+ return self.get_spritesheet_segment(s)
File without changes
@@ -0,0 +1,23 @@
1
+ from collections.abc import Callable
2
+
3
+ from numpy.random import choice, rand
4
+ from pygame import Vector2
5
+
6
+
7
+ def get_random_vector(scalar_mag: float = 1.0, non_negative: bool = False) -> Vector2:
8
+ def rand_sign() -> int:
9
+ return choice([-1, 1]) if not non_negative else 1
10
+
11
+ return Vector2(
12
+ x=rand() * rand_sign() * scalar_mag,
13
+ y=rand() * rand_sign() * scalar_mag,
14
+ )
15
+
16
+
17
+ def apply_vector_actions(
18
+ vector: Vector2, *actions: Callable[[Vector2], Vector2]
19
+ ) -> Vector2:
20
+ for action in actions:
21
+ vector = action(vector)
22
+
23
+ return vector
@@ -0,0 +1 @@
1
+ from .physics import PhysicsBody
@@ -0,0 +1,113 @@
1
+ from numpy.random import normal
2
+ from pygame import Vector2, Vector3
3
+
4
+
5
+ class PhysicsBody:
6
+ """Applies physics to a GameSprite"""
7
+
8
+ ZERO_VECTOR = Vector2(0, 0)
9
+ GRAVITY = 9.8
10
+
11
+ def __init__(
12
+ self,
13
+ mass: float = 1.0,
14
+ position: Vector2 = ZERO_VECTOR.copy(),
15
+ velocity: Vector2 = ZERO_VECTOR.copy(),
16
+ friction: float = 0.0,
17
+ elasticity: float = 0.0,
18
+ slip: float = 1.0,
19
+ ):
20
+ """Creates new instance of PhysicsBody object
21
+
22
+ Args:
23
+ mass (float, optional): The mass of the body. Defaults to 1.0.
24
+ position (Vector2, optional): The current positional vector of the body. Defaults to (0,0).
25
+ friction (float, optional): Static friction force. Defaults to 0.0.
26
+ elasticity (float, optional): Elasticity is used in caluclating loss of energy. 0 elasticity will have no effect. Defaults to 0.0.
27
+ slip (float, optional): Used in calculating change in static to kinetic friction. Defaults to 1.0.
28
+ """
29
+ self.mass = mass
30
+ self.velociy = velocity.copy()
31
+ self.position = position.copy()
32
+ self.static_friction = friction
33
+ self.kinetic_friction = 0
34
+ self.elasticity = 1 - elasticity
35
+ if self.static_friction > 0:
36
+ self.kinetic_friction = self.static_friction * normal(loc=slip)
37
+
38
+ def force(self, acceleration: Vector2, dtime: float) -> Vector2:
39
+ """Applies an acceleration vector to this body's current velocity with the given change in time.
40
+ If friction was given, friction is calculated into the new velocity
41
+
42
+ Args:
43
+ acceleration (Vector2): The acceleration to apply
44
+ dtime (float): The change in time
45
+
46
+ Returns:
47
+ Vector2: The new velocity at this moment in time
48
+ """
49
+ new_velocity = acceleration * dtime
50
+ force = acceleration * self.mass
51
+
52
+ if force.magnitude() > self.friction_force().magnitude():
53
+ self.velociy += new_velocity
54
+ else:
55
+ self.velociy -= self.velociy * self.friction_force().magnitude() * dtime
56
+ self.move(dtime)
57
+
58
+ return self.velociy.copy()
59
+
60
+ def move(self, dtime: float) -> Vector2:
61
+ """Returns the position at this moment in time.\n
62
+ Applies velocity over dtime"""
63
+ self.position += self.velociy * dtime
64
+
65
+ return self.position.copy()
66
+
67
+ def on_collide(self, other):
68
+ """New velocity is given by V`= (V(m-s) + U2s)/(m+s)
69
+ for initial magnitudes Vm(self) and Us(other). Calculates loss of
70
+ energy from this body's elasticity
71
+
72
+ Args:
73
+ other (object): The object with which this body has collided
74
+ """
75
+ assert isinstance(other, PhysicsBody)
76
+
77
+ self.velociy *= self.mass - other.mass
78
+ self.velociy += other.velociy * 2 * other.mass
79
+ self.velociy /= self.mass + other.mass
80
+
81
+ # energy loss, if any
82
+ self.velociy *= self.elasticity
83
+
84
+ def momentum(self) -> Vector2:
85
+ """This body's current momentum
86
+
87
+ Returns:
88
+ Vector2: Vm of this body
89
+ """
90
+ return self.velociy * self.mass
91
+
92
+ def normal_force(self) -> Vector3:
93
+ """The normal force of this body
94
+
95
+ Returns:
96
+ Vector3: (0, 0, mg)
97
+ """
98
+ return Vector3(0, 0, self.mass * self.GRAVITY)
99
+
100
+ def friction_force(self) -> Vector3:
101
+ """fs * N if not in motion, else fk * N
102
+
103
+ Returns:
104
+ Vector3: (0, 0, fN)
105
+ """
106
+ return (
107
+ self.static_friction * self.normal_force()
108
+ if self.velociy == self.ZERO_VECTOR
109
+ else self.kinetic_friction * self.normal_force()
110
+ )
111
+
112
+ def __str__(self) -> str:
113
+ return str(self.position)
@@ -0,0 +1 @@
1
+ from .screen import ScreenReference
@@ -0,0 +1,46 @@
1
+ from abc import abstractmethod
2
+
3
+ import pygame.display as display
4
+ from pygame import FULLSCREEN, Color, Surface
5
+ from pygame.time import Clock
6
+
7
+ ColorType = str | tuple[int, int, int] | Color
8
+
9
+
10
+ class ScreenReference:
11
+ """defines information needed to udpate the screen"""
12
+
13
+ def __init__(
14
+ self,
15
+ width: float = 0.0,
16
+ height: float = 0.0,
17
+ frames_per_second: int = 60,
18
+ bg_color: ColorType | None = None,
19
+ bg_image: Surface | None = None,
20
+ ):
21
+ self.clock = Clock()
22
+ self.bg_image = bg_image
23
+ self.bg_color = bg_color
24
+ self.frames_per_second = frames_per_second
25
+ self.width = width
26
+ self.height = height
27
+ if self.width == 0.0 and self.height == 0.0:
28
+ self.screen = display.set_mode((0, 0), FULLSCREEN)
29
+ self.width = self.screen.get_width()
30
+ self.height = self.screen.get_height()
31
+ else:
32
+ self.screen = display.set_mode((self.width, self.height))
33
+
34
+ def resize_screen(self, width: float, height: float):
35
+ """resizes to fullscreen by default"""
36
+ self.width = width
37
+ self.height = height
38
+ self.screen = display.set_mode((self.width, self.height))
39
+
40
+ def get_delta_time(self, units: int = 1000) -> float:
41
+ return self.clock.tick(self.frames_per_second) / units
42
+
43
+ @abstractmethod
44
+ def update_screen(self):
45
+ """Define logic for screen activity. Required"""
46
+ raise NotImplementedError()
@@ -0,0 +1,4 @@
1
+ from .sprites import (
2
+ GameSprite,
3
+ PhysicsSprite,
4
+ )
@@ -0,0 +1,110 @@
1
+ from abc import abstractmethod
2
+ from collections.abc import Callable
3
+ from typing import Any
4
+
5
+ from pygame import Rect, Surface, Vector2
6
+ from pygame.sprite import Sprite
7
+
8
+ from system_b.physics import PhysicsBody
9
+
10
+
11
+ class GameSprite(Sprite):
12
+ def __init__(
13
+ self,
14
+ image: Surface,
15
+ position: Vector2,
16
+ boundaries: Rect | None = None,
17
+ id: int = 0,
18
+ ):
19
+ """Implement a Game Sprite object. Subclass must implement GameSprite.update
20
+
21
+ Args:
22
+ image (Surface): The image for the sprite
23
+ position (Vector2): Where to put the sprite
24
+ boundaries (Rect | None, optional): Optional boundary vectors for this sprite (xmax, xmin, ymax, ymin). Defaults to None.
25
+ """
26
+ super().__init__()
27
+ self.image = image
28
+ _, _, *dimensions = image.get_rect()
29
+ self.rect = Rect(position.x, position.y, *dimensions)
30
+ self.position = position
31
+ self.boundaries = boundaries
32
+ self.id = id
33
+
34
+ @abstractmethod
35
+ def update(self, *args: Any, **kwargs: Any):
36
+ """Required. Define logic to perform on this sprite once for every game loop
37
+
38
+ Args:
39
+ *args, **kwargs
40
+ """
41
+ raise NotImplementedError()
42
+
43
+ def _update_pos(
44
+ self,
45
+ position: Vector2 | None = None,
46
+ xbounds: Callable[[], None] | None = None,
47
+ ybounds: Callable[[], None] | None = None,
48
+ ):
49
+ """Updates position of this sprite. Maintains any given boundaries. Invokes callbacks given when boundaries are reached
50
+
51
+ Args:
52
+ position (Vector2 | None, optional): The new position of this sprite. Defaults to None.
53
+ xbounds (Callable[[], None] | None, optional): X boundary callable. Invoked when boundaries are reached, if given. Defaults to None.
54
+ ybounds (Callable[[], None] | None, optional): Y boundary callable. Invoked when boundaries are reached, if given. Defaults to None.
55
+ """
56
+ if position is not None:
57
+ self.position = position
58
+
59
+ x, y, w, h = self.rect
60
+
61
+ if self.boundaries is not None:
62
+ bx_max, bx_min, by_max, by_min = self.boundaries
63
+
64
+ if self.position.y > by_max - h / 2:
65
+ if ybounds is not None:
66
+ ybounds()
67
+
68
+ self.position.y = by_max - h / 2
69
+
70
+ elif self.position.y < by_min + h / 2:
71
+ if ybounds is not None:
72
+ ybounds()
73
+
74
+ self.position.y = by_min + h / 2
75
+
76
+ if self.position.x > bx_max - w / 2:
77
+ if xbounds is not None:
78
+ xbounds()
79
+
80
+ self.position.x = bx_max - w / 2
81
+
82
+ elif self.position.x < bx_min + w / 2:
83
+ if xbounds is not None:
84
+ xbounds()
85
+
86
+ self.position.x = bx_min + w / 2
87
+
88
+ x = self.position.x - w / 2
89
+ y = self.position.y - h / 2
90
+
91
+ self.rect = Rect(x, y, w, h)
92
+
93
+ def __str__(self) -> str:
94
+ return str(tuple(self.rect))
95
+
96
+
97
+ class PhysicsSprite(GameSprite):
98
+ """GameSprite that uses a PhysicsBody reference to apply physics"""
99
+
100
+ def __init__(
101
+ self,
102
+ image: Surface,
103
+ position: Vector2,
104
+ physics_body: PhysicsBody,
105
+ boundaries: Rect | None = None,
106
+ id: int = 0,
107
+ ):
108
+ super().__init__(image, position, boundaries, id)
109
+ self.physics_body = physics_body
110
+ self.physics_body.position = self.position
@@ -0,0 +1 @@
1
+ from .toolkit import *
@@ -0,0 +1,6 @@
1
+ from .toolkit import main
2
+
3
+ import sys
4
+
5
+ if __name__ == "__main__":
6
+ main(sys.argv)
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import shutil
4
+ import os
5
+
6
+ from system_b.demo import demo
7
+
8
+ def main(args):
9
+ if len(args) == 1:
10
+ print("Thank you for using game-utils!")
11
+ print(" --demo to play the demo game")
12
+ print(" --copy to copy the demo game file into a new project")
13
+ elif "--demo" in args:
14
+ print("Running demo...")
15
+ demo.main()
16
+ print("Game over...")
17
+ elif "--copy" in args:
18
+ shutil.copy(os.path.relpath(demo.__file__), os.path.curdir)
19
+
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.4
2
+ Name: system-b
3
+ Version: 1.1.4
4
+ Summary: A pygame engine for common game operations like vector math and maintaining events in the game loop
5
+ Author-email: Bee Hall <bhall1224@gmail.com>
6
+ Project-URL: Homepage, https://gitlab.com/bhall/game-utils
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: <=3.13
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: pygame
14
+ Requires-Dist: numpy
15
+ Dynamic: license-file
16
+
17
+ # Game Utils
18
+
19
+ >1.1.4
20
+
21
+ ## `game-utils` is a pygame engine. The engine includes modules to facilitate boilerplate game operations.
22
+
23
+ ### Requirements
24
+
25
+ **Dependences:**
26
+ - pygame
27
+ - numpy
28
+
29
+ **Demo Game**
30
+
31
+ Run: `python -m game_utils.demo` or `python -m game_utils.toolkit --demo` to play the demo bouncy ball game!
32
+
33
+ Run: `python -m game_utils.toolkit --copy` to copy the sample project to your directory!
34
+
35
+ ---
36
+
37
+ ## See Also...
38
+
39
+ <p><b>My ongoing game projects!</b></p>
40
+
41
+ - [GitLab](https://gitlab.com/bhall)
@@ -0,0 +1,34 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/system_b/__init__.py
5
+ src/system_b.egg-info/PKG-INFO
6
+ src/system_b.egg-info/SOURCES.txt
7
+ src/system_b.egg-info/dependency_links.txt
8
+ src/system_b.egg-info/requires.txt
9
+ src/system_b.egg-info/top_level.txt
10
+ src/system_b/controller/__init__.py
11
+ src/system_b/controller/controller.py
12
+ src/system_b/demo/__init__.py
13
+ src/system_b/demo/__main__.py
14
+ src/system_b/demo/demo.py
15
+ src/system_b/exceptions/__init__.py
16
+ src/system_b/exceptions/game_exception.py
17
+ src/system_b/game/__init__.py
18
+ src/system_b/game/game.py
19
+ src/system_b/images/__init__.py
20
+ src/system_b/images/images.py
21
+ src/system_b/math/__init__.py
22
+ src/system_b/math/vector_utils.py
23
+ src/system_b/physics/__init__.py
24
+ src/system_b/physics/physics.py
25
+ src/system_b/screen/__init__.py
26
+ src/system_b/screen/screen.py
27
+ src/system_b/sprites/__init__.py
28
+ src/system_b/sprites/sprites.py
29
+ src/system_b/toolkit/__init__.py
30
+ src/system_b/toolkit/__main__.py
31
+ src/system_b/toolkit/toolkit.py
32
+ tests/test_game.py
33
+ tests/test_images.py
34
+ tests/test_physics.py
@@ -0,0 +1,2 @@
1
+ pygame
2
+ numpy
@@ -0,0 +1 @@
1
+ system_b
@@ -0,0 +1,37 @@
1
+ import logging
2
+
3
+ import pygame.event
4
+ from pygame.locals import K_ESCAPE, QUIT, USEREVENT
5
+
6
+ from system_b.game import Game
7
+ from system_b.screen import ScreenReference
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logger.setLevel(logging.WARNING)
11
+
12
+ TESTEVENT = USEREVENT
13
+
14
+
15
+ class MockGame(Game):
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.state = 0
19
+ self.event = False
20
+
21
+ def update(self):
22
+ self.state += 1
23
+ if self.state == 1:
24
+ pygame.event.post(pygame.event.Event(TESTEVENT))
25
+ if self.state == 3:
26
+ self._running = False
27
+
28
+ def events(self, event, *args, **kwargs):
29
+ if event.type == TESTEVENT:
30
+ self.event = True
31
+
32
+
33
+ def test_run():
34
+ tg = MockGame()
35
+ tg.run()
36
+ assert tg.state == 3
37
+ assert tg.event == True
@@ -0,0 +1,35 @@
1
+ from pygame import Rect, Surface
2
+
3
+ from system_b.images import *
4
+
5
+ SPRITESHEET_SIM_RECT = Rect(0, 0, 100, 100)
6
+ SPRITESHEET_SIM__SURFACT = Surface(SPRITESHEET_SIM_RECT.size)
7
+
8
+
9
+ def test_sprite_sheet():
10
+ ssheet = SpriteSheet(spritesheet=SPRITESHEET_SIM__SURFACT)
11
+
12
+ seg = ssheet.get_spritesheet_segment(Rect(0, 0, 20, 20))
13
+ assert seg.get_rect().size == (20, 20)
14
+
15
+
16
+ def test_sprite_sheet_list():
17
+ ssheet_list = SpriteSheetList(
18
+ spritesheet=SPRITESHEET_SIM__SURFACT,
19
+ n_sprites=5,
20
+ )
21
+
22
+ seg = ssheet_list[0]
23
+ assert seg.get_rect().size == (100, 20)
24
+
25
+
26
+ def test_sprite_sheet_map():
27
+ ssheet_map = SpriteSheetMap(
28
+ spritesheet=SPRITESHEET_SIM__SURFACT,
29
+ keys=["a", "b", "c", "d", "e"],
30
+ n_sprites=5,
31
+ )
32
+
33
+ seg = ssheet_map["a"]
34
+ assert seg is not None
35
+ assert seg.get_rect().size == (100, 20)
@@ -0,0 +1,77 @@
1
+ import math
2
+
3
+ from pygame import Vector2
4
+
5
+ from system_b.physics import *
6
+
7
+
8
+ def test_physics_body():
9
+ pb = PhysicsBody()
10
+
11
+ assert pb is not None
12
+ assert pb.elasticity == 1.0
13
+ assert pb.mass == 1.0
14
+ assert pb.static_friction == 0.0
15
+ assert pb.kinetic_friction == 0.0
16
+
17
+ pb2 = PhysicsBody(
18
+ mass=2.0,
19
+ friction=0.009,
20
+ elasticity=0.02,
21
+ slip=1.1,
22
+ )
23
+
24
+ assert pb2 is not None
25
+ assert math.fabs(pb2.kinetic_friction) > 0
26
+
27
+
28
+ def test_forces():
29
+ pb = PhysicsBody(mass=2.0)
30
+
31
+ v = pb.force(Vector2(0.1, -0.1), 0.1)
32
+ assert v is not None
33
+ assert v == Vector2(0.01, -0.01)
34
+
35
+ v2 = pb.move(0.08)
36
+
37
+ assert v2 is not None
38
+ assert (v2 - v).magnitude() != 0
39
+
40
+
41
+ def test_collisions():
42
+ v_i1 = Vector2(-1, 0)
43
+ d_i1 = Vector2(1, 0)
44
+
45
+ pb = PhysicsBody(
46
+ mass=2.0,
47
+ position=d_i1,
48
+ velocity=v_i1,
49
+ )
50
+
51
+ v_i2 = Vector2(1, 0)
52
+ d_i2 = Vector2(-1, 0)
53
+
54
+ pb2 = PhysicsBody(
55
+ mass=3.0,
56
+ position=d_i2,
57
+ velocity=v_i2,
58
+ )
59
+
60
+ t1 = 0.1
61
+
62
+ d1_t1 = pb.move(t1)
63
+ d2_t1 = pb2.move(t1)
64
+
65
+ assert d1_t1 is not d_i1
66
+ assert d2_t1 is not d_i2
67
+
68
+ pb.on_collide(pb2)
69
+ pb2.on_collide(pb)
70
+
71
+ t2 = 0.2
72
+
73
+ d1_t2 = pb.move(t2)
74
+ d2_t2 = pb2.move(t2)
75
+
76
+ assert d1_t2 is not d1_t1
77
+ assert d2_t2 is not d2_t1