Skip to content

Fable completion#17

Merged
CAG07 merged 28 commits into
mainfrom
fable-completion
Jul 6, 2026
Merged

Fable completion#17
CAG07 merged 28 commits into
mainfrom
fable-completion

Conversation

@CAG07

@CAG07 CAG07 commented Jul 6, 2026

Copy link
Copy Markdown
Owner

No description provided.

CAG07 added 23 commits July 2, 2026 23:51
…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.
Copilot AI review requested due to automatic review settings July 6, 2026 01:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 thin main.py entry 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.

Comment thread src/ui/renderer.py Outdated
Comment thread src/ui/renderer.py Outdated
Comment thread README.md
Comment thread README.md Outdated
CAG07 and others added 3 commits July 5, 2026 20:20
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>
@CAG07
CAG07 merged commit c64e149 into main Jul 6, 2026
5 checks passed
@CAG07
CAG07 deleted the fable-completion branch July 6, 2026 01:22
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.

2 participants