AI Tycoon
An interactive tycoon-style simulation rendered with React 19,
Three.js, and @react-three/cannon for physics. The player starts
as Victorin Elsoñador — sole engineer, sole dreamer — and
grows a small AI model company in the country of Notchance
Parsimony. Each simulated tick, revenue, salaries, hardware
maintenance, and rent are deducted from cash; staff productivity
gradually raises the model’s quality; and random events fire from
a fully deterministic, seeded event loop.
Demo

The demo at /demos/ai-tycoon mounts the simulation as a
single React 19 island inside an Astro 7 page. The screenshot
above shows one tick of the engine: cash $24,590.25, two racks
on the compute floor, a DECISION REQUIRED prompt from the
Notchance Parsimony regulator (Defy or Comply, 10-rep penalty for
compliance), Victorin’s motivational speech bubble, and the last
ten events scrolling in the event log at the bottom.
The HUD on the left tracks eight meters — Cash, Day /
month, GPU (compute tier), Workers (office + remote),
Subs, Revenue, Morale, Reputation, and
Quality — and updates every tick. The right rail is the
Operations panel: it lists hire-office (+$800 / tick),
hire-remote (+$400 / tick), and the next Compute Floor
upgrade with its live price. Below it sits the simulation speed
selector (Pause / 0.25× / 0.5× / 1× / 2× / 4×).
What’s inside
Three endings, deterministic from one seed
Every run starts from a Mulberry32-seeded GameState. The same
seed reproduces the same company name, the same event sequence,
and the same outcome. There are three terminal endings:
| Ending | Trigger |
|---|---|
BANKRUPT |
cash <= 0 for 8 consecutive ticks |
INDUSTRY FAILED |
industryViability <= 0.1 and reputation < 40 for 16 consecutive ticks |
OPEN-WEIGHTS CHAMPION |
modelQuality >= 95, reputation >= 80, and tick >= 120 all true at once |
Once an ending fires, the engine locks in state.ending and the
EndScreen component takes over.
Eight event kinds
Every tick, the engine rolls one event from the seeded RNG. The kind affects different meters and sometimes spawns a sub-system (like the regulator NPC).
| Kind | Effect |
|---|---|
motivationalSpeech |
morale += 10; +5 % model quality growth for 4 ticks |
badReview |
morale -= 8; random internet critic |
govBlock |
Triggers the regulator NPC; player chooses Defy or Comply (-10 rep for comply) |
hardwareSpike |
Next upgrade costs 2× for 4 ticks (vendor gouging) |
communityLove |
reputation += 5 |
investor |
One-shot cash injection from a named investor |
competitorRelease |
Quarterly (tick % 720) competitor drop; qualityLossPct + subscriberLossPct dip |
founderDecision |
Victorin takes action on his own (5 % per tick; hire or upgrade if affordable) |
The event log at the bottom of the simulation shell keeps the last ten events visible at any speed, and auto-scrolls to the newest entry.
The regulator (NPC)
Government blocking events spawn a regulator character that walks
into the office over 3 ticks (arriving), stands at the door
(present), and walks back out after the player resolves the
prompt (leaving). The DECISION REQUIRED panel sits above the
office with a 3-second countdown; if the player ignores it,
Victorin decides randomly and the outcome is logged as a
founderDecision.
Defying the regulator has no immediate reputation cost — Victorin’s attitude toward Notchance Parsimony is canonical defiance, so complying is the only way to lose rep.
Six hardware tiers
hardwareTiers lists the full ladder from a solo laptop to a
32-GPU datacenter cluster. Each upgrade raises
modelQualityCeiling by compute * 2, and every tier adds
maintenance costs ($150 × (tier + 1) × 24 per simulated day).
| Tier | Name | Base price | Compute |
|---|---|---|---|
| 0 | Solo Laptop | $200 | 1 |
| 1 | Single Workstation GPU | $2,500 | 4 |
| 2 | 4-GPU Rig | $12,000 | 14 |
| 3 | 8-GPU Cluster | $40,000 | 35 |
| 4 | 16-GPU Pod | $110,000 | 80 |
| 5 | 32-GPU Datacenter Cluster | $300,000 | 180 |
The “GPU” meter in the HUD shows the current tier (0-5). Final
upgrade cost is basePrice * hardwarePriceIndex * spikeMultiplier,
so a hardware spike temporarily doubles the price for the next
four ticks.
Staff: founder + up to 11 hires
The founder (Victorin Elsoñador, tier: talent, productivity: 0.7, salaryPerTick: 0) is always present. Hires come in two
flavors — office (a 3D character on the floor, +$800 / tick)
or remote (floats off-screen connected by dashed lines to a
cloud, +$400 / tick). The cap is 12 total staff (founder +
11). Each hire is generated from a random officeRoles /
remoteRoles entry and tagged with a normal, pro, or
talent tier; the tier feeds into TIER_QUALITY_BONUS, which
multiplies the per-tick model-quality growth.
Victorin’s autonomy
Every tick there’s a 5 % chance Victorin acts on his own. He
will hire a new teammate (office or remote, 50/50) if there’s a
free slot, or buy the next hardware tier if the company can
afford it. Founder-driven events are logged as founderDecision
with a separate message (“Victorin Elsoñador hired a new office
teammate.”), so the event log always tells you whether a hire
came from the player or from Victorin.
The office scene
A Three.js orthographic isometric camera renders the office
floor with assets/isometricUrls, assets/furnitureUrls, and
assets/characterUrls (Kenney Blocky Characters furniture kit
assets). Each tick, characters walk to a new desk via
officeMovement.ts, the server rack grows when hardware is
upgraded, and remote workers stay connected to the cloud with
dashed lines.
Architecture
- Astro 7 routes
/demos/ai-tycoonand renders a single React 19 island (client:load) for the simulation engine, HUD, and Three.js canvas. - Pure simulation core.
demo/engine/exposesstepTick,applyAction, and the event atoms fromevents.tsas pure functions overGameState— no mutation, no side-effects. Every action returns a brand-new state object. - Deterministic RNG.
engine/rng.tsships a Mulberry32 PRNG (mulberry32) pluspickIntandpickOnehelpers. Every atom that needs randomness takes an explicitrngparameter, so vitest specs can replay any tick from a fixed seed. - State is
useState-held, never lifted.index.tsxowns the singleGameStateviauseStateand mirrors it into astateRefso thesetIntervaltick loop always reads the latest state without re-creating timers. The interval is recreated only whenpaused,ending, orweekSpeedMschange. - Three.js scene graph is built with
@react-three/fiber, physics comes from@react-three/cannon(gravity + mass on box characters), and all sprite assets are loaded from the Kenney furniture kit URLs indemo/assets/. - Tailwind 4 (Vite plugin) styles the HUD, decision panel,
end screen, and event log with CSS custom-property tokens
defined in
src/styles/global.css. - TypeScript strict across the engine, components, and
tests.
applyActionuses an exhaustive_exhaustive: nevercheck on theActiondiscriminated union, so any new action kind is a compile-time error until its reducer arm exists.
Trade-offs
- No save / no resume. The seed is regenerated on every page
load (
nextSimulationSeed(current.seed)on mount). The sim is meant to be replayed, not persisted. - Three.js + cannon add weight. The full demo bundle
(Three.js, cannon-es, Kenney sprites) is heavier than the
resume-generator demo. It only loads on
/demos/ai-tycoon, not on the rest of the site. - Worker salaries are paid even when paused is not true —
pausing halts
stepTick, so salaries and rent stop accruing. Victorin’s autonomy RNG is also paused. - Determinism costs replayability. Same seed → same game. Players who want to branch from a particular moment have to fork the seed manually.
@react-three/cannonbox physics is intentionally simple (gravity + mass, no joints). Characters look like they’re standing on the floor; they don’t collide with each other.
Try the demo
sersofts.org/demos/ai-tycoon on the live site. The demo works on desktop and tablet; phones get the simulation shell but the embedded overlay is tuned for ≥ 760 px viewports. Click + in the HUD to expand any meter, hit Pause to freeze the simulation, and try Defy on the first regulator prompt to see Victorin’s preferred path.
Files of note
demo/engine/sim.ts—stepTick,applyAction,stepCashFlow,stepModelQuality, and all the per-tick sub-reducers. TheActiondiscriminated union lives here.demo/engine/endings.ts—checkBankrupt,checkIndustryFailed,checkChampion, and the streak thresholds (BANKRUPT_STREAK_THRESHOLD = 8,INDUSTRY_FAILED_STREAK_THRESHOLD = 16,CHAMPION_MIN_TICK = 120).demo/engine/events.ts—sampleNextEventand per-kind reducers (applyMotivationalSpeech,applyBadReview,applyHardwareSpike,applyCommunityLove,applyInvestor,applyCompetitorRelease,scheduleGovBlock).demo/engine/rng.ts—mulberry32,pickInt,pickOne.demo/engine/initialState.ts—createInitialState(seed); the canonical startingGameState.demo/index.tsx— top-level island, owns theuseState/stateRefpair and thesetIntervaltick loop.demo/components/Scene.tsx— Three.js orthographic isometric scene + physics + the HUD overlay shell.demo/components/HUD.tsx— left-rail meters (cash, day, GPU, workers, subs, revenue, morale, reputation, quality).demo/components/DecisionPanel.tsx— right-rail operations panel (hire-office, hire-remote, compute floor upgrade).demo/components/DecisionPrompt.tsx— regulator decision panel with the Defy / Comply buttons.demo/components/EndScreen.tsx— bankrupt / industry-failed / champion overlays with a Restart button that re-seeds.demo/components/EventFeed.tsx— last ten events with auto-scroll.demo/data/hardwareTiers.ts— six-tier ladder, base prices, compute multipliers.demo/data/companyNames.ts— WoW-flavored name pool (Sylvanas,Ironforge,Frostmourne, …) with category badges.demo/data/founderAutonomy.ts— 5 % per-tick probability Victorin acts on his own.tests/unit/— vitest specs forstepTick, endings, and event reducers.tests/e2e/— Playwright specs for demo hydration, the operations panel, and the GPU upgrade adding a rack.
