Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ coverage.xml
dist/
build/
*.egg-info/
benchmarks/.venv/
benchmarks/outputs/

# OS/X and editor junk
.DS_Store
Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/tauri/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rusty_runways_tauri"
version = "2.0.4"
version = "2.2.1"
edition = "2021"

[dependencies]
Expand Down
18 changes: 17 additions & 1 deletion apps/tauri/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ struct AppState {
game: Mutex<Option<Game>>,
}

fn default_starting_cash() -> f32 {
650_000.0
}

#[derive(Deserialize)]
struct NewGameArgs {
#[serde(default)]
seed: Option<u64>,
#[serde(rename = "numAirports", alias = "num_airports")]
num_airports: Option<usize>,
#[serde(rename = "startingCash", alias = "starting_cash")]
#[serde(
rename = "startingCash",
alias = "starting_cash",
default = "default_starting_cash"
)]
starting_cash: f32,
}

Expand Down Expand Up @@ -109,6 +117,13 @@ fn maintenance(state: State<AppState>, plane: usize) -> Result<(), String> {
.map_err(|e| e.to_string())
}

#[tauri::command]
fn sell_plane_cmd(state: State<AppState>, plane: usize) -> Result<f32, String> {
let mut guard = state.game.lock().map_err(|_| "state poisoned")?;
let game = guard.as_mut().ok_or("no game running")?;
game.sell_plane(plane).map_err(|e| e.to_string())
}

