zombie-escape 1.7.1__py3-none-any.whl → 1.8.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.
@@ -16,15 +16,11 @@ from ..entities_constants import (
16
16
  SURVIVOR_RADIUS,
17
17
  ZOMBIE_RADIUS,
18
18
  )
19
- from .constants import (
20
- SURVIVOR_CONVERSION_LINE_KEYS,
21
- SURVIVOR_MESSAGE_DURATION_MS,
22
- SURVIVOR_SPEED_PENALTY_PER_PASSENGER,
23
- )
24
- from ..localization import translate as tr
19
+ from .constants import SURVIVOR_MESSAGE_DURATION_MS, SURVIVOR_SPEED_PENALTY_PER_PASSENGER
20
+ from ..localization import translate as tr, translate_dict, translate_list
25
21
  from ..models import GameData, ProgressState
26
22
  from ..rng import get_rng
27
- from ..entities import Survivor, spritecollideany_walls, WallIndex
23
+ from ..entities import Survivor, Zombie, spritecollideany_walls, WallIndex
28
24
  from .spawn import _create_zombie
29
25
  from .utils import find_nearby_offscreen_spawn_position, rect_visible_on_screen
30
26
 
@@ -51,6 +47,10 @@ def update_survivors(
51
47
  wall_group,
52
48
  wall_index=wall_index,
53
49
  cell_size=game_data.cell_size,
50
+ wall_cells=game_data.layout.wall_cells,
51
+ bevel_corners=game_data.layout.bevel_corners,
52
+ grid_cols=game_data.stage.grid_cols,
53
+ grid_rows=game_data.stage.grid_rows,
54
54
  level_width=game_data.level_width,
55
55
  level_height=game_data.level_height,
56
56
  )
@@ -150,11 +150,40 @@ def add_survivor_message(game_data: GameData, text: str) -> None:
150
150
  game_data.state.survivor_messages.append({"text": text, "expires_at": expires})
151
151
 
152
152
 
153
- def random_survivor_conversion_line() -> str:
154
- if not SURVIVOR_CONVERSION_LINE_KEYS:
153
+ def _normalize_legacy_conversion_lines(data: dict[str, Any]) -> list[str]:
154
+ numbered: list[tuple[int, str]] = []
155
+ others: list[tuple[str, str]] = []
156
+ for key, value in data.items():
157
+ if not value:
158
+ continue
159
+ text = str(value)
160
+ if isinstance(key, str) and key.startswith("line"):
161
+ suffix = key[4:]
162
+ if suffix.isdigit():
163
+ numbered.append((int(suffix), text))
164
+ continue
165
+ others.append((str(key), text))
166
+ numbered.sort(key=lambda item: item[0])
167
+ others.sort(key=lambda item: item[0])
168
+ return [text for _, text in numbered] + [text for _, text in others]
169
+
170
+
171
+ def _get_survivor_conversion_messages(stage_id: str) -> list[str]:
172
+ key = f"stages.{stage_id}.survivor_conversion_messages"
173
+ raw = translate_list(key)
174
+ if raw:
175
+ return [str(item) for item in raw if item]
176
+ legacy = translate_dict(f"stages.{stage_id}.conversion_lines")
177
+ if legacy:
178
+ return _normalize_legacy_conversion_lines(legacy)
179
+ return []
180
+
181
+
182
+ def random_survivor_conversion_line(stage_id: str) -> str:
183
+ lines = _get_survivor_conversion_messages(stage_id)
184
+ if not lines:
155
185
  return ""
156
- key = RNG.choice(SURVIVOR_CONVERSION_LINE_KEYS)
157
- return tr(key)
186
+ return RNG.choice(lines)
158
187
 
159
188
 
160
189
  def cleanup_survivor_messages(state: ProgressState) -> None:
