zombie-escape 1.8.0__py3-none-any.whl → 1.10.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.
@@ -11,6 +11,9 @@ HUMANOID_OUTLINE_COLOR = (0, 80, 200)
11
11
  HUMANOID_OUTLINE_WIDTH = 1
12
12
  BUDDY_COLOR = (0, 180, 63)
13
13
  SURVIVOR_COLOR = (198, 198, 198)
14
+ FALLING_ZOMBIE_COLOR = (45, 45, 45)
15
+ FALLING_WHIRLWIND_COLOR = (200, 200, 200, 120)
16
+ FALLING_DUST_COLOR = (70, 70, 70, 130)
14
17
 
15
18
 
16
19
  @dataclass(frozen=True)
@@ -35,11 +38,14 @@ class RenderAssets:
35
38
  footprint_min_fade: float
36
39
  internal_wall_grid_snap: int
37
40
  flashlight_bonus_step: float
41
+ flashlight_hatch_extra_scale: float
42
+
38
43
 
39
44
  FOG_RADIUS_SCALE = 1.2
40
45
  FOG_HATCH_PIXEL_SCALE = 2
41
46
 
42
47
  FLASHLIGHT_FOG_SCALE_STEP = 0.3
48
+ FLASHLIGHT_HATCH_EXTRA_SCALE = 0.12
43
49
 
44
50
  FOOTPRINT_RADIUS = 2
45
51
  FOOTPRINT_OVERVIEW_RADIUS = 3
@@ -55,6 +61,7 @@ FOG_RINGS = [
55
61
  FogRing(radius_factor=0.968, thickness=12),
56
62
  ]
57
63
 
64
+
58
65
  def build_render_assets(cell_size: int) -> RenderAssets:
59
66
  return RenderAssets(
60
67
  screen_width=SCREEN_WIDTH,
@@ -71,11 +78,15 @@ def build_render_assets(cell_size: int) -> RenderAssets:
71
78
  footprint_min_fade=FOOTPRINT_MIN_FADE,
72
79
  internal_wall_grid_snap=cell_size,
73
80
  flashlight_bonus_step=FLASHLIGHT_FOG_SCALE_STEP,
81
+ flashlight_hatch_extra_scale=FLASHLIGHT_HATCH_EXTRA_SCALE,
74
82
  )
75
83
 
76
84
 
