Skip to content

Latest commit

 

History

History
189 lines (156 loc) · 78.1 KB

File metadata and controls

189 lines (156 loc) · 78.1 KB

2026-02-08

  • Added programmatic play in src/env.py with 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_ms through rendering so blink timing is deterministic.

2026-02-14

  • 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.md describing unlock thresholds and randomized line color behavior.
  • Expanded GAME_RULES.md into 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.md with 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.md milestones.
  • 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.md and GAME_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_stations in src/config.py (unlock milestones now generate up to 20 stations).
  • Updated path unlock milestones to [0, 90, 300, 650] in src/config.py.
  • Added pre-timeout passenger warning blink: passengers in the last 10 seconds before passenger_max_wait_time_ms now 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.md line/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.md to 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.

2026-02-15

  • 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 (Buy and 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 courier via shared font_name config.
  • Expanded README.md programmatic play docs to list the MiniMetroEnv API, action schemas, valid input constraints, and observation/step return fields.
  • Added programmatic buy_line action to purchase metro lines through env.step(...), with optional path_index targeting and validation.

2026-02-16

  • 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.

2026-02-17

  • 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.md to 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.

2026-04-28

  • Added Python/py313 workflow guidance in AGENTS.md with a CLAUDE.md shim, 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 in current/, completed work in done/, and completed full and agents-repo-fit themes migrated under done/.

2026-07-10

  • 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.

2026-07-11

  • 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.

2026-07-12

  • 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 uint8 temporal-history ring with exact multiscale chronology, zero pre-history, isolated terminal/reset stacks, fail-closed recovery, contiguous VecFrameStack equivalence, 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.

2026-07-13

  • 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 Mediator facade 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 Mediator facade while preserving public methods, RNG/BFS and mapping lookup order, live passenger-list mutation timing, travel-plan identity and ownership, and passenger-delivery behavior.

2026-07-14

  • Extracted the 12 path-lifecycle transitions into a dependency-light stateless component behind unchanged real Mediator methods, 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 reducing src/mediator.py below 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.

2026-07-19

  • Extracted 16 passenger-flow and simulation-transition algorithms into a dependency-light stateless component behind unchanged Mediator methods, preserving late collaborator resolution, three fresh graph phases, live iterator and partial-failure timing, and deterministic gameplay while reducing src/mediator.py to 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 Mediator methods, preserving canonical facade state, late dependency and public-hook resolution, subclass/evaluation-order behavior, and player-equivalent control semantics while reducing src/mediator.py to 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.

2026-07-20

  • 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 HEAD byte 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 -> 4 pixel 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.

2026-07-21

  • 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/checkout v4→v7, actions/setup-python v5→v7, actions/setup-node v4→v7) onto the Node 24 runtime, verifying against each action's v7 action.yml that 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/checkout v7.0.1, actions/setup-python and actions/setup-node v7.0.0 — each also the current latest release) with # vX.Y.Z comments; the SHAs were independently re-resolved from the official actions/* 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_unassignment action 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.

