import type { EnterpriseState, EnterpriseLead, EnterpriseContract, EnterpriseSegment, EnterprisePipelineStage, DeveloperEcosystem, TierServingMetrics, } from '@ai-tycoon/shared'; import { BASE_LEAD_RATE, LEAD_EXPIRY_TICKS, PIPELINE_STAGE_TIMEOUTS, PIPELINE_TRANSITION_RATES, SLA_PENALTY_FRACTION, CONTRACT_DURATION_BY_SEGMENT, ENTERPRISE_DEAL_VALUES, ENTERPRISE_SLA_REQUIREMENTS, ENTERPRISE_CAPABILITY_REQUIREMENTS, ENTERPRISE_TOKENS_PER_TICK, ENTERPRISE_REJECTION_SLA_MULTIPLIER, } from '@ai-tycoon/shared'; import { ENTERPRISE_NAMES } from '../../data/enterpriseNames'; let leadIdCounter = 0; function generateLeadId(): string { return `lead_${Date.now()}_${++leadIdCounter}`; } function randomInRange(min: number, max: number): number { return min + Math.random() * (max - min); } function pickSegment(reputation: number): EnterpriseSegment { const roll = Math.random(); if (reputation > 70 && roll < 0.15) return 'government'; if (reputation > 50 && roll < 0.35) return 'enterprise'; if (roll < 0.6) return 'mid-market'; return 'startup'; } function pickCompanyName(segment: EnterpriseSegment, existingNames: Set): string { const pool = ENTERPRISE_NAMES[segment]; const available = pool.filter(n => !existingNames.has(n)); if (available.length === 0) return `${segment}-client-${Math.floor(Math.random() * 9999)}`; return available[Math.floor(Math.random() * available.length)]; } export interface EnterprisePipelineResult { enterprise: EnterpriseState; contractRevenue: number; slaPenalties: number; contractTokenDemand: number; } export function processEnterprisePipeline( ent: EnterpriseState, reputation: number, modelCapability: number, safetyScore: number, salesHeadcount: number, salesEffectiveness: number, devEcosystem: DeveloperEcosystem, seasonalEntMultiplier: number, currentTick: number, enterpriseServingMetrics: TierServingMetrics, ): EnterprisePipelineResult { const pipeline = [...ent.pipeline]; const activeContracts = [...ent.activeContracts]; const effectiveSales = salesHeadcount > 0 ? Math.min(2, salesHeadcount * salesEffectiveness * 0.2) : 0; // --- Lead generation --- const leadRate = BASE_LEAD_RATE * (1 + reputation / 100) * (1 + devEcosystem.startupsAdopted * 0.001) * (effectiveSales > 0 ? effectiveSales : 0.1) * seasonalEntMultiplier; if (Math.random() < leadRate && pipeline.length < 20) { const existingNames = new Set([ ...pipeline.map(l => l.companyName), ...activeContracts.map(c => c.customerName), ]); const segment = pickSegment(reputation); const vals = ENTERPRISE_DEAL_VALUES[segment]; const toks = ENTERPRISE_TOKENS_PER_TICK[segment]; pipeline.push({ id: generateLeadId(), companyName: pickCompanyName(segment, existingNames), segment, stage: 'lead', enteredStageAtTick: currentTick, dealValue: randomInRange(vals.min, vals.max), tokensPerTick: randomInRange(toks.min, toks.max), requiredCapability: ENTERPRISE_CAPABILITY_REQUIREMENTS[segment], requiredSlaUptime: ENTERPRISE_SLA_REQUIREMENTS[segment], requiredSafetyScore: segment === 'government' ? 60 : 30, winProbability: 0.5, expiresAtTick: currentTick + LEAD_EXPIRY_TICKS, }); } // --- Pipeline progression --- const stageOrder: EnterprisePipelineStage[] = ['lead', 'qualification', 'poc', 'negotiation']; const nextStageMap: Record = { lead: 'qualification', qualification: 'poc', poc: 'negotiation', negotiation: 'active', }; const survivingLeads: EnterpriseLead[] = []; const newContracts: EnterpriseContract[] = []; for (const lead of pipeline) { if (currentTick > lead.expiresAtTick) continue; const timeout = PIPELINE_STAGE_TIMEOUTS[lead.stage]; if (currentTick - lead.enteredStageAtTick > timeout) continue; const transKey = `${lead.stage}->${nextStageMap[lead.stage]}`; const baseRate = PIPELINE_TRANSITION_RATES[transKey] ?? 0; let transitionProb = baseRate * effectiveSales; if (lead.stage === 'qualification') { const capRatio = Math.min(2, modelCapability / Math.max(1, lead.requiredCapability)); transitionProb *= capRatio > 1 ? capRatio : capRatio * 0.3; } else if (lead.stage === 'poc') { const entDemand = enterpriseServingMetrics.demandTokens; const entRejected = enterpriseServingMetrics.rejectedTokens; const rejectRate = entDemand > 0 ? entRejected / entDemand : 0; transitionProb *= Math.max(0.2, 1 - rejectRate * 5); } else if (lead.stage === 'negotiation') { transitionProb *= Math.max(0.3, 1 - (lead.dealValue / 10_000_000) * 0.5); } if (lead.stage === 'qualification' && safetyScore < lead.requiredSafetyScore) { transitionProb *= 0.2; } if (Math.random() < transitionProb) { const nextStage = nextStageMap[lead.stage]; if (nextStage === 'active') { const duration = CONTRACT_DURATION_BY_SEGMENT[lead.segment]; const pricePerMToken = (lead.dealValue / duration) / (lead.tokensPerTick / 1_000_000); newContracts.push({ id: lead.id, customerName: lead.companyName, segment: lead.segment, tokensPerTick: lead.tokensPerTick, pricePerMToken: Math.max(0.1, pricePerMToken), slaUptime: lead.requiredSlaUptime, startTick: currentTick, durationTicks: duration, satisfaction: 0.7, renewalProbability: 0.5, slaViolations: 0, slaPenaltiesPaid: 0, uptimeTicks: 0, totalTicks: 0, }); } else { survivingLeads.push({ ...lead, stage: nextStage, enteredStageAtTick: currentTick, }); } } else { survivingLeads.push(lead); } } // --- Active contracts: SLA, satisfaction, renewal --- let contractRevenue = 0; let slaPenalties = 0; let contractTokenDemand = 0; const survivingContracts: EnterpriseContract[] = []; for (const contract of [...activeContracts, ...newContracts]) { const updated = { ...contract }; updated.totalTicks++; const entDemand = enterpriseServingMetrics.demandTokens; const entServed = enterpriseServingMetrics.servedTokens; const entRejected = enterpriseServingMetrics.rejectedTokens; const servedFraction = entDemand > 0 ? entServed / entDemand : 1; const wasRejected = entRejected > 0; const qualityMet = enterpriseServingMetrics.avgQualityDelivered >= 0.85; if (servedFraction >= updated.slaUptime && qualityMet && !wasRejected) { updated.uptimeTicks++; } else { updated.slaViolations++; const severityMultiplier = wasRejected ? ENTERPRISE_REJECTION_SLA_MULTIPLIER : 1.0; const penalty = updated.pricePerMToken * (updated.tokensPerTick / 1_000_000) * SLA_PENALTY_FRACTION * severityMultiplier; slaPenalties += penalty; updated.slaPenaltiesPaid += penalty; updated.satisfaction = Math.max(0, updated.satisfaction - (wasRejected ? 0.01 : 0.005)); } if (updated.totalTicks > 0 && updated.slaViolations === 0) { updated.satisfaction = Math.min(1, updated.satisfaction + 0.001); } const tickRevenue = (updated.tokensPerTick / 1_000_000) * updated.pricePerMToken; contractRevenue += tickRevenue; contractTokenDemand += updated.tokensPerTick; if (currentTick >= updated.startTick + updated.durationTicks) { const renewalProb = updated.satisfaction * 0.6 + 0.3 - (updated.slaViolations * 0.01); if (Math.random() < Math.max(0, renewalProb)) { updated.startTick = currentTick; updated.totalTicks = 0; updated.uptimeTicks = 0; updated.slaViolations = 0; updated.slaPenaltiesPaid = 0; survivingContracts.push(updated); } } else { survivingContracts.push(updated); } } return { enterprise: { ...ent, pipeline: survivingLeads, activeContracts: survivingContracts, totalApiCallsPerTick: contractTokenDemand / ent.averageTokensPerCall, leadGenerationRate: leadRate, }, contractRevenue, slaPenalties, contractTokenDemand, }; }