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/game.py ADDED
@@ -0,0 +1,573 @@
1
+ import sys
2
+ import operator
3
+ import time
4
+ import types
5
+ from time import perf_counter, sleep
6
+
7
+ import pygame
8
+ import pgzero.clock
9
+ import pgzero.keyboard
10
+ import pgzero.screen
11
+ import pgzero.loaders
12
+ import builtins
13
+
14
+ from . import constants
15
+ from typing import Any
16
+
17
+ screen = None # This global surface is what actors draw to
18
+ DISPLAY_FLAGS = pygame.SHOWN
19
+
20
+
21
+ class Game:
22
+ """
23
+ Simple game-wide state container for beginner projects.
24
+
25
+ Use this to keep track of things like score, level, timers, and
26
+ the current game state without needing lots of global variables.
27
+ """
28
+
29
+ def __init__(self):
30
+ self._start_callback = None
31
+ self._clock = None
32
+ self._is_restarting = False
33
+ self._restart_clear_mode = "immediate"
34
+ self.reset()
35
+
36
+ def _bind_engine(self, start_callback=None, clock=None):
37
+ self._start_callback = start_callback
38
+ self._clock = clock
39
+
40
+ def reset(self):
41
+ """Reset the game back to its default starting values."""
42
+ self.state = "playing"
43
+
44
+ self.score = 0
45
+ self.level = 1
46
+
47
+ self.timer = 0
48
+ self.frame_count = 0
49
+
50
+ self.data = {}
51
+
52
+ def restart(self, delay=0.0, clear_clock="immediate"):
53
+ """
54
+ Restart the game.
55
+
56
+ clear_clock options:
57
+ - "immediate": clear scheduled events right away
58
+ - "on_restart": clear scheduled events when the restart actually happens
59
+ - "never": do not clear scheduled events
60
+
61
+ Note: any previously scheduled restart is always unscheduled first.
62
+ """
63
+ if self._is_restarting:
64
+ raise RuntimeError(
65
+ "game.restart() was called from inside start(). "
66
+ "Do not call game.restart() inside start(). "
67
+ "The library already calls start() during restart."
68
+ )
69
+
70
+ if clear_clock not in ("immediate", "on_restart", "never"):
71
+ raise ValueError("clear_clock must be 'immediate', 'on_restart', or 'never'")
72
+
73
+ self._check_can_delay(delay)
74
+
75
+ self._restart_clear_mode = clear_clock
76
+
77
+ if self._clock:
78
+ # Always cancel any pending restart first
79
+ self._clock.unschedule(self._do_restart)
80
+
81
+ # Only clear everything else immediately if requested
82
+ if clear_clock == "immediate":
83
+ self._clock.clear()
84
+
85
+ if delay and delay > 0:
86
+ self._clock.schedule_unique(self._do_restart, delay)
87
+ return
88
+
89
+ self._do_restart()
90
+
91
+ def _check_can_delay(self, delay):
92
+ """Raise a helpful error if a delayed restart can't be scheduled.
93
+
94
+ Delays need the game clock, which is only connected once the game
95
+ starts running, so this happens when the call is at the top level
96
+ of the game file instead of inside a function.
97
+ """
98
+ if delay and delay > 0 and self._clock is None:
99
+ raise RuntimeError(
100
+ "A delayed restart can only be used while the game is "
101
+ "running. Call it from inside a function such as update() "
102
+ "or on_key_down(), not at the top level of your file."
103
+ )
104
+
105
+ def _do_restart(self):
106
+ if self._is_restarting:
107
+ return
108
+
109
+ self._is_restarting = True
110
+ try:
111
+ # Delay clock clearing until the actual restart moment
112
+ if self._clock and self._restart_clear_mode == "on_restart":
113
+ self._clock.clear()
114
+ # clear() may remove this scheduled callback, so make sure
115
+ # we do not accidentally re-enter or depend on future calls
116
+
117
+ self.reset()
118
+ if callable(self._start_callback):
119
+ self._start_callback()
120
+ finally:
121
+ self._is_restarting = False
122
+
123
+ def _end(self, state, restart_after, clear_clock):
124
+ """Leave the "playing" state, optionally scheduling a restart."""
125
+ if self.state != "playing":
126
+ return
127
+ self._check_can_delay(restart_after)
128
+ self.state = state
129
+
130
+ if self._clock and clear_clock == "immediate":
131
+ self._clock.clear()
132
+
133
+ if restart_after is not None:
134
+ restart_mode = clear_clock if clear_clock != "immediate" else "never"
135
+ self.restart(restart_after, clear_clock=restart_mode)
136
+
137
+ def win(self, restart_after=None, clear_clock="immediate"):
138
+ self._end("won", restart_after, clear_clock)
139
+
140
+ def lose(self, restart_after=None, clear_clock="immediate"):
141
+ self._end("lost", restart_after, clear_clock)
142
+
143
+ def over(self, restart_after=None, clear_clock="immediate"):
144
+ self._end("over", restart_after, clear_clock)
145
+
146
+ def start(self):
147
+ """Start or resume normal gameplay."""
148
+ self.state = "playing"
149
+
150
+ def pause(self):
151
+ """Pause the game."""
152
+ if self.state == "playing":
153
+ self.state = "paused"
154
+
155
+ def resume(self):
156
+ """Resume the game if paused."""
157
+ if self.state == "paused":
158
+ self.state = "playing"
159
+
160
+ def next_level(self):
161
+ """Advance to the next level."""
162
+ self.level += 1
163
+
164
+ def add_score(self, points):
165
+ """Increase the score by the given number of points."""
166
+ self.score += points
167
+
168
+ def tick(self, dt):
169
+ """
170
+ Update common counters.
171
+ dt = time since last frame (in seconds)
172
+ """
173
+ self.frame_count += 1
174
+
175
+ if self.state == "playing":
176
+ self.timer += dt
177
+
178
+ # These two methods don't change how attributes work at runtime. Their
179
+ # type hints tell editors and linters (e.g. Pylance) that students may
180
+ # add their own attributes, such as ``game.lives = 3``, without being
181
+ # warned that the attribute doesn't exist. Do not remove them.
182
+ def __getattr__(self, name: str) -> Any:
183
+ raise AttributeError(f"'Game' object has no attribute '{name}'")
184
+
185
+ def __setattr__(self, name: str, value: Any) -> None:
186
+ object.__setattr__(self, name, value)
187
+
188
+
189
+ def exit():
190
+ """Wait for up to a second for all sounds to play out
191
+ and then exit
192
+ """
193
+ t0 = time.time()
194
+ while pygame.mixer.get_busy():
195
+ time.sleep(0.1)
196
+ if time.time() - t0 > 1.0:
197
+ break
198
+ sys.exit()
199
+
200
+
201
+ def positional_parameters(handler):
202
+ """Get the positional parameters of the given function."""
203
+ code = handler.__code__
204
+ return code.co_varnames[: code.co_argcount]
205
+
206
+
207
+ class DEFAULTICON:
208
+ """Sentinel indicating that we want to use the default icon."""
209
+
210
+
211
+ class PGZeroGame:
212
+ """The core game loop for Pygame Zero.
213
+
214
+ Dispatch events, call update functions, draw. Repeat.
215
+ """
216
+
217
+ def __init__(self, mod: types.ModuleType, fps: bool = False):
218
+ """Construct a game loop given the pgzero module mod.
219
+
220
+ If fps is True, show a FPS count at the bottom left of the window.
221
+ """
222
+ self.mod = mod
223
+ self.screen = None
224
+ self.width = None
225
+ self.height = None
226
+ self.title = None
227
+ self.icon = None
228
+ self.fps = fps
229
+ self.keyboard = pgzero.keyboard.keyboard
230
+ self.handlers = {}
231
+
232
+ def reinit_screen(self) -> bool:
233
+ """Reinitialise the window.
234
+
235
+ Return True if the dimensions of the screen changed.
236
+
237
+ """
238
+ global screen
239
+ changed = False
240
+ mod = self.mod
241
+
242
+ icon = getattr(self.mod, "ICON", DEFAULTICON)
243
+ if icon and icon != self.icon:
244
+ self.show_icon()
245
+
246
+ w = getattr(mod, "WIDTH", 800)
247
+ h = getattr(mod, "HEIGHT", 600)
248
+ if w != self.width or h != self.height:
249
+ self.screen = pygame.display.set_mode((w, h), DISPLAY_FLAGS, vsync=1)
250
+ pgzero.screen.screen_instance._set_surface(self.screen)
251
+
252
+ # Set the global screen that actors blit to
253
+ screen = self.screen
254
+ self.width = w
255
+ self.height = h
256
+
257
+ # Dimensions changed, request a redraw
258
+ changed = True
259
+
260
+ title = getattr(self.mod, "TITLE", "Pygame Zero Game")
261
+ if title != self.title:
262
+ pygame.display.set_caption(title)
263
+ self.title = title
264
+
265
+ return changed
266
+
267
+ @staticmethod
268
+ def show_default_icon():
269
+ """Show a default icon loaded from Pygame Zero resources."""
270
+ from io import BytesIO
271
+ from pkgutil import get_data
272
+
273
+ buf = BytesIO(get_data(__name__, "data/icon.png"))
274
+ pygame.display.set_icon(pygame.image.load(buf))
275
+
276
+ def show_icon(self):
277
+ icon = getattr(self.mod, "ICON", DEFAULTICON)
278
+ if icon is DEFAULTICON:
279
+ self.show_default_icon()
280
+ else:
281
+ pygame.display.set_icon(pgzero.loaders.images.load(icon))
282
+ self.icon = icon
283
+
284
+ EVENT_HANDLERS = {
285
+ pygame.MOUSEBUTTONDOWN: "on_mouse_down",
286
+ pygame.MOUSEBUTTONUP: "on_mouse_up",
287
+ pygame.MOUSEMOTION: "on_mouse_move",
288
+ pygame.KEYDOWN: "on_key_down",
289
+ pygame.KEYUP: "on_key_up",
290
+ constants.MUSIC_END: "on_music_end",
291
+ }
292
+
293
+ def map_buttons(val):
294
+ return {c for c, pressed in zip(constants.mouse, val) if pressed}
295
+
296
+ EVENT_PARAM_MAPPERS = {"buttons": map_buttons, "button": constants.mouse, "key": constants.keys}
297
+
298
+ def load_handlers(self):
299
+ from .spellcheck import spellcheck
300
+
301
+ spellcheck(vars(self.mod))
302
+ self.handlers = {}
303
+ for type, name in self.EVENT_HANDLERS.items():
304
+ handler = getattr(self.mod, name, None)
305
+ if callable(handler):
306
+ self.handlers[type] = self.prepare_handler(handler)
307
+
308
+ def prepare_handler(self, handler):
309
+ """Adapt a pgzero game's raw handler function to take a Pygame Event.
310
+
311
+ Returns a one-argument function of the form ``handler(event)``.
312
+ This will ensure that the correct arguments are passed to the raw
313
+ handler based on its argument spec.
314
+
315
+ The wrapped handler will also map certain parameter values using
316
+ callables from EVENT_PARAM_MAPPERS; this ensures that the value of
317
+ 'button' inside the handler is a real instance of constants.mouse,
318
+ which means (among other things) that it will print as a symbolic value
319
+ rather than a naive integer.
320
+
321
+ """
322
+ code = handler.__code__
323
+ param_names = code.co_varnames[: code.co_argcount]
324
+
325
+ def make_getter(mapper, getter):
326
+ if mapper:
327
+ return lambda event: mapper(getter(event))
328
+ return getter
329
+
330
+ param_handlers = []
331
+ for name in param_names:
332
+ getter = operator.attrgetter(name)
333
+ mapper = self.EVENT_PARAM_MAPPERS.get(name)
334
+ param_handlers.append((name, make_getter(mapper, getter)))
335
+
336
+ def prep_args(event):
337
+ return {name: get(event) for name, get in param_handlers}
338
+
339
+ def new_handler(event):
340
+ try:
341
+ prepped = prep_args(event)
342
+ except ValueError:
343
+ # If we couldn't construct the keys/mouse objects representing
344
+ # the button that was pressed, then skip the event handler.
345
+ #
346
+ # This happens because Pygame can generate key codes that it
347
+ # does not have constants for.
348
+ return
349
+ else:
350
+ return handler(**prepped)
351
+
352
+ return new_handler
353
+
354
+ def get_update_func(self):
355
+ """Get a one-argument update function.
356
+
357
+ If the module defines a function matching ::
358
+
359
+ update(dt)
360
+
361
+ or ::
362
+
363
+ update()
364
+
365
+ then this will be called. Otherwise return a no-op function.
366
+
367
+ """
368
+ try:
369
+ update = self.mod.update
370
+ except AttributeError:
371
+ return None
372
+ else:
373
+ if update.__code__.co_argcount == 0:
374
+ return lambda dt: update()
375
+ return update
376
+
377
+ def get_draw_func(self):
378
+ """Get a draw function.
379
+
380
+ If no draw function is define, raise an exception.
381
+
382
+ """
383
+ try:
384
+ draw = self.mod.draw
385
+ except AttributeError:
386
+ return lambda: None
387
+ else:
388
+ if draw.__code__.co_argcount != 0:
389
+ raise TypeError("draw() must not take any arguments.")
390
+ return draw
391
+
392
+ def get_start_func(self):
393
+ """Get the start() function from the user module if it exists."""
394
+ start = getattr(self.mod, "start", None)
395
+ if start is not None and not callable(start):
396
+ raise TypeError(f"{start!r} is not callable")
397
+ return start
398
+
399
+ def run(self):
400
+ """Invoke the main loop, and then clean up."""
401
+ try:
402
+ self.mainloop()
403
+ finally:
404
+ pygame.display.quit()
405
+ pygame.mixer.quit()
406
+
407
+ def inject_global_handlers(self):
408
+ """Inject handlers provide by the Pygame Zero system.
409
+
410
+ Some of these wrap user handlers so must be injected later.
411
+ """
412
+ self.handlers[pygame.QUIT] = lambda e: sys.exit(0)
413
+ self.handlers[pygame.VIDEOEXPOSE] = lambda e: None
414
+
415
+ user_key_down = self.handlers.get(pygame.KEYDOWN)
416
+ user_key_up = self.handlers.get(pygame.KEYUP)
417
+
418
+ def key_down(event):
419
+ if event.key == pygame.K_q and event.mod & (pygame.KMOD_CTRL | pygame.KMOD_META):
420
+ sys.exit(0)
421
+ self.keyboard._press(event.key)
422
+ if user_key_down:
423
+ return user_key_down(event)
424
+
425
+ def key_up(event):
426
+ self.keyboard._release(event.key)
427
+ if user_key_up:
428
+ return user_key_up(event)
429
+
430
+ self.handlers[pygame.KEYDOWN] = key_down
431
+ self.handlers[pygame.KEYUP] = key_up
432
+
433
+ def tick_game_state(self, dt):
434
+ """Tick the global game object if it exists."""
435
+ game_obj = getattr(self.mod, "game", None)
436
+
437
+ if game_obj is None:
438
+ game_obj = getattr(builtins, "game", None)
439
+
440
+ if game_obj is None:
441
+ return False
442
+
443
+ tick = getattr(game_obj, "tick", None)
444
+ if not callable(tick):
445
+ return False
446
+
447
+ tick(dt)
448
+ return True
449
+
450
+ def handle_events(self, dt, update) -> bool:
451
+ """Handle all events for the current frame.
452
+
453
+ Return True if an event was handled.
454
+ """
455
+ updated = False
456
+
457
+ for event in pygame.event.get():
458
+ if event.type == constants.MUSIC_END:
459
+ # internal music bookkeeping
460
+ from . import music
461
+
462
+ music._on_music_end()
463
+
464
+ handler = self.handlers.get(event.type)
465
+ if handler:
466
+ handler(event)
467
+ updated = True
468
+
469
+ clock = pgzero.clock.clock
470
+ clock.tick(dt)
471
+ updated |= clock.fired
472
+
473
+ updated |= self.tick_game_state(dt)
474
+
475
+ if update:
476
+ update(dt)
477
+ updated = True
478
+
479
+ updated |= self.reinit_screen()
480
+ return updated
481
+
482
+ def mainloop(self):
483
+ """Run the main loop of Pygame Zero."""
484
+ self.reinit_screen()
485
+
486
+ update = self.get_update_func()
487
+ draw = self.get_draw_func()
488
+ start = self.get_start_func()
489
+
490
+ game_obj = getattr(self.mod, "game", None)
491
+ if game_obj is None:
492
+ game_obj = getattr(builtins, "game", None)
493
+
494
+ if game_obj is not None and hasattr(game_obj, "_bind_engine"):
495
+ game_obj._bind_engine(start_callback=start, clock=pgzero.clock.clock)
496
+
497
+ self.load_handlers()
498
+ self.inject_global_handlers()
499
+
500
+ if start:
501
+ start()
502
+
503
+ logic_timer = Timer("logic", print=self.fps)
504
+ draw_timer = Timer("draw", print=self.fps)
505
+ for i, dt in enumerate(frames(60)):
506
+ with logic_timer:
507
+ updated = self.handle_events(dt, update)
508
+
509
+ if updated:
510
+ with draw_timer:
511
+ draw()
512
+
513
+ if self.fps and i and i % 60 == 0:
514
+ ftime_ms = draw_timer.get_mean() + logic_timer.get_mean()
515
+ fps = 1000 / ftime_ms
516
+
517
+ print(f"fps: {fps:0.1f} time per frame: {ftime_ms:0.1f}ms")
518
+ pygame.display.flip()
519
+
520
+
521
+ def frames(fps=60):
522
+ """Iterate over frames at the given fps, yielding time delta (in s)."""
523
+ tgt = 1 / fps # target frame time
524
+
525
+ t = perf_counter()
526
+ dt = tgt
527
+
528
+ while True:
529
+ yield dt
530
+ nextt = perf_counter()
531
+ dt = nextt - t
532
+ if dt < tgt:
533
+ sleep(tgt - dt)
534
+ nextt = perf_counter()
535
+ dt = nextt - t
536
+ t = nextt
537
+
538
+
539
+ class Timer:
540
+ """Context manager to time the game loop."""
541
+
542
+ __slots__ = (
543
+ "name",
544
+ "total",
545
+ "count",
546
+ "worst",
547
+ "start",
548
+ "print",
549
+ )
550
+
551
+ def __init__(self, name, print=False):
552
+ self.name = name
553
+ self.total = 0
554
+ self.count = 0
555
+ self.worst = 0
556
+ self.print = print
557
+
558
+ def __enter__(self):
559
+ self.start = perf_counter()
560
+
561
+ def __exit__(self, *_):
562
+ t = (perf_counter() - self.start) * 1e3
563
+ self.count += 1
564
+ self.total += t
565
+ if t > self.worst:
566
+ self.worst = t
567
+
568
+ def get_mean(self) -> float:
569
+ mean = self.total / self.count
570
+ if self.print:
571
+ print(f"{self.name} mean: {mean:0.1f}ms " f"worst: {self.worst:0.1f}ms")
572
+ self.worst = self.total = self.count = 0
573
+ return mean
pgzero/keyboard.py ADDED
@@ -0,0 +1,67 @@
1
+ import re
2
+ from warnings import warn
3
+
4
+ from .constants import keys
5
+
6
+ DEPRECATED_KEY_RE = re.compile(r"[A-Z]")
7
+ PREFIX_RE = re.compile(r"^K_(?!\d$)")
8
+
9
+
10
+ class Keyboard:
11
+ """The current state of the keyboard.
12
+
13
+ Each attribute represents a key. For example, ::
14
+
15
+ keyboard.a
16
+
17
+ is True if the 'A' key is depressed, and False otherwise.
18
+
19
+ """
20
+
21
+ # The current key state. This may as well be a class attribute - there's
22
+ # only one keyboard.
23
+ _pressed = set()
24
+
25
+ def __getattr__(self, kname):
26
+ # return is a reserved word, so alias enter to return
27
+ if kname == "enter":
28
+ kname = "return"
29
+ elif DEPRECATED_KEY_RE.match(kname):
30
+ # warn(
31
+ # "Uppercase keyboard attributes (eg. keyboard.%s) are "
32
+ # "deprecated." % kname,
33
+ # DeprecationWarning,
34
+ # 2
35
+ # )
36
+ kname = PREFIX_RE.sub("", kname)
37
+ try:
38
+ key = keys[kname.upper()]
39
+ except AttributeError:
40
+ raise AttributeError('The key "%s" does not exist' % key)
41
+ return key.value in self._pressed
42
+
43
+ def _press(self, key):
44
+ """Called by Game to mark the key as pressed."""
45
+ self._pressed.add(key)
46
+
47
+ def _release(self, key):
48
+ """Called by Game to mark the key as released."""
49
+ self._pressed.discard(key)
50
+
51
+ def __getitem__(self, k):
52
+ if isinstance(k, keys):
53
+ return k.value in self._pressed
54
+ else:
55
+ # warn(
56
+ # "String lookup in keyboard (eg. keyboard[%r]) is "
57
+ # "deprecated." % k,
58
+ # DeprecationWarning,
59
+ # 2,
60
+ # )
61
+ return getattr(self, k)
62
+
63
+ def __repr__(self):
64
+ return "<Keyboard pressed={}>".format(self._pressed)
65
+
66
+
67
+ keyboard = Keyboard()