Parametric Maneuver Simulation Test Harness

Overview

Unit tests mock state per-phase and cannot catch cross-phase integration bugs (e.g., fuel exhaustion across escape → interplanetary → capture). The simulation harness runs the real AutomationEngine in a closed loop with in-process Keplerian physics, testing all 240 valid rendezvous strategy/target/SOI combinations plus landing scenarios.

Architecture

ManeuverSimulation (harness.py) — tick loop
  ├── AutomationEngine (production code, unmodified)
  │     reads state ──→ DictBackedState (state.py) — in-memory
  │     sends gRPC  ──→ SteeringCapture (physics.py) — captures commands
  ├── KeplerianPhysics (physics.py) — integrates between ticks
  └── ScenarioConfig (scenarios.py) — defines initial conditions

Tick Loop Flow (Sub-Stepping)

Matches the real tick_loop.py sub-stepping: effective_dt is split into sub-steps of at most MAX_SUB_DT (10s). Each sub-step runs:

  1. physics.step_ship() — apply thrust, consume fuel, leapfrog-integrate
  2. physics.propagate_bodies() — Kepler-propagate celestial bodies
  3. Propagate target entity (station/ship co-orbiting its parent)
  4. Advance game time by sub_dt
  5. engine.evaluate_all_ships(tick) — reads state, runs phase handlers, sends steering commands
  6. Check termination (maneuver cleared = complete)

Key Design Decision — Instant Attitude Alignment

Phase handlers set ATTITUDE_DIRECTION with a direction vector. The harness assumes instant alignment (ship attitude quaternion always matches desired direction). This tests automation logic, not the attitude controller.

Components

DictBackedState (state.py)

In-memory implementation of TickEngineState’s 19 async methods + 1 property. All dict values are strings (matching Redis hgetall format). Stores ships, bodies, stations, jumpgates, rules, maneuvers, and archives.

SteeringCapture (physics.py)

Mock gRPC stub replacing PhysicsStub. Captures SetSteeringCommand requests (attitude_mode, direction, thrust_level, attitude_hold, translation) and RequestService requests (docking). No network I/O.

KeplerianPhysics (physics.py)

Between-tick integration:

  • step_ship(): Resolve thrust direction from captured steering state, compute thrust + fuel consumption (matching simulation.py), leapfrog integrate, instant attitude alignment, ground detection + AGL computation, update ship dict.
  • propagate_bodies(): Kepler-propagate each body relative to its parent using universal variable formulation (kepler_propagate from qlaw.py). Uses snapshot-based propagation to prevent sequential-update drift (child bodies compute relative state from pre-update parent positions).

Supported attitude modes: PROGRADE, RETROGRADE, NORMAL, ANTINORMAL, RADIAL, ANTIRADIAL, DIRECTION, HOLD, TARGET_RETROGRADE, SURFACE_RETROGRADE (subtracts body rotation velocity), LOCAL_VERTICAL (points away from body center).

Ground detection: after position integration, computes AGL to all bodies. If AGL ≤ 0, clamps ship to body surface, zeros velocity, sets landed_body_name.

ScenarioConfig (scenarios.py)

Defines initial conditions for each test scenario:

  • maneuver_type: "rendezvous" (default) or "land"
  • action_params: extra params like target_body, target_lat, target_lon
  • target_orbit: optional (not used for landing scenarios)

Provides generate_matrix() producing 240 rendezvous combinations, SMOKE_SCENARIOS for full end-to-end rendezvous runs, LANDING_SCENARIOS for landing phase tests, and LANDING_SMOKE_SCENARIOS for full landing runs.

ManeuverSimulation (harness.py)

Orchestrates the tick loop. Patches PhysicsStubSteeringCapture, creates AutomationEngine, runs tick loop, returns SimResult with outcome, tick count, phase history, fuel remaining, and final distance/velocity.

Branches rule creation on maneuver_type: rendezvous builds target entity + rendezvous action, landing builds land action from action_params. Celestial bodies include rotation parameters (rotation_period, prime_meridian_at_epoch) needed by the landing automation’s body-frame coordinate transforms.

RvzSimulation is a backward-compatible alias for ManeuverSimulation.

240-Combination Matrix

Dimension Values Count
Strategy (non-brach) hohmann, fast, express, parabolic, lambert 5
Strategy (brach) brachistochrone 1
Target type station, ship, jumpgate, lagrange 4
SOI type same, cross 2
Budget mode (brach only) efficient, fastest, None + 2 fuel fractions 5
Ship class cargo_hauler, fast_frigate, long_range_explorer 3

