Files
AIHostingTycoon/docs/architecture.md
josh c1cc70eeb9
Balance Check / balance-simulation (pull_request) Successful in 38s
Balance Check / multi-run-balance (pull_request) Successful in 13m44s
Rename AI Tycoon to Token Empire across entire codebase
Full rebrand: UI display text, package scope (@ai-tycoon/* -> @token-empire/*),
localStorage keys, Docker/CI image paths, database names, and documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-27 21:04:07 -04:00

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

token-empire/
├── 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:

  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. 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 token-empire-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 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 setState per 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

  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: Achievement definitions are set once at game start via setAchievementDefinitions, avoiding repeated imports in the hot tick path.