Skip to content

fix: make generate_mobs hang-proof for any level data#3

Merged
kennethsinder merged 1 commit into
masterfrom
fix/generate-mobs-hangproof
Jun 14, 2026
Merged

fix: make generate_mobs hang-proof for any level data#3
kennethsinder merged 1 commit into
masterfrom
fix/generate-mobs-hangproof

Conversation

@kennethsinder

Copy link
Copy Markdown
Owner

What

The mob-placement loop in World.generate_mobs rejection-sampled a non-spawn platform whose x differed from the longest platform's, looping until it found one:

which = self.rng.randint(1, len(self.platforms) - 1)
while self.platforms[which][0] == longest[0]:
    which = self.rng.randint(1, len(self.platforms) - 1)

If every non-spawn platform shares the longest's x (e.g. a hand-built level whose platforms all stack at one x), no roll can ever satisfy the exit condition and the build hangs forever — an unrecoverable freeze, not a catchable crash.

Until now this was only guarded at the editor boundary (EditorModel.playability refused to test-play such a level). That left World.from_level_data itself unsafe for every other caller (tests, future code, a hand-edited .dat). This fixes it structurally in the engine so the editor guard is no longer load-bearing for safety.

Changes

  • generate_mobs is now provably terminating for any input. It pre-computes whether any eligible (differently-placed) non-spawn platform exists and places no ground mobs when none do, instead of spinning. When eligible platforms exist it consumes RNG identically to before — so procedurally-generated levels stay bit-for-bit reproducible (the determinism tests confirm this).
  • from_level_data fails legibly. <2 platforms now raises a clear ValueError instead of an opaque IndexError deep inside gather_platform_info (which seeds its extremes from platforms[1]).
  • Editor guard relaxed + made honest. With the hang impossible in the engine, EditorModel.playability drops the distinct-x rule and just enforces the real floor (≥2 platforms). Levels previously rejected on safety grounds are now allowed.

Tests

  • test_degenerate_level_does_not_hang — builds the exact all-same-x level that used to hang, on a watchdog thread (so a regression fails loudly rather than freezing the whole suite, since pytest-timeout isn't a dep) and asserts only the patrol fly is placed.
  • test_single_platform_level_raises — the ValueError floor.
  • Editor playability tests updated to the relaxed contract.

98 passed locally; ruff / ruff format / mypy clean; headless --frames 120 smoke exit 0.

🤖 Generated with Claude Code

The mob-placement loop in World.generate_mobs rejection-sampled a non-spawn
platform whose x differed from the longest platform's, looping until it found
one. If every non-spawn platform shared the longest's x (e.g. a hand-built
level whose platforms all stack at one x), no roll could ever satisfy the exit
condition and the build hung forever — an unrecoverable freeze.

Previously this was only guarded at the editor boundary (EditorModel.playability
refused to test-play such levels). That left World.from_level_data itself unsafe
for any other caller. Fix it structurally in the engine instead:

- generate_mobs pre-checks whether any eligible platform exists and places no
  ground mobs when none do, so the loop is provably terminating for any input.
  When eligible platforms exist it consumes RNG identically to before, so
  procedurally-generated levels stay bit-for-bit reproducible.
- from_level_data now raises a clear ValueError for <2 platforms instead of an
  opaque IndexError deep in gather_platform_info (which seeds from platforms[1]).
- With the hang impossible in the engine, EditorModel.playability drops the
  distinct-x rule and just enforces the real floor (>=2 platforms); levels that
  used to be rejected on safety grounds are now allowed.

Tests: engine-level no-hang regression (watchdog thread, so a regression fails
loudly rather than freezing the suite) + single-platform ValueError; editor
playability tests updated to match the relaxed, honest contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kennethsinder
kennethsinder merged commit b790baa into master Jun 14, 2026
9 checks passed
@kennethsinder
kennethsinder deleted the fix/generate-mobs-hangproof branch June 14, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant