import type { GameState, MarketState, ModelCapabilities } from '@ai-tycoon/shared'; import { CONSUMER_TOKENS_PER_SUBSCRIBER, API_TOKENS_PER_DEVELOPER_PER_TICK, BATCH_API_DEMAND_PER_DEV, makeInitialServingMetrics } from '@ai-tycoon/shared'; import type { TrafficPriority, TierServingMetrics } from '@ai-tycoon/shared'; import { computeSeasonal } from './seasonalSystem'; import { updateObsolescence } from './obsolescenceSystem'; import { buildPlayerProfile, buildCompetitorProfile, computeMarketShares, updateTAMGrowth } from './tamSystem'; import { processConsumerTiers } from './consumerTierSystem'; import { processApiTiers } from './apiTierSystem'; import { processProductLines } from './productLines'; import { processDeveloperEcosystem } from './developerEcosystem'; import { processEnterprisePipeline } from './enterprisePipeline'; import { processServingPipeline } from './servingPipeline'; import type { DemandByTier } from './servingPipeline'; import type { ResearchBonuses } from '../researchBonuses'; export interface MarketTickResult { marketState: MarketState; apiRevenue: number; subscriptionRevenue: number; totalTokenDemand: number; } const SEGMENT_CAPABILITY_WEIGHTS: Record>> = { consumer: { creative: 0.35, knowledge: 0.25, reasoning: 0.15, multimodal: 0.15, coding: 0.05, agents: 0.05 }, enterprise: { reasoning: 0.25, coding: 0.20, agents: 0.20, knowledge: 0.15, math: 0.10, multimodal: 0.10 }, developer: { coding: 0.35, reasoning: 0.20, agents: 0.20, math: 0.15, knowledge: 0.10 }, research: { reasoning: 0.30, math: 0.30, knowledge: 0.20, coding: 0.10, agents: 0.10 }, }; function getSegmentQuality( segment: 'consumer' | 'enterprise' | 'developer' | 'research', capabilities: ModelCapabilities, fallbackScore: number, ): number { const weights = SEGMENT_CAPABILITY_WEIGHTS[segment]; if (!weights) return fallbackScore / 100; let weightedSum = 0; let totalWeight = 0; for (const [cap, weight] of Object.entries(weights)) { const score = capabilities[cap as keyof ModelCapabilities] ?? 0; if (score > 0) { weightedSum += (score / 100) * weight; totalWeight += weight; } } return totalWeight > 0 ? weightedSum / totalWeight : fallbackScore / 100; } export function processMarketV2( state: GameState, currentTickCapacity: number, effectiveInferenceFlops?: number, researchBonuses?: ResearchBonuses, ): MarketTickResult { const caps = state.models.bestDeployedCapabilities; const hasDeployed = state.models.bestDeployedModelScore > 0; const consumerQuality = getSegmentQuality('consumer', caps, state.models.bestDeployedModelScore); const enterpriseQuality = getSegmentQuality('enterprise', caps, state.models.bestDeployedModelScore); const modelQuality = hasDeployed ? (consumerQuality + enterpriseQuality) / 2 : state.models.bestDeployedModelScore / 100; const seasonal = computeSeasonal(state.meta.tickCount); const obsolescence = updateObsolescence( state.market.obsolescence, state.meta.currentEra, state.meta.tickCount, ); const freeApiDevs = state.market.apiTiers.tiers.free.developerCount; const totalApiDevs = state.market.apiTiers.totalDevelopers; const engineeringCount = state.talent.departments.engineering.headcount; const devEcosystem = processDeveloperEcosystem( state.market.developerEcosystem, state.market.openSourcedModels.length, freeApiDevs, totalApiDevs, engineeringCount, state.meta.currentEra, ); const chatProduct = state.models.productLines.find(p => p.type === 'chat-product'); const textApi = state.models.productLines.find(p => p.type === 'text-api'); const chatPrice = chatProduct?.pricing.subscriptionPrice ?? 20; const apiOutPrice = textApi?.pricing.outputTokenPrice ?? 3; const hasAnyFreeTier = state.market.consumerTiers.tiers.free.config.isActive || state.market.apiTiers.tiers.free.config.isActive; const playerProfile = buildPlayerProfile( modelQuality, chatPrice, apiOutPrice, state.reputation.score, devEcosystem, obsolescence, hasAnyFreeTier, ); const activeRivals = state.competitors.rivals.filter(r => r.status === 'active'); const competitorProfiles = activeRivals.map(buildCompetitorProfile); const allProfiles = [playerProfile, ...competitorProfiles]; let tam = updateTAMGrowth(state.market.tam, state.meta.currentEra); tam = computeMarketShares(tam, allProfiles, obsolescence.marketQualityBaseline); const playerConsumerCustomers = tam.segments.consumer.shares.find(s => s.playerId === 'player')?.customers ?? 0; const playerDevCustomers = tam.segments.developer.shares.find(s => s.playerId === 'player')?.customers ?? 0; const playerEntCustomers = tam.segments.enterprise.shares.find(s => s.playerId === 'player')?.customers ?? 0; // --- Product Lines (compute first to get token demand) --- const productResult = processProductLines( state.market.codeAssistant, state.market.agentsPlatform, caps, playerDevCustomers, playerEntCustomers, seasonal.multipliers.consumer, seasonal.multipliers.enterprise, ); // --- Pre-compute demand estimates by tier for serving pipeline --- const consumerTiers = state.market.consumerTiers; const apiTiers = state.market.apiTiers; const enterprise = state.market.enterprise; const consumerPaidTokens = (consumerTiers.tiers.plus.userCount + consumerTiers.tiers.pro.userCount + consumerTiers.tiers.team.userCount) * CONSUMER_TOKENS_PER_SUBSCRIBER; const consumerFreeTokens = consumerTiers.tiers.free.userCount * CONSUMER_TOKENS_PER_SUBSCRIBER; const apiPaidTokens = apiTiers.tiers.payg.developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK.payg + apiTiers.tiers.scale.developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK.scale + apiTiers.tiers['enterprise-api'].developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK['enterprise-api'] + productResult.codeAssistantTokenDemand; const apiFreeTokens = apiTiers.tiers.free.developerCount * API_TOKENS_PER_DEVELOPER_PER_TICK.free; let enterpriseTokens = 0; for (const contract of enterprise.activeContracts) { enterpriseTokens += contract.tokensPerTick; } enterpriseTokens += productResult.agentsPlatformTokenDemand; const demandByTier: DemandByTier = { 'enterprise': enterpriseTokens, 'api-paid': apiPaidTokens, 'consumer-paid': consumerPaidTokens, 'api-free': apiFreeTokens, 'consumer-free': consumerFreeTokens, }; // --- Batch API demand --- let batchDemand = 0; if (state.market.overloadPolicy.batchApiEnabled) { for (const id of ['free', 'payg', 'scale', 'enterprise-api'] as const) { batchDemand += apiTiers.tiers[id].developerCount * (BATCH_API_DEMAND_PER_DEV[id] ?? 0); } batchDemand *= Math.max(0.1, modelQuality); } const completedResearch = state.research?.completedResearch ?? []; // --- Serving Pipeline --- const servingResult = processServingPipeline({ modelsState: state.models, effectiveInferenceFlops: effectiveInferenceFlops ?? currentTickCapacity, overloadPolicy: state.market.overloadPolicy, demandByTier, batchApi: { ...state.market.batchApi, totalBatchDemand: batchDemand, }, modelQuality, researchUnlocks: { servingRoutingUnlocked: completedResearch.includes('request-routing'), priorityQueuesUnlocked: completedResearch.includes('priority-queues'), batchApiUnlocked: completedResearch.includes('request-batching'), autoScalingBonus: completedResearch.includes('auto-scaling') ? 0.2 : 0, }, }); const sm = servingResult.servingMetrics; // --- Consumer Tiers (now with serving metrics) --- const consumerResult = processConsumerTiers( state.market.consumerTiers, playerConsumerCustomers, modelQuality, state.reputation.score, seasonal.multipliers.consumer, state.infrastructure.networkLatencyPenalty, sm.tierMetrics['consumer-paid'], sm.tierMetrics['consumer-free'], ); // --- API Tiers (now with serving metrics) --- const apiResult = processApiTiers( state.market.apiTiers, playerDevCustomers, modelQuality, seasonal.multipliers.api, devEcosystem, sm.tierMetrics['api-paid'], sm.tierMetrics['api-free'], ); // --- Enterprise Pipeline (now with serving metrics) --- const salesDept = state.talent.departments.sales; const enterpriseResult = processEnterprisePipeline( state.market.enterprise, state.reputation.score, state.models.bestDeployedModelScore, state.models.bestDeployedSafetyScore, salesDept.headcount, salesDept.effectiveness, devEcosystem, seasonal.multipliers.enterprise, state.meta.tickCount, sm.tierMetrics['enterprise'], ); // --- Aggregate revenue --- const subscriptionRevenue = consumerResult.subscriptionRevenue + productResult.codeAssistantRevenue + productResult.agentsPlatformRevenue; let apiRevenue = apiResult.apiRevenue + enterpriseResult.contractRevenue - enterpriseResult.slaPenalties + servingResult.batchRevenue; const totalTokenDemand = consumerResult.totalConsumerTokenDemand + apiResult.totalApiTokenDemand + enterpriseResult.contractTokenDemand + productResult.codeAssistantTokenDemand + productResult.agentsPlatformTokenDemand; // --- Subscriber history --- const subscriberHistory = [...(state.market.subscriberHistory || [])]; if (state.meta.tickCount % 60 === 0) { subscriberHistory.push({ tick: state.meta.tickCount, subscribers: consumerResult.consumerTiers.totalUsers }); if (subscriberHistory.length > 500) subscriberHistory.shift(); } // --- Open source effects --- const openSourceCount = state.market.openSourcedModels.length; if (openSourceCount > 0) { const revenueReduction = openSourceCount * 0.10 * 0.3; apiRevenue = apiRevenue * (1 - revenueReduction); } return { marketState: { ...state.market, tam, consumerTiers: consumerResult.consumerTiers, apiTiers: apiResult.apiTiers, codeAssistant: productResult.codeAssistant, agentsPlatform: productResult.agentsPlatform, enterprise: enterpriseResult.enterprise, developerEcosystem: devEcosystem, seasonalPhase: seasonal.phase, seasonalMultiplier: seasonal.multipliers.consumer, obsolescence, servingMetrics: sm, batchApi: servingResult.batchApi, subscriberHistory, }, apiRevenue: Math.max(0, apiRevenue), subscriptionRevenue, totalTokenDemand, }; }