- Create researchBonuses utility to aggregate tech tree effects into all game systems (infrastructure energy costs, compute efficiency, training speed, model capability, reputation) - Rework model capability from sqrt(compute) to 4-pillar formula (params + compute + data + research) - Make context window affect benchmarks and inference speed - Add MoE tradeoffs: 1.5x VRAM, 0.8x training speed - Enforce research point costs as a gate for unlocking research - Add real consequences to data contamination events (reputation hit, legal costs) - Scale talent costs from $0.03 to $5/tick per headcount - Scale compliance costs 100x to be meaningful - Rework competitor acquisition: cheaper but grants headcount, RP, and reputation - Remove dead code: sfxVolume, autoSaveInterval, notificationsEnabled, FAST_FORWARD_BATCH_SIZE, CHINCHILLA_OPTIMAL_RATIO Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
Architecture Guide
Layer Stack
React UI Layer (components, pages, dashboards)
↕ reads/writes via Zustand selectors and actions
Zustand State Layer (14 slices, persisted to localStorage)
↕ engine writes state, UI reads it
Game Engine (pure TypeScript, zero DOM dependencies)
↕ uses formulas, runs systems
Simulation Core (math, scaling laws, cost curves)
The game engine and simulation core have no React dependency. They can run identically for real-time play, offline catch-up, and automated testing.
Monorepo Layout
ai-tycoon/
├── turbo.json # Turborepo task config
├── pnpm-workspace.yaml # Workspace definition
│
├── apps/
│ ├── web/ # React frontend (Vite)
│ │ └── src/
│ │ ├── components/ # layout/, common/, charts/, game/
│ │ ├── pages/ # One page per game system
│ │ ├── store/ # Zustand store with slice pattern
│ │ ├── hooks/ # useGameLoop, useOfflineCatchUp, etc.
│ │ └── lib/ # API client, formatters
│ │
│ └── server/ # Hono backend
│ └── src/
│ ├── db/ # Drizzle schema + migrations
│ ├── routes/ # auth, saves, leaderboards
│ ├── middleware/ # auth, rate limiting
│ └── services/ # Business logic
│
└── packages/
├── shared/ # Shared types & constants
│ └── src/
│ ├── types/ # All game state type definitions
│ ├── constants/ # gameBalance.ts (single source of truth)
│ └── utils/ # Number/money/time formatting
│
└── game-engine/ # Pure TS game simulation
└── src/
├── engine.ts # GameEngine class (tick scheduling)
├── tick.ts # Tick processor (orchestrates systems)
├── systems/ # One file per simulation system
└── data/ # Tech tree, achievements, datasets
Tick System
The game uses a fixed-tick model: 1 tick = 1 second of game time at 1x speed.
Game Loop
The useGameLoop hook runs in the React app:
requestAnimationFramefires continuously- An accumulator tracks elapsed time since the last tick
- When accumulated time exceeds
TICK_INTERVAL_MS / gameSpeed, a tick fires processTick(state)runs all systems and returns a partial state update- A single
setStatecall applies the update to Zustand
Speed controls (1x, 2x, 5x) divide the tick interval, so 5x processes 5 ticks per real second.
Per-Tick Processing Order
Each tick runs these systems sequentially, since later systems depend on earlier results:
1. Infrastructure — GPU health, failures, maintenance
2. Models — Training progress, model completion
3. Market — Demand, subscribers, API calls, revenue calc
4. Compute — Capacity, utilization, allocation
5. Talent — Department effectiveness, morale
6. Research — R&D progress, project completion
7. Reputation — Safety incidents, regulatory standing, score
8. Economy — Revenue, expenses, net cash flow
9. Data — Data acquisition, user data flywheel
10. Competitors — AI rival decisions and actions
11. Era check — Threshold-based era transitions
12. Valuation — Dynamic company valuation
13. Achievements — Milestone checks (every 10 ticks)
Offline Catch-Up
When the player returns after being away:
- Elapsed ticks =
min((now - lastTick) / interval, MAX_OFFLINE_TICKS) - Max offline cap: 24 hours (86,400 ticks)
- Ticks process with reduced fidelity (
OFFLINE_EFFICIENCY = 0.8) - A progress bar shows catch-up progress
- A summary screen reports what happened while away
State Management
Zustand Store
The store uses a slice pattern with 14 slices, each owning a portion of the game state:
| Slice | Responsibility |
|---|---|
gameMetaSlice |
Tick count, era, pause state, speed, timestamps |
economySlice |
Money, revenue, expenses, funding, financial history |
infrastructureSlice |
Datacenters, GPUs, locations, total FLOPS |
computeSlice |
Training/inference allocation, utilization |
researchSlice |
Tech tree, active/completed projects |
modelsSlice |
Trained models, active training, deployment |
marketSlice |
Subscribers, API demand, pricing, overload policy |
competitorSlice |
Rival AI labs, their models, actions |
talentSlice |
Departments, headcount, morale, hiring |
dataSlice |
Datasets, quality, user data generation |
reputationSlice |
Safety record, public perception, regulatory standing |
achievementSlice |
Unlocked achievements |
uiSlice |
Active page, notifications, modals (not persisted) |
Persistence
- localStorage: Auto-save every 60 ticks under key
ai-tycoon-save. The Zustandpersistmiddleware handles serialization. - Cloud saves: Optional. POST to
/api/savesevery 5 minutes when authenticated. Requires the Hono backend + PostgreSQL. - Save format versioning: A
versionfield in meta enables migration functions for breaking state changes.
State Flow
User Action (click "Buy GPU")
→ Zustand action (buyGPU)
→ Mutates store directly (immer-style)
→ React re-renders via selectors
Tick fires
→ processTick(currentState)
→ Returns Partial<GameState>
→ Zustand merges into store
→ React re-renders
Actions that happen instantly (buying, selling, hiring) mutate the store directly. The tick system handles everything time-based (training progress, revenue accrual, demand changes).
Game Engine Systems
Economy System (economySystem.ts)
Calculates per-tick financials:
- Revenue: Market revenue (API tokens + subscriptions)
- Expenses: GPU energy costs + maintenance + talent salaries + compliance costs
- Compliance: Scales with model capability and era index:
capability * REGULATION_COMPLIANCE_PER_CAPABILITY * (1 + eraIdx * 0.5) / 100 - Financial snapshots: Recorded every 60 ticks, circular buffer of 1000
Infrastructure System (infrastructureSystem.ts)
- Each GPU has a per-tick failure probability (
GPU_FAILURE_RATE_BASE) - Redundancy level reduces failure rate by
REDUNDANCY_FAILURE_REDUCTION - Failed GPUs are removed from capacity calculations
- Total FLOPS = sum of healthy GPU FLOPS across all datacenters
- Locations have different energy costs, latency tiers, and regulatory environments
Model System (modelSystem.ts)
Training a model:
- Player commits compute + data tokens + researchers
- Progress increments per tick, boosted by researcher and engineer effectiveness
- On completion,
createTrainedModelgenerates capability scores:- Base capability from
sqrt(compute) * 5 + log10(1 + dataTokens/1e8) * 10 + researchBonus - Each capability dimension (reasoning, coding, creative, multimodal, agents) gets a random multiplier
- Safety score:
30 + safetyResearch * 15 + random * 10 - Benchmark penalized by high safety:
(safetyScore - 60) * 0.1when safety > 60
- Base capability from
Market System (marketSystem.ts)
- Subscribers: Grow based on model quality, churn based on satisfaction
- API demand: Function of subscriber count and pricing
- Open-source effect: Boosts subscriber growth, reduces revenue per model
- Overload handling: When demand > capacity, player-configured policy determines behavior (queue, degrade quality, prioritize enterprise)
- Subscriber history: Sampled every 60 ticks, max 500 entries
Reputation System (reputationSystem.ts)
Composite score (0-100) from four factors:
- Safety record (30%): Damaged by safety incidents
- Public perception (30%): Affected by incidents and open-sourcing models
- Employee satisfaction (20%): Driven by department morale averages
- Regulatory standing (20%):
50 + safetyResearch * 8 - eraIndex * 5
Safety incidents trigger when deployed models have safety scores below LOW_SAFETY_THRESHOLD (40), checked every 60 ticks with probability SAFETY_INCIDENT_PROBABILITY_BASE * (threshold - safetyLevel).
Competitor System (competitorSystem.ts)
- 3+ AI rival labs with archetypes (safety-first, move-fast, big tech, open-source)
- Scripted core milestones for pacing
- Personality-driven decision-making
- Light reactive behavior to player actions
- Can be acquired in bigtech/agi eras
Achievement System (achievementSystem.ts)
- 15 achievement definitions with dot-path field access conditions
- Virtual fields for computed values (
meta._eraIndex,meta._deployedModelCount,infrastructure._totalGpuCount) - Checked every 10 ticks for performance
- Notifications on unlock
Funding System (fundingSystem.ts)
- 6 rounds: Seed → Series A → B → C → D → IPO
- Each round has amount, dilution percentage, and requirements (min revenue, users, reputation)
- Dynamic valuation computed from revenue, subscribers, and model capability
- Dilution reduces founder equity permanently
Frontend Architecture
Routing
No router library — the active page is stored in Zustand (uiSlice.activePage). The sidebar sets it, MainLayout renders the corresponding page component. This keeps things simple for a single-page game.
Pages
| Page | System |
|---|---|
| Dashboard | Overview KPIs, tutorial hints, trend charts |
| Infrastructure | Datacenter management, GPU purchasing |
| Research | Tech tree, active projects |
| Models | Training, deployment, tuning |
| Market | Pricing, subscribers, overload policy |
| Talent | Department management, hiring |
| Data | Dataset marketplace, data quality |
| Competitors | Rival labs, acquisition |
| Finance | Cash flow, funding rounds, financial history |
| Achievements | Achievement grid with unlock status |
| Leaderboard | Global rankings by category |
| Settings | Sound, save management, export/import |
Progressive Disclosure
- Pages unlock as the player progresses through eras
- Newly available pages get a "NEW" badge in the sidebar
- Tutorial hints appear contextually and persist dismissal to localStorage
Charts
All charts use Recharts with consistent dark-theme styling. Chart data comes from circular buffer history arrays in the game state (financial snapshots, subscriber history, reputation history).
Backend Architecture
API Routes
POST /api/auth/anonymous # Create anonymous session (UUID token)
POST /api/auth/link # Link email/password to anonymous session
POST /api/auth/login # Email/password login
GET /api/saves # List saves for authenticated user
POST /api/saves # Create/update save
GET /api/saves/:id # Load specific save
GET /api/leaderboard/:cat # Get leaderboard by category
POST /api/leaderboard # Submit score
Database
PostgreSQL with Drizzle ORM. Tables: users, saves, leaderboard_entries.
Auth
Anonymous-first: players get a UUID token on first visit. Optional email/password linking for cross-device play. JWT-based session tokens.
Performance Considerations
- Game engine runs outside React's render cycle; single
setStateper tick - Achievements check every 10 ticks
- History arrays use circular buffers with configurable max sizes
- Snapshots sample at intervals (every 60-120 ticks), not every tick
- All systems are bounded O(n) where n is small (number of GPUs, models, competitors)
- Speed modes simply increase tick frequency, not computation complexity
Key Design Decisions
-
Pure engine: The game-engine package has zero DOM/React dependencies, enabling offline catch-up, testing, and potential server-side simulation.
-
Single constants file: All balance numbers live in
gameBalance.ts. One place to tune everything. -
Slice pattern: Each game system owns its state slice. Slices compose into the full store without coupling.
-
Notifications via
_notifications: The tick processor attaches notifications as a side-channel property on the result, keeping the return type clean while enabling the UI to show contextual alerts. -
Cached definitions: Achievement definitions are set once at game start via
setAchievementDefinitions, avoiding repeated imports in the hot tick path.