Fable completion#17
Merged
Merged
Conversation
…rcy rule - Fix critical bug where clear_inactive() ran before detecting which missiles had reached their targets, so ABM/ICBM arrivals never spawned explosions and cities were never actually destroyed. - Add attack pacing ($5791-style): ICBMs/smart bombs now launch automatically each wave, gated by the highest in-flight missile's altitude, in batches of ATTACK_BATCH_SIZE. - Add mercy-rule-aware target selection shared by ICBM spawns, MIRV children, and flier shots. - Wire MIRV split checks into the per-frame update using the altitude-scan approach described in the disassembly ($5379/$56d1). - Wire smart bomb evasion detection from live nearby explosions. - Add flier spawn/flight/fire lifecycle with per-wave cooldowns. - Add mercy-rule immediate wave-end check ($59fa). - Add DefenseManager.destroy_silo_at for ICBM impacts on silos. - Add tests covering all of the above.
- New Renderer (src/ui/renderer.py): draws sky/ground/cities/silos/ missiles/explosions/crosshair/HUD onto a native 256x231 surface, then integer-upscales it onto the window with pillarboxing for fullscreen/F11 (verified via screenshot at multiple aspect ratios). - New src/ui/palette.py: per-wave color palettes and 30Hz explosion/ flash color cycling. - Rewrite main.py to actually call the renderer every frame (it previously drew nothing during gameplay), add F11 fullscreen toggle, trackball-style relative-mouse crosshair control, A/S/D or 1/2/3 keyboard silo firing, left-click fire-nearest, wave-end tally display and auto-advance, and reset-to-attract after game over. - Bump default scale to 3x per spec; update tests accordingly and point the entry-point test at main.py instead of the retired missile-defense.py. - Fix a mercy-rule bug in Game._pick_targets: a single multi-target call (MIRV bursts) computed the "already targeted" city set once and reused it across all draws, so one MIRV split could target more distinct cities than the per-wave allowance permits.
…reen - Fix a significant gameplay bug: CityManager.start_wave() was fully restoring every destroyed city each wave, which defeats the core survival mechanic (the disassembly only restores silos per wave; destroyed cities persist until repaired by a banked bonus city). - Add CityManager.try_repair_craters(), spending banked bonus cities to patch craters immediately when both are available, called after every bonus-city check and at wave end. - Add get_score_multiplier(): 1x (waves 1-2) up to 6x (wave 11+), per the disassembly's every-other-wave schedule. Wire it into kill scoring (ICBM/smart bomb/flier) and the wave-end bonus. - Track last_wave_surviving_cities/last_wave_remaining_abms on Game for tally breakdown display. - Add a count-up tally screen in main.py: ticks through surviving cities + unfired ABMs one at a time with a (silent until M5) tally-tick sound cue, animating the displayed score toward the final total before the next wave starts. - Add tests for the multiplier schedule, city persistence across waves, crater repair, and the tally count-up/advance timing.
- New src/ui/synth.py: numpy waveform generators (sine/square/noise/ sweep + envelope shaping) and a synthesis recipe per sound event, matching the disassembly's sound-channel table ($78f1) -- ABM launch, explosion, silo-low warning, can't-fire buzzer, wave-start alarm, wave-end/tally chime, tally ticks, bonus city fanfare, game-over rumble, and looping flier drone / smart bomb warble. - Rewrite AudioManager: real .wav files in data/sfx/ (if present) still take priority, but every SoundEvent is now synthesized procedurally as a fallback via pygame.sndarray, so the game has full audio with zero shipped samples. Add start_loop/stop_loop for the two continuous effects. - Add SILO_LOW_THRESHOLD config constant for the "LOW" ammo warning. - Add tests for every synth recipe and the AudioManager loop API.
main.py had grown into the only file absorbing every new concern (input handling, audio-cue triggering, screen rendering, tally display) while everything else stayed put in src/. Move MissileCommandApp, parse_args, and the app-level constants into src/app.py so main.py goes back to being a thin CLI entry point. - Rename renderer._get_font to the public get_font since it's now used across module boundaries (src/app.py). - Split tests/test_main.py accordingly: tests/test_app.py covers the moved application logic, tests/test_main.py is now a thin entry-point/re-export smoke test.
Researched https://6502disassembly.com/va-missile-command/wave-guide.html (linked from the disassembly as the "Attack wave details" reference) for the actual per-wave numbers, since several values were rough guesses: - ICBM_COUNT_TABLE: real per-wave ICBM budget (12,15,18,12,16,...,22), replacing the min(8+wave*2,30) approximation. - SMART_BOMB_START_WAVE corrected from 4 to 6. - FLIER_WAVE_TABLE: real per-wave (cooldown, fire-rate, altitude-range) triples for waves 2-8 (values continue unchanged after wave 8). Flier.create_random now picks altitude within the wave's real band instead of a fixed mid-screen value, and cooldowns are used directly as frame counts (removes the ad-hoc FLIER_TIMER_SCALE guess). - Fliers now move 1px/3 frames (bomber) or 1px/2 frames (satellite) via a per-type move_interval, instead of an arbitrary "speed" value. - Add src/attract.py: AttractDemo, an autoplay bot matching the disassembly's described attract-mode AI (targets ICBMs in spawn order, leads 16 frames, never more than 2 ABMs in flight, fires from the nearest silo). Not yet wired into the app loop.
Iterated against three references directly (extracted disassembly PDF screenshot, a mobile-port screenshot, and a user-provided reference) rather than guessing proportions: - Cities: small flat-ground tuft clusters, base aligned exactly to GROUND_Y (previously read from CITY_Y=216, 4px above the actual ground line, leaving a visible gap). - Silos: wide, short, flat-topped mound (bigger than a city tuft), sized and spaced so all 6 cities fit cleanly between the 3 silo mounds with zero overlap. - Ammo display: remaining ABMs render as a triangular pyramid of small rocket icons on the mound's flat top (row sizes 1/2/3/4 = 10, matching SILO_CAPACITY), filled top-down as ammo depletes, spread and lowered to match the reference proportions. Icons stay a single consistent color -- the earlier low-ammo amber tint on the icons is removed; "LOW"/"OUT" status is communicated only via the HUD banner (present in the disassembly's own screenshot and required by SPEC.md).
- MissileCommandApp now drives a hidden AttractDemo while in ATTRACT state and renders its live gameplay behind the title/high-score overlay, matching the disassembly's description of attract mode (the computer actually plays a wave; it isn't pre-recorded). - AttractDemo now tracks its own crosshair_pos, updated to each predicted intercept point as it fires, so the crosshair visibly moves during the demo instead of sitting off-screen. - Extract the '1'-to-start handling into _start_game_from_attract() so it's unit-testable without a live pygame event loop. - Add a proper top-10 high-score table render (name + zero-padded score per rank) replacing the single high-score line. - _reset_to_attract() now also restarts the demo, so a fresh demo wave begins immediately after a game over. - Add tests/test_attract.py and app-level wiring tests.
It previously ran from y=145 to y~225, overlapping the ground line and cities/silos at the bottom of the 231px screen. Shifted up to start at y=78, centering it in the space between the title/subtitle and the ground.
A scripted bot plays up to 20 waves, asserting the game never exceeds any slot-table limit (ABM/ICBM/explosion/smart-bomb) and terminates within a bounded frame budget. The bot leads its shots using predicted missile position (matching src/attract.py's autoplay approach) rather than firing at the missile's assigned target -- aiming at the target caused near-total interception failure, since the ABM's explosion often arrives at the same frame as (or after) the missile's own impact. Also includes a "never fires" pathological bot to confirm mercy-rule attrition still reaches GAME_OVER cleanly with no defense at all.
…ls, mute Closes 4 gaps identified in a SPEC.md compliance review: 1. Operator options: add --cities N (4-7) and --bonus-interval N (0/8000/10000/12000/14000) CLI flags, threaded into a CityManager(num_cities=..., bonus_threshold=...) constructed at init() and again in _reset_to_attract() so they persist across games. CityManager gains a num_cities field (capped at 6, the number of physical city slots this recreation models). 2. Wave-intro screen: add an app-level "WAVE N / GET READY" phase (_begin_wave_intro/_pending_wave_intro) between WAVE_END's tally and Game.start_wave() actually being called, playing the wave-start alarm at the start of the intro rather than the moment attacks begin. Implemented purely in src/app.py -- Game itself gains no new state, since Game.update() already no-ops for any non-RUNNING state. 3. High-score entry is no longer a blocking nested while-loop (_prompt_initials) that bypassed the normal event/update/render cycle. Converted to a per-frame flow (_awaiting_initials, _update_initials_entry) driven by the same _handle_events/_update/ _render loop as everything else, with slot-advance routed through the normal MOUSEBUTTONDOWN handler. 4. --mute CLI flag to disable audio entirely (AudioManager.enabled), closing the SPEC-requested debug mute flag gap. Also fixes a test-isolation bug: several new tests constructed MissileCommandApp() without overriding scores_file, so save_high_scores was writing to the real project scores.json on every test run. All score-writing tests now use tmp_path.
… code
- CityManager: destroy_city out-of-range/already-destroyed guards,
destroy_city_at no-match, replace_random_crater with zero bonus
cities or no craters, num_cities clamping (4 and the 7->6 cap).
- DefenseManager: fire()/fire_nearest() invalid-index and all-empty
guards, get_silo() valid/invalid lookup.
- ABM/ICBM/SmartBomb: update() no-ops once inactive; smart bomb
evasion correctly picks the nearest of multiple tracked explosions
rather than just the first in the list.
- Remove src.models.missile.fixed_mul: dead code, never called
anywhere and had no test coverage.
src/models/{city,defense,missile}.py line coverage: 99-100%, 100%,
98% respectively.
Root cause of "way too difficult from wave 1": the wave-guide's ICBM
"speed" column is NOT a velocity multiplier -- researched directly
from the disassembly project ("the number of frames that the game
waits between moving the ICBMs... a value of zero means it updates
the missiles on every frame... advances the missile one unit per
frame" when it does move). The old WAVE_SPEEDS table (1-8) instead
applied that number as a continuous per-frame velocity with no
delay at all, making even "slow" wave 1 roughly 5-6x faster than the
real arcade -- confirmed empirically: wave 1 now takes ~21s to cross
the screen with no delay mechanism it was ~4s.
- Replace WAVE_SPEEDS with ICBM_MOVE_DELAY_TABLE, the real researched
per-wave delay-frames values (4.8125 at wave 1 down to 0 at wave 15+).
- Add a fractional accumulator (move_wait_counter) to ICBM/SmartBomb:
each frame adds 1 unit; a move fires once the accumulator reaches
(move_delay + 1), carrying the remainder so fractional delays average
out correctly across many frames. A naive decrement-by-1 counter
(my first attempt) collapsed any delay < 1 frame to "moves every
frame", making waves 5, 8, and 11 indistinguishable -- caught via
empirical measurement before committing.
- Step magnitude (compute_increments' "speed" parameter) is now a
constant (ICBM_BASE_STEP_SPEED = 1, "one unit per frame" when
moving), no longer wave-dependent -- the whole difficulty ramp comes
from move_delay alone, matching the real mechanism.
- get_wave_speed() -> get_wave_move_delay(); threaded through
spawn_icbm, the attack-pacing spawner, and flier-launched ICBMs.
MIRV children inherit their parent's move_delay.
- Add regression tests for the accumulator's average-cadence behavior
and per-wave monotonic ordering.
The flier previously rendered as a simple 3-point arrow/triangle, which didn't read as an airplane at all. Traced the actual flier sprite from the disassembly PDF's own reference screenshot (page 2) and rebuilt it as a small asymmetric side-view jet silhouette (pointed nose, canopy bump, vertical tail fin, single swept wing below the fuselage) -- a side-view profile reads as "airplane" far more clearly than a symmetric top-down dart at the tiny pixel sizes this sprite renders at. Mirrors correctly for both flight directions.
The original arcade's 256x231 resolution (~1.11:1) pillarboxed heavily on any modern widescreen display. Per user request, widened the internal render surface to 410x231 (aspect 1.7749, within 0.2% of 16:9's 1.7778) rather than stretching the existing image -- silo and city X positions are scaled proportionally from the original 256-wide layout so they spread out to fill the extra width instead of being distorted. Center silo stays exactly at SCREEN_WIDTH // 2. - SILO_POSITIONS: (32,128,224) -> (51,205,359) - CITY_POSITIONS: (48,72,96,160,184,208) -> (77,115,154,256,295,333) - Flier.create_random's screen_width default updated to match (call site already passed SCREEN_WIDTH explicitly, so this was dead but now-misleading code, not a live bug) - All HUD/crosshair/attract-mode positioning already referenced SCREEN_WIDTH symbolically rather than hardcoding 256, so no renderer changes were needed beyond the position tables - Fixed 2 tests that hardcoded the old absolute silo X positions
Two real issues found while validating the reported "cities/silos
don't look damaged when hit" and "only left click works" behavior:
1. Destruction was already working correctly at the game-logic level
(verified via a fresh no-defense simulation: 2/3 silos and 3/6
cities destroyed exactly as expected), but the rendering made it
nearly invisible:
- Destroyed silos kept drawing the full-color, undamaged mound,
with only a tiny 16x4 floating bar where the ammo pyramid used
to be -- looked identical to "silo present but out of ammo".
- Destroyed cities were a thin 12x3 sliver, easy to miss entirely
next to the other five intact cities.
Fixed by darkening the whole silo mound when destroyed and adding a
bright wreckage "X" mark, and by rendering destroyed cities as a
full-footprint collapsed-rubble silhouette instead of a thin line.
2. Verified against the real arcade cabinet (one trackball + three
dedicated fire buttons, each wired to a specific left/center/right
battery -- not proximity-based auto-select): the existing keyboard
controls (A/S/D) already matched this, but left-click was mapped to
"fire nearest", which doesn't exist on the real machine. Remapped
left/middle/right mouse buttons to left/center/right silo, matching
the cabinet's 3-button layout; removed the now-dead _fire_nearest
app method (game.fire_nearest itself is still used by the attract
demo's AI, which legitimately wants proximity targeting).
Also validated (no change needed): 10 ABMs/silo, 30 total, is correct
per the cabinet's documented specs, and silo ammo does restore fully
every wave (start_wave already calls defenses.restore_all()) -- the
"feels like too few" perception is likely the intentional scarcity
described by the disassembly project itself, possibly compounded by
the pre-fix wave-1 speed bug forcing wasteful panic-fire.
Two compounding bugs, found by instrumenting real simulations and diffing every destruction event against the causing missile's actual on-screen position: 1. has_passed_target() used OR logic across axes: a missile was marked "arrived" (exploding and destroying a city/silo) the moment EITHER its X or Y coordinate crossed the target, even while the other axis was still 20-30+ pixels away. Confirmed via direct instrumentation: missiles were registering hits while still visibly airborne, nowhere near the ground -- exactly matching the "destruction out of nowhere" report. Since x_inc/y_inc share the same distance denominator, a straight-line missile reaches both axes' targets at approximately the same time by construction, so requiring BOTH (AND) still arrives promptly in the normal case. Verified: re-running the same instrumented scan across 25 wave/seed combinations went from anomalies on nearly every run to 3 total, and those remaining cases are a much more benign overshoot-while-1px-short scenario where the explosion still renders at the correct target position. 2. MIRV splitting had no wave gate at all. The shipped ROM's own check at $56d1 is a documented bug (compares wave < 1, always false) -- the disassembly's own comment on that line says it "should probably be #$02", meaning the intended design blocks MIRVs on wave 1. Implemented the intended fix (MIRV_START_WAVE=2) rather than the shipped bug, per direct user confirmation that MIRVs did not appear this early in the original game. This was very likely the dominant cause of "too many inbound attacks" on wave 1, since MIRVs multiply concurrent (though not total-budget) ICBMs by up to 3x whenever eligible. Both fixes verified end-to-end via instrumented no-defense simulations, not just isolated unit tests.
Per user request/memory: any explosion (ABM intercept or ICBM/smart bomb impact) that visually reaches the ground line now permanently bites a small wedge-shaped crater out of the terrain there, whether or not it destroyed a city or silo -- including near-misses on open ground between cities. Matches the original arcade's well-known persistent battlefield-scarring look, distinct from the discrete "city/silo destroyed" state that already existed. - Game.ground_craters: list of X positions, capped at MAX_GROUND_CRATERS with FIFO eviction; persists across waves (reset only when a brand-new game starts), matching the original. - _maybe_crater_ground() triggers whenever center_y + blast radius reaches GROUND_Y, called from both ABM and ICBM arrival handling in _process_arrivals(). - Renderer draws each crater as a small inverted-wedge cut into the ground band, using the sky color, before silo mounds so mounds still render on top.
The has_passed_target fix from the previous commit was real but not the dominant cause -- this was. Any time a player successfully shot down an ICBM/smart bomb mid-flight (via an ABM explosion), the city or silo it had been heading toward would get destroyed anyway on the very next frame, even though the missile never reached the ground. Root cause: collision detection (explosion vs ICBM, step 7 of Game.update()) calls slot.deactivate(), setting is_active=False, but runs AFTER clear_inactive() (step 5) for that same frame. So the freshly-intercepted missile sits in the slot table until the following frame's clear_inactive() call -- which itself runs AFTER _process_arrivals() (step 3) on that next frame. _process_arrivals() saw an inactive slot and assumed it meant "reached target naturally", spawning an explosion and destroying a city/silo at the missile's original target position, regardless of where it actually was when shot down. Confirmed directly: an ICBM shot down at altitude 50 (nowhere near the ~216-220 ground line) still destroyed its target city one frame later, before this fix. Also confirmed the earlier has_passed_target fix alone wasn't sufficient -- it only affects natural ground arrivals, not this interception path, which no-defense simulations never exercise (explaining why prior testing missed it). Fix: ICBM gains an `intercepted` flag and an intercept() method, distinct from deactivate(). The collision handler now calls intercept() instead of deactivate(), and _process_arrivals() skips any inactive missile with intercepted=True. Verified with a scripted-defender simulation (reused from the headless sim test's bot) across 10 seeds of actively-defended play -- zero anomalies, versus the bug reproducing on essentially every interception before the fix.
Left/Right cycle the highlighted character, Return/Space confirm and advance to the next slot -- an alternative to the existing mouse-scrub (move to select, click to confirm) flow, not a replacement. Left/Right recompute the current charset bucket index (the same formula _update_initials_entry already uses every frame to derive the letter from crosshair_x) and jump crosshair_x to the center of the adjacent bucket. This reuses the existing per-frame lookup as the single source of truth rather than tracking a separate keyboard-only index that could drift out of sync with mouse input -- and avoids a subtler bug from a first attempt at this fix: adding a fixed pixel step to crosshair_x is fragile to float rounding, occasionally landing a hair short of (or past) a bucket boundary and failing to advance, or overshooting by an extra bucket, depending on exactly where within the current bucket the crosshair happened to sit.
Re-verified the wave-guide's per-wave ICBM delay table byte-for-byte (it matched what was already implemented exactly), and separately found a documented claim that an unattended wave loses 3 cities in about 15 seconds -- which, surprisingly, would require the CURRENT values to run faster, not slower (attack pacing already fills all 8 ICBM slots within ~3.6s with no defense; the ~28s it currently takes to lose 3 cities is entirely due to individual missile fall time). That contradicts direct, repeated user feedback after hands-on testing: wave 1 needs to be slower than it currently is, and the ramp into wave 2-3 is too steep (the researched table roughly halves the delay every wave, e.g. a single ICBM's full-screen fall time going ~21s -> ~10s from wave 1 to wave 3). Prioritizing that direct feedback over the ambiguous secondary "15 seconds" claim and my own possibly-imperfect reconstruction of the exact step-size mechanic. Replaced ICBM_MOVE_DELAY_TABLE with a deliberately gentler, hand-tuned curve: wave 1 fall time ~40s (was ~21s), and the ramp toward "fast" spread across all 15 waves instead of front-loaded into the first 8 (wave 3 now ~25s, was ~10s). This is an explicit, documented deviation from the literal disassembly values, not a research error -- SPEC.md's "disassembly wins" default is overridden here by direct user request.
Root cause: the highlighted letter was recomputed unconditionally every frame from crosshair_x. pygame.mouse.set_pos() -- called every frame to recenter the OS cursor for trackball emulation -- itself generates a synthetic MOUSEMOTION event (documented pygame/SDL behavior), which was being picked up as if it were real user input on the very next frame. In practice this meant the letter continuously drifted back toward whatever crosshair_x implied, silently overwriting a keyboard press within a frame or two -- confirmed directly: with the physical mouse completely untouched, running the real event loop for 120 idle frames alone changed the letter, with no help from a mouse at all. Two-part fix: 1. Stop clobbering pygame's own event queue: pygame.event.clear (pygame.MOUSEMOTION) right after the recenter call discards the synthetic event set_pos() just generated, before it can be misread as real motion on the next frame. Verified genuine mouse motion (posted earlier in the same frame, before the recenter runs) is unaffected. 2. Decouple the two input paths entirely: the letter is now set directly by whichever input touched it (_apply_crosshair_to_initials for real MOUSEMOTION events, _handle_initials_keydown for Left/ Right), instead of _update_initials_entry recomputing it from crosshair_x unconditionally every frame. Return/Space confirms, mirroring the existing left-click behavior. Also confirmed via the real app flow (not just the bare Game object): silo ammo does restore to 10/silo at the start of every new wave, including through a full wave-end tally -> wave-intro -> next-wave transition.
There was a problem hiding this comment.
Pull request overview
This PR completes a substantial modernization of the Missile Command codebase: it replaces the legacy root-level pygame implementation with a src/-based application architecture, adds a full rendering/audio stack, and brings gameplay parameters (wave pacing, fliers, MIRVs, scoring multiplier, mercy rule) in line with the intended wave-guide behavior while expanding the playfield to a widescreen-friendly width.
Changes:
- Introduces a new app/controller layer (
src/app.py) with a thinmain.pyentry point, plus attract-mode autoplay and improved high-score entry UX. - Implements new UI subsystems: renderer + palettes, audio cue tracking, and procedural (numpy) synth-backed audio with graceful fallback.
- Reworks core gameplay loop and models: move-delay based missile pacing, MIRV wave gate, interception correctness, city persistence/repair, flier tables, and extensive new/updated tests (including headless simulation).
Reviewed changes
Copilot reviewed 39 out of 47 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| text.py | Removed legacy initials input UI module. |
| tests/test_models.py | Expanded unit tests for movement/arrival semantics, wave helpers, and multiple regression cases. |
| tests/test_missile.py | Updates flier movement tests to validate screen-width-independent crossing time. |
| tests/test_main.py | Simplifies main tests to verify main.py re-exports and non-throwing exit code behavior. |
| tests/test_high_scores_audio.py | Makes audio tests resilient to changing sound-file table size. |
| tests/test_headless_simulation.py | Adds a headless 20-wave stability/slot-limit simulation test. |
| tests/test_game.py | Adds broad integration coverage for pacing, MIRVs, cratering, mercy rule, scoring multiplier, and fliers. |
| tests/test_defense.py | Updates tests to reflect config-driven silo positions and new city persistence/repair behavior. |
| tests/test_audio_cues_synth.py | Adds tests for audio cue mapping and procedural synth waveform generation. |
| tests/test_attract.py | Adds tests for attract-mode autoplay AI behavior and invariants. |
| tests/test_app.py | Adds comprehensive tests for the new MissileCommandApp controller and CLI options. |
| src/utils/functions.py | Replaces get_wave_speed with move-delay, adds wave tables helpers and score multiplier, scales wave-end bonus. |
| src/utils/init.py | Updates exports to get_wave_move_delay. |
| src/ui/synth.py | Adds numpy-based procedural POKEY-style sound synthesis recipes. |
| src/ui/renderer.py | Adds a native-surface renderer with scaling, HUD, tally screens, and “THE END” animation. |
| src/ui/palette.py | Adds per-wave palettes and flashing/cycling colors. |
| src/ui/audio.py | Updates audio system to load samples when present or synthesize via numpy; adds looping support. |
| src/ui/audio_cues.py | Adds frame-to-frame game-state → sound-cue tracking (including loops). |
| src/models/missile.py | Fixes arrival detection semantics, adds move-delay pacing, interception flagging, MIRV wave gate, and flier speed calibration. |
| src/models/explosion.py | Refines octagon geometry and collision math; adjusts hold timing documentation/behavior. |
| src/models/defense.py | Adds destroy_silo_at helper used by arrival logic. |
| src/models/city.py | Makes destroyed cities persist across waves; adds safe destroy semantics and crater repair via bonus cities. |
| src/game.py | Major update: new update pipeline, arrivals vs interceptions, pacing/batches, cratering, flier integration, mercy rule, multiplier scoring. |
| src/config.py | Widens screen, adds wave tables/constants for pacing/fliers/smart bombs, and new rendering constants. |
| src/attract.py | Adds attract-mode autoplay demo AI. |
| src/app.py | Adds the new top-level pygame app controller (loop, input, rendering orchestration, audio, highscores). |
| scores.json | Updates bundled high score data formatting/values. |
| requirements.txt | Adds numpy dependency for procedural audio. |
| README.md | Updates documentation for new architecture/features/controls/CLI and gameplay details. |
| Pipfile | Updates dependency versions and dev dependencies; bumps required Python version. |
| missile.py | Removed legacy missile implementation. |
| missile-defense.py | Removed legacy entry point / game loop. |
| mcgame.py | Removed legacy game logic/controller. |
| main.py | Refactored to a thin CLI entry point delegating to src/app.py. |
| functions.py | Removed legacy shared utility module. |
| explosion.py | Removed legacy explosion implementation. |
| defense.py | Removed legacy defense/silo implementation. |
| data/sfx/README.md | Clarifies that wav files are optional overrides for synthesized sounds. |
| config.py | Removed legacy root-level config. |
| city.py | Removed legacy city implementation. |
| .gitignore | Adds coverage artifacts; keeps scores.json ignored; adds local tool exclusions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.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.
No description provided.