Non-brachistochrone: 5 × 4 × 2 × 1 × 3 = 120 Brachistochrone: 1 × 4 × 2 × 5 × 3 = 120 Total: 240 scenarios

Landing Scenarios

Scenario Ship Class Target Body Type Start Orbit
land_luna_untargeted_frigate fast_frigate Luna untargeted 100 km lunar
land_luna_targeted_frigate fast_frigate Luna targeted (0,0) 100 km lunar
land_earth_untargeted_hauler cargo_hauler Earth untargeted 400 km LEO

Landing phases (untargeted): deorbit → braking → vertical_descent → terminal → touchdown Landing phases (targeted): plane_change → coast_to_deorbit → deorbit → braking → vertical_descent → terminal → touchdown

Test Layers (Four Layers)

Layer 1 — Phase-Transition Tests (240 scenarios)

Run each scenario for 200 ticks. Verify at least one phase transition occurs (automation engine starts processing the maneuver).

  • Runtime: ~24s (200 ticks × ~0.5ms/tick × 240)
  • Runs: Every PR

Layer 1b — Landing Phase-Transition Tests (3 scenarios)

Run each landing scenario for 500 ticks. Verify at least one phase transition occurs (automation engine starts processing the landing maneuver).

  • Runtime: ~1s
  • Runs: Every PR

Layer 2 — Full-Scenario Smoke Tests (~12 rendezvous + 1 landing, @pytest.mark.slow)

Run representative scenarios to completion. Verify outcome == "completed" and fuel_remaining >= 0.

  • Runtime: ~120s (rendezvous) + ~1s (landing)
  • Runs: On demand / nightly

Layer 3 — Regression Tests (specific failure modes)

  • Demon fuel exhaustion: brachistochrone fastest cross-SOI must not exhaust fuel
  • Insufficient Δv abort: 100 kg fuel cross-SOI should abort cleanly
  • Cargo hauler capture fuel: low-TWR ship must reserve capture fuel

  • Runtime: ~8s
  • Runs: Every PR

Runtime Budget

Layer Count Ticks/test Time/tick Total
Rendezvous phase-transition 240 200 ~0.5ms ~24s
Landing phase-transition 3 500 ~0.5ms ~1s
Full smoke (rendezvous) 12 ~20k avg ~0.5ms ~120s
Full smoke (landing) 1 ~663 ~0.5ms ~1s
Regression 3 ~5k avg ~0.5ms ~8s

Phase-transition + regression = ~33s (every PR). Full smoke = ~121s (marked @pytest.mark.slow).

Standard Orbits

  • Same-SOI: Ship at 400 km LEO (Earth), target at 35,786 km GEO
  • Cross-SOI: Ship at 400 km LEO (Earth), target at 100 km lunar orbit

Differences: Harness vs Real Game

Aspect Harness Real Game
Attitude control Instant alignment — quaternion set to target direction each step PD controller with ~8s settling time, cosine-scaled thrust during slew
Body propagation Kepler two-body (universal variable) — exact per parent Leapfrog N-body — bodies experience mutual perturbations
Ship gravity Full N-body from all bodies Full N-body from all bodies
Network In-process mock gRPC (SteeringCapture) Real gRPC to physics service
State storage In-memory dicts (DictBackedState) Redis hashes
Time scale Fixed effective_dt per scenario Configurable, dynamic time scale
Thrust direction Applied in commanded direction (ATTITUDE_DIRECTION) Applied in ship’s actual +Z (body frame forward)

Known Limitations

Vis-viva circularize divergence: The apse-targeted circularize phase (used by Hohmann, Lambert, and other two-impulse strategies) can diverge in the harness. The vis-viva formula assumes pure two-body dynamics, but the harness integrates with full N-body gravity. Over many apse burns, small perturbations accumulate and the orbit goes hyperbolic, triggering regression stall detection. This causes the automation to loop through circularize → phase → transfer_plan indefinitely.

This does NOT affect brachistochrone scenarios (constant-thrust, no apse targeting) or the 240 phase-transition tests (only 200 ticks, too short for divergence). The smoke scenarios use brachistochrone exclusively.

These limitations reflect harness physics fidelity, not automation bugs. The real game’s physics service handles these cases correctly because the integration and control loop are tightly coupled.


Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.