2026-07-22

  • Fixed the recursive oracle's critical invalid-reference false positive on checkpoints taken while a metro traverses a PaddingSegment: the metroMotion current-segment station checks now accept the legitimately absent None endpoints 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.py screen-state machine (title, playing, pause menu, game over) with one factory-driven reconstruction path, new deterministic src/ui/menu_screens.py chrome, a rewritten src/main.py loop that auto-starts playing under max_frames and at the title otherwise, and a Mediator pause-reason model (user/menu behind the exact is_paused bool 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_game frame-composition branches are pinned by a mutation-checked loop suite driving the real controller (TITLE advance(0), same-frame restart rebind to the new triple with advance(0), gameplay-then-menu compositing under the held menu reason), and GAME_RULES.md no longer claims keyboard speed keys clear the user pause.
  • Hardened both segment reference checks in the recursive oracle kind-aware: None endpoint stations are now accepted only for PaddingSegment records while PathSegment records 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-Mediator loader (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.json is 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-process shortuuid minting 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 serviceAction record 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, since canonical_checkpoint itself 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 to ValueError, 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-range pathOrder are 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 plus recursive_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-mutation validate_resource_control_layout reserved-band check that its 200×100 and 10×20 prepare_layout probes trip but the archived GM-03e baseline (7ff9d9c) predates; kept the still-comparable input-dispatch, progression-purchase, and speed-action cases, deleted scripts/input_coordinator_differential_layout.py, dropped rendering.game_renderer from the loaded-origin assertion, bumped the scenario to v2, 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_checkpoint no longer raises (checkpoint runtime carriage graph is malformed, plus the sibling checkpoint 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_action no 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 new fleet_validation.service_action_passenger_is_live) instead of demanding the oracle match, threaded through a new opt-in allow_stale_bound on carriage_state_is_canonical/service_cache_is_canonical that defaults strict so the carriage-lifecycle guards, fleet-management, and path-replacement callers are unchanged; recursive_checkpoint_carriages._validate_service_cache mirrors 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-end run_scenario proof (finding-free), the full py313 suite, 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.json slot 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 the PLAYING->GAME_OVER promotion 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; AppController gained optional inert build_from/autosave seams (omitting both reproduces the GM-07a baseline exactly), main.run_game owns the patchable module-level AUTOSAVE_PATH plus the state-gated window-close save/delete, and ui/menu_screens.py gained the three-button title layout, continue_available painting, and a byte-stable draw_notice banner — 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 in menu_screens._font (now a fresh per-call bundled font) that the newly-rendering title chrome exposed under the suite's per-class pygame.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-deriving carriage_state_is_canonical — directly and nested inside _queue_state_is_canonical — reject the carriage attach/detach precondition and full-fleet postconditions, so can_attach_carriage/attach_carriage/can_detach_carriage/detach_carriage silently no-opped on every path during the one-tick self-healing window. Threaded the existing opt-in allow_stale_bound through _queue_state_is_canonical and the three carriage_management guard sites (the shared _valid_host_and_path precondition 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-equality transaction_state_matches) and rolled back verbatim on failure. Reclassified the wrong-holder/wrong-oracle cases 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 keeping moving/nonexact-kind/wrong-timer strictly 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.py persists lifetime deliveries to saves/highscores.json as a strict schema-v1 document (exact-key validation before field access, non-ASCII content rejected, forward versions/bad types refused), with a pure record_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-tolerant load_highscores that never raises (even on a RecursionError from pathological nesting), and a save_highscores that 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. AppController gained an optional inert highscores recorder seam that fires exactly once at the PLAYING->GAME_OVER promotion (reading mediator.deliveries only when the seam is present, storing the result in public last_highscore_result), main.run_game binds the seam plus a patchable HIGHSCORES_PATH and a single record_highscore that both the promotion seam and the mutually exclusive window-close game-over record funnel through, and a new byte-stable menu_screens.draw_best_indicator paints a "new best" banner after the renderer's game-over frame so game_renderer stays 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): when os.fdopen itself raises (OOM/EMFILE) the with never takes ownership of the mkstemp descriptor, so the raw fd leaked and -- on Windows -- the finally's temporary.unlink() then failed with PermissionError, masking the original error and leaving .tmp litter; both writers now track a handle_opened flag and close the raw fd in finally when the with never 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 patch os.fdopen to raise for each writer and assert the prior destination stays byte-intact, no .tmp litter remains, the injected error propagates unmasked, and the descriptor is closed; the success path and frozen save-v1 fixture bytes are unchanged, and the full py313 suite 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_OVER promotion out of AppController.handle_event into a public idempotent reconcile_game_over() (a no-op unless still PLAYING and mediator.is_game_over), called at the top of handle_event so the historical inline promotion is byte-preserved and once per frame in main.run_game after session.advance with 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 still PLAYING/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 and test_main composition 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_canonical reject the locomotive assign/queue/cancel precondition and postconditions, so can_assign_locomotive/assign_locomotive, can_queue_locomotive_unassignment/queue_locomotive_unassignment, can_cancel_unassignment/cancel_unassignment, and the public queued_locomotives_for_path count 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-in allow_stale_bound through the fleet-management guards via a new documented _fleet_state_is_canonical helper (the four candidate/precondition gates plus the assign ownership postcondition and the public queued-count read), added an allow_stale_bound parameter to _detach (passed True only by the queue immediate-detach fast path; the automatic settle reconciler and its own _detach call stay strict) and to reconcile_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, hardened assign itself to the full snapshot/rollback pattern the carriage attach uses -- snapshot_transaction_state plus a new symmetric added_owner mode on transaction_state_matches plus restore_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_collection helpers. Path-lifecycle removal and settle keep 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 their can_*-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 raw is_unassignment_queued flag 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 in test_gm06c_carriage_stale_sibling.py::TestFleetOpsTolerateStaleCache.
  • Implemented the typed settings store and SETTINGS screen (GM-08a, D-029): new src/settings.py persists presentation-only preferences to saves/settings.json as 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-SAFE load_settings that returns DEFAULT_SETTINGS on any malformed/forward-version/missing file and never raises; it reuses the save-schema validators and joins the persistence isolation scan. AppController gained an AppScreen.SETTINGS state and an optional inert settings seam, holding the value in public current_settings, reachable from the title and pause menus (appended after the existing entries so their rects stay byte-identical; opening from pause keeps the menu hold and Back returns to the origin), with fullscreen/reduced-motion toggles and 25%-step volume cyclers that persist through the seam. main.run_game injects the seam over a patchable SETTINGS_PATH, applies fullscreen via a change-gated pygame.display.set_mode(FULLSCREEN|SCALED) with window-surface reassignment, and threads reduced_motion into renderer.draw; the kwarg-filtering _call_flexibly dispatch was extracted to src/rendering/flexible_draw.py so reduced_motion reaches the station/passenger/path_button blink predicates (held steady) and the station snap blip (suppressed) while every default-False path stays byte-identical and game_renderer stays under 500 lines. Volumes are stored for GM-08b's audio consumer; settings touch no Mediator/config balance 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.py synthesizes short deterministic tones in-process (no external assets, pygame/numpy only, 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. ProceduralAudio reads the mixer's actual negotiated rate/channels from pygame.mixer.get_init() and builds one channel-shaped Sound per event; create_audio degrades to a no-op NullAudio on any device/build failure so audio-init never blocks play; a pure, duck-typed, tolerant snapshot_of/diff_and_play per-frame counter differ plays one tone per newly-occurred delta. Audio is a pure main.run_game consumer at the post-reconcile_game_over hook (no AppController/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 — the max_frames gate leaked a real mixer into the pre-existing unbounded run_game loop tests (which patch only main.pygame while audio.py holds its own) — fixed structurally by defaulting run_game's backend to inert NullAudio and opting into the real mixer only at the __main__ entry point (empirically verified mixer.get_init() stays None); 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). audio lives outside rendering/ and joins both isolation scans, so headless, agent, recursive, and RL play open no device; the full py313 suite stays green (12 skips) and an SDL_AUDIODRIVER=dummy smoke 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 pure src/tutorial.py observes 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 new AppScreen.TUTORIAL + optional inert build_tutorial seam runs it (Escape skips to the title with the letterbox-cancel; never autosaves or records), main.run_game drives advance_tutorial once per frame beside reconcile_game_over and paints draw_tutorial_overlay over 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 own overdue_passenger_threshold to 10**9 on the instance (a per-instance write, not a Mediator/config change) so the game never flips is_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 blocks replace_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 cold start_state=TUTORIAL building 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); tutorial joins the isolation scan and is imported only by app_controller; no Mediator/GameSession/rendering/schema/observation/checkpoint change; full py313 suite green (12 skips), budgets held (tutorial.py 222, app_controller.py 393, main.py 428, menu_screens.py 289).

2026-07-23

  • Opened GM-09 (maps and geographic constraints) with GM-09a, the behavior-preserving Classic map abstraction (D-032): new data-only src/maps.py holds an immutable frozen MapDefinition (identity + station-shape palette as __post_init__-coerced tuples), the CLASSIC definition capturing today's config values, and a version-aware resolve_map(map_id, version) that raises a clear named error rather than return the wrong map; it imports only config + geometry.type, so mediator/get_entity consume a MapDefinition one-way with no pygame pull and no cycle. Mediator gained an optional map_definition (default CLASSIC) threading the palette into get_random_stations (new keyword-only params defaulting via None-sentinel to the config globals), and save_game.serialize_game gained a fail-closed _require_classic_map guard (only classic@1 serializes, adding no bytes so save-v1.json stays 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-seed choice stress. 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-score mapDefinitionVersion defer 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; full py313 suite green (12 skips), maps.py 81 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.TaskSpec gained optional map_id/map_definition_version (appended last, default None) with a validating guard; task_descriptor adds mapId/mapDefinitionVersion/descriptorVersion:2 ONLY 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_dict emits history for v2+v3, and __post_init__ keeps schema and map in lockstep. create_training_manifest selects v3 only when map-bound; task_spec_from_manifest reconstructs the map (None for v1/v2 → legacy hash) and PlayerEnvThunk/make_env_thunks thread it to subprocess workers; PlayerPixelEnv gained map params (default None) resolving the map into the Mediator. scripts/train_rl.py gained --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 to scripts/fixtures/legacy-training-manifest-v1.json (reconstructs to c2ef342f… in CI), and EXPECTED_LF_TRAINING was re-pinned since the training sources legitimately changed (maps.py stays out of TRAINING_SOURCE_PATHS but is captured by compute_content_fingerprint). Save-schema map field and high-score mapDefinitionVersion defer to GM-09f; full py313 suite green (1357 tests, 12 skips).
  • Added the first alternate map with GM-09b, the RIVER map + terrain/station regions (D-034). MapDefinition gained additive deeply-immutable spawn_regions/rivers rect tuples (tuple-coerced + positive-area-validated); RIVER is a central vertical river splitting the play area into two station_size-eroded banks, registered so KNOWN_MAP_IDS == ("classic", "river"). Region-aware spawning threads spawn_regions keyword-only through get_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 — the test_gm09a_maps fingerprints and the frozen save-v1.json are unmoved. A new small src/rendering/terrain_renderer.py paints the river at the top of GameRenderer.draw (so the RL pixel observation sees it too); the save guard was hardened to STRUCTURAL equality (a forged classic-with-terrain is rejected). No geometry.Polygon (shapely/uuid/broken contains) — 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. Full py313 suite green (1372 tests, 12 skips); game_renderer 484, terrain_renderer 31, all budgets held.
  • Delivered the river-crossing tunnel-budget mechanic with GM-09c, making the RIVER map a real obstacle (D-035). A new dependency-light src/crossings.py (imports only geometry.point) owns the pure Liang-Barsky crossing geometry -- segment_crosses_band returns the entry point (a zero-length graze deliberately not counted), path_crossings counts 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 gate within_tunnel_budget. MapDefinition gained tunnel_budget: int | None (RIVER=3, CLASSIC=None); Mediator exposes num_tunnels/consumed_tunnels/available_tunnels as three DERIVED properties reading the live map_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) at end_path_on_station and the finish_path_creation commit boundary; replace_path gates in preflight. env.observe adds a tunnels block as a SIBLING of fleet (never a fleet key -- the checkpoint's exact whitelist), and terrain_renderer.draw_crossings paints 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, a num_tunnels cached-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. Full py313 suite 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 PURE MapDefinition addition 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_position accepts any of the three banks), the terrain/crossing renderers and path_crossings (loop over all bands), the derived tunnel count/gate, the tunnels observation, 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-river RIVER; KNOWN_MAP_IDS == ("classic", "delta", "river"). CLASSIC and RIVER stay byte-identical (unchanged test_gm09a_maps fingerprints + frozen save-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 delta genuinely trains on DELTA and the checkpoint holds across schema v1-v4. All six test-hardening MINORs folded (membership KNOWN_MAP_IDS, RNG-trajectory determinism, ValueError+delta@1 save 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. Full py313 suite green (1416 tests, 12 skips); maps.py 238 lines; GM-09b's exact-KNOWN_MAP_IDS test loosened to membership so later maps don't break it.

2026-07-24

  • 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 PURE MapDefinition addition 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_maps fingerprints + frozen save-v1.json unmoved, 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 promoting crossings.segment_crosses_band to 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). Full py313 suite green (1428 tests, 12 skips); maps.py 292 lines. GM-09f (in-game menu + save-schema map field + high-score mapDefinitionVersion) 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 keys mapId/mapDefinitionVersion, so a non-Classic game (river/delta/lake) saves and loads with its map intact; validate_save is two-phase (read + support-check schemaVersion with 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_game replaces the old _require_classic_map guard with a fail-closed pair: STRUCTURAL map_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_game synthesizes classic@1 for a v1 doc (keys absent) and resolves the map fail-closed for v2 (unknown id / unsupported version raise), threading map_definition into the Mediator; tunnel counts stay derived. The byte-frozen save-v1.json is unchanged and still loads as Classic; the deterministic v1->v2 header-only upgrade is pinned by a new frozen save-v2-classic.json (15485 bytes, SHA 60f2bc16... -- 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 CLASSIC would coerce a FALSEY MapDefinition into classic@1 and lose its terrain -- now defaults only on is None) and made _validate_map_identity a true non-empty-ASCII/no-whitespace mirror of rl.manifest_schema (both matched the code-vs-D-038-contract gap), with regressions plus a forged-over-budget LOAD test. Full py313 suite 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-score mapDefinitionVersion) 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_highscore hands the seam self.mediator (not .deliveries) and main.run_game's promotion closure drops its SimpleNamespace(deliveries=...) wrapper, so the frame-accurate reconcile and the window-close QUIT both call the IDENTICAL record_highscore(mediator), which reads mediator.map_definition.{map_id, map_definition_version} DIRECTLY (no or classic default -- a missing map records nothing rather than mislabelling). highscores becomes schema v2 keyed by the full (map, mapDefinitionVersion, rulesVersion) identity via one shared _identity helper (sort + cap + rank, so no predicate keys on a subset), with record_score gaining a required map_definition_version and the entry map tightened to the save's mapId grammar; stateContract stays stable. A legacy v1 board is NOT migrated -- START-EMPTY -- because a v1 map="classic" label is not provably accurate (the recorder was classic-hardcoded while GM-09f made non-Classic saves loadable via Continue), so synthesizing classic@1 would 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_loop real-recorder stubs + test_gm07e's local spy). Full py313 suite green (1459 tests, 12 skips); highscores.py 272 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/lake from the title. AppController gains current_map_id (default classic), cycled by an appended title map control (title_layout appends the key so prior title rects stay byte-identical; draw_title_screen gains current_map_id and paints a Map: {Name} button; main threads controller.current_map_id). The build_game seam becomes uniformly Callable[[str], GameTriple] -- main.run_game's build_game(map_id) resolves map_by_id(map_id) into Mediator(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 off self.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_TRAINING pins only training sources). Full py313 suite 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 in PathLifecycle.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 by increment_time on expiry), so a headless MiniMetroEnv rollout checkpointed a cancelled draft indefinitely; _paint_creation_snap_blip now records each painted blip (the tuple start_snap_blip actually appends -- Station.start_snap_blip now 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 -- since start_snap_blip has 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 because prune_visual_effects rebuilds the tuples each tick; a color match would erase the survivor). (2) A mid-draft remove_path runs assign_paths_to_buttons, binding the still-drafting path to a button; abort now detaches that one mapping (path_to_button.pop + PathButton.remove_path if it still points at the draft) surgically -- NOT a full reassign -- so no colored button points at a removed line. finish forgets 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.path derefs to None either way); save-v1.json/save-v2-classic.json (which serialize snapBlips, 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-09c test_rejected_multistation_creation was 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 the finish/end_path comments still said abort was "unchanged / left to a follow-up") and drove the receipt-to-append coupling and the commit-time receipt clear. Full py313 suite green (1484 tests, 12 skips); path_lifecycle.py gains 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_index is steps-derived (no new persisted scalar); resolve_week_boundary() releases it. The calendar is OPT-IN, default OFF -- only INTERACTIVE main.run_game (build_game/build_from, gated on max_frames is None) enables it, so RL/tutorial/headless never pause. The human shell adds AppScreen.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 in MiniMetroEnv._complete_step, but PlayerPixelEnv drives via advance_exact and 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 True OFFER 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-Mock is True test, an exact-cancel-event assertion, and real-run_game gating + OFFER-loop integration tests). Full py313 suite 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/pure generate_offers) draws OFFERS_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_boundary stores current_offers at the hold and resolve_week_boundary clears them; draw_offer_screen previews the labels read-only. The offer RNG is a dedicated per-week random.Random derived READ-ONLY from python_random.getstate() + week_index — a DUAL-PLAN-REVIEW pivot: Codex BLOCKED the first plan (a persisted spawn(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_offers stays ()). 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). Full py313 suite 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_screen paints them, and AppController._handle_offer arms a button on press and, on a matching release (the GM-10a arming discipline, so a stale gameplay release cannot choose), calls Mediator.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, named ValueError on an unknown kind) then clears + releases; None is 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 full serialize_game doc byte-identical). The real effects are GM-10d-g: NEW_LINE can ride the already-persisted purchased_num_paths (Continue-safe standalone), while LOCOMOTIVE/CARRIAGE hit _require_running_config and TUNNEL needs a persisted bonus, so those land with GM-10h. Full py313 suite 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() bumps purchased_num_paths (capped at num_paths, no line_credits spend — record_path_purchase minus the cost); Mediator._grant_free_line calls it and refreshes the derived caches via update_unlocked_num_paths() (the exact purchase-flow refresh), wired into the _apply_offer NEW_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; numPaths unchanged so _require_running_config holds), 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 new src/weekly_offers.py WeeklyOffers facade (D-023) because mediator.py crossed the 1000-line hard ceiling; the extraction is behavior-preserving (mediator 940 lines; all offer tests green). Full py313 suite 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 key tunnelBonus and v3-relaxes save_load._require_running_config so a grown fleet loads: numPaths == config always, numMetros/numCarriages == config for v1/v2 but >= config for v3 (the fleet is persisted as its grown TOTALS -- no bonus field -- because 17 tests + the carriage rollback assign num_metros/num_carriages, so they can't be derived). The tunnel gains a stored Mediator.tunnel_bonus folded into num_tunnels AND, the load-bearing fix, into crossings.within_tunnel_budget (which reads the map budget directly, so a bonus threaded only through num_tunnels would show in the observation/legality yet never unblock a crossing). serialize_game runs a new _require_valid_upgrade_state FIRST 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 frozen save-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). Full py313 suite green (1566 tests). GM-10e (locomotive upgrade -- a trivial num_metros += 1 arm 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 become host.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_tunnels all 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 new test_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, an available_tunnels readout assertion, and a stale GM-10d test name/doc). Full py313 suite 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 key pendingOffers (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, because WEEK_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_game runs _require_valid_pending_offers FIRST (offers == the canonical derivation when held, else empty) so a desynced tuple can't be written; deserialize_game restores 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 frozen save-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 == canonical re-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). Full py313 suite green (1599 tests). GM-10 COMPLETE (calendar + offers + choice + 4 effects + mid-offer persistence); GM-11 (balance/recursive playtest) opens next toward v1.0.