@@ -224,7 +253,7 @@ def handle_survivor_zombie_collisions(
224
253
  min_x = survivor.rect.centerx - search_radius
225
254
  max_x = survivor.rect.centerx + search_radius
226
255
  start_idx = bisect_left(zombie_xs, min_x)
227
- collided = False
256
+ collided_zombie: Zombie | None = None
228
257
  for idx in range(start_idx, len(zombies)):
229
258
  zombie_x = zombie_xs[idx]
230
259
  if zombie_x > max_x:
@@ -237,10 +266,10 @@ def handle_survivor_zombie_collisions(
237
266
  continue
238
267
  dx = zombie_x - survivor.rect.centerx
239
268
  if dx * dx + dy * dy <= search_radius_sq:
240
- collided = True
269
+ collided_zombie = zombie
241
270
  break
242
271
 
243
- if not collided:
272
+ if collided_zombie is None:
244
273
  continue
245
274
  if not rect_visible_on_screen(camera, survivor.rect):
246
275
  spawn_pos = find_nearby_offscreen_spawn_position(
@@ -250,13 +279,15 @@ def handle_survivor_zombie_collisions(
250
279
  survivor.teleport(spawn_pos)
251
280
  continue
252
281
  survivor.kill()
253
- line = random_survivor_conversion_line()
282
+ line = random_survivor_conversion_line(game_data.stage.id)
254
283
  if line:
255
284
  add_survivor_message(game_data, line)
256
285
  new_zombie = _create_zombie(
257
286
  config,
258
287
  start_pos=survivor.rect.center,
259
288
  stage=game_data.stage,
289
+ tracker=bool(getattr(collided_zombie, "tracker", False)),
290
+ wall_follower=bool(getattr(collided_zombie, "wall_follower", False)),
260
291
  )
261
292
  zombie_group.add(new_zombie)
262
293
  game_data.groups.all_sprites.add(new_zombie, layer=1)
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Tuple
4
+
5
+ import pygame
6
+
7
+ DEADZONE = 0.25
8
+ JOY_BUTTON_A = 0
9
+ JOY_BUTTON_BACK = 6
10
+ JOY_BUTTON_START = 7
11
+ JOY_BUTTON_RB = 5
12
+
13
+ CONTROLLER_AVAILABLE = hasattr(pygame, "controller")
14
+ CONTROLLER_DEVICE_ADDED = getattr(pygame, "CONTROLLERDEVICEADDED", None)
15
+ CONTROLLER_DEVICE_REMOVED = getattr(pygame, "CONTROLLERDEVICEREMOVED", None)
16
+ CONTROLLER_BUTTON_DOWN = getattr(pygame, "CONTROLLERBUTTONDOWN", None)
17
+ CONTROLLER_BUTTON_A = getattr(pygame, "CONTROLLER_BUTTON_A", None)
18
+ CONTROLLER_BUTTON_BACK = getattr(pygame, "CONTROLLER_BUTTON_BACK", None)
19
+ CONTROLLER_BUTTON_START = getattr(pygame, "CONTROLLER_BUTTON_START", None)
20
+ CONTROLLER_BUTTON_DPAD_UP = getattr(pygame, "CONTROLLER_BUTTON_DPAD_UP", None)
21
+ CONTROLLER_BUTTON_DPAD_DOWN = getattr(pygame, "CONTROLLER_BUTTON_DPAD_DOWN", None)
22
+ CONTROLLER_BUTTON_DPAD_LEFT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_LEFT", None)
23
+ CONTROLLER_BUTTON_DPAD_RIGHT = getattr(pygame, "CONTROLLER_BUTTON_DPAD_RIGHT", None)
24
+ CONTROLLER_BUTTON_RB = getattr(pygame, "CONTROLLER_BUTTON_RIGHTSHOULDER", None)
25
+ CONTROLLER_AXIS_LEFTX = getattr(pygame, "CONTROLLER_AXIS_LEFTX", None)
26
+ CONTROLLER_AXIS_LEFTY = getattr(pygame, "CONTROLLER_AXIS_LEFTY", None)
27
+ CONTROLLER_AXIS_TRIGGERRIGHT = getattr(
28
+ pygame, "CONTROLLER_AXIS_TRIGGERRIGHT", None
29
+ )
30
+
31
+
32
+ def init_first_controller() -> pygame.controller.Controller | None:
33
+ if not CONTROLLER_AVAILABLE:
34
+ return None
35
+ try:
36
+ if pygame.controller.get_count() > 0:
37
+ controller = pygame.controller.Controller(0)
38
+ if not controller.get_init():
39
+ controller.init()
40
+ return controller
41
+ except pygame.error:
42
+ return None
43
+ return None
44
+
45
+
46
+ def init_first_joystick() -> pygame.joystick.Joystick | None:
47
+ try:
48
+ if pygame.joystick.get_count() > 0:
49
+ joystick = pygame.joystick.Joystick(0)
50
+ if not joystick.get_init():
51
+ joystick.init()
52
+ return joystick
53
+ except pygame.error:
54
+ return None
55
+ return None
56
+
57
+
58
+ def is_confirm_event(event: pygame.event.Event) -> bool:
59
+ if CONTROLLER_BUTTON_DOWN is not None and event.type == CONTROLLER_BUTTON_DOWN:
60
+ return CONTROLLER_BUTTON_A is not None and event.button == CONTROLLER_BUTTON_A
61
+ if event.type == pygame.JOYBUTTONDOWN:
62
+ return event.button == JOY_BUTTON_A
63
+ return False
64
+
65
+
66
+ def is_start_event(event: pygame.event.Event) -> bool:
67
+ 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
+ )
72
+ if event.type == pygame.JOYBUTTONDOWN:
73
+ return event.button == JOY_BUTTON_START
74
+ return False
75
+
76
+
77
+ def is_select_event(event: pygame.event.Event) -> bool:
78
+ 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
+ )
83
+ if event.type == pygame.JOYBUTTONDOWN:
84
+ return event.button == JOY_BUTTON_BACK
85
+ return False
86
+
87
+
88
+ def read_gamepad_move(
89
+ controller: pygame.controller.Controller | None,
90
+ joystick: pygame.joystick.Joystick | None,
91
+ *,
92
+ deadzone: float = DEADZONE,
93
+ ) -> Tuple[float, float]:
94
+ x = 0.0
95
+ y = 0.0
96
+ if controller and controller.get_init():
97
+ if CONTROLLER_AXIS_LEFTX is None or CONTROLLER_AXIS_LEFTY is None:
98
+ return 0.0, 0.0
99
+ x = float(controller.get_axis(CONTROLLER_AXIS_LEFTX))
100
+ y = float(controller.get_axis(CONTROLLER_AXIS_LEFTY))
101
+ if abs(x) < deadzone:
102
+ x = 0.0
103
+ if abs(y) < deadzone:
104
+ y = 0.0
105
+ if (
106
+ CONTROLLER_BUTTON_DPAD_LEFT is not None
107
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_LEFT)
108
+ ):
109
+ x = -1.0
110
+ elif (
111
+ CONTROLLER_BUTTON_DPAD_RIGHT is not None
112
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_RIGHT)
113
+ ):
114
+ x = 1.0
115
+ if (
116
+ CONTROLLER_BUTTON_DPAD_UP is not None
117
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_UP)
118
+ ):
119
+ y = -1.0
120
+ elif (
121
+ CONTROLLER_BUTTON_DPAD_DOWN is not None
122
+ and controller.get_button(CONTROLLER_BUTTON_DPAD_DOWN)
123
+ ):
124
+ y = 1.0
125
+ return x, y
126
+
127
+ if joystick and joystick.get_init():
128
+ if joystick.get_numaxes() >= 2:
129
+ x = float(joystick.get_axis(0))
130
+ y = float(joystick.get_axis(1))
131
+ if abs(x) < deadzone:
132
+ x = 0.0
133
+ if abs(y) < deadzone:
134
+ y = 0.0
135
+ if joystick.get_numhats() > 0:
136
+ hat_x, hat_y = joystick.get_hat(0)
137
+ if hat_x:
138
+ x = float(hat_x)
139
+ if hat_y:
140
+ y = float(-hat_y)
141
+ return x, y
142
+
143
+
144
+ def is_accel_active(
145
+ keys: pygame.key.ScancodeWrapper,
146
+ controller: pygame.controller.Controller | None,
147
+ joystick: pygame.joystick.Joystick | None,
148
+ ) -> bool:
149
+ if keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]:
150
+ return True
151
+ if controller and controller.get_init():
152
+ if (
153
+ CONTROLLER_BUTTON_RB is not None
154
+ and controller.get_button(CONTROLLER_BUTTON_RB)
155
+ ):
156
+ return True
157
+ if CONTROLLER_AXIS_TRIGGERRIGHT is not None:
158
+ if controller.get_axis(CONTROLLER_AXIS_TRIGGERRIGHT) > DEADZONE:
159
+ return True
160
+ if joystick and joystick.get_init():
161
+ if joystick.get_numbuttons() > JOY_BUTTON_RB:
162
+ if joystick.get_button(JOY_BUTTON_RB):
163
+ return True
164
+ if joystick.get_numaxes() > 5:
165
+ if joystick.get_axis(5) > DEADZONE:
166
+ return True
167
+ return False
@@ -6,6 +6,7 @@ EXITS_PER_SIDE = 1 # currently fixed to 1 per side (can be tuned)
6
6
  NUM_WALL_LINES = 80 # reduced density (roughly 1/5 of previous 450)
