Initial scaffold: AI Tycoon monorepo with core game loop

Turborepo monorepo with three packages:
- packages/shared: TypeScript types for all 14 game systems + balance constants + formatting utils
- packages/game-engine: Pure TS simulation engine with tick processor, economy, infrastructure, compute, research, market, and reputation systems
- apps/web: React + Vite + Tailwind + Zustand frontend with sidebar dashboard layout, new game screen, dashboard with charts, infrastructure management, and model training pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 16:53:46 -04:00
commit fdc8e544ae
57 changed files with 4753 additions and 0 deletions
@@ -0,0 +1,42 @@
export const TICK_INTERVAL_MS = 1000;
export const MAX_OFFLINE_TICKS = 86_400;
export const OFFLINE_EFFICIENCY = 0.8;
export const FAST_FORWARD_BATCH_SIZE = 100;
export const AUTO_SAVE_INTERVAL_TICKS = 60;
export const FINANCIAL_SNAPSHOT_INTERVAL = 60;
export const MAX_FINANCIAL_HISTORY = 1000;
export const MAX_EVENT_HISTORY = 50;
export const MAX_REPUTATION_HISTORY = 500;
export const STARTING_MONEY = 50_000;
export const BASE_ENERGY_COST_PER_FLOP = 0.001;
export const BASE_MAINTENANCE_PER_GPU = 0.5;
export const TRAINING_BASE_TICKS = 120;
export const TRAINING_COMPUTE_MULTIPLIER = 0.8;
export const TRAINING_DATA_QUALITY_WEIGHT = 0.3;
export const CAPABILITY_FORMULA = {
computeWeight: 0.4,
dataWeight: 0.3,
researcherWeight: 0.2,
efficiencyWeight: 0.1,
};
export const CONSUMER_BASE_GROWTH = 0.002;
export const CONSUMER_QUALITY_GROWTH_MULTIPLIER = 0.01;
export const CONSUMER_PRICE_ELASTICITY = -0.5;
export const CONSUMER_BASE_CHURN = 0.001;
export const API_TOKENS_PER_REQUEST = 500;
export const API_REVENUE_PER_MTOK = 1.0;
export const ERA_THRESHOLDS = {
scaleup: { revenue: 10_000, capability: 15, reputation: 30 },
bigtech: { revenue: 1_000_000, capability: 50, reputation: 60 },
agi: { revenue: 100_000_000, capability: 90, reputation: 70 },
};
export const GPU_PRICE_VOLATILITY = 0.02;
export const GPU_FAILURE_RATE_BASE = 0.0001;
export const REDUNDANCY_FAILURE_REDUCTION = 0.5;
+15
View File
@@ -0,0 +1,15 @@
export * from './types/gameState';
export * from './types/economy';
export * from './types/infrastructure';
export * from './types/compute';
export * from './types/research';
export * from './types/models';
export * from './types/market';
export * from './types/competitors';
export * from './types/talent';
export * from './types/data';
export * from './types/reputation';
export * from './types/events';
export * from './types/achievements';
export * from './utils/formatting';
export * from './constants/gameBalance';
+28
View File
@@ -0,0 +1,28 @@
export interface AchievementState {
unlocked: UnlockedAchievement[];
progress: Record<string, number>;
}
export interface UnlockedAchievement {
id: string;
unlockedAtTick: number;
}
export interface AchievementDefinition {
id: string;
name: string;
description: string;
icon: string;
condition: AchievementCondition;
}
export interface AchievementCondition {
field: string;
operator: 'gt' | 'gte' | 'eq';
value: number;
}
export const INITIAL_ACHIEVEMENTS: AchievementState = {
unlocked: [],
progress: {},
};
+35
View File
@@ -0,0 +1,35 @@
export interface CompetitorState {
rivals: Competitor[];
industryBenchmark: number;
}
export interface Competitor {
id: string;
name: string;
archetype: CompetitorArchetype;
personality: CompetitorPersonality;
status: 'active' | 'acquired' | 'failed';
estimatedCapability: number;
estimatedRevenue: number;
estimatedUsers: number;
reputation: number;
latestModelName: string;
completedMilestones: string[];
nextMilestoneAtTick: number;
}
export type CompetitorArchetype = 'safety-first' | 'move-fast' | 'big-tech' | 'open-source' | 'stealth-startup';
export interface CompetitorPersonality {
aggression: number;
safetyFocus: number;
openSourceTendency: number;
marketingFocus: number;
researchFocus: number;
riskTolerance: number;
}
export const INITIAL_COMPETITORS: CompetitorState = {
rivals: [],
industryBenchmark: 0,
};
+17
View File
@@ -0,0 +1,17 @@
export interface ComputeState {
totalFlops: number;
trainingAllocation: number;
inferenceAllocation: number;
inferenceUtilization: number;
tokensPerSecondCapacity: number;
tokensPerSecondDemand: number;
}
export const INITIAL_COMPUTE: ComputeState = {
totalFlops: 0,
trainingAllocation: 0.5,
inferenceAllocation: 0.5,
inferenceUtilization: 0,
tokensPerSecondCapacity: 0,
tokensPerSecondDemand: 0,
};
+47
View File
@@ -0,0 +1,47 @@
export interface DataState {
ownedDatasets: OwnedDataset[];
userDataGenerationRate: number;
totalTrainingTokens: number;
partnerships: DataPartnership[];
}
export interface OwnedDataset {
id: string;
name: string;
domain: DataDomain;
sizeTokens: number;
quality: number;
legalRisk: number;
acquiredAtTick: number;
}
export type DataDomain = 'web' | 'books' | 'code' | 'scientific' | 'conversation'
| 'multilingual' | 'images' | 'video' | 'audio' | 'synthetic';
export interface DataPartnership {
id: string;
partnerName: string;
domain: DataDomain;
tokensPerTick: number;
costPerTick: number;
exclusivity: boolean;
durationTicks: number;
startTick: number;
}
export const INITIAL_DATA: DataState = {
ownedDatasets: [
{
id: 'web-crawl-basic',
name: 'Basic Web Crawl',
domain: 'web',
sizeTokens: 1_000_000_000,
quality: 0.3,
legalRisk: 0.2,
acquiredAtTick: 0,
},
],
userDataGenerationRate: 0,
totalTrainingTokens: 1_000_000_000,
partnerships: [],
};
+63
View File
@@ -0,0 +1,63 @@
export interface EconomyState {
money: number;
totalRevenue: number;
totalExpenses: number;
revenuePerTick: number;
expensesPerTick: number;
funding: FundingState;
financialHistory: FinancialSnapshot[];
}
export interface FundingState {
totalRaised: number;
currentRound: FundingRound | null;
completedRounds: CompletedFundingRound[];
founderEquity: number;
valuation: number;
isPublic: boolean;
}
export type FundingRoundType = 'seed' | 'seriesA' | 'seriesB' | 'seriesC' | 'seriesD' | 'ipo';
export interface FundingRound {
type: FundingRoundType;
targetAmount: number;
dilution: number;
requirements: {
minRevenue?: number;
minUsers?: number;
minReputation?: number;
};
}
export interface CompletedFundingRound {
type: FundingRoundType;
amount: number;
dilution: number;
completedAtTick: number;
}
export interface FinancialSnapshot {
tick: number;
money: number;
revenue: number;
expenses: number;
valuation: number;
}
export const INITIAL_ECONOMY: EconomyState = {
money: 50_000,
totalRevenue: 0,
totalExpenses: 0,
revenuePerTick: 0,
expensesPerTick: 0,
funding: {
totalRaised: 0,
currentRound: null,
completedRounds: [],
founderEquity: 1.0,
valuation: 100_000,
isPublic: false,
},
financialHistory: [],
};
+74
View File
@@ -0,0 +1,74 @@
import type { Era } from './gameState';
export interface EventState {
activeEvents: ActiveEvent[];
eventHistory: EventHistoryEntry[];
eventCooldowns: Record<string, number>;
eventOccurrences: Record<string, number>;
}
export interface ActiveEvent {
eventId: string;
instanceId: string;
triggeredAtTick: number;
expiresAtTick: number;
title: string;
description: string;
category: EventCategory;
choices: EventChoice[];
defaultChoiceIndex: number;
}
export type EventCategory = 'industry' | 'regulatory' | 'pr' | 'internal' | 'market';
export interface EventChoice {
label: string;
description: string;
consequences: EventConsequence[];
}
export interface EventConsequence {
type: 'money' | 'reputation' | 'compute' | 'talent' | 'research_speed'
| 'regulation' | 'competitor' | 'unlock' | 'lock' | 'chain_event';
value: number;
target?: string;
delay?: number;
}
export interface EventHistoryEntry {
eventId: string;
instanceId: string;
title: string;
category: EventCategory;
tick: number;
chosenOptionIndex: number;
}
export interface EventDefinition {
id: string;
title: string;
descriptionTemplate: string;
category: EventCategory;
eras: Era[];
weight: number;
cooldownTicks: number;
maxOccurrences: number;
prerequisites: string[];
conditions: EventCondition[];
choices: EventChoice[];
defaultChoiceIndex: number;
expiryTicks: number;
}
export interface EventCondition {
field: string;
operator: 'gt' | 'lt' | 'gte' | 'lte' | 'eq';
value: number;
}
export const INITIAL_EVENTS: EventState = {
activeEvents: [],
eventHistory: [],
eventCooldowns: {},
eventOccurrences: {},
};
+63
View File
@@ -0,0 +1,63 @@
import type { EconomyState } from './economy';
import type { InfrastructureState } from './infrastructure';
import type { ComputeState } from './compute';
import type { ResearchState } from './research';
import type { ModelsState } from './models';
import type { MarketState } from './market';
import type { CompetitorState } from './competitors';
import type { TalentState } from './talent';
import type { DataState } from './data';
import type { ReputationState } from './reputation';
import type { EventState } from './events';
import type { AchievementState } from './achievements';
export interface GameState {
meta: GameMeta;
economy: EconomyState;
infrastructure: InfrastructureState;
compute: ComputeState;
research: ResearchState;
models: ModelsState;
market: MarketState;
competitors: CompetitorState;
talent: TalentState;
data: DataState;
reputation: ReputationState;
events: EventState;
achievements: AchievementState;
}
export interface GameMeta {
saveVersion: number;
companyName: string;
currentEra: Era;
tickCount: number;
lastTickTimestamp: number;
gameSpeed: GameSpeed;
isPaused: boolean;
createdAt: number;
totalPlayTime: number;
settings: GameSettings;
}
export type Era = 'startup' | 'scaleup' | 'bigtech' | 'agi';
export type GameSpeed = 1 | 2 | 5;
export interface GameSettings {
autoSaveInterval: number;
notificationsEnabled: boolean;
soundEnabled: boolean;
musicVolume: number;
sfxVolume: number;
}
export const INITIAL_SETTINGS: GameSettings = {
autoSaveInterval: 60,
notificationsEnabled: true,
soundEnabled: true,
musicVolume: 0.5,
sfxVolume: 0.7,
};
export const SAVE_VERSION = 1;
@@ -0,0 +1,84 @@
import type { Era } from './gameState';
export interface InfrastructureState {
dataCenters: DataCenter[];
gpuMarketPrices: Record<GpuType, number>;
totalFlops: number;
totalUptime: number;
}
export interface DataCenter {
id: string;
name: string;
location: LocationId;
gpus: GpuInventory[];
maxCapacity: number;
coolingLevel: number;
redundancyLevel: number;
currentUptime: number;
energyCostPerTick: number;
maintenanceCostPerTick: number;
}
export interface GpuInventory {
type: GpuType;
count: number;
healthyCount: number;
failedCount: number;
}
export type GpuType = 'consumer' | 't4' | 'a100' | 'h100' | 'b200' | 'custom';
export type LocationId = 'us-west' | 'us-east' | 'eu-west' | 'eu-north' | 'asia-east' | 'asia-south' | 'middle-east';
export interface LocationConfig {
id: LocationId;
name: string;
energyCostMultiplier: number;
latencyTier: number;
regulatoryStrictness: number;
politicalStability: number;
availableAt: Era;
}
export interface GpuConfig {
type: GpuType;
name: string;
flopsPerUnit: number;
basePowerDraw: number;
basePrice: number;
availableAt: Era;
}
export const GPU_CONFIGS: Record<GpuType, GpuConfig> = {
consumer: { type: 'consumer', name: 'Consumer GPU', flopsPerUnit: 1, basePowerDraw: 0.3, basePrice: 800, availableAt: 'startup' },
t4: { type: 't4', name: 'NVIDIA T4', flopsPerUnit: 8, basePowerDraw: 0.5, basePrice: 5_000, availableAt: 'startup' },
a100: { type: 'a100', name: 'NVIDIA A100', flopsPerUnit: 40, basePowerDraw: 1.0, basePrice: 15_000, availableAt: 'scaleup' },
h100: { type: 'h100', name: 'NVIDIA H100', flopsPerUnit: 120, basePowerDraw: 1.2, basePrice: 35_000, availableAt: 'scaleup' },
b200: { type: 'b200', name: 'NVIDIA B200', flopsPerUnit: 400, basePowerDraw: 1.5, basePrice: 50_000, availableAt: 'bigtech' },
custom: { type: 'custom', name: 'Custom ASIC', flopsPerUnit: 800, basePowerDraw: 1.0, basePrice: 80_000, availableAt: 'agi' },
};
export const LOCATION_CONFIGS: Record<LocationId, LocationConfig> = {
'us-west': { id: 'us-west', name: 'US West (Oregon)', energyCostMultiplier: 1.0, latencyTier: 1, regulatoryStrictness: 0.3, politicalStability: 0.9, availableAt: 'startup' },
'us-east': { id: 'us-east', name: 'US East (Virginia)', energyCostMultiplier: 1.1, latencyTier: 1, regulatoryStrictness: 0.3, politicalStability: 0.9, availableAt: 'startup' },
'eu-west': { id: 'eu-west', name: 'EU West (Ireland)', energyCostMultiplier: 1.3, latencyTier: 2, regulatoryStrictness: 0.7, politicalStability: 0.85, availableAt: 'scaleup' },
'eu-north': { id: 'eu-north', name: 'EU North (Sweden)', energyCostMultiplier: 0.8, latencyTier: 2, regulatoryStrictness: 0.6, politicalStability: 0.95, availableAt: 'scaleup' },
'asia-east': { id: 'asia-east', name: 'Asia East (Tokyo)', energyCostMultiplier: 1.4, latencyTier: 3, regulatoryStrictness: 0.4, politicalStability: 0.9, availableAt: 'scaleup' },
'asia-south': { id: 'asia-south', name: 'Asia South (Mumbai)', energyCostMultiplier: 0.6, latencyTier: 3, regulatoryStrictness: 0.2, politicalStability: 0.7, availableAt: 'bigtech' },
'middle-east': { id: 'middle-east', name: 'Middle East (UAE)', energyCostMultiplier: 0.5, latencyTier: 3, regulatoryStrictness: 0.1, politicalStability: 0.6, availableAt: 'bigtech' },
};
export const INITIAL_INFRASTRUCTURE: InfrastructureState = {
dataCenters: [],
gpuMarketPrices: {
consumer: 800,
t4: 5_000,
a100: 15_000,
h100: 35_000,
b200: 50_000,
custom: 80_000,
},
totalFlops: 0,
totalUptime: 1,
};
+73
View File
@@ -0,0 +1,73 @@
export interface MarketState {
consumers: ConsumerMarket;
enterprise: EnterpriseMarket;
overloadPolicy: OverloadPolicy;
openSourcedModels: string[];
}
export interface ConsumerMarket {
totalSubscribers: number;
churnRatePerTick: number;
growthRatePerTick: number;
satisfaction: number;
viralCoefficient: number;
}
export interface EnterpriseMarket {
activeContracts: EnterpriseContract[];
pendingRFPs: EnterpriseRFP[];
totalApiCallsPerTick: number;
averageTokensPerCall: number;
}
export interface EnterpriseContract {
id: string;
customerName: string;
segment: 'startup' | 'mid-market' | 'enterprise' | 'government';
tokensPerTick: number;
pricePerMToken: number;
slaUptime: number;
startTick: number;
durationTicks: number;
satisfaction: number;
}
export interface EnterpriseRFP {
id: string;
customerName: string;
segment: 'startup' | 'mid-market' | 'enterprise' | 'government';
requiredCapability: number;
offeredPricePerMToken: number;
requiredSlaUptime: number;
expiresAtTick: number;
}
export interface OverloadPolicy {
maxQueueDepth: number;
rateLimitPerCustomer: number;
degradeQualityUnderLoad: boolean;
prioritizeEnterprise: boolean;
}
export const INITIAL_MARKET: MarketState = {
consumers: {
totalSubscribers: 0,
churnRatePerTick: 0.001,
growthRatePerTick: 0,
satisfaction: 0.5,
viralCoefficient: 0,
},
enterprise: {
activeContracts: [],
pendingRFPs: [],
totalApiCallsPerTick: 0,
averageTokensPerCall: 500,
},
overloadPolicy: {
maxQueueDepth: 100,
rateLimitPerCustomer: 1000,
degradeQualityUnderLoad: false,
prioritizeEnterprise: true,
},
openSourcedModels: [],
};
+106
View File
@@ -0,0 +1,106 @@
export interface ModelsState {
trainedModels: TrainedModel[];
activeTraining: TrainingJob | null;
productLines: ProductLine[];
}
export interface TrainedModel {
id: string;
name: string;
generation: number;
parameterCount: number;
trainingDataSize: number;
capabilities: ModelCapabilities;
safetyScore: number;
benchmarkScore: number;
tuning: ModelTuning;
isDeployed: boolean;
trainedAtTick: number;
}
export interface ModelCapabilities {
reasoning: number;
coding: number;
creative: number;
multimodal: number;
agents: number;
speed: number;
}
export interface ModelTuning {
preset: TuningPreset;
verbosity?: number;
safetyLevel?: number;
creativity?: number;
speedQuality?: number;
refusalRate?: number;
}
export type TuningPreset = 'helpful-safe' | 'max-capability' | 'enterprise' | 'creative';
export interface TrainingJob {
modelName: string;
generation: number;
allocatedCompute: number;
allocatedDataTokens: number;
progressTicks: number;
totalTicks: number;
estimatedCapability: number;
}
export interface ProductLine {
id: string;
type: ProductLineType;
name: string;
modelId: string | null;
isActive: boolean;
pricing: ProductPricing;
}
export type ProductLineType = 'text-api' | 'chat-product' | 'image' | 'code' | 'agents';
export interface ProductPricing {
inputTokenPrice: number;
outputTokenPrice: number;
thinkingTokenBudget: number;
cachingEnabled: boolean;
subscriptionPrice: number;
freeTokenAllowance: number;
}
export const INITIAL_MODELS: ModelsState = {
trainedModels: [],
activeTraining: null,
productLines: [
{
id: 'text-api',
type: 'text-api',
name: 'Text API',
modelId: null,
isActive: false,
pricing: {
inputTokenPrice: 1.0,
outputTokenPrice: 3.0,
thinkingTokenBudget: 0,
cachingEnabled: false,
subscriptionPrice: 0,
freeTokenAllowance: 0,
},
},
{
id: 'chat-product',
type: 'chat-product',
name: 'Chat Product',
modelId: null,
isActive: false,
pricing: {
inputTokenPrice: 0,
outputTokenPrice: 0,
thinkingTokenBudget: 0,
cachingEnabled: false,
subscriptionPrice: 20,
freeTokenAllowance: 10_000,
},
},
],
};
+22
View File
@@ -0,0 +1,22 @@
export interface ReputationState {
score: number;
safetyRecord: number;
publicPerception: number;
employeeSatisfaction: number;
regulatoryStanding: number;
reputationHistory: ReputationSnapshot[];
}
export interface ReputationSnapshot {
tick: number;
score: number;
}
export const INITIAL_REPUTATION: ReputationState = {
score: 50,
safetyRecord: 50,
publicPerception: 50,
employeeSatisfaction: 70,
regulatoryStanding: 50,
reputationHistory: [],
};
+45
View File
@@ -0,0 +1,45 @@
import type { Era } from './gameState';
export interface ResearchState {
completedResearch: string[];
activeResearch: ActiveResearch | null;
researchPoints: number;
}
export interface ActiveResearch {
researchId: string;
progressTicks: number;
totalTicks: number;
allocatedResearchers: number;
allocatedCompute: number;
}
export interface ResearchNode {
id: string;
name: string;
description: string;
era: Era;
category: 'generation' | 'efficiency' | 'safety' | 'specialization' | 'infrastructure';
branch?: 'reasoning' | 'coding' | 'creative' | 'multimodal' | 'agents';
prerequisites: string[];
cost: {
researchPoints: number;
compute: number;
ticks: number;
};
effects: ResearchEffect[];
}
export interface ResearchEffect {
type: 'unlock_gpu' | 'unlock_model_tier' | 'efficiency_boost'
| 'capability_boost' | 'cost_reduction' | 'unlock_feature'
| 'unlock_product_line' | 'safety_boost';
target: string;
value: number;
}
export const INITIAL_RESEARCH: ResearchState = {
completedResearch: [],
activeResearch: null,
researchPoints: 0,
};
+49
View File
@@ -0,0 +1,49 @@
export interface TalentState {
departments: Record<DepartmentId, Department>;
keyHires: KeyHire[];
hiringPipeline: HiringCandidate[];
totalSalaryPerTick: number;
}
export type DepartmentId = 'research' | 'engineering' | 'operations' | 'sales';
export interface Department {
id: DepartmentId;
headcount: number;
budget: number;
effectiveness: number;
morale: number;
}
export interface KeyHire {
id: string;
name: string;
department: DepartmentId;
specialAbility: string;
effects: { type: string; value: number }[];
salary: number;
hiredAtTick: number;
loyalty: number;
}
export interface HiringCandidate {
id: string;
name: string;
department: DepartmentId;
quality: number;
salaryCost: number;
expiresAtTick: number;
isKeyHire: boolean;
}
export const INITIAL_TALENT: TalentState = {
departments: {
research: { id: 'research', headcount: 2, budget: 5_000, effectiveness: 0.5, morale: 0.8 },
engineering: { id: 'engineering', headcount: 3, budget: 7_000, effectiveness: 0.5, morale: 0.8 },
operations: { id: 'operations', headcount: 1, budget: 3_000, effectiveness: 0.5, morale: 0.8 },
sales: { id: 'sales', headcount: 0, budget: 0, effectiveness: 0, morale: 0.8 },
},
keyHires: [],
hiringPipeline: [],
totalSalaryPerTick: 0,
};
+33
View File
@@ -0,0 +1,33 @@
export function formatNumber(n: number): string {
if (n < 0) return `-${formatNumber(-n)}`;
if (n < 1_000) return Math.floor(n).toString();
if (n < 1_000_000) return `${(n / 1_000).toFixed(1)}K`;
if (n < 1_000_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n < 1_000_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
return `${(n / 1_000_000_000_000).toFixed(1)}T`;
}
export function formatMoney(n: number): string {
if (n < 0) return `-$${formatNumber(-n)}`;
return `$${formatNumber(n)}`;
}
export function formatPercent(n: number, decimals = 1): string {
return `${(n * 100).toFixed(decimals)}%`;
}
export function formatTokens(n: number): string {
return `${formatNumber(n)} tok`;
}
export function formatFlops(n: number): string {
return `${formatNumber(n)} FLOPS`;
}
export function formatDuration(ticks: number): string {
if (ticks < 60) return `${ticks}s`;
if (ticks < 3600) return `${Math.floor(ticks / 60)}m ${ticks % 60}s`;
const hours = Math.floor(ticks / 3600);
const minutes = Math.floor((ticks % 3600) / 60);
return `${hours}h ${minutes}m`;
}