zombie-escape 1.5.4__py3-none-any.whl → 1.7.1__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 (37) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/entities.py +501 -537
  3. zombie_escape/entities_constants.py +102 -0
  4. zombie_escape/gameplay/__init__.py +75 -2
  5. zombie_escape/gameplay/ambient.py +50 -0
  6. zombie_escape/gameplay/constants.py +46 -0
  7. zombie_escape/gameplay/footprints.py +60 -0
  8. zombie_escape/gameplay/interactions.py +354 -0
  9. zombie_escape/gameplay/layout.py +190 -0
  10. zombie_escape/gameplay/movement.py +220 -0
  11. zombie_escape/gameplay/spawn.py +618 -0
  12. zombie_escape/gameplay/state.py +137 -0
  13. zombie_escape/gameplay/survivors.py +306 -0
  14. zombie_escape/gameplay/utils.py +147 -0
  15. zombie_escape/gameplay_constants.py +0 -148
  16. zombie_escape/level_blueprints.py +123 -10
  17. zombie_escape/level_constants.py +6 -13
  18. zombie_escape/locales/ui.en.json +10 -1
  19. zombie_escape/locales/ui.ja.json +10 -1
  20. zombie_escape/models.py +15 -9
  21. zombie_escape/render.py +42 -27
  22. zombie_escape/render_assets.py +533 -23
  23. zombie_escape/render_constants.py +57 -22
  24. zombie_escape/rng.py +9 -9
  25. zombie_escape/screens/__init__.py +59 -29
  26. zombie_escape/screens/game_over.py +3 -3
  27. zombie_escape/screens/gameplay.py +45 -27
  28. zombie_escape/screens/title.py +5 -2
  29. zombie_escape/stage_constants.py +34 -1
  30. zombie_escape/zombie_escape.py +30 -12
  31. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/METADATA +1 -1
  32. zombie_escape-1.7.1.dist-info/RECORD +45 -0
  33. zombie_escape/gameplay/logic.py +0 -1917
  34. zombie_escape-1.5.4.dist-info/RECORD +0 -35
  35. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/WHEEL +0 -0
  36. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/entry_points.txt +0 -0
  37. {zombie_escape-1.5.4.dist-info → zombie_escape-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import pygame
6
+
7
+ from ..colors import get_environment_palette
8
+ from ..entities import SteelBeam, Wall
9
+ from ..entities_constants import INTERNAL_WALL_HEALTH, STEEL_BEAM_HEALTH
10
+ from .constants import OUTER_WALL_HEALTH
11
+ from ..level_blueprints import choose_blueprint
12
+ from ..models import GameData
13
+
14
+ __all__ = ["generate_level_from_blueprint"]
15
+
16
+
17
+ def _rect_for_cell(x_idx: int, y_idx: int, cell_size: int) -> pygame.Rect:
18
+ return pygame.Rect(
19
+ x_idx * cell_size,
20
+ y_idx * cell_size,
21
+ cell_size,
22
+ cell_size,
23
+ )
24
+
25
+
26
+ def generate_level_from_blueprint(
27
+ game_data: GameData, config: dict[str, Any]
28
+ ) -> dict[str, list[pygame.Rect]]:
29
+ """Build walls/spawn candidates/outside area from a blueprint grid."""
30
+ wall_group = game_data.groups.wall_group
31
+ all_sprites = game_data.groups.all_sprites
32
+ stage = game_data.stage
33
+
34
+ steel_conf = config.get("steel_beams", {})
35
+ steel_enabled = steel_conf.get("enabled", False)
36
+
37
+ blueprint_data = choose_blueprint(
38
+ config,
39
+ cols=stage.grid_cols,
40
+ rows=stage.grid_rows,
41
+ wall_algo=stage.wall_algorithm,
42
+ )
43
+ if isinstance(blueprint_data, dict):
44
+ blueprint = blueprint_data.get("grid", [])
45
+ steel_cells_raw = blueprint_data.get("steel_cells", set())
46
+ else:
47
+ blueprint = blueprint_data
48
+ steel_cells_raw = set()
49
+
50
+ steel_cells = (
51
+ {(int(x), int(y)) for x, y in steel_cells_raw} if steel_enabled else set()
52
+ )
53
+ cell_size = game_data.cell_size
54
+ outer_wall_cells = {
55
+ (x, y)
56
+ for y, row in enumerate(blueprint)
57
+ for x, ch in enumerate(row)
58
+ if ch == "B"
59
+ }
60
+ wall_cells = {
61
+ (x, y)
62
+ for y, row in enumerate(blueprint)
63
+ for x, ch in enumerate(row)
64
+ if ch in {"B", "1"}
65
+ }
66
+
67
+ def _has_wall(nx: int, ny: int) -> bool:
68
+ if nx < 0 or ny < 0 or nx >= stage.grid_cols or ny >= stage.grid_rows:
69
+ return True
70
+ return (nx, ny) in wall_cells
71
+
72
+ outside_rects: list[pygame.Rect] = []
73
+ walkable_cells: list[pygame.Rect] = []
74
+ player_cells: list[pygame.Rect] = []
75
+ car_cells: list[pygame.Rect] = []
76
+ zombie_cells: list[pygame.Rect] = []
77
+ palette = get_environment_palette(game_data.state.ambient_palette_key)
78
+
79
+ def add_beam_to_groups(beam: SteelBeam) -> None:
80
+ if getattr(beam, "_added_to_groups", False):
81
+ return
82
+ wall_group.add(beam)
83
+ all_sprites.add(beam, layer=0)
84
+ beam._added_to_groups = True
85
+
86
+ for y, row in enumerate(blueprint):
87
+ if len(row) != stage.grid_cols:
88
+ raise ValueError(
89
+ "Blueprint width mismatch at row "
90
+ f"{y}: {len(row)} != {stage.grid_cols}"
91
+ )
92
+ for x, ch in enumerate(row):
93
+ cell_rect = _rect_for_cell(x, y, cell_size)
94
+ cell_has_beam = steel_enabled and (x, y) in steel_cells
95
+ if ch == "O":
96
+ outside_rects.append(cell_rect)
97
+ continue
98
+ if ch == "B":
99
+ draw_bottom_side = not _has_wall(x, y + 1)
100
+ wall = Wall(
101
+ cell_rect.x,
102
+ cell_rect.y,
103
+ cell_rect.width,
104
+ cell_rect.height,
105
+ health=OUTER_WALL_HEALTH,
106
+ palette=palette,
107
+ palette_category="outer_wall",
108
+ bevel_depth=0,
109
+ draw_bottom_side=draw_bottom_side,
110
+ )
111
+ wall_group.add(wall)
112
+ all_sprites.add(wall, layer=0)
113
+ continue
114
+ if ch == "E":
115
+ if not cell_has_beam:
116
+ walkable_cells.append(cell_rect)
117
+ elif ch == "1":
118
+ beam = None
119
+ if cell_has_beam:
120
+ beam = SteelBeam(
121
+ cell_rect.x,
122
+ cell_rect.y,
123
+ cell_rect.width,
124
+ health=STEEL_BEAM_HEALTH,
125
+ palette=palette,
126
+ )
127
+ draw_bottom_side = not _has_wall(x, y + 1)
128
+ bevel_mask = (
129
+ not _has_wall(x, y - 1)
130
+ and not _has_wall(x - 1, y)
131
+ and not _has_wall(x - 1, y - 1),
132
+ not _has_wall(x, y - 1)
133
+ and not _has_wall(x + 1, y)
134
+ and not _has_wall(x + 1, y - 1),
135
+ not _has_wall(x, y + 1)
136
+ and not _has_wall(x + 1, y)
137
+ and not _has_wall(x + 1, y + 1),
138
+ not _has_wall(x, y + 1)
139
+ and not _has_wall(x - 1, y)
140
+ and not _has_wall(x - 1, y + 1),
141
+ )
142
+ wall = Wall(
143
+ cell_rect.x,
144
+ cell_rect.y,
145
+ cell_rect.width,
146
+ cell_rect.height,
147
+ health=INTERNAL_WALL_HEALTH,
148
+ palette=palette,
149
+ palette_category="inner_wall",
150
+ bevel_mask=bevel_mask,
151
+ draw_bottom_side=draw_bottom_side,
152
+ on_destroy=(lambda _w, b=beam: add_beam_to_groups(b))
153
+ if beam
154
+ else None,
155
+ )
156
+ wall_group.add(wall)
157
+ all_sprites.add(wall, layer=0)
158
+ else:
159
+ if not cell_has_beam:
160
+ walkable_cells.append(cell_rect)
161
+
162
+ if ch == "P":
163
+ player_cells.append(cell_rect)
164
+ if ch == "C":
165
+ car_cells.append(cell_rect)
166
+ if ch == "Z":
167
+ zombie_cells.append(cell_rect)
168
+
169
+ if cell_has_beam and ch != "1":
170
+ beam = SteelBeam(
171
+ cell_rect.x,
172
+ cell_rect.y,
173
+ cell_rect.width,
174
+ health=STEEL_BEAM_HEALTH,
175
+ palette=palette,
176
+ )
177
+ add_beam_to_groups(beam)
178
+
179
+ game_data.layout.outer_rect = (0, 0, game_data.level_width, game_data.level_height)
180
+ game_data.layout.inner_rect = (0, 0, game_data.level_width, game_data.level_height)
181
+ game_data.layout.outside_rects = outside_rects
182
+ game_data.layout.walkable_cells = walkable_cells
183
+ game_data.layout.outer_wall_cells = outer_wall_cells
184
+
185
+ return {
186
+ "player_cells": player_cells,
187
+ "car_cells": car_cells,
188
+ "zombie_cells": zombie_cells,
189
+ "walkable_cells": walkable_cells,
190
+ }
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Any, Sequence
5
+
6
+ import pygame
7
+
8
+ from ..entities import Car, Player, Survivor, Wall, WallIndex, Zombie, walls_for_radius
9
+ from ..entities_constants import (
10
+ CAR_SPEED,
11
+ PLAYER_SPEED,
12
+ ZOMBIE_SEPARATION_DISTANCE,
13
+ ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE,
14
+ )
15
+ from ..models import GameData
16
+ from .constants import MAX_ZOMBIES
17
+ from .spawn import spawn_weighted_zombie
18
+ from .survivors import update_survivors
19
+ from .utils import rect_visible_on_screen
20
+
21
+
22
+ def process_player_input(
23
+ keys: Sequence[bool], player: Player, car: Car | None
24
+ ) -> tuple[float, float, float, float]:
25
+ """Process keyboard input and return movement deltas."""
26
+ dx_input, dy_input = 0, 0
27
+ if keys[pygame.K_w] or keys[pygame.K_UP]:
28
+ dy_input -= 1
29
+ if keys[pygame.K_s] or keys[pygame.K_DOWN]:
30
+ dy_input += 1
31
+ if keys[pygame.K_a] or keys[pygame.K_LEFT]:
32
+ dx_input -= 1
33
+ if keys[pygame.K_d] or keys[pygame.K_RIGHT]:
34
+ dx_input += 1
35
+
36
+ player_dx, player_dy, car_dx, car_dy = 0, 0, 0, 0
37
+
38
+ if player.in_car and car and car.alive():
39
+ target_speed = getattr(car, "speed", CAR_SPEED)
40
+ move_len = math.hypot(dx_input, dy_input)
41
+ if move_len > 0:
42
+ car_dx, car_dy = (
43
+ (dx_input / move_len) * target_speed,
44
+ (dy_input / move_len) * target_speed,
45
+ )
46
+ elif not player.in_car:
47
+ target_speed = PLAYER_SPEED
48
+ move_len = math.hypot(dx_input, dy_input)
49
+ if move_len > 0:
50
+ player_dx, player_dy = (
51
+ (dx_input / move_len) * target_speed,
52
+ (dy_input / move_len) * target_speed,
53
+ )
54
+
55
+ return player_dx, player_dy, car_dx, car_dy
56
+
57
+
58
+ def update_entities(
59
+ game_data: GameData,
60
+ player_dx: float,
61
+ player_dy: float,
62
+ car_dx: float,
63
+ car_dy: float,
64
+ config: dict[str, Any],
65
+ wall_index: WallIndex | None = None,
66
+ ) -> None:
67
+ """Update positions and states of game entities."""
68
+ player = game_data.player
69
+ assert player is not None
70
+ car = game_data.car
71
+ wall_group = game_data.groups.wall_group
72
+ all_sprites = game_data.groups.all_sprites
73
+ zombie_group = game_data.groups.zombie_group
74
+ survivor_group = game_data.groups.survivor_group
75
+ camera = game_data.camera
76
+ stage = game_data.stage
77
+ active_car = car if car and car.alive() else None
78
+
79
+ all_walls = list(wall_group) if wall_index is None else None
80
+
81
+ def _walls_near(center: tuple[float, float], radius: float) -> list[Wall]:
82
+ if wall_index is None:
83
+ return all_walls or []
84
+ return walls_for_radius(
85
+ wall_index,
86
+ center,
87
+ radius,
88
+ cell_size=game_data.cell_size,
89
+ grid_cols=stage.grid_cols,
90
+ grid_rows=stage.grid_rows,
91
+ )
92
+
93
+ # Update player/car movement
94
+ if player.in_car and active_car:
95
+ car_walls = _walls_near((active_car.x, active_car.y), 150.0)
96
+ active_car.move(car_dx, car_dy, car_walls)
97
+ player.rect.center = active_car.rect.center
98
+ player.x, player.y = active_car.x, active_car.y
99
+ elif not player.in_car:
100
+ # Ensure player is in all_sprites if not in car
101
+ if player not in all_sprites:
102
+ all_sprites.add(player, layer=2)
103
+ player.move(
104
+ player_dx,
105
+ player_dy,
106
+ wall_group,
107
+ wall_index=wall_index,
108
+ cell_size=game_data.cell_size,
109
+ level_width=game_data.level_width,
110
+ level_height=game_data.level_height,
111
+ )
112
+ else:
113
+ # Player flagged as in-car but car is gone; drop them back to foot control
114
+ player.in_car = False
115
+
116
+ # Update camera
117
+ target_for_camera = active_car if player.in_car and active_car else player
118
+ camera.update(target_for_camera)
119
+
120
+ update_survivors(game_data, wall_index=wall_index)
121
+
122
+ # Spawn new zombies if needed
123
+ current_time = pygame.time.get_ticks()
124
+ spawn_interval = max(1, stage.spawn_interval_ms)
125
+ spawn_blocked = stage.survival_stage and game_data.state.dawn_ready
126
+ if (
127
+ len(zombie_group) < MAX_ZOMBIES
128
+ and not spawn_blocked
129
+ and current_time - game_data.state.last_zombie_spawn_time > spawn_interval
130
+ ):
131
+ if spawn_weighted_zombie(game_data, config):
132
+ game_data.state.last_zombie_spawn_time = current_time
133
+
134
+ # Update zombies
135
+ target_center = (
136
+ active_car.rect.center if player.in_car and active_car else player.rect.center
137
+ )
138
+ buddies = [
139
+ survivor
140
+ for survivor in survivor_group
141
+ if survivor.alive() and survivor.is_buddy and not survivor.rescued
142
+ ]
143
+ buddies_on_screen = [
144
+ buddy for buddy in buddies if rect_visible_on_screen(camera, buddy.rect)
145
+ ]
146
+
147
+ survivors_on_screen: list[Survivor] = []
148
+ if stage.rescue_stage:
149
+ for survivor in survivor_group:
150
+ if survivor.alive():
151
+ if rect_visible_on_screen(camera, survivor.rect):
152
+ survivors_on_screen.append(survivor)
153
+
154
+ zombies_sorted: list[Zombie] = sorted(list(zombie_group), key=lambda z: z.x)
155
+
156
+ def _nearby_zombies(index: int) -> list[Zombie]:
157
+ center = zombies_sorted[index]
158
+ neighbors: list[Zombie] = []
159
+ search_radius = ZOMBIE_SEPARATION_DISTANCE + PLAYER_SPEED
160
+ for left in range(index - 1, -1, -1):
161
+ other = zombies_sorted[left]
162
+ if center.x - other.x > search_radius:
163
+ break
164
+ if other.alive():
165
+ neighbors.append(other)
166
+ for right in range(index + 1, len(zombies_sorted)):
167
+ other = zombies_sorted[right]
168
+ if other.x - center.x > search_radius:
169
+ break
170
+ if other.alive():
171
+ neighbors.append(other)
172
+ return neighbors
173
+
174
+ for idx, zombie in enumerate(zombies_sorted):
175
+ target = target_center
176
+ if buddies_on_screen:
177
+ dist_to_target_sq = (target_center[0] - zombie.x) ** 2 + (
178
+ target_center[1] - zombie.y
179
+ ) ** 2
180
+ nearest_buddy = min(
181
+ buddies_on_screen,
182
+ key=lambda buddy: (buddy.rect.centerx - zombie.x) ** 2
183
+ + (buddy.rect.centery - zombie.y) ** 2,
184
+ )
185
+ dist_to_buddy_sq = (nearest_buddy.rect.centerx - zombie.x) ** 2 + (
186
+ nearest_buddy.rect.centery - zombie.y
187
+ ) ** 2
188
+ if dist_to_buddy_sq < dist_to_target_sq:
189
+ target = nearest_buddy.rect.center
190
+
191
+ if stage.rescue_stage:
192
+ zombie_on_screen = rect_visible_on_screen(camera, zombie.rect)
193
+ if zombie_on_screen:
194
+ candidate_positions: list[tuple[int, int]] = []
195
+ for survivor in survivors_on_screen:
196
+ candidate_positions.append(survivor.rect.center)
197
+ for buddy in buddies_on_screen:
198
+ candidate_positions.append(buddy.rect.center)
199
+ candidate_positions.append(player.rect.center)
200
+ if candidate_positions:
201
+ target = min(
202
+ candidate_positions,
203
+ key=lambda pos: (pos[0] - zombie.x) ** 2
204
+ + (pos[1] - zombie.y) ** 2,
205
+ )
206
+ nearby_candidates = _nearby_zombies(idx)
207
+ zombie_search_radius = ZOMBIE_WALL_FOLLOW_SENSOR_DISTANCE + zombie.radius + 120
208
+ nearby_walls = _walls_near((zombie.x, zombie.y), zombie_search_radius)
209
+ zombie.update(
210
+ target,
211
+ nearby_walls,
212
+ nearby_candidates,
213
+ footprints=game_data.state.footprints,
214
+ cell_size=game_data.cell_size,
215
+ grid_cols=stage.grid_cols,
216
+ grid_rows=stage.grid_rows,
217
+ level_width=game_data.level_width,
218
+ level_height=game_data.level_height,
219
+ outer_wall_cells=game_data.layout.outer_wall_cells,
220
+ )