Python¶
The Python bindings expose the Rust core engine for scripting, data analysis, and reinforcement‑learning workflows. They respect the same economic defaults as the game clients: when you build an environment without a YAML override you start with $650 000, a 12‑airport network, weekly restocks, and six‑hour fuel updates. Every CLI command can be issued from Python, so automated agents interact with the simulation just like human players.
As part of the balancing process we routinely let heuristic agents play through procedurally generated worlds so we can observe cash curves, order feasibility, and upgrade cadence. The tooling used for that process lives in the benchmarks/ directory and is referenced near the end of this document.
Installation¶
- PyPI (recommended):
pip install rusty-runways
- With Gymnasium support (for Gym wrappers):
pip install 'rusty-runways[gym]'
- Local dev build:
cd crates/py
maturin develop --release
Imports at a Glance¶
- Engine bindings:
from rusty_runways_py import GameEnv, VectorGameEnv - Gym wrappers:
from rusty_runways import RustyRunwaysGymEnv, RustyRunwaysGymVectorEnv, make_sb3_envs
Gymnasium is only required for the Gym wrappers. See the Gym section for details.
GameEnv (single environment)¶
Constructor
GameEnv(seed: int | None = None,
num_airports: int | None = None,
cash: float | None = None,
config_path: str | None = None)
Common usage
from rusty_runways_py import GameEnv
# Start from a seeded random world
g = GameEnv(seed=1, num_airports=5, cash=650_000)
g.step(1)
print(g.time(), g.cash())
print(g.drain_log())
# Start from a custom world YAML
g2 = GameEnv(config_path="examples/sample_world.yaml")
print(g2.time(), g2.cash())
Key methods
reset(seed=None, num_airports=None, cash=None, config_path=None): Reinitialize the world.step(hours: int): Advance simulation time byhours.execute(cmd: str): Run CLI command (see CLI docs for syntax).sell_plane(plane_id: int) -> float: Sell a parked, empty plane (returns refund).state_json() -> str: JSON snapshot of the observable state.state_py() -> dict: Python dict snapshot (JSON decoded).full_state_json() -> str: Full internal state snapshot.load_full_state_json(s: str): Restore full internal state snapshot.time() -> int,cash() -> float,seed() -> int.drain_log() -> list[str]: Retrieve and clear sim log.orders_at_plane(plane_id: int) -> list[int]: Order IDs available at that plane’s airport.airport_ids() -> list[int]: All airport IDs in the world.models_json() -> str: JSON list of available airplane models (name + specs) for the current game.models_py(py) -> list[dict]: Python list version of the above.
Inspecting state
obs = g.state_py()
print(obs["airports"][0])
print(obs["planes"][0])
VectorGameEnv (multiple environments)¶
Constructor
VectorGameEnv(n_envs: int,
seed: int | None = None,
num_airports: int | None = None,
cash: float | None = None,
config_path: str | None = None)
Core vector API
env_count() / __len__(): Number of envs.seeds() -> list[int]: Per‑env seeds.reset_all(seed=None, num_airports=None, cash=None): Vector reset; values can be scalars or lists.reset_at(idx, seed=None, num_airports=None, cash=None): Reset a single env.step_all(hours, parallel=True): Advance all envs (Rayon‑parallel whenparallel=True).step_masked(hours, mask, parallel=True): Advance a subset by boolean mask.execute_all(cmds, parallel=True) -> list[tuple[bool, Optional[str]]]: Run a command (orNone) per env.state_all_json() / state_all_py(): Vector snapshots.times() -> list[int],cashes() -> list[float],drain_logs() -> list[list[str]].orders_at_plane_all(plane_id) -> list[list[int]],airport_ids_all() -> list[list[int]].sell_plane(env_idx: int, plane_id: int) -> float: Sell a plane in a specific environment.
Examples
from rusty_runways_py import VectorGameEnv
env = VectorGameEnv(4, seed=1)
env.step_all(1, parallel=True)
print(env.times()) # [1, 1, 1, 1]
print(env.cashes())
# Determinism check: parallel vs serial stepping
serial = VectorGameEnv(4, seed=1)
env.step_all(3, parallel=True)
serial.step_all(3, parallel=False)
assert env.times() == serial.times() == [4, 4, 4, 4]
# Start from a custom world for all envs
env2 = VectorGameEnv(4, config_path="examples/sample_world.yaml")
print(env2.times())
Gymnasium Wrappers¶
Wrappers live under the pure‑Python package rusty_runways and require gymnasium:
RustyRunwaysGymEnv: Single‑env wrapper overGameEnv.RustyRunwaysGymVectorEnv: Vector wrapper overVectorGameEnv(implementsgym.vector.VectorEnv).make_sb3_envs(n_envs, seed=None, **kwargs): Convenience to buildDummyVecEnv/SubprocVecEnvinputs for SB3.
Observation and action spaces
- Observation:
Box(float32, shape=(14,))summary features derived from state JSON. - Action:
MultiDiscrete([6, 16, 64, 256])encoding[op, plane_id, selector, dest_index]whereopin 0 ADVANCE, 1 REFUEL, 2 UNLOAD_ALL, 3 MAINTENANCE, 4 DEPART_TO_INDEX, 5 LOAD_ORDER. - Reward: By default, delta cash per step; can be customized with
reward_fn(state, prev_state)on the single‑env wrapper.
Single‑env example
from rusty_runways import RustyRunwaysGymEnv
env = RustyRunwaysGymEnv(seed=1, num_airports=5)
obs, info = env.reset()
for _ in range(10):
action = env.action_space.sample()
obs, reward, terminated, truncated, info = env.step(action)
if terminated or truncated:
obs, info = env.reset()
Vector (Gym VectorEnv) example
from rusty_runways import RustyRunwaysGymVectorEnv
venv = RustyRunwaysGymVectorEnv(8, seed=123, num_airports=5)
obs, info = venv.reset()
acts = venv.action_space.sample()
venv.step_async(acts)
obs, rewards, terminated, truncated, infos = venv.step_wait()
Stable‑Baselines3 example
from stable_baselines3 import PPO
from stable_baselines3.common.vec_env import DummyVecEnv
from rusty_runways import make_sb3_envs
vec_env = DummyVecEnv(make_sb3_envs(4, seed=1, num_airports=5))
model = PPO("MlpPolicy", vec_env, verbose=1)
model.learn(total_timesteps=10_000)
Dependency note
- Install via extra for full features:
pip install 'rusty-runways[gym]'. - Or install directly:
pip install gymnasium. - If Gymnasium is not installed, attempting to use the wrappers will raise a helpful ImportError explaining how to enable them.
Notes¶
- The bindings enforce the same constraints as the Rust engine: planes must be parked to refuel or sell, deadlines continue to expire, and economic defaults mirror the tuned values.
- Seeds control determinism. When you pass a scalar seed to
VectorGameEnv, each environment receivesseed + index, so parallel runs remain reproducible. - Parallel stepping releases the GIL and uses Rayon internally, allowing large vector environments to scale efficiently across CPU cores.
Loading YAML Worlds¶
All constructors accept config_path. The engine reads the YAML, applies defaults for any missing fields, and returns an environment seeded with those parameters. This pattern makes balance testing fast, because you can edit the YAML on disk, call reset(config_path=...), and immediately observe how the new weights, deadlines, or restock cadence influence the simulation.
from rusty_runways_py import GameEnv
env = GameEnv(config_path="benchmarks/sanity.yaml")
print(env.cash(), env.seed())
env.execute("SHOW AIRPORTS WITH ORDERS")
To build YAML files programmatically (for sweeps or automated tests), write them to a temporary path with yaml.safe_dump, hand that path to GameEnv or VectorGameEnv, and delete the file once the run completes. The loader does not keep the file handle open after parsing.
Custom Airplane Catalog in YAML¶
World YAML supports a top‑level airplanes section that either replaces or extends the built‑in catalog:
airplanes:
strategy: add # or: replace
models:
- name: WorkshopCombi
mtow: 15000.0
cruise_speed: 520.0
fuel_capacity: 3200.0
fuel_consumption: 260.0
operating_cost: 950.0
payload_capacity: 3200.0
passenger_capacity: 24
purchase_price: 780000.0
min_runway_length: 1200.0
role: Mixed # Cargo | Passenger | Mixed
After loading a YAML with custom airplanes, query the models from Python:
from rusty_runways_py import GameEnv
g = GameEnv(config_path="examples/sample_world.yaml")
models = g.models_py()
print([m["name"] for m in models])
Sanity Benchmarks and the Heuristic Agent¶
The benchmarks/ folder contains a deterministic heuristic agent used during development. Running it regularly helps verify that code or tuning changes keep the starter plane’s feasibility and upgrade timing inside the target window.
- Edit or create a scenario file (for example
benchmarks/sanity.yaml). - Execute
python benchmarks/run_benchmarks.py --scenario-config benchmarks/sanity.yamlto simulate all listed seeds. - Render per-seed charts with
python benchmarks/sanity_report.py. The script writes feasibility, margin, cash, and route-length plots for early/mid/late phases alongside a JSON summary so you can diff statistics between branches.
These scripts rely solely on the public Python API, so you can copy their helpers into custom analytics pipelines or reinforcement-learning loops.
Benchmark Agents¶
The repository contains a small benchmarking harness (see benchmarks/run_benchmarks.py) that exercises the Python bindings using a greedy hauling agent. This script evaluates multiple seeds in one go, records cash/fleet/delivery timelines, and renders plots summarizing the progression. To experiment with it locally:
- Initialise the virtual environment and build the bindings from source:
./benchmarks/setup.sh
source benchmarks/.venv/bin/activate
- Run the benchmarking driver:
python benchmarks/run_benchmarks.py --seeds 0 1 2 --hours 168 --airports 12
The script prints per-seed statistics (order feasibility, margin-per-hour, upgrade timing). It also emits CSV timelines and PNG plots under benchmarks/outputs/ so you can inspect cash/fleet/delivery curves by seed.
For exploratory analysis you can now describe arbitrarily rich batches in YAML and pass them via --scenario-config. Each scenario may define:
- the world knobs (
num_airports,starting_cash,gameplay.orders.*, etc.); the runner creates temporary YAML configs and cleans them up afterwards; - sweeps over a single parameter (e.g., vary only
gameplay.restock_cycle_hours) or named variants with bespoke overrides; - the seed list and duration for each variant.
All runs end up under benchmarks/outputs/<scenario>/ alongside a CSV timeline per seed. The driver also produces a scenario_summary.csv plus aggregate line/bar charts in benchmarks/outputs/summary/, letting you isolate the impact of the swept parameter (restock cadence, order weights, fuel intervals, and so on).
An abridged configuration example:
defaults:
hours: 240
seeds: [0, 1, 2]
cash: 650000.0
num_airports: 12
gameplay:
restock_cycle_hours: 168
fuel_interval_hours: 6
orders:
regenerate: true
generate_initial: true
max_deadline_hours: 96
min_weight: 180.0
max_weight: 650.0
alpha: 0.12
beta: 0.55
scenarios:
- name: baseline
- name: restock-sweep
sweep:
parameter: gameplay.restock_cycle_hours
values: [96, 120, 168, 240]
- name: order-weight-variants
variants:
- label: light-cargo
overrides:
gameplay:
orders:
min_weight: 150.0
max_weight: 600.0
See benchmarks/scenarios.example.yaml for a more complete walkthrough including external config_path worlds.
Feel free to adapt the harness for richer experiments (e.g., alternative agents, different reward functions, or automated regression checks). Because it builds against the local branch, the outputs always reflect the current balance knobs.