zombie-escape 1.12.0__py3-none-any.whl → 1.12.3__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.
@@ -25,7 +25,8 @@ def rect_visible_on_screen(camera: Camera | None, rect: pygame.Rect) -> bool:
25
25
 
26
26
 
27
27
  def _scatter_positions_on_walkable(
28
- walkable_cells: list[pygame.Rect],
28
+ walkable_cells: list[tuple[int, int]],
29
+ cell_size: int,
29
30
  spawn_rate: float,
30
31
  *,
31
32
  jitter_ratio: float = 0.35,
@@ -35,17 +36,21 @@ def _scatter_positions_on_walkable(
35
36
  return positions
36
37
 
37
38
  clamped_rate = max(0.0, min(1.0, spawn_rate))
38
- for cell in walkable_cells:
39
+ for cell_x, cell_y in walkable_cells:
39
40
  if RNG.random() >= clamped_rate:
40
41
  continue
41
- jitter_x = RNG.uniform(-cell.width * jitter_ratio, cell.width * jitter_ratio)
42
- jitter_y = RNG.uniform(-cell.height * jitter_ratio, cell.height * jitter_ratio)
43
- positions.append((int(cell.centerx + jitter_x), int(cell.centery + jitter_y)))
42
+ jitter_extent = cell_size * jitter_ratio
43
+ jitter_x = RNG.uniform(-jitter_extent, jitter_extent)
44
+ jitter_y = RNG.uniform(-jitter_extent, jitter_extent)
45
+ base_x = (cell_x * cell_size) + (cell_size / 2)
46
+ base_y = (cell_y * cell_size) + (cell_size / 2)
47
+ positions.append((int(base_x + jitter_x), int(base_y + jitter_y)))
44
48
  return positions
45
49
 
46
50
 
47
51
  def find_interior_spawn_positions(
48
- walkable_cells: list[pygame.Rect],
52
+ walkable_cells: list[tuple[int, int]],
53
+ cell_size: int,
49
54
  spawn_rate: float,
50
55
  *,
51
56
  player: Player | None = None,
@@ -53,12 +58,14 @@ def find_interior_spawn_positions(
53
58
  ) -> list[tuple[int, int]]:
54
59
  positions = _scatter_positions_on_walkable(
55
60
  walkable_cells,
61
+ cell_size,
56
62
  spawn_rate,
57
63
  jitter_ratio=0.35,
58
64
  )
59
65
  if not positions and spawn_rate > 0:
60
66
  positions = _scatter_positions_on_walkable(
61
67
  walkable_cells,
68
+ cell_size,
62
69
  spawn_rate * 1.5,
63
70
  jitter_ratio=0.35,
64
71
  )
@@ -78,7 +85,8 @@ def find_interior_spawn_positions(
78
85
 
79
86
 
80
87
  def find_nearby_offscreen_spawn_position(
81
- walkable_cells: list[pygame.Rect],
88
+ walkable_cells: list[tuple[int, int]],
89
+ cell_size: int,
82
90
  *,
83
91
  player: Player | None = None,
84
92
  camera: Camera | None = None,
@@ -104,10 +112,14 @@ def find_nearby_offscreen_spawn_position(
104
112
  None if max_player_dist is None else max_player_dist * max_player_dist
105
113
  )
106
114
  for _ in range(max(1, attempts)):
107
- cell = RNG.choice(walkable_cells)
108
- jitter_x = RNG.uniform(-cell.width * 0.35, cell.width * 0.35)
109
- jitter_y = RNG.uniform(-cell.height * 0.35, cell.height * 0.35)
110
- candidate = (int(cell.centerx + jitter_x), int(cell.centery + jitter_y))
115
+ cell_x, cell_y = RNG.choice(walkable_cells)
116
+ jitter_extent = cell_size * 0.35
117
+ jitter_x = RNG.uniform(-jitter_extent, jitter_extent)
118
+ jitter_y = RNG.uniform(-jitter_extent, jitter_extent)
119
+ candidate = (
120
+ int((cell_x * cell_size) + (cell_size / 2) + jitter_x),
121
+ int((cell_y * cell_size) + (cell_size / 2) + jitter_y),
122
+ )
111
123
  if player is not None and (
112
124
  min_distance_sq is not None or max_distance_sq is not None
113
125
  ):
@@ -123,8 +135,11 @@ def find_nearby_offscreen_spawn_position(
123
135
  return candidate
124
136
  if player is not None and (min_distance_sq is not None or max_distance_sq is not None):
125
137
  for _ in range(20):
126
- cell = RNG.choice(walkable_cells)
127
- center = (cell.centerx, cell.centery)
138
+ cell_x, cell_y = RNG.choice(walkable_cells)
139
+ center = (
140
+ (cell_x * cell_size) + (cell_size / 2),
141
+ (cell_y * cell_size) + (cell_size / 2),
142
+ )
128
143
  if view_rect is not None and view_rect.collidepoint(center):
129
144
  continue
130
145
  dx = center[0] - player.x
@@ -134,15 +149,19 @@ def find_nearby_offscreen_spawn_position(
134
149
  continue
135
150
  if max_distance_sq is not None and dist_sq > max_distance_sq:
136
151
  continue
137
- fallback_x = RNG.uniform(-cell.width * 0.2, cell.width * 0.2)
138
- fallback_y = RNG.uniform(-cell.height * 0.2, cell.height * 0.2)
139
- return (int(cell.centerx + fallback_x), int(cell.centery + fallback_y))
140
- fallback_cell = RNG.choice(walkable_cells)
141
- fallback_x = RNG.uniform(-fallback_cell.width * 0.35, fallback_cell.width * 0.35)
142
- fallback_y = RNG.uniform(-fallback_cell.height * 0.35, fallback_cell.height * 0.35)
152
+ fallback_extent = cell_size * 0.2
153
+ fallback_x = RNG.uniform(-fallback_extent, fallback_extent)
154
+ fallback_y = RNG.uniform(-fallback_extent, fallback_extent)
155
+ return (int(center[0] + fallback_x), int(center[1] + fallback_y))
156
+ fallback_cell_x, fallback_cell_y = RNG.choice(walkable_cells)
157
+ fallback_center_x = (fallback_cell_x * cell_size) + (cell_size / 2)
158
+ fallback_center_y = (fallback_cell_y * cell_size) + (cell_size / 2)
159
+ fallback_extent = cell_size * 0.35
160
+ fallback_x = RNG.uniform(-fallback_extent, fallback_extent)
161
+ fallback_y = RNG.uniform(-fallback_extent, fallback_extent)
143
162
  return (
144
- int(fallback_cell.centerx + fallback_x),
145
- int(fallback_cell.centery + fallback_y),
163
+ int(fallback_center_x + fallback_x),
164
+ int(fallback_center_y + fallback_y),
146
165
  )
147
166
 
148
167
 
@@ -218,9 +218,10 @@ def _place_walls_grid_wire(
218
218
  grid[y][x] = "1"
219
219
 
220
220
 
221
- def _place_walls_sparse(
221
+ def _place_walls_sparse_moore(
222
222
  grid: list[list[str]],
223
223
  *,
224
+ density: float = SPARSE_WALL_DENSITY,
224
225
  forbidden_cells: set[tuple[int, int]] | None = None,
225
226
  ) -> None:
226
227
  """Place isolated wall tiles at a low density, avoiding adjacency."""
@@ -234,7 +235,7 @@ def _place_walls_sparse(
234
235
  continue
235
236
  if grid[y][x] != ".":
236
237
  continue
237
- if RNG.random() >= SPARSE_WALL_DENSITY:
238
+ if RNG.random() >= density:
238
239
  continue
239
240
  if (
240
241
  grid[y - 1][x] == "1"
@@ -250,11 +251,40 @@ def _place_walls_sparse(
250
251
  grid[y][x] = "1"
251
252
 
252
253
 
254
+ def _place_walls_sparse_ortho(
255
+ grid: list[list[str]],
256
+ *,
257
+ density: float = SPARSE_WALL_DENSITY,
258
+ forbidden_cells: set[tuple[int, int]] | None = None,
259
+ ) -> None:
260
+ """Place isolated wall tiles at a low density, avoiding orthogonal adjacency."""
261
+ cols, rows = len(grid[0]), len(grid)
262
+ forbidden = _collect_exit_adjacent_cells(grid)
263
+ if forbidden_cells:
264
+ forbidden |= forbidden_cells
265
+ for y in range(2, rows - 2):
266
+ for x in range(2, cols - 2):
267
+ if (x, y) in forbidden:
268
+ continue
269
+ if grid[y][x] != ".":
270
+ continue
271
+ if RNG.random() >= density:
272
+ continue
273
+ if (
274
+ grid[y - 1][x] == "1"
275
+ or grid[y + 1][x] == "1"
276
+ or grid[y][x - 1] == "1"
277
+ or grid[y][x + 1] == "1"
278
+ ):
279
+ continue
280
+ grid[y][x] = "1"
281
+
253
282
  WALL_ALGORITHMS = {
254
283
  "default": _place_walls_default,
255
284
  "empty": _place_walls_empty,
256
285
  "grid_wire": _place_walls_grid_wire,
257
- "sparse": _place_walls_sparse,
286
+ "sparse_moore": _place_walls_sparse_moore,
287
+ "sparse_ortho": _place_walls_sparse_ortho,
258
288
  }
259
289
 
260
290
 
@@ -321,6 +351,56 @@ def _generate_random_blueprint(
321
351
  reserved_cells.add((zx, zy))
322
352
 
323
353
  # Select and run the wall placement algorithm (after reserving spawns)
354
+ sparse_density = SPARSE_WALL_DENSITY
355
+ original_wall_algo = wall_algo
356
+ if wall_algo == "sparse":
357
+ print(
358
+ "WARNING: 'sparse' is deprecated. Use 'sparse_moore' instead."
359
+ )
360
+ wall_algo = "sparse_moore"
361
+ elif wall_algo.startswith("sparse."):
362
+ print(
363
+ "WARNING: 'sparse.<int>%' is deprecated. Use "
364
+ "'sparse_moore.<int>%' instead."
365
+ )
366
+ suffix = wall_algo[len("sparse.") :]
367
+ wall_algo = "sparse_moore"
368
+ if suffix.endswith("%") and suffix[:-1].isdigit():
369
+ percent = int(suffix[:-1])
370
+ if 0 <= percent <= 100:
371
+ sparse_density = percent / 100.0
372
+ else:
373
+ print(
374
+ "WARNING: Sparse wall density must be 0-100%. "
375
+ f"Got '{suffix}'. Falling back to default sparse density."
376
+ )
377
+ else:
378
+ print(
379
+ "WARNING: Invalid sparse wall format. Use "
380
+ "'sparse_moore.<int>%' or 'sparse_ortho.<int>%'. "
381
+ f"Got '{original_wall_algo}'. Falling back to default sparse density."
382
+ )
383
+ if wall_algo.startswith("sparse_moore.") or wall_algo.startswith("sparse_ortho."):
384
+ base, suffix = wall_algo.split(".", 1)
385
+ if suffix.endswith("%") and suffix[:-1].isdigit():
386
+ percent = int(suffix[:-1])
387
+ if 0 <= percent <= 100:
388
+ sparse_density = percent / 100.0
389
+ wall_algo = base
390
+ else:
391
+ print(
392
+ "WARNING: Sparse wall density must be 0-100%. "
393
+ f"Got '{suffix}'. Falling back to default sparse density."
394
+ )
395
+ wall_algo = base
396
+ else:
397
+ print(
398
+ "WARNING: Invalid sparse wall format. Use "
399
+ "'sparse_moore.<int>%' or 'sparse_ortho.<int>%'. "
400
+ f"Got '{wall_algo}'. Falling back to default sparse density."
401
+ )
402
+ wall_algo = base
403
+
324
404
  if wall_algo not in WALL_ALGORITHMS:
325
405
  print(
326
406
  f"WARNING: Unknown wall algorithm '{wall_algo}'. Falling back to 'default'."
@@ -328,7 +408,10 @@ def _generate_random_blueprint(
328
408
  wall_algo = "default"
329
409
 
330
410
  algo_func = WALL_ALGORITHMS[wall_algo]
331
- algo_func(grid, forbidden_cells=reserved_cells)
411
+ if wall_algo in {"sparse_moore", "sparse_ortho"}:
412
+ algo_func(grid, density=sparse_density, forbidden_cells=reserved_cells)
413
+ else:
414
+ algo_func(grid, forbidden_cells=reserved_cells)
332
415
 
333
416
  steel_beams = _place_steel_beams(
334
417
  grid, chance=steel_chance, forbidden_cells=reserved_cells
zombie_escape/models.py CHANGED
@@ -26,10 +26,9 @@ if TYPE_CHECKING: # pragma: no cover - typing-only imports
26
26
  class LevelLayout:
27
27
  """Container for level layout rectangles and cell sets."""
28
28
 
29
- outer_rect: tuple[int, int, int, int]
30
- inner_rect: tuple[int, int, int, int]
31
- outside_rects: list[pygame.Rect]
32
- walkable_cells: list[pygame.Rect]
29
+ field_rect: pygame.Rect
30
+ outside_cells: set[tuple[int, int]]
31
+ walkable_cells: list[tuple[int, int]]
33
32
  outer_wall_cells: set[tuple[int, int]]
34
33
  wall_cells: set[tuple[int, int]]
35
34
  fall_spawn_cells: set[tuple[int, int]]
@@ -106,6 +105,8 @@ class ProgressState:
106
105
  falling_zombies: list[FallingZombie]
107
106
  falling_spawn_carry: int
108
107
  dust_rings: list[DustRing]
108
+ player_wall_target_cell: tuple[int, int] | None
109
+ player_wall_target_ttl: int
109
110
 
110
111
 
111
112
  @dataclass
zombie_escape/render.py CHANGED
@@ -807,11 +807,16 @@ def _draw_play_area(
807
807
  camera: Camera,
808
808
  assets: RenderAssets,
809
809
  palette: Any,
810
- outer_rect: tuple[int, int, int, int],
811
- outside_rects: list[pygame.Rect],
810
+ field_rect: pygame.Rect,
811
+ outside_cells: set[tuple[int, int]],
812
812
  fall_spawn_cells: set[tuple[int, int]],
813
813
  ) -> tuple[int, int, int, int, set[tuple[int, int]]]:
814
- xs, ys, xe, ye = outer_rect
814
+ xs, ys, xe, ye = (
815
+ field_rect.left,
816
+ field_rect.top,
817
+ field_rect.right,
818
+ field_rect.bottom,
819
+ )
815
820
  xs //= assets.internal_wall_grid_snap
816
821
  ys //= assets.internal_wall_grid_snap
817
822
  xe //= assets.internal_wall_grid_snap
@@ -826,15 +831,6 @@ def _draw_play_area(
826
831
  play_area_screen_rect = camera.apply_rect(play_area_rect)
827
832
  pygame.draw.rect(screen, palette.floor_primary, play_area_screen_rect)
828
833
 
829
- outside_cells = {
830
- (r.x // assets.internal_wall_grid_snap, r.y // assets.internal_wall_grid_snap)
831
- for r in outside_rects
832
- }
833
- for rect_obj in outside_rects:
834
- sr = camera.apply_rect(rect_obj)
835
- if sr.colliderect(screen.get_rect()):
836
- pygame.draw.rect(screen, palette.outside, sr)
837
-
838
834
  view_world = pygame.Rect(
839
835
  -camera.camera.x,
840
836
  -camera.camera.y,
@@ -855,6 +851,19 @@ def _draw_play_area(
855
851
  for y in range(start_y, end_y):
856
852
  for x in range(start_x, end_x):
857
853
  if (x, y) in outside_cells:
854
+ lx, ly = (
855
+ x * assets.internal_wall_grid_snap,
856
+ y * assets.internal_wall_grid_snap,
857
+ )
858
+ r = pygame.Rect(
859
+ lx,
860
+ ly,
861
+ assets.internal_wall_grid_snap,
862
+ assets.internal_wall_grid_snap,
863
+ )
864
+ sr = camera.apply_rect(r)
865
+ if sr.colliderect(screen.get_rect()):
866
+ pygame.draw.rect(screen, palette.outside, sr)
858
867
  continue
859
868
  use_secondary = ((x // 2) + (y // 2)) % 2 == 0
860
869
  if (x, y) in fall_spawn_cells:
@@ -966,11 +975,15 @@ def _draw_entity_shadows(
966
975
  *,
967
976
  light_source_pos: tuple[int, int] | None,
968
977
  exclude_car: Car | None,
978
+ outside_cells: set[tuple[int, int]] | None,
979
+ cell_size: int,
969
980
  shadow_radius: int = int(ZOMBIE_RADIUS * ENTITY_SHADOW_RADIUS_MULT),
970
981
  alpha: int = ENTITY_SHADOW_ALPHA,
971
982
  ) -> bool:
972
983
  if light_source_pos is None or shadow_radius <= 0:
973
984
  return False
985
+ if cell_size <= 0:
986
+ outside_cells = None
974
987
  shadow_surface = _get_shadow_circle_surface(
975
988
  shadow_radius,
976
989
  alpha,
@@ -990,6 +1003,13 @@ def _draw_entity_shadows(
990
1003
  continue
991
1004
  if not isinstance(entity, (Zombie, Survivor, Car)):
992
1005
  continue
1006
+ if outside_cells:
1007
+ cell = (
1008
+ int(entity.rect.centerx // cell_size),
1009
+ int(entity.rect.centery // cell_size),
1010
+ )
1011
+ if cell in outside_cells:
1012
+ continue
993
1013
  cx, cy = entity.rect.center
994
1014
  dx = cx - px
995
1015
  dy = cy - py
@@ -1022,6 +1042,8 @@ def _draw_single_entity_shadow(
1022
1042
  *,
1023
1043
  entity: pygame.sprite.Sprite | None,
1024
1044
  light_source_pos: tuple[int, int] | None,
1045
+ outside_cells: set[tuple[int, int]] | None,
1046
+ cell_size: int,
1025
1047
  shadow_radius: int,
1026
1048
  alpha: int,
1027
1049
  edge_softness: float = ENTITY_SHADOW_EDGE_SOFTNESS,
@@ -1033,6 +1055,13 @@ def _draw_single_entity_shadow(
1033
1055
  or shadow_radius <= 0
1034
1056
  ):
1035
1057
  return False
1058
+ if outside_cells and cell_size > 0:
1059
+ cell = (
1060
+ int(entity.rect.centerx // cell_size),
1061
+ int(entity.rect.centery // cell_size),
1062
+ )
1063
+ if cell in outside_cells:
1064
+ return False
1036
1065
  shadow_surface = _get_shadow_circle_surface(
1037
1066
  shadow_radius,
1038
1067
  alpha,
@@ -1464,8 +1493,8 @@ def draw(
1464
1493
 
1465
1494
  camera = game_data.camera
1466
1495
  stage = game_data.stage
1467
- outer_rect = game_data.layout.outer_rect
1468
- outside_rects = game_data.layout.outside_rects or []
1496
+ field_rect = game_data.layout.field_rect
1497
+ outside_cells = game_data.layout.outside_cells
1469
1498
  all_sprites = game_data.groups.all_sprites
1470
1499
  fog_surfaces = game_data.fog
1471
1500
  footprints = state.footprints
@@ -1493,8 +1522,8 @@ def draw(
1493
1522
  camera,
1494
1523
  assets,
1495
1524
  palette,
1496
- outer_rect,
1497
- outside_rects,
1525
+ field_rect,
1526
+ outside_cells,
1498
1527
  game_data.layout.fall_spawn_cells,
1499
1528
  )
1500
1529
  shadow_layer = _get_shadow_layer(screen.get_size())
@@ -1520,6 +1549,8 @@ def draw(
1520
1549
  all_sprites,
1521
1550
  light_source_pos=fov_target.rect.center if fov_target else None,
1522
1551
  exclude_car=active_car if player.in_car else None,
1552
+ outside_cells=outside_cells,
1553
+ cell_size=game_data.cell_size,
1523
1554
  )
1524
1555
  player_shadow_alpha = max(1, int(ENTITY_SHADOW_ALPHA * PLAYER_SHADOW_ALPHA_MULT))
1525
1556
  player_shadow_radius = int(ZOMBIE_RADIUS * PLAYER_SHADOW_RADIUS_MULT)
@@ -1529,6 +1560,8 @@ def draw(
1529
1560
  camera,
1530
1561
  entity=active_car,
1531
1562
  light_source_pos=fov_target.rect.center if fov_target else None,
1563
+ outside_cells=outside_cells,
1564
+ cell_size=game_data.cell_size,
1532
1565
  shadow_radius=player_shadow_radius,
1533
1566
  alpha=player_shadow_alpha,
1534
1567
  )
@@ -1538,6 +1571,8 @@ def draw(
1538
1571
  camera,
1539
1572
  entity=player,
1540
1573
  light_source_pos=fov_target.rect.center if fov_target else None,
1574
+ outside_cells=outside_cells,
1575
+ cell_size=game_data.cell_size,
1541
1576
  shadow_radius=player_shadow_radius,
1542
1577
  alpha=player_shadow_alpha,
1543
1578
  )