7
7
  WALL_MIN_LEN = 3
8
8
  WALL_MAX_LEN = 10
9
+ SPARSE_WALL_DENSITY = 0.10
9
10
  SPAWN_MARGIN = 3 # keep spawns away from walls/edges
10
11
  SPAWN_ZOMBIES = 3
11
12
 
@@ -201,10 +202,37 @@ def _place_walls_grid_wire(grid: list[list[str]]) -> None:
201
202
  grid[y][x] = "1"
202
203
 
203
204
 
205
+ def _place_walls_sparse(grid: list[list[str]]) -> None:
206
+ """Place isolated wall tiles at a low density, avoiding adjacency."""
207
+ cols, rows = len(grid[0]), len(grid)
208
+ forbidden = _collect_exit_adjacent_cells(grid)
209
+ for y in range(2, rows - 2):
210
+ for x in range(2, cols - 2):
211
+ if (x, y) in forbidden:
212
+ continue
213
+ if grid[y][x] != ".":
214
+ continue
215
+ if RNG.random() >= SPARSE_WALL_DENSITY:
216
+ continue
217
+ if (
218
+ grid[y - 1][x] == "1"
219
+ or grid[y + 1][x] == "1"
220
+ or grid[y][x - 1] == "1"
221
+ or grid[y][x + 1] == "1"
222
+ or grid[y - 1][x - 1] == "1"
223
+ or grid[y - 1][x + 1] == "1"
224
+ or grid[y + 1][x - 1] == "1"
225
+ or grid[y + 1][x + 1] == "1"
226
+ ):
227
+ continue
228
+ grid[y][x] = "1"
229
+
230
+
204
231
  WALL_ALGORITHMS = {
205
232
  "default": _place_walls_default,
206
233
  "empty": _place_walls_empty,
207
234
  "grid_wire": _place_walls_grid_wire,
235
+ "sparse": _place_walls_sparse,
208
236
  }
