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/runner.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from . import storage
|
|
2
|
+
from . import clock
|
|
3
|
+
from . import loaders
|
|
4
|
+
from . import __version__
|
|
5
|
+
from .game import PGZeroGame, DISPLAY_FLAGS
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
from argparse import ArgumentParser
|
|
8
|
+
import warnings
|
|
9
|
+
import sys
|
|
10
|
+
import os
|
|
11
|
+
import pygame
|
|
12
|
+
|
|
13
|
+
pygame.mixer.pre_init(frequency=22050, size=-16, channels=2)
|
|
14
|
+
pygame.init()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _check_python_ok_for_pygame():
|
|
18
|
+
"""If we're on a Mac, is this a full Framework python?
|
|
19
|
+
|
|
20
|
+
There is a problem with PyGame on Macs running in a virtual env.
|
|
21
|
+
If the Python used is from the venv, it will not allow full window and
|
|
22
|
+
keyboard interaction. Instead, we need the original framework Python
|
|
23
|
+
to get PyGame working properly.
|
|
24
|
+
|
|
25
|
+
The problem doesn't occur on Linux and Windows.
|
|
26
|
+
"""
|
|
27
|
+
if sys.platform == "darwin": # This is a Mac
|
|
28
|
+
return "Library/Frameworks" in sys.executable
|
|
29
|
+
else:
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _substitute_full_framework_python():
|
|
34
|
+
"""Need to change the OS/X Python executable to the full Mac version,
|
|
35
|
+
while maintaining the virtualenv environment, so things still run
|
|
36
|
+
in an encapsulated way.
|
|
37
|
+
|
|
38
|
+
We do this by extract the paths that virtualenv has added to the system
|
|
39
|
+
path, and prefixing them to the current PYTHONPATH.
|
|
40
|
+
|
|
41
|
+
Then we use os.execv() to start a replacement process that uses the
|
|
42
|
+
same environment as the previous one.
|
|
43
|
+
"""
|
|
44
|
+
PYVER = "{}.{}".format(*sys.version_info[:2])
|
|
45
|
+
base_fw = "/Library/Frameworks/Python.framework/Versions/"
|
|
46
|
+
framework_python = base_fw + "{pv}/bin/python{pv}".format(pv=PYVER)
|
|
47
|
+
venv_base = os.environ.get("VIRTUAL_ENV")
|
|
48
|
+
if not venv_base or not os.path.exists(framework_python):
|
|
49
|
+
# Do nothing if virtual env hasn't been set up or if we can't
|
|
50
|
+
# find the framework Python interpreter
|
|
51
|
+
return
|
|
52
|
+
venv_paths = [p for p in sys.path if p.startswith(venv_base)]
|
|
53
|
+
# Need to allow for PYTHONPATH not already existing in environment
|
|
54
|
+
os.environ["PYTHONPATH"] = ":".join(venv_paths + [os.environ.get("PYTHONPATH", "")]).rstrip(":")
|
|
55
|
+
# Pass command line args to the new process
|
|
56
|
+
os.execv(framework_python, ["python", "-m", "pgzero"] + sys.argv[1:])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main():
|
|
60
|
+
|
|
61
|
+
# Pygame won't run from a normal virtualenv copy of Python on a Mac
|
|
62
|
+
if not _check_python_ok_for_pygame():
|
|
63
|
+
_substitute_full_framework_python()
|
|
64
|
+
|
|
65
|
+
parser = ArgumentParser()
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--fps",
|
|
68
|
+
action="store_true",
|
|
69
|
+
help="Print periodic FPS measurements on the terminal.",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
72
|
+
parser.add_argument("game", help="The Pygame Zero game to run (a Python file or directory).")
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
|
|
75
|
+
if __debug__:
|
|
76
|
+
warnings.simplefilter("default", DeprecationWarning)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
load_and_run(args.game, fps=args.fps)
|
|
80
|
+
except NoMainModule as e:
|
|
81
|
+
sys.exit(e)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class NoMainModule(Exception):
|
|
85
|
+
"""Indicate that we couldn't find a main module to run."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_and_run(path, *, fps: bool = False):
|
|
89
|
+
"""Load and run the given Python file or directory.
|
|
90
|
+
|
|
91
|
+
If a file, run this as the main PGZero game module.
|
|
92
|
+
|
|
93
|
+
If a directory, run the first file inside the directory containing:
|
|
94
|
+
|
|
95
|
+
* `__main__.py`
|
|
96
|
+
* `main.py`
|
|
97
|
+
* `run_game.py`
|
|
98
|
+
* `<name>.py` where `<name>` is the basename of the directory
|
|
99
|
+
|
|
100
|
+
Note that the 'import pgzrun' IDE mode doesn't pass through this entry
|
|
101
|
+
point, as the module is already loaded.
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
path = path.rstrip(os.sep)
|
|
105
|
+
try:
|
|
106
|
+
with open(path, "rb") as f:
|
|
107
|
+
src = f.read()
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
raise NoMainModule(f"Error: {path} does not exist.")
|
|
110
|
+
except (IsADirectoryError, PermissionError):
|
|
111
|
+
name = os.path.basename(path)
|
|
112
|
+
for candidate in (
|
|
113
|
+
"__main__.py",
|
|
114
|
+
"main.py",
|
|
115
|
+
"run_game.py",
|
|
116
|
+
f"{name}.py",
|
|
117
|
+
):
|
|
118
|
+
try:
|
|
119
|
+
with open(os.path.join(path, candidate), "rb") as f:
|
|
120
|
+
src = f.read()
|
|
121
|
+
break
|
|
122
|
+
except FileNotFoundError:
|
|
123
|
+
pass
|
|
124
|
+
else:
|
|
125
|
+
raise NoMainModule(f"""\
|
|
126
|
+
Error: {path} is a directory.
|
|
127
|
+
|
|
128
|
+
To run a directory with pgzrun, it must contain a file named __main__.py,
|
|
129
|
+
main.py, run_game.py, or a .py file with the same name as the directory.
|
|
130
|
+
|
|
131
|
+
You can also run a specific file with:
|
|
132
|
+
|
|
133
|
+
pgzrun path/to/your/file.py
|
|
134
|
+
""")
|
|
135
|
+
else:
|
|
136
|
+
name, _ = os.path.splitext(os.path.basename(path))
|
|
137
|
+
|
|
138
|
+
code = compile(src, os.path.basename(path), "exec", dont_inherit=True)
|
|
139
|
+
mod = ModuleType(name)
|
|
140
|
+
mod.__file__ = path
|
|
141
|
+
mod.__name__ = name
|
|
142
|
+
sys.modules[name] = mod
|
|
143
|
+
|
|
144
|
+
# Indicate that we're running with the pgzrun runner
|
|
145
|
+
# This disables the 'import pgzrun' module
|
|
146
|
+
sys._pgzrun = True
|
|
147
|
+
|
|
148
|
+
prepare_mod(mod)
|
|
149
|
+
exec(code, mod.__dict__)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
run_mod(mod, fps=fps)
|
|
153
|
+
finally:
|
|
154
|
+
# Clean some of the state we created, useful in testing
|
|
155
|
+
pygame.display.quit()
|
|
156
|
+
clock.clock.clear()
|
|
157
|
+
del sys.modules[name]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def prepare_mod(mod):
|
|
161
|
+
"""Prepare to execute the module code for Pygame Zero.
|
|
162
|
+
|
|
163
|
+
To allow the module to load assets, we configure the loader path to
|
|
164
|
+
load relative to the module's __file__ path.
|
|
165
|
+
|
|
166
|
+
When executing the module some things need to already exist:
|
|
167
|
+
|
|
168
|
+
* Our extra builtins need to be defined (by copying them into Python's
|
|
169
|
+
`builtins` module)
|
|
170
|
+
* A window needs to exist (because we use convert_alpha() to convert
|
|
171
|
+
image surfaces for blitting to the screen). See open_loading_window().
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
storage.storage._set_filename_from_path(mod.__file__)
|
|
175
|
+
loaders.set_root(mod.__file__)
|
|
176
|
+
|
|
177
|
+
open_loading_window()
|
|
178
|
+
|
|
179
|
+
from .game import Game
|
|
180
|
+
from . import builtins as pgzero_builtins
|
|
181
|
+
import builtins as python_builtins
|
|
182
|
+
|
|
183
|
+
for name in pgzero_builtins.__all__:
|
|
184
|
+
python_builtins.__dict__.setdefault(name, getattr(pgzero_builtins, name))
|
|
185
|
+
|
|
186
|
+
# setdefault() keeps any name that already exists, so these are replaced
|
|
187
|
+
# explicitly:
|
|
188
|
+
# * exit: Python already has its own exit(), which doesn't wait for
|
|
189
|
+
# sounds to finish playing.
|
|
190
|
+
# * game: each run starts with a fresh Game, so score and state from an
|
|
191
|
+
# earlier run (e.g. in the test suite) don't carry over.
|
|
192
|
+
python_builtins.exit = pgzero_builtins.exit
|
|
193
|
+
python_builtins.game = pgzero_builtins.game = Game()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def open_loading_window():
|
|
197
|
+
"""Create a hidden window for the game module's top-level code to use.
|
|
198
|
+
|
|
199
|
+
Several Pygame surface operations access the state of the screen as a
|
|
200
|
+
global:
|
|
201
|
+
|
|
202
|
+
* Surface.convert_alpha() without arguments converts a surface for fast
|
|
203
|
+
blitting to the display.
|
|
204
|
+
* Surface() without flags creates a surface identical to the display
|
|
205
|
+
format.
|
|
206
|
+
|
|
207
|
+
There's no good API to expose what the display format is until we create
|
|
208
|
+
a window, so we create a hidden one, which lets code like a top-level
|
|
209
|
+
``Actor("alien")`` load its image. The expectation is that when we create
|
|
210
|
+
the real window this will have the same display format and blits etc will
|
|
211
|
+
still be fast.
|
|
212
|
+
|
|
213
|
+
run_mod() closes this window before opening the real one. Resizing it
|
|
214
|
+
instead would leave the game window off-centre on the screen.
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
# An icon needs to exist before the window is created.
|
|
218
|
+
PGZeroGame.show_default_icon()
|
|
219
|
+
pygame.display.set_mode(
|
|
220
|
+
(100, 100),
|
|
221
|
+
flags=(DISPLAY_FLAGS & ~pygame.SHOWN) | pygame.HIDDEN,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def run_mod(mod, **kwargs):
|
|
226
|
+
"""Run the module."""
|
|
227
|
+
# Start from a fresh display so the game window opens centred, rather
|
|
228
|
+
# than reusing the hidden loading window from prepare_mod().
|
|
229
|
+
pygame.display.quit()
|
|
230
|
+
pygame.display.init()
|
|
231
|
+
PGZeroGame.show_default_icon()
|
|
232
|
+
|
|
233
|
+
PGZeroGame(mod, **kwargs).run()
|
pgzero/screen.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
import pygame.draw
|
|
3
|
+
|
|
4
|
+
from . import ptext
|
|
5
|
+
from .rect import RECT_CLASSES, ZRect
|
|
6
|
+
from . import loaders
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def round_pos(pos):
|
|
10
|
+
"""Round a tuple position so it can be used for drawing."""
|
|
11
|
+
try:
|
|
12
|
+
x, y = pos
|
|
13
|
+
except TypeError:
|
|
14
|
+
raise TypeError("Coordinate must be a tuple (not {!r})".format(pos)) from None
|
|
15
|
+
try:
|
|
16
|
+
return round(x), round(y)
|
|
17
|
+
except TypeError:
|
|
18
|
+
raise TypeError("Coordinate values must be numbers (not {!r})".format(pos)) from None # noqa
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def make_color(arg):
|
|
22
|
+
if isinstance(arg, tuple):
|
|
23
|
+
return arg
|
|
24
|
+
return tuple(pygame.Color(arg))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SurfacePainter:
|
|
28
|
+
"""Interface to pygame.draw that is bound to a surface."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, screen):
|
|
31
|
+
self._screen = screen
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def _surf(self):
|
|
35
|
+
return self._screen.surface
|
|
36
|
+
|
|
37
|
+
def line(self, start, end, color, width=1):
|
|
38
|
+
"""Draw a line from start to end."""
|
|
39
|
+
start = round_pos(start)
|
|
40
|
+
end = round_pos(end)
|
|
41
|
+
pygame.draw.line(self._surf, make_color(color), start, end, width)
|
|
42
|
+
|
|
43
|
+
def circle(self, pos, radius, color, width=1):
|
|
44
|
+
"""Draw a circle."""
|
|
45
|
+
pos = round_pos(pos)
|
|
46
|
+
pygame.draw.circle(self._surf, make_color(color), pos, radius, width)
|
|
47
|
+
|
|
48
|
+
def filled_circle(self, pos, radius, color):
|
|
49
|
+
"""Draw a filled circle."""
|
|
50
|
+
pos = round_pos(pos)
|
|
51
|
+
pygame.draw.circle(self._surf, make_color(color), pos, radius, 0)
|
|
52
|
+
|
|
53
|
+
def polygon(self, points, color):
|
|
54
|
+
"""Draw a polygon."""
|
|
55
|
+
try:
|
|
56
|
+
iter(points)
|
|
57
|
+
except TypeError:
|
|
58
|
+
raise TypeError("screen.draw.filled_polygon() requires an iterable of points to draw") from None # noqa
|
|
59
|
+
points = [round_pos(point) for point in points]
|
|
60
|
+
pygame.draw.polygon(self._surf, make_color(color), points, 1)
|
|
61
|
+
|
|
62
|
+
def filled_polygon(self, points, color):
|
|
63
|
+
"""Draw a filled polygon."""
|
|
64
|
+
try:
|
|
65
|
+
iter(points)
|
|
66
|
+
except TypeError:
|
|
67
|
+
raise TypeError("screen.draw.filled_polygon() requires an iterable of points to draw") from None # noqa
|
|
68
|
+
points = [round_pos(point) for point in points]
|
|
69
|
+
pygame.draw.polygon(self._surf, make_color(color), points, 0)
|
|
70
|
+
|
|
71
|
+
def _coerce_rect(self, rect, color_or_size, color, method_name):
|
|
72
|
+
"""Unpack ``(rect, color)`` or ``((x, y), (w, h), color)``.
|
|
73
|
+
|
|
74
|
+
Return (pygame.Rect, (l, t, w, h), color).
|
|
75
|
+
"""
|
|
76
|
+
if isinstance(rect, RECT_CLASSES):
|
|
77
|
+
if color is None:
|
|
78
|
+
color = color_or_size
|
|
79
|
+
elif color_or_size is not None:
|
|
80
|
+
raise TypeError(f"screen.draw.{method_name}() got too many positional arguments for rect form")
|
|
81
|
+
l, t, w, h = rect
|
|
82
|
+
else:
|
|
83
|
+
try:
|
|
84
|
+
(l, t), (w, h) = rect, color_or_size
|
|
85
|
+
l, t, w, h = float(l), float(t), float(w), float(h)
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
raise TypeError(f"screen.draw.{method_name}() requires a rect or ((x, y), (w, h))") from None
|
|
88
|
+
if color is None:
|
|
89
|
+
raise TypeError(f"screen.draw.{method_name}() requires a color")
|
|
90
|
+
return pygame.Rect(round(l), round(t), round(w), round(h)), (l, t, w, h), color
|
|
91
|
+
|
|
92
|
+
def rect(self, rect, color_or_size=None, color=None, width=1):
|
|
93
|
+
"""Draw a rectangle: rect(rect, color) or rect((x, y), (w, h), color)."""
|
|
94
|
+
rect, (l, t, w, h), color = self._coerce_rect(rect, color_or_size, color, "rect")
|
|
95
|
+
|
|
96
|
+
if width <= 1:
|
|
97
|
+
pygame.draw.rect(self._surf, make_color(color), rect, width)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
c = make_color(color)
|
|
101
|
+
|
|
102
|
+
hw = width / 2
|
|
103
|
+
l1, l2 = round(l - hw), round(l + hw)
|
|
104
|
+
r1, r2 = round(l + w - hw), round(l + w + hw)
|
|
105
|
+
t1, t2 = round(t - hw), round(t + hw)
|
|
106
|
+
b1, b2 = round(t + h - hw), round(t + h + hw)
|
|
107
|
+
|
|
108
|
+
def r(x1, y1, x2, y2):
|
|
109
|
+
rr = pygame.Rect(x1, y1, x2 - x1, y2 - y1)
|
|
110
|
+
pygame.draw.rect(self._surf, c, rr, 0)
|
|
111
|
+
|
|
112
|
+
r(l1, t1, r2, t2)
|
|
113
|
+
r(l1, t2, l2, b1)
|
|
114
|
+
r(r1, t2, r2, b1)
|
|
115
|
+
r(l1, b1, r2, b2)
|
|
116
|
+
|
|
117
|
+
def filled_rect(self, rect, color_or_size=None, color=None):
|
|
118
|
+
"""Draw a filled rectangle: filled_rect(rect, color) or filled_rect((x, y), (w, h), color)."""
|
|
119
|
+
rect, _, color = self._coerce_rect(rect, color_or_size, color, "filled_rect")
|
|
120
|
+
pygame.draw.rect(self._surf, make_color(color), rect, 0)
|
|
121
|
+
|
|
122
|
+
def text(self, *args, **kwargs):
|
|
123
|
+
"""Draw text to the screen."""
|
|
124
|
+
# FIXME: expose ptext parameters, for autocompletion and autodoc
|
|
125
|
+
ptext.draw(*args, surf=self._surf, **kwargs)
|
|
126
|
+
|
|
127
|
+
def textbox(self, *args, **kwargs):
|
|
128
|
+
"""Draw text to the screen, wrapped to fit a box"""
|
|
129
|
+
# FIXME: expose ptext parameters, for autocompletion and autodoc
|
|
130
|
+
ptext.drawbox(*args, surf=self._surf, **kwargs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def blit_gradient(start, stop, dest_surface):
|
|
134
|
+
"""Blit a gradient into a destination surface.
|
|
135
|
+
|
|
136
|
+
The function does not return anything as the gradient is written
|
|
137
|
+
in-place in the destination surface.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
start: The starting (top) color as a tuple (red, green, blue).
|
|
141
|
+
stop: The stopping (bottom) color as a tuple (red, green, blue).
|
|
142
|
+
dest_surface: A pygame.Surface to write the gradient into.
|
|
143
|
+
Returns:
|
|
144
|
+
None."""
|
|
145
|
+
surface_compact = pygame.Surface((2, 2))
|
|
146
|
+
pixelarray = pygame.PixelArray(surface_compact)
|
|
147
|
+
pixelarray[0][0] = start
|
|
148
|
+
pixelarray[0][1] = stop
|
|
149
|
+
pixelarray[1][0] = start
|
|
150
|
+
pixelarray[1][1] = stop
|
|
151
|
+
pygame.transform.smoothscale(
|
|
152
|
+
surface_compact,
|
|
153
|
+
pygame.PixelArray(dest_surface).shape,
|
|
154
|
+
dest_surface=dest_surface,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Screen:
|
|
159
|
+
"""Interface to the screen."""
|
|
160
|
+
|
|
161
|
+
def _set_surface(self, surface):
|
|
162
|
+
self.surface = surface
|
|
163
|
+
self.width, self.height = surface.get_size()
|
|
164
|
+
|
|
165
|
+
def bounds(self):
|
|
166
|
+
"""Return a Rect representing the bounds of the screen."""
|
|
167
|
+
return ZRect((0, 0), (self.width, self.height))
|
|
168
|
+
|
|
169
|
+
def clear(self):
|
|
170
|
+
"""Clear the screen to black."""
|
|
171
|
+
self.fill((0, 0, 0))
|
|
172
|
+
|
|
173
|
+
def fill(self, color, gcolor=None):
|
|
174
|
+
"""Fill the screen with a colour."""
|
|
175
|
+
if gcolor:
|
|
176
|
+
start = make_color(color)
|
|
177
|
+
stop = make_color(gcolor)
|
|
178
|
+
blit_gradient(start, stop, self.surface)
|
|
179
|
+
else:
|
|
180
|
+
self.surface.fill(make_color(color))
|
|
181
|
+
|
|
182
|
+
def blit(self, image, pos):
|
|
183
|
+
"""Draw a sprite onto the screen.
|
|
184
|
+
|
|
185
|
+
"blit" is an archaic name for this operation, but one that is is still
|
|
186
|
+
frequently used, for example in Pygame. See the `Wikipedia article`__
|
|
187
|
+
for more about the etymology of the term.
|
|
188
|
+
|
|
189
|
+
.. __: http://en.wikipedia.org/wiki/Bit_blit
|
|
190
|
+
|
|
191
|
+
:param image: A Surface or the name of an image object to load.
|
|
192
|
+
:param pos: The coordinates at which the top-left corner of the sprite
|
|
193
|
+
will be positioned. This may be given as a pair of
|
|
194
|
+
coordinates or as a Rect. If a Rect is given the sprite
|
|
195
|
+
will be drawn at ``rect.topleft``.
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
if isinstance(image, str):
|
|
199
|
+
image = loaders.images.load(image)
|
|
200
|
+
self.surface.blit(image, pos, None, pygame.BLEND_ALPHA_SDL2)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def draw(self):
|
|
204
|
+
return SurfacePainter(self)
|
|
205
|
+
|
|
206
|
+
def __repr__(self):
|
|
207
|
+
return "<Screen width={} height={}>".format(self.width, self.height)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
screen_instance = Screen()
|
pgzero/soundfmt.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Identify WAV file formats.
|
|
2
|
+
|
|
3
|
+
This is used only to give better error messages in the event that a sound
|
|
4
|
+
file is not loadable by Pygame.
|
|
5
|
+
|
|
6
|
+
This is based on the 'magic' information for the 'file' command, at
|
|
7
|
+
|
|
8
|
+
https://github.com/file/file/blob/86e34444a26860f2ad9895a2d77cb16d1ca4c48b/magic/Magdir/riff
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from struct import unpack_from
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MagicReader:
|
|
16
|
+
"""Interface to reading the magic numbers in a file's header."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path):
|
|
19
|
+
with open(path, 'rb') as f:
|
|
20
|
+
self.bytes = f.read(64 * 1024)
|
|
21
|
+
|
|
22
|
+
def read_bytes(self, offset, length=4):
|
|
23
|
+
return self.bytes[offset:offset + length]
|
|
24
|
+
|
|
25
|
+
def read_leshort(self, offset):
|
|
26
|
+
"""Read an unsigned short at the given offset."""
|
|
27
|
+
return unpack_from('<H', self.bytes, offset)[0]
|
|
28
|
+
|
|
29
|
+
def read_lelong(self, offset):
|
|
30
|
+
"""Read an unsigned long at the given offset."""
|
|
31
|
+
return unpack_from('<L', self.bytes, offset)[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
CODECS = {
|
|
35
|
+
1: 'Microsoft PCM',
|
|
36
|
+
2: 'Microsoft ADPCM',
|
|
37
|
+
3: 'PCM (float)',
|
|
38
|
+
6: 'ITU G.711 A-law',
|
|
39
|
+
7: 'ITU G.711 µ-law',
|
|
40
|
+
8: 'Microsoft DTS',
|
|
41
|
+
17: 'IMA ADPCM',
|
|
42
|
+
20: 'ITU G.723 ADPCM (Yamaha)',
|
|
43
|
+
34: 'DSP Group Truespeech',
|
|
44
|
+
49: 'GSM 6.10',
|
|
45
|
+
64: 'ITU G.721 ADPCM',
|
|
46
|
+
80: 'MPEG',
|
|
47
|
+
85: 'MP3',
|
|
48
|
+
112: 'Lernout & Hauspie CELP',
|
|
49
|
+
114: 'Lernout & Hauspie SBC',
|
|
50
|
+
0x2001: 'DTS',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def riff_wave(f, offset):
|
|
55
|
+
"""Read the WAVE format information."""
|
|
56
|
+
encoding = f.read_leshort(offset)
|
|
57
|
+
yield CODECS.get(encoding, 'unknown encoding %d' % encoding)
|
|
58
|
+
if encoding in (1, 3):
|
|
59
|
+
bitrate = f.read_leshort(offset + 14)
|
|
60
|
+
if 0 < bitrate < 1024:
|
|
61
|
+
yield '%d bit' % bitrate
|
|
62
|
+
channels = f.read_leshort(offset + 2)
|
|
63
|
+
if channels == 1:
|
|
64
|
+
yield 'mono'
|
|
65
|
+
elif channels == 2:
|
|
66
|
+
yield 'stereo'
|
|
67
|
+
elif 2 < channels < 128:
|
|
68
|
+
yield '%d channels' % channels
|
|
69
|
+
|
|
70
|
+
hz = f.read_lelong(offset + 4)
|
|
71
|
+
if 0 < hz < 1000000:
|
|
72
|
+
yield '%d Hz' % hz
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def riff_walk(f, offset):
|
|
76
|
+
"""Search chunks trying to find b'fmt '"""
|
|
77
|
+
chunk = f.read_bytes(offset)
|
|
78
|
+
if chunk == b'fmt ':
|
|
79
|
+
if f.read_lelong(offset + 4) < 0x80:
|
|
80
|
+
return list(riff_wave(f, offset + 8))
|
|
81
|
+
elif chunk[:3] == b'VP8':
|
|
82
|
+
return ['VP8 encoding']
|
|
83
|
+
elif chunk in (b'LIST', b'DISP', b'bext', b'Fake', b'fact'):
|
|
84
|
+
off = f.read_lelong(offset + 4)
|
|
85
|
+
return riff_walk(f, off + 4)
|
|
86
|
+
return ["Unknown WAVE encoding"]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def identify(path):
|
|
90
|
+
f = MagicReader(path)
|
|
91
|
+
if f.read_bytes(0) != b'RIFF':
|
|
92
|
+
return 'Unknown format (not RIFF WAVE)'
|
|
93
|
+
|
|
94
|
+
if f.read_bytes(8) != b'WAVE':
|
|
95
|
+
return 'Unknown RIFF format (not WAVE)'
|
|
96
|
+
|
|
97
|
+
return 'WAV audio encoded as ' + ', '.join(riff_walk(f, 12))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == '__main__':
|
|
101
|
+
import sys
|
|
102
|
+
tabstop = max(len(a) for a in sys.argv[1:]) + 1
|
|
103
|
+
for path in sys.argv[1:]:
|
|
104
|
+
sndfmt = identify(path)
|
|
105
|
+
print(('%s:' % path).ljust(tabstop), sndfmt)
|