Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
CI / build-and-push (push) Successful in 42s
CI / build-and-push (push) Successful in 42s
Replaces the simplified single-subscriber market with a full competitive simulation: shared TAM with softmax market shares across 4 segments, multi-tier consumer subscriptions (Free/Plus/Pro/Team) and API tiers (Free/PAYG/Scale/Enterprise), enterprise sales pipeline (Lead→Qualification→POC→Negotiation→Active→Renewal) with SLA tracking, developer ecosystem flywheel, technology obsolescence pressure, seasonal demand cycles, and two new product lines (Code Assistant, AI Agents Platform). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import type {
|
||||
EnterpriseState,
|
||||
EnterpriseLead,
|
||||
EnterpriseContract,
|
||||
EnterpriseSegment,
|
||||
EnterprisePipelineStage,
|
||||
DeveloperEcosystem,
|
||||
} 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,
|
||||
} 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>): 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,
|
||||
demandCapacityRatio: number,
|
||||
): EnterprisePipelineResult {
|
||||
const pipeline = [...ent.pipeline];
|
||||
const activeContracts = [...ent.activeContracts];
|
||||
|
||||
const effectiveSales = salesHeadcount > 0
|
||||
? Math.min(1, salesHeadcount * salesEffectiveness / Math.max(1, pipeline.length))
|
||||
: 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<EnterprisePipelineStage, EnterprisePipelineStage | 'active'> = {
|
||||
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') {
|
||||
transitionProb *= modelCapability >= lead.requiredCapability ? 1 : 0.1;
|
||||
} else if (lead.stage === 'poc') {
|
||||
transitionProb *= Math.max(0.2, 1 - Math.max(0, demandCapacityRatio - 0.9) * 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++;
|
||||
|
||||
if (demandCapacityRatio <= (1 / updated.slaUptime)) {
|
||||
updated.uptimeTicks++;
|
||||
} else {
|
||||
updated.slaViolations++;
|
||||
const penalty = updated.pricePerMToken * (updated.tokensPerTick / 1_000_000) * SLA_PENALTY_FRACTION;
|
||||
slaPenalties += penalty;
|
||||
updated.slaPenaltiesPaid += penalty;
|
||||
updated.satisfaction = Math.max(0, updated.satisfaction - 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user