zombie-escape 1.14.4__py3-none-any.whl → 1.15.2__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 (42) hide show
  1. zombie_escape/__about__.py +1 -1
  2. zombie_escape/config.py +1 -0
  3. zombie_escape/entities.py +126 -199
  4. zombie_escape/entities_constants.py +11 -1
  5. zombie_escape/export_images.py +4 -4
  6. zombie_escape/font_utils.py +47 -0
  7. zombie_escape/gameplay/__init__.py +2 -1
  8. zombie_escape/gameplay/constants.py +4 -0
  9. zombie_escape/gameplay/interactions.py +83 -16
  10. zombie_escape/gameplay/layout.py +9 -15
  11. zombie_escape/gameplay/movement.py +45 -29
  12. zombie_escape/gameplay/spawn.py +15 -29
  13. zombie_escape/gameplay/state.py +62 -7
  14. zombie_escape/gameplay/survivors.py +61 -10
  15. zombie_escape/gameplay/utils.py +33 -0
  16. zombie_escape/level_blueprints.py +35 -31
  17. zombie_escape/level_constants.py +2 -2
  18. zombie_escape/locales/ui.en.json +19 -8
  19. zombie_escape/locales/ui.ja.json +19 -8
  20. zombie_escape/localization.py +7 -1
  21. zombie_escape/models.py +21 -6
  22. zombie_escape/render/__init__.py +2 -2
  23. zombie_escape/render/core.py +113 -81
  24. zombie_escape/render/hud.py +112 -40
  25. zombie_escape/render/overview.py +93 -2
  26. zombie_escape/render/shadows.py +2 -2
  27. zombie_escape/render_constants.py +12 -0
  28. zombie_escape/screens/__init__.py +6 -189
  29. zombie_escape/screens/game_over.py +8 -21
  30. zombie_escape/screens/gameplay.py +71 -26
  31. zombie_escape/screens/settings.py +114 -43
  32. zombie_escape/screens/title.py +128 -47
  33. zombie_escape/stage_constants.py +37 -8
  34. zombie_escape/windowing.py +508 -0
  35. zombie_escape/world_grid.py +7 -5
  36. zombie_escape/zombie_escape.py +26 -13
  37. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/METADATA +24 -24
  38. zombie_escape-1.15.2.dist-info/RECORD +54 -0
  39. zombie_escape-1.14.4.dist-info/RECORD +0 -53
  40. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/WHEEL +0 -0
  41. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/entry_points.txt +0 -0
  42. {zombie_escape-1.14.4.dist-info → zombie_escape-1.15.2.dist-info}/licenses/LICENSE.txt +0 -0
@@ -0,0 +1,508 @@
1
+ """Window and presentation helpers for zombie_escape."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import pygame
8
+ from pygame import surface
9
+
10
+ from .screen_constants import (
11
+ DEFAULT_WINDOW_SCALE,
12
+ SCREEN_HEIGHT,
13
+ SCREEN_WIDTH,
14
+ WINDOW_SCALE_MAX,
15
+ WINDOW_SCALE_MIN,
16
+ )
17
+
18
+ if TYPE_CHECKING: # pragma: no cover - typing only
19
+ from .models import GameData
20
+
21
+ current_window_scale = DEFAULT_WINDOW_SCALE # Applied to the OS window only
22
+ current_maximized = False
23
+ last_window_scale = DEFAULT_WINDOW_SCALE
24
+ last_window_position: tuple[int, int] | None = None
25
+ current_window_size = (
26
+ int(SCREEN_WIDTH * DEFAULT_WINDOW_SCALE),
27
+ int(SCREEN_HEIGHT * DEFAULT_WINDOW_SCALE),
28
+ )
29
+ last_logged_window_size = current_window_size
30
+ _scaled_logical_size = (SCREEN_WIDTH, SCREEN_HEIGHT)
31
+
32
+ __all__ = [
33
+ "present",
34
+ "present_direct",
35
+ "apply_window_scale",
36
+ "prime_scaled_logical_size",
37
+ "nudge_window_scale",
38
+ "nudge_menu_window_scale",
39
+ "toggle_fullscreen",
40
+ "sync_window_size",
41
+ "adjust_menu_logical_size",
42
+ "set_scaled_logical_size",
43
+ ]
44
+
45
+
46
+ def present(logical_surface: surface.Surface) -> None:
47
+ """Scale the logical surface directly to the window and flip buffers."""
48
+ window = pygame.display.get_surface()
49
+ if window is None:
50
+ return
51
+ window_size = _fetch_window_size(window)
52
+ _update_window_size(window_size, source="frame")
53
+ logical_size = logical_surface.get_size()
54
+ if _use_scaled_display():
55
+ target_size = window.get_size()
56
+ if logical_size == target_size:
57
+ scaled_surface = logical_surface
58
+ elif logical_size[0] * 2 == target_size[0] and logical_size[1] * 2 == target_size[1]:
59
+ scaled_surface = pygame.transform.scale2x(logical_surface)
60
+ else:
61
+ scaled_surface = pygame.transform.scale(logical_surface, target_size)
62
+ window.blit(scaled_surface, (0, 0))
63
+ elif window_size == logical_size:
64
+ window.blit(logical_surface, (0, 0))
65
+ else:
66
+ # Preserve aspect ratio with letterboxing.
67
+ scale_x = window_size[0] / max(1, logical_size[0])
68
+ scale_y = window_size[1] / max(1, logical_size[1])
69
+ scale = min(scale_x, scale_y)
70
+ scaled_width = max(1, int(logical_size[0] * scale))
71
+ scaled_height = max(1, int(logical_size[1] * scale))
72
+ window.fill((0, 0, 0))
73
+ if (scaled_width, scaled_height) == logical_size:
74
+ scaled_surface = logical_surface
75
+ else:
76
+ scaled_surface = pygame.transform.scale(logical_surface, (scaled_width, scaled_height))
77
+ offset_x = (window_size[0] - scaled_width) // 2
78
+ offset_y = (window_size[1] - scaled_height) // 2
79
+ window.blit(scaled_surface, (offset_x, offset_y))
80
+ pygame.display.flip()
81
+
82
+
83
+ def present_direct(screen: surface.Surface) -> None:
84
+ """Flip the display without scaling; intended for direct window rendering."""
85
+ window = pygame.display.get_surface()
86
+ if window is None:
87
+ return
88
+ if window is not screen:
89
+ window.blit(screen, (0, 0))
90
+ pygame.display.flip()
91
+
92
+
93
+ def apply_window_scale(scale: float, *, game_data: "GameData | None" = None) -> surface.Surface:
94
+ """Resize the OS window; logical render surface stays constant."""
95
+ global current_window_scale, current_maximized, last_window_scale
96
+
97
+ clamped_scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, scale))
98
+ current_window_scale = clamped_scale
99
+ last_window_scale = clamped_scale
100
+ current_maximized = False
101
+
102
+ window_width = max(1, int(SCREEN_WIDTH * current_window_scale))
103
+ window_height = max(1, int(SCREEN_HEIGHT * current_window_scale))
104
+
105
+ flags = pygame.RESIZABLE
106
+ if _use_scaled_display():
107
+ flags |= pygame.SCALED
108
+ new_window = pygame.display.set_mode(_scaled_logical_size, flags)
109
+ _set_window_size((window_width, window_height))
110
+ else:
111
+ new_window = pygame.display.set_mode((window_width, window_height), flags)
112
+ _update_window_size((window_width, window_height), source="apply_scale")
113
+ _update_window_caption()
114
+
115
+ if game_data is not None:
116
+ game_data.state.overview_created = False
117
+
118
+ return new_window
119
+
120
+
121
+ def prime_scaled_logical_size(size: tuple[int, int]) -> None:
122
+ """Set initial logical render size before the first window is created."""
123
+ global _scaled_logical_size
124
+ target = _normalize_window_size(size)
125
+ _scaled_logical_size = target
126
+
127
+
128
+ def nudge_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
129
+ """Scale the window relative to the current zoom level."""
130
+ delta = 1.0 if multiplier >= 1.0 else -1.0
131
+ target_scale = current_window_scale + delta
132
+ return apply_window_scale(target_scale, game_data=game_data)
133
+
134
+
135
+ def nudge_menu_window_scale(multiplier: float, *, game_data: "GameData | None" = None) -> surface.Surface:
136
+ """Scale the window and update menu logical size consistently."""
137
+ delta = 1.0 if multiplier >= 1.0 else -1.0
138
+ target_scale = current_window_scale + delta
139
+ set_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT), preserve_window_size=False, game_data=game_data)
140
+ return apply_window_scale(target_scale, game_data=game_data)
141
+
142
+
143
+ def toggle_fullscreen(*, game_data: "GameData | None" = None) -> surface.Surface | None:
144
+ """Toggle fullscreen without persisting the setting."""
145
+ global current_maximized, last_window_scale, last_window_position
146
+ if current_maximized:
147
+ current_maximized = False
148
+ window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
149
+ window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
150
+ _set_sdl2_fullscreen(False, None)
151
+ flags = pygame.RESIZABLE
152
+ if _use_scaled_display():
153
+ flags |= pygame.SCALED
154
+ window = pygame.display.set_mode(_scaled_logical_size, flags)
155
+ _set_window_size((window_width, window_height))
156
+ else:
157
+ window = pygame.display.set_mode((window_width, window_height), flags)
158
+ if last_window_position is not None:
159
+ _restore_window_position(last_window_position)
160
+ _restore_window()
161
+ _update_window_caption()
162
+ _update_window_size((window_width, window_height), source="toggle_windowed")
163
+ else:
164
+ last_window_scale = current_window_scale
165
+ last_window_position = _fetch_window_position()
166
+ current_maximized = True
167
+ display_index = _fetch_display_index()
168
+ window = None
169
+ if _set_sdl2_fullscreen(True, display_index):
170
+ window = pygame.display.get_surface()
171
+ if window is None:
172
+ flags = pygame.FULLSCREEN
173
+ render_size = (0, 0)
174
+ if _use_scaled_display():
175
+ flags |= pygame.SCALED
176
+ render_size = _scaled_logical_size
177
+ if display_index is None:
178
+ window = pygame.display.set_mode(render_size, flags)
179
+ else:
180
+ window = pygame.display.set_mode(render_size, flags, display=display_index)
181
+ window_width, window_height = _fetch_window_size(window)
182
+ _update_window_caption()
183
+ _update_window_size((window_width, window_height), source="toggle_fullscreen")
184
+ pygame.mouse.set_visible(not current_maximized)
185
+ if game_data is not None:
186
+ game_data.state.overview_created = False
187
+ return window
188
+
189
+
190
+ def sync_window_size(event: pygame.event.Event, *, game_data: "GameData | None" = None) -> None:
191
+ """Synchronize tracked window size with SDL window events."""
192
+ global current_window_scale, last_window_scale
193
+ size = getattr(event, "size", None)
194
+ if not size:
195
+ width = getattr(event, "x", None)
196
+ height = getattr(event, "y", None)
197
+ if width is not None and height is not None:
198
+ size = (width, height)
199
+ if not size:
200
+ return
201
+ window_width, window_height = _normalize_window_size(size)
202
+ _update_window_size((window_width, window_height), source="window_event")
203
+ if not current_maximized:
204
+ scale_x = window_width / max(1, SCREEN_WIDTH)
205
+ scale_y = window_height / max(1, SCREEN_HEIGHT)
206
+ scale = max(WINDOW_SCALE_MIN, min(WINDOW_SCALE_MAX, min(scale_x, scale_y)))
207
+ current_window_scale = scale
208
+ last_window_scale = scale
209
+ _update_window_caption()
210
+ if game_data is not None:
211
+ game_data.state.overview_created = False
212
+
213
+
214
+ def set_scaled_logical_size(
215
+ size: tuple[int, int],
216
+ *,
217
+ preserve_window_size: bool = True,
218
+ game_data: "GameData | None" = None,
219
+ ) -> None:
220
+ """Update the logical render size when using pygame.SCALED."""
221
+ global _scaled_logical_size
222
+ if not _use_scaled_display():
223
+ return
224
+ target = _normalize_window_size(size)
225
+ if target == _scaled_logical_size:
226
+ return
227
+ previous_window_size = _fetch_window_size(pygame.display.get_surface())
228
+ _scaled_logical_size = target
229
+ flags = pygame.SCALED
230
+ if current_maximized:
231
+ flags |= pygame.FULLSCREEN
232
+ pygame.display.set_mode(_scaled_logical_size, flags)
233
+ else:
234
+ flags |= pygame.RESIZABLE
235
+ pygame.display.set_mode(_scaled_logical_size, flags)
236
+ if preserve_window_size:
237
+ _set_window_size(previous_window_size)
238
+ _update_window_caption()
239
+ if game_data is not None:
240
+ game_data.state.overview_created = False
241
+
242
+
243
+ def adjust_menu_logical_size(*, game_data: "GameData | None" = None) -> None:
244
+ """Match menu render size to the current window scale."""
245
+ set_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT), game_data=game_data)
246
+
247
+
248
+ def _fetch_window_size(window: surface.Surface | None) -> tuple[int, int]:
249
+ if hasattr(pygame.display, "get_window_size"):
250
+ size = pygame.display.get_window_size()
251
+ if size != (0, 0):
252
+ return _normalize_window_size(size)
253
+ if window is not None:
254
+ return _normalize_window_size(window.get_size())
255
+ window_width = max(1, int(SCREEN_WIDTH * last_window_scale))
256
+ window_height = max(1, int(SCREEN_HEIGHT * last_window_scale))
257
+ return window_width, window_height
258
+
259
+
260
+ def _normalize_window_size(size: tuple[int, int]) -> tuple[int, int]:
261
+ width = max(1, int(size[0]))
262
+ height = max(1, int(size[1]))
263
+ return width, height
264
+
265
+
266
+ def _set_window_size(size: tuple[int, int]) -> None:
267
+ setter = getattr(pygame.display, "set_window_size", None)
268
+ if callable(setter):
269
+ try:
270
+ setter(size)
271
+ return
272
+ except Exception:
273
+ pass
274
+ try:
275
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
276
+ except Exception:
277
+ return
278
+ try:
279
+ window = sdl2.Window.from_display_module()
280
+ except Exception:
281
+ return
282
+ setter = getattr(window, "set_size", None)
283
+ if callable(setter):
284
+ try:
285
+ setter(size)
286
+ return
287
+ except Exception:
288
+ return
289
+ try:
290
+ window.size = size
291
+ except Exception:
292
+ return
293
+
294
+
295
+ def _use_scaled_display() -> bool:
296
+ return hasattr(pygame, "SCALED")
297
+
298
+
299
+ def _fetch_window_position() -> tuple[int, int] | None:
300
+ try:
301
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
302
+ except Exception:
303
+ return None
304
+ try:
305
+ window = sdl2.Window.from_display_module()
306
+ except Exception:
307
+ return None
308
+ try:
309
+ position = window.position
310
+ except Exception:
311
+ return None
312
+ if not position:
313
+ return None
314
+ return (int(position[0]), int(position[1]))
315
+
316
+
317
+ def _restore_window_position(position: tuple[int, int]) -> None:
318
+ try:
319
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
320
+ except Exception:
321
+ return
322
+ try:
323
+ window = sdl2.Window.from_display_module()
324
+ except Exception:
325
+ return
326
+ setter = getattr(window, "set_position", None)
327
+ if setter is not None:
328
+ try:
329
+ setter(position)
330
+ return
331
+ except Exception:
332
+ return
333
+ try:
334
+ window.position = position
335
+ except Exception:
336
+ return
337
+
338
+
339
+ def _fetch_display_index() -> int | None:
340
+ try:
341
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
342
+ except Exception:
343
+ return None
344
+ try:
345
+ window = sdl2.Window.from_display_module()
346
+ except Exception:
347
+ return None
348
+
349
+ display_index = _infer_display_index_from_position(window, sdl2)
350
+ if display_index is not None:
351
+ return display_index
352
+
353
+ try:
354
+ return window.get_display_index()
355
+ except Exception:
356
+ return None
357
+
358
+
359
+ def _infer_display_index_from_position(window: object, sdl2: object) -> int | None:
360
+ try:
361
+ position = window.position # type: ignore[attr-defined]
362
+ except Exception:
363
+ return None
364
+ if not position:
365
+ return None
366
+
367
+ center_x, center_y = _window_center_from_position(window, position)
368
+ display_count = _get_display_count(sdl2)
369
+ if display_count is None:
370
+ return None
371
+
372
+ for display_index in range(display_count):
373
+ bounds = _get_display_bounds(sdl2, display_index)
374
+ if bounds is None:
375
+ continue
376
+ x, y, width, height = bounds
377
+ if x <= center_x < x + width and y <= center_y < y + height:
378
+ return display_index
379
+ return None
380
+
381
+
382
+ def _window_center_from_position(window: object, position: tuple[int, int]) -> tuple[int, int]:
383
+ x, y = position
384
+ try:
385
+ width, height = window.size # type: ignore[attr-defined]
386
+ except Exception:
387
+ width, height = _fetch_window_size(None)
388
+ return x + width // 2, y + height // 2
389
+
390
+
391
+ def _get_display_count(sdl2: object) -> int | None:
392
+ candidate = getattr(sdl2, "get_num_video_displays", None)
393
+ if candidate is None:
394
+ candidate = getattr(getattr(sdl2, "video", None), "get_num_video_displays", None)
395
+ if candidate is None:
396
+ return None
397
+ try:
398
+ return int(candidate())
399
+ except Exception:
400
+ return None
401
+
402
+
403
+ def _get_display_bounds(sdl2: object, display_index: int) -> tuple[int, int, int, int] | None:
404
+ candidate = getattr(sdl2, "get_display_bounds", None)
405
+ if candidate is None:
406
+ candidate = getattr(getattr(sdl2, "video", None), "get_display_bounds", None)
407
+ if candidate is None:
408
+ return None
409
+ try:
410
+ bounds = candidate(display_index)
411
+ except Exception:
412
+ return None
413
+ if bounds is None:
414
+ return None
415
+ if hasattr(bounds, "x"):
416
+ return (int(bounds.x), int(bounds.y), int(bounds.w), int(bounds.h))
417
+ if isinstance(bounds, (tuple, list)) and len(bounds) >= 4:
418
+ return (int(bounds[0]), int(bounds[1]), int(bounds[2]), int(bounds[3]))
419
+ return None
420
+
421
+
422
+ def _set_sdl2_fullscreen(enable: bool, display_index: int | None) -> bool:
423
+ try:
424
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
425
+ except Exception:
426
+ return False
427
+ try:
428
+ window = sdl2.Window.from_display_module()
429
+ except Exception:
430
+ return False
431
+
432
+ if enable and display_index is not None:
433
+ setter = getattr(window, "set_display_index", None)
434
+ if setter is not None:
435
+ try:
436
+ setter(display_index)
437
+ except Exception:
438
+ pass
439
+
440
+ if hasattr(window, "fullscreen"):
441
+ try:
442
+ window.fullscreen = enable
443
+ return True
444
+ except Exception:
445
+ pass
446
+
447
+ setter = getattr(window, "set_fullscreen", None)
448
+ if setter is None:
449
+ return False
450
+ try:
451
+ setter(enable)
452
+ return True
453
+ except Exception:
454
+ pass
455
+
456
+ if enable:
457
+ for attr_name in ("WINDOW_FULLSCREEN_DESKTOP", "FULLSCREEN_DESKTOP", "WINDOW_FULLSCREEN"):
458
+ mode = getattr(sdl2, attr_name, None)
459
+ if mode is None:
460
+ continue
461
+ try:
462
+ setter(mode)
463
+ return True
464
+ except Exception:
465
+ continue
466
+ return False
467
+
468
+
469
+ def _update_window_size(size: tuple[int, int], *, source: str) -> None:
470
+ global current_window_size, last_logged_window_size
471
+ current_window_size = size
472
+ if size != last_logged_window_size:
473
+ print(f"WINDOW_SIZE {size[0]}x{size[1]}")
474
+ last_logged_window_size = size
475
+
476
+
477
+ def _update_window_caption() -> None:
478
+ pygame.display.set_caption("Zombie Escape")
479
+
480
+
481
+ def _maximize_window() -> None:
482
+ try:
483
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
484
+ except Exception:
485
+ return
486
+ try:
487
+ window = sdl2.Window.from_display_module()
488
+ except Exception:
489
+ return
490
+ try:
491
+ window.maximize()
492
+ except Exception:
493
+ return
494
+
495
+
496
+ def _restore_window() -> None:
497
+ try:
498
+ from pygame import _sdl2 as sdl2 # type: ignore[import-not-found]
499
+ except Exception:
500
+ return
501
+ try:
502
+ window = sdl2.Window.from_display_module()
503
+ except Exception:
504
+ return
505
+ try:
506
+ window.restore()
507
+ except Exception:
508
+ return
@@ -5,6 +5,7 @@ from typing import Iterable, TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from .entities import Wall
8
+ from .models import LevelLayout
8
9
 
9
10
 
10
11
  WallIndex = dict[tuple[int, int], list["Wall"]]
@@ -54,23 +55,24 @@ def walls_for_radius(
54
55
  return candidates
55
56
 
56
57
 
57
- def apply_tile_edge_nudge(
58
+ def apply_cell_edge_nudge(
58
59
  x: float,
59
60
  y: float,
60
61
  dx: float,
61
62
  dy: float,
62
63
  *,
64
+ layout: LevelLayout,
63
65
  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
66
  strength: float = 0.03,
69
67
  edge_margin_ratio: float = 0.15,
70
68
  min_margin: float = 2.0,
71
69
  ) -> tuple[float, float]:
72
70
  if dx == 0 and dy == 0:
73
71
  return dx, dy
72
+ wall_cells = layout.wall_cells
73
+ bevel_corners = layout.bevel_corners
74
+ grid_cols = layout.grid_cols
75
+ grid_rows = layout.grid_rows
74
76
  if cell_size <= 0 or not wall_cells:
75
77
  return dx, dy
76
78
  cell_x = int(x // cell_size)
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import os
4
5
  import sys
5
6
  import traceback # For error reporting
6
7
  from pathlib import Path
@@ -19,7 +20,7 @@ from .entities_constants import (
19
20
  SURVIVOR_MIN_SPEED_FACTOR,
20
21
  )
21
22
  from .gameplay import calculate_car_speed_for_passengers
22
- from .level_constants import DEFAULT_TILE_SIZE
23
+ from .level_constants import DEFAULT_CELL_SIZE
23
24
  from .localization import set_language
24
25
  from .models import Stage
25
26
  from .render_constants import RenderAssets, build_render_assets
@@ -29,7 +30,13 @@ from .screen_constants import (
29
30
  SCREEN_HEIGHT,
30
31
  SCREEN_WIDTH,
31
32
  )
32
- from .screens import ScreenID, ScreenTransition, apply_window_scale
33
+ from .screens import ScreenID, ScreenTransition
34
+ from .windowing import (
35
+ adjust_menu_logical_size,
36
+ apply_window_scale,
37
+ prime_scaled_logical_size,
38
+ set_scaled_logical_size,
39
+ )
33
40
  from .screens.game_over import game_over_screen
34
41
  from .screens.settings import settings_screen
35
42
  from .screens.title import MAX_SEED_DIGITS, title_screen
@@ -92,6 +99,7 @@ def main() -> None:
92
99
  args, remaining = _parse_cli_args(sys.argv[1:])
93
100
  sys.argv = [sys.argv[0]] + remaining
94
101
 
102
+ os.environ.setdefault("SDL_RENDER_SCALE_QUALITY", "0")
95
103
  pygame.init()
96
104
  pygame.joystick.init()
97
105
  if hasattr(pygame, "controller"):
@@ -104,9 +112,11 @@ def main() -> None:
104
112
 
105
113
  from .screens.gameplay import gameplay_screen
106
114
 
115
+ prime_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT))
107
116
  apply_window_scale(DEFAULT_WINDOW_SCALE)
108
117
  pygame.mouse.set_visible(True)
109
- screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
118
+ logical_screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
119
+ menu_screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
110
120
  clock = pygame.time.Clock()
111
121
 
112
122
  debug_mode = bool(args.debug)
@@ -119,7 +129,7 @@ def main() -> None:
119
129
  from .export_images import export_images
120
130
 
121
131
  output_dir = Path.cwd() / "imgs" / "exports"
122
- saved = export_images(output_dir, cell_size=DEFAULT_TILE_SIZE)
132
+ saved = export_images(output_dir, cell_size=DEFAULT_CELL_SIZE)
123
133
  print(f"Exported {len(saved)} images to {output_dir}")
124
134
  pygame.quit()
125
135
  return
@@ -182,15 +192,16 @@ def main() -> None:
182
192
  transition = None
183
193
 
184
194
  if next_screen == ScreenID.TITLE:
195
+ adjust_menu_logical_size()
185
196
  seed_input = None if title_seed_is_auto else title_seed_text
186
197
  transition = title_screen(
187
- screen,
198
+ menu_screen,
188
199
  clock,
189
200
  config,
190
201
  FPS,
191
202
  stages=STAGES,
192
203
  default_stage_id=last_stage_id or DEFAULT_STAGE_ID,
193
- screen_size=(SCREEN_WIDTH, SCREEN_HEIGHT),
204
+ screen_size=menu_screen.get_size(),
194
205
  seed_text=seed_input,
195
206
  seed_is_auto=title_seed_is_auto,
196
207
  )
@@ -198,28 +209,30 @@ def main() -> None:
198
209
  title_seed_text = transition.seed_text
199
210
  title_seed_is_auto = transition.seed_is_auto
200
211
  elif next_screen == ScreenID.SETTINGS:
212
+ adjust_menu_logical_size()
201
213
  config = settings_screen(
202
- screen,
214
+ menu_screen,
203
215
  clock,
204
216
  config,
205
217
  FPS,
206
218
  config_path=config_path,
207
- screen_size=(SCREEN_WIDTH, SCREEN_HEIGHT),
219
+ screen_size=menu_screen.get_size(),
208
220
  )
209
221
  set_language(config.get("language"))
210
222
  transition = ScreenTransition(ScreenID.TITLE)
211
223
  elif next_screen == ScreenID.GAMEPLAY:
224
+ set_scaled_logical_size((SCREEN_WIDTH, SCREEN_HEIGHT))
212
225
  stage = incoming.stage
213
226
  seed_value = incoming.seed
214
227
  if stage is None:
215
228
  transition = ScreenTransition(ScreenID.TITLE)
216
229
  else:
217
230
  last_stage_id = stage.id
218
- render_assets = build_render_assets(stage.tile_size)
231
+ render_assets = build_render_assets(stage.cell_size)
219
232
  try:
220
233
  gs = _profiled_gameplay_screen if args.profile else gameplay_screen
221
234
  transition = gs(
222
- screen,
235
+ logical_screen,
223
236
  clock,
224
237
  config,
225
238
  FPS,
@@ -246,11 +259,11 @@ def main() -> None:
246
259
  if game_data is not None:
247
260
  render_assets = build_render_assets(game_data.cell_size)
248
261
  elif stage is not None:
249
- render_assets = build_render_assets(stage.tile_size)
262
+ render_assets = build_render_assets(stage.cell_size)
250
263
  else:
251
- render_assets = build_render_assets(DEFAULT_TILE_SIZE)
264
+ render_assets = build_render_assets(DEFAULT_CELL_SIZE)
252
265
  transition = game_over_screen(
253
- screen,
266
+ logical_screen,
254
267
  clock,
255
268
  config_payload,
256
269
  FPS,