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,24 @@
import type { GameState, ComputeState, InfrastructureState } from '@ai-tycoon/shared';
export function processCompute(state: GameState, infrastructure: InfrastructureState): ComputeState {
const totalFlops = infrastructure.totalFlops;
const trainingAllocation = state.compute.trainingAllocation;
const inferenceAllocation = 1 - trainingAllocation;
const inferenceFlops = totalFlops * inferenceAllocation;
const tokensPerSecondCapacity = inferenceFlops * 10;
const tokensPerSecondDemand = state.compute.tokensPerSecondDemand;
const inferenceUtilization = tokensPerSecondCapacity > 0
? Math.min(1, tokensPerSecondDemand / tokensPerSecondCapacity)
: 0;
return {
totalFlops,
trainingAllocation,
inferenceAllocation,
inferenceUtilization,
tokensPerSecondCapacity,
tokensPerSecondDemand,
};
}
@@ -0,0 +1,45 @@
import type { GameState, EconomyState, InfrastructureState } from '@ai-tycoon/shared';
import { FINANCIAL_SNAPSHOT_INTERVAL, MAX_FINANCIAL_HISTORY } from '@ai-tycoon/shared';
import type { MarketTickResult } from './marketSystem';
export function processEconomy(
state: GameState,
market: MarketTickResult,
infrastructure: InfrastructureState,
): EconomyState {
const revenue = market.apiRevenue + market.subscriptionRevenue;
const infraExpenses = infrastructure.dataCenters.reduce((sum, dc) => {
return sum + dc.energyCostPerTick + dc.maintenanceCostPerTick;
}, 0);
const talentExpenses = state.talent.totalSalaryPerTick;
const dataExpenses = state.data.partnerships.reduce((sum, p) => sum + p.costPerTick, 0);
const expenses = infraExpenses + talentExpenses + dataExpenses;
const money = state.economy.money + revenue - expenses;
const financialHistory = [...state.economy.financialHistory];
if (state.meta.tickCount % FINANCIAL_SNAPSHOT_INTERVAL === 0) {
financialHistory.push({
tick: state.meta.tickCount,
money,
revenue,
expenses,
valuation: state.economy.funding.valuation,
});
if (financialHistory.length > MAX_FINANCIAL_HISTORY) {
financialHistory.shift();
}
}
return {
...state.economy,
money: Math.max(0, money),
totalRevenue: state.economy.totalRevenue + revenue,
totalExpenses: state.economy.totalExpenses + expenses,
revenuePerTick: revenue,
expensesPerTick: expenses,
financialHistory,
};
}
@@ -0,0 +1,72 @@
import type { GameState, InfrastructureState } from '@ai-tycoon/shared';
import {
GPU_CONFIGS,
LOCATION_CONFIGS,
GPU_PRICE_VOLATILITY,
GPU_FAILURE_RATE_BASE,
REDUNDANCY_FAILURE_REDUCTION,
BASE_ENERGY_COST_PER_FLOP,
BASE_MAINTENANCE_PER_GPU,
} from '@ai-tycoon/shared';
import type { GpuType } from '@ai-tycoon/shared';
export function processInfrastructure(state: GameState): InfrastructureState {
const dataCenters = state.infrastructure.dataCenters.map(dc => {
const location = LOCATION_CONFIGS[dc.location];
const gpus = dc.gpus.map(inv => {
const failureRate = GPU_FAILURE_RATE_BASE * (1 - dc.redundancyLevel * REDUNDANCY_FAILURE_REDUCTION);
let newFailed = inv.failedCount;
for (let i = 0; i < inv.healthyCount; i++) {
if (Math.random() < failureRate) newFailed++;
}
const healthyCount = Math.max(0, inv.count - newFailed);
return { ...inv, healthyCount, failedCount: newFailed };
});
let totalFlops = 0;
let totalPower = 0;
let totalGpuCount = 0;
for (const inv of gpus) {
const config = GPU_CONFIGS[inv.type];
totalFlops += inv.healthyCount * config.flopsPerUnit;
totalPower += inv.healthyCount * config.basePowerDraw;
totalGpuCount += inv.count;
}
const energyCostPerTick = totalPower * BASE_ENERGY_COST_PER_FLOP * location.energyCostMultiplier;
const maintenanceCostPerTick = totalGpuCount * BASE_MAINTENANCE_PER_GPU;
const currentUptime = totalGpuCount > 0
? gpus.reduce((s, inv) => s + inv.healthyCount, 0) / totalGpuCount
: 1;
return { ...dc, gpus, energyCostPerTick, maintenanceCostPerTick, currentUptime };
});
const gpuMarketPrices = { ...state.infrastructure.gpuMarketPrices };
for (const gpuType of Object.keys(gpuMarketPrices) as GpuType[]) {
const basePrice = GPU_CONFIGS[gpuType].basePrice;
const variation = (Math.random() - 0.5) * 2 * GPU_PRICE_VOLATILITY;
const currentPrice = gpuMarketPrices[gpuType];
const newPrice = currentPrice * (1 + variation);
gpuMarketPrices[gpuType] = Math.max(basePrice * 0.7, Math.min(basePrice * 1.5, newPrice));
}
let totalFlops = 0;
let totalUptime = 0;
let dcCount = 0;
for (const dc of dataCenters) {
for (const inv of dc.gpus) {
totalFlops += inv.healthyCount * GPU_CONFIGS[inv.type].flopsPerUnit;
}
totalUptime += dc.currentUptime;
dcCount++;
}
return {
dataCenters,
gpuMarketPrices,
totalFlops,
totalUptime: dcCount > 0 ? totalUptime / dcCount : 1,
};
}
@@ -0,0 +1,67 @@
import type { GameState, MarketState, ComputeState } from '@ai-tycoon/shared';
import {
CONSUMER_BASE_GROWTH,
CONSUMER_QUALITY_GROWTH_MULTIPLIER,
CONSUMER_BASE_CHURN,
API_TOKENS_PER_REQUEST,
} from '@ai-tycoon/shared';
export interface MarketTickResult {
marketState: MarketState;
apiRevenue: number;
subscriptionRevenue: number;
}
export function processMarket(state: GameState, compute: ComputeState): MarketTickResult {
const bestModel = state.models.trainedModels
.filter(m => m.isDeployed)
.sort((a, b) => b.benchmarkScore - a.benchmarkScore)[0];
const modelQuality = bestModel ? bestModel.benchmarkScore / 100 : 0;
const chatProduct = state.models.productLines.find(p => p.type === 'chat-product');
const textApi = state.models.productLines.find(p => p.type === 'text-api');
const consumers = { ...state.market.consumers };
if (chatProduct?.isActive && bestModel) {
const growthRate = CONSUMER_BASE_GROWTH + modelQuality * CONSUMER_QUALITY_GROWTH_MULTIPLIER;
const churnRate = CONSUMER_BASE_CHURN * (1 + (1 - consumers.satisfaction));
consumers.growthRatePerTick = growthRate;
consumers.churnRatePerTick = churnRate;
const newSubs = consumers.totalSubscribers * growthRate;
const lostSubs = consumers.totalSubscribers * churnRate;
consumers.totalSubscribers = Math.max(0, consumers.totalSubscribers + newSubs - lostSubs);
if (consumers.totalSubscribers < 10 && modelQuality > 0) {
consumers.totalSubscribers += 1;
}
consumers.satisfaction = Math.min(1, Math.max(0,
0.3 + modelQuality * 0.5 + (1 - compute.inferenceUtilization) * 0.2,
));
}
const subscriptionRevenue = chatProduct?.isActive
? consumers.totalSubscribers * (chatProduct.pricing.subscriptionPrice / 30 / 24 / 3600)
: 0;
const enterprise = { ...state.market.enterprise };
let apiRevenue = 0;
if (textApi?.isActive && bestModel) {
let totalTokens = 0;
for (const contract of enterprise.activeContracts) {
totalTokens += contract.tokensPerTick;
apiRevenue += (contract.tokensPerTick / 1_000_000) * contract.pricePerMToken;
}
enterprise.totalApiCallsPerTick = totalTokens / API_TOKENS_PER_REQUEST;
}
return {
marketState: {
...state.market,
consumers,
enterprise,
},
apiRevenue,
subscriptionRevenue,
};
}
@@ -0,0 +1,27 @@
import type { GameState, ReputationState } from '@ai-tycoon/shared';
import { MAX_REPUTATION_HISTORY } from '@ai-tycoon/shared';
export function processReputation(state: GameState): ReputationState {
const { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation;
const score = Math.round(
safetyRecord * 0.3 +
publicPerception * 0.3 +
employeeSatisfaction * 0.2 +
regulatoryStanding * 0.2,
);
const reputationHistory = [...state.reputation.reputationHistory];
if (state.meta.tickCount % 120 === 0) {
reputationHistory.push({ tick: state.meta.tickCount, score });
if (reputationHistory.length > MAX_REPUTATION_HISTORY) {
reputationHistory.shift();
}
}
return {
...state.reputation,
score,
reputationHistory,
};
}
@@ -0,0 +1,29 @@
import type { GameState, ResearchState, ComputeState } from '@ai-tycoon/shared';
export function processResearch(state: GameState, compute: ComputeState): ResearchState {
const active = state.research.activeResearch;
if (!active) return state.research;
const researcherBoost = state.talent.departments.research.headcount *
state.talent.departments.research.effectiveness;
const speedMultiplier = 1 + researcherBoost * 0.1;
const newProgress = active.progressTicks + speedMultiplier;
if (newProgress >= active.totalTicks) {
return {
...state.research,
completedResearch: [...state.research.completedResearch, active.researchId],
activeResearch: null,
researchPoints: state.research.researchPoints + 1,
};
}
return {
...state.research,
activeResearch: {
...active,
progressTicks: newProgress,
},
};
}