Skip to content

Modernize: Python 2 + pygame 1.9.3 → Python 3.12+ + pygame-ce#1

Merged
kennethsinder merged 8 commits into
masterfrom
modernize-py3-pygame-ce
Jun 14, 2026
Merged

Modernize: Python 2 + pygame 1.9.3 → Python 3.12+ + pygame-ce#1
kennethsinder merged 8 commits into
masterfrom
modernize-py3-pygame-ce

Conversation

@kennethsinder

Copy link
Copy Markdown
Owner

Brings the 2014 Python 2 / pygame 1.9.3 game up to a modern, cross-platform, tested codebase. Eight phased commits (each green on its own).

What changed

Before After
Language Python 2 (won't run) Python 3.12–3.14, fully typed
Engine pygame 1.9.3 (uninstallable) pygame-ce 2.5.7
Architecture one ~1,600-line god-class scene stack · fixed-timestep loop · World · typed entities · dataclasses
Assets 256 MB, ~50% junk 9 MB (music→OGG, title→JPEG, pruned, recompressed)
Dead code 2 duplicate trees + a platform.py shadowing stdlib deleted; single src/exit_dash
Tooling a pygame==1.9.3 line in a txt file uv · ruff · mypy · pytest · pre-commit · 3×3 GitHub Actions CI
Tests none 76, all headless
Cross-platform broke outside repo root package-anchored assets (verified an installed wheel runs from any dir)

Faithfully preserved (locked by golden tests)

Collision/jump feel, .dat level format, per-character stats, move-the-world camera, autoscroll, torch/darkness, and the procedural generator's RNG call order.

Deliberate calls (documented in CLAUDE.md)

  • Entities are plain typed classes, not pygame.sprite.Sprite — the move-the-world camera + bespoke rendering never use Group.draw, so the Sprite base was pure friction.
  • Fixed-timestep, not naive delta-time — same frame-rate independence without the collision tunnelling raw dt-scaling would cause.

Verification

uv run pytest (76 pass) · ruff check/ruff format --check · mypy src · headless smoke run · installed-wheel run from a temp dir.

🤖 Generated with Claude Code

kennethsinder and others added 8 commits June 14, 2026 09:42
- Add pyproject.toml (PEP 621): Python 3.12+, pygame-ce, uv, ruff, mypy, pytest
- Add CI matrix (3 OS x py3.12/3.13/3.14), pre-commit, .editorconfig, .gitattributes
- Scaffold src/exit_dash engine skeleton: fixed-timestep app loop, scene stack,
  input snapshot, package-anchored asset/config paths
- Delete dead duplicate code tree (MainGame.py + top-level module dupes) that was
  shadowing the stdlib `platform` module; untrack .idea/
- Normalize line endings to LF (fixes mixed CRLF/LF level data)
- Initialize CLAUDE.md, AGENTS.md, CREDITS.md; rewrite README for the modern setup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Port legacy data/ tree to Python 3 (print(), range, package-relative imports,
  missing os import) so it imports cleanly as the behavioral reference
- Move all game assets into src/exit_dash/assets/ (package-anchored, ships in wheel)
- New typed IO layer in src/exit_dash:
  * core/settings.py  - Settings dataclass, JSON persistence, legacy .cfg migration
  * world/level.py + world/loader.py - LevelData records + robust .dat reader/writer
    (normalizes line endings, -999 sentinels, semantic round-trip)
  * world/hints.py + core/keybindings.py - {TOKEN} hint interpolation
- Headless pytest suite (44 tests): settings round-trip/migration, level
  load/round-trip/known-values/line-endings, hints, case-sensitive asset-existence,
  engine smoke test
- Exclude legacy data/ from ruff (Python-2 reference; deleted at cutover)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add scripts/asset_manifest.py: auditable referenced-vs-orphan report
  (static literals + dynamic filename families, basename match = conservative)
- Delete unused/ and crossover/ asset dirs, all .bmp and .svg source files
- Music: keep 4 curated tracks, convert MP3/WAV -> OGG (Vorbis q5); 102MB -> 3.1MB
- Backgrounds: keep 5 referenced + small theme tiles; convert 7.3MB title PNG ->
  1.5MB JPEG; 29MB -> 7.4MB
- character/: drop spritesheet masters, alt-colour sheets, preview/sample; 14MB -> 0.2MB
- Lossless recompress survivors (oxipng, jpegoptim)
- Add tests/test_audio.py: curated music + SFX load in pygame (skips without a mixer)

All deletions are recoverable via git history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Character: physics + 6 collision methods ported verbatim (numerically) into clean
  typed code; fixed-timestep, animation/flash keyed to the fixed rate
- PlayableCharacter (PlayerInput snapshot input, move-the-world camera, HUD, respawn,
  autoscroll), AICharacter (slime/snail/fly/fish AI, quirks preserved), and all world
  objects: Platform, Block, Pool, FallingSpike, Door, Key, Checkpoint, Torch,
  BackgroundFoliage, Background
- core/resources.py: cached image/font/sound loader with magenta-placeholder fallback
  for missing assets; core/input.py PlayerInput decouples player from raw pygame input
- Golden physics tests (jump apex, terminal velocity, no-tunnel landing) + a
  construct-every-entity headless test -> 64 tests total
- Fix a latent infinite loop in the background scale2x path

Decision: entities are plain typed classes, not pygame.sprite.Sprite. The move-the-world
camera + bespoke per-entity rendering never use Group.draw/update, so the Sprite base
only added friction (Optional image/rect, update()/alive() name clashes). The World will
hold typed lists and the scene draws in explicit layer order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- world/world.py: World holds every level entity collection and ports the shared
  assembly helpers (gather_platform_info, generate_mobs/fences/foliage/torches,
  finalize_blocks/level, decide_dark, darken/illuminate)
- World.from_level_data builds a level from a parsed .dat (the load path); World.generate
  ports generateRandomLevel (procedural path), reusing the helpers
- Randomness flows through an injected random.Random so generated levels are
  reproducible from a seed and tests are deterministic
- Add PlayableCharacter.set_map_obj_x (camera world-shift) to complete the camera API
- Tests: build all 4 shipped levels, load determinism, generator reproducibility (74 tests)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- scenes/level.py: LevelScene plays a level — per-frame update advances the simulation
  in the original order (enemies, icicles, then player+camera scroll) and draw renders
  in the original blit order (background -> ... -> player+HUD -> keys), plus the level/hint
  banner and torch darkness
- Reaching the door advances to the next level (loads shipped 1-4, generates beyond);
  running out of lives ends the run
- Split Door/Key into update (logic) + draw (render) now that entities are plain classes
- __main__ launches LevelScene with loaded settings (+ --level / --character flags);
  headless --frames run now exercises the full game loop
- Application.quit() clears the asset cache (assets are pygame-session-bound)
- Tests: LevelScene boots and runs headless for levels 1-2 without the player dying

The game is now runnable: uv run exit-dash-hyperion --windowed

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- scenes/title.py: TitleScene with title background, character select (1/2/3), controls
  hint; starts the chosen level. scenes/gameover.py: win/lose screen, any key -> title
- LevelScene now transitions to GameOverScene (won on finishing the final level, lost on
  death) instead of quitting; final_level configurable
- __main__ starts at the title screen for normal play; headless runs go straight into a
  level so CI exercises the gameplay loop
- Tests: title->level (per character), gameover->title, esc->quit, level-win->gameover

Deferred as future work (game is fully playable without them): the in-game level editor
and the options menu. Tracked in CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Delete the legacy Python-2 data/ package, RunGame.py and settings.cfg now that
  src/exit_dash is at parity and the behavior is captured by tests
- Drop the ruff exclude that exempted the legacy tree
- Update CLAUDE.md / README to the finished state; document the two deferred original
  features (options menu, level editor) as good next tasks
- Verified: ruff + ruff-format + mypy + pytest (76) all green; headless run works; and a
  built wheel installs in an isolated env and runs from a different directory (proving
  package-anchored asset loading, no CWD dependency)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kennethsinder
kennethsinder merged commit 1c73292 into master Jun 14, 2026
9 checks passed
@kennethsinder
kennethsinder deleted the modernize-py3-pygame-ce branch June 14, 2026 18:42
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