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
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@ai-tycoon/game-engine",
"private": true,
"version": "0.0.1",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@ai-tycoon/shared": "workspace:*"
},
"devDependencies": {
"@ai-tycoon/tsconfig": "workspace:*",
"typescript": "^5.8.0"
}
}
+82
View File
@@ -0,0 +1,82 @@
import type { GameState } from '@ai-tycoon/shared';
import { processTick } from './tick';
export interface GameEngineCallbacks {
getState: () => GameState;
setState: (partial: Partial<GameState>) => void;
onTick?: (tickCount: number) => void;
onEraChange?: (era: GameState['meta']['currentEra']) => void;
}
export class GameEngine {
private callbacks: GameEngineCallbacks;
private lastFrameTime = 0;
private accumulator = 0;
private animFrameId: number | null = null;
private tickIntervalMs = 1000;
constructor(callbacks: GameEngineCallbacks) {
this.callbacks = callbacks;
}
start(): void {
if (this.animFrameId !== null) return;
this.lastFrameTime = performance.now();
this.accumulator = 0;
this.loop(this.lastFrameTime);
}
stop(): void {
if (this.animFrameId !== null) {
cancelAnimationFrame(this.animFrameId);
this.animFrameId = null;
}
}
setSpeed(speed: number): void {
this.tickIntervalMs = 1000 / speed;
}
processOfflineTicks(missedTicks: number): { revenue: number; expenses: number; ticksProcessed: number } {
let totalRevenue = 0;
let totalExpenses = 0;
for (let i = 0; i < missedTicks; i++) {
const state = this.callbacks.getState();
const result = processTick(state);
this.callbacks.setState(result);
totalRevenue += result.economy?.revenuePerTick ?? 0;
totalExpenses += result.economy?.expensesPerTick ?? 0;
}
return { revenue: totalRevenue, expenses: totalExpenses, ticksProcessed: missedTicks };
}
private loop = (now: number): void => {
const delta = now - this.lastFrameTime;
this.lastFrameTime = now;
const state = this.callbacks.getState();
if (!state.meta.isPaused) {
this.accumulator += delta;
let ticksThisFrame = 0;
const maxTicksPerFrame = 10;
while (this.accumulator >= this.tickIntervalMs && ticksThisFrame < maxTicksPerFrame) {
const currentState = this.callbacks.getState();
const result = processTick(currentState);
this.callbacks.setState(result);
this.accumulator -= this.tickIntervalMs;
ticksThisFrame++;
this.callbacks.onTick?.(currentState.meta.tickCount + 1);
}
if (this.accumulator > this.tickIntervalMs * maxTicksPerFrame) {
this.accumulator = 0;
}
}
this.animFrameId = requestAnimationFrame(this.loop);
};
}
+2
View File
@@ -0,0 +1,2 @@
export { GameEngine } from './engine';
export { processTick } from './tick';
@@ -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,
},
};
}
+33
View File
@@ -0,0 +1,33 @@
import type { GameState } from '@ai-tycoon/shared';
import { processEconomy } from './systems/economySystem';
import { processInfrastructure } from './systems/infrastructureSystem';
import { processCompute } from './systems/computeSystem';
import { processResearch } from './systems/researchSystem';
import { processMarket } from './systems/marketSystem';
import { processReputation } from './systems/reputationSystem';
export function processTick(state: GameState): Partial<GameState> {
const infrastructure = processInfrastructure(state);
const compute = processCompute(state, infrastructure);
const research = processResearch(state, compute);
const market = processMarket(state, compute);
const reputation = processReputation(state);
const economy = processEconomy(state, market, infrastructure);
const tickCount = state.meta.tickCount + 1;
return {
meta: {
...state.meta,
tickCount,
lastTickTimestamp: Date.now(),
totalPlayTime: state.meta.totalPlayTime + 1,
},
economy,
infrastructure,
compute,
research,
market: market.marketState,
reputation,
};
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@ai-tycoon/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "@ai-tycoon/shared",
"private": true,
"version": "0.0.1",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@ai-tycoon/tsconfig": "workspace:*",
"typescript": "^5.8.0"
}
}
@@ -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`;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "@ai-tycoon/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./base.json",
"compilerOptions": {
"lib": ["ES2022"],
"module": "ESNext",
"outDir": "dist",
"rootDir": "src"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "@ai-tycoon/tsconfig",
"private": true,
"files": ["base.json", "react.json", "node.json"]
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "./base.json",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"noEmit": true
}
}