Modernize: Python 2 + pygame 1.9.3 → Python 3.12+ + pygame-ce#1
Merged
Conversation
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
World· typed entities · dataclassesplatform.pyshadowing stdlibsrc/exit_dashpygame==1.9.3line in a txt fileFaithfully preserved (locked by golden tests)
Collision/jump feel,
.datlevel 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)pygame.sprite.Sprite— the move-the-world camera + bespoke rendering never useGroup.draw, so the Sprite base was pure friction.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