209
237
 
210
238
 
@@ -28,7 +28,7 @@
28
28
  "hints": {
29
29
  "navigate": "Up/Down: choose an option",
30
30
  "page_switch": "Left/Right: switch stage groups",
31
- "confirm": "Enter/Space: activate selection"
31
+ "confirm": "Enter/Space/South: activate selection"
32
32
  },
33
33
  "option_help": {
34
34
  "settings": "Open Settings to adjust assists and localization.",
@@ -56,7 +56,12 @@
56
56
  "line1": "It hurts!",
57
57
  "line2": "I can't hold on!",
58
58
  "line3": "Stay back!"
59
- }
59
+ },
60
+ "survivor_conversion_messages": [
61
+ "It hurts!",
62
+ "I can't hold on!",
63
+ "Stay back!"
64
+ ]
60
65
  },
61
66
  "stage5": {
62
67
  "name": "#5 Survive Until Dawn",
@@ -75,8 +80,33 @@
75
80
  "description": "Narrow corridors. Get surrounded, and there’s no escape."
76
81
  },
77
82
  "stage9": {
78
- "name": "# Evacuate Survivors 2",
79
- "description": "Evacuate like stage 4, but narrow corridors. Requires fuel."
83
+ "name": "#9 Evacuate Survivors 2",
84
+ "description": "Evacuate like stage 4, but narrow corridors. Requires fuel.",
85
+ "survivor_conversion_messages": [
86
+ "It hurts!",
87
+ "I can't hold on!",
88
+ "Stay back!",
89
+ "Stop!"
90
+ ]
91
+ },
92
+ "stage10": {
93
+ "name": "#10 Outbreak",
94
+ "description": "Zombies have infiltrated a building where survivors took shelter.",
95
+ "survivor_conversion_messages": [
96
+ "It hurts!",
97
+ "I can't hold on!",
98
+ "Stay back!",
99
+ "Stop!",
100
+ "Whoa!",
101
+ "Eeeeek!",
102
+ "Noooo!",
103
+ "Help me!",
104
+ "Get away!",
105
+ "Someone help!",
106
+ "Don't touch me!",
107
+ "No, no, no!",
108
+ "Please!"
109
+ ]
80
110
  }
