- Added programmatic play in
src/env.pywith structured and NumPy observations. - Added high-level actions in
src/mediator.py(create/remove paths, pause/resume, step time). - Added agent playthrough logging and replay helpers in
src/agent_play.py. - Added game-over rules for passengers waiting too long in manual and programmatic play.
- Added game-over overlay with final score and stopped the main loop on game-over.
- Added clickable restart/exit buttons and keyboard shortcuts on the game-over screen.
- Added unlock blink for newly available stations and metro-line buttons (3 blinks in 1 second).
- Passed
time_msthrough rendering so blink timing is deterministic.
- Added progressive metro line unlock milestones tied to cumulative travels handled: 1 line at start, then 2/3/4 at 100/250/500 travels.
- Switched line colors to runtime-randomized color allocation so each run can produce a different line color set.
- Added game rules documentation in
GAME_RULES.mddescribing unlock thresholds and randomized line color behavior. - Expanded
GAME_RULES.mdinto a full implementation-aligned rules reference covering objective, stations/passengers, lines/metros, progression, routing, spawning, game-over, controls, and programmatic actions. - Updated locked path button visuals to draw as empty ring outlines (edge-only) instead of filled circles, and synchronized lock state updates with unlock progression.
- Added station progression tied to cumulative travels: start with 3 stations, then unlock additional stations at 30, 80, 150, 240, ... travels (increment +20 each unlock) up to 10 stations.
- Added mediator logic to spawn newly unlocked stations from a pre-generated station pool while preserving existing station/path state.
- Updated
GAME_RULES.mdwith station unlock progression details. - Fixed path button color regression so unlocked line buttons keep the assigned metro line color instead of being reset to default gray on lock-state refresh.
- Fixed path rendering order centering so active paths are offset around zero based on current path count, preventing single-path geometry from self-crossing due to forced negative offsets.
- Switched passenger spawning from one global cadence to per-station rhythms by tracking station-specific spawn intervals/timers and spawning passengers independently per station.
- Changed station unlock baseline to 10 travels and updated
GAME_RULES.mdmilestones. - Added keyboard speed controls (1x/2x/4x via keys
1/2/3) and wired simulation timing, metro movement, spawn-step progression, and wait-time updates to respect the selected speed. - Updated controls docs in
README.mdandGAME_RULES.md. - Fixed passenger route selection to prefer the shortest reachable destination route (with transfer-aware tie-breaking) so riders board eligible metros instead of waiting for longer alternatives.
- Updated boarding behavior so waiting passengers can board the first arriving metro with space when that metro can still lead to a valid destination route, even if their prior plan targeted another line.
- Increased station cap from 10 to 20 by updating
num_stationsinsrc/config.py(unlock milestones now generate up to 20 stations). - Updated path unlock milestones to
[0, 90, 300, 650]insrc/config.py. - Added pre-timeout passenger warning blink: passengers in the last 10 seconds before
passenger_max_wait_time_msnow blink on/off in station queues. - Threaded render-time wait thresholds through holder/station rendering so passenger warning blink is deterministic from mediator time.
- Added resolution-adaptive rendering via a virtual game surface + viewport transform (
src/ui/viewport.py) with letterboxed scaling to resizable windows. - Updated main-loop rendering/input flow to draw to virtual space, scale to the window, and remap mouse events from window coordinates back into virtual coordinates.
- Refactored game-over overlay and path-button layout to compute positions from render-surface dimensions instead of fixed screen constants.
- Updated
GAME_RULES.mdline/station progression and passenger spawning timing details to match current implementation. - Added rare one-of-a-kind station shapes (diamond, pentagon, star) and shape rendering support.
- Added station-pool generation logic so unique shapes can only appear after the 10th station slot and at most once each per run.
- Updated
ARCHITECTURE.mdto reflect the latest project file structure. - Fixed passenger wait blink logic so passengers at or past the max wait threshold also blink instead of only pre-timeout passengers.
- Fixed path segment offset direction to use a stable station-pair orientation so reversed A/B segments stay parallel and no longer cross each other.
- Added metro motion profiling with 1-second acceleration and 1-second deceleration, including graceful handling for short inter-station distances.
- Added conditional metro station stops so metros only dwell when there are eligible passengers to board that line.
- Added boarding-duration timing at stations so each boarding passenger consumes 0.5 seconds of dwell time.
- Fixed metro stop-planning crash on padding segments by guarding next-station lookup when a segment endpoint is not a station.
- Fixed zero-length direction vectors to return
(0, 0)so metro rotation never receives NaN angles on collapsed/zero-distance segments. - Fixed metro station-dwell deadlock by only scheduling boarding stops when metro capacity is available now or will be freed by alighting at that station.
- Changed metro line unlocks to score purchases by clicking locked path buttons instead of auto-unlocking from travels.
- Kept unlock milestone economics by using incremental line purchase prices derived from
[0, 90, 300, 650]. - Added locked-button hover buy hints with two lines of text (
Buyand price), using gray text when unaffordable and black text when affordable. - Updated game rules docs for purchase-based line unlocks and the new locked-button hover/click behavior.
- Changed UI font usage to
couriervia sharedfont_nameconfig. - Expanded
README.mdprogrammatic play docs to list theMiniMetroEnvAPI, action schemas, valid input constraints, and observation/step return fields. - Added programmatic
buy_lineaction to purchase metro lines throughenv.step(...), with optionalpath_indextargeting and validation.
- Improved random metro line color generation to prefer hues that are more distinct from already selected line colors.
- Added color-distance helper utilities and tests for hue wrap distance and distinct hue selection behavior.
- Changed station pool generation so new station positions are less likely to spawn far from the current station cluster center.
- Changed metro rendering so passenger icons are displayed inside each metro car in a 2x3 grid that moves with the car.
- Improved metro passenger icon spacing and rotated passenger icon placement to follow metro car orientation.
- Rebalanced metro passenger slot layout to a uniform 3x2 in-car grid to prevent overlap while preserving car-aligned rotation.
- Updated metro passenger slot spacing so row and column icon gaps match the side margins inside the metro car.
- Added station snap blips: when a line endpoint snaps onto a station during path creation, the station emits a short outward ring in the line color.
- Triggered snap blips for both drag-snap station additions and direct line-complete snaps on mouse release.
- Added station and mediator tests covering snap blip lifecycle, rendering, and snap-trigger calls.
- Updated
GAME_RULES.mdto document the station snap-blip behavior during line creation. - Added bottom-left simulation speed controls (
Pause,1x,2x,4x) with clickable UI buttons wired to pause and speed state. - Switched speed-control button labels to iconography: pause bars, single play, double play, and four-play symbols.
- Changed runtime-randomized metro line colors to a less saturated palette for softer visuals.
- Updated metro passenger rendering so passenger icon orientation rotates with the car instead of staying upright.
- Added Python/py313 workflow guidance in
AGENTS.mdwith aCLAUDE.mdshim, reflected the removed.cursor/rules, and added an initial review artifact directory. - Moved review artifacts under the docs thread area and documented robust full-codebase Codex/Claude review commands.
- Ran the first full-codebase review, fixed terminal-state API mutation, malformed action handling, loop routing closure, stale travel-plan cleanup, the graph node hash contract, and the stale Ruff pre-commit hook, then added focused regressions.
- Renamed review artifacts into the broader
docs/threads/lifecycle, with active work incurrent/, completed work indone/, and completedfullandagents-repo-fitthemes migrated underdone/.
- Onboarded the deterministic proposal-only recursive playtest with fresh-process verification, exact local and civ-engine runtime provenance, crash-recoverable manifest/ledger finalization, promoted regression coverage, and stable equal-cost BFS routing.
- Rebuilt the player presentation boundary around fixed 60 Hz updates, metro interpolation, immutable symmetric route layouts, bounded antialiased route caching, lazy headless-safe resources, prepared first-frame hitboxes, pure geometry drawing, and deterministic software-surface rendering; refreshed the warm minimal visual style and outlined line-colored metros without changing balance or mechanic constants.
- Added a player-equivalent Gymnasium pixel environment with isolated seeded simulations, a fingerprinted low-level mouse/keyboard protocol, exact fixed-step decisions, strict terminal metrics, and a deterministic positive-delivery curriculum; added a compact frame-stacked PPO training/evaluation stack with spawn-safe workers, content-compatible manifests, hashed artifacts, and opt-in cross-content evaluation.
- Finalized the RL setup with universal hashed core/RL locks, exact-byte artifact authentication, authoritative train/evaluation seeds, authenticated resume and drift-safe evaluation, real spawned PPO lifecycle coverage, pinned recursive compatibility evidence, and fresh/resume Windows CI smoke tests.
- Researched visual RL architectures and evaluation practice, documented the model-selection rationale, and upgraded fresh training to an eight-frame SB3-Contrib RecurrentPPO policy with separate actor/critic LSTMs, delivery-total-aligned returns, recurrent evaluation state, manifest-bound resume settings, feed-forward PPO/frame-stack ablation controls, a measured lower-memory recurrent batch default, censoring-aware delivery reports, and authenticated pre-recurrent PPO compatibility coverage.
- Stabilized path-button gameplay tests after Linux CI exposed a global-random station-overlap flake; replacement stations now use each mediator's isolated seeded simulation context.
- Opened a persistent, adversarially reviewed game-maturity thread with an exact remote resume transaction, dependency-ordered product work units, canonical delivery/credit migration, reproducible initial overload evidence, a resource-estimated twelve-frame multiscale-history candidate pending live profiling, per-experiment durability, and explicit completion gates spanning gameplay, architecture, persistence, content, balance, and long-horizon RL evaluation.
- Separated lifetime passenger deliveries from spendable line credits, aligned structured rewards with the delivery objective, and versioned agent-play/checkpoint/recursive evidence with genuine v1 reconstruction, strict v2 contracts, fail-closed reward-mode checks, fresh-process verifier coverage, and sub-500-line recursive contract/checkpoint modules.
- Replaced ambiguous score presentation with a canonical deliveries/line-credits HUD and delivery-first game-over result, added compact-layout and pixel-sensitivity regressions with before/after evidence, and corrected the documented passenger cadence using executable 1x/2x/4x, full-station, and quantization tests.
- Raised the fresh-game overdue-passenger threshold from one to two so the first overdue station passenger warns and the second ends the game, retained writable compatibility aliases, and versioned recursive and agent-play evidence to v3 while replaying genuine v1/v2 records at the historical threshold.
- Added immutable temporal-history descriptors and separately authenticated fingerprints in training-manifest v2, exact manifest-v1 contiguous-stack normalization and byte preservation, fail-closed pre-wrapper train/evaluation guards, and focused schema/legacy coverage while retaining the eight-contiguous-frame runtime default pending the vector-history and resource-profile stages.
- Added a bounded per-environment
uint8temporal-history ring with exact multiscale chronology, zero pre-history, isolated terminal/reset stacks, fail-closed recovery, contiguousVecFrameStackequivalence, and pinned candidate memory accounting; runtime integration remains staged for GM-02c. - Integrated exact manifest-declared temporal history across fresh training, resume, and evaluation; added mutually exclusive contiguous/named controls, recurrent and spawned multiscale coverage, genuine old-stack compatibility, model-space mismatch checks, evaluation history reporting, and a Windows multiscale lifecycle smoke while retaining eight contiguous frames as the unpromoted default.
- Added a test-first matched-history resource profiler with cyclic fresh-process campaigns, exact storage/padded-batch/MAC and promotion contracts, a production-horizon two-update RecurrentPPO worker, and a dependency-free Windows launcher/descendant working-set supervisor; the harness is staged for remote verification before any default-promotion measurements run.
- Ran the remotely gated matched-resource campaigns, rejected the operationally invalid primary result, promoted the fully valid ten-frame multiscale fallback within the preregistered RAM/throughput gates, made that exact descriptor the fresh recurrent default while preserving explicit PPO's contiguous-eight behavior and persisted resume identity, and made invalid/incomplete/mismatched campaign aggregates fail closed.
- Reframed the ten-frame recurrent policy as the bounded player-pixel baseline and added a reviewed GM-12 research ladder for compact semantic strategy state, direct assisted hybrid policies, and pixel-only actors trained with privileged teachers/auxiliary critics; each lane keeps truthful protocol/task identity and is promoted only by matched held-out passenger deliveries.
- Split the 1,158-line mediator characterization suite into a shared fixture plus six behavior-focused modules under 500 lines, preserving all 57 test bodies, three helpers, and six explanatory comments exactly while leaving production code unchanged.
- Extracted line/station/economy progression into a dependency-free single-owner aggregate behind explicit writable
Mediatorfacade properties and methods, with characterization coverage for cached unlocks, purchase rejection, entity/RNG identity, UI timing, checkpoint consumers, and delivery-hook order and no gameplay or public API change. - Extracted dependency-free stateless route queries, selection, compression, and lazy planning proposals behind the
Mediatorfacade while preserving public methods, RNG/BFS and mapping lookup order, live passenger-list mutation timing, travel-plan identity and ownership, and passenger-delivery behavior.
- Extracted the 12 path-lifecycle transitions into a dependency-light stateless component behind unchanged real
Mediatormethods, retaining canonical facade-owned topology state, late public-hook and factory resolution, mutation/identity/partial-failure timing, focused direct plus facade characterization, and a reproducible archived-baseline differential while reducingsrc/mediator.pybelow 1,000 lines. - Strengthened the repository delivery policy so each minimal coherent unit is reviewed, validated, scoped, and committed promptly while failing, in-flight, and partial checkpoint commits remain prohibited.
- Extracted 16 passenger-flow and simulation-transition algorithms into a dependency-light stateless component behind unchanged
Mediatormethods, preserving late collaborator resolution, three fresh graph phases, live iterator and partial-failure timing, and deterministic gameplay while reducingsrc/mediator.pyto 735 lines. - Extracted 19 input, layout, compatibility-render, path-button UI, pause/speed, and structured-action algorithms into a dependency-light stateless coordinator behind unchanged
Mediatormethods, preserving canonical facade state, late dependency and public-hook resolution, subclass/evaluation-order behavior, and player-equivalent control semantics while reducingsrc/mediator.pyto 605 lines. - Replaced recursive execution's mutable sibling dependency with one descriptor-bound ignored
/.civ-engine-pin/checkout, enforcing package/lock/CI parity plus physical package,dist, runtime-entry, Git commit, clean-status, and complete runtime-tree identity before execution.
- Added a cross-platform, descriptor-authorized civ-engine setup and verification boundary with ownership-checked lock/transaction cleanup and exact manual crash recovery, immutable provenance captures, complete non-generated
HEADbyte authentication, exclusive-copy no-clobber publication plus final-path reauthentication, a strict root install graph and missing-only exact root link without root npm extraction, Node-distribution npm plus pin-local TypeScript execution without shell/PATH lookup, an explicit trusted bootstrap with shared post-start taint detection, parser-shared canary selection, a fixed post-start zero-argument full-suite test guard, child-lifetime cooperative leases with pre/post-verification ownership checks, fail-closed shadow/tamper handling, and full-history Ubuntu/Windows CI dogfooding. - Corrected production defects exposed after exact-link setup passed on both hosted platforms: controlled child and Git-planner paths now follow selected-platform rules, while publication verification observes each source directory, entry, byte sequence, and link target before its destination counterpart so concurrent destination-first snapshots cannot evade the fail-closed comparison.
- Completed the local GM-04c finalization proof without production changes: repeated setup remained stable, the canonical guarded Node suite passed 241 of 245 registered tests with four expected platform skips, a clean recursive run passed public fresh-process verification with no fix candidate, and an isolated fixture proved the categorical dependency guard exits before the engine body when resolution targets the independently fingerprinted 2.4.1 sibling.
- Added atomic programmatic line replacement with exact selector and station validation, off-live geometry, identity- and pose-preserving semantic metro rebinding, immediate scoped waiting-rider replanning, safe-alight onboard markers, and full topology/passenger/RNG rollback on any effect-phase failure.
- Added selected-line hold-drag-release redraw with an immutable off-live preview, deterministic selected/invalid feedback, preserved click/purchase/speed/create behavior, exact manual/structured canonical equality, and topology-aware interpolation that prevents a zero-step post-edit metro jump.
- Added collision-resolved selected-line endpoint and insertion handles with two-phase mouse activation, atomic extension/one-step shortening/interior and loop-closing insertion, cache-free lane-consistent feedback, fast/fidelity pixel reachability, outside-viewport and game-over cleanup, and unchanged structured/checkpoint/action identities.
- Added a conserved locomotive inventory as a read-only total-minus-assigned resource, exposed exact labeled structured counts and a third player HUD line, preserved automatic line assignment and every legacy lifecycle/failure seam, and proved genuine checkpoint v1/v2 reconstruction plus fast/fidelity low-level
4 -> 3 -> 4pixel visibility without a schema or action change. - Replaced automatic line allocation with explicit visible and structured locomotive assignment plus empty-train queued return, supporting multiple locomotives per line, delayed inventory refund at the next real station, no-boarding return trips, transactional ownership rollback, player-equivalent fast/fidelity controls, checkpoint v3 queue state, and replay-safe recursive/agent v4 contracts while preserving frozen v1/v2/v3 behavior through one shared legacy transition.
- Added attached-only carriage composition with two derived fungible units, deterministic rollback-safe attach/detach and whole-consist lifecycle accounting, executable-action station timing, route-following consist rendering and player controls, structured composition, UUID-free checkpoint v4, and replay-safe recursive/agent v5 while preserving frozen legacy behavior and the GM-06d rider/removal deferrals.
- Bumped the deprecated Node 20 GitHub Actions in
.github/workflows/test.yml(actions/checkoutv4→v7,actions/setup-pythonv5→v7,actions/setup-nodev4→v7) onto the Node 24 runtime, verifying against each action's v7action.ymlthat every used input still exists and that the only removed inputs are unused ones; adversarially reviewed (Codex plus an independent harness reviewer; the Claude CLI reviewer was unreachable on an expired OAuth session) with the change gated on both hosted CI jobs passing. - Hardened the same workflow by pinning all three v7 actions to immutable full commit SHAs (
actions/checkoutv7.0.1,actions/setup-pythonandactions/setup-nodev7.0.0 — each also the current latest release) with# vX.Y.Zcomments; the SHAs were independently re-resolved from the officialactions/*repositories by an adversarial reviewer (overall PASS) and gated on green CI. - Hardened the deferred fleet edge cases under owner-approved soft-cap alighting: occupied locomotives can be queued for return with empty-preference selection and a guaranteed oracle-quiet one-batch rider drain, queued returns gained a live-only
cancel_unassignmentaction rejected across persisted v1-v5, line removal became a rider-conserving snapshot/rollback transaction that credits destination-shape deliveries and restores the complete progression/RNG footprint on failure, and a narrow unconditional reconcile seam repairs only provably-safe residual fleet shapes — with queue/cancel service-cache reconciliation closing a paused-window checkpoint crash found by adversarial review.
- Fixed the recursive oracle's critical
invalid-referencefalse positive on checkpoints taken while a metro traverses aPaddingSegment: the metroMotion current-segment station checks now accept the legitimately absentNoneendpoints exactly like the topology-segment checks, the GM-06d paused-queue pin asserts its scenario is finding-free end-to-end, a direct mid-padding reference-integrity regression covers the oracle, and the frozen v1-v5 fixture outcomes remain byte-exact. - Extracted the human application shell (GM-07a): new
src/app_controller.pyscreen-state machine (title, playing, pause menu, game over) with one factory-driven reconstruction path, new deterministicsrc/ui/menu_screens.pychrome, a rewrittensrc/main.pyloop that auto-starts playing undermax_framesand at the title otherwise, and aMediatorpause-reason model (user/menubehind the exactis_pausedbool facade) so Escape opens a modal pause menu that Space can never dismiss; checkpoints, observations, and all frozen artifacts stay byte-identical. - Fixed the GM-07a implementation-review findings: pause-menu controls now arm on press so a control fires only on a DOWN+UP pair inside its own rect (a drag released over Restart after a mid-drag Escape no longer discards the run), the
run_gameframe-composition branches are pinned by a mutation-checked loop suite driving the real controller (TITLEadvance(0), same-frame restart rebind to the new triple withadvance(0), gameplay-then-menu compositing under the held menu reason), andGAME_RULES.mdno longer claims keyboard speed keys clear the user pause. - Hardened both segment reference checks in the recursive oracle kind-aware:
Noneendpoint stations are now accepted only forPaddingSegmentrecords whilePathSegmentrecords require valid in-range station indices at the topology and metroMotion sites, restoring detection of a dangling traversed endpoint that the 83b62e5 blanket allowance made silent, with red-first regressions for both kinds at both sites and frozen v1-v5 fixture outcomes verified byte-exact. - Implemented versioned save/load (GM-07b): strict fail-closed save schema v1 (
src/save_schema.py+src/save_schema_records.py— exact keys, exact scalar types, ID grammar/global uniqueness, reference resolution, validate-equal derived fields, pinned ASCII canonical bytes), a pure attribute-only serializer with a save-local atomic writer (src/save_game.py), and the repo's first JSON-to-Mediatorloader (src/save_load.py— RNG overwrite, post-construction ID assignment, segments-before-metros rebind, direct-append over-capacity queues, synchronous button restoration, RNG-neutral service reconcile with persisted-timer re-apply); loaded games are checkpoint-identical (RNG included), honor pre-save entity IDs in structured actions, and replay byte-identical trajectories in-process and when fresh processes reload the same save file;saves/is config-owned and git-ignored,scripts/fixtures/save-v1.jsonis frozen with length/SHA pins; the two originally red byte-identity assertions were re-aimed at strictly stronger honest oracles (cross-process load→re-save idempotence plus checkpoint-equal regeneration) because per-processshortuuidminting makes fresh-build byte identity impossible by design. - Fixed the GM-07b adversarial-review findings (two independent NOT CLEAN lanes, one converging blocker): schema v1 now persists each metro's bound station-service action verbatim (nullable
serviceActionrecord with fail-closed timing/speed invariants) and the loader restores it without re-deriving, closing the reachable save-cannot-load and silent-divergence windows (codex seed-127, harness seeds 4501/9001 — regression-locked in lockstep vs never-saved controls under a UUID-normalized save-document oracle, sincecanonical_checkpointitself rejects those boundaries — a known pre-existing defect left for its own follow-up); RNG states are numeric-domain-pinned with residual setter failures normalized toValueError, path/metro station references validate against the active prefix (plus the saver's non-prefix live-list rejection), duplicate JSON keys are rejected at every object level, consecutive duplicate path stations and out-of-rangepathOrderare rejected, the atomic writer and the post-construction loader failure gained real fault-injection regressions, the cross-process proof now replays released active ticks under distinct hash seeds against an in-process control, the isolation scan covers all four save modules plusrecursive_checkpoint.py, and the v1 fixture was refrozen with the new field. - Retired the layout/render case from the frozen GM-03f input-coordinator differential (
scripts/verify_input_coordinator_differential.py), which had been unable to run since GM-06c added the pre-mutationvalidate_resource_control_layoutreserved-band check that its 200×100 and 10×20prepare_layoutprobes trip but the archived GM-03e baseline (7ff9d9c) predates; kept the still-comparable input-dispatch, progression-purchase, and speed-action cases, deletedscripts/input_coordinator_differential_layout.py, droppedrendering.game_rendererfrom the loaded-origin assertion, bumped the scenario tov2, and regenerated the committed golden/summary to 3 cases / 11 records / 57 events (a23179b6…, baseline == candidate == expected), reconciling the ARCHITECTURE.md verifier description and its GM-03e-baseline / GM-03f-extraction naming. - Fixed the GM-07b follow-up:
canonical_checkpointno longer raises (checkpoint runtime carriage graph is malformed, plus the siblingcheckpoint Metro service cache is stale) on legitimately reachable multi-locomotive boundaries where a later metro consumes an earlier metro's boarding rider inside one tick, leaving that metro's bound_station_service_actionno longer equal to the re-derivable oracle at the tick boundary — the exact real state GM-07b save/load already persists verbatim. The v4 checkpoint verifier now validates a bound service cache structurally (known kind, boarding-invariant timers, and a live passenger via the newfleet_validation.service_action_passenger_is_live) instead of demanding the oracle match, threaded through a new opt-inallow_stale_boundoncarriage_state_is_canonical/service_cache_is_canonicalthat defaults strict so the carriage-lifecycle guards, fleet-management, and path-replacement callers are unchanged;recursive_checkpoint_carriages._validate_service_cachemirrors the same structural contract and also stops rejecting the stale-reset null cache. Corruption is still rejected (unknown kind, dangling passenger, off-invariant timers, bound-while-off-station, null-with-nonzero-timers), no simulation behavior or serialized checkpoint bytes change (the action tuple is never serialized), and the frozen v1-v5 fixture outcomes stay byte-exact — verified with a red-first two-locomotive (seed 4501) + stale-reset regression, an end-to-endrun_scenarioproof (finding-free), the fullpy313suite, and an independent adversarial review (35,200 fuzzed states, no refutations). - Implemented atomic autosave, Continue, and menu integration (GM-07c, D-027): the human shell autosaves to a single
saves/autosave.jsonslot on pause-menu entry and on Exit to Title (before releasing the menu hold), keeps it on a mid-run window close, deletes it at thePLAYING->GAME_OVERpromotion and the game-over exits, and offers a title-screen Continue that resumes checkpoint-identically while releasing the menu pause and honoring a held user pause;AppControllergained optional inertbuild_from/autosaveseams (omitting both reproduces the GM-07a baseline exactly),main.run_gameowns the patchable module-levelAUTOSAVE_PATHplus the state-gated window-close save/delete, andui/menu_screens.pygained the three-button title layout,continue_availablepainting, and a byte-stabledraw_noticebanner — no schema, observation, or isolation-scan change, and no headless/agent/recursive/RL surface imports the save modules. Also fixed a latent cross-pygame-cycle font-cache staleness inmenu_screens._font(now a fresh per-call bundled font) that the newly-rendering title chrome exposed under the suite's per-classpygame.init()/quit()fixtures. - Closed the mutation-path twin of the GM-07b:C checkpoint fix: the same reachable stale-bound
_station_service_action(a later same-station metro consumed an earlier metro's cached boarding rider inside one tick) also made the strict oracle-derivingcarriage_state_is_canonical— directly and nested inside_queue_state_is_canonical— reject the carriage attach/detach precondition and full-fleet postconditions, socan_attach_carriage/attach_carriage/can_detach_carriage/detach_carriagesilently no-opped on every path during the one-tick self-healing window. Threaded the existing opt-inallow_stale_boundthrough_queue_state_is_canonicaland the threecarriage_managementguard sites (the shared_valid_host_and_pathprecondition plus both attach/detach full-fleet postconditions) so a carriage op tolerates an unrelated metro's stale-but-structural cache exactly as the checkpoint verifier does, while the target metro's own post-reconcile cache stays strictly oracle-bound (service_cache_is_canonical(..., allow_unbound=False)unchanged) and every fleet-management and path-lifecycle guard keeps its strict default. A stale target is reconciled away by the op itself; an unrelated stale sibling is committed-around untouched (enforced by the snapshot-equalitytransaction_state_matches) and rolled back verbatim on failure. Reclassified thewrong-holder/wrong-oraclecases of the GM-06c malformed-cache preflight from reject to permit-and-reconcile — they are the legitimate stale-but-structural shapes the checkpoint already blesses — while keepingmoving/nonexact-kind/wrong-timerstrictly rejected; a seed-9 public repro plus Case-B sibling-preservation attach/detach regressions pin it, no simulation behavior or serialized checkpoint bytes change, and the frozen v1-v5 fixture outcomes stay byte-exact. - Implemented the map-and-rules high-score leaderboard (GM-07d, D-028): new
src/highscores.pypersists lifetime deliveries tosaves/highscores.jsonas a strict schema-v1 document (exact-key validation before field access, non-ASCII content rejected, forward versions/bad types refused), with a purerecord_score(map/rulesVersion required and inputs validated) that returns a new board ranked map-asc/rulesVersion-asc/deliveries-desc with stable ties, capping only the recorded key so no other key is dropped, a START-EMPTY-tolerantload_highscoresthat never raises (even on a RecursionError from pathological nesting), and asave_highscoresthat validates the board before writing through its own save-local copy of the GM-07b canonical-ASCII atomic writer; it reuses the save-schema validators and canonical bytes and so joins the persistence isolation scan.AppControllergained an optional inerthighscoresrecorder seam that fires exactly once at thePLAYING->GAME_OVERpromotion (readingmediator.deliveriesonly when the seam is present, storing the result in publiclast_highscore_result),main.run_gamebinds the seam plus a patchableHIGHSCORES_PATHand a singlerecord_highscorethat both the promotion seam and the mutually exclusive window-close game-over record funnel through, and a new byte-stablemenu_screens.draw_best_indicatorpaints a "new best" banner after the renderer's game-over frame sogame_rendererstays at 494 lines; no schema, observation, protocol, or frozen-artifact change, and no headless/agent/recursive/RL surface imports the leaderboard. The escalated external Codex persistence review folded here (validate-before-write, cross-key isolation, required keys, single recorder); the eventless-game-over record leans on D-027's window-close net, with a deterministic-per-frame reconciliation left as a follow-up. - Fixed the GM-07d review's MINOR file-descriptor leak in both save-local atomic-writer twins (
save_game,save_highscores): whenos.fdopenitself raises (OOM/EMFILE) thewithnever takes ownership of themkstempdescriptor, so the raw fd leaked and -- on Windows -- thefinally'stemporary.unlink()then failed withPermissionError, masking the original error and leaving.tmplitter; both writers now track ahandle_openedflag and close the raw fd infinallywhen thewithnever ran (guarded against an already-closed fd, since some fdopen failure paths close it before raising), staying byte-for-byte identical. Red-first fault-injection regressions patchos.fdopento raise for each writer and assert the prior destination stays byte-intact, no.tmplitter remains, the injected error propagates unmasked, and the descriptor is closed; the success path and frozensave-v1fixture bytes are unchanged, and the fullpy313suite plus an independent adversarial review (no refutations) pass. - Closed the GM-07d follow-up with deterministic per-frame game-over reconciliation (GM-07e): extracted the
PLAYING->GAME_OVERpromotion out ofAppController.handle_eventinto a public idempotentreconcile_game_over()(a no-op unless stillPLAYINGandmediator.is_game_over), called at the top ofhandle_eventso the historical inline promotion is byte-preserved and once per frame inmain.run_gameaftersession.advancewith a render-state re-read, so a tick that ends the run with no promoting event now records the high score, deletes the autosave, and paints the best indicator the frame it ends — instead of lagging until the next incidental event or the window-close QUIT (which players rarely see, since pressing Restart records-and-leaves in the same event). The window-close QUIT record stays mutually exclusive: it fires only while the state is stillPLAYING/PAUSE_MENU, which the per-frame reconcile now closes first, so a finished run still records exactly once. Red-first controller and run-loop tests (test/test_gm07e_game_over_reconcile.py) pin eventless-record-once, idempotence, no-op-unless-playing, the reconcile-not-QUIT recorder surface, the indicator-after-renderer paint order, and no-autosave-recreation on the post-game-over QUIT; every mutation-pinned GM-07a/GM-07c/GM-07d run-loop andtest_maincomposition assertion stays green (full suite 1231/12). Adversarially reviewed across three independent lanes (Codex ultra plus two harness lenses) with no correctness defect found and five doc/test findings folded; no schema, observation, persistence-logic, or frozen-artifact change. - Closed the locomotive twin of the GM-07b:D carriage fix (GM-07b:E): the same reachable stale-bound
_station_service_action(a later same-station metro consumed an earlier metro's cached boarding rider inside one tick) also made the strict_queue_state_is_canonicalreject the locomotive assign/queue/cancel precondition and postconditions, socan_assign_locomotive/assign_locomotive,can_queue_locomotive_unassignment/queue_locomotive_unassignment,can_cancel_unassignment/cancel_unassignment, and the publicqueued_locomotives_for_pathcount silently no-opped (or read a spurious 0) on every path during the one-tick self-healing window even with free locomotives or a genuinely queued metro. Threaded the existing opt-inallow_stale_boundthrough the fleet-management guards via a new documented_fleet_state_is_canonicalhelper (the four candidate/precondition gates plus the assign ownership postcondition and the public queued-count read), added anallow_stale_boundparameter to_detach(passedTrueonly by the queue immediate-detach fast path; the automaticsettlereconciler and its own_detachcall stay strict) and toreconcile_queue_transition(the queue/cancel at-station rebind), so a locomotive op tolerates an unrelated metro's stale-but-structural cache exactly as the carriage and checkpoint guards do while the touched metro's own post-reconcile cache stays strictly oracle-bound (service_cache_is_canonical(..., allow_unbound=False)unchanged). Following the escalated adversarial review, hardenedassignitself to the full snapshot/rollback pattern the carriageattachuses --snapshot_transaction_stateplus a new symmetricadded_ownermode ontransaction_state_matchesplusrestore_transaction_state, replacing the lightweight owner-collection checks -- so an unrelated sibling is pinned by identity on commit and the whole state is restored verbatim on any failure (including an effectful metro factory) instead of relying on the factory being a pure constructor; removed the now-dead_is_exact_append/_restore_owner_lists/_restore_collectionhelpers. Path-lifecycle removal andsettlekeep the strict default (deliberately scoped, mirroring GM-07b:D's carriage-only scope). No save/checkpoint schema or serialization change and the frozen v1-v5 and save-v1 fixtures stay byte-exact (no frozen scenario acts inside a stale window); the fix intentionally makes the fleet controls -- and thus theircan_*-driven rendered enabled/disabled color and any post-action observation -- become active during the window, while the rendered fleet-button badge is unaffected because it counts the rawis_unassignment_queuedflag directly, which the fix never changes. Covered by a seed-9 public repro plus Case-B sibling-preservation cancel/queue-fast-path-detach regressions, effectful-factory commit-around and rollback regressions, a public-count assertion, and a settle-stays-strict pin intest_gm06c_carriage_stale_sibling.py::TestFleetOpsTolerateStaleCache. - Implemented the typed settings store and SETTINGS screen (GM-08a, D-029): new
src/settings.pypersists presentation-only preferences tosaves/settings.jsonas a strict schema-v1 document (exact-key validation, integer-percent volumes, non-ASCII/out-of-range rejected, validated before every write through its own save-local copy of the GM-07b canonical-ASCII atomic writer) with a FAIL-SAFEload_settingsthat returnsDEFAULT_SETTINGSon any malformed/forward-version/missing file and never raises; it reuses the save-schema validators and joins the persistence isolation scan.AppControllergained anAppScreen.SETTINGSstate and an optional inertsettingsseam, holding the value in publiccurrent_settings, reachable from the title and pause menus (appended after the existing entries so their rects stay byte-identical; opening from pause keeps themenuhold and Back returns to the origin), with fullscreen/reduced-motion toggles and 25%-step volume cyclers that persist through the seam.main.run_gameinjects the seam over a patchableSETTINGS_PATH, appliesfullscreenvia a change-gatedpygame.display.set_mode(FULLSCREEN|SCALED)with window-surface reassignment, and threadsreduced_motionintorenderer.draw; the kwarg-filtering_call_flexiblydispatch was extracted tosrc/rendering/flexible_draw.pysoreduced_motionreaches thestation/passenger/path_buttonblink predicates (held steady) and the station snap blip (suppressed) while every default-False path stays byte-identical andgame_rendererstays under 500 lines. Volumes are stored for GM-08b's audio consumer; settings touch noMediator/configbalance and change no save schema, checkpoint, observation, or frozen artifact, and no headless/agent/recursive/RL surface imports the store. - Implemented procedural-tone gameplay audio (GM-08b, D-030): new
src/audio.pysynthesizes short deterministic tones in-process (no external assets,pygame/numpyonly, its own constants) — a distinct SFX plays on delivery, line purchase, station unlock, game over (False→True edge), and endpoint snap, each scaled by the GM-08a master and SFX volumes.ProceduralAudioreads the mixer's actual negotiated rate/channels frompygame.mixer.get_init()and builds one channel-shapedSoundper event;create_audiodegrades to a no-opNullAudioon any device/build failure so audio-init never blocks play; a pure, duck-typed, tolerantsnapshot_of/diff_and_playper-frame counter differ plays one tone per newly-occurred delta. Audio is a puremain.run_gameconsumer at the post-reconcile_game_overhook (noAppController/Mediator/GameSession/rendering/schema change) that owns its own session reference and re-baselines on a session change so Continue/New Game/Restart never fire a spurious burst. Both adversarial review lanes (a harness lens and escalated external Codex ultra) independently caught the same MAJOR — themax_framesgate leaked a real mixer into the pre-existing unboundedrun_gameloop tests (which patch onlymain.pygamewhileaudio.pyholds its own) — fixed structurally by defaultingrun_game's backend to inertNullAudioand opting into the real mixer only at the__main__entry point (empirically verifiedmixer.get_init()staysNone); Codex's two MINORs folded (generate against the mixer's real sample rate; the same-frame-as-Continue purchase tone documented as best-effort like snap).audiolives outsiderendering/and joins both isolation scans, so headless, agent, recursive, and RL play open no device; the fullpy313suite stays green (12 skips) and anSDL_AUDIODRIVER=dummysmoke plays tones end-to-end. - Implemented the coached in-game tutorial (GM-08c, D-031), completing the GM-08 milestone: the owner chose a "coached seeded game" (a real
Mediator(seed=42)game with a coaching overlay) over a dedicated scenario. New puresrc/tutorial.pyobserves the live mediator each frame (the GM-08b snapshot pattern, stdlib-only, reads attributes only) and advances seven real-control-gated lessons — draw, reroute, add a train, deliver, overload pressure, pause, speed. A newAppScreen.TUTORIAL+ optional inertbuild_tutorialseam runs it (Escape skips to the title with the letterbox-cancel; never autosaves or records),main.run_gamedrivesadvance_tutorialonce per frame besidereconcile_game_overand paintsdraw_tutorial_overlayover the game frame, and the title gains a "Tutorial" entry appended after Settings (prior four rects byte-identical). An empirical seed/sim probe and the combined adversarial plan review drove two pre-code corrections: the tutorial mediator raises its ownoverdue_passenger_thresholdto10**9on the instance (a per-instance write, not aMediator/configchange) so the game never flipsis_game_over— which would freeze the sim and paint game-over chrome, soft-locking the time-dependent lessons — and reroute precedes the train (a metro mid-service persistently blocksreplace_path). The escalated external Codex lane then caught three reachable soft-locks the harness lane (CLEAN) missed — a reroute that soft-locks when a delete-and-redraw mints a fresh path id, a train step baselining at the metro cap, and a coldstart_state=TUTORIALbuilding an ordinary freeze-prone game — all fixed red-first (reroute accepts any route-topology change; train/pause/speed are current-state checks; the constructor starts the real tutorial on a cold TUTORIAL entry) with regressions. Seed 42's three distinct-shape stations make any drawn line deliver (verified ~9-14 s in a scripted-gesture headless run that completes all seven lessons and never game-overs);tutorialjoins the isolation scan and is imported only byapp_controller; noMediator/GameSession/rendering/schema/observation/checkpoint change; fullpy313suite green (12 skips), budgets held (tutorial.py222,app_controller.py393,main.py428,menu_screens.py289).
- Opened GM-09 (maps and geographic constraints) with GM-09a, the behavior-preserving Classic map abstraction (D-032): new data-only
src/maps.pyholds an immutable frozenMapDefinition(identity + station-shape palette as__post_init__-coerced tuples), theCLASSICdefinition capturing today's config values, and a version-awareresolve_map(map_id, version)that raises a clear named error rather than return the wrong map; it imports onlyconfig+geometry.type, somediator/get_entityconsume aMapDefinitionone-way with no pygame pull and no cycle.Mediatorgained an optionalmap_definition(defaultCLASSIC) threading the palette intoget_random_stations(new keyword-only params defaulting via None-sentinel to the config globals), andsave_game.serialize_gamegained a fail-closed_require_classic_mapguard (onlyclassic@1serializes, adding no bytes sosave-v1.jsonstays frozen). The refactor is byte-identical to pre-change behavior — construction (stations + path colors + both RNG states) and a 300-step trajectory reproduce pinned pre-change fingerprints for seeds 0/1, corroborated by a 60-seed list-vs-tuple equivalence check and, in review, an independent pre-change-code reconstruction plus a 20,000-seedchoicestress. This unit was scoped by a DUAL adversarial plan review (harness + external Codex ultra, both NOT CLEAN) that recommended splitting the roadmap's GM-09a to isolate this deterministic-behavior change from the versioned task-descriptor identity (now GM-09a2) and drove maps-data-only, the version-aware lookup, tuple immutability, the fail-closed save guard, and an RNG-state/color/trajectory determinism proof; the save-schema map field and high-scoremapDefinitionVersiondefer to GM-09f. The implementation review ran the harness lane (CLEAN, independent pre-change reconstruction) plus a broad empirical byte-identity proof because the external Codex lane declined the egress this run; fullpy313suite green (12 skips),maps.py81 lines, all budgets held. - Completed the GM-09 opening with GM-09a2, the versioned RL task-descriptor identity (D-033), under strict legacy-byte-compatibility.
rl.protocol.TaskSpecgained optionalmap_id/map_definition_version(appended last, defaultNone) with a validating guard;task_descriptoraddsmapId/mapDefinitionVersion/descriptorVersion:2ONLY for a map-bound spec, so a map-absent descriptor is byte-identical and keeps its exact legacy fingerprint (c2ef342f…verified; classic-bound →efec72da…). The training manifest gained an explicit v3 (_V3_KEYS = _V2_KEYS | {mapId, mapDefinitionVersion}); v1/v2 stay map-free/exact-key-valid,to_dictemits history for v2+v3, and__post_init__keeps schema and map in lockstep.create_training_manifestselects v3 only when map-bound;task_spec_from_manifestreconstructs the map (None for v1/v2 → legacy hash) andPlayerEnvThunk/make_env_thunksthread it to subprocess workers;PlayerPixelEnvgained map params (default None) resolving the map into the Mediator.scripts/train_rl.pygained--map(fresh omitted → map-free v2; a resumed run INHERITS the map from its manifest — the resume manifest is parsed before the spec — so a genuine pre-map run still resumes, review MAJOR-1). The real git-ignored legacy manifest is committed sanitized toscripts/fixtures/legacy-training-manifest-v1.json(reconstructs toc2ef342f…in CI), andEXPECTED_LF_TRAININGwas re-pinned since the training sources legitimately changed (maps.pystays out ofTRAINING_SOURCE_PATHSbut is captured bycompute_content_fingerprint). Save-schema map field and high-scoremapDefinitionVersiondefer to GM-09f; fullpy313suite green (1357 tests, 12 skips). - Added the first alternate map with GM-09b, the
RIVERmap + terrain/station regions (D-034).MapDefinitiongained additive deeply-immutablespawn_regions/riversrect tuples (tuple-coerced + positive-area-validated);RIVERis a central vertical river splitting the play area into twostation_size-eroded banks, registered soKNOWN_MAP_IDS == ("classic", "river"). Region-aware spawning threadsspawn_regionskeyword-only throughget_random_stations → get_random_station → get_station_spawn_position, rejection-sampling candidates onto a bank (bounded, named error on exhaustion) with a FALSY no-region fast path, so CLASSIC (empty regions) draws byte-identically — thetest_gm09a_mapsfingerprints and the frozensave-v1.jsonare unmoved. A new smallsrc/rendering/terrain_renderer.pypaints the river at the top ofGameRenderer.draw(so the RL pixel observation sees it too); the save guard was hardened to STRUCTURAL equality (a forged classic-with-terrain is rejected). Nogeometry.Polygon(shapely/uuid/brokencontains) — plain tuples +pygame.draw.rect+ a pure point-in-rect test. The scope decision (defer the save-v2/high-score-v2 migration to GM-09f, keep the fail-closed guard) was dual-plan-review-confirmed sound: no save-capable path constructs a river Mediator in GM-09b. Fullpy313suite green (1372 tests, 12 skips);game_renderer484,terrain_renderer31, all budgets held. - Delivered the river-crossing tunnel-budget mechanic with GM-09c, making the
RIVERmap a real obstacle (D-035). A new dependency-lightsrc/crossings.py(imports onlygeometry.point) owns the pure Liang-Barsky crossing geometry --segment_crosses_bandreturns the entry point (a zero-length graze deliberately not counted),path_crossingscounts one entry per band on a path's CENTERLINE (skipping a 2-station loop's retraced closure so it can't double-charge) -- plus the shared route-edit gatewithin_tunnel_budget.MapDefinitiongainedtunnel_budget: int | None(RIVER=3, CLASSIC=None);Mediatorexposesnum_tunnels/consumed_tunnels/available_tunnelsas three DERIVED properties reading the livemap_definition, so removal/reroute refund for free with zero snapshot state and a swapped map never fails open. The gate counts the REAL resolved draft (never a route predicted from raw indices) atend_path_on_stationand thefinish_path_creationcommit boundary;replace_pathgates in preflight.env.observeadds atunnelsblock as a SIBLING offleet(never a fleet key -- the checkpoint's exact whitelist), andterrain_renderer.draw_crossingspaints a tunnel-portal marker on each crossing; CLASSIC (no rivers, unbounded) is byte-identical. THREE dual adversarial impl-review rounds (harness + external Codex ultra) drove it: round 1 caught a commit-boundary bypass, anum_tunnelscached-at-construction fail-open on map swap, and a snap-blip leak; round 2 caught a route-predicting pre-check that false-rejected an explicit-closure loop[X,Y,X]; round 3 caught that the round-2 blip/button cleanup, folded into the SHARED abort/button-assign methods, broke CLASSIC byte-identity and mis-owned a reclaimed-color blip. Final disposition reverted those shared methods byte-for-byte to pre-change (CLASSIC stays byte-identical; the mechanic is just the two gates, which short-circuit on the unbounded map); the transient blip and ghost-button are pre-existing abort behaviors present in HEAD, deferred to a scoped follow-up. Fullpy313suite green (1402 tests, 12 skips); RIVER budget=3 empirically verified solvable (a connected cross-river network builds, a 4th distinct crossing is rejected, removal/reroute refund). - Added the second alternate map with GM-09d,
DELTA(D-036) -- two vertical rivers (a delta's twin channels) splitting the play area into THREE land banks,tunnel_budget=4. It is a PUREMapDefinitionaddition with NO new machinery: its purpose is to prove the GM-09b/GM-09c map layer generalizes, and it does -- the region-aware spawn (_sample_positionaccepts any of the three banks), the terrain/crossing renderers andpath_crossings(loop over all bands), the derived tunnel count/gate, thetunnelsobservation, and the fail-closed save guard already handle N regions/rivers with no code change. A line spanning the whole map crosses both channels and uses two tunnels, exercising the multi-band crossing count more than the single-riverRIVER;KNOWN_MAP_IDS == ("classic", "delta", "river"). CLASSIC and RIVER stay byte-identical (unchangedtest_gm09a_mapsfingerprints + frozensave-v1.json, verified at seeds 0/1/4207). Dual adversarial impl review (harness SHIP + external Codex ultra FIX-FIRST, both MINOR-only, no BLOCKER/MAJOR): Codex verified across 10,000 seeds (all 27 initial bank sequences; budget 4 meaningful) and confirmed--map deltagenuinely trains on DELTA and the checkpoint holds across schema v1-v4. All six test-hardening MINORs folded (membershipKNOWN_MAP_IDS, RNG-trajectory determinism,ValueError+delta@1save guard, two-portal render, both-channels terrain, over-budget ceiling); the two latent geometry MINORs (a 1px glyph-vertex seam and small-screen degeneracy, both pre-existing and shared with RIVER) were re-dispositioned and documented. Fullpy313suite green (1416 tests, 12 skips);maps.py238 lines; GM-09b's exact-KNOWN_MAP_IDStest loosened to membership so later maps don't break it.
- Added the third alternate map with GM-09e,
LAKE(D-037) -- a single bounded central lake (spanning no screen edge),tunnel_budget=3, again a PUREMapDefinitionaddition with NO new machinery. It exercises the one map-layer generality dimension RIVER/DELTA never did: a PARTIAL band (bounded in x AND y). A line whose centerline passes through the lake spends a tunnel; a line routed around it (bending at an intermediate station beside the lake) spends none. The land is a frame of four overlapping strips whose union is the screen minus the lake, and the reused spawn/render/crossing/gate/save code handles it unchanged, so CLASSIC/RIVER/DELTA stay byte-identical (test_gm09a_mapsfingerprints + frozensave-v1.jsonunmoved, verified at seeds 0/1/4207). Dual adversarial impl review: harness SHIP (6000-station spawn sweep, full partial-band crossing matrix), external Codex ultra FIX-FIRST with a MAJOR the harness endorsed the opposite of -- my load-bearing CLAIM that "the lake never gates connectivity" is FALSE (lines bend only at stations, so at an exhausted budget a station whose only routes cross the lake is gated until a tunnel is freed, exactly as on the rivers). The CODE was correct; the defect was my mischaracterization, corrected across the map comment,GAME_RULES.md, D-037,ARCHITECTURE.md, and the test (the lake makes crossing more often avoidable, not always). Codex also caught the edge-collinear crossing miscount GM-09c had DEFERRED as "unreachable" but LAKE made reachable (integer water edges); folded by promotingcrossings.segment_crosses_bandto STRICT-interior semantics (a segment along an edge counts zero), verified to leave RIVER/DELTA counts unmoved. All test-hardening folded (14 tests: strict-interior edge, real budget ceiling, four-strip-frame spawn). Fullpy313suite green (1428 tests, 12 skips);maps.py292 lines. GM-09f (in-game menu + save-schema map field + high-scoremapDefinitionVersion) is next -- the deferred map/save integration. - Began the map/save integration with GM-09f, the SAVE-SCHEMA v2 map field (D-038) -- the first of a plan-review-driven split (save-schema, then high-score identity, then the in-game menu). The save schema gains
SAVE_SCHEMA_VERSION_V2 = 2(a superset of v1) with two additive top-level keysmapId/mapDefinitionVersion, so a non-Classic game (river/delta/lake) saves and loads with its map intact;validate_saveis two-phase (read + support-checkschemaVersionwith a named error BEFORE choosing the version-aware exact-key set, so a v1-doc-with-map-keys and a v2-doc-without both fail closed).serialize_gamereplaces the old_require_classic_mapguard with a fail-closed pair: STRUCTURALmap_definition == resolve_map(id, version)(generalizing GM-09b's== CLASSIC, since a v2 save records only the identity and rebuilds terrain from the registry on load) and a shared_require_legal_map_state(stations on the map's land,consumed_tunnels <= num_tunnels) applied on serialize AND post-load, so a forged illegal state is refused both ways.deserialize_gamesynthesizesclassic@1for a v1 doc (keys absent) and resolves the map fail-closed for v2 (unknown id / unsupported version raise), threadingmap_definitioninto the Mediator; tunnel counts stay derived. The byte-frozensave-v1.jsonis unchanged and still loads as Classic; the deterministic v1->v2 header-only upgrade is pinned by a new frozensave-v2-classic.json(15485 bytes, SHA60f2bc16...-- exactly Codex's prediction) that the idempotence + cross-process determinism tests target. HIGH-RISK, so escalated to a DUAL plan review (both lanes REVISE, direction + split confirmed) that drove the two load-bearing choices: the guard must be STRUCTURAL (mere resolvability fails open into the GM-09b forged-Classic bug -- verified: 2 of 20 seed-0 CLASSIC stations sit in RIVER's band) and identity alone needs STATE-legality (a valid identity + illegal state is still corrupt). The DUAL impl review (harness SHIP + Codex FIX-FIRST) folded a latent serialize fail-open (getattr(...) or CLASSICwould coerce a FALSEYMapDefinitionintoclassic@1and lose its terrain -- now defaults only onis None) and made_validate_map_identitya true non-empty-ASCII/no-whitespace mirror ofrl.manifest_schema(both matched the code-vs-D-038-contract gap), with regressions plus a forged-over-budget LOAD test. Fullpy313suite green (1450 tests, 12 skips); the three GM-09b/d/e "not serializable" tests flipped to round-trips (the forged-classic rejection stays green). GM-09f2 (high-scoremapDefinitionVersion) is next, then GM-09f3 (in-game menu, last so it can't feed an alternate map to the still-classic-hardcoded score recorder). - Continued the map/save integration with GM-09f2, the HIGH-SCORE map identity (D-039) -- the second of the GM-09f split, landing map-awareness in the recorder BEFORE the menu makes non-Classic maps selectable, so GM-09f3 needs zero recorder change. Both game-over surfaces UNIFY on the live mediator:
app_controller._record_highscorehands the seamself.mediator(not.deliveries) andmain.run_game's promotion closure drops itsSimpleNamespace(deliveries=...)wrapper, so the frame-accurate reconcile and the window-close QUIT both call the IDENTICALrecord_highscore(mediator), which readsmediator.map_definition.{map_id, map_definition_version}DIRECTLY (noor classicdefault -- a missing map records nothing rather than mislabelling).highscoresbecomes schema v2 keyed by the full(map, mapDefinitionVersion, rulesVersion)identity via one shared_identityhelper (sort + cap + rank, so no predicate keys on a subset), withrecord_scoregaining a requiredmap_definition_versionand the entrymaptightened to the save's mapId grammar;stateContractstays stable. A legacy v1 board is NOT migrated -- START-EMPTY -- because a v1map="classic"label is not provably accurate (the recorder was classic-hardcoded while GM-09f made non-Classic saves loadable via Continue), so synthesizingclassic@1would preserve contamination. HIGH-RISK -> DUAL plan review (both REVISE, design UPHELD) drove the whole-mediator seam (REQUIRED to keep MAJOR-3: a minimal context would force the controller to read the map), the START-EMPTY pivot (Codex MAJOR-2, which also killed a migrate-before-validate hazard), the ONE-_identity-helper rank fix (else classic@2 miscounts against classic@1), and the two omitted test files (test_gm07d_run_game_loopreal-recorder stubs +test_gm07e's local spy). Fullpy313suite green (1459 tests, 12 skips);highscores.py272 lines. GM-09f3 (in-game map menu) is the final GM-09f sub-unit. - COMPLETED GM-09f with GM-09f3, the in-game MAP MENU (D-040) -- the payoff that lets a human pick
classic/river/delta/lakefrom the title.AppControllergainscurrent_map_id(default classic), cycled by an appended titlemapcontrol (title_layoutappends the key so prior title rects stay byte-identical;draw_title_screengainscurrent_map_idand paints aMap: {Name}button;mainthreadscontroller.current_map_id). Thebuild_gameseam becomes uniformlyCallable[[str], GameTriple]--main.run_game'sbuild_game(map_id)resolvesmap_by_id(map_id)intoMediator(map_definition=...)(every downstream layer was already map-aware, GM-09a-f2). NEW GAME / ENTER build the picker; RESTART (pause + game-over) rebuilds the CURRENT game's map read live offself.mediator.map_definition.map_id(_restart_current_game), so restarting a Continued River game gives River even with the picker on Lake; Continue installs the SAVED map; the tutorial stays Classic. Dual plan review (both REVISE, architecture UPHELD): Codex caught the Restart-switches-map MAJOR the harness rated acceptable, drove the uniform seam arity, the 11-callable fake update (incl. the dangerous_title_build_game(mediator=None)positional collision), and the crossing-gate composition test; alphabetical cycle order kept. Editing app_controller/main/menu_screens rotates the live RL content fingerprint (expected; no fixture repin --EXPECTED_LF_TRAININGpins only training sources). Fullpy313suite green (1472 tests, 12 skips); app_controller 424, main 440, menu_screens 297 lines. GM-09 (maps + save/high-score/menu integration) is COMPLETE; GM-10 (weekly progression) opens next. - Closed the GM-09c abort-inertness follow-up (
task_384488d0), fixing the two PRE-EXISTING non-inert traces a cancelled path draft left inPathLifecycle.abort_path_creation-- both surfaced by the GM-09c review but deferred there because the obvious fixes broke CLASSIC byte-identity. (1) The transient snap-blips a draft paints as it grows (add_station_to_path/end_path_on_station) leaked into the canonical checkpoint (serialized raw, pruned only byincrement_timeon expiry), so a headlessMiniMetroEnvrollout checkpointed a cancelled draft indefinitely;_paint_creation_snap_blipnow records each painted blip (the tuplestart_snap_blipactually appends --Station.start_snap_blipnow RETURNS it, coupling the receipt to the real append so a non-appending station leaves no phantom) and abort drops exactly those by LAST value-match, which -- sincestart_snap_bliphas one caller and only one draft is live -- is provably the draft's own blip even when a removed line's reclaimed-color blip lingers beside it (identity is unusable becauseprune_visual_effectsrebuilds the tuples each tick; a color match would erase the survivor). (2) A mid-draftremove_pathrunsassign_paths_to_buttons, binding the still-drafting path to a button; abort now detaches that one mapping (path_to_button.pop+PathButton.remove_pathif it still points at the draft) surgically -- NOT a full reassign -- so no colored button points at a removed line.finishforgets a committed draft's receipts (no unbounded bookkeeping). Byte-identity is exact and PROVEN by a whole-src HEAD-shadow differential: a drag-then-FINISH (committed line) and the ghost scenario are byte-identical to pre-change HEAD, and only a drag-then-ABORT differs, by exactly the removed draft blips (the button detach is checkpoint-invisible --button.pathderefs toNoneeither way);save-v1.json/save-v2-classic.json(which serializesnapBlips, empty in the fixtures) and the GM-09a construct/trajectory fingerprints are unmoved. TDD: six red-first tests (fully-inert CLASSIC checkpoint, reclaimed-color collision, an adversarial same-time ordering case that a remove-FIRST would fail, duplicate re-snap, ghost button, no-op-when-unbound guard) plus a component-level abort-detach test; the GM-09ctest_rejected_multistation_creationwas strengthened from RNG-inert to FULLY checkpoint-inert. Triple adversarial review (two harness lanes -- byte-identity + correctness/contract, both HOLD -- and external Codex ultra) because the last three review rounds each caught a byte-identity regression in this exact area; Codex caught a stale-doc regression BOTH harness lanes missed (this ARCHITECTURE.md GM-09c note and thefinish/end_pathcomments still said abort was "unchanged / left to a follow-up") and drove the receipt-to-append coupling and the commit-time receipt clear. Fullpy313suite green (1484 tests, 12 skips);path_lifecycle.pygains three small helpers, no new module. - Opened GM-10 with GM-10a, the simulation CALENDAR (D-041) -- the foundation for weekly progression. A "week" is
config.WEEK_LENGTH_STEPS(1200 ≈ 20s at 1x);Mediator.increment_time, AFTER the complete tick (post queued-return settlement), holds a new"week"pause reason when the calendar is enabled, a new boundary crossed (old//W < steps//W), and not game over."week"joins_PAUSE_REASONS(never cleared by Space/speed);week_indexissteps-derived (no new persisted scalar);resolve_week_boundary()releases it. The calendar is OPT-IN, default OFF -- only INTERACTIVEmain.run_game(build_game/build_from, gated onmax_frames is None) enables it, so RL/tutorial/headless never pause. The human shell addsAppScreen.OFFER:reconcile_week_boundary()(per-frame AFTER game-over reconcile, cancelling any gesture) promotes to a modal whose armed Continue resolves the week; window-close mid-offer resolves+autosaves; offer-frame audio consumed silently; saving blocked while pending. HIGH-RISK -> DUAL plan review, both REVISE (harness 1 BLOCKER; Codex 2 BLOCKER + 4 MAJOR with reproduced counterexamples). GATING to the human shell resolved the BLOCKERs structurally: my first plan resolved a headless freeze only inMiniMetroEnv._complete_step, butPlayerPixelEnvdrives viaadvance_exactand the tutorial is a third direct-Mediator shell -- all would soft-lock at step 1200; gating (week_calendar default OFF) means the branch is never taken off the human path (no env.py/checkpoint/save change, no determinism risk). Codex also refuted my "pause is trajectory-invariant" probe (it bypassed the FixedStepClock cadence) -- gating moots it. The hold-after-full-tick (settlement), terminal precedence, gesture-cancel+arming, and window-close edges were all folded with pinned regressions. The DUAL impl review then confirmed the production code CORRECT on both lanes, with all findings TEST-STRENGTH: Codex mutation-proved six survivors the harness rated shippable (an exact-landing-only hold, a hold-before-settlement, a dropped not-game-over guard, a truthy-not-is TrueOFFER guard, a wrong letterbox-cancel event, and a missing run-loop OFFER promotion/QUIT path), each now pinned (a genuine-crossing speed-4 test, a queued-settlement-parity test, a live-Mockis Truetest, an exact-cancel-event assertion, and real-run_gamegating + OFFER-loop integration tests). Fullpy313suite green (1507 tests). GM-10b (dedicated-RNG offers) opens next. - Continued GM-10 with GM-10b, the dedicated-RNG weekly OFFER GENERATOR (D-042). A new stdlib-only
src/offers.py(OfferKind/Offer/puregenerate_offers) drawsOFFERS_PER_WEEK(2) DISTINCT upgrade offers from a map-appropriate pool (New Line / +1 Locomotive / +1 Carriage, plus +1 Tunnel only on a finite-tunnel map);Mediator._maybe_hold_week_boundarystorescurrent_offersat the hold andresolve_week_boundaryclears them;draw_offer_screenpreviews the labels read-only. The offer RNG is a dedicated per-weekrandom.Randomderived READ-ONLY frompython_random.getstate()+week_index— a DUAL-PLAN-REVIEW pivot: Codex BLOCKED the first plan (a persistedspawn(3)stream deferred to GM-10h would RESET on Continue and diverge, violating README's "Continue resumes exactly"), so offers are instead derived from the already-persisted gameplay RNG state, making them Continue-EXACT with ZERO new save/checkpoint/observation bytes and gameplay-INERT (getstate consumes no draws — station spawns stay byte-identical, every frozen fixture untouched). Gated to the human shell like the calendar, so RL/headless/tutorial never generate (current_offersstays()). Empirically pre-validated (cadence ~4-6 weeks/game; separate-stream inertness; spawn byte-compat; Continue-exactness of the boundary python-state — all proven before planning). Dual plan review (harness REVISE + Codex BLOCK → the stateless pivot) + dual impl review folded. Applying a choice is GM-10c, per-kind effects GM-10d-g, applied-offer persistence GM-10h (which must not trail GM-10c). Fullpy313suite green (1527 tests). - Continued GM-10 with GM-10c, the week-boundary CHOICE CONTROLS (D-043). The GM-10b read-only preview becomes interactive:
menu_screens.offer_menu_layout(width, height, count)returns one button per offer (offer_0..offer_{count-1}),draw_offer_screenpaints them, andAppController._handle_offerarms a button on press and, on a matching release (the GM-10a arming discipline, so a stale gameplay release cannot choose), callsMediator.resolve_week_boundary(current_offers[i]).resolve_week_boundary(offer=None)gains the optional chosen offer: it dispatches to a new_apply_offer(match offer.kind, namedValueErroron an unknown kind) then clears + releases;Noneis the window-close forced resolve (unchanged). The per-kind arms are NO-OP stubs — choosing changes NO game state, so GM-10c is Continue-safe with ZERO new persisted bytes (locked by a test asserting every kind leaves the fullserialize_gamedoc byte-identical). The real effects are GM-10d-g: NEW_LINE can ride the already-persistedpurchased_num_paths(Continue-safe standalone), while LOCOMOTIVE/CARRIAGE hit_require_running_configand TUNNEL needs a persisted bonus, so those land with GM-10h. Fullpy313suite green (1540 tests). - Continued GM-10 with GM-10d, the FIRST real per-kind offer effect (D-044): choosing NEW_LINE unlocks a free line.
NetworkProgression.grant_free_path()bumpspurchased_num_paths(capped atnum_paths, noline_creditsspend —record_path_purchaseminus the cost);Mediator._grant_free_linecalls it and refreshes the derived caches viaupdate_unlocked_num_paths()(the exact purchase-flow refresh), wired into the_apply_offerNEW_LINE arm (locomotive/carriage/tunnel stay no-op — GM-10e/f/g). Empirically proven Continue-safe standalone (probe: grant → purchased 1→2, unlocked 1→2, credits unchanged; serialize→deserialize reproduces both;numPathsunchanged so_require_running_configholds), so NO save/checkpoint-schema change and GM-10d precedes GM-10h. The GM-10c all-kinds-inert test narrowed to the three still-stub kinds. Known limitation (GM-11 balance): a NEW_LINE offer at the line cap is a wasted no-op pick. Dual impl review (harness SHIP + Codex FIX-FIRST → production correct by BOTH; folded 4 gaps — Codex caught a robustness MAJOR (resolve now CONFINES application to a currently-presented pending choice, so no out-of-band/headless call can grant an upgrade and bypass the economy) + a mutation-weak cap>=(an above-cap test now pins it) + an unpinned unlock-blink + a stale comment). The GM-10a-d week/offer LOGIC was factored into a newsrc/weekly_offers.pyWeeklyOffersfacade (D-023) becausemediator.pycrossed the 1000-line hard ceiling; the extraction is behavior-preserving (mediator 940 lines; all offer tests green). Fullpy313suite green (1550 tests). - Continued GM-10 with GM-10h, the fleet/tunnel upgrade-bonus PERSISTENCE (D-045) -- the prerequisite the still-stub GM-10e/f/g effects need, pulled ahead of them because a fleet/tunnel bonus is not Continue-safe today. An additive save-schema
SAVE_SCHEMA_VERSION_V3 = 3(SUPPORTED = {1,2,3}) adds ONE keytunnelBonusand v3-relaxessave_load._require_running_configso a grown fleet loads:numPaths == configalways,numMetros/numCarriages == configfor v1/v2 but>= configfor v3 (the fleet is persisted as its grown TOTALS -- no bonus field -- because 17 tests + the carriage rollback assignnum_metros/num_carriages, so they can't be derived). The tunnel gains a storedMediator.tunnel_bonusfolded intonum_tunnelsAND, the load-bearing fix, intocrossings.within_tunnel_budget(which reads the map budget directly, so a bonus threaded only throughnum_tunnelswould show in the observation/legality yet never unblock a crossing).serialize_gameruns a new_require_valid_upgrade_stateFIRST so a below-config fleet or a nonzero tunnel bonus on an unbounded map is rejected BEFORE the atomic write (a desynced/forged state can't clobber a valid autosave). v1/v2 fixtures stay byte-frozen; a new frozensave-v3-classic.json(15501 bytes) pins the v2->v3 upgrade; the three save contract tests repoint. NO checkpoint change (a bonus absorbs into the totals; RL never applies an offer). HIGH-RISK persistence migration -> DUAL plan review (harness REVISE + Codex ultra BLOCK, 2 BLOCKER + 5 MAJOR) drove a design PIVOT to the simpler relax-pin-not-field design (Codex's own suggestion), resolved the serialize-clobber BLOCKER via the pre-write guard, scoped the checkpoint out, re-homed mid-offer persistence to GM-10i, and rejected a nonzero tunnel bonus on unbounded maps (reachability). Fullpy313suite green (1566 tests). GM-10e (locomotive upgrade -- a trivialnum_metros += 1arm on this infrastructure) opens next. - Filled the LAST three per-kind offer EFFECTS (GM-10e/f/g, D-046) on the GM-10h persistence infrastructure, delivered TOGETHER since each is a single line:
weekly_offers.apply_offer's LOCOMOTIVE/CARRIAGE/TUNNEL arms becomehost.num_metros += 1/host.num_carriages += 1/host.tunnel_bonus += 1. No cache refresh (unlike NEW_LINE's button locks) --available_locomotives/available_carriages/num_tunnels/available_tunnelsall derive, and the grown state persists via save-schema v3 with no further schema work (Continue-exact, proven by round-trip). TUNNEL is offered only on a bounded map (the pool excludes it on CLASSIC), so the arm needs no guard; all three run only through the confinement-guarded human apply path, so RL/headless never applies one. The GM-10c-era "stub kinds are state-inert" test was retired (no kind is a no-op now); each effect's growth + CONTAINMENT is pinned in the newtest_gm10efg_effects.py(incl. the applied-TUNNEL-unblocks-a-real-crossing path through the live gate). Dual impl review (harness SHIP + Codex FIX-FIRST -> production correct by BOTH lanes; folded 4 test-strength gaps -- full-serialize_game-doc containment, an end-to-end slot-usability check that the grown fleet total is really ASSIGNABLE/ATTACHABLE not just a bigger derived count, anavailable_tunnelsreadout assertion, and a stale GM-10d test name/doc). Fullpy313suite green (1575 tests). GM-10 now has its full upgrade set (New Line + Locomotive + Carriage + Tunnel, all persisted); GM-10i (mid-offer PENDING-offer persistence, re-homed from GM-10h) completes GM-10, then GM-11/12/13 to v1.0. - Completed GM-10 with GM-10i, mid-offer PENDING-offer PERSISTENCE (D-047): a mid-offer save now records the held "week" boundary so a Continue reloads INTO the modal re-presenting the SAME offers (was: the window-close force-resolved past it). An additive save-schema
SAVE_SCHEMA_VERSION_V4 = 4(SUPPORTED = {1,2,3,4}) adds ONE keypendingOffers(the ordered shown offer kinds) + gates the"week"pause reason into a v4-only vocabulary. Design PIVOT from the dual plan review: STORE the offer kinds rather than re-derive on load, becauseWEEK_LENGTH_STEPS/OFFERS_PER_WEEK/the pool are provisional GM-11-tunable defaults (Codex BLOCKER-1: a re-derive would diverge across a balance change), so a v4 save stays self-contained.serialize_gameruns_require_valid_pending_offersFIRST (offers == the canonical derivation when held, else empty) so a desynced tuple can't be written;deserialize_gamerestores verbatim, rejecting a "week"+isGameOver save and a TUNNEL-on-unbounded save. Both plan lanes also caught the version-gate BLOCKER (the v3-only fleet-pin/validators must extend to v4 or a grown-fleet mid-offer save fails Continue — invisible to a fresh-start TDD) — every gate widened to explicit v4 membership. v1/v2/v3 fixtures byte-frozen; new frozensave-v4-classic.json(v3→v4 upgrade) +save-v4-river-pending.json(capability). NO checkpoint change (RL never holds a boundary). HIGH-RISK persistence migration → DUAL plan review (harness REVISE + Codex BLOCK → the persist-not-re-derive pivot + the v4 gate matrix) + DUAL impl review (harness SHIP w/ real mutation probes + Codex FIX-FIRST → matrix correct by BOTH; Codex caught the serialize== canonicalre-save-instability → the load-symmetric guard; folded the cross-version re-save, pool-legality/malformed rejects, unknown/duplicate-kind + v1/v2-reject-grown-fleet tests, the river-fixture byte pin, and stale docstrings). Fullpy313suite green (1599 tests). GM-10 COMPLETE (calendar + offers + choice + 4 effects + mid-offer persistence); GM-11 (balance/recursive playtest) opens next toward v1.0.