pgzero-plus 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pgzero-plus might be problematic. Click here for more details.
- pgzero/__init__.py +41 -0
- pgzero/__main__.py +3 -0
- pgzero/actor.py +597 -0
- pgzero/animation.py +244 -0
- pgzero/builtins.py +45 -0
- pgzero/clock.py +200 -0
- pgzero/constants.py +33 -0
- pgzero/data/icon.png +0 -0
- pgzero/data/joypad.png +0 -0
- pgzero/game.py +573 -0
- pgzero/keyboard.py +67 -0
- pgzero/loaders.py +308 -0
- pgzero/music.py +180 -0
- pgzero/ptext.py +500 -0
- pgzero/rect.py +523 -0
- pgzero/runner.py +233 -0
- pgzero/screen.py +210 -0
- pgzero/soundfmt.py +105 -0
- pgzero/spellcheck.py +173 -0
- pgzero/sprites.py +182 -0
- pgzero/storage.py +191 -0
- pgzero/tilemaps.py +307 -0
- pgzero/tone.py +132 -0
- pgzero_plus-0.1.0.dist-info/METADATA +259 -0
- pgzero_plus-0.1.0.dist-info/RECORD +30 -0
- pgzero_plus-0.1.0.dist-info/WHEEL +5 -0
- pgzero_plus-0.1.0.dist-info/entry_points.txt +2 -0
- pgzero_plus-0.1.0.dist-info/licenses/COPYING +165 -0
- pgzero_plus-0.1.0.dist-info/top_level.txt +2 -0
- pgzrun.py +34 -0
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
|