#[derive(Serialize)]
struct OrderDto {
id: usize,
Expand Down Expand Up @@ -352,6 +367,7 @@ fn main() {
unload_all,
refuel_plane,
maintenance,
sell_plane_cmd,
plane_info,
airport_orders,
list_models,
Expand Down
2 changes: 1 addition & 1 deletion apps/tauri/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "RustyRunways",
"version": "2.0.4",
"version": "2.2.1",
"identifier": "com.rustyrunways.desktop",
"build": {
"beforeDevCommand": "npm run dev -- --host localhost --port 5173 --strictPort",
Expand Down
2 changes: 1 addition & 1 deletion apps/tauri/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vite_react_shadcn_ts",
"private": true,
"version": "2.0.4",
"version": "2.2.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
9 changes: 9 additions & 0 deletions apps/tauri/ui/src/api/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ export async function refuelPlane(plane: number): Promise<void> {
}
}

export async function sellPlane(plane: number): Promise<number> {
if (isTauri()) {
return await invoke<number>('sell_plane_cmd', { plane })
} else {
const wasm = await import(/* @vite-ignore */ wasmModulePath())
return await wasm.sell_plane(plane)
}
}

export async function maintenance(plane: number): Promise<void> {
if (isTauri()) {
await invoke('maintenance', { plane })
Expand Down
29 changes: 26 additions & 3 deletions apps/tauri/ui/src/components/AirplaneDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import {
Filter,
Plus,
Minus,
MapPin
MapPin,
CircleDollarSign
} from "lucide-react";
import { airportOrders as apiAirportOrders, planeInfo as apiPlaneInfo, departPlane as apiDepart, loadOrder as apiLoad, unloadOrder as apiUnload, refuelPlane as apiRefuel, maintenance as apiMaint, canFly as apiCanFly, reachability as apiReach } from "@/api/game";
import { airportOrders as apiAirportOrders, planeInfo as apiPlaneInfo, departPlane as apiDepart, loadOrder as apiLoad, unloadOrder as apiUnload, refuelPlane as apiRefuel, maintenance as apiMaint, canFly as apiCanFly, reachability as apiReach, sellPlane as apiSell } from "@/api/game";

interface Order {
id: string;
Expand All @@ -43,12 +44,14 @@ interface AirplaneDetailScreenProps {
airplaneId: string;
onBack: () => void;
airportsData?: { id: number; name: string }[];
onSold?: (planeId: string, refund: number) => void | Promise<void>;
}

export const AirplaneDetailScreen = ({
airplaneId,
onBack,
airportsData
airportsData,
onSold
}: AirplaneDetailScreenProps) => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -210,6 +213,18 @@ export const AirplaneDetailScreen = ({
await refresh();
}

async function handleSell() {
try {
setError(null);
const refund = await apiSell(parseInt(airplane.id, 10));
if (onSold) {
await onSold(airplane.id, refund);
}
} catch (e: unknown) {
setError(e instanceof Error ? e.message : String(e));
}
}

async function handleDispatch() {
if (!selectedDestination) return;
await apiDepart(parseInt(airplane.id, 10), parseInt(selectedDestination, 10));
Expand Down Expand Up @@ -263,6 +278,14 @@ export const AirplaneDetailScreen = ({
<Fuel className="w-4 h-4 mr-1" />
Refuel
</Button>
<Button
variant="destructive"
onClick={handleSell}
disabled={airplane.loadedOrders.length > 0 || airplane.status.toLowerCase() !== 'parked'}
>
<CircleDollarSign className="w-4 h-4 mr-1" />
Sell
</Button>
</div>
</div>

Expand Down
7 changes: 7 additions & 0 deletions apps/tauri/ui/src/components/GameScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ export const GameScreen = ({ onMainMenu }: GameScreenProps) => {
setSelectedAirportId("");
};

const handlePlaneSold = async (planeId: string, refund: number) => {
addLog('success', `Sold plane ${planeId} for $${refund.toFixed(2)}`);
await refresh();
handleBackToMain();
};

const handleAdvanceTime = async () => {
const obs = await apiAdvance(1);
setCash(obs.cash);
Expand Down Expand Up @@ -200,6 +206,7 @@ export const GameScreen = ({ onMainMenu }: GameScreenProps) => {
airplaneId={selectedAirplaneId}
onBack={handleBackToMain}
airportsData={airports.map((a) => ({ id: a.id, name: a.name }))}
onSold={handlePlaneSold}
/>
);
}
Expand Down
18 changes: 14 additions & 4 deletions apps/tauri/ui/src/components/MainMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ interface GameConfig {
export const MainMenu = ({ onStartGame, onLoadGame, onLoadConfig }: MainMenuProps) => {
const [gameConfig, setGameConfig] = useState<GameConfig>({
seed: "",
airportCount: 10,
startingCash: 100000,
airportCount: 12,
startingCash: 650000,
});
const [loadGameName, setLoadGameName] = useState("");
const [selectedFile, setSelectedFile] = useState<File | null>(null);
Expand Down Expand Up @@ -100,7 +100,12 @@ export const MainMenu = ({ onStartGame, onLoadGame, onLoadConfig }: MainMenuProp
min="5"
max="50"
value={gameConfig.airportCount}
onChange={(e) => setGameConfig({ ...gameConfig, airportCount: parseInt(e.target.value) || 10 })}
onChange={(e) =>
setGameConfig({
...gameConfig,
airportCount: parseInt(e.target.value) || 12,
})
}
className="bg-secondary/50 border-aviation-blue/20 focus:border-aviation-blue/50"
/>
</div>
Expand All @@ -114,7 +119,12 @@ export const MainMenu = ({ onStartGame, onLoadGame, onLoadConfig }: MainMenuProp
max="1000000"
step="10000"
value={gameConfig.startingCash}
onChange={(e) => setGameConfig({ ...gameConfig, startingCash: parseInt(e.target.value) || 100000 })}
onChange={(e) =>
setGameConfig({
...gameConfig,
startingCash: parseInt(e.target.value) || 650000,
})
}
className="bg-secondary/50 border-aviation-blue/20 focus:border-aviation-blue/50"
/>
</div>
Expand Down
53 changes: 53 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Benchmark Harness

The benchmarking harness drives the Python bindings to simulate many seeds across configurable scenarios. It lives entirely in this directory so that the data, plots, and configuration live alongside the driver script.

## Getting Started

Run `./setup.sh` once to create a virtual environment with the Python bindings and plotting dependencies compiled. Activate the environment with `source .venv/bin/activate`, then execute `python run_benchmarks.py` with any command line options you need. The script prints a progress bar per scenario, followed by per-seed statistics and aggregate summaries.

## Supplying Scenarios

Large experiments are described in a YAML file passed with `--scenario-config`. The file contains optional `defaults` and a `scenarios` list. Defaults provide world knobs that every scenario inherits unless it overrides them. Typical fields include `hours`, `seeds`, `cash`, `num_airports`, and any nested `gameplay` values such as restock cadence, fuel interval, or specific `orders` parameters (e.g., `min_weight`, `max_weight`, `alpha`, `beta`).

Each item in `scenarios` can:

- reference an existing world on disk with `config_path`, or
- specify a `world` override (through plain YAML nesting) that the harness will materialise into a temporary configuration which is deleted after the run.

Scenarios may vary the parameters in several ways:

- `sweep`: define a single dotted path (such as `gameplay.orders.min_weight`) and a list of values. The harness expands the sweep into separate variants, runs them, and records the varied parameter in the summary output. Multiple sweeps can be attached to a scenario by supplying a list. The example configuration demonstrates restock, starting cash, airport count, min-weight, max-weight, `alpha`, and `beta` sweeps.
- `variants`: provide named overrides for bespoke combinations. This is useful when pairs of values must change together (for instance, `min_weight` and `max_weight`) to stay within plausible ranges. Each variant may add optional `metadata` to describe the combination.
- `include_base`: set to `false` when you only want the explicit variants and not the inherited default world.

Seeds and horizon hours can be assigned globally, per scenario, or per variant. When none are given, the command line defaults supplied to `run_benchmarks.py` take effect.

## Outputs

All artifacts are written under `benchmarks/outputs`. Each scenario (and each expanded variant) receives its own subdirectory containing:

- `seed_<n>_timeline.csv` files with hour-by-hour cash, fleet size, and deliveries.
- PNG plots that show cash, fleet size, and deliveries over time for every seed.
- A `margin_per_hour.png` bar chart that compares the profitability of each seed.

In addition to the per-scenario folders, the root of `outputs` contains:

- `scenario_summary.csv`, which aggregates the results per variant (averaged across seeds) and records the parameter value that was swept.
- A `summary/` directory holding comparison charts generated from sweeps. When a parameter is numeric, the harness produces a line plot showing each metric against the parameter value. For categorical or labelled variants, it renders bar charts.

These files are intended to be disposable. You can safely delete the entire `outputs` directory between runs if you want a clean slate.

### Default Sanity Sweep

`sanity.yaml` describes a lightweight run of the default game knobs across ten seeds. After running `python run_benchmarks.py --scenario-config benchmarks/sanity.yaml`, execute `./sanity_report.py` to render phase-by-phase charts (`sanity_feasible_ratio.png`, `sanity_phase_margins.png`, etc.) under `benchmarks/outputs/sanity-baseline/`. The helper script also emits `sanity_summary.json` with descriptive statistics so you can quickly spot regressions in early, mid, or late progression.

## Example Configuration

See `scenarios.example.yaml` in this directory for a ready-to-run template. It includes the default gameplay settings drawn from the sample world and demonstrates sweeps over restock cadence, starting cash, airport counts, minimum and maximum order weights, and the `alpha`/`beta` parameters. It also provides explicit weight combinations so you can observe how paired adjustments behave compared to isolated changes.

## Greedy Agent

The bundled policy in `run_benchmarks.py` operates with a few simple heuristics. It inspects parked airplanes one at a time, unloading any cargo that just arrived, refuelling when a prospective trip would consume more than ninety percent of the remaining range, and ranking visible orders by value per kilometre subject to payload and range checks. Once an order is selected it spends an hour loading, records the route distance, and departs toward the destination. When every aircraft is tied up (loading, refuelling, in transit, or undergoing maintenance) the harness advances time in one-hour increments until another decision is needed; mechanical failures trigger maintenance immediately.

The agent keeps an eye on the player's cash and, when the runway allows and funds comfortably exceed the purchase price, buys a second SparrowLight to increase throughput. Additional aircraft are left for future experimentation—the goal here is a deterministic baseline that finishes quickly so the sweeps remain approachable.
Loading
Loading