6c7348f924
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
303 lines
13 KiB
Markdown
303 lines
13 KiB
Markdown
# 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/, events/, 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/ # Event definitions, tech tree, 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:
|
|
|
|
1. `requestAnimationFrame` fires continuously
|
|
2. An accumulator tracks elapsed time since the last tick
|
|
3. When accumulated time exceeds `TICK_INTERVAL_MS / gameSpeed`, a tick fires
|
|
4. `processTick(state)` runs all systems and returns a partial state update
|
|
5. A single `setState` call 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. Events — Random + condition-triggered events
|
|
12. Era check — Threshold-based era transitions
|
|
13. Valuation — Dynamic company valuation
|
|
14. 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 in batches of `FAST_FORWARD_BATCH_SIZE` (100) 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 |
|
|
| `eventSlice` | Active events, history, cooldowns |
|
|
| `achievementSlice` | Unlocked achievements |
|
|
| `uiSlice` | Active page, notifications, modals (not persisted) |
|
|
|
|
### Persistence
|
|
|
|
- **localStorage**: Auto-save every 60 ticks under key `ai-tycoon-save`. The Zustand `persist` middleware handles serialization.
|
|
- **Cloud saves**: Optional. POST to `/api/saves` every 5 minutes when authenticated. Requires the Hono backend + PostgreSQL.
|
|
- **Save format versioning**: A `version` field 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:
|
|
1. Player commits compute + data tokens + researchers
|
|
2. Progress increments per tick, boosted by researcher and engineer effectiveness
|
|
3. On completion, `createTrainedModel` generates 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.1` when safety > 60
|
|
|
|
### 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 events and incidents
|
|
- **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)`.
|
|
|
|
### Event System (`eventSystem.ts`)
|
|
|
|
- 40+ event definitions across 6 categories
|
|
- Events have conditions (era, tick count, state thresholds), weights, cooldowns, and max occurrences
|
|
- Most events present 2-3 choices with consequences affecting money, reputation, compute, or other state
|
|
- Template system with variable interpolation prevents repetition
|
|
- Geopolitical events (export controls, energy crises, natural disasters) affect specific regions
|
|
|
|
### 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 `setState` per tick
|
|
- Achievements check every 10 ticks, events every 30 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
|
|
|
|
1. **Pure engine**: The game-engine package has zero DOM/React dependencies, enabling offline catch-up, testing, and potential server-side simulation.
|
|
|
|
2. **Single constants file**: All balance numbers live in `gameBalance.ts`. One place to tune everything.
|
|
|
|
3. **Slice pattern**: Each game system owns its state slice. Slices compose into the full store without coupling.
|
|
|
|
4. **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.
|
|
|
|
5. **Cached definitions**: Event and achievement definitions are set once at game start via `setEventDefinitions` / `setAchievementDefinitions`, avoiding repeated imports in the hot tick path.
|