pgzero-plus 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pgzero-plus might be problematic. Click here for more details.

pgzero/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Pygame Zero Plus."""
2
+
3
+ import os
4
+ import warnings
5
+
6
+ # Every way of starting a game (``pgzrun game.py``, ``import pgzrun``,
7
+ # ``python -m pgzero``) imports this package before pygame, which is when
8
+ # SDL and pygame read these settings.
9
+
10
+ # Codio has no sound device, so use SDL's silent audio driver there.
11
+ if (
12
+ "CODIO_BOX_DOMAIN" in os.environ
13
+ or "CODIO_HOSTNAME" in os.environ
14
+ or os.environ.get("USER") == "codio"
15
+ or os.path.exists("/home/codio")
16
+ ):
17
+ os.environ["SDL_AUDIODRIVER"] = "dummy"
18
+ warnings.filterwarnings("ignore", message=".*avx2 capable.*", category=RuntimeWarning)
19
+
20
+ # Hide pygame welcome message
21
+ os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "1"
22
+
23
+ # Suppress pkg_resources deprecation warning (from pygame)
24
+ warnings.filterwarnings(
25
+ "ignore",
26
+ message=r"pkg_resources is deprecated as an API.*",
27
+ category=UserWarning,
28
+ )
29
+
30
+ try:
31
+ from importlib.metadata import PackageNotFoundError, version
32
+ except ImportError:
33
+ # Python 3.6 and 3.7
34
+ from importlib_metadata import PackageNotFoundError, version
35
+
36
+ try:
37
+ __version__ = version("pgzero-plus")
38
+ except PackageNotFoundError:
39
+ __version__ = "unknown"
40
+
41
+ __all__ = ["__version__"]
pgzero/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from pgzero.runner import main
2
+
3
+ main()
pgzero/actor.py ADDED
@@ -0,0 +1,597 @@
1
+ import time
2
+ import pygame
3
+ from math import radians, sin, cos, atan2, degrees, sqrt
4
+
5
+ from . import game
6
+ from . import loaders
7
+ from . import rect
8
+ from . import spellcheck
9
+
10
+ ANCHORS = {
11
+ "x": {
12
+ "left": 0.0,
13
+ "center": 0.5,
14
+ "middle": 0.5,
15
+ "right": 1.0,
16
+ },
17
+ "y": {
18
+ "top": 0.0,
19
+ "center": 0.5,
20
+ "middle": 0.5,
21
+ "bottom": 1.0,
22
+ },
23
+ }
24
+
25
+
26
+ def calculate_anchor(value, dim, total):
27
+ if isinstance(value, str):
28
+ try:
29
+ return total * ANCHORS[dim][value]
30
+ except KeyError:
31
+ raise ValueError("%r is not a valid %s-anchor name" % (value, dim))
32
+ return float(value)
33
+
34
+
35
+ # These are methods (of the same name) on pygame.Rect
36
+ SYMBOLIC_POSITIONS = set(
37
+ (
38
+ "topleft",
39
+ "bottomleft",
40
+ "topright",
41
+ "bottomright",
42
+ "midtop",
43
+ "midleft",
44
+ "midbottom",
45
+ "midright",
46
+ "center",
47
+ )
48
+ )
49
+
50
+ # Provides more meaningful default-arguments e.g. for display in IDEs etc.
51
+ POS_TOPLEFT = None
52
+ ANCHOR_CENTER = None
53
+
54
+ MAX_ALPHA = 255 # Based on pygame's max alpha.
55
+
56
+
57
+ def transform_anchor(ax, ay, w, h, angle):
58
+ """Transform anchor based upon a rotation of a surface of size w x h."""
59
+ theta = -radians(angle)
60
+
61
+ sintheta = sin(theta)
62
+ costheta = cos(theta)
63
+
64
+ # Dims of the transformed rect
65
+ tw = abs(w * costheta) + abs(h * sintheta)
66
+ th = abs(w * sintheta) + abs(h * costheta)
67
+
68
+ # Offset of the anchor from the center
69
+ cax = ax - w * 0.5
70
+ cay = ay - h * 0.5
71
+
72
+ # Rotated offset of the anchor from the center
73
+ rax = cax * costheta - cay * sintheta
74
+ ray = cax * sintheta + cay * costheta
75
+
76
+ return (tw * 0.5 + rax, th * 0.5 + ray)
77
+
78
+
79
+ def rotate_vector(x, y, angle):
80
+ """Rotate a vector using the same rotation convention as pygame surfaces."""
81
+ theta = -radians(angle)
82
+ sintheta = sin(theta)
83
+ costheta = cos(theta)
84
+
85
+ return (
86
+ x * costheta - y * sintheta,
87
+ x * sintheta + y * costheta,
88
+ )
89
+
90
+
91
+ def _set_opacity(actor, current_surface):
92
+ alpha = int(actor.opacity * MAX_ALPHA + 0.5)
93
+
94
+ if alpha == MAX_ALPHA:
95
+ return current_surface
96
+
97
+ alpha_img = pygame.Surface(current_surface.get_size(), pygame.SRCALPHA)
98
+ alpha_img.fill((255, 255, 255, alpha))
99
+ alpha_img.blit(current_surface, (0, 0), special_flags=pygame.BLEND_RGBA_MULT)
100
+ return alpha_img
101
+
102
+
103
+ def _set_scale(actor, current_surface):
104
+ if actor._scale == 1.0:
105
+ return current_surface
106
+
107
+ w, h = current_surface.get_size()
108
+ sw = max(1, int(round(w * actor._scale)))
109
+ sh = max(1, int(round(h * actor._scale)))
110
+ return pygame.transform.scale(current_surface, (sw, sh))
111
+
112
+
113
+ def _set_flip(actor, current_surface):
114
+ surf = current_surface
115
+
116
+ if actor._flip_d:
117
+ surf = pygame.transform.rotate(surf, -90)
118
+ surf = pygame.transform.flip(surf, True, False)
119
+
120
+ if actor._flip_h or actor._flip_v:
121
+ surf = pygame.transform.flip(surf, actor._flip_h, actor._flip_v)
122
+
123
+ return surf
124
+
125
+
126
+ def _set_angle(actor, current_surface):
127
+ if actor._angle % 360 == 0:
128
+ return current_surface
129
+ return pygame.transform.rotate(current_surface, actor._angle)
130
+
131
+
132
+ class Actor:
133
+ EXPECTED_INIT_KWARGS = SYMBOLIC_POSITIONS
134
+ DELEGATED_ATTRIBUTES = [a for a in dir(rect.ZRect) if not a.startswith("_")]
135
+
136
+ function_order = [_set_opacity, _set_scale, _set_flip, _set_angle]
137
+
138
+ _anchor = _anchor_value = (0, 0)
139
+ _angle = 0.0
140
+ _opacity = 1.0
141
+ _scale = 1.0
142
+ _flip_h = False
143
+ _flip_v = False
144
+ _flip_d = False
145
+ _animate_counter = 0
146
+ fps = 5
147
+ direction = 0
148
+ _collision_rect_spec = None
149
+
150
+ def _build_transformed_surf(self):
151
+ cache_len = len(self._surface_cache)
152
+ if cache_len == 0:
153
+ last = self._orig_surf
154
+ else:
155
+ last = self._surface_cache[-1]
156
+
157
+ for f in self.function_order[cache_len:]:
158
+ new_surf = f(self, last)
159
+ self._surface_cache.append(new_surf)
160
+ last = new_surf
161
+
162
+ return self._surface_cache[-1]
163
+
164
+ def __init__(self, image, pos=POS_TOPLEFT, anchor=ANCHOR_CENTER, collision_rect=None, **kwargs):
165
+ self._handle_unexpected_kwargs(kwargs)
166
+
167
+ self._surface_cache = []
168
+ self.__dict__["_rect"] = rect.ZRect((0, 0), (0, 0))
169
+ self._collision_rect_spec = collision_rect
170
+ self._images = []
171
+
172
+ self.image = image
173
+ self._init_position(pos, anchor, **kwargs)
174
+
175
+ def __getattr__(self, attr):
176
+ if attr in self.__class__.DELEGATED_ATTRIBUTES:
177
+ return getattr(self._rect, attr)
178
+ else:
179
+ return object.__getattribute__(self, attr)
180
+
181
+ def __setattr__(self, attr, value):
182
+ if attr in self.__class__.DELEGATED_ATTRIBUTES:
183
+ return setattr(self._rect, attr, value)
184
+ else:
185
+ return object.__setattr__(self, attr, value)
186
+
187
+ def __iter__(self):
188
+ return iter(self._rect)
189
+
190
+ def __repr__(self):
191
+ return "<{} {!r} pos={!r}>".format(type(self).__name__, self._image_name, self.pos)
192
+
193
+ def __dir__(self):
194
+ standard_attributes = [key for key in self.__dict__.keys() if not key.startswith("_")]
195
+ return standard_attributes + self.__class__.DELEGATED_ATTRIBUTES
196
+
197
+ def _handle_unexpected_kwargs(self, kwargs):
198
+ unexpected_kwargs = set(kwargs.keys()) - self.EXPECTED_INIT_KWARGS
199
+ if not unexpected_kwargs:
200
+ return
201
+
202
+ typos, _ = spellcheck.compare(unexpected_kwargs, self.EXPECTED_INIT_KWARGS)
203
+ for found, suggested in typos:
204
+ raise TypeError("Unexpected keyword argument '{}' (did you mean '{}'?)".format(found, suggested))
205
+
206
+ def _init_position(self, pos, anchor, **kwargs):
207
+ if anchor is None:
208
+ anchor = ("center", "center")
209
+ self.anchor = anchor
210
+
211
+ symbolic_pos_args = {k: kwargs[k] for k in kwargs if k in SYMBOLIC_POSITIONS}
212
+
213
+ if not pos and not symbolic_pos_args:
214
+ self.topleft = (0, 0)
215
+ elif pos and symbolic_pos_args:
216
+ raise TypeError("'pos' argument cannot be mixed with 'topleft', " "'topright' etc. argument.")
217
+ elif pos:
218
+ self.pos = pos
219
+ else:
220
+ self._set_symbolic_pos(symbolic_pos_args)
221
+
222
+ def _set_symbolic_pos(self, symbolic_pos_dict):
223
+ if len(symbolic_pos_dict) == 0:
224
+ raise TypeError("No position-setting keyword arguments ('topleft', " "'topright' etc) found.")
225
+ if len(symbolic_pos_dict) > 1:
226
+ raise TypeError("Only one 'topleft', 'topright' etc. argument is allowed.")
227
+
228
+ setter_name, position = symbolic_pos_dict.popitem()
229
+ setattr(self, setter_name, position)
230
+
231
+ def _update_transform(self, function):
232
+ if function in self.function_order:
233
+ i = self.function_order.index(function)
234
+ del self._surface_cache[i:]
235
+ else:
236
+ raise IndexError("function {!r} does not have a registered order." "".format(function))
237
+
238
+ def _body_spec(self):
239
+ """Return the unscaled body as (width, height, offset_x, offset_y).
240
+
241
+ The body is the actor's authoritative rectangle. All position anchors,
242
+ symbolic positions, and collision checks are based on this rectangle.
243
+
244
+ collision_rect formats:
245
+ - None: use the full image as the body
246
+ - (width, height): centered body
247
+ - (width, height, offset_x, offset_y): body center offset from image center
248
+ """
249
+ image_w, image_h = self._orig_surf.get_size()
250
+
251
+ if self._collision_rect_spec is None:
252
+ return float(image_w), float(image_h), 0.0, 0.0
253
+
254
+ if len(self._collision_rect_spec) == 2:
255
+ width, height = self._collision_rect_spec
256
+ return float(width), float(height), 0.0, 0.0
257
+
258
+ if len(self._collision_rect_spec) == 4:
259
+ width, height, offset_x, offset_y = self._collision_rect_spec
260
+ return float(width), float(height), float(offset_x), float(offset_y)
261
+
262
+ raise ValueError("collision_rect must be None, (width, height), or " "(width, height, offset_x, offset_y).")
263
+
264
+ def _transformed_body_size(self):
265
+ body_w, body_h, _, _ = self._body_spec()
266
+
267
+ w = body_w * self._scale
268
+ h = body_h * self._scale
269
+
270
+ if self._flip_d:
271
+ w, h = h, w
272
+
273
+ if self._angle != 0.0:
274
+ ra = radians(self._angle)
275
+ sin_a = sin(ra)
276
+ cos_a = cos(ra)
277
+ return (
278
+ abs(w * cos_a) + abs(h * sin_a),
279
+ abs(w * sin_a) + abs(h * cos_a),
280
+ )
281
+
282
+ return w, h
283
+
284
+ @property
285
+ def anchor(self):
286
+ return self._anchor_value
287
+
288
+ @anchor.setter
289
+ def anchor(self, val):
290
+ self._anchor_value = val
291
+ self._calc_anchor()
292
+
293
+ def _calc_anchor(self):
294
+ if not hasattr(self, "_orig_surf"):
295
+ return
296
+
297
+ ax, ay = self._anchor_value
298
+ body_w, body_h, _, _ = self._body_spec()
299
+
300
+ ax = calculate_anchor(ax, "x", body_w)
301
+ ay = calculate_anchor(ay, "y", body_h)
302
+ self._untransformed_anchor = ax, ay
303
+
304
+ transformed_w = body_w * self._scale
305
+ transformed_h = body_h * self._scale
306
+
307
+ anchor_x = ax * self._scale
308
+ anchor_y = ay * self._scale
309
+
310
+ if self._flip_d:
311
+ anchor_x, anchor_y = anchor_y, anchor_x
312
+ transformed_w, transformed_h = transformed_h, transformed_w
313
+
314
+ if self._angle != 0.0:
315
+ self._anchor = transform_anchor(
316
+ anchor_x,
317
+ anchor_y,
318
+ transformed_w,
319
+ transformed_h,
320
+ self._angle,
321
+ )
322
+ else:
323
+ self._anchor = anchor_x, anchor_y
324
+
325
+ @property
326
+ def angle(self):
327
+ return self._angle
328
+
329
+ @angle.setter
330
+ def angle(self, angle):
331
+ self._angle = angle
332
+ self._update_pos()
333
+ self._update_transform(_set_angle)
334
+
335
+ @property
336
+ def opacity(self):
337
+ """Get/set the current opacity value."""
338
+ return self._opacity
339
+
340
+ @opacity.setter
341
+ def opacity(self, opacity):
342
+ self._opacity = min(1.0, max(0.0, opacity))
343
+ self._update_transform(_set_opacity)
344
+
345
+ @property
346
+ def scale(self):
347
+ return self._scale
348
+
349
+ @scale.setter
350
+ def scale(self, scale):
351
+ self._scale = scale
352
+ self._update_pos()
353
+ self._update_transform(_set_scale)
354
+
355
+ @property
356
+ def flip_h(self):
357
+ return self._flip_h
358
+
359
+ @flip_h.setter
360
+ def flip_h(self, value):
361
+ self._flip_h = value
362
+ self._update_pos()
363
+ self._update_transform(_set_flip)
364
+
365
+ @property
366
+ def flip_v(self):
367
+ return self._flip_v
368
+
369
+ @flip_v.setter
370
+ def flip_v(self, value):
371
+ self._flip_v = value
372
+ self._update_pos()
373
+ self._update_transform(_set_flip)
374
+
375
+ @property
376
+ def flip_d(self):
377
+ return self._flip_d
378
+
379
+ @flip_d.setter
380
+ def flip_d(self, value):
381
+ self._flip_d = value
382
+ self._update_pos()
383
+ self._update_transform(_set_flip)
384
+
385
+ @property
386
+ def pos(self):
387
+ px, py = self.topleft
388
+ ax, ay = self._anchor
389
+ return px + ax, py + ay
390
+
391
+ @pos.setter
392
+ def pos(self, pos):
393
+ px, py = pos
394
+ ax, ay = self._anchor
395
+ self.topleft = px - ax, py - ay
396
+
397
+ def rect(self):
398
+ return self._rect.copy()
399
+
400
+ @property
401
+ def x(self):
402
+ ax = self._anchor[0]
403
+ return self.left + ax
404
+
405
+ @x.setter
406
+ def x(self, px):
407
+ self.left = px - self._anchor[0]
408
+
409
+ @property
410
+ def y(self):
411
+ ay = self._anchor[1]
412
+ return self.top + ay
413
+
414
+ @y.setter
415
+ def y(self, py):
416
+ self.top = py - self._anchor[1]
417
+
418
+ # @property
419
+ # def image(self):
420
+ # if self._image_name is not None:
421
+ # return self._image_name
422
+ # return self._orig_surf
423
+ @property
424
+ def image(self) -> str:
425
+ return self._image_name or ""
426
+
427
+ @image.setter
428
+ def image(self, image):
429
+ if isinstance(image, pygame.Surface):
430
+ self._image_name = None
431
+ self._orig_surf = image
432
+ else:
433
+ self._image_name = image
434
+ self._orig_surf = loaders.images.load(image)
435
+
436
+ self._surface_cache.clear()
437
+ self._update_pos()
438
+
439
+ def _update_pos(self):
440
+ if not hasattr(self, "_orig_surf"):
441
+ return
442
+
443
+ p = getattr(self, "pos", (0, 0))
444
+
445
+ self.width, self.height = self._transformed_body_size()
446
+
447
+ self._calc_anchor()
448
+ self.pos = p
449
+
450
+ def _image_center_offset_from_body_center(self):
451
+ _, _, offset_x, offset_y = self._body_spec()
452
+
453
+ # offset_x / offset_y describe the body center compared to the image
454
+ # center, before scale or transforms. To draw the image, we need the
455
+ # opposite vector: image center compared to body center.
456
+ x = -offset_x * self._scale
457
+ y = -offset_y * self._scale
458
+
459
+ # The diagonal flip used by Tiled/Pygame here acts like swapping x/y
460
+ # around the body's center.
461
+ if self._flip_d:
462
+ x, y = y, x
463
+
464
+ if self._flip_h:
465
+ x = -x
466
+
467
+ if self._flip_v:
468
+ y = -y
469
+
470
+ if self._angle != 0.0:
471
+ x, y = rotate_vector(x, y, self._angle)
472
+
473
+ return x, y
474
+
475
+ def _image_topleft(self):
476
+ s = self._build_transformed_surf()
477
+ image_offset_x, image_offset_y = self._image_center_offset_from_body_center()
478
+ body_cx, body_cy = self.center
479
+
480
+ return (
481
+ body_cx + image_offset_x - s.get_width() / 2,
482
+ body_cy + image_offset_y - s.get_height() / 2,
483
+ )
484
+
485
+ def _move_body_to_preserve_image_topleft(self, image_topleft):
486
+ s = self._build_transformed_surf()
487
+ image_offset_x, image_offset_y = self._image_center_offset_from_body_center()
488
+
489
+ image_cx = image_topleft[0] + s.get_width() / 2
490
+ image_cy = image_topleft[1] + s.get_height() / 2
491
+
492
+ self.center = (
493
+ image_cx - image_offset_x,
494
+ image_cy - image_offset_y,
495
+ )
496
+
497
+ def draw(self):
498
+ s = self._build_transformed_surf()
499
+ game.screen.blit(s, self._image_topleft())
500
+
501
+ @property
502
+ def images(self):
503
+ return self._images
504
+
505
+ @images.setter
506
+ def images(self, images):
507
+ self._images = images
508
+ if self._images:
509
+ self.image = self._images[0]
510
+
511
+ def next_image(self):
512
+ if not self._images:
513
+ return
514
+
515
+ if self.image in self._images:
516
+ current = self._images.index(self.image)
517
+ self.image = self._images[(current + 1) % len(self._images)]
518
+ else:
519
+ self.image = self._images[0]
520
+
521
+ def animate(self):
522
+ now = int(time.time() * self.fps)
523
+ if now != self._animate_counter:
524
+ self._animate_counter = now
525
+ self.next_image()
526
+
527
+ def colliderect(self, other):
528
+ if hasattr(other, "_rect"):
529
+ other_rect = other._rect
530
+ else:
531
+ other_rect = other
532
+
533
+ return self._rect.colliderect(other_rect)
534
+
535
+ def collidelist(self, others):
536
+ for n, other in enumerate(others):
537
+ if self.colliderect(other):
538
+ return n
539
+ return -1
540
+
541
+ def collidepoint(self, *args):
542
+ return self._rect.collidepoint(*args)
543
+
544
+ @property
545
+ def collision_rect(self):
546
+ return self._rect
547
+
548
+ @collision_rect.setter
549
+ def collision_rect(self, value):
550
+ if value is not None and len(value) not in (2, 4):
551
+ raise ValueError("collision_rect must be None, (width, height), or " "(width, height, offset_x, offset_y).")
552
+
553
+ old_image_topleft = self._image_topleft() if hasattr(self, "_orig_surf") else None
554
+
555
+ self._collision_rect_spec = value
556
+ self._update_pos()
557
+
558
+ # Changing the body should not make the image appear to jump.
559
+ # The body moves to match the new collision rectangle, while the
560
+ # already-drawn image stays in the same visual place.
561
+ if old_image_topleft is not None:
562
+ self._move_body_to_preserve_image_topleft(old_image_topleft)
563
+
564
+ def draw_collision_rect(self):
565
+ r = self.collision_rect
566
+ pygame.draw.rect(
567
+ game.screen,
568
+ (255, 0, 0),
569
+ (r.x, r.y, r.w, r.h),
570
+ 2,
571
+ )
572
+
573
+ def angle_to(self, target):
574
+ """Return the angle from this actors position to target, in degrees."""
575
+ if isinstance(target, Actor):
576
+ tx, ty = target.pos
577
+ else:
578
+ tx, ty = target
579
+ myx, myy = self.pos
580
+ dx = tx - myx
581
+ dy = myy - ty
582
+ return degrees(atan2(dy, dx))
583
+
584
+ def distance_to(self, target):
585
+ """Return the distance from this actor's pos to target, in pixels."""
586
+ if isinstance(target, Actor):
587
+ tx, ty = target.pos
588
+ else:
589
+ tx, ty = target
590
+ myx, myy = self.pos
591
+ dx = tx - myx
592
+ dy = ty - myy
593
+ return sqrt(dx * dx + dy * dy)
594
+
595
+ def unload_image(self):
596
+ if self._image_name is not None:
597
+ loaders.images.unload(self._image_name)