Files
AIHostingTycoon/packages/game-engine/src/systems/market/index.ts
T
josh 63e56dc229
Balance Check / balance-simulation (push) Successful in 51s
Balance Check / multi-run-balance (push) Successful in 13m19s
CI / build-and-push (push) Successful in 45s
Fix consumer subscription pricing exploit with perceived-value-based elasticity
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>
2026-04-26 21:51:03 -04:00

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,
};
}