77
85
  __all__ = [
78
86
  "BUDDY_COLOR",
87
+ "FALLING_ZOMBIE_COLOR",
88
+ "FALLING_WHIRLWIND_COLOR",
89
+ "FALLING_DUST_COLOR",
79
90
  "HUMANOID_OUTLINE_COLOR",
80
91
  "HUMANOID_OUTLINE_WIDTH",
81
92
  "SURVIVOR_COLOR",
@@ -83,5 +94,6 @@ __all__ = [
83
94
  "RenderAssets",
84
95
  "FOG_RADIUS_SCALE",
85
96
  "FLASHLIGHT_FOG_SCALE_STEP",
97
+ "FLASHLIGHT_HATCH_EXTRA_SCALE",
86
98
  "build_render_assets",
87
99
  ]
@@ -159,6 +159,7 @@ def toggle_fullscreen(
159
159
  window_width, window_height = _fetch_window_size(window)
160
160
  _update_window_caption(window_width, window_height)
161
161
  _update_window_size((window_width, window_height), source="toggle_fullscreen")
162
+ pygame.mouse.set_visible(not current_maximized)
162
163
  if game_data is not None:
163
164
  game_data.state.overview_created = False
164
165
  return window
@@ -130,16 +130,16 @@ def game_over_screen(
130
130
  LIGHT_GRAY,
131
131
  (screen_width // 2, summary_y),
132
132
  )
133
- elif stage and stage.survival_stage:
134
- elapsed_ms = max(0, state.survival_elapsed_ms)
135
- goal_ms = max(0, state.survival_goal_ms)
133
+ elif stage and stage.endurance_stage:
134
+ elapsed_ms = max(0, state.endurance_elapsed_ms)
135
+ goal_ms = max(0, state.endurance_goal_ms)
136
136
  if goal_ms:
137
137
  elapsed_ms = min(elapsed_ms, goal_ms)
138
138
  display_ms = int(elapsed_ms * SURVIVAL_FAKE_CLOCK_RATIO)
139
139
  hours = display_ms // 3_600_000
140
140
  minutes = (display_ms % 3_600_000) // 60_000
141
141
  time_label = f"{int(hours):02d}:{int(minutes):02d}"
142
- msg = tr("game_over.survival_duration", time=time_label)
142
+ msg = tr("game_over.endurance_duration", time=time_label)
143
143
  show_message(
144
144
  screen,
145
145
  msg,
@@ -8,7 +8,6 @@ from pygame import surface, time
8
8
  from ..colors import LIGHT_GRAY, RED, WHITE, YELLOW
9
9
  from ..gameplay_constants import (
10
10
  CAR_HINT_DELAY_MS_DEFAULT,
11
- DEFAULT_FLASHLIGHT_SPAWN_COUNT,
12
11
  SURVIVAL_TIME_ACCEL_SUBSTEPS,
13
12
  SURVIVAL_TIME_ACCEL_MAX_SUBSTEP,
14
13
  )
@@ -29,7 +28,7 @@ from ..gameplay import (
29
28
  sync_ambient_palette_with_flashlights,
30
29
  update_entities,
31
30
  update_footprints,
32
- update_survival_timer,
31
+ update_endurance_timer,
33
32
  )
34
33
  from ..input_utils import (
35
34
  CONTROLLER_BUTTON_DOWN,
@@ -43,7 +42,7 @@ from ..input_utils import (
43
42
  read_gamepad_move,
44
43
  )
45
44
  from ..gameplay.spawn import _alive_waiting_cars
46
- from ..entities import build_wall_index
45
+ from ..world_grid import build_wall_index
47
46
  from ..localization import translate as tr
48
47
  from ..models import Stage
49
48
  from ..render import draw, prewarm_fog_overlays, show_message
@@ -83,11 +82,11 @@ def gameplay_screen(
83
82
  game_data = initialize_game_state(config, stage)
84
83
  game_data.state.seed = applied_seed
85
84
  game_data.state.debug_mode = debug_mode
86
- if debug_mode and stage.survival_stage:
87
- goal_ms = max(0, stage.survival_goal_ms)
85
+ if debug_mode and stage.endurance_stage:
86
+ goal_ms = max(0, stage.endurance_goal_ms)
88
87
  if goal_ms > 0:
89
88
  remaining = 3 * 60 * 1000 # 3 minutes in ms
90
- game_data.state.survival_elapsed_ms = max(0, goal_ms - remaining)
89
+ game_data.state.endurance_elapsed_ms = max(0, goal_ms - remaining)
91
90
  game_data.state.dawn_ready = False
92
91
  game_data.state.dawn_prompt_at = None
93
92
  game_data.state.dawn_carbonized = False
@@ -99,7 +98,6 @@ def gameplay_screen(
99
98
  paused_manual = False
100
99
  paused_focus = False
101
100
  ignore_focus_loss_until = 0
102
- last_fov_target = None
103
101
  controller = init_first_controller()
104
102
  joystick = init_first_joystick() if controller is None else None
105
103
 
@@ -131,19 +129,18 @@ def gameplay_screen(
131
129
  if fuel_can:
132
130
  game_data.fuel = fuel_can
133
131
  game_data.groups.all_sprites.add(fuel_can, layer=1)
132
+ flashlight_count = stage.initial_flashlight_count
134
133
  flashlights = place_flashlights(
135
134
  layout_data["walkable_cells"],
136
135
  player,
137
136
  cars=game_data.waiting_cars,
138
- count=max(1, DEFAULT_FLASHLIGHT_SPAWN_COUNT),
137
+ count=max(0, flashlight_count),
139
138
  )
140
139
  game_data.flashlights = flashlights
141
140
  game_data.groups.all_sprites.add(flashlights, layer=1)
142
141
 
143
142
  spawn_initial_zombies(game_data, player, layout_data, config)
144
143
  update_footprints(game_data, config)
145
- last_fov_target = player
146
-
147
144
  while True:
148
145
  dt = clock.tick(fps) / 1000.0
149
146
  if game_data.state.game_over or game_data.state.game_won:
@@ -157,7 +154,6 @@ def gameplay_screen(
157
154
  render_assets,
158
155
  screen,
159
156
  game_data,
160
- last_fov_target,
161
157
  config=config,
162
158
  hint_color=None,
163
159
  present_fn=present,
@@ -261,7 +257,6 @@ def gameplay_screen(
261
257
  render_assets,
262
258
  screen,
263
259
  game_data,
264
- last_fov_target,
265
260
  config=config,
266
261
  do_flip=not show_pause_overlay,
267
262
  present_fn=present,
@@ -325,7 +320,6 @@ def gameplay_screen(
325
320
  wall_index = build_wall_index(
326
321
  game_data.groups.wall_group, cell_size=game_data.cell_size
327
322
  )
328
- frame_fov_target = None
329
323
  for _ in range(substeps):
330
324
  player_ref = game_data.player
331
325
  if player_ref is None:
@@ -349,19 +343,12 @@ def gameplay_screen(
349
343
  if accel_active:
350
344
  step_ms = max(1, step_ms)
351
345
  game_data.state.elapsed_play_ms += step_ms
352
- update_survival_timer(game_data, step_ms)
346
+ update_endurance_timer(game_data, step_ms)
353
347
  cleanup_survivor_messages(game_data.state)
354
- sub_fov_target = check_interactions(game_data, config)
355
- if sub_fov_target:
356
- frame_fov_target = sub_fov_target
348
+ check_interactions(game_data, config)
357
349
  if game_data.state.game_over or game_data.state.game_won:
358
350
  break
359
351
 
360
- if frame_fov_target:
361
- last_fov_target = frame_fov_target
362
- else:
363
- frame_fov_target = last_fov_target
364
-
365
352
  player = game_data.player
366
353
  if player is None:
367
354
  raise ValueError("Player missing from game data")
@@ -370,7 +357,7 @@ def gameplay_screen(
370
357
  hint_delay = car_hint_conf.get("delay_ms", CAR_HINT_DELAY_MS_DEFAULT)
371
358
  elapsed_ms = game_data.state.elapsed_play_ms
372
359
  has_fuel = game_data.state.has_fuel
373
- hint_enabled = car_hint_conf.get("enabled", True) and not stage.survival_stage
360
+ hint_enabled = car_hint_conf.get("enabled", True) and not stage.endurance_stage
374
361
  hint_target = None
375
362
  hint_color = YELLOW
376
363
  hint_expires_at = game_data.state.hint_expires_at
@@ -417,7 +404,6 @@ def gameplay_screen(
417
404
  render_assets,
418
405
  screen,
419
406
  game_data,
420
- frame_fov_target,
421
407
  config=config,
422
408
  hint_target=hint_target,
423
409
  hint_color=hint_color,
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from .entities_constants import ZOMBIE_AGING_DURATION_FRAMES
6
6
  from .gameplay_constants import SURVIVOR_SPAWN_RATE
7
+ from .level_constants import DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS
7
8
  from .models import Stage
8
9
 
9
10
  STAGES: list[Stage] = [
@@ -23,6 +24,7 @@ STAGES: list[Stage] = [
23
24
  requires_fuel=True,
24
25
  exterior_spawn_weight=0.97,
25
26
  interior_spawn_weight=0.03,
27
+ initial_interior_spawn_rate=0.007,
26
28
  ),
27
29
  Stage(
28
30
  id="stage3",
@@ -33,6 +35,7 @@ STAGES: list[Stage] = [
33
35
  requires_fuel=True,
34
36
  exterior_spawn_weight=0.97,
35
37
  interior_spawn_weight=0.03,
38
+ initial_interior_spawn_rate=0.007,
36
39
  ),
37
40
  Stage(
38
41
  id="stage4",
@@ -42,6 +45,7 @@ STAGES: list[Stage] = [
42
45
  rescue_stage=True,
43
46
  waiting_car_target_count=2,
44
47
  survivor_spawn_rate=SURVIVOR_SPAWN_RATE,
48
+ initial_interior_spawn_rate=0.007,
45
49
  ),
46
50
  Stage(
47
51
  id="stage5",
@@ -49,8 +53,8 @@ STAGES: list[Stage] = [
49
53
  description_key="stages.stage5.description",
50
54
  available=True,
51
55
  requires_fuel=True,
52
- survival_stage=True,
53
- survival_goal_ms=1_200_000,
56
+ endurance_stage=True,
57
+ endurance_goal_ms=1_200_000,
54
58
  fuel_spawn_count=0,
55
59
  exterior_spawn_weight=0.4,
56
60
  interior_spawn_weight=0.6,
@@ -137,6 +141,90 @@ STAGES: list[Stage] = [
137
141
  waiting_car_target_count=1,
138
142
  survivor_spawn_rate=0.35,
139
143
  ),
144
+ Stage(
145
+ id="stage11",
146
+ name_key="stages.stage11.name",
147
+ description_key="stages.stage11.description",
148
+ grid_cols=120,
149
+ grid_rows=7,
150
+ available=True,
151
+ wall_algorithm="sparse",
152
+ exterior_spawn_weight=0.3,
153
+ interior_spawn_weight=0.7,
154
+ zombie_normal_ratio=0.5,
155
+ zombie_tracker_ratio=0.5,
156
+ zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
157
+ initial_interior_spawn_rate=0.1,
158
+ waiting_car_target_count=1,
159
+ ),
160
+ Stage(
161
+ id="stage12",
162
+ name_key="stages.stage12.name",
163
+ description_key="stages.stage12.description",
164
+ grid_cols=32,
165
+ grid_rows=32,
166
+ available=True,
167
+ requires_fuel=True,
168
+ exterior_spawn_weight=0.5,
169
+ interior_spawn_weight=0.2,
170
+ interior_fall_spawn_weight=0.3,
171
+ fall_spawn_zones=[
172
+ (3, 3, 12, 12),
173
+ (3, 17, 12, 12),
174
+ (17, 3, 12, 12),
175
+ (17, 17, 12, 12),
176
+ ],
177
+ initial_flashlight_count=5,
178
+ zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
179
+ ),
180
+ Stage(
181
+ id="stage13",
182
+ name_key="stages.stage13.name",
183
+ description_key="stages.stage13.description",
184
+ available=True,
185
+ wall_algorithm="grid_wire",
186
+ buddy_required_count=1,
187
+ requires_fuel=True,
188
+ exterior_spawn_weight=0.6,
189
+ interior_spawn_weight=0.1,
190
+ interior_fall_spawn_weight=0.3,
191
+ zombie_normal_ratio=0.4,
192
+ zombie_tracker_ratio=0.3,
193
+ zombie_wall_follower_ratio=0.3,
194
+ zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
195
+ initial_flashlight_count=3,
196
+ fall_spawn_zones=[
197
+ (x, y, 2, 2)
198
+ for y in range(2, DEFAULT_GRID_ROWS - 2, 4)
199
+ for x in range(2, DEFAULT_GRID_COLS - 2, 4)
200
+ ],
201
+ ),
202
+ Stage(
203
+ id="stage14",
204
+ name_key="stages.stage14.name",
205
+ description_key="stages.stage14.description",
206
+ grid_cols=42,
207
+ grid_rows=27,
208
+ available=False,
209
+ requires_fuel=True,
210
+ exterior_spawn_weight=0.2,
211
+ interior_spawn_weight=0.1,
212
+ interior_fall_spawn_weight=0.7,
213
+ fall_spawn_zones=[
214
+ (4, 10, 3, 3),
215
+ (5, 20, 3, 3),
216
+ (15, 17, 3, 3),
217
+ (22, 16, 3, 3),
218
+ (17, 20, 3, 3),
219
+ (26, 22, 3, 3),
220
+ (31, 17, 3, 3),
221
+ (33, 10, 3, 3),
222
+ (34, 7, 3, 3),
223
+ (35, 13, 3, 3),
224
+ ],
225
+ initial_flashlight_count=5,
226
+ zombie_aging_duration_frames=ZOMBIE_AGING_DURATION_FRAMES * 2,
227
+ ),
140
228
  ]
141
229
  DEFAULT_STAGE_ID = "stage1"
142
230
 
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Iterable, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .entities import Wall
8
+
9
+
10
+ WallIndex = dict[tuple[int, int], list["Wall"]]
11
+
12
+
13
+ def build_wall_index(walls: Iterable["Wall"], *, cell_size: int) -> WallIndex:
14
+ index: WallIndex = {}
15
+ if cell_size <= 0:
16
+ return index
17
+ for wall in walls:
18
+ cell_x = int(wall.rect.centerx // cell_size)
19
+ cell_y = int(wall.rect.centery // cell_size)
20
+ index.setdefault((cell_x, cell_y), []).append(wall)
21
+ return index
22
+
23
+
24
+ def _infer_grid_size_from_index(wall_index: WallIndex) -> tuple[int | None, int | None]:
25
+ if not wall_index:
26
+ return None, None
27
+ max_col = max(cell[0] for cell in wall_index)
28
+ max_row = max(cell[1] for cell in wall_index)
29
+ return max_col + 1, max_row + 1
30
+
31
+
32
+ def walls_for_radius(
33
+ wall_index: WallIndex,
34
+ center: tuple[float, float],
35
+ radius: float,
36
+ *,
37
+ cell_size: int,
38
+ grid_cols: int | None = None,
39
+ grid_rows: int | None = None,
40
+ ) -> list["Wall"]:
41
+ if grid_cols is None or grid_rows is None:
42
+ grid_cols, grid_rows = _infer_grid_size_from_index(wall_index)
43
+ if grid_cols is None or grid_rows is None:
44
+ return []
45
+ search_radius = radius + cell_size
46
+ min_x = max(0, int((center[0] - search_radius) // cell_size))
47
+ max_x = min(grid_cols - 1, int((center[0] + search_radius) // cell_size))
48
+ min_y = max(0, int((center[1] - search_radius) // cell_size))
49
+ max_y = min(grid_rows - 1, int((center[1] + search_radius) // cell_size))
50
+ candidates: list[Wall] = []
51
+ for cy in range(min_y, max_y + 1):
52
+ for cx in range(min_x, max_x + 1):
53
+ candidates.extend(wall_index.get((cx, cy), []))
54
+ return candidates
55
+
56
+
57
+ def apply_tile_edge_nudge(
58
+ x: float,
59
+ y: float,
60
+ dx: float,
61
+ dy: float,
62
+ *,
63
+ cell_size: int,
64
+ wall_cells: set[tuple[int, int]] | None,
65
+ bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]] | None = None,
66
+ grid_cols: int,
67
+ grid_rows: int,
68
+ strength: float = 0.03,
69
+ edge_margin_ratio: float = 0.15,
70
+ min_margin: float = 2.0,
71
+ ) -> tuple[float, float]:
72
+ if dx == 0 and dy == 0:
73
+ return dx, dy
74
+ if cell_size <= 0 or not wall_cells:
75
+ return dx, dy
76
+ cell_x = int(x // cell_size)
77
+ cell_y = int(y // cell_size)
78
+ if cell_x < 0 or cell_y < 0 or cell_x >= grid_cols or cell_y >= grid_rows:
79
+ return dx, dy
80
+ speed = math.hypot(dx, dy)
81
+ if speed <= 0:
82
+ return dx, dy
83
+
84
+ edge_margin = max(min_margin, cell_size * edge_margin_ratio)
85
+ left_dist = x - (cell_x * cell_size)
86
+ right_dist = ((cell_x + 1) * cell_size) - x
87
+ top_dist = y - (cell_y * cell_size)
88
+ bottom_dist = ((cell_y + 1) * cell_size) - y
89
+
90
+ def apply_push(dist: float, direction: float) -> float:
91
+ if dist >= edge_margin:
92
+ return 0.0
93
+ ratio = (edge_margin - dist) / edge_margin
94
+ return ratio * speed * strength * direction
95
+
96
+ if (cell_x - 1, cell_y) in wall_cells:
97
+ dx += apply_push(left_dist, 1.0)
98
+ if (cell_x + 1, cell_y) in wall_cells:
99
+ dx += apply_push(right_dist, -1.0)
100
+ if (cell_x, cell_y - 1) in wall_cells:
101
+ dy += apply_push(top_dist, 1.0)
102
+ if (cell_x, cell_y + 1) in wall_cells:
103
+ dy += apply_push(bottom_dist, -1.0)
104
+
105
+ def apply_corner_push(dist_a: float, dist_b: float, boost: float = 1.0) -> float:
106
+ if dist_a >= edge_margin or dist_b >= edge_margin:
107
+ return 0.0
108
+ ratio = (edge_margin - min(dist_a, dist_b)) / edge_margin
109
+ return ratio * speed * strength * boost
110
+
111
+ if bevel_corners:
112
+ boosted = 1.25
113
+ corner_wall = bevel_corners.get((cell_x - 1, cell_y - 1))
114
+ if corner_wall and corner_wall[2]:
115
+ push = apply_corner_push(left_dist, top_dist, boosted)
116
+ dx += push
117
+ dy += push
118
+ corner_wall = bevel_corners.get((cell_x + 1, cell_y - 1))
119
+ if corner_wall and corner_wall[3]:
120
+ push = apply_corner_push(right_dist, top_dist, boosted)
121
+ dx -= push
122
+ dy += push
123
+ corner_wall = bevel_corners.get((cell_x + 1, cell_y + 1))
124
+ if corner_wall and corner_wall[0]:
125
+ push = apply_corner_push(right_dist, bottom_dist, boosted)
126
+ dx -= push
127
+ dy -= push
128
+ corner_wall = bevel_corners.get((cell_x - 1, cell_y + 1))
129
+ if corner_wall and corner_wall[1]:
130
+ push = apply_corner_push(left_dist, bottom_dist, boosted)
131
+ dx += push
132
+ dy -= push
133
+
134
+ return dx, dy
@@ -22,7 +22,7 @@ from .gameplay import calculate_car_speed_for_passengers
22
22
  from .level_constants import DEFAULT_TILE_SIZE
23
23
  from .localization import set_language
24
24
  from .models import GameData, Stage
25
- from .render_constants import build_render_assets
25
+ from .render_constants import RenderAssets, build_render_assets
26
26
  from .screen_constants import (
27
27
  DEFAULT_WINDOW_SCALE,
28
28
  FPS,
@@ -95,6 +95,7 @@ def main() -> None:
95
95
  from .screens.gameplay import gameplay_screen
96
96
 
97
97
  apply_window_scale(DEFAULT_WINDOW_SCALE)
98
+ pygame.mouse.set_visible(True)
98
99
  screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
99
100
  clock = pygame.time.Clock()
100
101
 
@@ -109,15 +110,54 @@ def main() -> None:
109
110
  save_config(config, config_path)
110
111
  set_language(config.get("language"))
111
112
 
113
+ def _profiled_gameplay_screen(
114
+ screen: pygame.Surface,
115
+ clock: pygame.time.Clock,
116
+ config: dict[str, Any],
117
+ fps: int,
118
+ stage: Stage,
119
+ *,
120
+ show_pause_overlay: bool,
121
+ seed: int | None,
122
+ render_assets: RenderAssets,
123
+ debug_mode: bool,
124
+ ) -> ScreenTransition:
125
+ import cProfile
126
+ import pstats
127
+
128
+ profiler = cProfile.Profile()
129
+ try:
130
+ return profiler.runcall(
131
+ gameplay_screen,
132
+ screen,
133
+ clock,
134
+ config,
135
+ fps,
136
+ stage,
137
+ show_pause_overlay=show_pause_overlay,
138
+ seed=seed,
139
+ render_assets=render_assets,
140
+ debug_mode=debug_mode,
141
+ )
142
+ finally:
143
+ output_path = Path(args.profile_output)
144
+ profiler.dump_stats(output_path)
145
+ summary_path = output_path.with_suffix(".txt")
146
+ with summary_path.open("w", encoding="utf-8") as handle:
147
+ stats = pstats.Stats(
148
+ profiler,
149
+ stream=handle,
150
+ ).sort_stats("tottime")
151
+ stats.print_stats(50)
152
+ print(f"Profile saved to {output_path} and {summary_path}")
153
+
112
154
  next_screen = ScreenID.TITLE
113
- pending_stage: Stage | None = None
114
- pending_game_data: GameData | None = None
115
- pending_config: dict[str, Any] | None = None
116
- pending_seed: int | None = None
155
+ transition: ScreenTransition | None = None
117
156
  running = True
118
157
 
119
158
  while running:
120
- transition: ScreenTransition | None = None
159
+ incoming = transition
160
+ transition = None
121
161
 
122
162
  if next_screen == ScreenID.TITLE:
123
163
  seed_input = None if title_seed_is_auto else title_seed_text
@@ -147,56 +187,26 @@ def main() -> None:
147
187
  set_language(config.get("language"))
148
188
  transition = ScreenTransition(ScreenID.TITLE)
149
189
  elif next_screen == ScreenID.GAMEPLAY:
150
- stage = pending_stage
151
- pending_stage = None
152
- seed_value = pending_seed
153
- pending_seed = None
190
+ stage = incoming.stage
191
+ seed_value = incoming.seed
154
192
  if stage is None:
155
193
  transition = ScreenTransition(ScreenID.TITLE)
156
194
  else:
157
195
  last_stage_id = stage.id
196
+ render_assets = build_render_assets(stage.tile_size)
158
197
  try:
159
- if args.profile:
160
- import cProfile
161
- import pstats
162
-
163
- profiler = cProfile.Profile()
164
- try:
165
- render_assets = build_render_assets(stage.tile_size)
166
- transition = profiler.runcall(
167
- gameplay_screen,
168
- screen,
169
- clock,
170
- config,
171
- FPS,
172
- stage,
173
- show_pause_overlay=not debug_mode,
174
- seed=seed_value,
175
- render_assets=render_assets,
176
- debug_mode=debug_mode,
177
- )
178
- finally:
179
- output_path = Path(args.profile_output)
180
- profiler.dump_stats(output_path)
181
- summary_path = output_path.with_suffix(".txt")
182
- stats = pstats.Stats(profiler).sort_stats("tottime")
183
- with summary_path.open("w", encoding="utf-8") as handle:
184
- stats.stream = handle
185
- stats.print_stats(50)
186
- print(f"Profile saved to {output_path} and {summary_path}")
187
- else:
188
- render_assets = build_render_assets(stage.tile_size)
189
- transition = gameplay_screen(
190
- screen,
191
- clock,
192
- config,
193
- FPS,
194
- stage,
195
- show_pause_overlay=not debug_mode,
196
- seed=seed_value,
197
- render_assets=render_assets,
198
- debug_mode=debug_mode,
199
- )
198
+ gs = _profiled_gameplay_screen if args.profile else gameplay_screen
199
+ transition = gs(
200
+ screen,
201
+ clock,
202
+ config,
203
+ FPS,
204
+ stage,
205
+ show_pause_overlay=not debug_mode,
206
+ seed=seed_value,
207
+ render_assets=render_assets,
208
+ debug_mode=debug_mode,
209
+ )
200
210
  except SystemExit:
201
211
  running = False
202
212
  break
@@ -206,12 +216,10 @@ def main() -> None:
206
216
  running = False
207
217
  break
208
218
  elif next_screen == ScreenID.GAME_OVER:
209
- game_data = pending_game_data
210
- stage = pending_stage
211
- config_payload = pending_config
212
- pending_game_data = None
213
- pending_stage = None
214
- pending_config = None
219
+ game_data = incoming.game_data if incoming else None
220
+ stage = incoming.stage if incoming else None
221
+ config_payload = incoming.config if incoming else None
222
+ assert config_payload is not None
215
223
  if game_data is not None:
216
224
  render_assets = build_render_assets(game_data.cell_size)
217
225
  elif stage is not None:
@@ -235,10 +243,6 @@ def main() -> None:
235
243
  if not transition:
236
244
  break
237
245
 
238
- pending_stage = transition.stage
239
- pending_game_data = transition.game_data
240
- pending_config = transition.config
241
- pending_seed = transition.seed
242
246
  if transition.next_screen == ScreenID.GAMEPLAY:
243
247
  title_seed_text = cli_seed_text
244
248
  title_seed_is_auto = cli_seed_is_auto
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zombie-escape
3
- Version: 1.8.0
3
+ Version: 1.10.0
4
4
  Summary: Top-down zombie survival game built with pygame.
5
5
  Project-URL: Homepage, https://github.com/tos-kamiya/zombie-escape
6
6
  Author-email: Toshihiro Kamiya <kamiya@mbj.nifty.com>
@@ -12,12 +12,19 @@ Classifier: Programming Language :: Python
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
15
16
  Classifier: Programming Language :: Python :: Implementation :: CPython
16
17
  Requires-Python: >=3.10
17
18
  Requires-Dist: numpy
18
19
  Requires-Dist: platformdirs
19
20
  Requires-Dist: pygame
20
21
  Requires-Dist: python-i18n
22
+ Requires-Dist: typing-extensions; python_version < '3.11'
23
+ Provides-Extra: dev
24
+ Requires-Dist: pydeps; extra == 'dev'
25
+ Requires-Dist: pyright; extra == 'dev'
26
+ Requires-Dist: pytest; extra == 'dev'
27
+ Requires-Dist: ruff; extra == 'dev'
21
28
  Description-Content-Type: text/markdown
22
29
 
23
30
  # Zombie Escape