63e56dc229
Players could set astronomical prices and still retain subscribers because price elasticity floored at 10% for any price above $100, satisfaction ignored pricing entirely, and churn had no price component. Introduces perceived value per tier (model quality × reputation), replaces the broken linear formula with sigmoid decay, adds price-aware satisfaction blending, and applies per-tier price-based churn multipliers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
277 lines
10 KiB
TypeScript
277 lines
10 KiB
TypeScript
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<string, Partial<Record<keyof ModelCapabilities, number>>> = {
|
|
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,
|
|
};
|
|
}
|