zombie-escape 1.13.1__py3-none-any.whl → 1.14.4__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 (41) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/colors.py +7 -21
  3. zombie_escape/entities.py +100 -191
  4. zombie_escape/export_images.py +39 -33
  5. zombie_escape/gameplay/ambient.py +2 -6
  6. zombie_escape/gameplay/footprints.py +8 -11
  7. zombie_escape/gameplay/interactions.py +17 -58
  8. zombie_escape/gameplay/layout.py +20 -46
  9. zombie_escape/gameplay/movement.py +7 -21
  10. zombie_escape/gameplay/spawn.py +12 -40
  11. zombie_escape/gameplay/state.py +1 -0
  12. zombie_escape/gameplay/survivors.py +5 -16
  13. zombie_escape/gameplay/utils.py +4 -13
  14. zombie_escape/input_utils.py +8 -31
  15. zombie_escape/level_blueprints.py +112 -69
  16. zombie_escape/level_constants.py +8 -0
  17. zombie_escape/locales/ui.en.json +12 -0
  18. zombie_escape/locales/ui.ja.json +12 -0
  19. zombie_escape/localization.py +3 -11
  20. zombie_escape/models.py +26 -9
  21. zombie_escape/render/__init__.py +30 -0
  22. zombie_escape/render/core.py +992 -0
  23. zombie_escape/render/hud.py +444 -0
  24. zombie_escape/render/overview.py +218 -0
  25. zombie_escape/render/shadows.py +343 -0
  26. zombie_escape/render_assets.py +11 -33
  27. zombie_escape/rng.py +4 -8
  28. zombie_escape/screens/__init__.py +14 -30
  29. zombie_escape/screens/game_over.py +43 -15
  30. zombie_escape/screens/gameplay.py +41 -104
  31. zombie_escape/screens/settings.py +19 -104
  32. zombie_escape/screens/title.py +36 -176
  33. zombie_escape/stage_constants.py +192 -67
  34. zombie_escape/zombie_escape.py +1 -1
  35. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
  36. zombie_escape-1.14.4.dist-info/RECORD +53 -0
  37. zombie_escape/render.py +0 -1746
  38. zombie_escape-1.13.1.dist-info/RECORD +0 -49
  39. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
  40. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
  41. {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import pygame
6
+ from pygame import sprite, surface
7
+
8
+ from ..entities import Camera, Car, Player, SteelBeam, Survivor, Zombie
9
+ from ..entities_constants import JUMP_SHADOW_OFFSET, ZOMBIE_RADIUS
10
+ from ..render_constants import (
11
+ ENTITY_SHADOW_ALPHA,
12
+ ENTITY_SHADOW_EDGE_SOFTNESS,
13
+ ENTITY_SHADOW_RADIUS_MULT,
14
+ SHADOW_MIN_RATIO,
15
+ SHADOW_OVERSAMPLE,
16
+ SHADOW_RADIUS_RATIO,
17
+ SHADOW_STEPS,
18
+ )
19
+
20
+ _SHADOW_TILE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
21
+ _SHADOW_LAYER_CACHE: dict[tuple[int, int], surface.Surface] = {}
22
+ _SHADOW_CIRCLE_CACHE: dict[tuple[int, int, float], surface.Surface] = {}
23
+
24
+
25
+ def _get_shadow_tile_surface(
26
+ cell_size: int,
27
+ alpha: int,
28
+ *,
29
+ edge_softness: float = 0.35,
30
+ ) -> surface.Surface:
31
+ key = (max(1, cell_size), max(0, min(255, alpha)), edge_softness)
32
+ if key in _SHADOW_TILE_CACHE:
33
+ return _SHADOW_TILE_CACHE[key]
34
+ size = key[0]
35
+ oversample = SHADOW_OVERSAMPLE
36
+ render_size = size * oversample
37
+ render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
38
+ base_alpha = key[1]
39
+ if edge_softness <= 0:
40
+ render_surf.fill((0, 0, 0, base_alpha))
41
+ if oversample > 1:
42
+ surf = pygame.transform.smoothscale(render_surf, (size, size))
43
+ else:
44
+ surf = render_surf
45
+ _SHADOW_TILE_CACHE[key] = surf
46
+ return surf
47
+
48
+ softness = max(0.0, min(1.0, edge_softness))
49
+ fade_band = max(1, int(render_size * softness))
50
+ base_radius = max(1, int(render_size * SHADOW_RADIUS_RATIO))
51
+
52
+ render_surf.fill((0, 0, 0, 0))
53
+ steps = SHADOW_STEPS
54
+ min_ratio = SHADOW_MIN_RATIO
55
+ for idx in range(steps):
56
+ t = idx / (steps - 1) if steps > 1 else 1.0
57
+ inset = int(fade_band * t)
58
+ rect_size = render_size - inset * 2
59
+ if rect_size <= 0:
60
+ continue
61
+ radius = max(0, base_radius - inset)
62
+ layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
63
+ pygame.draw.rect(
64
+ render_surf,
65
+ (0, 0, 0, layer_alpha),
66
+ pygame.Rect(inset, inset, rect_size, rect_size),
67
+ border_radius=radius,
68
+ )
69
+
70
+ if oversample > 1:
71
+ surf = pygame.transform.smoothscale(render_surf, (size, size))
72
+ else:
73
+ surf = render_surf
74
+ _SHADOW_TILE_CACHE[key] = surf
75
+ return surf
76
+
77
+
78
+ def _get_shadow_layer(size: tuple[int, int]) -> surface.Surface:
79
+ key = (max(1, size[0]), max(1, size[1]))
80
+ if key in _SHADOW_LAYER_CACHE:
81
+ return _SHADOW_LAYER_CACHE[key]
82
+ layer = pygame.Surface(key, pygame.SRCALPHA)
83
+ _SHADOW_LAYER_CACHE[key] = layer
84
+ return layer
85
+
86
+
87
+ def _get_shadow_circle_surface(
88
+ radius: int,
89
+ alpha: int,
90
+ *,
91
+ edge_softness: float = 0.12,
92
+ ) -> surface.Surface:
93
+ key = (max(1, radius), max(0, min(255, alpha)), edge_softness)
94
+ if key in _SHADOW_CIRCLE_CACHE:
95
+ return _SHADOW_CIRCLE_CACHE[key]
96
+ radius = key[0]
97
+ oversample = SHADOW_OVERSAMPLE
98
+ render_radius = radius * oversample
99
+ render_size = render_radius * 2
100
+ render_surf = pygame.Surface((render_size, render_size), pygame.SRCALPHA)
101
+ base_alpha = key[1]
102
+ if edge_softness <= 0:
103
+ pygame.draw.circle(
104
+ render_surf,
105
+ (0, 0, 0, base_alpha),
106
+ (render_radius, render_radius),
107
+ render_radius,
108
+ )
109
+ if oversample > 1:
110
+ surf = pygame.transform.smoothscale(render_surf, (radius * 2, radius * 2))
111
+ else:
112
+ surf = render_surf
113
+ _SHADOW_CIRCLE_CACHE[key] = surf
114
+ return surf
115
+
116
+ softness = max(0.0, min(1.0, edge_softness))
117
+ fade_band = max(1, int(render_radius * softness))
118
+ steps = SHADOW_STEPS
119
+ min_ratio = SHADOW_MIN_RATIO
120
+ render_surf.fill((0, 0, 0, 0))
121
+ for idx in range(steps):
122
+ t = idx / (steps - 1) if steps > 1 else 1.0
123
+ inset = int(fade_band * t)
124
+ circle_radius = render_radius - inset
125
+ if circle_radius <= 0:
126
+ continue
127
+ layer_alpha = int(base_alpha * (min_ratio + (1.0 - min_ratio) * t))
128
+ pygame.draw.circle(
129
+ render_surf,
130
+ (0, 0, 0, layer_alpha),
131
+ (render_radius, render_radius),
132
+ circle_radius,
133
+ )
134
+
135
+ if oversample > 1:
136
+ surf = pygame.transform.smoothscale(render_surf, (radius * 2, radius * 2))
137
+ else:
138
+ surf = render_surf
139
+ _SHADOW_CIRCLE_CACHE[key] = surf
140
+ return surf
141
+
142
+
143
+ def _abs_clip(value: float, min_v: float, max_v: float) -> float:
144
+ value_sign = 1.0 if value >= 0.0 else -1.0
145
+ value = abs(value)
146
+ if value < min_v:
147
+ value = min_v
148
+ elif value > max_v:
149
+ value = max_v
150
+ return value_sign * value
151
+
152
+
153
+ def _draw_wall_shadows(
154
+ shadow_layer: surface.Surface,
155
+ camera: Camera,
156
+ *,
157
+ wall_cells: set[tuple[int, int]],
158
+ wall_group: sprite.Group | None,
159
+ outer_wall_cells: set[tuple[int, int]] | None,
160
+ cell_size: int,
161
+ light_source_pos: tuple[int, int] | None,
162
+ alpha: int = 68,
163
+ ) -> bool:
164
+ if not wall_cells or cell_size <= 0 or light_source_pos is None:
165
+ return False
166
+ inner_wall_cells = set(wall_cells)
167
+ if outer_wall_cells:
168
+ inner_wall_cells.difference_update(outer_wall_cells)
169
+ if wall_group and cell_size > 0:
170
+ for wall in wall_group:
171
+ if isinstance(wall, SteelBeam):
172
+ cell_x = int(wall.rect.centerx // cell_size)
173
+ cell_y = int(wall.rect.centery // cell_size)
174
+ inner_wall_cells.add((cell_x, cell_y))
175
+ if not inner_wall_cells:
176
+ return False
177
+ base_shadow_size = max(cell_size + 2, int(cell_size * 1.35))
178
+ shadow_size = max(1, int(base_shadow_size * 1.5))
179
+ shadow_surface = _get_shadow_tile_surface(
180
+ shadow_size,
181
+ alpha,
182
+ edge_softness=0.12,
183
+ )
184
+ screen_rect = shadow_layer.get_rect()
185
+ px, py = light_source_pos
186
+ drew = False
187
+ clip_max = shadow_size * 0.25
188
+ for cell_x, cell_y in inner_wall_cells:
189
+ world_x = cell_x * cell_size
190
+ world_y = cell_y * cell_size
191
+ wall_rect = pygame.Rect(world_x, world_y, cell_size, cell_size)
192
+ wall_screen_rect = camera.apply_rect(wall_rect)
193
+ if not wall_screen_rect.colliderect(screen_rect):
194
+ continue
195
+ center_x = world_x + cell_size / 2
196
+ center_y = world_y + cell_size / 2
197
+ dx = (center_x - px) * 0.5
198
+ dy = (center_y - py) * 0.5
199
+ dx = int(_abs_clip(dx, 0, clip_max))
200
+ dy = int(_abs_clip(dy, 0, clip_max))
201
+ shadow_rect = pygame.Rect(0, 0, shadow_size, shadow_size)
202
+ shadow_rect.center = (
203
+ int(center_x + dx),
204
+ int(center_y + dy),
205
+ )
206
+ shadow_screen_rect = camera.apply_rect(shadow_rect)
207
+ if not shadow_screen_rect.colliderect(screen_rect):
208
+ continue
209
+ shadow_layer.blit(
210
+ shadow_surface,
211
+ shadow_screen_rect.topleft,
212
+ special_flags=pygame.BLEND_RGBA_MAX,
213
+ )
214
+ drew = True
215
+ return drew
216
+
217
+
218
+ def _draw_entity_shadows(
219
+ shadow_layer: surface.Surface,
220
+ camera: Camera,
221
+ all_sprites: sprite.LayeredUpdates,
222
+ *,
223
+ light_source_pos: tuple[int, int] | None,
224
+ exclude_car: Car | None,
225
+ outside_cells: set[tuple[int, int]] | None,
226
+ cell_size: int,
227
+ shadow_radius: int = int(ZOMBIE_RADIUS * ENTITY_SHADOW_RADIUS_MULT),
228
+ alpha: int = ENTITY_SHADOW_ALPHA,
229
+ ) -> bool:
230
+ if light_source_pos is None or shadow_radius <= 0:
231
+ return False
232
+ if cell_size <= 0:
233
+ outside_cells = None
234
+ shadow_surface = _get_shadow_circle_surface(
235
+ shadow_radius,
236
+ alpha,
237
+ edge_softness=ENTITY_SHADOW_EDGE_SOFTNESS,
238
+ )
239
+ screen_rect = shadow_layer.get_rect()
240
+ px, py = light_source_pos
241
+ offset_dist = max(1.0, shadow_radius * 0.6)
242
+ drew = False
243
+ for entity in all_sprites:
244
+ if not entity.alive():
245
+ continue
246
+ if isinstance(entity, Player):
247
+ continue
248
+ if isinstance(entity, Car):
249
+ if exclude_car is not None and entity is exclude_car:
250
+ continue
251
+ if not isinstance(entity, (Zombie, Survivor, Car)):
252
+ continue
253
+ if outside_cells:
254
+ cell = (
255
+ int(entity.rect.centerx // cell_size),
256
+ int(entity.rect.centery // cell_size),
257
+ )
258
+ if cell in outside_cells:
259
+ continue
260
+ cx, cy = entity.rect.center
261
+ dx = cx - px
262
+ dy = cy - py
263
+ dist = math.hypot(dx, dy)
264
+ if dist > 0.001:
265
+ scale = offset_dist / dist
266
+ offset_x = dx * scale
267
+ offset_y = dy * scale
268
+ else:
269
+ offset_x = 0.0
270
+ offset_y = 0.0
271
+
272
+ jump_dy = 0.0
273
+ if getattr(entity, "is_jumping", False):
274
+ jump_dy = JUMP_SHADOW_OFFSET
275
+
276
+ shadow_rect = shadow_surface.get_rect(center=(int(cx + offset_x), int(cy + offset_y + jump_dy)))
277
+ shadow_screen_rect = camera.apply_rect(shadow_rect)
278
+ if not shadow_screen_rect.colliderect(screen_rect):
279
+ continue
280
+ shadow_layer.blit(
281
+ shadow_surface,
282
+ shadow_screen_rect.topleft,
283
+ special_flags=pygame.BLEND_RGBA_MAX,
284
+ )
285
+ drew = True
286
+ return drew
287
+
288
+
289
+ def _draw_single_entity_shadow(
290
+ shadow_layer: surface.Surface,
291
+ camera: Camera,
292
+ *,
293
+ entity: pygame.sprite.Sprite | None,
294
+ light_source_pos: tuple[int, int] | None,
295
+ outside_cells: set[tuple[int, int]] | None,
296
+ cell_size: int,
297
+ shadow_radius: int,
298
+ alpha: int,
299
+ edge_softness: float = ENTITY_SHADOW_EDGE_SOFTNESS,
300
+ ) -> bool:
301
+ if entity is None or not entity.alive() or light_source_pos is None or shadow_radius <= 0:
302
+ return False
303
+ if outside_cells and cell_size > 0:
304
+ cell = (
305
+ int(entity.rect.centerx // cell_size),
306
+ int(entity.rect.centery // cell_size),
307
+ )
308
+ if cell in outside_cells:
309
+ return False
310
+ shadow_surface = _get_shadow_circle_surface(
311
+ shadow_radius,
312
+ alpha,
313
+ edge_softness=edge_softness,
314
+ )
315
+ screen_rect = shadow_layer.get_rect()
316
+ px, py = light_source_pos
317
+ cx, cy = entity.rect.center
318
+ dx = cx - px
319
+ dy = cy - py
320
+ dist = math.hypot(dx, dy)
321
+ offset_dist = max(1.0, shadow_radius * 0.6)
322
+ if dist > 0.001:
323
+ scale = offset_dist / dist
324
+ offset_x = dx * scale
325
+ offset_y = dy * scale
326
+ else:
327
+ offset_x = 0.0
328
+ offset_y = 0.0
329
+
330
+ jump_dy = 0.0
331
+ if getattr(entity, "is_jumping", False):
332
+ jump_dy = JUMP_SHADOW_OFFSET
333
+
334
+ shadow_rect = shadow_surface.get_rect(center=(int(cx + offset_x), int(cy + offset_y + jump_dy)))
335
+ shadow_screen_rect = camera.apply_rect(shadow_rect)
336
+ if not shadow_screen_rect.colliderect(screen_rect):
337
+ return False
338
+ shadow_layer.blit(
339
+ shadow_surface,
340
+ shadow_screen_rect.topleft,
341
+ special_flags=pygame.BLEND_RGBA_MAX,
342
+ )
343
+ return True
@@ -46,9 +46,7 @@ def _draw_outlined_circle(
46
46
  pygame.draw.circle(surface, outline_color, center, radius, width=outline_width)
47
47
 
48
48
 
49
- def _brighten_color(
50
- color: tuple[int, int, int], *, factor: float = 1.25
51
- ) -> tuple[int, int, int]:
49
+ def _brighten_color(color: tuple[int, int, int], *, factor: float = 1.25) -> tuple[int, int, int]:
52
50
  return tuple(min(255, int(c * factor + 0.5)) for c in color)
53
51
 
54
52
 
@@ -58,9 +56,7 @@ _PLAYER_UPSCALE_FACTOR = 4
58
56
  _CAR_UPSCALE_FACTOR = 4
59
57
 
60
58
  _PLAYER_DIRECTIONAL_CACHE: dict[tuple[int, int], list[pygame.Surface]] = {}
61
- _SURVIVOR_DIRECTIONAL_CACHE: dict[
62
- tuple[int, bool, bool, int], list[pygame.Surface]
63
- ] = {}
59
+ _SURVIVOR_DIRECTIONAL_CACHE: dict[tuple[int, bool, bool, int], list[pygame.Surface]] = {}
64
60
  _ZOMBIE_DIRECTIONAL_CACHE: dict[tuple[int, bool, int], list[pygame.Surface]] = {}
65
61
  _RUBBLE_SURFACE_CACHE: dict[tuple, pygame.Surface] = {}
66
62
 
@@ -70,9 +66,7 @@ RUBBLE_SCALE_RATIO = 0.9
70
66
  RUBBLE_SHADOW_RATIO = 0.9
71
67
 
72
68
 
73
- def _scale_color(
74
- color: tuple[int, int, int], *, ratio: float
75
- ) -> tuple[int, int, int]:
69
+ def _scale_color(color: tuple[int, int, int], *, ratio: float) -> tuple[int, int, int]:
76
70
  return tuple(max(0, min(255, int(c * ratio + 0.5))) for c in color)
77
71
 
78
72
 
@@ -80,9 +74,7 @@ def rubble_offset_for_size(size: int) -> int:
80
74
  return max(1, int(round(size * RUBBLE_OFFSET_RATIO)))
81
75
 
82
76
 
83
- def angle_bin_from_vector(
84
- dx: float, dy: float, *, bins: int = ANGLE_BINS
85
- ) -> int | None:
77
+ def angle_bin_from_vector(dx: float, dy: float, *, bins: int = ANGLE_BINS) -> int | None:
86
78
  if dx == 0 and dy == 0:
87
79
  return None
88
80
  angle = math.atan2(dy, dx)
@@ -388,9 +380,7 @@ def resolve_steel_beam_colors(
388
380
  return STEEL_BEAM_COLOR, STEEL_BEAM_LINE_COLOR
389
381
 
390
382
 
391
- def build_player_directional_surfaces(
392
- radius: int, *, bins: int = ANGLE_BINS
393
- ) -> list[pygame.Surface]:
383
+ def build_player_directional_surfaces(radius: int, *, bins: int = ANGLE_BINS) -> list[pygame.Surface]:
394
384
  cache_key = (radius, bins)
395
385
  if cache_key in _PLAYER_DIRECTIONAL_CACHE:
396
386
  return _PLAYER_DIRECTIONAL_CACHE[cache_key]
@@ -545,9 +535,7 @@ def paint_car_surface(
545
535
  up_width = width * upscale
546
536
  up_height = height * upscale
547
537
  up_surface = pygame.Surface((up_width, up_height), pygame.SRCALPHA)
548
- _paint_car_surface_base(
549
- up_surface, width=up_width, height=up_height, color=color
550
- )
538
+ _paint_car_surface_base(up_surface, width=up_width, height=up_height, color=color)
551
539
  scaled = pygame.transform.smoothscale(up_surface, (width, height))
552
540
  surface.fill((0, 0, 0, 0))
553
541
  surface.blit(scaled, (0, 0))
@@ -604,9 +592,7 @@ def _paint_car_surface_base(
604
592
  pygame.draw.ellipse(surface, headlight_color, headlight_right)
605
593
 
606
594
 
607
- def build_car_directional_surfaces(
608
- base_surface: pygame.Surface, *, bins: int = ANGLE_BINS
609
- ) -> list[pygame.Surface]:
595
+ def build_car_directional_surfaces(base_surface: pygame.Surface, *, bins: int = ANGLE_BINS) -> list[pygame.Surface]:
610
596
  """Return pre-rotated car surfaces matching angle_bin_from_vector bins."""
611
597
  surfaces: list[pygame.Surface] = []
612
598
  upscale = _CAR_UPSCALE_FACTOR
@@ -660,9 +646,7 @@ def paint_wall_surface(
660
646
  ) -> None:
661
647
  face_width, face_height = face_size or target.get_size()
662
648
  if bevel_depth > 0 and any(bevel_mask):
663
- face_polygon = build_beveled_polygon(
664
- face_width, face_height, bevel_depth, bevel_mask
665
- )
649
+ face_polygon = build_beveled_polygon(face_width, face_height, bevel_depth, bevel_mask)
666
650
  pygame.draw.polygon(target, border_color, face_polygon)
667
651
  else:
668
652
  target.fill(border_color)
@@ -671,9 +655,7 @@ def paint_wall_surface(
671
655
  if inner_rect.width > 0 and inner_rect.height > 0:
672
656
  inner_depth = max(0, bevel_depth - border_width)
673
657
  if inner_depth > 0 and any(bevel_mask):
674
- inner_polygon = build_beveled_polygon(
675
- inner_rect.width, inner_rect.height, inner_depth, bevel_mask
676
- )
658
+ inner_polygon = build_beveled_polygon(inner_rect.width, inner_rect.height, inner_depth, bevel_mask)
677
659
  inner_offset_polygon = [
678
660
  (
679
661
  int(point[0] + inner_rect.left),
@@ -703,9 +685,7 @@ def paint_wall_surface(
703
685
  side_color = tuple(int(c * side_shade_ratio) for c in fill_color)
704
686
  side_surface = pygame.Surface(rect_obj.size, pygame.SRCALPHA)
705
687
  if bevel_depth > 0 and any(bevel_mask):
706
- side_polygon = build_beveled_polygon(
707
- rect_obj.width, rect_obj.height, bevel_depth, bevel_mask
708
- )
688
+ side_polygon = build_beveled_polygon(rect_obj.width, rect_obj.height, bevel_depth, bevel_mask)
709
689
  pygame.draw.polygon(side_surface, side_color, side_polygon)
710
690
  else:
711
691
  pygame.draw.rect(side_surface, side_color, rect_obj)
@@ -785,9 +765,7 @@ def build_rubble_wall_surface(
785
765
  final_surface = pygame.Surface((safe_size, safe_size), pygame.SRCALPHA)
786
766
  center = final_surface.get_rect().center
787
767
 
788
- shadow_rect = shadow_surface.get_rect(
789
- center=(center[0] + offset_px, center[1] + offset_px)
790
- )
768
+ shadow_rect = shadow_surface.get_rect(center=(center[0] + offset_px, center[1] + offset_px))
791
769
  final_surface.blit(shadow_surface, shadow_rect.topleft)
792
770
 
793
771
  top_rect = top_surface.get_rect(center=center)
zombie_escape/rng.py CHANGED
@@ -43,9 +43,7 @@ class DeterministicRNG:
43
43
  self._state[0] = seed32
44
44
  for i in range(1, self._N):
45
45
  prev = self._state[i - 1]
46
- self._state[i] = (
47
- (1812433253 * (prev ^ (prev >> 30)) + i) & 0xFFFFFFFF
48
- )
46
+ self._state[i] = (1812433253 * (prev ^ (prev >> 30)) + i) & 0xFFFFFFFF
49
47
  self._index = self._N
50
48
 
51
49
  @property
@@ -93,17 +91,15 @@ class DeterministicRNG:
93
91
  self._twist()
94
92
  y = self._state[self._index]
95
93
  self._index += 1
96
- y ^= (y >> 11)
94
+ y ^= y >> 11
97
95
  y ^= (y << 7) & 0x9D2C5680
98
96
  y ^= (y << 15) & 0xEFC60000
99
- y ^= (y >> 18)
97
+ y ^= y >> 18
100
98
  return y & 0xFFFFFFFF
101
99
 
102
100
  def _twist(self) -> None:
103
101
  for i in range(self._N):
104
- x = (self._state[i] & self._UPPER_MASK) + (
105
- self._state[(i + 1) % self._N] & self._LOWER_MASK
106
- )
102
+ x = (self._state[i] & self._UPPER_MASK) + (self._state[(i + 1) % self._N] & self._LOWER_MASK)
107
103
  xA = x >> 1
108
104
  if x & 1:
109
105
  xA ^= self._MATRIX_A
@@ -93,18 +93,14 @@ def present(logical_surface: surface.Surface) -> None:
93
93
  if (scaled_width, scaled_height) == logical_size:
94
94
  scaled_surface = logical_surface
95
95
  else:
96
- scaled_surface = pygame.transform.scale(
97
- logical_surface, (scaled_width, scaled_height)
98
- )
96
+ scaled_surface = pygame.transform.scale(logical_surface, (scaled_width, scaled_height))
99
97
  offset_x = (window_size[0] - scaled_width) // 2
100
98
  offset_y = (window_size[1] - scaled_height) // 2
101
99
  window.blit(scaled_surface, (offset_x, offset_y))
102
100
  pygame.display.flip()
103
101
 
104
102
 
105
- def apply_window_scale(
106
- scale: float, *, game_data: "GameData | None" = None
107
- ) -> surface.Surface:
103
+ def apply_window_scale(scale: float, *, game_data: "GameData | None" = None) -> surface.Surface:
108
104
  """Resize the OS window; logical render surface stays constant."""
109
105
  global current_window_scale, current_maximized, last_window_scale
110
106
 
@@ -116,11 +112,9 @@ def apply_window_scale(
116
112
  window_width = max(1, int(SCREEN_WIDTH * current_window_scale))
117
113
  window_height = max(1, int(SCREEN_HEIGHT * current_window_scale))
118
114
 
119
- new_window = pygame.display.set_mode(
120
- (window_width, window_height), pygame.RESIZABLE
121
- )
115
+ new_window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
122
116
  _update_window_size((window_width, window_height), source="apply_scale")
123
- _update_window_caption(window_width, window_height)
117
+ _update_window_caption()
124
118
 
125
119
  if game_data is not None:
126
120
  game_data.state.overview_created = False
@@ -128,28 +122,22 @@ def apply_window_scale(
128
122
  return new_window
129
123
 
130
124
 
131
- def nudge_window_scale(
132
- multiplier: float, *, game_data: "GameData | None" = None
133
- ) -> surface.Surface:
125
+ def nudge_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
134
126
  """Scale the window relative to the current zoom level."""
135
127
  target_scale = current_window_scale * multiplier
136
128
  return apply_window_scale(target_scale, game_data=game_data)
137
129
 
138
130
 
139
- def toggle_fullscreen(
140
- *, game_data: "GameData | None" = None
141
- ) -> surface.Surface | None:
131
+ def toggle_fullscreen(*, game_data: "GameData | None" = None) -> surface.Surface | None:
142
132
  """Toggle a maximized window without persisting the setting."""
143
133
  global current_maximized, last_window_scale
144
134
  if current_maximized:
145
135
  current_maximized = False
146
136
  window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
147
137
  window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
148
- window = pygame.display.set_mode(
149
- (window_width, window_height), pygame.RESIZABLE
150
- )
138
+ window = pygame.display.set_mode((window_width, window_height), pygame.RESIZABLE)
151
139
  _restore_window()
152
- _update_window_caption(window_width, window_height)
140
+ _update_window_caption()
153
141
  _update_window_size((window_width, window_height), source="toggle_windowed")
154
142
  else:
155
143
  last_window_scale = current_window_scale
@@ -157,7 +145,7 @@ def toggle_fullscreen(
157
145
  window = pygame.display.set_mode(_fetch_window_size(None), pygame.RESIZABLE)
158
146
  _maximize_window()
159
147
  window_width, window_height = _fetch_window_size(window)
160
- _update_window_caption(window_width, window_height)
148
+ _update_window_caption()
161
149
  _update_window_size((window_width, window_height), source="toggle_fullscreen")
162
150
  pygame.mouse.set_visible(not current_maximized)
163
151
  if game_data is not None:
@@ -165,9 +153,7 @@ def toggle_fullscreen(
165
153
  return window
166
154
 
167
155
 
168
- def sync_window_size(
169
- event: pygame.event.Event, *, game_data: "GameData | None" = None
170
- ) -> None:
156
+ def sync_window_size(event: pygame.event.Event, *, game_data: "GameData | None" = None) -> None:
171
157
  """Synchronize tracked window size with SDL window events."""
172
158
  global current_window_scale, last_window_scale
173
159
  size = getattr(event, "size", None)
@@ -179,16 +165,14 @@ def sync_window_size(
179
165
  if not size:
180
166
  return
181
167
  window_width, window_height = _normalize_window_size(size)
182
- _update_window_size(
183
- (window_width, window_height), source="window_event"
184
- )
168
+ _update_window_size((window_width, window_height), source="window_event")
185
169
  if not current_maximized:
186
170
  scale_x = window_width / max(1, SCREEN_WIDTH)
187
171
  scale_y = window_height / max(1, SCREEN_HEIGHT)
188
172
  scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, min(scale_x, scale_y)))
189
173
  current_window_scale = scale
190
174
  last_window_scale = scale
191
- _update_window_caption(window_width, window_height)
175
+ _update_window_caption()
192
176
  if game_data is not None:
193
177
  game_data.state.overview_created = False
194
178
 
@@ -219,8 +203,8 @@ def _update_window_size(size: tuple[int, int], *, source: str) -> None:
219
203
  last_logged_window_size = size
220
204
 
221
205
 
222
- def _update_window_caption(window_width: int, window_height: int) -> None:
223
- pygame.display.set_caption(f"Zombie Escape ({window_width}x{window_height})")
206
+ def _update_window_caption() -> None:
207
+ pygame.display.set_caption("Zombie Escape")
224
208
 
225
209
 
226
210
  def _maximize_window() -> None: