zombie-escape 1.2.1__tar.gz → 1.3.1__tar.gz
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-1.2.1 → zombie_escape-1.3.1}/PKG-INFO +5 -1
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/README.md +4 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/__about__.py +1 -1
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/entities.py +16 -14
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/gameplay/logic.py +22 -21
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/level_blueprints.py +9 -7
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/locales/ui.en.json +6 -2
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/locales/ui.ja.json +6 -2
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/models.py +1 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/render.py +9 -1
- zombie_escape-1.3.1/src/zombie_escape/rng.py +132 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/screens/__init__.py +3 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/screens/game_over.py +15 -1
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/screens/gameplay.py +6 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/screens/title.py +61 -5
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/zombie_escape.py +40 -3
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/.gitignore +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/LICENSE.txt +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/pyproject.toml +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/__init__.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/assets/fonts/misaki_gothic.ttf +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/colors.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/config.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/constants.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/font_utils.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/gameplay/__init__.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/localization.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/render_assets.py +0 -0
- {zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/screens/settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zombie-escape
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
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>
|
|
@@ -59,6 +59,10 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
59
59
|
- **Flashlight pickups:** Enable flashlight spawns that expand your visible radius when collected.
|
|
60
60
|
- **Steel beams:** Adds tougher single-cell obstacles (5% density) that block movement; hidden when stacked with an inner wall until that wall is destroyed.
|
|
61
61
|
|
|
62
|
+
### Shared Seeds
|
|
63
|
+
|
|
64
|
+
The title screen also lets you enter a numeric **seed**. Type digits (or pass `--seed <number>` on the CLI) to lock the procedural layout, wall placement, and pickups; share that seed with a friend and you will both play the exact same stage even on different machines. The current seed is shown at the bottom right of the title screen and in-game HUD. Backspace reverts to an automatically generated value so you can quickly roll a fresh challenge.
|
|
65
|
+
|
|
62
66
|
## Game Rules
|
|
63
67
|
|
|
64
68
|
### Stages
|
|
@@ -38,6 +38,10 @@ Open **Settings** from the title to toggle gameplay assists:
|
|
|
38
38
|
- **Flashlight pickups:** Enable flashlight spawns that expand your visible radius when collected.
|
|
39
39
|
- **Steel beams:** Adds tougher single-cell obstacles (5% density) that block movement; hidden when stacked with an inner wall until that wall is destroyed.
|
|
40
40
|
|
|
41
|
+
### Shared Seeds
|
|
42
|
+
|
|
43
|
+
The title screen also lets you enter a numeric **seed**. Type digits (or pass `--seed <number>` on the CLI) to lock the procedural layout, wall placement, and pickups; share that seed with a friend and you will both play the exact same stage even on different machines. The current seed is shown at the bottom right of the title screen and in-game HUD. Backspace reverts to an automatically generated value so you can quickly roll a fresh challenge.
|
|
44
|
+
|
|
41
45
|
## Game Rules
|
|
42
46
|
|
|
43
47
|
### Stages
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import math
|
|
6
|
-
import random
|
|
7
6
|
from enum import Enum
|
|
8
7
|
from typing import Callable, Iterable, Self
|
|
9
8
|
|
|
@@ -55,6 +54,9 @@ from .constants import (
|
|
|
55
54
|
ZOMBIE_SIGHT_RANGE,
|
|
56
55
|
ZOMBIE_SPEED,
|
|
57
56
|
)
|
|
57
|
+
from .rng import get_rng
|
|
58
|
+
|
|
59
|
+
RNG = get_rng()
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
# --- Camera Class ---
|
|
@@ -357,16 +359,16 @@ class Survivor(pygame.sprite.Sprite):
|
|
|
357
359
|
|
|
358
360
|
|
|
359
361
|
def random_position_outside_building() -> tuple[int, int]:
|
|
360
|
-
side =
|
|
362
|
+
side = RNG.choice(["top", "bottom", "left", "right"])
|
|
361
363
|
margin = 0
|
|
362
364
|
if side == "top":
|
|
363
|
-
x, y =
|
|
365
|
+
x, y = RNG.randint(0, LEVEL_WIDTH), -margin
|
|
364
366
|
elif side == "bottom":
|
|
365
|
-
x, y =
|
|
367
|
+
x, y = RNG.randint(0, LEVEL_WIDTH), LEVEL_HEIGHT + margin
|
|
366
368
|
elif side == "left":
|
|
367
|
-
x, y = -margin,
|
|
369
|
+
x, y = -margin, RNG.randint(0, LEVEL_HEIGHT)
|
|
368
370
|
else:
|
|
369
|
-
x, y = LEVEL_WIDTH + margin,
|
|
371
|
+
x, y = LEVEL_WIDTH + margin, RNG.randint(0, LEVEL_HEIGHT)
|
|
370
372
|
return x, y
|
|
371
373
|
|
|
372
374
|
|
|
@@ -398,12 +400,12 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
398
400
|
if speed > ZOMBIE_SPEED
|
|
399
401
|
else NORMAL_ZOMBIE_SPEED_JITTER
|
|
400
402
|
)
|
|
401
|
-
self.speed = speed +
|
|
403
|
+
self.speed = speed + RNG.uniform(-jitter, jitter)
|
|
402
404
|
self.x = float(self.rect.centerx)
|
|
403
405
|
self.y = float(self.rect.centery)
|
|
404
|
-
self.mode =
|
|
406
|
+
self.mode = RNG.choice(list(ZombieMode))
|
|
405
407
|
self.last_mode_change_time = pygame.time.get_ticks()
|
|
406
|
-
self.mode_change_interval = ZOMBIE_MODE_CHANGE_INTERVAL_MS +
|
|
408
|
+
self.mode_change_interval = ZOMBIE_MODE_CHANGE_INTERVAL_MS + RNG.randint(
|
|
407
409
|
-1000, 1000
|
|
408
410
|
)
|
|
409
411
|
self.was_in_sight = False
|
|
@@ -413,9 +415,9 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
413
415
|
self.mode = force_mode
|
|
414
416
|
else:
|
|
415
417
|
possible_modes = list(ZombieMode)
|
|
416
|
-
self.mode =
|
|
418
|
+
self.mode = RNG.choice(possible_modes)
|
|
417
419
|
self.last_mode_change_time = pygame.time.get_ticks()
|
|
418
|
-
self.mode_change_interval = ZOMBIE_MODE_CHANGE_INTERVAL_MS +
|
|
420
|
+
self.mode_change_interval = ZOMBIE_MODE_CHANGE_INTERVAL_MS + RNG.randint(
|
|
419
421
|
-1000, 1000
|
|
420
422
|
)
|
|
421
423
|
|
|
@@ -439,9 +441,9 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
439
441
|
* self.speed
|
|
440
442
|
* 0.8
|
|
441
443
|
)
|
|
442
|
-
move_y =
|
|
444
|
+
move_y = RNG.uniform(-self.speed * 0.6, self.speed * 0.6)
|
|
443
445
|
elif self.mode == ZombieMode.FLANK_Y:
|
|
444
|
-
move_x =
|
|
446
|
+
move_x = RNG.uniform(-self.speed * 0.6, self.speed * 0.6)
|
|
445
447
|
if dist > 0:
|
|
446
448
|
move_y = (
|
|
447
449
|
(dy_target / abs(dy_target) if dy_target != 0 else 0)
|
|
@@ -512,7 +514,7 @@ class Zombie(pygame.sprite.Sprite):
|
|
|
512
514
|
away_dy = next_y - closest.y
|
|
513
515
|
away_dist = math.hypot(away_dx, away_dy)
|
|
514
516
|
if away_dist == 0:
|
|
515
|
-
angle =
|
|
517
|
+
angle = RNG.uniform(0, 2 * math.pi)
|
|
516
518
|
away_dx, away_dy = math.cos(angle), math.sin(angle)
|
|
517
519
|
away_dist = 1
|
|
518
520
|
|
|
@@ -4,7 +4,6 @@ from bisect import bisect_left
|
|
|
4
4
|
from typing import Any, Mapping, Sequence
|
|
5
5
|
|
|
6
6
|
import math
|
|
7
|
-
import random
|
|
8
7
|
|
|
9
8
|
import pygame
|
|
10
9
|
|
|
@@ -56,6 +55,7 @@ from ..constants import (
|
|
|
56
55
|
from ..localization import translate as _
|
|
57
56
|
from ..level_blueprints import choose_blueprint
|
|
58
57
|
from ..models import Areas, GameData, Groups, ProgressState, Stage
|
|
58
|
+
from ..rng import get_rng
|
|
59
59
|
from ..entities import (
|
|
60
60
|
Camera,
|
|
61
61
|
Car,
|
|
@@ -70,6 +70,7 @@ from ..entities import (
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
LOGICAL_SCREEN_RECT = pygame.Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
|
|
73
|
+
RNG = get_rng()
|
|
73
74
|
|
|
74
75
|
__all__ = [
|
|
75
76
|
"create_zombie",
|
|
@@ -118,7 +119,7 @@ def create_zombie(
|
|
|
118
119
|
fast_conf = config.get("fast_zombies", {})
|
|
119
120
|
fast_enabled = fast_conf.get("enabled", True)
|
|
120
121
|
if fast_enabled:
|
|
121
|
-
base_speed =
|
|
122
|
+
base_speed = RNG.uniform(ZOMBIE_SPEED, FAST_ZOMBIE_BASE_SPEED)
|
|
122
123
|
else:
|
|
123
124
|
base_speed = ZOMBIE_SPEED
|
|
124
125
|
base_speed = min(base_speed, PLAYER_SPEED - 0.05)
|
|
@@ -263,7 +264,7 @@ def place_new_car(
|
|
|
263
264
|
|
|
264
265
|
max_attempts = 150
|
|
265
266
|
for attempt in range(max_attempts):
|
|
266
|
-
cell =
|
|
267
|
+
cell = RNG.choice(walkable_cells)
|
|
267
268
|
c_x, c_y = cell.center
|
|
268
269
|
temp_car = Car(c_x, c_y)
|
|
269
270
|
temp_rect = temp_car.rect.inflate(30, 30)
|
|
@@ -305,7 +306,7 @@ def place_fuel_can(
|
|
|
305
306
|
min_car_dist = 200
|
|
306
307
|
|
|
307
308
|
for attempt in range(200):
|
|
308
|
-
cell =
|
|
309
|
+
cell = RNG.choice(walkable_cells)
|
|
309
310
|
if (
|
|
310
311
|
math.hypot(cell.centerx - player.x, cell.centery - player.y)
|
|
311
312
|
< min_player_dist
|
|
@@ -325,7 +326,7 @@ def place_fuel_can(
|
|
|
325
326
|
return FuelCan(cell.centerx, cell.centery)
|
|
326
327
|
|
|
327
328
|
# Fallback: drop near a random walkable cell
|
|
328
|
-
cell =
|
|
329
|
+
cell = RNG.choice(walkable_cells)
|
|
329
330
|
return FuelCan(cell.centerx, cell.centery)
|
|
330
331
|
|
|
331
332
|
|
|
@@ -343,7 +344,7 @@ def place_flashlight(
|
|
|
343
344
|
min_car_dist = 200
|
|
344
345
|
|
|
345
346
|
for attempt in range(200):
|
|
346
|
-
cell =
|
|
347
|
+
cell = RNG.choice(walkable_cells)
|
|
347
348
|
if (
|
|
348
349
|
math.hypot(cell.centerx - player.x, cell.centery - player.y)
|
|
349
350
|
< min_player_dist
|
|
@@ -361,7 +362,7 @@ def place_flashlight(
|
|
|
361
362
|
continue
|
|
362
363
|
return Flashlight(cell.centerx, cell.centery)
|
|
363
364
|
|
|
364
|
-
cell =
|
|
365
|
+
cell = RNG.choice(walkable_cells)
|
|
365
366
|
return Flashlight(cell.centerx, cell.centery)
|
|
366
367
|
|
|
367
368
|
|
|
@@ -409,7 +410,7 @@ def place_companion(
|
|
|
409
410
|
min_car_dist = 180
|
|
410
411
|
|
|
411
412
|
for attempt in range(200):
|
|
412
|
-
cell =
|
|
413
|
+
cell = RNG.choice(walkable_cells)
|
|
413
414
|
if (
|
|
414
415
|
math.hypot(cell.centerx - player.x, cell.centery - player.y)
|
|
415
416
|
< min_player_dist
|
|
@@ -427,7 +428,7 @@ def place_companion(
|
|
|
427
428
|
continue
|
|
428
429
|
return Companion(cell.centerx, cell.centery)
|
|
429
430
|
|
|
430
|
-
cell =
|
|
431
|
+
cell = RNG.choice(walkable_cells)
|
|
431
432
|
return Companion(cell.centerx, cell.centery)
|
|
432
433
|
|
|
433
434
|
|
|
@@ -443,10 +444,10 @@ def scatter_positions_on_walkable(
|
|
|
443
444
|
|
|
444
445
|
clamped_rate = max(0.0, min(1.0, spawn_rate))
|
|
445
446
|
for cell in walkable_cells:
|
|
446
|
-
if
|
|
447
|
+
if RNG.random() >= clamped_rate:
|
|
447
448
|
continue
|
|
448
|
-
jitter_x =
|
|
449
|
-
jitter_y =
|
|
449
|
+
jitter_x = RNG.uniform(-cell.width * jitter_ratio, cell.width * jitter_ratio)
|
|
450
|
+
jitter_y = RNG.uniform(
|
|
450
451
|
-cell.height * jitter_ratio, cell.height * jitter_ratio
|
|
451
452
|
)
|
|
452
453
|
positions.append((int(cell.centerx + jitter_x), int(cell.centery + jitter_y)))
|
|
@@ -500,7 +501,7 @@ def update_survivors(game_data: GameData) -> None:
|
|
|
500
501
|
dy = point[1] - survivor.y
|
|
501
502
|
dist = math.hypot(dx, dy)
|
|
502
503
|
if dist == 0:
|
|
503
|
-
angle =
|
|
504
|
+
angle = RNG.uniform(0, math.tau)
|
|
504
505
|
dx, dy = math.cos(angle), math.sin(angle)
|
|
505
506
|
dist = 1
|
|
506
507
|
if dist < min_dist:
|
|
@@ -527,7 +528,7 @@ def update_survivors(game_data: GameData) -> None:
|
|
|
527
528
|
dy = other.y - survivor.y
|
|
528
529
|
dist = math.hypot(dx, dy)
|
|
529
530
|
if dist == 0:
|
|
530
|
-
angle =
|
|
531
|
+
angle = RNG.uniform(0, math.tau)
|
|
531
532
|
dx, dy = math.cos(angle), math.sin(angle)
|
|
532
533
|
dist = 1
|
|
533
534
|
if dist < survivor_overlap:
|
|
@@ -637,7 +638,6 @@ def log_waiting_car_count(game_data: GameData, *, force: bool = False) -> None:
|
|
|
637
638
|
current = len(game_data.waiting_cars)
|
|
638
639
|
if not force and current == game_data.last_logged_waiting_cars:
|
|
639
640
|
return
|
|
640
|
-
stage_id = getattr(game_data.stage, "id", "unknown")
|
|
641
641
|
game_data.last_logged_waiting_cars = current
|
|
642
642
|
|
|
643
643
|
|
|
@@ -662,7 +662,7 @@ def add_survivor_message(game_data: GameData, text: str) -> None:
|
|
|
662
662
|
def random_survivor_conversion_line() -> str:
|
|
663
663
|
if not SURVIVOR_CONVERSION_LINE_KEYS:
|
|
664
664
|
return ""
|
|
665
|
-
key =
|
|
665
|
+
key = RNG.choice(SURVIVOR_CONVERSION_LINE_KEYS)
|
|
666
666
|
return _(key)
|
|
667
667
|
|
|
668
668
|
|
|
@@ -685,8 +685,8 @@ def drop_survivors_from_car(game_data: GameData, origin: tuple[int, int]) -> Non
|
|
|
685
685
|
for survivor_idx in range(count):
|
|
686
686
|
placed = False
|
|
687
687
|
for attempt in range(6):
|
|
688
|
-
angle =
|
|
689
|
-
dist =
|
|
688
|
+
angle = RNG.uniform(0, math.tau)
|
|
689
|
+
dist = RNG.uniform(16, 40)
|
|
690
690
|
pos = (
|
|
691
691
|
origin[0] + math.cos(angle) * dist,
|
|
692
692
|
origin[1] + math.sin(angle) * dist,
|
|
@@ -878,6 +878,7 @@ def initialize_game_state(config: dict[str, Any], stage: Stage) -> GameData:
|
|
|
878
878
|
survivors_rescued=0,
|
|
879
879
|
survivor_messages=[],
|
|
880
880
|
survivor_capacity=SURVIVOR_MAX_SAFE_PASSENGERS,
|
|
881
|
+
seed=None,
|
|
881
882
|
)
|
|
882
883
|
|
|
883
884
|
# Create sprite groups
|
|
@@ -937,7 +938,7 @@ def setup_player_and_cars(
|
|
|
937
938
|
|
|
938
939
|
def pick_center(cells: list[pygame.Rect]) -> tuple[int, int]:
|
|
939
940
|
return (
|
|
940
|
-
|
|
941
|
+
RNG.choice(cells).center
|
|
941
942
|
if cells
|
|
942
943
|
else (LEVEL_WIDTH // 2, LEVEL_HEIGHT // 2)
|
|
943
944
|
)
|
|
@@ -952,7 +953,7 @@ def setup_player_and_cars(
|
|
|
952
953
|
"""Favor distant cells for the first car, otherwise fall back to random picks."""
|
|
953
954
|
if not car_candidates:
|
|
954
955
|
return (player_pos[0] + 200, player_pos[1])
|
|
955
|
-
|
|
956
|
+
RNG.shuffle(car_candidates)
|
|
956
957
|
for candidate in car_candidates:
|
|
957
958
|
if (
|
|
958
959
|
math.hypot(
|
|
@@ -1275,7 +1276,7 @@ def check_interactions(
|
|
|
1275
1276
|
state.game_over_at = state.game_over_at or pygame.time.get_ticks()
|
|
1276
1277
|
else:
|
|
1277
1278
|
if walkable_cells:
|
|
1278
|
-
new_cell =
|
|
1279
|
+
new_cell = RNG.choice(walkable_cells)
|
|
1279
1280
|
companion.teleport(new_cell.center)
|
|
1280
1281
|
else:
|
|
1281
1282
|
companion.teleport((LEVEL_WIDTH // 2, LEVEL_HEIGHT // 2))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Blueprint generator for randomized layouts.
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from .rng import get_rng
|
|
4
4
|
|
|
5
5
|
GRID_COLS = 48
|
|
6
6
|
GRID_ROWS = 30
|
|
@@ -12,6 +12,8 @@ WALL_MIN_LEN = 3
|
|
|
12
12
|
WALL_MAX_LEN = 10
|
|
13
13
|
SPAWN_MARGIN = 3 # keep spawns away from walls/edges
|
|
14
14
|
SPAWN_ZOMBIES = 3
|
|
15
|
+
|
|
16
|
+
RNG = get_rng()
|
|
15
17
|
STEEL_BEAM_CHANCE = 0.05
|
|
16
18
|
|
|
17
19
|
# Legend:
|
|
@@ -61,7 +63,7 @@ def _init_grid(cols: int, rows: int) -> list[list[str]]:
|
|
|
61
63
|
|
|
62
64
|
def _place_exits(grid: list[list[str]], exits_per_side: int) -> None:
|
|
63
65
|
cols, rows = len(grid[0]), len(grid)
|
|
64
|
-
rng =
|
|
66
|
+
rng = RNG.randint
|
|
65
67
|
used = set()
|
|
66
68
|
|
|
67
69
|
def pick_pos(side: str) -> tuple[int, int]:
|
|
@@ -87,13 +89,13 @@ def _place_exits(grid: list[list[str]], exits_per_side: int) -> None:
|
|
|
87
89
|
|
|
88
90
|
def _place_internal_walls(grid: list[list[str]]) -> None:
|
|
89
91
|
cols, rows = len(grid[0]), len(grid)
|
|
90
|
-
rng =
|
|
92
|
+
rng = RNG.randint
|
|
91
93
|
# Avoid placing walls adjacent to exits: collect forbidden cells (exits + neighbors)
|
|
92
94
|
forbidden = _collect_exit_adjacent_cells(grid)
|
|
93
95
|
|
|
94
96
|
for _ in range(NUM_WALL_LINES):
|
|
95
97
|
length = rng(WALL_MIN_LEN, WALL_MAX_LEN)
|
|
96
|
-
horizontal =
|
|
98
|
+
horizontal = RNG.choice([True, False])
|
|
97
99
|
if horizontal:
|
|
98
100
|
y = rng(2, rows - 3)
|
|
99
101
|
x = rng(2, cols - 2 - length)
|
|
@@ -125,7 +127,7 @@ def _place_steel_beams(
|
|
|
125
127
|
continue
|
|
126
128
|
if grid[y][x] not in (".", "1"):
|
|
127
129
|
continue
|
|
128
|
-
if
|
|
130
|
+
if RNG.random() < chance:
|
|
129
131
|
beams.add((x, y))
|
|
130
132
|
return beams
|
|
131
133
|
|
|
@@ -139,8 +141,8 @@ def _pick_empty_cell(
|
|
|
139
141
|
attempts = 0
|
|
140
142
|
while attempts < 2000:
|
|
141
143
|
attempts += 1
|
|
142
|
-
x =
|
|
143
|
-
y =
|
|
144
|
+
x = RNG.randint(margin, cols - margin - 1)
|
|
145
|
+
y = RNG.randint(margin, rows - margin - 1)
|
|
144
146
|
if grid[y][x] == "." and (x, y) not in forbidden:
|
|
145
147
|
return x, y
|
|
146
148
|
# Fallback: scan for any acceptable cell
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"settings": "Settings",
|
|
17
17
|
"quit": "Quit",
|
|
18
18
|
"locked_suffix": "[Locked]",
|
|
19
|
-
"window_hint": "Resize window: [ to shrink, ] to enlarge (menu only)"
|
|
19
|
+
"window_hint": "Resize window: [ to shrink, ] to enlarge (menu only)",
|
|
20
|
+
"seed_label": "Seed: %{value}",
|
|
21
|
+
"seed_hint": "Type 0-9 to set a custom seed, Backspace clears",
|
|
22
|
+
"seed_empty": "(auto)"
|
|
20
23
|
},
|
|
21
24
|
"stages": {
|
|
22
25
|
"stage1": {
|
|
@@ -47,7 +50,8 @@
|
|
|
47
50
|
"fast": "FastZ",
|
|
48
51
|
"car_hint": "CarHint",
|
|
49
52
|
"flashlight": "Flashlight",
|
|
50
|
-
"steel": "Steel"
|
|
53
|
+
"steel": "Steel",
|
|
54
|
+
"seed": "Seed %{value}"
|
|
51
55
|
},
|
|
52
56
|
"hud": {
|
|
53
57
|
"need_fuel": "Need fuel to drive!",
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"settings": "設定",
|
|
17
17
|
"quit": "終了",
|
|
18
18
|
"locked_suffix": "[Locked]",
|
|
19
|
-
"window_hint": "[キーでウィンドウを小さく、]キーで大きく(メニュー画面のみ)"
|
|
19
|
+
"window_hint": "[キーでウィンドウを小さく、]キーで大きく(メニュー画面のみ)",
|
|
20
|
+
"seed_label": "シード: %{value}",
|
|
21
|
+
"seed_hint": "数字キーで入力、BSでクリア",
|
|
22
|
+
"seed_empty": "(自動)"
|
|
20
23
|
},
|
|
21
24
|
"stages": {
|
|
22
25
|
"stage1": {
|
|
@@ -47,7 +50,8 @@
|
|
|
47
50
|
"fast": "高速ゾ",
|
|
48
51
|
"car_hint": "車ヒント",
|
|
49
52
|
"flashlight": "懐中電灯",
|
|
50
|
-
"steel": "鉄筋"
|
|
53
|
+
"steel": "鉄筋",
|
|
54
|
+
"seed": "シード %{value}"
|
|
51
55
|
},
|
|
52
56
|
"hud": {
|
|
53
57
|
"need_fuel": "燃料が必要です!",
|
|
@@ -280,6 +280,7 @@ def _draw_status_bar(
|
|
|
280
280
|
config: dict[str, Any],
|
|
281
281
|
*,
|
|
282
282
|
stage: Stage | None = None,
|
|
283
|
+
seed: int | None = None,
|
|
283
284
|
) -> None:
|
|
284
285
|
"""Render a compact status bar with current config flags and stage info."""
|
|
285
286
|
bar_rect = pygame.Rect(
|
|
@@ -327,6 +328,13 @@ def _draw_status_bar(
|
|
|
327
328
|
text_surface = font.render(status_text, False, color)
|
|
328
329
|
text_rect = text_surface.get_rect(left=12, centery=bar_rect.centery)
|
|
329
330
|
screen.blit(text_surface, text_rect)
|
|
331
|
+
if seed is not None:
|
|
332
|
+
seed_text = _("status.seed", value=str(seed))
|
|
333
|
+
seed_surface = font.render(seed_text, False, LIGHT_GRAY)
|
|
334
|
+
seed_rect = seed_surface.get_rect(
|
|
335
|
+
right=bar_rect.right - 12, centery=bar_rect.centery
|
|
336
|
+
)
|
|
337
|
+
screen.blit(seed_surface, seed_rect)
|
|
330
338
|
except pygame.error as e:
|
|
331
339
|
print(f"Error rendering status bar: {e}")
|
|
332
340
|
|
|
@@ -573,7 +581,7 @@ def draw(
|
|
|
573
581
|
screen.blit(msg_surface, msg_rect)
|
|
574
582
|
except pygame.error as e:
|
|
575
583
|
print(f"Error rendering survivor message: {e}")
|
|
576
|
-
_draw_status_bar(screen, assets, config, stage=stage)
|
|
584
|
+
_draw_status_bar(screen, assets, config, stage=stage, seed=state.seed)
|
|
577
585
|
if do_flip:
|
|
578
586
|
if present_fn:
|
|
579
587
|
present_fn(screen)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Deterministic random number helpers for reproducible runs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import secrets
|
|
6
|
+
from typing import MutableSequence, Sequence, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_seed() -> int:
|
|
12
|
+
"""Return a positive 63-bit seed for ad-hoc runs."""
|
|
13
|
+
return secrets.randbits(63) or 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeterministicRNG:
|
|
17
|
+
"""Mersenne Twister MT19937 implementation for deterministic runs."""
|
|
18
|
+
|
|
19
|
+
_N = 624
|
|
20
|
+
_M = 397
|
|
21
|
+
_MATRIX_A = 0x9908B0DF
|
|
22
|
+
_UPPER_MASK = 0x80000000
|
|
23
|
+
_LOWER_MASK = 0x7FFFFFFF
|
|
24
|
+
|
|
25
|
+
def __init__(self, seed: int | None = None) -> None:
|
|
26
|
+
self._state = [0] * self._N
|
|
27
|
+
self._index = self._N
|
|
28
|
+
self._seed_value: int | None = None
|
|
29
|
+
self.seed(seed)
|
|
30
|
+
|
|
31
|
+
def seed(self, value: int | None) -> None:
|
|
32
|
+
"""Seed using the MT19937 initialization routine."""
|
|
33
|
+
if value is None:
|
|
34
|
+
value = generate_seed()
|
|
35
|
+
try:
|
|
36
|
+
normalized = int(value)
|
|
37
|
+
except (TypeError, ValueError) as exc:
|
|
38
|
+
raise ValueError(f"Invalid seed value: {value}") from exc
|
|
39
|
+
self._seed_value = normalized
|
|
40
|
+
seed32 = normalized & 0xFFFFFFFF
|
|
41
|
+
if seed32 == 0:
|
|
42
|
+
seed32 = 5489 # default MT seed
|
|
43
|
+
self._state[0] = seed32
|
|
44
|
+
for i in range(1, self._N):
|
|
45
|
+
prev = self._state[i - 1]
|
|
46
|
+
self._state[i] = (
|
|
47
|
+
(1812433253 * (prev ^ (prev >> 30)) + i) & 0xFFFFFFFF
|
|
48
|
+
)
|
|
49
|
+
self._index = self._N
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def seed_value(self) -> int | None:
|
|
53
|
+
return self._seed_value
|
|
54
|
+
|
|
55
|
+
def random(self) -> float:
|
|
56
|
+
"""Return a float in the range [0.0, 1.0)."""
|
|
57
|
+
return self._next() / 4294967296.0 # 2**32
|
|
58
|
+
|
|
59
|
+
def randint(self, a: int, b: int) -> int:
|
|
60
|
+
if a > b:
|
|
61
|
+
raise ValueError("Lower bound must be <= upper bound for randint")
|
|
62
|
+
return a + self._randbelow(b - a + 1)
|
|
63
|
+
|
|
64
|
+
def choice(self, seq: Sequence[T]) -> T:
|
|
65
|
+
if not seq:
|
|
66
|
+
raise IndexError("Cannot choose from an empty sequence")
|
|
67
|
+
idx = self._randbelow(len(seq))
|
|
68
|
+
return seq[idx]
|
|
69
|
+
|
|
70
|
+
def shuffle(self, seq: MutableSequence[T]) -> None:
|
|
71
|
+
for i in range(len(seq) - 1, 0, -1):
|
|
72
|
+
j = self._randbelow(i + 1)
|
|
73
|
+
seq[i], seq[j] = seq[j], seq[i]
|
|
74
|
+
|
|
75
|
+
def uniform(self, a: float, b: float) -> float:
|
|
76
|
+
return a + (b - a) * self.random()
|
|
77
|
+
|
|
78
|
+
def _next(self) -> int:
|
|
79
|
+
return self._extract_number()
|
|
80
|
+
|
|
81
|
+
def _randbelow(self, bound: int) -> int:
|
|
82
|
+
if bound <= 0:
|
|
83
|
+
raise ValueError("Upper bound must be positive")
|
|
84
|
+
# Rejection sampling to avoid bias
|
|
85
|
+
limit = (1 << 32) - ((1 << 32) % bound)
|
|
86
|
+
while True:
|
|
87
|
+
value = self._next()
|
|
88
|
+
if value < limit:
|
|
89
|
+
return value % bound
|
|
90
|
+
|
|
91
|
+
def _extract_number(self) -> int:
|
|
92
|
+
if self._index >= self._N:
|
|
93
|
+
self._twist()
|
|
94
|
+
y = self._state[self._index]
|
|
95
|
+
self._index += 1
|
|
96
|
+
y ^= (y >> 11)
|
|
97
|
+
y ^= (y << 7) & 0x9D2C5680
|
|
98
|
+
y ^= (y << 15) & 0xEFC60000
|
|
99
|
+
y ^= (y >> 18)
|
|
100
|
+
return y & 0xFFFFFFFF
|
|
101
|
+
|
|
102
|
+
def _twist(self) -> None:
|
|
103
|
+
for i in range(self._N):
|
|
104
|
+
x = (self._state[i] & self._UPPER_MASK) + (
|
|
105
|
+
self._state[(i + 1) % self._N] & self._LOWER_MASK
|
|
106
|
+
)
|
|
107
|
+
xA = x >> 1
|
|
108
|
+
if x & 1:
|
|
109
|
+
xA ^= self._MATRIX_A
|
|
110
|
+
self._state[i] = self._state[(i + self._M) % self._N] ^ xA
|
|
111
|
+
self._index = 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
_GLOBAL_RNG = DeterministicRNG()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_rng() -> DeterministicRNG:
|
|
118
|
+
return _GLOBAL_RNG
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def seed_rng(seed: int | None) -> int:
|
|
122
|
+
_GLOBAL_RNG.seed(seed)
|
|
123
|
+
assert _GLOBAL_RNG.seed_value is not None
|
|
124
|
+
return _GLOBAL_RNG.seed_value
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
"DeterministicRNG",
|
|
129
|
+
"generate_seed",
|
|
130
|
+
"get_rng",
|
|
131
|
+
"seed_rng",
|
|
132
|
+
]
|
|
@@ -48,6 +48,9 @@ class ScreenTransition:
|
|
|
48
48
|
stage: Stage | None = None
|
|
49
49
|
game_data: GameData | None = None
|
|
50
50
|
config: dict | None = None
|
|
51
|
+
seed: int | None = None
|
|
52
|
+
seed_text: str | None = None
|
|
53
|
+
seed_is_auto: bool = False
|
|
51
54
|
|
|
52
55
|
|
|
53
56
|
current_window_scale = DEFAULT_WINDOW_SCALE # Applied to the OS window only
|
|
@@ -6,7 +6,8 @@ import pygame
|
|
|
6
6
|
from pygame import surface, time
|
|
7
7
|
|
|
8
8
|
from ..colors import BLACK, GREEN, LIGHT_GRAY, RED, WHITE
|
|
9
|
-
from ..
|
|
9
|
+
from ..font_utils import load_font
|
|
10
|
+
from ..localization import get_font_settings, translate as _
|
|
10
11
|
from ..models import GameData, Stage
|
|
11
12
|
from ..render import RenderAssets, draw_level_overview, show_message
|
|
12
13
|
from ..screens import ScreenID, ScreenTransition, present
|
|
@@ -119,6 +120,19 @@ def game_over_screen(
|
|
|
119
120
|
(screen_width // 2, screen_height // 2 + 24),
|
|
120
121
|
)
|
|
121
122
|
|
|
123
|
+
if state.seed is not None:
|
|
124
|
+
try:
|
|
125
|
+
font_settings = get_font_settings()
|
|
126
|
+
font = load_font(font_settings.resource, font_settings.scaled_size(11))
|
|
127
|
+
seed_text = _("status.seed", value=str(state.seed))
|
|
128
|
+
seed_surface = font.render(seed_text, False, LIGHT_GRAY)
|
|
129
|
+
seed_rect = seed_surface.get_rect(
|
|
130
|
+
right=screen_width - 14, bottom=screen_height - 12
|
|
131
|
+
)
|
|
132
|
+
screen.blit(seed_surface, seed_rect)
|
|
133
|
+
except pygame.error as exc:
|
|
134
|
+
print(f"Error rendering game-over seed text: {exc}")
|
|
135
|
+
|
|
122
136
|
present(screen)
|
|
123
137
|
clock.tick(fps)
|
|
124
138
|
|
|
@@ -15,6 +15,7 @@ from ..gameplay import logic
|
|
|
15
15
|
from ..localization import translate as _
|
|
16
16
|
from ..models import Stage
|
|
17
17
|
from ..render import draw, show_message
|
|
18
|
+
from ..rng import generate_seed, seed_rng
|
|
18
19
|
from ..screens import ScreenID, ScreenTransition, present
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
@@ -29,6 +30,7 @@ def gameplay_screen(
|
|
|
29
30
|
stage: Stage,
|
|
30
31
|
*,
|
|
31
32
|
show_pause_overlay: bool,
|
|
33
|
+
seed: int | None,
|
|
32
34
|
render_assets: "RenderAssets",
|
|
33
35
|
) -> ScreenTransition:
|
|
34
36
|
"""Main gameplay loop that returns the next screen transition."""
|
|
@@ -36,7 +38,11 @@ def gameplay_screen(
|
|
|
36
38
|
screen_width = screen.get_width()
|
|
37
39
|
screen_height = screen.get_height()
|
|
38
40
|
|
|
41
|
+
seed_value = seed if seed is not None else generate_seed()
|
|
42
|
+
applied_seed = seed_rng(seed_value)
|
|
43
|
+
|
|
39
44
|
game_data = logic.initialize_game_state(config, stage)
|
|
45
|
+
game_data.state.seed = applied_seed
|
|
40
46
|
paused_manual = False
|
|
41
47
|
paused_focus = False
|
|
42
48
|
last_fov_target = None
|
|
@@ -11,6 +11,15 @@ from ..localization import get_font_settings, translate as _
|
|
|
11
11
|
from ..models import Stage
|
|
12
12
|
from ..render import show_message
|
|
13
13
|
from ..screens import ScreenID, ScreenTransition, nudge_window_scale, present
|
|
14
|
+
from ..rng import generate_seed
|
|
15
|
+
|
|
16
|
+
MAX_SEED_DIGITS = 19
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _generate_auto_seed_text() -> str:
|
|
20
|
+
raw = generate_seed()
|
|
21
|
+
trimmed = raw // 100 # drop lower 2 digits for stability
|
|
22
|
+
return str(trimmed % 100000).zfill(5)
|
|
14
23
|
|
|
15
24
|
|
|
16
25
|
def title_screen(
|
|
@@ -22,6 +31,8 @@ def title_screen(
|
|
|
22
31
|
stages: Sequence[Stage],
|
|
23
32
|
default_stage_id: str,
|
|
24
33
|
screen_size: tuple[int, int],
|
|
34
|
+
seed_text: str | None = None,
|
|
35
|
+
seed_is_auto: bool = False,
|
|
25
36
|
) -> ScreenTransition:
|
|
26
37
|
"""Display the title menu and return the selected transition."""
|
|
27
38
|
|
|
@@ -40,12 +51,30 @@ def title_screen(
|
|
|
40
51
|
),
|
|
41
52
|
0,
|
|
42
53
|
)
|
|
54
|
+
generated = seed_text is None
|
|
55
|
+
current_seed_text = seed_text if seed_text is not None else _generate_auto_seed_text()
|
|
56
|
+
current_seed_auto = seed_is_auto or generated
|
|
43
57
|
|
|
44
58
|
while True:
|
|
45
59
|
for event in pygame.event.get():
|
|
46
60
|
if event.type == pygame.QUIT:
|
|
47
|
-
return ScreenTransition(
|
|
61
|
+
return ScreenTransition(
|
|
62
|
+
ScreenID.EXIT,
|
|
63
|
+
seed_text=current_seed_text,
|
|
64
|
+
seed_is_auto=current_seed_auto,
|
|
65
|
+
)
|
|
48
66
|
if event.type == pygame.KEYDOWN:
|
|
67
|
+
if event.key == pygame.K_BACKSPACE:
|
|
68
|
+
current_seed_text = _generate_auto_seed_text()
|
|
69
|
+
current_seed_auto = True
|
|
70
|
+
continue
|
|
71
|
+
if event.unicode and event.unicode.isdigit():
|
|
72
|
+
if current_seed_auto:
|
|
73
|
+
current_seed_text = ""
|
|
74
|
+
current_seed_auto = False
|
|
75
|
+
if len(current_seed_text) < MAX_SEED_DIGITS:
|
|
76
|
+
current_seed_text += event.unicode
|
|
77
|
+
continue
|
|
49
78
|
if event.key == pygame.K_LEFTBRACKET:
|
|
50
79
|
nudge_window_scale(0.5)
|
|
51
80
|
continue
|
|
@@ -59,13 +88,26 @@ def title_screen(
|
|
|
59
88
|
elif event.key in (pygame.K_RETURN, pygame.K_SPACE):
|
|
60
89
|
current = options[selected]
|
|
61
90
|
if current["type"] == "stage" and current.get("available"):
|
|
91
|
+
seed_value = int(current_seed_text) if current_seed_text else None
|
|
62
92
|
return ScreenTransition(
|
|
63
|
-
ScreenID.GAMEPLAY,
|
|
93
|
+
ScreenID.GAMEPLAY,
|
|
94
|
+
stage=current["stage"],
|
|
95
|
+
seed=seed_value,
|
|
96
|
+
seed_text=current_seed_text,
|
|
97
|
+
seed_is_auto=current_seed_auto,
|
|
64
98
|
)
|
|
65
99
|
if current["type"] == "settings":
|
|
66
|
-
return ScreenTransition(
|
|
100
|
+
return ScreenTransition(
|
|
101
|
+
ScreenID.SETTINGS,
|
|
102
|
+
seed_text=current_seed_text,
|
|
103
|
+
seed_is_auto=current_seed_auto,
|
|
104
|
+
)
|
|
67
105
|
if current["type"] == "quit":
|
|
68
|
-
return ScreenTransition(
|
|
106
|
+
return ScreenTransition(
|
|
107
|
+
ScreenID.EXIT,
|
|
108
|
+
seed_text=current_seed_text,
|
|
109
|
+
seed_is_auto=current_seed_auto,
|
|
110
|
+
)
|
|
69
111
|
|
|
70
112
|
screen.fill(BLACK)
|
|
71
113
|
show_message(
|
|
@@ -117,11 +159,25 @@ def title_screen(
|
|
|
117
159
|
desc_rect = desc_surface.get_rect(center=(width // 2, height // 2 + 74))
|
|
118
160
|
screen.blit(desc_surface, desc_rect)
|
|
119
161
|
|
|
162
|
+
seed_font = load_font(font_settings.resource, font_settings.scaled_size(12))
|
|
163
|
+
seed_value_display = (
|
|
164
|
+
current_seed_text if current_seed_text else _("menu.seed_empty")
|
|
165
|
+
)
|
|
166
|
+
seed_label = _("status.seed", value=seed_value_display)
|
|
167
|
+
seed_surface = seed_font.render(seed_label, False, LIGHT_GRAY)
|
|
168
|
+
seed_rect = seed_surface.get_rect(right=width - 14, bottom=height - 12)
|
|
169
|
+
screen.blit(seed_surface, seed_rect)
|
|
170
|
+
|
|
120
171
|
hint_font = load_font(font_settings.resource, font_settings.scaled_size(11))
|
|
121
172
|
hint_text = _("menu.window_hint")
|
|
122
173
|
hint_surface = hint_font.render(hint_text, False, LIGHT_GRAY)
|
|
123
|
-
hint_rect = hint_surface.get_rect(center=(width // 2, height -
|
|
174
|
+
hint_rect = hint_surface.get_rect(center=(width // 2, height - 60))
|
|
124
175
|
screen.blit(hint_surface, hint_rect)
|
|
176
|
+
|
|
177
|
+
seed_hint = _("menu.seed_hint")
|
|
178
|
+
seed_hint_surface = hint_font.render(seed_hint, False, GRAY)
|
|
179
|
+
seed_hint_rect = seed_hint_surface.get_rect(left=14, bottom=height - 12)
|
|
180
|
+
screen.blit(seed_hint_surface, seed_hint_rect)
|
|
125
181
|
except pygame.error as e:
|
|
126
182
|
print(f"Error rendering title screen: {e}")
|
|
127
183
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import argparse
|
|
3
4
|
import sys
|
|
4
5
|
import traceback # For error reporting
|
|
5
|
-
from typing import Any
|
|
6
|
+
from typing import Any, Tuple
|
|
6
7
|
|
|
7
8
|
import pygame
|
|
8
9
|
|
|
@@ -26,9 +27,26 @@ from .models import GameData, Stage, STAGES, DEFAULT_STAGE_ID
|
|
|
26
27
|
from .screens import ScreenID, ScreenTransition, apply_window_scale
|
|
27
28
|
from .screens.game_over import game_over_screen
|
|
28
29
|
from .screens.settings import settings_screen
|
|
29
|
-
from .screens.title import title_screen
|
|
30
|
+
from .screens.title import MAX_SEED_DIGITS, title_screen
|
|
30
31
|
from .gameplay.logic import calculate_car_speed_for_passengers
|
|
31
32
|
|
|
33
|
+
|
|
34
|
+
def _parse_cli_args(argv: list[str]) -> Tuple[argparse.Namespace, list[str]]:
|
|
35
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
36
|
+
parser.add_argument("--hide-pause-overlay", action="store_true")
|
|
37
|
+
parser.add_argument("--seed")
|
|
38
|
+
return parser.parse_known_args(argv)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sanitize_seed_text(raw: str | None) -> tuple[str | None, bool]:
|
|
42
|
+
if not raw:
|
|
43
|
+
return None, True
|
|
44
|
+
stripped = raw.strip()
|
|
45
|
+
if not stripped.isdigit():
|
|
46
|
+
print("Ignoring --seed value because it must contain only digits.")
|
|
47
|
+
return None, True
|
|
48
|
+
return stripped[:MAX_SEED_DIGITS], False
|
|
49
|
+
|
|
32
50
|
# Re-export the gameplay helpers constants for external callers/tests.
|
|
33
51
|
__all__ = [
|
|
34
52
|
"main",
|
|
@@ -41,6 +59,9 @@ __all__ = [
|
|
|
41
59
|
|
|
42
60
|
# --- Main Entry Point ---
|
|
43
61
|
def main() -> None:
|
|
62
|
+
args, remaining = _parse_cli_args(sys.argv[1:])
|
|
63
|
+
sys.argv = [sys.argv[0]] + remaining
|
|
64
|
+
|
|
44
65
|
pygame.init()
|
|
45
66
|
try:
|
|
46
67
|
pygame.font.init()
|
|
@@ -54,7 +75,9 @@ def main() -> None:
|
|
|
54
75
|
screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)).convert_alpha()
|
|
55
76
|
clock = pygame.time.Clock()
|
|
56
77
|
|
|
57
|
-
hide_pause_overlay =
|
|
78
|
+
hide_pause_overlay = bool(args.hide_pause_overlay)
|
|
79
|
+
cli_seed_text, cli_seed_is_auto = _sanitize_seed_text(args.seed)
|
|
80
|
+
title_seed_text, title_seed_is_auto = cli_seed_text, cli_seed_is_auto
|
|
58
81
|
|
|
59
82
|
config: dict[str, Any]
|
|
60
83
|
config, config_path = load_config()
|
|
@@ -66,12 +89,14 @@ def main() -> None:
|
|
|
66
89
|
pending_stage: Stage | None = None
|
|
67
90
|
pending_game_data: GameData | None = None
|
|
68
91
|
pending_config: dict[str, Any] | None = None
|
|
92
|
+
pending_seed: int | None = None
|
|
69
93
|
running = True
|
|
70
94
|
|
|
71
95
|
while running:
|
|
72
96
|
transition: ScreenTransition | None = None
|
|
73
97
|
|
|
74
98
|
if next_screen == ScreenID.TITLE:
|
|
99
|
+
seed_input = None if title_seed_is_auto else title_seed_text
|
|
75
100
|
transition = title_screen(
|
|
76
101
|
screen,
|
|
77
102
|
clock,
|
|
@@ -80,7 +105,12 @@ def main() -> None:
|
|
|
80
105
|
stages=STAGES,
|
|
81
106
|
default_stage_id=DEFAULT_STAGE_ID,
|
|
82
107
|
screen_size=(SCREEN_WIDTH, SCREEN_HEIGHT),
|
|
108
|
+
seed_text=seed_input,
|
|
109
|
+
seed_is_auto=title_seed_is_auto,
|
|
83
110
|
)
|
|
111
|
+
if transition.seed_text is not None:
|
|
112
|
+
title_seed_text = transition.seed_text
|
|
113
|
+
title_seed_is_auto = transition.seed_is_auto
|
|
84
114
|
elif next_screen == ScreenID.SETTINGS:
|
|
85
115
|
config = settings_screen(
|
|
86
116
|
screen,
|
|
@@ -95,6 +125,8 @@ def main() -> None:
|
|
|
95
125
|
elif next_screen == ScreenID.GAMEPLAY:
|
|
96
126
|
stage = pending_stage
|
|
97
127
|
pending_stage = None
|
|
128
|
+
seed_value = pending_seed
|
|
129
|
+
pending_seed = None
|
|
98
130
|
if stage is None:
|
|
99
131
|
transition = ScreenTransition(ScreenID.TITLE)
|
|
100
132
|
else:
|
|
@@ -106,6 +138,7 @@ def main() -> None:
|
|
|
106
138
|
FPS,
|
|
107
139
|
stage,
|
|
108
140
|
show_pause_overlay=not hide_pause_overlay,
|
|
141
|
+
seed=seed_value,
|
|
109
142
|
render_assets=RENDER_ASSETS,
|
|
110
143
|
)
|
|
111
144
|
except SystemExit:
|
|
@@ -143,6 +176,10 @@ def main() -> None:
|
|
|
143
176
|
pending_stage = transition.stage
|
|
144
177
|
pending_game_data = transition.game_data
|
|
145
178
|
pending_config = transition.config
|
|
179
|
+
pending_seed = transition.seed
|
|
180
|
+
if transition.next_screen == ScreenID.GAMEPLAY:
|
|
181
|
+
title_seed_text = cli_seed_text
|
|
182
|
+
title_seed_is_auto = cli_seed_is_auto
|
|
146
183
|
next_screen = transition.next_screen
|
|
147
184
|
|
|
148
185
|
pygame.quit() # Quit pygame only once at the very end of main
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/assets/fonts/Silkscreen-Regular.ttf
RENAMED
|
File without changes
|
{zombie_escape-1.2.1 → zombie_escape-1.3.1}/src/zombie_escape/assets/fonts/misaki_gothic.ttf
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|