A turn-based airline management game in the spirit of Railroad Tycoon and Aerobiz. Free, open source, browser-based, no backend. One turn = one quarter.
Play it: www.airhoncho.com — nothing to install.
npm install
npm run dev # http://localhost:5173- Choose a game — Present day (2026) or History, 2000 — then click a city to set your home base.
- Acquire aircraft — buy (cash now, an asset on the books) or lease (a month down, then rent every quarter).
- Click two cities to price a sector — you see what it costs to open before you commit — then open it and assign a parked aircraft.
- Set a pricing posture, then close the books on the quarter.
Escape cancels a half-made sector or deselects one. The sector dossier under the
map shows every component the sim used to settle the quarter. ? lists the
keyboard shortcuts. Games save to named slots, and a seed link carries the whole
world with it — the game is a pure function of seed, scenario, difficulty and home
city, so sharing the link shares the exact game.
Before you pick a home city you choose a game. Present day starts in 2026 and runs 25 years — a clear runway. History, 2000 starts in 2000 and runs 50 years through the real shocks on their real dates: the dot-com recession, 9/11, SARS, the 2008 crash, the ash cloud, COVID. Aircraft arrive on their historical launch dates — you fly period metal and adopt each new jet as it enters service, with a handful of fictional next-gen types launching on an uncertain future date, so being early is an edge. COVID is survivable: a carrier that would fail during a declared crisis takes a government bailout, booked as debt, a limited number of times. It is designed to model reality closely without killing every airline on the board.
Aircraft gauge against market size. Frequency is derived from an aircraft's speed, range and turnaround — you never set it — so an oversized jet on a thin market flies half-empty and loses money where a smaller one would have made some. Adding aircraft to a sector wins share with diminishing returns, so profit peaks at a fleet size rather than scaling for ever.
Who else is on your market. Every carrier flying a city pair splits its demand in proportion to how attractive their service is. A rival arriving on one of your sectors costs roughly a third of your share there, so a route that was comfortable stops being comfortable. The sector dossier names who is on it.
When to spend, and what to insure. Fuel wanders a long way over twenty-five years and takes your margins with it. You can lock some of it forward, but a hedge is insurance and not a free bet: it costs a premium, and because the industry-wide fare surcharge tracks the spot price, locking in before a fall means paying over the odds while fares drop around you. Technology is the largest lever you have — a fixture that skips it fails more than twice as often and ends with a tenth of the net worth.
Lease or buy, and what to do as the fleet ages. Maintenance climbs with airframe age along a saturating curve. Leasing preserves capital but costs rent for ever, and handing an aircraft back inside its term carries a break fee — so you cannot churn to a fresh airframe for free. Buying is cheaper over a long horizon but leaves you holding the aging, with a heavy maintenance visit as the way to reset the clock. Over 25 years on a good sector, buy-and-hold beats lease-and-churn by roughly a third — if you can find the capital up front.
Every carrier has a share price — book value plus a multiple of trailing earnings, scaled by growth — and your market cap is the score. Borrow against your assets at a credit-rating-driven interest rate, issue equity when the price is high, buy stakes in rivals and, past a majority, buy them out. An acquisition folds a rival's routes, fleet and balance sheet into yours, funded with cash or debt, and runs a few quarters of integration drag.
It works in both directions. The roll-up archetype hunts weak carriers the same way you would, and a much larger predator will seize a player whose share price has cratered — a hostile takeover, and a real way to lose, alongside simply running out of cash.
Win by finishing with the most valuable carrier, or by clearing the board.
| Command | What it does |
|---|---|
npm run dev |
Dev server. Regenerates map data first. |
npm run build |
Typecheck, then production build to dist/. |
npm test |
Vitest. The sim core runs headless in Node. |
npm run typecheck |
tsc --noEmit. |
npm run simulate |
Headless games with summary stats — see below. |
npm run build:map |
Regenerate src/data/world-110m.geo.json from Natural Earth. |
The balance-tuning instrument and the regression-test substrate. Any balance change should carry before/after output from this in the commit message.
npm run simulate -- --seed 42 --turns 100 --players ai
npm run simulate -- --seed 1 --games 20 --json
npm run simulate -- --scenario history --games 20 # the 2000 start, 50-year run--scenario history runs the 2000 start with the scripted events and launch
dates; the turn count defaults to whichever scenario's full horizon.
tests/regression.test.ts runs 20 all-AI games over the full 25-year horizon in
CI and asserts invariants rather than exact figures.
src/
sim/ pure TypeScript, zero DOM, fully deterministic
engine.ts turn resolution — applyAction() and endTurn()
conditions.ts what the world is doing to a sector right now
demand.ts gravity market sizing, share split, fares
events.ts the event deck, fuel walk and completion draw
tech.ts the technology tree
economics.ts route and carrier P&L for a quarter
fleet.ts aircraft types, derived frequency, maintenance by age
geo.ts great-circle distance and arc sampling
rng.ts seeded mulberry32; Math.random is banned in here
save.ts versioned save format with a migration ladder
world.ts loads and validates the JSON data
headless.ts full-game driver shared by the CLI and CI
ai/
common.ts shared probing used by rivals and the balance fixture alike
archetype.ts rival entry, personality and one turn of decisions
heuristic.ts the headless balance fixture that stands in for a player
ui/ rendering and input only; reads state, dispatches actions
data/ JSON: cities, aircraft, archetypes, rivals, events, tech, constants
The sim exposes applyAction(state, action) -> state and endTurn(state) -> state.
State is plain serializable data; the UI never mutates it directly.
tests/purity.test.ts enforces the rules the compiler can't: no Math.random,
no Date.now, no DOM, no UI imports anywhere under src/sim/.
Same seed plus same inputs produces the same game, always. The PRNG state is a
single uint32 that rides along in the save file, so loading a save resumes the
random stream mid-flight rather than restarting it. tests/save.test.ts asserts
this by comparing the next turn either side of a round-trip.
Cities, aircraft and balance constants are JSON in src/data/. Content is added
there first and code second. Figures in cities.json and aircraft.json are
designed game-balance values, not a factual reference — the aircraft are
fictional types with recognizable class stats.
Map geometry comes from Natural Earth 1:110m
(public domain) via the world-atlas package, converted to GeoJSON and rounded
at build time by scripts/build-map.mjs. The output is generated, not committed.
Five devDependencies, no runtime dependencies. Every one has to earn its place:
- vite, typescript, vitest — build, types, tests.
- world-atlas + topojson-client — build-time only, for the map geometry.
- @types/node — for
scripts/andtests/;src/sim/must not use it.
There is no ts-node/tsx: Node 23+ runs the TypeScript scripts natively.
Static output, no backend.
npm run build
npx wrangler deploy # see wrangler.jsoncSee CONTRIBUTING.md. The short version: read
CLAUDE.md first, keep src/sim/ pure and deterministic, put
constants in JSON rather than code, and bring headless before/after numbers for
anything that moves the economy.
- Map geometry — Natural Earth 1:110m,
public domain, via the
world-atlaspackage. - Inter by Rasmus Andersson, under the SIL Open Font License 1.1. The latin
subset is vendored at
src/assets/fonts/, unmodified, with the full licence alongside it.
The names are invented. The numbers are not.
Every aircraft in the game is fictional — Aros, Vanta, Boreal, Cirro, Tarn, Halcyon — because using real manufacturers' names and liveries would mean using their trademarks. But each type is modelled on a specific real aircraft, and its seats, range, cruise speed and fuel burn are taken from that aircraft's published figures. The Aros N3 flies like an A321neo because it is one, with the badge filed off.
Which real type each one is modelled on lives in the basis field in
src/data/aircraft.json, and it is not decoration:
tests/fleet.test.ts holds a table of published seats, range, cruise and
entry-into-service dates and checks every type against it, so a figure cannot
drift away from the aircraft it claims to be. The roster spans the A220, A320neo
family, 737 MAX, 757, MD-80, E-Jets, ATRs, 767, A330, 787, A350, 777, 747 and
A380, plus a handful of invented next-generation types that launch on an uncertain
future date.
Two things the table does not pin, deliberately. Operating costs — lease rates, maintenance, crew — are balance figures, anchored to real orders of magnitude but tuned for the game. And fuel burn is calibrated for ordering rather than level: the roster reproduces the published efficiency ranking of these aircraft almost exactly, while sitting about 28% below published absolute figures. See DECISIONS.md and IDEAS.md.
Nothing here is affiliated with, endorsed by, or derived from any real airline or manufacturer.
This game was built with Claude Code, working from the design brief in CLAUDE.md — which is why that file sits at the repository root and is treated as the source of truth. DECISIONS.md records what was decided along the way and why, including the things that were tried, measured and thrown out.
MIT — Copyright (c) 2026 Matt Cone.
The bundled Inter font is separately licensed under the SIL OFL 1.1, which is not affected by the MIT licence on the rest of the project.
package.json is marked private on purpose: this is an application, not an npm
package, and the flag stops it being published to the registry by accident. It
says nothing about the licence.