pyscratch-pysc 1.0.3__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.
Files changed (101) hide show
  1. assets/bullet_hell/enemy.py +130 -0
  2. assets/bullet_hell/enemy_bullets.py +230 -0
  3. assets/bullet_hell/main.py +11 -0
  4. assets/bullet_hell/old_verisons/bullet_hell.py +379 -0
  5. assets/bullet_hell/old_verisons/enemy.py +226 -0
  6. assets/bullet_hell/old_verisons/game_start.py +6 -0
  7. assets/bullet_hell/old_verisons/main.py +50 -0
  8. assets/bullet_hell/old_verisons/player.py +76 -0
  9. assets/bullet_hell/player.py +89 -0
  10. assets/bullet_hell/player_bullets.py +34 -0
  11. assets/bullet_hell/setting.py +33 -0
  12. examples/animated_sprite/main.py +7 -0
  13. examples/animated_sprite/my_sprite.py +79 -0
  14. examples/bullet_hell/enemy.py +152 -0
  15. examples/bullet_hell/enemy_bullet.py +88 -0
  16. examples/bullet_hell/main.py +17 -0
  17. examples/bullet_hell/player.py +39 -0
  18. examples/bullet_hell/player_bullet.py +31 -0
  19. examples/doodle_jump/main.py +9 -0
  20. examples/doodle_jump/platforms.py +51 -0
  21. examples/doodle_jump/player.py +52 -0
  22. examples/fish/assets/bullet_hell/enemy.py +130 -0
  23. examples/fish/assets/bullet_hell/enemy_bullets.py +230 -0
  24. examples/fish/assets/bullet_hell/main.py +11 -0
  25. examples/fish/assets/bullet_hell/old_verisons/bullet_hell.py +379 -0
  26. examples/fish/assets/bullet_hell/old_verisons/enemy.py +226 -0
  27. examples/fish/assets/bullet_hell/old_verisons/game_start.py +6 -0
  28. examples/fish/assets/bullet_hell/old_verisons/main.py +50 -0
  29. examples/fish/assets/bullet_hell/old_verisons/player.py +76 -0
  30. examples/fish/assets/bullet_hell/player.py +89 -0
  31. examples/fish/assets/bullet_hell/player_bullets.py +34 -0
  32. examples/fish/assets/bullet_hell/setting.py +33 -0
  33. examples/fish/fish.py +67 -0
  34. examples/fish/main.py +4 -0
  35. examples/getting-started/step 1 - create a sprite/main.py +11 -0
  36. examples/getting-started/step 1 - create a sprite/player.py +5 -0
  37. examples/getting-started/step 2 - control a sprite/main.py +11 -0
  38. examples/getting-started/step 2 - control a sprite/player.py +42 -0
  39. examples/getting-started/step 3 - backdrops/main.py +17 -0
  40. examples/getting-started/step 3 - backdrops/player.py +33 -0
  41. examples/getting-started/step 4 - clone a sprite/enemy.py +53 -0
  42. examples/getting-started/step 4 - clone a sprite/main.py +17 -0
  43. examples/getting-started/step 4 - clone a sprite/player.py +32 -0
  44. examples/getting-started/step 4 - clone a sprite (simple)/enemy.py +42 -0
  45. examples/getting-started/step 4 - clone a sprite (simple)/main.py +17 -0
  46. examples/getting-started/step 4 - clone a sprite (simple)/player.py +32 -0
  47. examples/getting-started/step 5 - local variables/enemy.py +52 -0
  48. examples/getting-started/step 5 - local variables/main.py +17 -0
  49. examples/getting-started/step 5 - local variables/player.py +49 -0
  50. examples/getting-started/step 6 - shared variables/enemy.py +64 -0
  51. examples/getting-started/step 6 - shared variables/main.py +17 -0
  52. examples/getting-started/step 6 - shared variables/player.py +80 -0
  53. examples/getting-started/step 7 - Referencing other sprites/enemy.py +83 -0
  54. examples/getting-started/step 7 - Referencing other sprites/hearts.py +39 -0
  55. examples/getting-started/step 7 - Referencing other sprites/main.py +23 -0
  56. examples/getting-started/step 7 - Referencing other sprites/player.py +59 -0
  57. examples/getting-started/step 8 - sprite variables/enemy.py +98 -0
  58. examples/getting-started/step 8 - sprite variables/hearts.py +39 -0
  59. examples/getting-started/step 8 - sprite variables/main.py +22 -0
  60. examples/getting-started/step 8 - sprite variables/player.py +63 -0
  61. examples/getting-started/step 9 - messages/enemy.py +98 -0
  62. examples/getting-started/step 9 - messages/hearts.py +39 -0
  63. examples/getting-started/step 9 - messages/main.py +23 -0
  64. examples/getting-started/step 9 - messages/player.py +78 -0
  65. examples/perspective_background/main.py +14 -0
  66. examples/perspective_background/player.py +52 -0
  67. examples/perspective_background/trees.py +39 -0
  68. examples/simple_pong/ball.py +72 -0
  69. examples/simple_pong/left_paddle.py +36 -0
  70. examples/simple_pong/main.py +21 -0
  71. examples/simple_pong/right_paddle.py +37 -0
  72. examples/simple_pong/score_display.py +54 -0
  73. pyscratch/__init__.py +48 -0
  74. pyscratch/event.py +263 -0
  75. pyscratch/game_module.py +1589 -0
  76. pyscratch/helper.py +561 -0
  77. pyscratch/sprite.py +1920 -0
  78. pyscratch/tools/sprite_preview/left_panel/frame_preview_card.py +238 -0
  79. pyscratch/tools/sprite_preview/left_panel/frame_preview_panel.py +42 -0
  80. pyscratch/tools/sprite_preview/main.py +18 -0
  81. pyscratch/tools/sprite_preview/main_panel/animation_display.py +77 -0
  82. pyscratch/tools/sprite_preview/main_panel/frame_bin.py +33 -0
  83. pyscratch/tools/sprite_preview/main_panel/play_edit_ui.py +64 -0
  84. pyscratch/tools/sprite_preview/main_panel/set_as_sprite_folder.py +22 -0
  85. pyscratch/tools/sprite_preview/main_panel/sprite_edit_ui.py +174 -0
  86. pyscratch/tools/sprite_preview/main_panel/warning_message.py +25 -0
  87. pyscratch/tools/sprite_preview/right_panel/back_button.py +35 -0
  88. pyscratch/tools/sprite_preview/right_panel/cut_button.py +32 -0
  89. pyscratch/tools/sprite_preview/right_panel/cut_parameter_fitting.py +152 -0
  90. pyscratch/tools/sprite_preview/right_panel/cut_parameters.py +42 -0
  91. pyscratch/tools/sprite_preview/right_panel/file_display.py +84 -0
  92. pyscratch/tools/sprite_preview/right_panel/file_display_area.py +57 -0
  93. pyscratch/tools/sprite_preview/right_panel/spritesheet_view.py +262 -0
  94. pyscratch/tools/sprite_preview/right_panel/ss_select_corner.py +208 -0
  95. pyscratch/tools/sprite_preview/settings.py +14 -0
  96. pyscratch/tools/sprite_preview/utils/input_box.py +235 -0
  97. pyscratch/tools/sprite_preview/utils/render_wrapped_file_name.py +86 -0
  98. pyscratch_pysc-1.0.3.dist-info/METADATA +37 -0
  99. pyscratch_pysc-1.0.3.dist-info/RECORD +101 -0
  100. pyscratch_pysc-1.0.3.dist-info/WHEEL +5 -0
  101. pyscratch_pysc-1.0.3.dist-info/top_level.txt +3 -0
