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.
- zombie_escape/__about__.py +1 -1
- zombie_escape/colors.py +7 -21
- zombie_escape/entities.py +100 -191
- zombie_escape/export_images.py +39 -33
- zombie_escape/gameplay/ambient.py +2 -6
- zombie_escape/gameplay/footprints.py +8 -11
- zombie_escape/gameplay/interactions.py +17 -58
- zombie_escape/gameplay/layout.py +20 -46
- zombie_escape/gameplay/movement.py +7 -21
- zombie_escape/gameplay/spawn.py +12 -40
- zombie_escape/gameplay/state.py +1 -0
- zombie_escape/gameplay/survivors.py +5 -16
- zombie_escape/gameplay/utils.py +4 -13
- zombie_escape/input_utils.py +8 -31
- zombie_escape/level_blueprints.py +112 -69
- zombie_escape/level_constants.py +8 -0
- zombie_escape/locales/ui.en.json +12 -0
- zombie_escape/locales/ui.ja.json +12 -0
- zombie_escape/localization.py +3 -11
- zombie_escape/models.py +26 -9
- zombie_escape/render/__init__.py +30 -0
- zombie_escape/render/core.py +992 -0
- zombie_escape/render/hud.py +444 -0
- zombie_escape/render/overview.py +218 -0
- zombie_escape/render/shadows.py +343 -0
- zombie_escape/render_assets.py +11 -33
- zombie_escape/rng.py +4 -8
- zombie_escape/screens/__init__.py +14 -30
- zombie_escape/screens/game_over.py +43 -15
- zombie_escape/screens/gameplay.py +41 -104
- zombie_escape/screens/settings.py +19 -104
- zombie_escape/screens/title.py +36 -176
- zombie_escape/stage_constants.py +192 -67
- zombie_escape/zombie_escape.py +1 -1
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/METADATA +100 -39
- zombie_escape-1.14.4.dist-info/RECORD +53 -0
- zombie_escape/render.py +0 -1746
- zombie_escape-1.13.1.dist-info/RECORD +0 -49
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/WHEEL +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/entry_points.txt +0 -0
- {zombie_escape-1.13.1.dist-info → zombie_escape-1.14.4.dist-info}/licenses/LICENSE.txt +0 -0
zombie_escape/gameplay/spawn.py
CHANGED
|
@@ -300,13 +300,8 @@ def _create_zombie(
|
|
|
300
300
|
level_width = grid_cols * tile_size
|
|
301
301
|
level_height = grid_rows * tile_size
|
|
302
302
|
if hint_pos is not None:
|
|
303
|
-
points = [
|
|
304
|
-
|
|
305
|
-
for _ in range(5)
|
|
306
|
-
]
|
|
307
|
-
points.sort(
|
|
308
|
-
key=lambda p: (p[0] - hint_pos[0]) ** 2 + (p[1] - hint_pos[1]) ** 2
|
|
309
|
-
)
|
|
303
|
+
points = [random_position_outside_building(level_width, level_height) for _ in range(5)]
|
|
304
|
+
points.sort(key=lambda p: (p[0] - hint_pos[0]) ** 2 + (p[1] - hint_pos[1]) ** 2)
|
|
310
305
|
start_pos = points[0]
|
|
311
306
|
else:
|
|
312
307
|
start_pos = random_position_outside_building(level_width, level_height)
|
|
@@ -392,9 +387,7 @@ def _place_flashlight(
|
|
|
392
387
|
continue
|
|
393
388
|
if cars:
|
|
394
389
|
if any(
|
|
395
|
-
(center[0] - parked.rect.centerx) ** 2
|
|
396
|
-
+ (center[1] - parked.rect.centery) ** 2
|
|
397
|
-
< min_car_dist_sq
|
|
390
|
+
(center[0] - parked.rect.centerx) ** 2 + (center[1] - parked.rect.centery) ** 2 < min_car_dist_sq
|
|
398
391
|
for parked in cars
|
|
399
392
|
):
|
|
400
393
|
continue
|
|
@@ -431,9 +424,7 @@ def place_flashlights(
|
|
|
431
424
|
break
|
|
432
425
|
# Avoid clustering too tightly
|
|
433
426
|
if any(
|
|
434
|
-
(other.rect.centerx - fl.rect.centerx) ** 2
|
|
435
|
-
+ (other.rect.centery - fl.rect.centery) ** 2
|
|
436
|
-
< 120 * 120
|
|
427
|
+
(other.rect.centerx - fl.rect.centerx) ** 2 + (other.rect.centery - fl.rect.centery) ** 2 < 120 * 120
|
|
437
428
|
for other in placed
|
|
438
429
|
):
|
|
439
430
|
continue
|
|
@@ -469,9 +460,7 @@ def _place_shoes(
|
|
|
469
460
|
continue
|
|
470
461
|
if cars:
|
|
471
462
|
if any(
|
|
472
|
-
(center[0] - parked.rect.centerx) ** 2
|
|
473
|
-
+ (center[1] - parked.rect.centery) ** 2
|
|
474
|
-
< min_car_dist_sq
|
|
463
|
+
(center[0] - parked.rect.centerx) ** 2 + (center[1] - parked.rect.centery) ** 2 < min_car_dist_sq
|
|
475
464
|
for parked in cars
|
|
476
465
|
):
|
|
477
466
|
continue
|
|
@@ -507,9 +496,7 @@ def place_shoes(
|
|
|
507
496
|
if not shoes:
|
|
508
497
|
break
|
|
509
498
|
if any(
|
|
510
|
-
(other.rect.centerx - shoes.rect.centerx) ** 2
|
|
511
|
-
+ (other.rect.centery - shoes.rect.centery) ** 2
|
|
512
|
-
< 120 * 120
|
|
499
|
+
(other.rect.centerx - shoes.rect.centerx) ** 2 + (other.rect.centery - shoes.rect.centery) ** 2 < 120 * 120
|
|
513
500
|
for other in placed
|
|
514
501
|
):
|
|
515
502
|
continue
|
|
@@ -568,30 +555,20 @@ def place_new_car(
|
|
|
568
555
|
temp_car = Car(c_x, c_y, appearance=appearance)
|
|
569
556
|
temp_rect = temp_car.rect.inflate(30, 30)
|
|
570
557
|
nearby_walls = pygame.sprite.Group()
|
|
571
|
-
nearby_walls.add(
|
|
572
|
-
[
|
|
573
|
-
w
|
|
574
|
-
for w in wall_group
|
|
575
|
-
if abs(w.rect.centerx - c_x) < 150 and abs(w.rect.centery - c_y) < 150
|
|
576
|
-
]
|
|
577
|
-
)
|
|
558
|
+
nearby_walls.add([w for w in wall_group if abs(w.rect.centerx - c_x) < 150 and abs(w.rect.centery - c_y) < 150])
|
|
578
559
|
collides_wall = spritecollideany_walls(temp_car, nearby_walls)
|
|
579
560
|
collides_player = temp_rect.colliderect(player.rect.inflate(50, 50))
|
|
580
561
|
car_overlap = False
|
|
581
562
|
if existing_cars:
|
|
582
563
|
car_overlap = any(
|
|
583
|
-
temp_car.rect.colliderect(other.rect)
|
|
584
|
-
for other in existing_cars
|
|
585
|
-
if other and other.alive()
|
|
564
|
+
temp_car.rect.colliderect(other.rect) for other in existing_cars if other and other.alive()
|
|
586
565
|
)
|
|
587
566
|
if not collides_wall and not collides_player and not car_overlap:
|
|
588
567
|
return temp_car
|
|
589
568
|
return None
|
|
590
569
|
|
|
591
570
|
|
|
592
|
-
def spawn_survivors(
|
|
593
|
-
game_data: GameData, layout_data: Mapping[str, list[tuple[int, int]]]
|
|
594
|
-
) -> list[Survivor]:
|
|
571
|
+
def spawn_survivors(game_data: GameData, layout_data: Mapping[str, list[tuple[int, int]]]) -> list[Survivor]:
|
|
595
572
|
"""Populate rescue-stage survivors and buddy-stage buddies."""
|
|
596
573
|
survivors: list[Survivor] = []
|
|
597
574
|
if not (game_data.stage.rescue_stage or game_data.stage.buddy_required_count > 0):
|
|
@@ -671,9 +648,7 @@ def setup_player_and_cars(
|
|
|
671
648
|
RNG.shuffle(car_candidates)
|
|
672
649
|
for candidate in car_candidates:
|
|
673
650
|
center = _cell_center(candidate, cell_size)
|
|
674
|
-
if (center[0] - player_pos[0]) ** 2 + (
|
|
675
|
-
center[1] - player_pos[1]
|
|
676
|
-
) ** 2 >= 400 * 400:
|
|
651
|
+
if (center[0] - player_pos[0]) ** 2 + (center[1] - player_pos[1]) ** 2 >= 400 * 400:
|
|
677
652
|
car_candidates.remove(candidate)
|
|
678
653
|
return center
|
|
679
654
|
choice = car_candidates.pop()
|
|
@@ -803,9 +778,7 @@ def spawn_waiting_car(game_data: GameData) -> Car | None:
|
|
|
803
778
|
return None
|
|
804
779
|
|
|
805
780
|
|
|
806
|
-
def maintain_waiting_car_supply(
|
|
807
|
-
game_data: GameData, *, minimum: int | None = None
|
|
808
|
-
) -> None:
|
|
781
|
+
def maintain_waiting_car_supply(game_data: GameData, *, minimum: int | None = None) -> None:
|
|
809
782
|
"""Ensure a baseline count of parked cars exists."""
|
|
810
783
|
target = 1 if minimum is None else max(0, minimum)
|
|
811
784
|
current = len(_alive_waiting_cars(game_data))
|
|
@@ -839,8 +812,7 @@ def nearest_waiting_car(game_data: GameData, origin: tuple[float, float]) -> Car
|
|
|
839
812
|
return None
|
|
840
813
|
return min(
|
|
841
814
|
cars,
|
|
842
|
-
key=lambda car: (car.rect.centerx - origin[0]) ** 2
|
|
843
|
-
+ (car.rect.centery - origin[1]) ** 2,
|
|
815
|
+
key=lambda car: (car.rect.centerx - origin[0]) ** 2 + (car.rect.centery - origin[1]) ** 2,
|
|
844
816
|
)
|
|
845
817
|
|
|
846
818
|
|
zombie_escape/gameplay/state.py
CHANGED
|
@@ -28,6 +28,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
28
28
|
overview_created=False,
|
|
29
29
|
footprints=[],
|
|
30
30
|
last_footprint_pos=None,
|
|
31
|
+
footprint_visible_toggle=True,
|
|
31
32
|
elapsed_play_ms=0,
|
|
32
33
|
has_fuel=starts_with_fuel,
|
|
33
34
|
flashlight_count=initial_flashlights,
|
|
@@ -63,9 +63,7 @@ def update_survivors(
|
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
# Gently prevent survivors from overlapping the player or each other
|
|
66
|
-
def _separate_from_point(
|
|
67
|
-
survivor: Survivor, point: tuple[float, float], min_dist: float
|
|
68
|
-
) -> None:
|
|
66
|
+
def _separate_from_point(survivor: Survivor, point: tuple[float, float], min_dist: float) -> None:
|
|
69
67
|
dx = point[0] - survivor.x
|
|
70
68
|
dy = point[1] - survivor.y
|
|
71
69
|
dist = math.hypot(dx, dy)
|
|
@@ -86,9 +84,7 @@ def update_survivors(
|
|
|
86
84
|
for survivor in survivors:
|
|
87
85
|
_separate_from_point(survivor, player_point, player_overlap)
|
|
88
86
|
|
|
89
|
-
survivors_with_x = sorted(
|
|
90
|
-
((survivor.x, survivor) for survivor in survivors), key=lambda item: item[0]
|
|
91
|
-
)
|
|
87
|
+
survivors_with_x = sorted(((survivor.x, survivor) for survivor in survivors), key=lambda item: item[0])
|
|
92
88
|
for i, (base_x, survivor) in enumerate(survivors_with_x):
|
|
93
89
|
for other_base_x, other in survivors_with_x[i + 1 :]:
|
|
94
90
|
if other_base_x - base_x > survivor_overlap:
|
|
@@ -112,10 +108,7 @@ def update_survivors(
|
|
|
112
108
|
other.rect.center = (int(other.x), int(other.y))
|
|
113
109
|
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
def calculate_car_speed_for_passengers(
|
|
117
|
-
passengers: int, *, capacity: int = SURVIVOR_MAX_SAFE_PASSENGERS
|
|
118
|
-
) -> float:
|
|
111
|
+
def calculate_car_speed_for_passengers(passengers: int, *, capacity: int = SURVIVOR_MAX_SAFE_PASSENGERS) -> float:
|
|
119
112
|
cap = max(1, capacity)
|
|
120
113
|
load_ratio = max(0.0, passengers / cap)
|
|
121
114
|
penalty = SURVIVOR_SPEED_PENALTY_PER_PASSENGER * load_ratio
|
|
@@ -196,9 +189,7 @@ def random_survivor_conversion_line(stage_id: str) -> str:
|
|
|
196
189
|
|
|
197
190
|
def cleanup_survivor_messages(state: ProgressState) -> None:
|
|
198
191
|
now = pygame.time.get_ticks()
|
|
199
|
-
state.survivor_messages = [
|
|
200
|
-
msg for msg in state.survivor_messages if msg.get("expires_at", 0) > now
|
|
201
|
-
]
|
|
192
|
+
state.survivor_messages = [msg for msg in state.survivor_messages if msg.get("expires_at", 0) > now]
|
|
202
193
|
|
|
203
194
|
|
|
204
195
|
def drop_survivors_from_car(game_data: GameData, origin: tuple[int, int]) -> None:
|
|
@@ -234,9 +225,7 @@ def drop_survivors_from_car(game_data: GameData, origin: tuple[int, int]) -> Non
|
|
|
234
225
|
apply_passenger_speed_penalty(game_data)
|
|
235
226
|
|
|
236
227
|
|
|
237
|
-
def handle_survivor_zombie_collisions(
|
|
238
|
-
game_data: GameData, config: dict[str, Any]
|
|
239
|
-
) -> None:
|
|
228
|
+
def handle_survivor_zombie_collisions(game_data: GameData, config: dict[str, Any]) -> None:
|
|
240
229
|
if not game_data.stage.rescue_stage:
|
|
241
230
|
return
|
|
242
231
|
survivor_group = game_data.groups.survivor_group
|
zombie_escape/gameplay/utils.py
CHANGED
|
@@ -105,12 +105,8 @@ def find_nearby_offscreen_spawn_position(
|
|
|
105
105
|
SCREEN_HEIGHT,
|
|
106
106
|
)
|
|
107
107
|
view_rect.inflate_ip(SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
108
|
-
min_distance_sq =
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
max_distance_sq = (
|
|
112
|
-
None if max_player_dist is None else max_player_dist * max_player_dist
|
|
113
|
-
)
|
|
108
|
+
min_distance_sq = None if min_player_dist is None else min_player_dist * min_player_dist
|
|
109
|
+
max_distance_sq = None if max_player_dist is None else max_player_dist * max_player_dist
|
|
114
110
|
for _ in range(max(1, attempts)):
|
|
115
111
|
cell_x, cell_y = RNG.choice(walkable_cells)
|
|
116
112
|
jitter_extent = cell_size * 0.35
|
|
@@ -120,9 +116,7 @@ def find_nearby_offscreen_spawn_position(
|
|
|
120
116
|
int((cell_x * cell_size) + (cell_size / 2) + jitter_x),
|
|
121
117
|
int((cell_y * cell_size) + (cell_size / 2) + jitter_y),
|
|
122
118
|
)
|
|
123
|
-
if player is not None and (
|
|
124
|
-
min_distance_sq is not None or max_distance_sq is not None
|
|
125
|
-
):
|
|
119
|
+
if player is not None and (min_distance_sq is not None or max_distance_sq is not None):
|
|
126
120
|
dx = candidate[0] - player.x
|
|
127
121
|
dy = candidate[1] - player.y
|
|
128
122
|
dist_sq = dx * dx + dy * dy
|
|
@@ -174,10 +168,7 @@ def find_exterior_spawn_position(
|
|
|
174
168
|
) -> tuple[int, int]:
|
|
175
169
|
if hint_pos is None:
|
|
176
170
|
return random_position_outside_building(level_width, level_height)
|
|
177
|
-
points = [
|
|
178
|
-
random_position_outside_building(level_width, level_height)
|
|
179
|
-
for _ in range(max(1, attempts))
|
|
180
|
-
]
|
|
171
|
+
points = [random_position_outside_building(level_width, level_height) for _ in range(max(1, attempts))]
|
|
181
172
|
return min(
|
|
182
173
|
points,
|
|
183
174
|
key=lambda pos: (pos[0] - hint_pos[0]) ** 2 + (pos[1] - hint_pos[1]) ** 2,
|
zombie_escape/input_utils.py
CHANGED
|
@@ -24,9 +24,7 @@ CONTROLLER_BUTTON_DPAD_RIGHT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_RIGHT", N
|
|
|
24
24
|
CONTROLLER_BUTTON_RB = getattr(pygame, "CONTROLLER_BUTTON_RIGHTSHOULDER", None)
|
|
25
25
|
CONTROLLER_AXIS_LEFTX = getattr(pygame, "CONTROLLER_AXIS_LEFTX", None)
|
|
26
26
|
CONTROLLER_AXIS_LEFTY = getattr(pygame, "CONTROLLER_AXIS_LEFTY", None)
|
|
27
|
-
CONTROLLER_AXIS_TRIGGERRIGHT = getattr(
|
|
28
|
-
pygame, "CONTROLLER_AXIS_TRIGGERRIGHT", None
|
|
29
|
-
)
|
|
27
|
+
CONTROLLER_AXIS_TRIGGERRIGHT = getattr(pygame, "CONTROLLER_AXIS_TRIGGERRIGHT", None)
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
def init_first_controller() -> pygame.controller.Controller | None:
|
|
@@ -65,10 +63,7 @@ def is_confirm_event(event: pygame.event.Event) -> bool:
|
|
|
65
63
|
|
|
66
64
|
def is_start_event(event: pygame.event.Event) -> bool:
|
|
67
65
|
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
68
|
-
return
|
|
69
|
-
CONTROLLER_BUTTON_START is not None
|
|
70
|
-
and event.button == CONTROLLER_BUTTON_START
|
|
71
|
-
)
|
|
66
|
+
return CONTROLLER_BUTTON_START is not None and event.button == CONTROLLER_BUTTON_START
|
|
72
67
|
if event.type == pygame.JOYBUTTONDOWN:
|
|
73
68
|
return event.button == JOY_BUTTON_START
|
|
74
69
|
return False
|
|
@@ -76,10 +71,7 @@ def is_start_event(event: pygame.event.Event) -> bool:
|
|
|
76
71
|
|
|
77
72
|
def is_select_event(event: pygame.event.Event) -> bool:
|
|
78
73
|
if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
|
|
79
|
-
return
|
|
80
|
-
CONTROLLER_BUTTON_BACK is not None
|
|
81
|
-
and event.button == CONTROLLER_BUTTON_BACK
|
|
82
|
-
)
|
|
74
|
+
return CONTROLLER_BUTTON_BACK is not None and event.button == CONTROLLER_BUTTON_BACK
|
|
83
75
|
if event.type == pygame.JOYBUTTONDOWN:
|
|
84
76
|
return event.button == JOY_BUTTON_BACK
|
|
85
77
|
return False
|
|
@@ -102,25 +94,13 @@ def read_gamepad_move(
|
|
|
102
94
|
x = 0.0
|
|
103
95
|
if abs(y) < deadzone:
|
|
104
96
|
y = 0.0
|
|
105
|
-
if (
|
|
106
|
-
CONTROLLER_BUTTON_DPAD_LEFT is not None
|
|
107
|
-
and controller.get_button(CONTROLLER_BUTTON_DPAD_LEFT)
|
|
108
|
-
):
|
|
97
|
+
if CONTROLLER_BUTTON_DPAD_LEFT is not None and controller.get_button(CONTROLLER_BUTTON_DPAD_LEFT):
|
|
109
98
|
x = -1.0
|
|
110
|
-
elif (
|
|
111
|
-
CONTROLLER_BUTTON_DPAD_RIGHT is not None
|
|
112
|
-
and controller.get_button(CONTROLLER_BUTTON_DPAD_RIGHT)
|
|
113
|
-
):
|
|
99
|
+
elif CONTROLLER_BUTTON_DPAD_RIGHT is not None and controller.get_button(CONTROLLER_BUTTON_DPAD_RIGHT):
|
|
114
100
|
x = 1.0
|
|
115
|
-
if (
|
|
116
|
-
CONTROLLER_BUTTON_DPAD_UP is not None
|
|
117
|
-
and controller.get_button(CONTROLLER_BUTTON_DPAD_UP)
|
|
118
|
-
):
|
|
101
|
+
if CONTROLLER_BUTTON_DPAD_UP is not None and controller.get_button(CONTROLLER_BUTTON_DPAD_UP):
|
|
119
102
|
y = -1.0
|
|
120
|
-
elif (
|
|
121
|
-
CONTROLLER_BUTTON_DPAD_DOWN is not None
|
|
122
|
-
and controller.get_button(CONTROLLER_BUTTON_DPAD_DOWN)
|
|
123
|
-
):
|
|
103
|
+
elif CONTROLLER_BUTTON_DPAD_DOWN is not None and controller.get_button(CONTROLLER_BUTTON_DPAD_DOWN):
|
|
124
104
|
y = 1.0
|
|
125
105
|
return x, y
|
|
126
106
|
|
|
@@ -149,10 +129,7 @@ def is_accel_active(
|
|
|
149
129
|
if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
|
|
150
130
|
return True
|
|
151
131
|
if controller and controller.get_init():
|
|
152
|
-
if (
|
|
153
|
-
CONTROLLER_BUTTON_RB is not None
|
|
154
|
-
and controller.get_button(CONTROLLER_BUTTON_RB)
|
|
155
|
-
):
|
|
132
|
+
if CONTROLLER_BUTTON_RB is not None and controller.get_button(CONTROLLER_BUTTON_RB):
|
|
156
133
|
return True
|
|
157
134
|
if CONTROLLER_AXIS_TRIGGERRIGHT is not None:
|
|
158
135
|
if controller.get_axis(CONTROLLER_AXIS_TRIGGERRIGHT) > DEADZONE:
|
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
from collections import deque
|
|
4
4
|
|
|
5
|
+
from .level_constants import (
|
|
6
|
+
DEFAULT_GRID_WIRE_WALL_LINES,
|
|
7
|
+
DEFAULT_SPARSE_WALL_DENSITY,
|
|
8
|
+
DEFAULT_STEEL_BEAM_CHANCE,
|
|
9
|
+
DEFAULT_WALL_LINES,
|
|
10
|
+
)
|
|
5
11
|
from .rng import get_rng, seed_rng
|
|
6
12
|
|
|
7
13
|
EXITS_PER_SIDE = 1 # currently fixed to 1 per side (can be tuned)
|
|
8
|
-
NUM_WALL_LINES = 80 # reduced density (roughly 1/5 of previous 450)
|
|
9
14
|
WALL_MIN_LEN = 3
|
|
10
15
|
WALL_MAX_LEN = 10
|
|
11
|
-
SPARSE_WALL_DENSITY = 0.10
|
|
12
16
|
SPAWN_MARGIN = 3 # keep spawns away from walls/edges
|
|
13
17
|
SPAWN_ZOMBIES = 3
|
|
14
18
|
|
|
15
19
|
RNG = get_rng()
|
|
16
|
-
STEEL_BEAM_CHANCE = 0.02
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
class MapGenerationError(Exception):
|
|
@@ -168,6 +171,7 @@ def _place_exits(grid: list[list[str]], exits_per_side: int) -> None:
|
|
|
168
171
|
def _place_walls_default(
|
|
169
172
|
grid: list[list[str]],
|
|
170
173
|
*,
|
|
174
|
+
line_count: int = DEFAULT_WALL_LINES,
|
|
171
175
|
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
172
176
|
) -> None:
|
|
173
177
|
cols, rows = len(grid[0]), len(grid)
|
|
@@ -177,7 +181,7 @@ def _place_walls_default(
|
|
|
177
181
|
if forbidden_cells:
|
|
178
182
|
forbidden |= forbidden_cells
|
|
179
183
|
|
|
180
|
-
for _ in range(
|
|
184
|
+
for _ in range(line_count):
|
|
181
185
|
length = rng(WALL_MIN_LEN, WALL_MAX_LEN)
|
|
182
186
|
horizontal = RNG.choice([True, False])
|
|
183
187
|
if horizontal:
|
|
@@ -210,6 +214,7 @@ def _place_walls_empty(
|
|
|
210
214
|
def _place_walls_grid_wire(
|
|
211
215
|
grid: list[list[str]],
|
|
212
216
|
*,
|
|
217
|
+
line_count: int = DEFAULT_GRID_WIRE_WALL_LINES,
|
|
213
218
|
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
214
219
|
) -> None:
|
|
215
220
|
"""
|
|
@@ -230,8 +235,7 @@ def _place_walls_grid_wire(
|
|
|
230
235
|
grid_v = [["." for _ in range(cols)] for _ in range(rows)]
|
|
231
236
|
grid_h = [["." for _ in range(cols)] for _ in range(rows)]
|
|
232
237
|
|
|
233
|
-
|
|
234
|
-
lines_per_pass = int(NUM_WALL_LINES * 0.7)
|
|
238
|
+
lines_per_pass = line_count
|
|
235
239
|
|
|
236
240
|
# --- Pass 1: Vertical Walls (on grid_v) ---
|
|
237
241
|
for _ in range(lines_per_pass):
|
|
@@ -239,24 +243,28 @@ def _place_walls_grid_wire(
|
|
|
239
243
|
x = rng(2, cols - 3)
|
|
240
244
|
y = rng(2, rows - 2 - length)
|
|
241
245
|
|
|
246
|
+
# Reject if the new segment would connect end-to-end with another vertical segment.
|
|
242
247
|
can_place = True
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
248
|
+
if grid_v[y - 1][x] == "1" or grid_v[y + length][x] == "1":
|
|
249
|
+
can_place = False
|
|
250
|
+
else:
|
|
251
|
+
for i in range(length):
|
|
252
|
+
cy = y + i
|
|
253
|
+
# 1. Global forbidden check (exits, outer walls in main grid)
|
|
254
|
+
if (x, cy) in forbidden:
|
|
255
|
+
can_place = False
|
|
256
|
+
break
|
|
257
|
+
if grid[cy][x] not in (".",):
|
|
258
|
+
can_place = False
|
|
259
|
+
break
|
|
260
|
+
# 2. Local self-overlap check
|
|
261
|
+
if grid_v[cy][x] != ".":
|
|
262
|
+
can_place = False
|
|
263
|
+
break
|
|
264
|
+
# 3. Parallel adjacency check (only against other vertical walls)
|
|
265
|
+
if grid_v[cy][x - 1] == "1" or grid_v[cy][x + 1] == "1":
|
|
266
|
+
can_place = False
|
|
267
|
+
break
|
|
260
268
|
|
|
261
269
|
if can_place:
|
|
262
270
|
for i in range(length):
|
|
@@ -268,24 +276,28 @@ def _place_walls_grid_wire(
|
|
|
268
276
|
x = rng(2, cols - 2 - length)
|
|
269
277
|
y = rng(2, rows - 3)
|
|
270
278
|
|
|
279
|
+
# Reject if the new segment would connect end-to-end with another horizontal segment.
|
|
271
280
|
can_place = True
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
281
|
+
if grid_h[y][x - 1] == "1" or grid_h[y][x + length] == "1":
|
|
282
|
+
can_place = False
|
|
283
|
+
else:
|
|
284
|
+
for i in range(length):
|
|
285
|
+
cx = x + i
|
|
286
|
+
# 1. Global forbidden check
|
|
287
|
+
if (cx, y) in forbidden:
|
|
288
|
+
can_place = False
|
|
289
|
+
break
|
|
290
|
+
if grid[y][cx] not in (".",):
|
|
291
|
+
can_place = False
|
|
292
|
+
break
|
|
293
|
+
# 2. Local self-overlap check
|
|
294
|
+
if grid_h[y][cx] != ".":
|
|
295
|
+
can_place = False
|
|
296
|
+
break
|
|
297
|
+
# 3. Parallel adjacency check (only against other horizontal walls)
|
|
298
|
+
if grid_h[y - 1][cx] == "1" or grid_h[y + 1][cx] == "1":
|
|
299
|
+
can_place = False
|
|
300
|
+
break
|
|
289
301
|
|
|
290
302
|
if can_place:
|
|
291
303
|
for i in range(length):
|
|
@@ -302,7 +314,7 @@ def _place_walls_grid_wire(
|
|
|
302
314
|
def _place_walls_sparse_moore(
|
|
303
315
|
grid: list[list[str]],
|
|
304
316
|
*,
|
|
305
|
-
density: float =
|
|
317
|
+
density: float = DEFAULT_SPARSE_WALL_DENSITY,
|
|
306
318
|
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
307
319
|
) -> None:
|
|
308
320
|
"""Place isolated wall tiles at a low density, avoiding adjacency."""
|
|
@@ -335,7 +347,7 @@ def _place_walls_sparse_moore(
|
|
|
335
347
|
def _place_walls_sparse_ortho(
|
|
336
348
|
grid: list[list[str]],
|
|
337
349
|
*,
|
|
338
|
-
density: float =
|
|
350
|
+
density: float = DEFAULT_SPARSE_WALL_DENSITY,
|
|
339
351
|
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
340
352
|
) -> None:
|
|
341
353
|
"""Place isolated wall tiles at a low density, avoiding orthogonal adjacency."""
|
|
@@ -351,15 +363,11 @@ def _place_walls_sparse_ortho(
|
|
|
351
363
|
continue
|
|
352
364
|
if RNG.random() >= density:
|
|
353
365
|
continue
|
|
354
|
-
if
|
|
355
|
-
grid[y - 1][x] == "1"
|
|
356
|
-
or grid[y + 1][x] == "1"
|
|
357
|
-
or grid[y][x - 1] == "1"
|
|
358
|
-
or grid[y][x + 1] == "1"
|
|
359
|
-
):
|
|
366
|
+
if grid[y - 1][x] == "1" or grid[y + 1][x] == "1" or grid[y][x - 1] == "1" or grid[y][x + 1] == "1":
|
|
360
367
|
continue
|
|
361
368
|
grid[y][x] = "1"
|
|
362
369
|
|
|
370
|
+
|
|
363
371
|
WALL_ALGORITHMS = {
|
|
364
372
|
"default": _place_walls_default,
|
|
365
373
|
"empty": _place_walls_empty,
|
|
@@ -372,7 +380,7 @@ WALL_ALGORITHMS = {
|
|
|
372
380
|
def _place_steel_beams(
|
|
373
381
|
grid: list[list[str]],
|
|
374
382
|
*,
|
|
375
|
-
chance: float =
|
|
383
|
+
chance: float = DEFAULT_STEEL_BEAM_CHANCE,
|
|
376
384
|
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
377
385
|
) -> set[tuple[int, int]]:
|
|
378
386
|
"""Pick individual cells for steel beams, avoiding exits and their neighbors."""
|
|
@@ -396,16 +404,32 @@ def _place_pitfalls(
|
|
|
396
404
|
grid: list[list[str]],
|
|
397
405
|
*,
|
|
398
406
|
density: float,
|
|
407
|
+
pitfall_zones: list[tuple[int, int, int, int]] | None = None,
|
|
399
408
|
forbidden_cells: set[tuple[int, int]] | None = None,
|
|
400
409
|
) -> None:
|
|
401
410
|
"""Replace empty floor tiles with pitfalls based on density."""
|
|
402
|
-
if density <= 0.0:
|
|
403
|
-
return
|
|
404
411
|
cols, rows = len(grid[0]), len(grid)
|
|
405
412
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
406
413
|
if forbidden_cells:
|
|
407
414
|
forbidden |= forbidden_cells
|
|
408
415
|
|
|
416
|
+
if pitfall_zones:
|
|
417
|
+
for col, row, width, height in pitfall_zones:
|
|
418
|
+
if width <= 0 or height <= 0:
|
|
419
|
+
continue
|
|
420
|
+
start_x = max(0, col)
|
|
421
|
+
start_y = max(0, row)
|
|
422
|
+
end_x = min(cols, col + width)
|
|
423
|
+
end_y = min(rows, row + height)
|
|
424
|
+
for y in range(start_y, end_y):
|
|
425
|
+
for x in range(start_x, end_x):
|
|
426
|
+
if (x, y) in forbidden:
|
|
427
|
+
continue
|
|
428
|
+
if grid[y][x] == ".":
|
|
429
|
+
grid[y][x] = "x"
|
|
430
|
+
|
|
431
|
+
if density <= 0.0:
|
|
432
|
+
return
|
|
409
433
|
for y in range(1, rows - 1):
|
|
410
434
|
for x in range(1, cols - 1):
|
|
411
435
|
if (x, y) in forbidden:
|
|
@@ -443,6 +467,7 @@ def _generate_random_blueprint(
|
|
|
443
467
|
rows: int,
|
|
444
468
|
wall_algo: str = "default",
|
|
445
469
|
pitfall_density: float = 0.0,
|
|
470
|
+
pitfall_zones: list[tuple[int, int, int, int]] | None = None,
|
|
446
471
|
) -> dict:
|
|
447
472
|
grid = _init_grid(cols, rows)
|
|
448
473
|
_place_exits(grid, EXITS_PER_SIDE)
|
|
@@ -476,18 +501,14 @@ def _generate_random_blueprint(
|
|
|
476
501
|
reserved_cells.add((sx, sy))
|
|
477
502
|
|
|
478
503
|
# Select and run the wall placement algorithm (after reserving spawns)
|
|
479
|
-
sparse_density =
|
|
504
|
+
sparse_density = DEFAULT_SPARSE_WALL_DENSITY
|
|
505
|
+
wall_line_count = DEFAULT_WALL_LINES
|
|
480
506
|
original_wall_algo = wall_algo
|
|
481
507
|
if wall_algo == "sparse":
|
|
482
|
-
print(
|
|
483
|
-
"WARNING: 'sparse' is deprecated. Use 'sparse_moore' instead."
|
|
484
|
-
)
|
|
508
|
+
print("WARNING: 'sparse' is deprecated. Use 'sparse_moore' instead.")
|
|
485
509
|
wall_algo = "sparse_moore"
|
|
486
510
|
elif wall_algo.startswith("sparse."):
|
|
487
|
-
print(
|
|
488
|
-
"WARNING: 'sparse.<int>%' is deprecated. Use "
|
|
489
|
-
"'sparse_moore.<int>%' instead."
|
|
490
|
-
)
|
|
511
|
+
print("WARNING: 'sparse.<int>%' is deprecated. Use 'sparse_moore.<int>%' instead.")
|
|
491
512
|
suffix = wall_algo[len("sparse.") :]
|
|
492
513
|
wall_algo = "sparse_moore"
|
|
493
514
|
if suffix.endswith("%") and suffix[:-1].isdigit():
|
|
@@ -505,16 +526,37 @@ def _generate_random_blueprint(
|
|
|
505
526
|
"'sparse_moore.<int>%' or 'sparse_ortho.<int>%'. "
|
|
506
527
|
f"Got '{original_wall_algo}'. Falling back to default sparse density."
|
|
507
528
|
)
|
|
529
|
+
if wall_algo.startswith("default.") or wall_algo.startswith("grid_wire."):
|
|
530
|
+
base, suffix = wall_algo.split(".", 1)
|
|
531
|
+
base_line_count = DEFAULT_WALL_LINES if base == "default" else DEFAULT_GRID_WIRE_WALL_LINES
|
|
532
|
+
if suffix.endswith("%") and suffix[:-1].isdigit():
|
|
533
|
+
percent = int(suffix[:-1])
|
|
534
|
+
if 0 <= percent <= 200:
|
|
535
|
+
wall_line_count = int(base_line_count * (percent / 100.0))
|
|
536
|
+
wall_algo = base
|
|
537
|
+
else:
|
|
538
|
+
print(
|
|
539
|
+
"WARNING: Wall line density must be 0-200%. "
|
|
540
|
+
f"Got '{suffix}'. Falling back to default line count."
|
|
541
|
+
)
|
|
542
|
+
wall_algo = base
|
|
543
|
+
else:
|
|
544
|
+
print(
|
|
545
|
+
"WARNING: Invalid wall line format. Use "
|
|
546
|
+
"'default.<int>%' or 'grid_wire.<int>%'. "
|
|
547
|
+
f"Got '{wall_algo}'. Falling back to default line count."
|
|
548
|
+
)
|
|
549
|
+
wall_algo = base
|
|
508
550
|
if wall_algo.startswith("sparse_moore.") or wall_algo.startswith("sparse_ortho."):
|
|
509
551
|
base, suffix = wall_algo.split(".", 1)
|
|
510
552
|
if suffix.endswith("%") and suffix[:-1].isdigit():
|
|
511
553
|
percent = int(suffix[:-1])
|
|
512
|
-
if 0 <= percent <=
|
|
554
|
+
if 0 <= percent <= 200:
|
|
513
555
|
sparse_density = percent / 100.0
|
|
514
556
|
wall_algo = base
|
|
515
557
|
else:
|
|
516
558
|
print(
|
|
517
|
-
"WARNING: Sparse wall density must be 0-
|
|
559
|
+
"WARNING: Sparse wall density must be 0-200%. "
|
|
518
560
|
f"Got '{suffix}'. Falling back to default sparse density."
|
|
519
561
|
)
|
|
520
562
|
wall_algo = base
|
|
@@ -527,27 +569,26 @@ def _generate_random_blueprint(
|
|
|
527
569
|
wall_algo = base
|
|
528
570
|
|
|
529
571
|
if wall_algo not in WALL_ALGORITHMS:
|
|
530
|
-
print(
|
|
531
|
-
f"WARNING: Unknown wall algorithm '{wall_algo}'. Falling back to 'default'."
|
|
532
|
-
)
|
|
572
|
+
print(f"WARNING: Unknown wall algorithm '{wall_algo}'. Falling back to 'default'.")
|
|
533
573
|
wall_algo = "default"
|
|
534
574
|
|
|
535
575
|
# Place pitfalls BEFORE walls so walls avoid them (consistent with spawn reservation)
|
|
536
576
|
_place_pitfalls(
|
|
537
577
|
grid,
|
|
538
578
|
density=pitfall_density,
|
|
579
|
+
pitfall_zones=pitfall_zones,
|
|
539
580
|
forbidden_cells=reserved_cells,
|
|
540
581
|
)
|
|
541
582
|
|
|
542
583
|
algo_func = WALL_ALGORITHMS[wall_algo]
|
|
543
584
|
if wall_algo in {"sparse_moore", "sparse_ortho"}:
|
|
544
585
|
algo_func(grid, density=sparse_density, forbidden_cells=reserved_cells)
|
|
586
|
+
elif wall_algo in {"default", "grid_wire"}:
|
|
587
|
+
algo_func(grid, line_count=wall_line_count, forbidden_cells=reserved_cells)
|
|
545
588
|
else:
|
|
546
589
|
algo_func(grid, forbidden_cells=reserved_cells)
|
|
547
590
|
|
|
548
|
-
steel_beams = _place_steel_beams(
|
|
549
|
-
grid, chance=steel_chance, forbidden_cells=reserved_cells
|
|
550
|
-
)
|
|
591
|
+
steel_beams = _place_steel_beams(grid, chance=steel_chance, forbidden_cells=reserved_cells)
|
|
551
592
|
|
|
552
593
|
blueprint_rows = ["".join(row) for row in grid]
|
|
553
594
|
return {"grid": blueprint_rows, "steel_cells": steel_beams}
|
|
@@ -560,14 +601,15 @@ def choose_blueprint(
|
|
|
560
601
|
rows: int,
|
|
561
602
|
wall_algo: str = "default",
|
|
562
603
|
pitfall_density: float = 0.0,
|
|
604
|
+
pitfall_zones: list[tuple[int, int, int, int]] | None = None,
|
|
563
605
|
base_seed: int | None = None,
|
|
564
606
|
) -> dict:
|
|
565
607
|
# Currently only random generation; hook for future variants.
|
|
566
608
|
steel_conf = config.get("steel_beams", {})
|
|
567
609
|
try:
|
|
568
|
-
steel_chance = float(steel_conf.get("chance",
|
|
610
|
+
steel_chance = float(steel_conf.get("chance", DEFAULT_STEEL_BEAM_CHANCE))
|
|
569
611
|
except (TypeError, ValueError):
|
|
570
|
-
steel_chance =
|
|
612
|
+
steel_chance = DEFAULT_STEEL_BEAM_CHANCE
|
|
571
613
|
|
|
572
614
|
for attempt in range(20):
|
|
573
615
|
if base_seed is not None:
|
|
@@ -579,6 +621,7 @@ def choose_blueprint(
|
|
|
579
621
|
rows=rows,
|
|
580
622
|
wall_algo=wall_algo,
|
|
581
623
|
pitfall_density=pitfall_density,
|
|
624
|
+
pitfall_zones=pitfall_zones,
|
|
582
625
|
)
|
|
583
626
|
|
|
584
627
|
car_reachable = validate_connectivity(blueprint["grid"])
|
zombie_escape/level_constants.py
CHANGED
|
@@ -5,9 +5,17 @@ from __future__ import annotations
|
|
|
5
5
|
DEFAULT_GRID_COLS = 48
|
|
6
6
|
DEFAULT_GRID_ROWS = 30
|
|
7
7
|
DEFAULT_TILE_SIZE = 50 # world units per cell; adjust to scale the whole map
|
|
8
|
+
DEFAULT_WALL_LINES = 80 # reduced density (roughly 1/5 of previous 450)
|
|
9
|
+
DEFAULT_GRID_WIRE_WALL_LINES = int(DEFAULT_WALL_LINES * 0.7)
|
|
10
|
+
DEFAULT_SPARSE_WALL_DENSITY = 0.10
|
|
11
|
+
DEFAULT_STEEL_BEAM_CHANCE = 0.02
|
|
8
12
|
|
|
9
13
|
__all__ = [
|
|
10
14
|
"DEFAULT_GRID_COLS",
|
|
11
15
|
"DEFAULT_GRID_ROWS",
|
|
12
16
|
"DEFAULT_TILE_SIZE",
|
|
17
|
+
"DEFAULT_WALL_LINES",
|
|
18
|
+
"DEFAULT_GRID_WIRE_WALL_LINES",
|
|
19
|
+
"DEFAULT_SPARSE_WALL_DENSITY",
|
|
20
|
+
"DEFAULT_STEEL_BEAM_CHANCE",
|
|
13
21
|
]
|