pgzero-plus 0.0.1a1__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.
pgzero/loaders.py ADDED
@@ -0,0 +1,308 @@
1
+ import os
2
+ import os.path
3
+ import sys
4
+
5
+ import pygame.image
6
+ import pygame.mixer
7
+
8
+ from . import ptext
9
+
10
+ # Root directory for loaders
11
+ # This is modified by calling set_root(), which is called by the game runner.
12
+ root = "."
13
+
14
+
15
+ def set_root(path):
16
+ """Configure all loaders to load from the given root.
17
+
18
+ path may be a file (such as a Python source file), in which case the root
19
+ is set to its containing directory.
20
+
21
+ """
22
+ global root
23
+ path = os.path.abspath(path)
24
+ if os.path.isdir(path):
25
+ root = path
26
+ else:
27
+ root = os.path.dirname(path)
28
+ sys.path.insert(0, root)
29
+
30
+
31
+ class InvalidCase(Exception):
32
+ """Indicate case errors early so they don't bite cross-platform users."""
33
+
34
+
35
+ try:
36
+ import win32api
37
+ except ImportError:
38
+
39
+ def real_path(path):
40
+ return path
41
+
42
+ else:
43
+
44
+ def real_path(path):
45
+ """Get the real capitalisation of a path on Windows."""
46
+ if not os.path.exists(path):
47
+ return path
48
+ return win32api.GetLongPathNameW(win32api.GetShortPathName(path))
49
+
50
+
51
+ def validate_lowercase(relpath):
52
+ if relpath.lower() != relpath:
53
+ raise InvalidCase(
54
+ "%r is not lower case.\n"
55
+ "You must use lower case filenames. This is to avoid "
56
+ "portability problems when run on another operating system "
57
+ "(because filenames on some operating systems are case-"
58
+ "sensitive and others are not)." % relpath
59
+ )
60
+
61
+
62
+ def validate_compatible_path(path):
63
+ """Validate that the given path can be loaded cross-platform."""
64
+ relpath = os.path.relpath(path, root)
65
+ validate_lowercase(relpath)
66
+
67
+ real = real_path(os.path.join(root, relpath))
68
+ real_rel = os.path.relpath(real, root)
69
+
70
+ if real_rel != relpath:
71
+ raise InvalidCase(
72
+ "%s is mis-capitalised on disk as %r.\nYou should rename it to be "
73
+ "correctly lowercase, for cross-platform portability." % (relpath, real_rel)
74
+ )
75
+
76
+
77
+ class ResourceLoader:
78
+ """Abstract resource loader.
79
+
80
+ A resource loader is a singleton; resources are loaded from a named
81
+ subdirectory of the global 'root'. The `.load()` method actually loads
82
+ a resource.
83
+
84
+ Additionally, attribute access can be used to access and cache resources.
85
+ Dotted paths can be used to traverse directories.
86
+
87
+ """
88
+
89
+ SKIP_CASE_VALIDATION = False
90
+
91
+ def __init__(self, subpath):
92
+ self._subpath = subpath
93
+ self._cache = {}
94
+ self._have_root = False
95
+
96
+ def validate_root(self, name):
97
+ r = self._root()
98
+ self._have_root = os.path.exists(r)
99
+
100
+ if self._have_root:
101
+ if not self.SKIP_CASE_VALIDATION:
102
+ validate_compatible_path(r)
103
+ else:
104
+ raise KeyError(
105
+ "No '{subpath}' directory found to load {type} "
106
+ "'{name}'.".format(subpath=self._subpath, type=self.TYPE, name=name)
107
+ )
108
+
109
+ def _root(self):
110
+ return os.path.join(root, self._subpath)
111
+
112
+ @staticmethod
113
+ def cache_key(name, args, kwargs):
114
+ kwpairs = sorted(kwargs.items())
115
+ return (name, args, tuple(kwpairs))
116
+
117
+ def load(self, name, *args, **kwargs):
118
+ key = self.cache_key(name, args, kwargs)
119
+
120
+ if key in self._cache:
121
+ return self._cache[key]
122
+
123
+ if not self._have_root:
124
+ self.validate_root(name)
125
+
126
+ p = os.path.join(self._root(), name)
127
+
128
+ if not os.path.isfile(p):
129
+ for ext in self.EXTNS:
130
+ p = os.path.join(self._root(), name + "." + ext)
131
+ if os.path.exists(p):
132
+ break
133
+ else:
134
+ raise KeyError(
135
+ "No {type} found like '{name}'. "
136
+ "Are you sure the {type} exists?".format(type=self.TYPE, name=name)
137
+ )
138
+
139
+ if not self.SKIP_CASE_VALIDATION:
140
+ validate_compatible_path(p)
141
+
142
+ res = self._cache[key] = self._load(p, *args, **kwargs)
143
+ return res
144
+
145
+ def unload(self, name, *args, **kwargs):
146
+ key = self.cache_key(name, args, kwargs)
147
+ if key in self._cache:
148
+ del self._cache[key]
149
+
150
+ def unload_all(self):
151
+ self._cache.clear()
152
+
153
+ def __getattr__(self, name):
154
+ p = os.path.join(self._root(), name)
155
+ if os.path.isdir(p):
156
+ resource = self.__class__(os.path.join(self._subpath, name))
157
+ else:
158
+ try:
159
+ resource = self.load(name)
160
+ except KeyError as e:
161
+ raise AttributeError(*e.args) from None
162
+
163
+ setattr(self, name, resource)
164
+ return resource
165
+
166
+ def __dir__(self):
167
+ standard_attributes = [key for key in self.__dict__.keys() if not key.startswith("_")]
168
+ resources = os.listdir(self._root())
169
+ resource_names = [os.path.splitext(r) for r in resources]
170
+ loadable_names = [name for name, ext in resource_names if name.isidentifier() and ext[1:] in self.EXTNS]
171
+ return standard_attributes + loadable_names
172
+
173
+
174
+ class ImageLoader(ResourceLoader):
175
+ EXTNS = ["png", "gif", "jpg", "jpeg", "bmp", "webp"]
176
+ TYPE = "image"
177
+
178
+ def _load(self, path):
179
+ return pygame.image.load(path).convert_alpha()
180
+
181
+ def __repr__(self):
182
+ return "<Images images={}>".format(self.__dir__())
183
+
184
+
185
+ class UnsupportedFormat(Exception):
186
+ """The resource was not in a supported format."""
187
+
188
+
189
+ class SoundLoader(ResourceLoader):
190
+ EXTNS = ["wav", "ogg", "oga"]
191
+ TYPE = "sound"
192
+
193
+ def __getattr__(self, name: str) -> pygame.mixer.Sound:
194
+ return super().__getattr__(name)
195
+
196
+ def _load(self, path):
197
+ try:
198
+ return pygame.mixer.Sound(path)
199
+ except pygame.error as err:
200
+ if not err.args[0].startswith(
201
+ (
202
+ "Unable to open file",
203
+ "Unknown WAVE format tag",
204
+ "MPEG formats not supported",
205
+ )
206
+ ):
207
+ raise
208
+ from .soundfmt import identify
209
+
210
+ try:
211
+ fmt = identify(path)
212
+ except Exception:
213
+ pass
214
+ else:
215
+ raise UnsupportedFormat("""
216
+ '{0}' is not in a supported audio format.
217
+
218
+ It appears to be:
219
+
220
+ {1}
221
+
222
+ Pygame supports only uncompressed WAV files (PCM or ADPCM) and compressed
223
+ Ogg Vorbis files. Try re-encoding the sound file, for example using Audacity:
224
+
225
+ http://audacityteam.org/
226
+ """.format(path, fmt).strip()) from None
227
+ raise
228
+
229
+ def __repr__(self):
230
+ try:
231
+ sound_list = self.__dir__()
232
+ except OSError:
233
+ sound_list = []
234
+ return "<Sounds sounds={}>".format(sound_list)
235
+
236
+
237
+ class FontLoader(ResourceLoader):
238
+ EXTNS = ["ttf"]
239
+ TYPE = "font"
240
+
241
+ def _load(self, path, fontsize=None):
242
+ return pygame.font.Font(path, fontsize or ptext.DEFAULT_FONT_SIZE)
243
+
244
+
245
+ class MapImageLoader(ImageLoader):
246
+ SKIP_CASE_VALIDATION = True
247
+
248
+
249
+ class TextLoader(ResourceLoader):
250
+ EXTNS = ["tmx", "tsx"]
251
+ TYPE = "map file"
252
+ SKIP_CASE_VALIDATION = True
253
+
254
+ def _load(self, path):
255
+ with open(path, "r", encoding="utf-8") as f:
256
+ return f.read()
257
+
258
+
259
+ images = ImageLoader("images")
260
+ sprites = ImageLoader("sprites")
261
+ mapimages = MapImageLoader("maps")
262
+ maps = TextLoader("maps")
263
+ sounds = SoundLoader("sounds")
264
+ fonts = FontLoader("fonts")
265
+
266
+
267
+ def getfont(
268
+ fontname=None,
269
+ fontsize=None,
270
+ sysfontname=None,
271
+ bold=None,
272
+ italic=None,
273
+ underline=None,
274
+ ):
275
+ """Monkey-patch for ptext.getfont().
276
+
277
+ This will use our loader and therefore obey our case validation, caching
278
+ and so on.
279
+
280
+ """
281
+ fontname = fontname or ptext.DEFAULT_FONT_NAME
282
+ fontsize = fontsize or ptext.DEFAULT_FONT_SIZE
283
+
284
+ key = (fontname, fontsize, sysfontname, bold, italic, underline)
285
+
286
+ if key in ptext._font_cache:
287
+ return ptext._font_cache[key]
288
+
289
+ if fontname is None:
290
+ font = ptext._font_cache.get(key)
291
+ if font:
292
+ return font
293
+ font = pygame.font.SysFont(sysfontname, fontsize)
294
+ else:
295
+ font = fonts.load(fontname, fontsize)
296
+
297
+ if bold is not None:
298
+ font.set_bold(bold)
299
+ if italic is not None:
300
+ font.set_italic(italic)
301
+ if underline is not None:
302
+ font.set_underline(underline)
303
+
304
+ ptext._font_cache[key] = font
305
+ return font
306
+
307
+
308
+ ptext.getfont = getfont
pgzero/music.py ADDED
@@ -0,0 +1,180 @@
1
+ from pygame.mixer import music as _music
2
+ from .loaders import ResourceLoader
3
+ from . import constants
4
+
5
+
6
+ __all__ = [
7
+ "rewind",
8
+ "stop",
9
+ "fadeout",
10
+ "set_volume",
11
+ "get_volume",
12
+ "get_pos",
13
+ "set_pos",
14
+ "play",
15
+ "play_once",
16
+ "queue",
17
+ "is_playing",
18
+ "is_paused",
19
+ "pause",
20
+ "unpause",
21
+ ]
22
+
23
+ _music.set_endevent(constants.MUSIC_END)
24
+
25
+
26
+ class _MusicLoader(ResourceLoader):
27
+ """Pygame's music API acts as a singleton with one 'current' track.
28
+
29
+ No objects are returned that represent different tracks, so this loader
30
+ can't return anything useful. But it can perform all the path name
31
+ validations and return the validated path, so that's what we do.
32
+
33
+ This loader should not be exposed to the user.
34
+
35
+ """
36
+
37
+ EXTNS = ["mp3", "ogg", "oga"]
38
+ TYPE = "music"
39
+
40
+ def _load(self, path):
41
+ return path
42
+
43
+
44
+ _loader = _MusicLoader("music")
45
+
46
+
47
+ # State of whether we are paused or not
48
+ _paused = False
49
+ # Current track that is playing
50
+ _current_track = None
51
+ # Track queued to play next, if any
52
+ _queued_track = None
53
+
54
+
55
+ def _play(name, loop):
56
+ global _paused, _current_track, _queued_track
57
+ path = _loader.load(name)
58
+ _music.load(path)
59
+ _music.play(loop)
60
+ _paused = False
61
+ _current_track = name
62
+ _queued_track = None
63
+
64
+
65
+ def _on_music_end():
66
+ """Update tracked music state when the current track finishes.
67
+
68
+ If a queued track exists, it becomes the current track. Otherwise,
69
+ no track is considered current anymore.
70
+
71
+ """
72
+ global _paused, _current_track, _queued_track
73
+
74
+ _paused = False
75
+
76
+ if _queued_track is not None:
77
+ _current_track = _queued_track
78
+ _queued_track = None
79
+ else:
80
+ _current_track = None
81
+
82
+
83
+ def play(name):
84
+ """Play a music file from the music/ directory.
85
+
86
+ The music will loop when it finishes playing.
87
+
88
+ """
89
+ _play(name, -1)
90
+
91
+
92
+ def play_once(name):
93
+ """Play a music file from the music/ directory."""
94
+ _play(name, 0)
95
+
96
+
97
+ def queue(name):
98
+ """Queue a music file to follow the current track.
99
+
100
+ This will load a music file and queue it. A queued music file will begin as
101
+ soon as the current music naturally ends. If the current music is ever
102
+ stopped or changed, the queued song will be lost.
103
+
104
+ """
105
+ global _queued_track
106
+ path = _loader.load(name)
107
+ _music.queue(path)
108
+ _queued_track = name
109
+
110
+
111
+ def is_playing(name=None):
112
+ """Return True if the music is playing and not paused.
113
+
114
+ If `name` is provided, return True only if that specific track is the
115
+ current track being played.
116
+
117
+ """
118
+ if not (_music.get_busy() and not _paused):
119
+ return False
120
+
121
+ if name is None:
122
+ return True
123
+
124
+ return _current_track == name
125
+
126
+
127
+ def is_paused():
128
+ """Return True if the music is currently paused."""
129
+ return _paused
130
+
131
+
132
+ def pause():
133
+ """Temporarily stop playback of the music stream.
134
+
135
+ Call `unpause()` to resume.
136
+
137
+ """
138
+ global _paused
139
+ if _music.get_busy():
140
+ _music.pause()
141
+ _paused = True
142
+
143
+
144
+ def unpause():
145
+ """Resume playback of the music stream after it has been paused."""
146
+ global _paused
147
+ if _paused:
148
+ _music.unpause()
149
+ _paused = False
150
+
151
+
152
+ def fadeout(seconds):
153
+ """Fade out and eventually stop the music playback.
154
+
155
+ :param seconds: The duration in seconds over which the sound will be faded
156
+ out. For example, to fade out over half a second, call
157
+ ``music.fadeout(0.5)``.
158
+
159
+ """
160
+ global _paused, _current_track, _queued_track
161
+ _music.fadeout(int(seconds * 1000))
162
+ _paused = False
163
+ _current_track = None
164
+ _queued_track = None
165
+
166
+
167
+ def stop():
168
+ """Stop playback of the music."""
169
+ global _paused, _current_track, _queued_track
170
+ _music.stop()
171
+ _paused = False
172
+ _current_track = None
173
+ _queued_track = None
174
+
175
+
176
+ rewind = _music.rewind
177
+ get_volume = _music.get_volume
178
+ set_volume = _music.set_volume
179
+ get_pos = _music.get_pos
180
+ set_pos = _music.set_pos