81
111
  },
82
112
  "status": {
@@ -91,10 +121,10 @@
91
121
  "hud": {
92
122
  "need_fuel": "Need fuel to drive!",
93
123
  "paused": "PAUSED",
94
- "resume_hint": "Press P or click to resume",
124
+ "pause_hint": "P/Start: Resume · ESC/Select: Return to Title",
95
125
  "survival_timer_label": "Until dawn %{time}",
96
126
  "time_accel": ">> 4x",
97
- "time_accel_hint": "Hold Shift: 4x"
127
+ "time_accel_hint": "Hold Shift/R1: 4x"
98
128
  },
99
129
  "survivors": {
100
130
  "too_many_aboard": "Too many aboard!"
@@ -117,11 +147,13 @@
117
147
  "settings": {
118
148
  "title": "Settings",
119
149
  "sections": {
150
+ "menu": "Menu",
120
151
  "localization": "Localization",
121
152
  "player_support": "Player support",
122
153
  "tougher_enemies": "Tougher enemies"
123
154
  },
124
155
  "rows": {
156
+ "return_to_title": "Return to Title",
125
157
  "language": "Language",
126
158
  "footprints": "Footprints",
127
159
  "car_hint": "Car hint",
@@ -131,9 +163,9 @@
131
163
  "hints": {
132
164
  "navigate": "Up/Down: select a setting",
133
165
  "adjust": "Left/Right: set value",
134
- "toggle": "Space/Enter: toggle value",
166
+ "toggle": "Space/Enter/South: toggle value",
135
167
  "reset": "R: reset to defaults",
136
- "exit": "Esc/Backspace: save and return",
168
+ "exit": "Esc/BS/Select: save and return",
137
169
  "fullscreen": "F: toggle fullscreen"
138
170
  },
139
171
  "config_path": "Config: %{path}",
@@ -146,7 +178,7 @@
146
178
  "game_over": {
147
179
  "win": "YOU ESCAPED!",
148
180
  "lose": "GAME OVER",
149
- "prompt": "ESC/SPACE: Title · R: Retry",
181
+ "prompt": "ESC/SPACE/Select/South: Title · R: Retry",
150
182
  "scream": "AAAAHHH!!",
151
183
  "survivors_summary": "Evacuated: %{count}",
152
184
  "survival_duration": "Time survived %{time}"
@@ -28,7 +28,7 @@
28
28
  "hints": {
29
29
  "navigate": "上下: 項目選択",
30
30
  "page_switch": "左右: ステージ群切替",
31
- "confirm": "Enter/Space: 決定または実行"
31
+ "confirm": "Enter/Space/South: 決定または実行"
32
32
  },
33
33
  "option_help": {
34
34
  "settings": "設定画面を開いて言語や補助オプションを変更します。",
@@ -56,7 +56,12 @@
56
56
  "line1": "痛いぃ!",
57
57
  "line2": "無理!無理!",
58
58
  "line3": "来るなぁ!"
59
- }
59
+ },
60
+ "survivor_conversion_messages": [
61
+ "痛いぃ!",
62
+ "無理!無理!",
63
+ "来るなぁ!"
64
+ ]
60
65
  },
61
66
  "stage5": {
62
67
  "name": "#5 持久戦",
@@ -76,7 +81,32 @@
76
81
  },
77
82
  "stage9": {
78
83
  "name": "#9 生存者救出 2",
79
- "description": "ステージ4と同じだが、狭い通路。燃料も必要。"
84
+ "description": "ステージ4と同じだが、狭い通路。燃料も必要。",
85
+ "survivor_conversion_messages": [
86
+ "痛いぃ!",
87
+ "無理!無理!",
88
+ "来るなぁ!",
89
+ "やめてぇ!"
90
+ ]
91
+ },
92
+ "stage10": {
93
+ "name": "#10 感染爆発",
94
+ "description": "建物に避難した生存者の中にゾンビが紛れ込んでいた。",
95
+ "survivor_conversion_messages": [
96
+ "痛いぃ!",
97
+ "無理!無理!",
98
+ "来るなぁ!",
99
+ "やめてぇ!",
100
+ "うわっ!",
101
+ "ひいいぃ",
102
+ "いやああ",
103
+ "助けて!",
104
+ "近寄るな!",
105
+ "誰か!",
106
+ "触るな!",
107
+ "やだ、やだ!",
108
+ "誰かー!"
109
+ ]
80
110
  }
81
111
  },
82
112
  "status": {
@@ -91,10 +121,10 @@
91
121
  "hud": {
92
122
  "need_fuel": "燃料が必要です!",
93
123
  "paused": "一時停止",
94
- "resume_hint": "Pキーかクリックで再開",
124
+ "pause_hint": "P/Start: ゲーム再開 ESC/Select: タイトルに戻る",
95
125
  "survival_timer_label": "夜明けまであと %{time}",
96
126
  "time_accel": ">> 4x",
97
- "time_accel_hint": "Shiftキー押下: 4x"
127
+ "time_accel_hint": "Shift/R1押下: 4x"
98
128
  },
99
129
  "survivors": {
100
130
  "too_many_aboard": "定員オーバー!"
@@ -117,11 +147,13 @@
117
147
  "settings": {
118
148
  "title": "設定",
119
149
  "sections": {
150
+ "menu": "メニュー",
120
151
  "localization": "言語設定",
121
152
  "player_support": "サポート",
122
153
  "tougher_enemies": "強敵"
123
154
  },
124
155
  "rows": {
156
+ "return_to_title": "タイトルに戻る",
125
157
  "language": "テキスト言語",
126
158
  "footprints": "足跡",
127
159
  "car_hint": "車のヒント",
@@ -131,9 +163,9 @@
131
163
  "hints": {
132
164
  "navigate": "上下: 項目選択",
133
165
  "adjust": "左右: 値の設定",
134
- "toggle": "スペース/エンター: 値のトグル",
166
+ "toggle": "Space/Enter/South: 値のトグル",
135
167
  "reset": "R: デフォルト設定に戻す",
136
- "exit": "Esc/BS: セーブして戻る",
168
+ "exit": "Esc/BS/Select: セーブして戻る",
137
169
  "fullscreen": "F: フルスクリーン切替"
138
170
  },
139
171
  "config_path": "設定ファイル: %{path}",
@@ -146,7 +178,7 @@
146
178
  "game_over": {
147
179
  "win": "脱出成功!",
148
180
  "lose": "ゲームオーバー",
149
- "prompt": "ESC/Space: タイトルへ R: 再挑戦",
181
+ "prompt": "ESC/Space/Select/South: タイトルへ R: 再挑戦",
150
182
  "scream": "ぎゃあーーー!!",
151
183
  "survivors_summary": "救出人数: %{count}",
152
184
  "survival_duration": "逃げ延びた時間 %{time}"
@@ -108,6 +108,13 @@ def translate_dict(key: str) -> dict[str, Any]:
108
108
  return result if isinstance(result, dict) else {}
109
109
 
110
110
 
111
+ def translate_list(key: str) -> list[Any]:
112
+ if not _CONFIGURED:
113
+ set_language(_CURRENT_LANGUAGE)
114
+ result = _lookup_locale_value(key)
115
+ return result if isinstance(result, list) else []
116
+
117
+
111
118
  def get_font_settings(*, name: str = "primary") -> FontSettings:
112
119
  _get_language_options() # ensure locale data is loaded
113
120
  locale_data = _LOCALE_DATA.get(_CURRENT_LANGUAGE) or _LOCALE_DATA.get(
@@ -128,6 +135,26 @@ def _qualify_key(key: str) -> str:
128
135
  return key if key.startswith("ui.") else f"ui.{key}"
129
136
 
130
137
 
138
+ def _lookup_locale_value(key: str) -> Any:
139
+ locale_data = _LOCALE_DATA.get(_CURRENT_LANGUAGE) or _LOCALE_DATA.get(
140
+ DEFAULT_LANGUAGE, {}
141
+ )
142
+ if not isinstance(locale_data, dict):
143
+ return None
144
+ qualified = _qualify_key(key)
145
+ path = qualified.split(".")
146
+ if path and path[0] == "ui":
147
+ path = path[1:]
148
+ current: Any = locale_data
149
+ for segment in path:
150
+ if not isinstance(current, dict):
151
+ return None
152
+ current = current.get(segment)
153
+ if current is None:
154
+ return None
155
+ return current
156
+
157
+
131
158
  def _get_language_options() -> tuple[LanguageOption, ...]:
132
159
  global _LANGUAGE_OPTIONS
133
160
  if _LANGUAGE_OPTIONS is not None:
@@ -182,5 +209,6 @@ __all__ = [
182
209
  "language_options",
183
210
  "set_language",
184
211
  "translate",
212
+ "translate_list",
185
213
  "translate_dict",
186
214
  ]
zombie_escape/models.py CHANGED
@@ -26,6 +26,8 @@ class LevelLayout:
26
26
  outside_rects: list[pygame.Rect]
27
27
  walkable_cells: list[pygame.Rect]
28
28
  outer_wall_cells: set[tuple[int, int]]
29
+ wall_cells: set[tuple[int, int]]
30
+ bevel_corners: dict[tuple[int, int], tuple[bool, bool, bool, bool]]
29
31
 
30
32
 
31
33
  @dataclass
@@ -14,6 +14,7 @@ from ..render import (
14
14
  _draw_status_bar,
15
15
  show_message,
16
16
  )
17
+ from ..input_utils import is_confirm_event, is_select_event
17
18
  from ..screens import (
18
19
  ScreenID,
19
20
  ScreenTransition,
@@ -181,3 +182,6 @@ def game_over_screen(
181
182
  stage=stage,
182
183
  seed=state.seed,
183
184
  )
185
+ if event.type in (pygame.CONTROLLERBUTTONDOWN, pygame.JOYBUTTONDOWN):
186
+ if is_select_event(event) or is_confirm_event(event):
187
+ return ScreenTransition(ScreenID.TITLE)