@@ -0,0 +1,1589 @@
1
+ """
2
+ Everything in this module is directly under the pyscratch namespace.
3
+ For example, instead of doing `pysc.game_module.is_key_pressed`,
4
+ you can also directly do `pysc.is_key_pressed`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ from functools import cache
9
+ from os import PathLike
10
+ import os
11
+ from pathlib import Path
12
+ import threading
13
+ import time
14
+ import json, inspect
15
+
16
+ import numpy as np
17
+ import pygame
18
+ import pymunk
19
+ from .event import _ConditionInterface, Event, Condition, TimerCondition, _declare_callback_type
20
+ from pymunk.pygame_util import DrawOptions
21
+ from typing import Any, Callable, Generic, Iterable, Literal, Optional, List, Dict, ParamSpec, Set, Tuple, TypeVar, Union, cast
22
+ from typing import TYPE_CHECKING
23
+ from . import helper
24
+
25
+ if TYPE_CHECKING:
26
+ from .sprite import Sprite
27
+
28
+ def _collision_begin(arbiter, space, data):
29
+ game = cast(Game, data['game'])
30
+ game._contact_pairs_set.add(arbiter.shapes)
31
+
32
+ for e, (a,b) in game._trigger_to_collision_pairs.items():
33
+ if (a._shape in arbiter.shapes) and (b._shape in arbiter.shapes):
34
+ e.trigger(arbiter)
35
+
36
+
37
+ colliding_types = arbiter.shapes[0].collision_type, arbiter.shapes[1].collision_type
38
+ collision_allowed = True
39
+ for collision_type, (allowed, triggers) in game._collision_type_to_trigger.items():
40
+ if collision_type in colliding_types:
41
+ [t.trigger(arbiter) for t in triggers]
42
+ collision_allowed = collision_allowed and allowed
43
+
44
+
45
+ if (arbiter.shapes[0].collision_type == 0) or (arbiter.shapes[1].collision_type == 0):
46
+ collision_allowed = False
47
+
48
+
49
+ return collision_allowed
50
+
51
+ def _collision_separate(arbiter, space, data):
52
+ game = cast(Game, data['game'])
53
+
54
+ if arbiter.shapes in game._contact_pairs_set:
55
+ game._contact_pairs_set.remove(arbiter.shapes)
56
+
57
+
58
+ reverse_order = arbiter.shapes[1], arbiter.shapes[0]
59
+ if reverse_order in game._contact_pairs_set:
60
+ game._contact_pairs_set.remove(reverse_order)
61
+
62
+
63
+ class _CloneEventManager:
64
+
65
+ def __init__(self):
66
+ # TODO: removed sprites stay here forever
67
+ self.identical_sprites_and_triggers: List[Tuple[Set[Sprite], List[Event]]] = []
68
+
69
+ def new_trigger(self, sprite:Sprite, trigger:Event):
70
+ new_lineage = True
71
+ for identical_sprites, triggers in self.identical_sprites_and_triggers:
72
+ if sprite in identical_sprites:
73
+ new_lineage = False
74
+ triggers.append(trigger)
75
+
76
+ if new_lineage:
77
+ self.identical_sprites_and_triggers.append((set([sprite]), [trigger]))
78
+
79
+
80
+ def on_clone(self, old_sprite:Sprite, new_sprite:Sprite):
81
+ # so that the cloning of the cloned sprite will trigger the same event
82
+ for identical_sprites, triggers in self.identical_sprites_and_triggers:
83
+ if not old_sprite in identical_sprites:
84
+ continue
85
+ identical_sprites.add(new_sprite)
86
+
87
+ for t in triggers:
88
+ t.trigger(new_sprite)
89
+
90
+
91
+
92
+ class _SpriteEventDependencyManager:
93
+
94
+ def __init__(self):
95
+
96
+ self.sprites: Dict[Sprite, List[Union[Event, _ConditionInterface]]] = {}
97
+
98
+ def add_event(self, event: Union[_ConditionInterface, Event], sprites: Iterable[Sprite]):
99
+ """
100
+ TODO: if the event is dependent to multiple sprites, the event will not be
101
+ completely dereferenced until all the sprites on which it depends are removed
102
+
103
+ """
104
+ for s in sprites:
105
+ if not s in self.sprites:
106
+ self.sprites[s] = []
107
+ self.sprites[s].append(event)
108
+
109
+
110
+ def sprite_removal(self, sprite: Sprite):
111
+
112
+ to_remove = self.sprites.get(sprite)
113
+ if not to_remove:
114
+ return
115
+
116
+ for e in to_remove:
117
+ e.remove()
118
+
119
+ T = TypeVar('T')
120
+ """@private"""
121
+ P = ParamSpec('P')
122
+ """@private"""
123
+
124
+ class _SpecificEventEmitter(Generic[P]):
125
+
126
+ def __init__(self):
127
+ self.key2triggers: Dict[Any, List[Event[P]]] = {}
128
+
129
+ def add_event(self, key, trigger:Event[P]):
130
+ if not key in self.key2triggers:
131
+ self.key2triggers[key] = []
132
+ self.key2triggers[key].append(trigger)
133
+
134
+
135
+ def on_event(self, key, *args: P.args, **kwargs: P.kwargs):
136
+ if not key in self.key2triggers:
137
+ return
138
+ for t in self.key2triggers[key]:
139
+ t.trigger(*args, **kwargs)
140
+
141
+
142
+
143
+ class _SavedSpriteStateManager:
144
+ default_filename = "saved_sprite_states.json"
145
+ def __init__(self):
146
+ self.states: Dict[str, Dict[str, Any]] = {}
147
+
148
+
149
+ def save_sprite_states(self, all_sprite: Iterable[Sprite], filename=None):
150
+ """
151
+ Save the x, y & direction of the sprites.
152
+
153
+ Usage:
154
+ ```python
155
+ # main.py
156
+ from pyscratch import game
157
+
158
+ game.start() # when you close the game window, the game.start() function finishes.
159
+ game.save_sprite_states() # then this function will be run.
160
+
161
+ ```
162
+ """
163
+ loc = {}
164
+ for s in all_sprite:
165
+ loc[s.identifier] = dict(x=s.x, y=s.y, direction=s.direction)
166
+
167
+ if not filename:
168
+ filename = self.default_filename
169
+ #caller_file = Path(inspect.stack()[-1].filename)
170
+
171
+ json.dump(loc, open(filename, "w"))
172
+ print("Sprite states saved.")
173
+
174
+ def load_saved_state(self, filename=None):
175
+
176
+ if not filename:
177
+ filename = self.default_filename
178
+
179
+ filename = Path(filename)
180
+ if filename.exists():
181
+ self.states= json.load(open(filename, "r"))
182
+
183
+
184
+ def get_state_of(self, sprite_id):
185
+
186
+ return self.states.get(sprite_id)
187
+
188
+
189
+ class Game:
190
+ """
191
+ This is the class that the `game` object belongs to. You cannot create another Game object.
192
+ To exit the game, either close the window, or to press the escape key (esc) by default
193
+ """
194
+
195
+ _singleton_lock = False
196
+ def __init__(self):
197
+ """@private"""
198
+ pygame.init()
199
+
200
+ assert not Game._singleton_lock, "Already instantiated."
201
+ Game._singleton_lock = True
202
+
203
+ # the screen is needed to load the images.
204
+ self._screen: pygame.Surface = pygame.display.set_mode((1280, 720), vsync=1)
205
+
206
+ self._space: pymunk.Space = pymunk.Space()
207
+
208
+
209
+ self._draw_options = DrawOptions(self._screen)
210
+
211
+ # sounds
212
+ self._mixer = pygame.mixer.init()
213
+
214
+ self._sounds = {}
215
+
216
+ # shared variables
217
+ self.shared_data: Dict[Any, Any] = {}
218
+ """
219
+ A dictionary of variables shared across the entire game. You can put anything in it.
220
+
221
+ The access of the items can be done directly through the game object.
222
+ For example, `game['my_data'] = "hello"` is just an alias of `game.shared_data['my_data'] = "hello"`
223
+
224
+ Bare in mind that the order of executions of different events and different files is arbitrary.
225
+ Therefore if variable is defined in one event and accessed in another,
226
+ a KeyError may be raised because variables are only accessible after the definition.
227
+
228
+ Instead, the all the variable should be defined outside the event (before the game start),
229
+ and variables should be accessed only within events to guarantee its definition.
230
+
231
+
232
+ Example:
233
+ ```python
234
+ from pyscratch import game
235
+
236
+ # same as `game.shared_data['score_left'] = 0`
237
+ game['score_left'] = 0
238
+ game['score_right'] = 0
239
+
240
+ def on_score(side):
241
+ if side == "left":
242
+ game['score_left'] += 1
243
+ else:
244
+ game['score_right'] += 1
245
+
246
+ print(f"Left score: {game['score_left']}")
247
+ print(f"Right score: {game['score_right']}")
248
+
249
+ game.when_received_message('score').add_handler(on_score)
250
+ game.broadcast_message('on_score', 'left')
251
+ ```
252
+ """
253
+
254
+ # sprite event dependency manager
255
+ self._sprite_event_dependency_manager = _SpriteEventDependencyManager()
256
+
257
+ #
258
+ self._clone_event_manager = _CloneEventManager()
259
+ """@private"""
260
+
261
+ # collision detection
262
+ self._trigger_to_collision_pairs: Dict[Event, Tuple[Sprite, Sprite]] = {}
263
+
264
+ self._collision_type_pair_to_trigger: Dict[Tuple[int, int], List[Event]] = {}
265
+
266
+ self._collision_type_to_trigger: Dict[int, Tuple[bool, List[Event]]] = {}
267
+
268
+ self._contact_pairs_set: Set[Tuple[pymunk.Shape, pymunk.Shape]] = set()
269
+
270
+
271
+ self._collision_handler = self._space.add_default_collision_handler()
272
+
273
+ self._collision_handler.data['game'] = self
274
+ self._collision_handler.begin = _collision_begin
275
+ self._collision_handler.separate = _collision_separate
276
+
277
+ # sprites updating and drawing
278
+ self._all_sprites = pygame.sprite.Group()
279
+
280
+ self._all_sprites_to_show = pygame.sprite.LayeredUpdates()
281
+
282
+
283
+ # # scheduled jobs
284
+ # self.pre_scheduled_jobs = []
285
+ # self.scheduled_jobs = []
286
+
287
+
288
+ self._all_pygame_events = []
289
+
290
+ self._all_triggers: List[Event] = [] # these are to be executed every iteration
291
+
292
+ self._all_conditions: List[_ConditionInterface] = [] # these are to be checked every iteration
293
+
294
+ #self.all_forever_jobs: List[Callable[[], None]] = []
295
+ self._all_message_subscriptions: Dict[str, List[Event]] = {}
296
+
297
+ # key events
298
+ key_event = self.create_pygame_event([pygame.KEYDOWN, pygame.KEYUP])
299
+ key_event.add_handler(self.__key_event_handler)
300
+ self._all_simple_key_triggers: List[Event] = [] # these are to be triggered by self.__key_event_handler only
301
+
302
+ # mouse dragging event
303
+ self._dragged_sprite = None
304
+ self._drag_offset = 0, 0
305
+ self.__clicked_sprite = None
306
+ self._sprite_click_release_trigger:Dict[Sprite, List[Event]] = {} #TODO: need to be able to destory the trigger here when the sprite is destoryed
307
+
308
+ self._sprite_click_trigger:Dict[Sprite, List[Event]] = {} #TODO: need to be able to destory the trigger here when the sprite is destoryed
309
+ self._mouse_drag_trigger = self.create_pygame_event([pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION])
310
+ self._mouse_drag_trigger.add_handler(self.__mouse_drag_handler)
311
+
312
+ ## Backdrops
313
+ self.backdrops: List[pygame.Surface] = []
314
+ """A list of all the loaded backdrop images. You will not need to interact with this property directly."""
315
+
316
+ self.__screen_width: int = 0
317
+ self.__screen_height: int = 0
318
+ self.__framerate: float = 0
319
+
320
+
321
+ self.__backdrop_index = None
322
+ self._backdrop_change_triggers: List[Event] = []
323
+
324
+ self._top_edge: Sprite
325
+ self._left_edge: Sprite
326
+ self._bottom_edge: Sprite
327
+ self._right_edge: Sprite
328
+
329
+
330
+ ## start event
331
+ self._game_start_triggers: List[Event] = []
332
+
333
+ ## global timer event
334
+ self._global_timer_triggers: List[Event] = []
335
+
336
+
337
+ self._current_time_ms: float = 0
338
+
339
+ self._specific_key_event_emitter: _SpecificEventEmitter[str] = _SpecificEventEmitter()
340
+
341
+ self._specific_backdrop_event_emitter: _SpecificEventEmitter[[]] = _SpecificEventEmitter()
342
+
343
+ self.max_number_sprite = 1000
344
+ """The maximum number of sprites in the game. Adding more sprites will lead to an error to prevent freezing. Default to 1000."""
345
+
346
+ self._sprite_count_per_file: Dict[str, int] = {}
347
+
348
+ self.__start = False
349
+ """set to false to end the loop"""
350
+
351
+
352
+ self.update_screen_mode()
353
+
354
+ self._saved_states_manager = _SavedSpriteStateManager()
355
+ self.__sprite_last_clicked_for_removal: Optional[Sprite] = None
356
+ self.__started_interactive = False
357
+
358
+
359
+ def __key_event_handler(self, e):
360
+ up_or_down = 'down' if e.type == pygame.KEYDOWN else 'up'
361
+ keyname = pygame.key.name(e.key)
362
+
363
+ self._specific_key_event_emitter.on_event(keyname, up_or_down)
364
+
365
+ for t in self._all_simple_key_triggers:
366
+ t.trigger(keyname, up_or_down)
367
+
368
+ def __getitem__(self, key):
369
+ return self.shared_data[key]
370
+
371
+ def __setitem__(self, k, v):
372
+ self.shared_data[k] = v
373
+
374
+
375
+ def __mouse_drag_handler(self, e):
376
+
377
+ if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
378
+
379
+ for s in reversed(list(self._all_sprites_to_show)):
380
+ if TYPE_CHECKING:
381
+ s = cast(Sprite, s)
382
+
383
+ # click is on the top sprite only
384
+ if s.is_touching_mouse():
385
+ self.__clicked_sprite = s
386
+ self.__sprite_last_clicked_for_removal = s
387
+
388
+ for t in self._sprite_click_trigger[s]:
389
+ t.trigger()
390
+
391
+ if not s.draggable:
392
+ break
393
+
394
+ s._set_is_dragging (True)
395
+ self._dragged_sprite = s
396
+ offset_x = s._body.position[0] - e.pos[0]
397
+ offset_y = s._body.position[1] - e.pos[1]
398
+ self._drag_offset = offset_x, offset_y
399
+ break
400
+
401
+
402
+
403
+ elif e.type == pygame.MOUSEBUTTONUP and e.button == 1:
404
+ if self._dragged_sprite:
405
+ self._dragged_sprite._set_is_dragging(False)
406
+ self._dragged_sprite = None
407
+
408
+ # TODO: what happens here?? why AND?
409
+ if self.__clicked_sprite and (temp:= self._sprite_click_release_trigger.get(self.__clicked_sprite)):
410
+ #temp = self._sprite_click_release_trigger.get(self.__clicked_sprite)
411
+ #if temp:
412
+ for t in temp:
413
+ t.trigger()
414
+ self.__clicked_sprite = None
415
+
416
+ elif e.type == pygame.MOUSEMOTION and self._dragged_sprite:
417
+ x = e.pos[0] + self._drag_offset[0]
418
+ y = e.pos[1] + self._drag_offset[1]
419
+ self._dragged_sprite.set_xy((x,y))
420
+
421
+
422
+
423
+ def update_screen_mode(self, *arg, **kwargs):
424
+ """
425
+ Update the screen, taking the arguments for
426
+ [`pygame.display.set_mode`](https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode).
427
+
428
+
429
+ Use this method to change the screen size:
430
+
431
+ `game.update_screen_mode((SCREEN_WIDTH, SCREEN_HEIGHT))`
432
+ """
433
+ self.__screen_args = arg
434
+ self.__screen_kwargs = kwargs
435
+
436
+ @property
437
+ def screen_width(self):
438
+ """The width of the screen. Not available until the game is started (should not be referenced outside events)."""
439
+ return self.__screen_width
440
+
441
+ @property
442
+ def screen_height(self):
443
+ """The height of the screen. Not available until the game is started (should not referenced outside events)."""
444
+ return self.__screen_height
445
+
446
+ @property
447
+ def framerate(self):
448
+ """The frame rate of the game. Not available until the game is started"""
449
+ return self.__framerate
450
+
451
+ def _do_autoremove(self):
452
+ for s in self._all_sprites:
453
+ if ((s.x < -s.oob_limit) or
454
+ (s.x > (s.oob_limit + self.__screen_width)) or
455
+ (s.y < -s.oob_limit) or
456
+ (s.y > (s.oob_limit + self.__screen_height))
457
+ ):
458
+ #print(s.x, s.y)
459
+ #print(s.oob_limit + self.__screen_width, s.oob_limit + self.__screen_height)
460
+ s.remove()
461
+ print(f"{s} is removed for going out of boundary above the specified limit.")
462
+
463
+ def _check_alive(self):
464
+
465
+ last_frame_time = 0
466
+ while True:
467
+
468
+ for i in range(10):
469
+ time.sleep(.2)
470
+ if not self.__start:
471
+ return
472
+ if not self._current_time_ms > last_frame_time:
473
+ print('Stucked in the same frame for more than 1 second. This can happen when an error occur or you are in a infinite loop without yielding. ')
474
+ os._exit(1)
475
+
476
+ last_frame_time = self._current_time_ms
477
+
478
+ def start_interactive(self, *args, sprite_removal_key='backspace', **kwargs):
479
+ """
480
+ @private
481
+ Start the game on another thread for jupyter notebook (experimental)
482
+ """
483
+ if self.__started_interactive:
484
+ raise RuntimeError("The game must not be restarted. Please restart the kernal. ")
485
+ self.__started_interactive = True
486
+
487
+ def remove_sprite(_):
488
+ if s:=self.__clicked_sprite:
489
+ s.remove()
490
+
491
+ self.when_key_pressed(sprite_removal_key).add_handler(remove_sprite)
492
+
493
+ t = threading.Thread(target=self.start, args=args, kwargs=kwargs)
494
+ t.start()
495
+ self.__screen_thread = t
496
+
497
+
498
+ return t
499
+
500
+ def stop(self):
501
+ """
502
+ @private
503
+ """
504
+ self.__start = False
505
+ if self.__started_interactive:
506
+ #self.__started_interactive = False
507
+ pygame.display.quit()
508
+
509
+
510
+ def start(
511
+ self,
512
+ framerate=30,
513
+ sim_step_min=300,
514
+ debug_draw=False,
515
+ event_count=False,
516
+ show_mouse_position: Optional[bool]=None,
517
+ exit_key: Optional[str]="escape",
518
+ saved_state_file=None,
519
+ print_fps = False,
520
+ use_frame_time = False
521
+ ):
522
+ """
523
+ Start the game.
524
+
525
+ Parameters
526
+ ---
527
+ framerate : int
528
+ The number of frames per second
529
+
530
+ sim_step_min: int
531
+ The number of physics steps per second. Increase this value if the physics is unstable and decrease it if the game runs slow.
532
+
533
+ debug_draw: bool
534
+ Whether or not to draw the collision shape for debugging purposes
535
+
536
+ event_count: bool
537
+ Whether or not to print out the number of active events for debugging purposes
538
+
539
+ show_mouse_position: bool
540
+ Whether or not to show the mouse position in the buttom-right corner
541
+
542
+ exit_key: Optional[str]
543
+ Very useful if you are working on a fullscreen game
544
+ Set to None to disable it.
545
+
546
+ saved_state_file: Optional[str]
547
+ The path of the saved state. Default location will be used if set to None.
548
+
549
+ print_fps: bool
550
+ Whether or not to print the fps
551
+
552
+ use_frame_time: bool
553
+ Use the number of frames to define game time. Note: highly experimental.
554
+
555
+ """
556
+
557
+ if not (len(self.__screen_args) or len(self.__screen_kwargs)):
558
+ self.__screen_kwargs = dict(size=(1280, 720))
559
+
560
+ self._screen = pygame.display.set_mode(*self.__screen_args, **self.__screen_kwargs)
561
+
562
+ self._saved_states_manager.load_saved_state(saved_state_file)
563
+ self.__framerate = framerate
564
+ self.__screen_width = self._screen.get_width()
565
+ self.__screen_height = self._screen.get_height()
566
+
567
+ guide_lines_font = pygame.font.Font(None, 30)
568
+
569
+ clock = pygame.time.Clock()
570
+
571
+
572
+
573
+ draw_every_n_step = sim_step_min//framerate+1
574
+
575
+ self._current_time_ms = 0
576
+
577
+ if exit_key:
578
+ self.when_key_pressed(exit_key).add_handler(lambda _: self.stop())
579
+
580
+ self.create_pygame_event([pygame.QUIT]).add_handler(lambda _: self.stop())
581
+
582
+ for t in self._game_start_triggers:
583
+ t.trigger()
584
+
585
+ cleanup_period = 2*framerate
586
+ loop_count = 0
587
+
588
+ threading.Thread(target=self._check_alive).start()
589
+ frame_interval = 1000/framerate
590
+
591
+ self.__start = True
592
+ while self.__start:
593
+ if print_fps:
594
+ print(f"FPS: {clock.get_fps()}")
595
+ loop_count += 1
596
+
597
+ dt = clock.tick(framerate)
598
+ self._current_time_ms += frame_interval if use_frame_time else dt
599
+ for i in range(draw_every_n_step):
600
+ self._space.step(dt/draw_every_n_step)
601
+
602
+ self._all_pygame_events = pygame.event.get()
603
+
604
+
605
+ # check conditions
606
+ for c in self._all_conditions:
607
+ c._check()
608
+
609
+ # execute
610
+ for t in self._all_triggers:
611
+ t._handle_all(self._current_time_ms)
612
+ # TODO: is it possible to remove t in the self.all_triggers here?
613
+ t._generators_proceed(self._current_time_ms)
614
+
615
+ # clean up
616
+ self._all_conditions = list(filter(lambda t: t.stay_active, self._all_conditions))
617
+ self._all_simple_key_triggers = list(filter(lambda t: t.stay_active, self._all_simple_key_triggers))
618
+ self._all_triggers = list(filter(lambda t: t.stay_active, self._all_triggers))
619
+
620
+
621
+ if event_count:
622
+ print("all_conditions", len(self._all_conditions))
623
+ print("all_triggers", len(self._all_triggers))
624
+ print("all sprite", len(self._all_sprites))
625
+ # print("all_simple_key_triggers", len(self.all_simple_key_triggers))
626
+
627
+ # Drawing
628
+
629
+ #self._screen.fill((30, 30, 30))
630
+ if not (self.__backdrop_index is None):
631
+ self._screen.blit(self.backdrops[self.__backdrop_index], (0, 0))
632
+ else:
633
+ self._screen.fill((255,255,255))
634
+ helper._draw_guide_lines(self._screen, guide_lines_font, 100, 500)
635
+
636
+ if show_mouse_position is None:
637
+ helper._show_mouse_position(self._screen, guide_lines_font)
638
+
639
+ if debug_draw:
640
+ self._space.debug_draw(self._draw_options)
641
+
642
+ self._all_sprites.update()
643
+ #self._all_sprites_to_show.update()
644
+ self._all_sprites_to_show.draw(self._screen)
645
+
646
+ if show_mouse_position:
647
+ helper._show_mouse_position(self._screen, guide_lines_font)
648
+
649
+
650
+ pygame.display.flip()
651
+ if not loop_count % cleanup_period:
652
+ self._do_autoremove()
653
+
654
+ def _get_saved_state(self, sprite_id):
655
+ return self._saved_states_manager.get_state_of(sprite_id)
656
+
657
+ def save_sprite_states(self):
658
+ """
659
+ *EXTENDED FEATURE, EXPERIMENTAL*
660
+
661
+ Save the x, y & direction of the sprites.
662
+
663
+ To retrieve the states of the sprites, refer to [`retrieve_saved_state`](./sprite#Sprite.retrieve_saved_state)
664
+
665
+
666
+ Usage:
667
+ ```python
668
+ from pyscratch import game
669
+
670
+ # Start the game. The program stays in this line until the game window is closed.
671
+ game.start(60)
672
+
673
+ # The program will get to this line when the game window is closed normally (not due to an error).
674
+ game.save_sprite_states()
675
+ ```
676
+
677
+ """
678
+ self._saved_states_manager.save_sprite_states(self._all_sprites)
679
+
680
+ def load_sound(self, key: str, path: str) :
681
+ """
682
+ Load the sound given a path, and index it with the key so it can be played later by `play_sound`
683
+
684
+ Example:
685
+ ```python
686
+ game.load_sound('sound1', 'path/to/sound.wav')
687
+ game.play_sound('sound1', volume=0.5)
688
+ ```
689
+ """
690
+ if key in self._sounds:
691
+ raise KeyError(f'{key} already loaded. Choose a different key name.')
692
+
693
+ self._sounds[key] = pygame.mixer.Sound(path)
694
+
695
+ def play_sound(self, key:str, volume=1.0):
696
+ """
697
+ Play the sound given a key.
698
+ This method does not wait for the sound to finish playing.
699
+
700
+ Example:
701
+ ```python
702
+ game.load_sound('sound1', 'path/to/sound.wav')
703
+ game.play_sound('sound1', volume=0.5)
704
+ ```
705
+ """
706
+ s = self._sounds[key]
707
+ s.set_volume(volume)
708
+ s.play()
709
+
710
+
711
+ def read_timer(self) -> float:
712
+ """get the time (in seconds) since the game started."""
713
+ return self._current_time_ms/1000
714
+
715
+
716
+ def set_gravity(self, xy: Tuple[float, float]):
717
+ """
718
+ @private
719
+ *EXTENDED FEATURE*
720
+
721
+ Change the gravity of the space. Works for sprites with dynamic body type only, which is not the default.
722
+ It will NOT work unless you explicitly make the sprite to have a dynamic body.
723
+ """
724
+ self._space.gravity = xy
725
+
726
+ def _new_sprite_of_file(self, caller_file):
727
+ if not caller_file in self._sprite_count_per_file:
728
+ self._sprite_count_per_file[caller_file] = 0
729
+ else:
730
+ self._sprite_count_per_file[caller_file] += 1
731
+
732
+ return self._sprite_count_per_file[caller_file]
733
+
734
+
735
+ def _add_sprite(self, sprite: Sprite, to_show=True, caller_file=None):
736
+
737
+ self._all_sprites.add(sprite)
738
+ if len(self._all_sprites) > self.max_number_sprite:
739
+ raise RuntimeError('Reached the maximum number sprite. ')
740
+ #self._space.add(sprite.body, sprite.shape)
741
+ self._sprite_click_trigger[sprite] = []
742
+ if to_show:
743
+ self._all_sprites_to_show.add(sprite)
744
+ sprite.update()
745
+
746
+ if self.__started_interactive:
747
+ sprite.set_draggable(True)
748
+
749
+ return self._new_sprite_of_file(caller_file)
750
+
751
+ def _cleanup_old_shape(self, old_shape):
752
+
753
+ remove_list = []
754
+ for pair in self._contact_pairs_set:
755
+ if old_shape in pair:
756
+ remove_list.append(pair)
757
+
758
+ for r in remove_list:
759
+ self._contact_pairs_set.remove(r)
760
+
761
+ def _remove_sprite(self, sprite: Sprite):
762
+ """
763
+ Remove the sprite from the game.
764
+
765
+ You can use the alias `Sprite.remove()` to do the same.
766
+ """
767
+
768
+ self._all_sprites.remove(sprite)
769
+ self._all_sprites_to_show.remove(sprite)
770
+
771
+ self._trigger_to_collision_pairs = {k: v for k, v in self._trigger_to_collision_pairs.items() if not sprite in v}
772
+
773
+
774
+ self._cleanup_old_shape(sprite._shape)
775
+
776
+ try:
777
+ self._space.remove(sprite._body, sprite._shape)
778
+ except:
779
+ print('removing non-existing shape or body')
780
+
781
+ self._sprite_event_dependency_manager.sprite_removal(sprite)
782
+
783
+
784
+ def _show_sprite(self, sprite: Sprite):
785
+ """
786
+ Show the sprite.
787
+
788
+ You can use the alias `sprite.show()` to do the same.
789
+ """
790
+ self._all_sprites_to_show.add(sprite)
791
+
792
+ def _hide_sprite(self, sprite: Sprite):
793
+ """
794
+ Hide the sprite.
795
+
796
+ You can use the alias `sprite.hide()` to do the same.
797
+ """
798
+ self._all_sprites_to_show.remove(sprite)
799
+
800
+ def bring_to_front(self, sprite: Sprite):
801
+ """
802
+ Bring the sprite to the front.
803
+ Analogous to the "go to [front] layer" block in Scratch
804
+ """
805
+ self._all_sprites_to_show.move_to_front(sprite)
806
+
807
+ def move_to_back(self, sprite: Sprite):
808
+ """
809
+ Move the sprite to the back.
810
+ Analogous to the "go to [back] layer" block in Scratch
811
+ """
812
+ self._all_sprites_to_show.move_to_back(sprite)
813
+
814
+ def change_layer(self, sprite: Sprite, layer: int):
815
+ """
816
+ Bring the sprite to a specific layer.
817
+ """
818
+ self._all_sprites_to_show.change_layer(sprite, layer)
819
+
820
+
821
+ def change_layer_by(self, sprite: Sprite, by: int):
822
+ """
823
+ Analogous to the "go to [forward/backward] [N] layer" block in Scratch
824
+ """
825
+ layer = self._all_sprites_to_show.get_layer_of_sprite(sprite)
826
+ self._all_sprites_to_show.change_layer(sprite, layer + by)
827
+
828
+ def get_layer_of_sprite(self, sprite: Sprite):
829
+ """
830
+ Returns the layer number of the given sprite
831
+ """
832
+ self._all_sprites_to_show.get_layer_of_sprite(sprite)
833
+
834
+ def set_backdrops(self, images: List[pygame.Surface]):
835
+ """
836
+ Set the list of all available backdrops. This function is meant to be run before the game start.
837
+
838
+ Example:
839
+ ```python
840
+ # load the image into python
841
+ background_image = pysc.load_image('assets/my_background.jpg')
842
+ background_image2 = pysc.load_image('assets/my_background2.jpg')
843
+ background_image3 = pysc.load_image('assets/my_background3.jpg')
844
+
845
+ # pass in a list of all the available backdrops.
846
+ pysc.game.set_backdrops([background_image, background_image2, background_image3])
847
+
848
+ # choose the backdrop at index 1 (background_image2)
849
+ pysc.game.switch_backdrop(1)
850
+ ```
851
+ """
852
+ self.backdrops = images
853
+
854
+ @property
855
+ def backdrop_index(self):
856
+ """
857
+ The index of the current backdrops. For example, if you do `game.switch_backdrop(0)`, `game.backdrop_index` would be `0`.
858
+ """
859
+ return self.__backdrop_index
860
+
861
+ def switch_backdrop(self, index:Optional[int]=None):
862
+ """
863
+ Change the backdrop by specifying the index of the backdrop.
864
+ """
865
+
866
+ if index != self.__backdrop_index:
867
+ self.__backdrop_index = index
868
+ for t in self._backdrop_change_triggers:
869
+ t.trigger(index)
870
+ self._specific_backdrop_event_emitter.on_event(self.__backdrop_index)
871
+
872
+ def next_backdrop(self):
873
+ """
874
+ Switch to the next backdrop.
875
+ """
876
+ if not self.__backdrop_index is None:
877
+ self.switch_backdrop((self.__backdrop_index+1) % len(self.backdrops))
878
+
879
+ # all events
880
+
881
+ ## scratch events
882
+
883
+ def when_game_start(self, associated_sprites : Iterable[Sprite]=[])->Event[[]]:
884
+ """
885
+ It is recommended to use the `Sprite.when_game_start` alias instead of this method,
886
+ so you don't need to specify the `associated_sprites` in every event.
887
+
888
+ Returns an `Event` that is triggered when you call `game.start`.
889
+ The event handler does not take in any parameter.
890
+
891
+ Parameters
892
+ ---
893
+ associated_sprites: List[Sprite]
894
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
895
+ """
896
+
897
+
898
+ t = self._create_event(associated_sprites)
899
+ self._game_start_triggers.append(t)
900
+
901
+ if TYPE_CHECKING:
902
+ def sample_callback()-> Any:
903
+ return
904
+ t = _declare_callback_type(t, sample_callback)
905
+ return t
906
+
907
+
908
+ def when_any_key_pressed(self, associated_sprites : Iterable[Sprite]=[]) -> Event[[str, str]]:
909
+ """
910
+ It is recommended to use the `Sprite.when_any_key_pressed` alias instead of this method,
911
+ so you don't need to specify the `associated_sprites` in every event.
912
+
913
+ Returns an `Event` that is triggered when a key is pressed or released.
914
+
915
+ The event handler have to take two parameters:
916
+ - **key** (str): The key that is pressed. For example, 'w', 'd', 'left', 'right', 'space'.
917
+ Uses [pygame.key.key_code](https://www.pygame.org/docs/ref/key.html#pygame.key.key_code) under the hood.
918
+
919
+ - **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
920
+
921
+ Parameters
922
+ ---
923
+ associated_sprites: List[Sprite]
924
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
925
+
926
+ """
927
+ t = self._create_event(associated_sprites)
928
+ self._all_simple_key_triggers.append(t)
929
+
930
+ if TYPE_CHECKING:
931
+ def sample_callback(key:str, updown:str)-> Any:
932
+ return
933
+ # this way the naming of the parameters is constrained too
934
+ t = _declare_callback_type(t, sample_callback)
935
+ #t = cast(Trigger[[str, str]], t)
936
+
937
+ return t
938
+
939
+ def when_key_pressed(self, key, associated_sprites : Iterable[Sprite]=[])-> Event[[str]]:
940
+ """
941
+ It is recommended to use the `Sprite.when_key_pressed` alias instead of this method,
942
+ so you don't need to specify the `associated_sprites` in every event.
943
+
944
+ Returns an `Event` that is triggered when a specific key is pressed or released.
945
+
946
+ The event handler have to take one parameter:
947
+ - **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
948
+
949
+ Parameters
950
+ ---
951
+ key: str
952
+ The key that triggers the event. For example, 'w', 'd', 'left', 'right', 'space'.
953
+ Uses [pygame.key.key_code](https://www.pygame.org/docs/ref/key.html#pygame.key.key_code) under the hood.
954
+
955
+ associated_sprites: List[Sprite]
956
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
957
+ """
958
+
959
+ t = self._create_event(associated_sprites)
960
+
961
+ if TYPE_CHECKING:
962
+ def sample_callback(updown:str)-> Any:
963
+ return
964
+ # this way the naming of the parameters is constrained too
965
+ t = _declare_callback_type(t, sample_callback)
966
+
967
+ self._specific_key_event_emitter.add_event(key, t)
968
+ return t
969
+
970
+ def when_this_sprite_clicked(self, sprite, other_associated_sprites: Iterable[Sprite]=[]) -> Event[[]]:
971
+ """
972
+ It is recommended to use the `Sprite.when_this_sprite_clicked` alias instead of this method.
973
+
974
+ Returns an `Event` that is triggered when the given sprite is clicked by mouse.
975
+ The event handler does not take in any parameter.
976
+
977
+ Parameters
978
+ ---
979
+ sprite: Sprite
980
+ The sprite on which you want the click to be detected. The removal of this sprite will lead to the removal of this event so
981
+ it does not need to be included in `other_assoicated_sprite`
982
+
983
+ other_associated_sprites: List[Sprite]
984
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
985
+ """
986
+
987
+ t = self._create_event(set(list(other_associated_sprites)+[sprite]))
988
+
989
+ if not sprite in self._sprite_click_trigger:
990
+ self._sprite_click_trigger[sprite] = []
991
+
992
+ self._sprite_click_trigger[sprite].append(t)
993
+ if TYPE_CHECKING:
994
+ def sample_callback()-> Any:
995
+ return
996
+ t = _declare_callback_type(t, sample_callback)
997
+ return t
998
+
999
+
1000
+ def when_this_sprite_click_released(self, sprite, other_associated_sprites: Iterable[Sprite]=[]) -> Event[[]]:
1001
+ """
1002
+ The alias `Sprite.when_this_sprite_click_released` is not yet implemented.
1003
+
1004
+ Returns an `Event` that is triggered when the mouse click of the given sprite is released.
1005
+ The event handler does not take in any parameter.
1006
+
1007
+ Parameters
1008
+ ---
1009
+ sprite: Sprite
1010
+ The sprite on which you want the click to be detected. The removal of this sprite will lead to the removal of this event so
1011
+ it does not need to be included in `other_assoicated_sprite`
1012
+
1013
+ other_associated_sprites: List[Sprite]
1014
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1015
+ """
1016
+
1017
+ t = self._create_event(set(list(other_associated_sprites)+[sprite]))
1018
+
1019
+ if not sprite in self._sprite_click_release_trigger:
1020
+ self._sprite_click_release_trigger[sprite] = []
1021
+
1022
+ self._sprite_click_release_trigger[sprite].append(t)
1023
+ if TYPE_CHECKING:
1024
+ def sample_callback()-> Any:
1025
+ return
1026
+ t = _declare_callback_type(t, sample_callback)
1027
+ return t
1028
+
1029
+
1030
+ def when_backdrop_switched(self, backdrop_index, associated_sprites : Iterable[Sprite]=[]) -> Event[[]]:
1031
+ """
1032
+ It is recommended to use the `Sprite.when_backdrop_switched` alias instead of this method,
1033
+ so you don't need to specify the `associated_sprites` in every event.
1034
+
1035
+ Returns an `Event` that is triggered when the game is switched to a backdrop at `backdrop_index`.
1036
+
1037
+ The event handler does not take in any parameter.
1038
+
1039
+ Parameters
1040
+ ---
1041
+ backdrop_index: int
1042
+ The index of the backdrop
1043
+
1044
+ associated_sprites: List[Sprite]
1045
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1046
+ """
1047
+
1048
+
1049
+ t = self._create_event(associated_sprites)
1050
+
1051
+ if TYPE_CHECKING:
1052
+ def sample_callback()-> Any:
1053
+ return
1054
+ t = _declare_callback_type(t, sample_callback)
1055
+
1056
+ self._specific_backdrop_event_emitter.add_event(backdrop_index, t)
1057
+ return t
1058
+
1059
+ def when_any_backdrop_switched(self, associated_sprites : Iterable[Sprite]=[]) -> Event[[int]]:
1060
+ """
1061
+ It is recommended to use the `Sprite.when_any_backdrop_switched` alias instead of this method,
1062
+ so you don't need to specify the `associated_sprites` in every event.
1063
+
1064
+ Returns an `Event` that is triggered when the backdrop is switched.
1065
+
1066
+ The event handler have to take one parameter:
1067
+ - **idx** (int): The index of the new backdrop
1068
+
1069
+ Parameters
1070
+ ---
1071
+ associated_sprites: List[Sprite]
1072
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1073
+ """
1074
+
1075
+ t = self._create_event(associated_sprites)
1076
+ self._backdrop_change_triggers.append(t)
1077
+ if TYPE_CHECKING:
1078
+ def sample_callback(idx: int)-> Any:
1079
+ return
1080
+ t = _declare_callback_type(t, sample_callback)
1081
+
1082
+ return t
1083
+
1084
+ def when_timer_above(self, t, associated_sprites : Iterable[Sprite]=[]) -> Condition:
1085
+ """
1086
+ It is recommended to use the `Sprite.when_timer_above` alias instead of this method,
1087
+ so you don't need to specify the `associated_sprites` in every event.
1088
+
1089
+ Returns a `Condition` that is triggered after the game have started for `t` seconds.
1090
+ A `Condition` works the same way an `Event` does.
1091
+
1092
+ The event handler have to take one parameter:
1093
+ - **n** (int): This value will always be zero
1094
+
1095
+ Parameters
1096
+ ---
1097
+ associated_sprites: List[Sprite]
1098
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1099
+ """
1100
+ t = t*1000
1101
+ return self.when_condition_met(lambda:(self._current_time_ms>t), 1, associated_sprites)
1102
+
1103
+ def when_started_as_clone(self, sprite, associated_sprites : Iterable[Sprite]=[]) -> Event[[Sprite]]:
1104
+ """
1105
+ Returns an `Event` that is triggered after the given sprite is cloned by `Sprite.create_clone`.
1106
+ Cloning of the clone will also trigger the event.
1107
+
1108
+ The event handler have to take one parameter:
1109
+ - **clone_sprite** (Sprite): The newly created clone.
1110
+
1111
+ Parameters
1112
+ ---
1113
+ associated_sprites: List[Sprite]
1114
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1115
+ """
1116
+
1117
+
1118
+ trigger = self._create_event(associated_sprites)
1119
+ self._clone_event_manager.new_trigger(sprite, trigger)
1120
+ if TYPE_CHECKING:
1121
+ def sample_callback(clone_sprite: Sprite)-> Any:
1122
+ return
1123
+ trigger = _declare_callback_type(trigger, sample_callback)
1124
+ return trigger
1125
+
1126
+ def when_receive_message(self, topic: str, associated_sprites : Iterable[Sprite]=[]) -> Event[[Any]]:
1127
+ """
1128
+ It is recommended to use the `Sprite.when_receive_message` alias instead of this method,
1129
+ so you don't need to specify the `associated_sprites` in every event.
1130
+
1131
+ Returns an `Event` that is triggered after a message of the given `topic` is broadcasted.
1132
+
1133
+ The event handler have to take one parameter:
1134
+ - **data** (Any): This parameter can be anything passed on by the message.
1135
+
1136
+ Parameters
1137
+ ---
1138
+ topic: str
1139
+ Can be any string. If the topic equals the topic of a broadcast, the event will be triggered.
1140
+ associated_sprites: List[Sprite]
1141
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1142
+ """
1143
+
1144
+
1145
+ trigger = self._create_event(associated_sprites)
1146
+ self.__new_subscription(topic, trigger)
1147
+ if TYPE_CHECKING:
1148
+ def sample_callback(data: Any)-> Any:
1149
+ return
1150
+ trigger = _declare_callback_type(trigger, sample_callback)
1151
+ return trigger
1152
+
1153
+
1154
+ def broadcast_message(self, topic: str, data: Any=None):
1155
+ """
1156
+ Sends a message of a given `topic` and `data`.
1157
+ Triggers any event that subscribes to the topic.
1158
+ The handlers of the events will receive `data` as the parameter.
1159
+
1160
+ Example:
1161
+ ```python
1162
+ def event_handler(data):
1163
+ print(data) # data will be "hello world!"
1164
+
1165
+ game.when_receive_message('print_message').add_handler(event_handler)
1166
+ game.broadcast_message('print_message', data='hello world!')
1167
+
1168
+ # "hello world!" will be printed out
1169
+ ```
1170
+ Parameters
1171
+ ---
1172
+ topic: str
1173
+ Can be any string. If the topic of an message event equals the topic of the broadcast, the event will be triggered.
1174
+
1175
+ data: Any
1176
+ Any arbitory data that will be passed to the event handler
1177
+
1178
+ """
1179
+ if not topic in self._all_message_subscriptions:
1180
+ return
1181
+
1182
+ self._all_message_subscriptions[topic] = list(filter(lambda t: t.stay_active, self._all_message_subscriptions[topic]))
1183
+ for e in self._all_message_subscriptions[topic]:
1184
+ e.trigger(data)
1185
+
1186
+ def __new_subscription(self, topic: str, trigger: Event):
1187
+ if not (topic in self._all_message_subscriptions):
1188
+ self._all_message_subscriptions[topic] = []
1189
+
1190
+ self._all_message_subscriptions[topic].append(trigger)
1191
+
1192
+ def start_handler(self, handler:Optional[Callable[[], Any]]=None, associated_sprites: Iterable[Sprite]=[]):
1193
+ """
1194
+ *EXTENDED FEATURE*
1195
+
1196
+ It is recommended to use the `Sprite.start_handler` alias instead of this method,
1197
+ so you don't need to specify the `associated_sprites` in every event.
1198
+
1199
+ Run the event handler immediately. Useful when creating a sprite within a function.
1200
+
1201
+ The handler does not take in any parameters.
1202
+
1203
+ Parameters
1204
+ ---
1205
+ handler: Function
1206
+ A function to run.
1207
+
1208
+ associated_sprites: List[Sprite]
1209
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1210
+
1211
+ """
1212
+ e = self._create_event(associated_sprites)
1213
+ if handler:
1214
+ e.add_handler(handler)
1215
+ e.trigger()
1216
+ return e
1217
+
1218
+
1219
+ ## advance events
1220
+ def create_pygame_event(self, types: List[int], associated_sprites : Iterable[Sprite]=[]) -> Event[[pygame.event.Event]]:
1221
+ """
1222
+ *EXTENDED FEATURE*
1223
+
1224
+ Receives specific types of pygame events when they happen.
1225
+
1226
+ See pygame.event for more details: https://www.pygame.org/docs/ref/event.html
1227
+
1228
+ ```python
1229
+ def key_press_event(event):
1230
+ # the event argument is a `pygame.event.Event`.
1231
+ if event.type == pygame.KEYDOWN and event.key == pygame.K_d:
1232
+ print("the key d is down")
1233
+
1234
+ if event.type == pygame.KEYUP and event.key == pygame.K_d:
1235
+ print("the key d is up")
1236
+
1237
+ pysc.game.create_pygame_event([pygame.KEYDOWN, pygame.KEYUP])
1238
+ ```
1239
+
1240
+ The event handler have to take one parameter:
1241
+ - **event** (pygame.event.Event): An pygame event object.
1242
+
1243
+ Parameters
1244
+ ---
1245
+ types: List[int]
1246
+ A list of the pygame event flags.
1247
+
1248
+ associated_sprites: List[Sprite]
1249
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1250
+ """
1251
+
1252
+
1253
+ condition = self.when_condition_met(associated_sprites)
1254
+
1255
+ def checker_hijack():
1256
+ for e in self._all_pygame_events:
1257
+ if e.type in types:
1258
+ condition.trigger.trigger(e)
1259
+
1260
+ if not condition.trigger.stay_active:
1261
+ condition.remove()
1262
+
1263
+ condition.change_checker(checker_hijack)
1264
+
1265
+ return cast(Event[pygame.event.Event], condition.trigger)
1266
+
1267
+
1268
+ def _create_specific_collision_trigger(self, sprite1: Sprite, sprite2: Sprite, other_associated_sprites: Iterable[Sprite]=[]):
1269
+ """
1270
+ *EXTENDED FEATURE, EXPERIMENTAL*
1271
+
1272
+ DOCUMENTATION NOT COMPLETED
1273
+
1274
+ Parameters
1275
+ ---
1276
+ associated_sprites: List[Sprite]
1277
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1278
+ """
1279
+ #"""Cannot change the collision type of the object after calling this function"""
1280
+ trigger = self._create_event(set(list(other_associated_sprites)+[sprite1, sprite2]))
1281
+
1282
+ self._trigger_to_collision_pairs[trigger] = sprite1, sprite2
1283
+
1284
+
1285
+ return trigger
1286
+
1287
+ def _create_type2type_collision_trigger(self, type_a:int, type_b:int, collision_suppressed=False, associated_sprites: Iterable[Sprite]=[]):
1288
+ """
1289
+ *EXTENDED FEATURE*
1290
+
1291
+ DOCUMENTATION NOT COMPLETED
1292
+
1293
+ Parameters
1294
+ ---
1295
+ associated_sprites: List[Sprite]
1296
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1297
+ """
1298
+
1299
+ pair = (type_a, type_b) if type_a>type_b else (type_b, type_a)
1300
+
1301
+ h = self._space.add_collision_handler(*pair)
1302
+ trigger = self._create_event(associated_sprites)
1303
+
1304
+
1305
+
1306
+ if not pair in self._collision_type_pair_to_trigger:
1307
+ self._collision_type_pair_to_trigger[pair] = []
1308
+ self._collision_type_pair_to_trigger[pair].append(trigger)
1309
+
1310
+ collision_allowed = not collision_suppressed
1311
+ def begin(arbiter, space, data):
1312
+ game = cast(Game, data['game'])
1313
+ game._contact_pairs_set.add(arbiter.shapes)
1314
+
1315
+ for t in game._collision_type_pair_to_trigger[pair]:
1316
+ t.trigger(arbiter)
1317
+ return collision_allowed
1318
+
1319
+ h.data['game'] = self
1320
+ h.begin = begin
1321
+ h.separate = _collision_separate
1322
+
1323
+
1324
+ return trigger
1325
+
1326
+
1327
+ def _create_type_collision_trigger(self, collision_type:int , collision_suppressed=False, associated_sprites: Iterable[Sprite]=[]):
1328
+ """
1329
+ *EXTENDED FEATURE*
1330
+
1331
+ DOCUMENTATION NOT COMPLETED
1332
+
1333
+ Parameters
1334
+ ---
1335
+ associated_sprites: List[Sprite]
1336
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1337
+ """
1338
+
1339
+ trigger = self._create_event(associated_sprites)
1340
+
1341
+ collision_allowed = not collision_suppressed
1342
+
1343
+
1344
+ if not collision_type in self._collision_type_to_trigger:
1345
+ self._collision_type_to_trigger[collision_type] = collision_allowed, []
1346
+
1347
+
1348
+ self._collision_type_to_trigger[collision_type][1].append(trigger)
1349
+
1350
+ return trigger
1351
+
1352
+ def _suppress_type_collision(self, collision_type, collision_suppressed=True):
1353
+ """
1354
+ *EXTENDED FEATURE*
1355
+
1356
+ DOCUMENTATION NOT COMPLETED
1357
+
1358
+ """
1359
+ collision_allowed = not collision_suppressed
1360
+
1361
+ if not collision_type in self._collision_type_to_trigger:
1362
+ self._collision_type_to_trigger[collision_type] = collision_allowed, []
1363
+ else:
1364
+ t_list = self._collision_type_to_trigger[collision_type][1]
1365
+ self._collision_type_to_trigger[collision_type] = collision_allowed, t_list
1366
+
1367
+
1368
+ def when_timer_reset(self, reset_period: Optional[float]=None, repeats: Optional[int]=None, associated_sprites: Iterable[Sprite]=[]) -> TimerCondition:
1369
+ """
1370
+ *EXTENDED FEATURE*
1371
+
1372
+ Repeats an event for `repeats` time for every `reset_period` seconds.
1373
+
1374
+ ```python
1375
+
1376
+ def print_counts(n):
1377
+ print(n) # n is the number of remaining repeats
1378
+
1379
+ # every one second, for 100 times
1380
+ pysc.game.when_timer_reset(1, 100).add_handler(print_counts)
1381
+
1382
+ # will start printing 99, 98, ..., 0 every 1 second.
1383
+ ```
1384
+
1385
+ The event handler have to take one parameter:
1386
+ - **n** (int): The number of remaining repeats
1387
+
1388
+ Parameters
1389
+ ---
1390
+ reset_period: float
1391
+ The reset period of the timer. The handlers are triggered on timer reset.
1392
+
1393
+ repeats: int or None
1394
+ How many times to repeat. Set to None for infinite repeats.
1395
+
1396
+ associated_sprites: List[Sprite]
1397
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1398
+ """
1399
+ if reset_period is None:
1400
+ reset_period = np.inf
1401
+
1402
+ if repeats is None:
1403
+ _repeats = np.inf
1404
+ else:
1405
+ _repeats = repeats
1406
+
1407
+ condition = TimerCondition(reset_period, _repeats)
1408
+ self._all_conditions.append(condition)
1409
+ self._all_triggers.append(condition.trigger)
1410
+
1411
+ self._sprite_event_dependency_manager.add_event(
1412
+ condition, associated_sprites
1413
+ )
1414
+ return condition
1415
+
1416
+
1417
+ def when_condition_met(self, checker=lambda: False, repeats: Optional[int]=None, associated_sprites: Iterable[Sprite]=[])-> Condition:
1418
+ """
1419
+ *EXTENDED FEATURE*
1420
+
1421
+ For every frame, if a condition is met, the event is triggered. Repeated up to `repeats` times.
1422
+
1423
+ The condition is provided by a function that takes no argument and returns a boolean.
1424
+
1425
+ ```python
1426
+ def slowly_move_sprite_out_of_edge(n):
1427
+ my_sprite.x += 1
1428
+
1429
+ pysc.game.when_condition_met(lambda: (my_sprite.x<0), None).add_handler(slowly_move_sprite_out_of_edge)
1430
+ ```
1431
+
1432
+ The event handler have to take one parameter:
1433
+ - **n** (int): The number of remaining repeats
1434
+
1435
+ Parameters
1436
+ ---
1437
+ checker: Callable[[], bool]
1438
+ A function that takes no argument and returns a boolean.
1439
+ The checker is run one every frame. If it returns true, the handler is called.
1440
+
1441
+ repeats: int or None
1442
+ How many times to repeat. Set to None for infinite repeats.
1443
+
1444
+
1445
+ Parameters
1446
+ ---
1447
+ associated_sprites: List[Sprite]
1448
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1449
+ """
1450
+
1451
+ if repeats is None:
1452
+ _repeats = np.inf
1453
+ else:
1454
+ _repeats = repeats
1455
+
1456
+ condition = Condition(checker, _repeats)
1457
+ self._all_conditions.append(condition)
1458
+ self._all_triggers.append(condition.trigger)
1459
+
1460
+ self._sprite_event_dependency_manager.add_event(
1461
+ condition, associated_sprites
1462
+ )
1463
+ return condition
1464
+
1465
+ def when_mouse_click(self, associated_sprites: Iterable[Sprite]=[] ) -> Event[[Tuple[int, int], int, str]]:
1466
+ """
1467
+ *EXTENDED FEATURE*
1468
+
1469
+ Returns an `Event` that is triggered when the mouse is clicked or released.
1470
+
1471
+ The event handler have to take three parameters:
1472
+ - **pos** (Tuple[int, int]): The location of the click
1473
+ - **button** (int): Indicates which button is clicked. 0 for left, 1 for middle, 2 for right and other numbers for scrolling.
1474
+ - **updown** (str): Either 'up' or 'down' that indicates whether it is a press or a release
1475
+
1476
+ Parameters
1477
+ ---
1478
+ associated_sprites: List[Sprite]
1479
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1480
+
1481
+ """
1482
+ event_internal = self.create_pygame_event([pygame.MOUSEBUTTONUP, pygame.MOUSEBUTTONDOWN], associated_sprites)
1483
+ event = self._create_event(associated_sprites)
1484
+ def handler(e: pygame.event.Event):
1485
+ updown = "up" if e.type == pygame.MOUSEBUTTONUP else "down"
1486
+ event.trigger(e.pos, e.button, updown)
1487
+ event_internal.add_handler(handler)
1488
+
1489
+ return event
1490
+
1491
+ def when_mouse_scroll(self, associated_sprites: Iterable[Sprite]=[] ) -> Event[[str]]:
1492
+ """
1493
+ *EXTENDED FEATURE*
1494
+
1495
+ Returns an `Event` that is triggered when the mouse is scrolled.
1496
+
1497
+ The event handler have to take one parameters:
1498
+ - **updown** (str): Either 'up' or 'down' that indicates whether it is a scroll up or a scroll down.
1499
+
1500
+ Parameters
1501
+ ---
1502
+ associated_sprites: List[Sprite]
1503
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1504
+
1505
+ """
1506
+ event_internal = self.create_pygame_event([pygame.MOUSEWHEEL], associated_sprites)
1507
+ event = self._create_event(associated_sprites)
1508
+ def handler(e):
1509
+ updown = "up" if e.y > 0 else "down"
1510
+ event.trigger(updown)
1511
+ event_internal.add_handler(handler)
1512
+
1513
+ return event
1514
+
1515
+
1516
+ def _create_event(self, associated_sprites: Iterable[Sprite]=[]) -> Event:
1517
+ """
1518
+
1519
+ Parameters
1520
+ ---
1521
+ associated_sprites: List[Sprite]
1522
+ A list of sprites that this event depends on. Removal of any of these sprites leads to the removal of the event.
1523
+ """
1524
+
1525
+ trigger = Event()
1526
+ self._all_triggers.append(trigger)
1527
+
1528
+ self._sprite_event_dependency_manager.add_event(
1529
+ trigger, associated_sprites
1530
+ )
1531
+ return trigger
1532
+
1533
+
1534
+ game = Game()
1535
+ """
1536
+ The singleton Game object. This is the object that represent the game.
1537
+ """
1538
+
1539
+
1540
+ def is_key_pressed(key: str) -> bool:
1541
+ """
1542
+ Returns a bool(True/False) that indicates if the given key is pressed.
1543
+
1544
+ Usage
1545
+ ```python
1546
+ if is_key_pressed('space'):
1547
+ print('space pressed')
1548
+ ```
1549
+ """
1550
+ keycode = pygame.key.key_code(key)
1551
+ result = pygame.key.get_pressed()
1552
+ return result[keycode]
1553
+
1554
+
1555
+ def get_mouse_pos() -> Tuple[int, int]:
1556
+ """
1557
+ Returns the mouse coordinate.
1558
+
1559
+ Usage
1560
+ ```python
1561
+ mouse_x, mouse_y = get_mouse_pos()
1562
+ ```
1563
+ """
1564
+ return pygame.mouse.get_pos()
1565
+
1566
+
1567
+ def get_mouse_presses() -> Tuple[bool, bool, bool]:
1568
+ """
1569
+ Returns the mouse presses.
1570
+ ```python
1571
+ is_left_click, is_middle_click, is_right_click = get_mouse_presses()
1572
+ ```
1573
+ """
1574
+ return pygame.mouse.get_pressed(num_buttons=3)
1575
+
1576
+ # def _is_touching(sprite_a:Sprite, sprite_b:Sprite):
1577
+ # """
1578
+ # pymunk
1579
+ # """
1580
+ # for pair in game._contact_pairs_set:
1581
+
1582
+ # if (sprite_a._shape in pair) and (sprite_b._shape in pair):
1583
+ # return True
1584
+ # return False
1585
+
1586
+
1587
+ # def _is_touching_mouse(sprite: Sprite):
1588
+ # return sprite._shape.point_query(pygame.mouse.get_pos()).distance <= 0
1589
+