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,230 @@
|
||||
import type { GameState, MarketState, BenchmarkResult, Competitor } from '@ai-tycoon/shared';
|
||||
import { CONSUMER_TOKENS_PER_SUBSCRIBER } from '@ai-tycoon/shared';
|
||||
import { BENCHMARKS } from '../../data/benchmarks';
|
||||
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';
|
||||
|
||||
export interface MarketTickResult {
|
||||
marketState: MarketState;
|
||||
apiRevenue: number;
|
||||
subscriptionRevenue: number;
|
||||
totalTokenDemand: number;
|
||||
}
|
||||
|
||||
function getSegmentQuality(
|
||||
segment: 'consumer' | 'enterprise' | 'developer' | 'research',
|
||||
benchmarkResults: BenchmarkResult[],
|
||||
fallbackScore: number,
|
||||
): number {
|
||||
if (benchmarkResults.length === 0) return fallbackScore / 100;
|
||||
|
||||
const bestByBenchmark = new Map<string, number>();
|
||||
for (const r of benchmarkResults) {
|
||||
const prev = bestByBenchmark.get(r.benchmarkId) ?? 0;
|
||||
if (r.score > prev) bestByBenchmark.set(r.benchmarkId, r.score);
|
||||
}
|
||||
|
||||
let weightedSum = 0;
|
||||
let totalWeight = 0;
|
||||
for (const bench of BENCHMARKS) {
|
||||
const score = bestByBenchmark.get(bench.id);
|
||||
if (score == null) continue;
|
||||
const weight = bench.marketRelevance[segment];
|
||||
weightedSum += (score / 100) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (totalWeight === 0) return fallbackScore / 100;
|
||||
return weightedSum / totalWeight;
|
||||
}
|
||||
|
||||
export function processMarketV2(state: GameState, currentTickCapacity: number): MarketTickResult {
|
||||
const consumerQuality = getSegmentQuality('consumer', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
||||
const enterpriseQuality = getSegmentQuality('enterprise', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
||||
const modelQuality = state.models.benchmarkResults.length > 0
|
||||
? (consumerQuality + enterpriseQuality) / 2
|
||||
: state.models.bestDeployedModelScore / 100;
|
||||
|
||||
// --- Seasonal ---
|
||||
const seasonal = computeSeasonal(state.meta.tickCount);
|
||||
|
||||
// --- Obsolescence ---
|
||||
const obsolescence = updateObsolescence(
|
||||
state.market.obsolescence,
|
||||
state.meta.currentEra,
|
||||
state.meta.tickCount,
|
||||
);
|
||||
|
||||
// --- Developer Ecosystem ---
|
||||
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,
|
||||
);
|
||||
|
||||
// --- TAM & Market Shares ---
|
||||
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;
|
||||
|
||||
// --- Consumer Tiers ---
|
||||
const consumerDemandEstimate = state.market.consumerTiers.totalUsers * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
||||
const demandCapacityRatio = currentTickCapacity > 0
|
||||
? consumerDemandEstimate / currentTickCapacity
|
||||
: consumerDemandEstimate > 0 ? 10 : 0;
|
||||
|
||||
const consumerResult = processConsumerTiers(
|
||||
state.market.consumerTiers,
|
||||
playerConsumerCustomers,
|
||||
modelQuality,
|
||||
seasonal.multipliers.consumer,
|
||||
demandCapacityRatio,
|
||||
state.infrastructure.networkLatencyPenalty,
|
||||
state.market.overloadPolicy,
|
||||
);
|
||||
|
||||
// --- API Tiers ---
|
||||
const apiResult = processApiTiers(
|
||||
state.market.apiTiers,
|
||||
playerDevCustomers,
|
||||
modelQuality,
|
||||
seasonal.multipliers.api,
|
||||
devEcosystem,
|
||||
);
|
||||
|
||||
// --- Product Lines ---
|
||||
const productResult = processProductLines(
|
||||
state.market.codeAssistant,
|
||||
state.market.agentsPlatform,
|
||||
state.models.benchmarkResults,
|
||||
playerDevCustomers,
|
||||
playerEntCustomers,
|
||||
seasonal.multipliers.consumer,
|
||||
seasonal.multipliers.enterprise,
|
||||
);
|
||||
|
||||
// --- Enterprise Pipeline ---
|
||||
const salesDept = state.talent.departments.sales;
|
||||
const salesHeadcount = salesDept.headcount;
|
||||
const salesEffectiveness = salesDept.effectiveness;
|
||||
|
||||
const enterpriseResult = processEnterprisePipeline(
|
||||
state.market.enterprise,
|
||||
state.reputation.score,
|
||||
state.models.bestDeployedModelScore,
|
||||
state.models.bestDeployedSafetyScore,
|
||||
salesHeadcount,
|
||||
salesEffectiveness,
|
||||
devEcosystem,
|
||||
seasonal.multipliers.enterprise,
|
||||
state.meta.tickCount,
|
||||
demandCapacityRatio,
|
||||
);
|
||||
|
||||
// --- Aggregate revenue ---
|
||||
const subscriptionRevenue = consumerResult.subscriptionRevenue
|
||||
+ productResult.codeAssistantRevenue
|
||||
+ productResult.agentsPlatformRevenue;
|
||||
|
||||
const apiRevenue = apiResult.apiRevenue
|
||||
+ enterpriseResult.contractRevenue
|
||||
- enterpriseResult.slaPenalties;
|
||||
|
||||
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;
|
||||
const adjustedApiRevenue = 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,
|
||||
subscriberHistory,
|
||||
},
|
||||
apiRevenue: Math.max(0, adjustedApiRevenue),
|
||||
subscriptionRevenue,
|
||||
totalTokenDemand,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
subscriberHistory,
|
||||
},
|
||||
apiRevenue: Math.max(0, apiRevenue),
|
||||
subscriptionRevenue,
|
||||
totalTokenDemand,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user