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/spellcheck.py ADDED
@@ -0,0 +1,173 @@
1
+ import re
2
+
3
+ from operator import itemgetter
4
+
5
+ from .game import PGZeroGame, positional_parameters
6
+
7
+
8
+ def distance(a, b):
9
+ """Compute the distance between a and b.
10
+
11
+ This is based on Damerau-Levenshtein distance, but we modify the cost
12
+ of some edits, like insertion or removal of '_', or capitalisation changes.
13
+
14
+ """
15
+ d = {}
16
+ la = len(a)
17
+ lb = len(b)
18
+ for i in range(la + 1):
19
+ d[i, 0] = i
20
+ for j in range(1, lb + 1):
21
+ d[0, j] = j
22
+
23
+ for i, ca in enumerate(a, start=1):
24
+ for j, cb in enumerate(b, start=1):
25
+ cost = int(ca != cb)
26
+ if ca.lower() == cb.lower():
27
+ subst_cost = 0
28
+ else:
29
+ subst_cost = 1.25 * cost
30
+ insertion_cost = 0.7 if cb == "_" else 1.0
31
+ deletion_cost = 0.7 if ca == "_" else 1.0
32
+ d[i, j] = min(
33
+ d[i - 1, j] + deletion_cost, # deletion
34
+ d[i, j - 1] + insertion_cost, # insertion
35
+ d[i - 1, j - 1] + subst_cost, # substitution
36
+ )
37
+ if i > 1 and j > 1 and ca == b[j - 2] and a[i - 2] == cb:
38
+ d[i, j] = min(d[i, j], d[i - 2, j - 2] + cost) # transposition
39
+
40
+ return d[la, lb]
41
+
42
+
43
+ def suggest(word, candidates):
44
+ """Suggest good candidates as corrections for the given word.
45
+
46
+ Suggestions will be ordered from best to worst.
47
+
48
+ """
49
+ candidates_with_score = [(c, distance(word, c)) for c in candidates]
50
+ good_candidates = [(c, d) for c, d in candidates_with_score if d < 2.6]
51
+ good_candidates.sort(key=itemgetter(1))
52
+ # print(word, good_candidates)
53
+ return [c for c, d in good_candidates]
54
+
55
+
56
+ def compare(have, want):
57
+ """Compare a set of names we have (from user input) to those we want.
58
+
59
+ This is a greedy algorithm that will take the best answer for each word
60
+ in have in turn.
61
+
62
+ Returns (typos with suggestions, unrecognised functions)
63
+ """
64
+ want = set(want)
65
+ have = set(have)
66
+ matched = want & have
67
+ want -= matched
68
+ have -= matched
69
+ typos = []
70
+ for w in have:
71
+ suggestions = suggest(w, want)
72
+ if suggestions:
73
+ s = suggestions[0]
74
+ typos.append((w, s))
75
+ want.discard(s)
76
+ have -= set(w for w, s in typos)
77
+ return typos, have
78
+
79
+
80
+ # The list of hooks we support
81
+
82
+ EVENT_HOOKS = list(PGZeroGame.EVENT_HANDLERS.values())
83
+
84
+ HOOKS = ["draw", "update", "start"]
85
+
86
+
87
+ # The list of magic module-level constants
88
+ CONSTS = ["TITLE", "WIDTH", "HEIGHT", "ICON"]
89
+
90
+ # Available parameters for each hook
91
+ # NB. update() takes one or zero positional parameter but we don't constrain
92
+ # the name.
93
+ #
94
+ # FIXME: These are from the documentation; there could be some missing here
95
+ VALID_PARAMS = {
96
+ "on_mouse_down": ["pos", "button"],
97
+ "on_mouse_up": ["pos", "button"],
98
+ "on_mouse_move": ["pos", "buttons", "rel"],
99
+ "on_key_up": ["key", "mod"],
100
+ "on_key_down": ["unicode", "key", "mod"],
101
+ "draw": [],
102
+ "on_music_end": [],
103
+ }
104
+
105
+
106
+ class InvalidParameter(Exception):
107
+ """A parameter to a hook was invalid."""
108
+
109
+
110
+ class SpellCheckResult:
111
+ def warn(self, msg, found, suggestion):
112
+ print(msg.format(found=found, suggestion=suggestion))
113
+
114
+ def warn_event_handlers(self, typos, missing):
115
+ if not typos or missing:
116
+ return
117
+ print("Warning: Found some 'on' functions that are not " "valid event handlers.")
118
+ for found, suggestion in typos:
119
+ print(" {found} (did you mean {suggestion}?)".format(found=found, suggestion=suggestion))
120
+ for f in missing:
121
+ print(" ", f)
122
+
123
+ def error(self, msg, found, suggestion):
124
+ raise InvalidParameter(msg.format(found=found, suggestion=suggestion))
125
+
126
+
127
+ def spellcheck(namespace, result=SpellCheckResult()):
128
+ """Spell check the names in the given module.
129
+
130
+ Where hooks are found, validate their positional parameters and offer
131
+ suggestions where mispelled.
132
+
133
+ """
134
+ funcs = {}
135
+ consts = []
136
+ for name, val in namespace.items():
137
+ if callable(val) and not isinstance(val, type):
138
+ funcs[name] = val
139
+ elif isinstance(val, (str, int)):
140
+ consts.append(name)
141
+
142
+ typos, _ = compare(funcs, HOOKS)
143
+ for found, suggestion in typos:
144
+ result.warn("Warning: found function named {found}: " "did you mean {suggestion}?", found, suggestion)
145
+
146
+ typos, missing = compare(funcs, EVENT_HOOKS)
147
+ maybe_hook = re.compile("on[A-Z_]")
148
+ missing_hooks = [f for f in missing if re.match(maybe_hook, f)]
149
+ result.warn_event_handlers(typos, missing_hooks)
150
+
151
+ typos, _ = compare(consts, CONSTS)
152
+ for found, suggestion in typos:
153
+ result.warn("Warning: found constant named {found}: " "did you mean {suggestion}?", found, suggestion)
154
+
155
+ for name, handler in funcs.items():
156
+ try:
157
+ valid = VALID_PARAMS[name]
158
+ except KeyError:
159
+ continue
160
+ else:
161
+ param_names = positional_parameters(handler)
162
+ for param in param_names:
163
+ if param in valid:
164
+ continue
165
+ suggestions = suggest(param, valid)
166
+ if suggestions:
167
+ result.error(
168
+ "%s() hook accepts no parameter {found}; " "did you mean {suggestion}?" % name,
169
+ param,
170
+ suggestions[0],
171
+ )
172
+ else:
173
+ result.error("%s() hook accepts no parameter {found}" % name, param, None)
pgzero/sprites.py ADDED
@@ -0,0 +1,182 @@
1
+ import pygame
2
+
3
+ from . import loaders
4
+ from .actor import Actor, POS_TOPLEFT, ANCHOR_CENTER
5
+
6
+
7
+ class SpriteSheet:
8
+ def __init__(self, name):
9
+ self.name = name
10
+ self.sheet = loaders.sprites.load(name)
11
+
12
+ def image_at(self, rectangle, color_key=None):
13
+ rect = pygame.Rect(rectangle)
14
+ image = pygame.Surface(rect.size, pygame.SRCALPHA).convert_alpha()
15
+ image.blit(self.sheet, (0, 0), rect)
16
+
17
+ if color_key is not None:
18
+ if color_key == -1:
19
+ color_key = image.get_at((0, 0))
20
+ image.set_colorkey(color_key, pygame.RLEACCEL)
21
+
22
+ return image
23
+
24
+ def images_at(self, rects, color_key=None):
25
+ return [self.image_at(rect, color_key) for rect in rects]
26
+
27
+ def load_strip(self, rect, image_count, color_key=None):
28
+ rects = [(rect[0] + rect[2] * i, rect[1], rect[2], rect[3]) for i in range(image_count)]
29
+ return self.images_at(rects, color_key)
30
+
31
+
32
+ class Sprite:
33
+ def __init__(
34
+ self,
35
+ image_name,
36
+ frame_width,
37
+ frame_height,
38
+ row_number,
39
+ frame_count,
40
+ fps=10,
41
+ transparent_color=None,
42
+ loop=True,
43
+ ):
44
+ self.image_name = image_name
45
+ self.frame_width = frame_width
46
+ self.frame_height = frame_height
47
+ self.row_number = row_number
48
+ self.frame_count = frame_count
49
+ self.fps = fps
50
+ self.transparent_color = transparent_color
51
+ self.loop = loop
52
+ self.finished = False
53
+ self.frame_index = 0
54
+ self.last_update_time = pygame.time.get_ticks()
55
+
56
+ ss = SpriteSheet(image_name)
57
+ sheet_width, sheet_height = ss.sheet.get_size()
58
+ if frame_height * (row_number + 1) > sheet_height:
59
+ raise ValueError(
60
+ f"Row {row_number} is outside the sprite sheet. "
61
+ f"The sheet is {sheet_width}x{sheet_height}, "
62
+ f"but row {row_number} with frame height {frame_height} "
63
+ f"would end at y={frame_height * (row_number + 1)}."
64
+ )
65
+ if frame_width * frame_count > sheet_width:
66
+ raise ValueError(
67
+ f"The requested frames go past the edge of the sprite sheet. "
68
+ f"The sheet is {sheet_width} pixels wide, but "
69
+ f"{frame_count} frames with width {frame_width} need "
70
+ f"{frame_width * frame_count} pixels."
71
+ )
72
+
73
+ first_rect = (
74
+ 0,
75
+ frame_height * row_number,
76
+ frame_width,
77
+ frame_height,
78
+ )
79
+ self.images = ss.load_strip(first_rect, frame_count, transparent_color)
80
+
81
+ if not self.images:
82
+ raise ValueError("Sprite must contain at least one frame")
83
+
84
+ @property
85
+ def image(self):
86
+ return self.images[self.frame_index]
87
+
88
+ def copy(self):
89
+ return Sprite(
90
+ self.image_name,
91
+ self.frame_width,
92
+ self.frame_height,
93
+ self.row_number,
94
+ self.frame_count,
95
+ self.fps,
96
+ self.transparent_color,
97
+ self.loop,
98
+ )
99
+
100
+ def reset(self):
101
+ self.set_frame(0)
102
+
103
+ def set_frame(self, frame_index):
104
+ if frame_index < 0 or frame_index >= len(self.images):
105
+ raise ValueError(f"Frame index {frame_index} is out of range")
106
+
107
+ self.frame_index = frame_index
108
+ self.finished = False
109
+ self.last_update_time = pygame.time.get_ticks()
110
+ return self.image
111
+
112
+ def next_frame(self):
113
+ if self.finished:
114
+ return self.image
115
+
116
+ current_time = pygame.time.get_ticks()
117
+ frame_duration_ms = 1000 / self.fps
118
+
119
+ if current_time - self.last_update_time >= frame_duration_ms:
120
+ if self.frame_index < len(self.images) - 1:
121
+ self.frame_index += 1
122
+ elif self.loop:
123
+ self.frame_index = 0
124
+ else:
125
+ self.finished = True
126
+
127
+ self.last_update_time = current_time
128
+
129
+ return self.image
130
+
131
+
132
+ class SpriteActor(Actor):
133
+ def __init__(
134
+ self,
135
+ sprite,
136
+ pos=POS_TOPLEFT,
137
+ anchor=ANCHOR_CENTER,
138
+ collision_rect=None,
139
+ **kwargs,
140
+ ):
141
+ self._sprite = sprite
142
+ self._paused = False
143
+
144
+ super().__init__(
145
+ sprite.image,
146
+ pos=pos,
147
+ anchor=anchor,
148
+ collision_rect=collision_rect,
149
+ **kwargs,
150
+ )
151
+
152
+ @property
153
+ def sprite(self):
154
+ return self._sprite
155
+
156
+ @sprite.setter
157
+ def sprite(self, sprite):
158
+ old_sprite = self._sprite
159
+ self._sprite = sprite
160
+ if old_sprite is not sprite:
161
+ self._sprite.reset()
162
+ self.image = self._sprite.image
163
+
164
+ def pause(self):
165
+ self._paused = True
166
+
167
+ def resume(self):
168
+ self._paused = False
169
+
170
+ @property
171
+ def paused(self):
172
+ return self._paused
173
+
174
+ def update_animation(self):
175
+ if not self._paused:
176
+ frame = self._sprite.next_frame()
177
+ if frame is not self._orig_surf:
178
+ self.image = frame
179
+
180
+ def draw(self):
181
+ self.update_animation()
182
+ super().draw()
pgzero/storage.py ADDED
@@ -0,0 +1,191 @@
1
+ import json
2
+ import os
3
+ import platform
4
+ from hashlib import sha1
5
+
6
+
7
+ __all__ = ['StorageCorruptionException', 'JSONEncodingException', 'Storage']
8
+
9
+
10
+ class StorageCorruptionException(Exception):
11
+ """The data in the storage is corrupted."""
12
+
13
+
14
+ class JSONEncodingException(Exception):
15
+ """The data in the storage is corrupted."""
16
+
17
+
18
+ def _get_platform_pgzero_path():
19
+ r"""Get the storage directory for pgzero save data.
20
+
21
+ Under Windows, return %APPDATA%\pgzero. Under Linux/MacOS, return
22
+ ~/.config/pgzero/saves.
23
+
24
+ """
25
+ if platform.system() == 'Windows':
26
+ try:
27
+ appdata = os.environ['APPDATA']
28
+ except KeyError:
29
+ raise KeyError(
30
+ "Couldn't find the AppData directory for Pygame Zero save "
31
+ "data. Please set the %APPDATA% environment variable."
32
+ )
33
+ return os.path.join(appdata, 'pgzero')
34
+ return os.path.expanduser(os.path.join('~', '.config/pgzero/saves'))
35
+
36
+
37
+ class Storage(dict):
38
+ """Behaves like a dictionary that serialises itself to disk.
39
+
40
+ The name of the file will be based on the basename of the script plus
41
+ a hash of the script's path, ensuring that each script on the filesystem
42
+ has a unique save file.
43
+
44
+ """
45
+ STORAGE_DIR = _get_platform_pgzero_path()
46
+
47
+ # Keep a reference to all defined storages
48
+ storages = []
49
+
50
+ def __init__(self, filename=None):
51
+ super().__init__()
52
+ self.loaded = False
53
+ self._save_file = filename
54
+ self.storages.append(self)
55
+
56
+ @classmethod
57
+ def save_all(cls):
58
+ """Save all instances of Storage."""
59
+ cls._ensure_save_path()
60
+ for storage in cls.storages:
61
+ storage.save()
62
+
63
+ @classmethod
64
+ def _ensure_save_path(cls):
65
+ """Ensure that the directory for all save game data exists."""
66
+ try:
67
+ os.makedirs(cls.STORAGE_DIR)
68
+ except (IsADirectoryError, PermissionError):
69
+ pass
70
+ except FileExistsError:
71
+ pass
72
+
73
+ def _set_filename_from_path(self, file_path):
74
+ """Set the path to save to from the given filename.
75
+
76
+ We include the basename of the game script, to help with identifying
77
+ the save data, as well as the sha1 hash of the full path of the file on
78
+ disk, to ensure that every separate game has its own storage.
79
+
80
+ """
81
+ if not os.path.isabs(file_path):
82
+ file_path = os.path.abspath(file_path)
83
+
84
+ fn_hash = sha1(file_path.encode('utf-8')).hexdigest()
85
+ base, _ = os.path.splitext(os.path.basename(file_path))
86
+ self._save_file = '{}-{}.json'.format(base, fn_hash)
87
+ self.load()
88
+
89
+ @property
90
+ def path(self):
91
+ """Get the path for data data."""
92
+ if self._save_file is None:
93
+ raise ValueError(
94
+ "Cannot save/load storage as no filename is set."
95
+ )
96
+ return os.path.join(self.STORAGE_DIR, self._save_file)
97
+
98
+ def load(self):
99
+ """Load data into the storage from disk.
100
+
101
+ This replaces all previous contents of the storage. If there is no save
102
+ file found then the storage will be empty.
103
+
104
+ """
105
+ self.clear()
106
+
107
+ try:
108
+ with open(self.path, 'r') as f:
109
+ data = json.load(f)
110
+ except FileNotFoundError:
111
+ # If no file exists, it's fine
112
+ pass
113
+ except json.JSONDecodeError:
114
+ raise StorageCorruptionException(
115
+ "Storage is corrupted. Couldn't load the data."
116
+ )
117
+ else:
118
+ self.update(data)
119
+ self.loaded = True
120
+
121
+ def save(self):
122
+ """Save data to disk."""
123
+ if not self and not self.loaded:
124
+ return
125
+ try:
126
+ data = json.dumps(self)
127
+ except TypeError:
128
+ msgs = [
129
+ "{} - type {}".format(*item)
130
+ for item in self._get_json_error_keys(self)
131
+ ]
132
+ if not msgs:
133
+ # Didn't find an explanation, so let original error propagate
134
+ raise
135
+ raise JSONEncodingException(
136
+ "The following storage entries cannot be stored, because "
137
+ "they are not JSON serializable types:\n{}\n\n"
138
+ "Only the following types of objects are JSON-serialisable: "
139
+ "dict, list/tuple, str, float/int, bool, and None".format(
140
+ "\n".join(msgs)
141
+ )
142
+ )
143
+ else:
144
+ path = self.path
145
+ with open(path, 'w+') as f:
146
+ f.write(data)
147
+ print("Saved storage to", path)
148
+
149
+ # Constants for use with isinstance in _get_json_error_keys()
150
+ JSON_PRIMITIVES = (float, int, str, bool, type(None))
151
+ JSON_SEQ = (list, tuple)
152
+ JSON_MAPPING = dict
153
+
154
+ @classmethod
155
+ def _get_json_error_keys(cls, obj, json_path='storage'):
156
+ """Identify the paths with the storage which failed to be JSON encoded.
157
+
158
+ Return an iterable of message strings.
159
+
160
+ """
161
+ if isinstance(obj, dict):
162
+ # TODO: also check/report that k is a string, as only strings are
163
+ # valid as object keys in JSON.
164
+ for k, v in obj.items():
165
+ if isinstance(v, cls.JSON_PRIMITIVES):
166
+ continue
167
+ subpath = '{}[{!r}]'.format(json_path, k)
168
+ yield from cls._get_json_error_keys(v, subpath)
169
+ elif isinstance(obj, (list, tuple)):
170
+ for i, value in enumerate(obj):
171
+ if isinstance(value, cls.JSON_PRIMITIVES):
172
+ continue
173
+ subpath = '{}[{}]'.format(json_path, i)
174
+ yield from cls._get_json_error_keys(value, subpath)
175
+ elif isinstance(obj, cls.JSON_PRIMITIVES):
176
+ return
177
+ else:
178
+ # TODO: Could have a custom JSONEncoder in the future, in which
179
+ # case we would have to actually run json.dumps on it to get the
180
+ # result and possibly even drill down on the parameters we got
181
+ # back, if that object happens to have a list as an attribute, for
182
+ # example.
183
+ t = type(obj)
184
+ if t.__module__ != 'builtins':
185
+ typename = '{t.__module__}.{t.__qualname__}'.format(t=t)
186
+ else:
187
+ typename = t.__qualname__
188
+ yield json_path, typename
189
+
190
+
191
+ storage = Storage()