Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
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:
2026-04-25 08:30:24 -04:00
parent 4c1c0e9ff2
commit 09a5cb69a7
34 changed files with 2851 additions and 408 deletions
+45 -7
View File
@@ -1,11 +1,6 @@
import type { Competitor } from '@ai-tycoon/shared';
/**
* Initial rival AI companies that compete with the player from the start.
* Names are fictional parodies -- any resemblance to real companies is purely satirical.
*/
export const INITIAL_RIVALS: Competitor[] = [
// ── Safety-first lab (Anthropic parody) ──────────────────────────────
{
id: 'competitor_prometheus',
name: 'Prometheus AI',
@@ -26,9 +21,23 @@ export const INITIAL_RIVALS: Competitor[] = [
latestModelName: 'Aegis-1',
completedMilestones: [],
nextMilestoneAtTick: 300,
products: {
hasFreeTier: true,
chatPrice: 25,
apiInputPrice: 1.5,
apiOutputPrice: 4.0,
hasCodeAssistant: false,
codeAssistantPrice: 0,
hasAgentsPlatform: false,
agentsPlatformPrice: 0,
},
pricingStrategy: { aggressiveness: 0.2, premiumPositioning: 0.7 },
modelFreshness: 0.8,
lastModelReleaseTick: 0,
developerEcosystemScore: 25,
marketShares: { consumer: 0.15, developer: 0.20, enterprise: 0.10, government: 0.05 },
},
// ── Move-fast startup (xAI / Musk parody) ────────────────────────────
{
id: 'competitor_nexus',
name: 'Nexus Labs',
@@ -49,9 +58,23 @@ export const INITIAL_RIVALS: Competitor[] = [
latestModelName: 'Blitz-0.9',
completedMilestones: [],
nextMilestoneAtTick: 300,
products: {
hasFreeTier: true,
chatPrice: 15,
apiInputPrice: 0.8,
apiOutputPrice: 2.0,
hasCodeAssistant: false,
codeAssistantPrice: 0,
hasAgentsPlatform: false,
agentsPlatformPrice: 0,
},
pricingStrategy: { aggressiveness: 0.8, premiumPositioning: 0.2 },
modelFreshness: 0.7,
lastModelReleaseTick: 0,
developerEcosystemScore: 30,
marketShares: { consumer: 0.20, developer: 0.25, enterprise: 0.05, government: 0.02 },
},
// ── Big-tech giant (Google parody) ────────────────────────────────────
{
id: 'competitor_titan',
name: 'Titan Computing',
@@ -72,5 +95,20 @@ export const INITIAL_RIVALS: Competitor[] = [
latestModelName: 'Colossus 2.0',
completedMilestones: [],
nextMilestoneAtTick: 300,
products: {
hasFreeTier: true,
chatPrice: 20,
apiInputPrice: 1.0,
apiOutputPrice: 3.0,
hasCodeAssistant: false,
codeAssistantPrice: 0,
hasAgentsPlatform: false,
agentsPlatformPrice: 0,
},
pricingStrategy: { aggressiveness: 0.5, premiumPositioning: 0.5 },
modelFreshness: 0.9,
lastModelReleaseTick: 0,
developerEcosystemScore: 45,
marketShares: { consumer: 0.35, developer: 0.30, enterprise: 0.40, government: 0.50 },
},
];
@@ -0,0 +1,31 @@
import type { EnterpriseSegment } from '@ai-tycoon/shared';
export const ENTERPRISE_NAMES: Record<EnterpriseSegment, string[]> = {
startup: [
'PixelForge', 'NovaByte', 'CloudSpark', 'DataLeap', 'VectorVault',
'SynthWave AI', 'CodePilot', 'BrightLoop', 'Innova Labs', 'ScaleKit',
'DeepRoot', 'FluxPoint', 'MindBridge', 'HyperNode', 'NeuralSeed',
'LatticeAI', 'StreamForge', 'QuantumLeaf', 'VeloCity AI', 'ArcStack',
],
'mid-market': [
'Meridian Health', 'Cascade Logistics', 'PrimeRetail Group', 'Atlas Financial',
'TerraForm Solutions', 'NorthStar Analytics', 'BridgePoint Media', 'Keystone Legal',
'Orion Pharmaceuticals', 'Summit Education', 'ClearView Insurance', 'Horizon Dynamics',
'PeakWare Systems', 'CorePath Consulting', 'BlueStar Manufacturing', 'Vertex Commerce',
'RiverStone Banking', 'Zenith Telecom', 'IronGate Security', 'ElmBridge Tech',
],
enterprise: [
'GlobalBank Corp', 'TransContinental Airlines', 'MegaRetail Holdings', 'United Telecom',
'Pinnacle Pharmaceuticals', 'Imperial Energy', 'Vanguard Insurance', 'Sterling Automotive',
'Apex Media Group', 'Continental Healthcare', 'Sovereign Financial', 'Atlas Industries',
'Dominion Logistics', 'Pacific Semiconductor', 'NorthAm Rail Systems', 'OceanGate Maritime',
'Crown Broadcasting', 'Titan Manufacturing', 'Citadel Consulting', 'Granite Resources',
],
government: [
'Dept. of National Intelligence', 'Federal Research Authority', 'National Health Service AI',
'Defense Advanced Projects', 'Central Tax Administration', 'State Education Bureau',
'Environmental Protection AI', 'National Cybersecurity Center', 'Federal Aviation Systems',
'Dept. of Energy Computing', 'National Weather Service', 'Census Analytics Division',
'Judicial Analytics Office', 'Immigration Processing AI', 'Veterans Affairs Digital',
],
};
+54
View File
@@ -379,6 +379,60 @@ export const TECH_TREE: ResearchNode[] = [
],
},
// === MARKET / PRODUCTS ===
{
id: 'code-assistant-product',
name: 'Code Assistant Product',
description: 'Launch an AI code assistant product for developers. Requires Code Generation research.',
era: 'scaleup',
category: 'specialization',
branch: 'coding',
prerequisites: ['code-generation'],
cost: { researchPoints: 2, compute: 20, ticks: 150 },
effects: [{ type: 'unlock_product_line', target: 'code-assistant', value: 1 }],
},
{
id: 'developer-relations',
name: 'Developer Relations',
description: 'Invest in developer community building. Unlocks dev-rel budget allocation and boosts API adoption.',
era: 'startup',
category: 'efficiency',
prerequisites: [],
cost: { researchPoints: 0, compute: 3, ticks: 45 },
effects: [{ type: 'unlock_feature', target: 'developer-relations', value: 1 }],
},
{
id: 'enterprise-sales',
name: 'Enterprise Sales',
description: 'Build a formal enterprise sales pipeline. Unlocks enterprise lead generation and contract management.',
era: 'startup',
category: 'efficiency',
prerequisites: [],
cost: { researchPoints: 0, compute: 3, ticks: 45 },
effects: [{ type: 'unlock_feature', target: 'enterprise-sales', value: 1 }],
},
{
id: 'sdk-platform',
name: 'SDK Platform',
description: 'Comprehensive SDK and tooling platform. Significantly boosts developer ecosystem growth.',
era: 'scaleup',
category: 'efficiency',
prerequisites: ['developer-relations'],
cost: { researchPoints: 2, compute: 15, ticks: 120 },
effects: [{ type: 'efficiency_boost', target: 'sdk_coverage', value: 0.3 }],
},
{
id: 'agents-platform-product',
name: 'Agents Platform Product',
description: 'Launch an enterprise AI agents platform. Requires Agentic Architecture research.',
era: 'bigtech',
category: 'specialization',
branch: 'agents',
prerequisites: ['agentic-architecture'],
cost: { researchPoints: 4, compute: 60, ticks: 300 },
effects: [{ type: 'unlock_product_line', target: 'agents-platform', value: 1 }],
},
// === DATA ===
{
id: 'data-pipeline',
@@ -1,44 +1,99 @@
import type { GameState, CompetitorState } from '@ai-tycoon/shared';
import type { GameState, CompetitorState, Competitor } from '@ai-tycoon/shared';
import {
COMPETITOR_PRODUCT_THRESHOLDS,
COMPETITOR_CATCHUP_SHARE_THRESHOLD,
COMPETITOR_CATCHUP_PRICE_CUT,
FRESHNESS_DECAY_RATE,
} from '@ai-tycoon/shared';
function updateCompetitorProducts(rival: Competitor): Competitor['products'] {
const cap = rival.estimatedCapability;
const p = rival.products;
const pricing = rival.pricingStrategy;
const baseChatPrice = 20 * (1 + pricing.premiumPositioning * 0.5 - pricing.aggressiveness * 0.3);
const baseApiOut = 3.0 * (1 + pricing.premiumPositioning * 0.3 - pricing.aggressiveness * 0.4);
return {
hasFreeTier: cap >= COMPETITOR_PRODUCT_THRESHOLDS.freeTierAndChat,
chatPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.freeTierAndChat
? Math.max(5, baseChatPrice) : p.chatPrice,
apiInputPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
? Math.max(0.2, baseApiOut * 0.33) : p.apiInputPrice,
apiOutputPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
? Math.max(0.5, baseApiOut) : p.apiOutputPrice,
hasCodeAssistant: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant,
codeAssistantPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
? Math.max(10, 20 * (1 - pricing.aggressiveness * 0.3)) : 0,
hasAgentsPlatform: cap >= COMPETITOR_PRODUCT_THRESHOLDS.agentsPlatform,
agentsPlatformPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.agentsPlatform
? Math.max(50, 100 * (1 - pricing.aggressiveness * 0.2)) : 0,
};
}
export function processCompetitors(state: GameState): CompetitorState {
const tick = state.meta.tickCount;
const rivals = state.competitors.rivals.map(rival => {
if (rival.status !== 'active') return rival;
if (tick < rival.nextMilestoneAtTick) return rival;
const updated = { ...rival };
// Freshness decay each tick
updated.modelFreshness = Math.max(0, updated.modelFreshness - FRESHNESS_DECAY_RATE);
// Developer ecosystem growth based on personality
const ecoGrowth = rival.personality.openSourceTendency * 0.1 + rival.personality.marketingFocus * 0.05;
updated.developerEcosystemScore = Math.min(100,
updated.developerEcosystemScore + ecoGrowth * 0.01,
);
// Catch-up: if any market share < threshold, cut prices
const minShare = Math.min(...Object.values(updated.marketShares));
if (minShare < COMPETITOR_CATCHUP_SHARE_THRESHOLD) {
updated.pricingStrategy = {
...updated.pricingStrategy,
aggressiveness: Math.min(1, updated.pricingStrategy.aggressiveness + COMPETITOR_CATCHUP_PRICE_CUT * 0.1),
};
}
if (tick < rival.nextMilestoneAtTick) {
updated.products = updateCompetitorProducts(updated);
return updated;
}
// Milestone reached — capability jump + model release
const { personality } = rival;
const capGrowth = (2 + personality.researchFocus * 5 + personality.riskTolerance * 3) *
(1 + tick * 0.00005);
const revenueGrowth = rival.estimatedRevenue * (0.02 + personality.marketingFocus * 0.03);
const userGrowth = rival.estimatedUsers * (0.01 + personality.marketingFocus * 0.02);
const newCapability = Math.min(95, rival.estimatedCapability + capGrowth);
const newRevenue = rival.estimatedRevenue + revenueGrowth + 50;
const newUsers = rival.estimatedUsers + userGrowth + 100;
updated.estimatedCapability = Math.min(95, rival.estimatedCapability + capGrowth);
updated.estimatedRevenue = rival.estimatedRevenue + revenueGrowth + 50;
updated.estimatedUsers = Math.floor(rival.estimatedUsers + userGrowth + 100);
const repChange = personality.safetyFocus > 0.6
? 1
: personality.riskTolerance > 0.7 ? -1 : 0;
updated.reputation = Math.min(100, Math.max(0, rival.reputation + repChange));
const modelNames = [
'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',
'Nova', 'Quantum', 'Nexus', 'Apex', 'Zenith',
];
const modelIdx = Math.floor(newCapability / 10);
const latestModelName = `${rival.name.split(' ')[0]}-${modelNames[Math.min(modelIdx, modelNames.length - 1)]}`;
const modelIdx = Math.floor(updated.estimatedCapability / 10);
updated.latestModelName = `${rival.name.split(' ')[0]}-${modelNames[Math.min(modelIdx, modelNames.length - 1)]}`;
// Model release resets freshness
updated.modelFreshness = 1.0;
updated.lastModelReleaseTick = tick;
updated.products = updateCompetitorProducts(updated);
const milestoneInterval = 200 + Math.floor(Math.random() * 200);
updated.nextMilestoneAtTick = tick + milestoneInterval;
return {
...rival,
estimatedCapability: newCapability,
estimatedRevenue: newRevenue,
estimatedUsers: Math.floor(newUsers),
reputation: Math.min(100, Math.max(0, rival.reputation + repChange)),
latestModelName,
nextMilestoneAtTick: tick + milestoneInterval,
};
return updated;
});
const allCaps = [
@@ -1,7 +1,7 @@
import type { GameState, DataState } from '@ai-tycoon/shared';
export function processData(state: GameState): DataState {
const subscribers = state.market.consumers.totalSubscribers;
const subscribers = state.market.consumerTiers.totalUsers;
const userDataRate = subscribers * 0.5;
const partnershipTokens = state.data.partnerships.reduce((sum, p) => sum + p.tokensPerTick, 0);
@@ -26,7 +26,8 @@ export function processEconomy(
const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra);
const complianceCost = bestCapability > 30 ? bestCapability * REGULATION_COMPLIANCE_PER_CAPABILITY * (1 + eraIdx * 0.5) / 100 : 0;
const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost + extraCosts;
const devRelExpenses = state.market.developerEcosystem.devRelSpending;
const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost + devRelExpenses + extraCosts;
const money = state.economy.money + revenue - expenses;
@@ -22,7 +22,7 @@ export function canRaiseFunding(state: GameState): { canRaise: boolean; nextRoun
if (reqs.minRevenue && state.economy.totalRevenue < reqs.minRevenue) {
return { canRaise: false, nextRound, reason: `Need $${reqs.minRevenue.toLocaleString()} total revenue` };
}
if (reqs.minUsers && state.market.consumers.totalSubscribers < reqs.minUsers) {
if (reqs.minUsers && state.market.consumerTiers.totalUsers < reqs.minUsers) {
return { canRaise: false, nextRound, reason: `Need ${reqs.minUsers.toLocaleString()} subscribers` };
}
if (reqs.minReputation && state.reputation.score < reqs.minReputation) {
@@ -34,7 +34,7 @@ export function canRaiseFunding(state: GameState): { canRaise: boolean; nextRoun
export function computeValuation(state: GameState): number {
const revenueMultiple = state.economy.revenuePerTick * 86400 * 365;
const subscriberValue = state.market.consumers.totalSubscribers * 500;
const subscriberValue = state.market.consumerTiers.totalUsers * 500;
const capabilityValue = Math.pow(state.models.bestDeployedModelScore, 2) * 1000;
return Math.max(100_000, revenueMultiple * 10 + subscriberValue + capabilityValue);
}
@@ -0,0 +1,97 @@
import type { ApiTierState, ApiTierId, DeveloperEcosystem } from '@ai-tycoon/shared';
import {
API_TIER_ORDER,
API_CONVERSION_RATES,
API_TIER_CHURN_RATES,
API_TOKENS_PER_DEVELOPER_PER_TICK,
} from '@ai-tycoon/shared';
export interface ApiTickResult {
apiTiers: ApiTierState;
apiRevenue: number;
totalApiTokenDemand: number;
}
export function processApiTiers(
tiers: ApiTierState,
playerDevCustomers: number,
modelQuality: number,
seasonalApiMultiplier: number,
ecosystem: DeveloperEcosystem,
): ApiTickResult {
const updated: ApiTierState = {
tiers: { ...tiers.tiers },
totalDevelopers: 0,
totalTokensPerTick: 0,
};
for (const id of API_TIER_ORDER) {
updated.tiers[id] = { ...tiers.tiers[id], config: { ...tiers.tiers[id].config } };
}
if (modelQuality <= 0) {
return { apiTiers: updated, apiRevenue: 0, totalApiTokenDemand: 0 };
}
const targetFreeDevelopers = playerDevCustomers * 0.1 * seasonalApiMultiplier;
const freeGrowth = (targetFreeDevelopers - updated.tiers.free.developerCount) * 0.03;
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount + freeGrowth);
const freeChurn = updated.tiers.free.developerCount * API_TIER_CHURN_RATES.free;
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount - freeChurn);
updated.tiers.free.churnRate = API_TIER_CHURN_RATES.free;
const prevTierMap: Record<ApiTierId, ApiTierId | null> = {
free: null,
payg: 'free',
scale: 'payg',
'enterprise-api': 'scale',
};
for (const id of API_TIER_ORDER) {
if (id === 'free') continue;
const tier = updated.tiers[id];
if (!tier.config.isActive) continue;
const prevId = prevTierMap[id];
if (!prevId) continue;
const prevTier = updated.tiers[prevId];
const convKey = `${prevId}->${id}`;
const baseRate = API_CONVERSION_RATES[convKey] ?? 0;
const ecosystemBoost = 1 + ecosystem.ecosystemScore / 200;
const convRate = baseRate * Math.max(0.1, modelQuality) * ecosystemBoost * seasonalApiMultiplier;
const converting = prevTier.developerCount * convRate;
prevTier.developerCount = Math.max(0, prevTier.developerCount - converting);
tier.developerCount += converting;
tier.churnRate = API_TIER_CHURN_RATES[id];
const churned = tier.developerCount * tier.churnRate;
tier.developerCount = Math.max(0, tier.developerCount - churned);
}
let totalDevelopers = 0;
let totalTokens = 0;
let apiRevenue = 0;
for (const id of API_TIER_ORDER) {
const tier = updated.tiers[id];
totalDevelopers += tier.developerCount;
const tokensPerDev = API_TOKENS_PER_DEVELOPER_PER_TICK[id];
tier.tokensPerTick = tier.developerCount * tokensPerDev;
totalTokens += tier.tokensPerTick;
apiRevenue += tier.developerCount * (tier.config.monthlyFee / 86400);
apiRevenue += (tier.tokensPerTick / 1_000_000) * tier.config.outputTokenPrice;
}
updated.totalDevelopers = totalDevelopers;
updated.totalTokensPerTick = totalTokens;
return {
apiTiers: updated,
apiRevenue: Math.max(0, apiRevenue),
totalApiTokenDemand: totalTokens,
};
}
@@ -0,0 +1,127 @@
import type { ConsumerTierState, ConsumerTierId } from '@ai-tycoon/shared';
import {
CONSUMER_TIER_ORDER,
CONVERSION_RATES,
TIER_CHURN_RATES,
FREE_TIER_ADOPTION_RATE,
CONSUMER_TOKENS_PER_SUBSCRIBER,
OVERLOAD_PENALTY_EXPONENT,
NETWORK_DEGRADATION,
} from '@ai-tycoon/shared';
export interface ConsumerTickResult {
consumerTiers: ConsumerTierState;
subscriptionRevenue: number;
totalConsumerTokenDemand: number;
}
export function processConsumerTiers(
tiers: ConsumerTierState,
playerConsumerCustomers: number,
modelQuality: number,
seasonalConsumerMultiplier: number,
demandCapacityRatio: number,
networkLatencyPenalty: number,
overloadPolicy: { degradeQualityUnderLoad: boolean; prioritizeEnterprise: boolean },
): ConsumerTickResult {
const updated = {
tiers: { ...tiers.tiers },
totalUsers: 0,
satisfaction: tiers.satisfaction,
viralCoefficient: tiers.viralCoefficient,
};
for (const id of CONSUMER_TIER_ORDER) {
updated.tiers[id] = { ...tiers.tiers[id], config: { ...tiers.tiers[id].config } };
}
if (modelQuality <= 0) {
return { consumerTiers: updated, subscriptionRevenue: 0, totalConsumerTokenDemand: 0 };
}
const qualityFactor = Math.max(0.1, modelQuality);
const freeAdoption = playerConsumerCustomers * FREE_TIER_ADOPTION_RATE * seasonalConsumerMultiplier;
const targetFreeUsers = Math.max(updated.tiers.free.userCount, freeAdoption);
const freeGrowth = (targetFreeUsers - updated.tiers.free.userCount) * 0.05;
updated.tiers.free.userCount = Math.max(0, updated.tiers.free.userCount + freeGrowth);
updated.tiers.free.churnRate = TIER_CHURN_RATES.free;
const freeChurn = updated.tiers.free.userCount * TIER_CHURN_RATES.free;
updated.tiers.free.userCount = Math.max(0, updated.tiers.free.userCount - freeChurn);
const prevTierMap: Record<ConsumerTierId, ConsumerTierId | null> = {
free: null,
plus: 'free',
pro: 'plus',
team: 'pro',
};
for (const id of CONSUMER_TIER_ORDER) {
if (id === 'free') continue;
const tier = updated.tiers[id];
if (!tier.config.isActive) continue;
if (modelQuality * 100 < tier.config.requiredModelQuality) continue;
const prevId = prevTierMap[id];
if (!prevId) continue;
const prevTier = updated.tiers[prevId];
const conversionKey = `${prevId}->${id}`;
const baseRate = CONVERSION_RATES[conversionKey] ?? 0;
const priceAttr = tier.config.price > 0
? Math.max(0.1, 1 - tier.config.price / 100)
: 1;
const convRate = baseRate * qualityFactor * priceAttr * seasonalConsumerMultiplier;
const converting = prevTier.userCount * convRate;
prevTier.userCount = Math.max(0, prevTier.userCount - converting);
tier.userCount += converting;
tier.conversionRateFromBelow = convRate;
tier.churnRate = TIER_CHURN_RATES[id];
const churnMultiplier = 1 + (1 - updated.satisfaction) * 2;
const churned = tier.userCount * tier.churnRate * churnMultiplier;
tier.userCount = Math.max(0, tier.userCount - churned);
}
let totalUsers = 0;
let subscriptionRevenue = 0;
let totalTokenDemand = 0;
for (const id of CONSUMER_TIER_ORDER) {
const tier = updated.tiers[id];
totalUsers += tier.userCount;
subscriptionRevenue += tier.userCount * (tier.config.price / 86400);
totalTokenDemand += tier.userCount * CONSUMER_TOKENS_PER_SUBSCRIBER;
}
updated.totalUsers = totalUsers;
let headroomBonus = 0;
let overloadPenalty = 0;
if (demandCapacityRatio <= 1) {
headroomBonus = (1 - demandCapacityRatio) * 0.2;
} else {
overloadPenalty = Math.min(1, Math.pow(demandCapacityRatio - 1, OVERLOAD_PENALTY_EXPONENT));
}
const netLatencyPenalty = networkLatencyPenalty * NETWORK_DEGRADATION.satisfactionPenaltyPerLatency;
updated.satisfaction = Math.min(1, Math.max(0,
0.3 + modelQuality * 0.5 + headroomBonus - overloadPenalty - netLatencyPenalty,
));
if (overloadPolicy.degradeQualityUnderLoad && demandCapacityRatio > 0.85) {
updated.satisfaction = Math.max(0, updated.satisfaction - 0.02);
}
if (overloadPolicy.prioritizeEnterprise && demandCapacityRatio > 0.9) {
updated.satisfaction = Math.max(0, updated.satisfaction - 0.01);
}
updated.viralCoefficient = modelQuality > 0.5 ? 1 + (modelQuality - 0.5) * 2 : 0;
return {
consumerTiers: updated,
subscriptionRevenue,
totalConsumerTokenDemand: totalTokenDemand,
};
}
@@ -0,0 +1,69 @@
import type { DeveloperEcosystem } from '@ai-tycoon/shared';
import {
BASE_DEV_GROWTH,
FREE_TIER_DEV_MULTIPLIER,
OPEN_SOURCE_DEV_BOOST,
DEV_REL_EFFECTIVENESS,
SDK_GROWTH_BONUS,
DEV_ECOSYSTEM_WEIGHTS,
STARTUP_ADOPTION_PER_DEV,
ENTERPRISE_REFERRAL_PER_STARTUP,
TAM_BASE_SIZES,
} from '@ai-tycoon/shared';
import type { Era } from '@ai-tycoon/shared';
export function processDeveloperEcosystem(
eco: DeveloperEcosystem,
openSourceCount: number,
apiFreeTierDevs: number,
apiTotalDevs: number,
engineeringHeadcount: number,
era: Era,
): DeveloperEcosystem {
const updated = { ...eco };
const growthRate =
BASE_DEV_GROWTH +
apiFreeTierDevs * FREE_TIER_DEV_MULTIPLIER +
openSourceCount * OPEN_SOURCE_DEV_BOOST +
updated.devRelSpending * DEV_REL_EFFECTIVENESS +
updated.sdkCoverage * SDK_GROWTH_BONUS;
updated.communityGrowthRate = growthRate;
updated.communitySize = Math.max(0, updated.communitySize + updated.communitySize * growthRate);
if (updated.communitySize < 10 && apiTotalDevs > 0) {
updated.communitySize += 1 + apiTotalDevs * 0.1;
}
updated.activeDevelopers = apiTotalDevs;
updated.openSourceContributions = openSourceCount;
const sdkTarget = Math.min(1, engineeringHeadcount / 50);
updated.sdkCoverage += (sdkTarget - updated.sdkCoverage) * 0.005;
updated.sdkCoverage = Math.min(1, Math.max(0, updated.sdkCoverage));
const docTarget = Math.min(1, updated.devRelSpending / 500);
updated.documentationQuality += (docTarget - updated.documentationQuality) * 0.003;
updated.documentationQuality = Math.min(1, Math.max(0, updated.documentationQuality));
const eraCap = TAM_BASE_SIZES[era].developer;
const communityNorm = Math.min(1, updated.communitySize / Math.max(1, eraCap * 0.1));
const activeRatio = updated.communitySize > 0
? Math.min(1, updated.activeDevelopers / updated.communitySize)
: 0;
const osNorm = Math.min(1, openSourceCount / 5);
updated.ecosystemScore = (
DEV_ECOSYSTEM_WEIGHTS.communitySize * communityNorm +
DEV_ECOSYSTEM_WEIGHTS.activeRatio * activeRatio +
DEV_ECOSYSTEM_WEIGHTS.sdkCoverage * updated.sdkCoverage +
DEV_ECOSYSTEM_WEIGHTS.docQuality * updated.documentationQuality +
DEV_ECOSYSTEM_WEIGHTS.openSource * osNorm
) * 100;
updated.startupsAdopted = Math.floor(updated.activeDevelopers * STARTUP_ADOPTION_PER_DEV);
updated.enterpriseReferrals = Math.floor(updated.startupsAdopted * ENTERPRISE_REFERRAL_PER_STARTUP);
return updated;
}
@@ -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,
};
}
@@ -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,
};
}
@@ -0,0 +1,39 @@
import type { ObsolescenceState, Era } from '@ai-tycoon/shared';
import {
OBSOLESCENCE_BASELINE_GROWTH,
OBSOLESCENCE_ERA_ACCELERATOR,
FRESHNESS_DECAY_RATE,
NEW_MODEL_BOOST_TICKS,
} from '@ai-tycoon/shared';
export function updateObsolescence(
obs: ObsolescenceState,
era: Era,
currentTick: number,
): ObsolescenceState {
const accelerator = OBSOLESCENCE_ERA_ACCELERATOR[era];
const newBaseline = obs.marketQualityBaseline + OBSOLESCENCE_BASELINE_GROWTH * accelerator;
const ticksSinceRelease = currentTick - obs.lastModelReleaseTick;
const freshness = obs.lastModelReleaseTick > 0
? Math.max(0, 1 - ticksSinceRelease * FRESHNESS_DECAY_RATE)
: 0;
const boostRemaining = Math.max(0, obs.newModelBoostRemaining - 1);
return {
...obs,
marketQualityBaseline: newBaseline,
playerModelFreshness: freshness,
newModelBoostRemaining: boostRemaining,
};
}
export function onModelDeployed(obs: ObsolescenceState, tick: number): ObsolescenceState {
return {
...obs,
playerModelFreshness: 1.0,
lastModelReleaseTick: tick,
newModelBoostRemaining: NEW_MODEL_BOOST_TICKS,
};
}
@@ -0,0 +1,100 @@
import type { CodeAssistantState, AgentsPlatformState, BenchmarkResult } from '@ai-tycoon/shared';
import {
CODE_ASSISTANT_MIN_CODING_SCORE,
CODE_ASSISTANT_BASE_ADOPTION_RATE,
CODE_ASSISTANT_CHURN_RATE,
AGENTS_PLATFORM_MIN_AGENTS_SCORE,
AGENTS_PLATFORM_BASE_ADOPTION_RATE,
AGENTS_PLATFORM_CHURN_RATE,
} from '@ai-tycoon/shared';
import { BENCHMARKS } from '../../data/benchmarks';
function getBenchmarkScore(benchmarkId: string, results: BenchmarkResult[]): number {
let best = 0;
for (const r of results) {
if (r.benchmarkId === benchmarkId && r.score > best) best = r.score;
}
return best;
}
function getCodingScore(results: BenchmarkResult[]): number {
const codeBench = BENCHMARKS.find(b => b.id === 'codeforce');
if (!codeBench) return 0;
return getBenchmarkScore(codeBench.id, results);
}
function getAgentsScore(results: BenchmarkResult[]): number {
const agentBench = BENCHMARKS.find(b => b.id === 'agentarena');
if (!agentBench) return 0;
return getBenchmarkScore(agentBench.id, results);
}
export interface ProductLineResult {
codeAssistant: CodeAssistantState;
agentsPlatform: AgentsPlatformState;
codeAssistantRevenue: number;
agentsPlatformRevenue: number;
codeAssistantTokenDemand: number;
agentsPlatformTokenDemand: number;
}
export function processProductLines(
ca: CodeAssistantState,
ap: AgentsPlatformState,
benchmarkResults: BenchmarkResult[],
playerDevCustomers: number,
playerEntCustomers: number,
seasonalConsumerMult: number,
seasonalEntMult: number,
): ProductLineResult {
const updatedCA = { ...ca };
const updatedAP = { ...ap };
let caRevenue = 0;
let apRevenue = 0;
// --- Code Assistant ---
updatedCA.qualityScore = getCodingScore(benchmarkResults);
if (updatedCA.isUnlocked && updatedCA.isActive && updatedCA.qualityScore >= CODE_ASSISTANT_MIN_CODING_SCORE) {
const qualityFactor = updatedCA.qualityScore / 100;
const priceAttr = Math.max(0.1, 1 - updatedCA.pricePerSeat / 50);
const targetSeats = playerDevCustomers * 0.05 * qualityFactor;
const growth = CODE_ASSISTANT_BASE_ADOPTION_RATE * qualityFactor * priceAttr * seasonalConsumerMult;
const churn = CODE_ASSISTANT_CHURN_RATE * (1 + (1 - qualityFactor) * 2);
updatedCA.seats = Math.max(0, updatedCA.seats + updatedCA.seats * growth - updatedCA.seats * churn);
if (updatedCA.seats < 10 && targetSeats > 10) {
updatedCA.seats += targetSeats * 0.01;
}
updatedCA.satisfaction = Math.min(1, 0.3 + qualityFactor * 0.5);
caRevenue = updatedCA.seats * (updatedCA.pricePerSeat / 86400);
}
// --- Agents Platform ---
updatedAP.qualityScore = getAgentsScore(benchmarkResults);
if (updatedAP.isUnlocked && updatedAP.isActive && updatedAP.qualityScore >= AGENTS_PLATFORM_MIN_AGENTS_SCORE) {
const qualityFactor = updatedAP.qualityScore / 100;
const priceAttr = Math.max(0.1, 1 - updatedAP.pricePerSeat / 250);
const targetSeats = playerEntCustomers * 0.02 * qualityFactor;
const growth = AGENTS_PLATFORM_BASE_ADOPTION_RATE * qualityFactor * priceAttr * seasonalEntMult;
const churn = AGENTS_PLATFORM_CHURN_RATE * (1 + (1 - qualityFactor) * 2);
updatedAP.seats = Math.max(0, updatedAP.seats + updatedAP.seats * growth - updatedAP.seats * churn);
if (updatedAP.seats < 5 && targetSeats > 5) {
updatedAP.seats += targetSeats * 0.01;
}
updatedAP.satisfaction = Math.min(1, 0.3 + qualityFactor * 0.5);
apRevenue = updatedAP.seats * (updatedAP.pricePerSeat / 86400);
}
const caTokenDemand = updatedCA.seats * 2;
const apTokenDemand = updatedAP.seats * 10;
return {
codeAssistant: updatedCA,
agentsPlatform: updatedAP,
codeAssistantRevenue: caRevenue,
agentsPlatformRevenue: apRevenue,
codeAssistantTokenDemand: caTokenDemand,
agentsPlatformTokenDemand: apTokenDemand,
};
}
@@ -0,0 +1,25 @@
import type { SeasonalPhase } from '@ai-tycoon/shared';
import { SEASONAL_CYCLE_TICKS, SEASONAL_MULTIPLIERS } from '@ai-tycoon/shared';
export interface SeasonalResult {
phase: SeasonalPhase;
multipliers: { consumer: number; api: number; enterprise: number };
}
const PHASES: SeasonalPhase[] = ['q1', 'q2', 'q3', 'q4'];
export function computeSeasonal(tickCount: number): SeasonalResult {
const positionInCycle = tickCount % SEASONAL_CYCLE_TICKS;
const quarterLength = SEASONAL_CYCLE_TICKS / 4;
const phaseIndex = Math.min(3, Math.floor(positionInCycle / quarterLength));
const phase = PHASES[phaseIndex];
const raw = SEASONAL_MULTIPLIERS[phase];
return {
phase,
multipliers: {
consumer: raw.consumer,
api: raw.api,
enterprise: raw.enterprise,
},
};
}
@@ -0,0 +1,190 @@
import type {
TotalAddressableMarket,
TAMSegmentId,
MarketShareEntry,
Competitor,
Era,
ObsolescenceState,
DeveloperEcosystem,
} from '@ai-tycoon/shared';
import {
TAM_BASE_SIZES,
TAM_GROWTH_PER_TICK,
SHARE_TEMPERATURE,
SHARE_MIGRATION_SPEED,
ATTRACTIVENESS_WEIGHTS,
OBSOLESCENCE_PENALTY_WEIGHT,
NEW_MODEL_BOOST_VALUE,
} from '@ai-tycoon/shared';
export interface ParticipantProfile {
id: string;
qualityScore: number;
priceScore: number;
reputation: number;
ecosystemScore: number;
freshness: number;
hasFreeTier: boolean;
}
export function buildPlayerProfile(
qualityScore: number,
chatPrice: number,
apiOutputPrice: number,
reputation: number,
ecosystem: DeveloperEcosystem,
obsolescence: ObsolescenceState,
hasFreeTier: boolean,
): ParticipantProfile {
const avgPrice = (chatPrice + apiOutputPrice) / 2;
const priceScore = Math.max(0, Math.min(1, 1 - avgPrice / 100));
let freshness = obsolescence.playerModelFreshness;
if (obsolescence.newModelBoostRemaining > 0) {
freshness = Math.min(1, freshness + NEW_MODEL_BOOST_VALUE);
}
return {
id: 'player',
qualityScore: Math.min(1, qualityScore),
priceScore,
reputation,
ecosystemScore: ecosystem.ecosystemScore,
freshness,
hasFreeTier,
};
}
export function buildCompetitorProfile(c: Competitor): ParticipantProfile {
const avgPrice = (c.products.chatPrice + c.products.apiOutputPrice) / 2;
const priceScore = Math.max(0, Math.min(1, 1 - avgPrice / 100));
return {
id: c.id,
qualityScore: Math.min(1, c.estimatedCapability / 100),
priceScore,
reputation: c.reputation,
ecosystemScore: c.developerEcosystemScore,
freshness: c.modelFreshness,
hasFreeTier: c.products.hasFreeTier,
};
}
function computeAttractiveness(
p: ParticipantProfile,
segment: TAMSegmentId,
qualityBaseline: number,
): number {
const w = ATTRACTIVENESS_WEIGHTS[segment];
let score =
w.quality * p.qualityScore +
w.price * p.priceScore +
w.reputation * (p.reputation / 100) +
w.ecosystem * (p.ecosystemScore / 100) +
w.freshness * p.freshness +
w.freeTier * (p.hasFreeTier ? 1 : 0);
const qualityGap = qualityBaseline / 100 - p.qualityScore;
if (qualityGap > 0) {
score -= qualityGap * OBSOLESCENCE_PENALTY_WEIGHT;
}
return Math.max(0.01, score);
}
function softmaxShares(scores: number[]): number[] {
const maxScore = Math.max(...scores);
const exps = scores.map(s => Math.exp((s - maxScore) * SHARE_TEMPERATURE));
const sumExp = exps.reduce((a, b) => a + b, 0);
return exps.map(e => e / sumExp);
}
export function computeMarketShares(
tam: TotalAddressableMarket,
participants: ParticipantProfile[],
qualityBaseline: number,
): TotalAddressableMarket {
const segments = { ...tam.segments };
const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government'];
for (const segId of segmentIds) {
const seg = segments[segId];
const scores = participants.map(p => computeAttractiveness(p, segId, qualityBaseline));
const targetShares = softmaxShares(scores);
const oldShareMap = new Map<string, MarketShareEntry>();
for (const entry of seg.shares) {
oldShareMap.set(entry.playerId, entry);
}
const newShares: MarketShareEntry[] = participants.map((p, i) => {
const old = oldShareMap.get(p.id);
const oldShare = old?.sharePercent ?? 0;
const migratedShare = oldShare + (targetShares[i] - oldShare) * SHARE_MIGRATION_SPEED;
return {
playerId: p.id,
sharePercent: migratedShare,
customers: Math.floor(migratedShare * seg.totalSize),
attractivenessScore: scores[i],
};
});
const totalShare = newShares.reduce((s, e) => s + e.sharePercent, 0);
if (totalShare > 0) {
for (const entry of newShares) {
entry.sharePercent /= totalShare;
entry.customers = Math.floor(entry.sharePercent * seg.totalSize);
}
}
segments[segId] = { ...seg, shares: newShares };
}
return { segments };
}
export function updateTAMGrowth(tam: TotalAddressableMarket, era: Era): TotalAddressableMarket {
const baseSizes = TAM_BASE_SIZES[era];
const segments = { ...tam.segments };
const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government'];
for (const segId of segmentIds) {
const seg = segments[segId];
const base = baseSizes[segId];
const grown = seg.totalSize + seg.totalSize * TAM_GROWTH_PER_TICK;
segments[segId] = {
...seg,
totalSize: Math.max(base, grown),
};
}
return { segments };
}
export function initializeTAM(era: Era, competitors: Competitor[]): TotalAddressableMarket {
const baseSizes = TAM_BASE_SIZES[era];
const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government'];
const segments = {} as Record<TAMSegmentId, { totalSize: number; shares: MarketShareEntry[] }>;
for (const segId of segmentIds) {
const shares: MarketShareEntry[] = [
{ playerId: 'player', sharePercent: 0.05, customers: 0, attractivenessScore: 0 },
...competitors.map(c => ({
playerId: c.id,
sharePercent: c.marketShares[segId] ?? 0.1,
customers: 0,
attractivenessScore: 0,
})),
];
const totalShare = shares.reduce((s, e) => s + e.sharePercent, 0);
for (const entry of shares) {
entry.sharePercent /= totalShare;
entry.customers = Math.floor(entry.sharePercent * baseSizes[segId]);
}
segments[segId] = { totalSize: baseSizes[segId], shares };
}
return { segments };
}
@@ -1,194 +1,8 @@
import type { GameState, MarketState, BenchmarkResult } from '@ai-tycoon/shared';
import {
CONSUMER_BASE_GROWTH,
CONSUMER_QUALITY_GROWTH_MULTIPLIER,
CONSUMER_BASE_CHURN,
CONSUMER_TOKENS_PER_SUBSCRIBER,
API_TOKENS_PER_REQUEST,
OPEN_SOURCE_REVENUE_PENALTY,
OPEN_SOURCE_TALENT_ATTRACTION,
MARKET_SIZE_CAP,
NETWORK_DEGRADATION,
MARKET_CAP_QUALITY_BONUS,
MARKET_CAP_REPUTATION_BONUS,
OVERLOAD_PENALTY_EXPONENT,
} from '@ai-tycoon/shared';
import { BENCHMARKS } from '../data/benchmarks';
import type { GameState } from '@ai-tycoon/shared';
import { processMarketV2 } from './market/index';
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 processMarket(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;
const chatProduct = state.models.productLines.find(p => p.type === 'chat-product');
const textApi = state.models.productLines.find(p => p.type === 'text-api');
// --- Consumer market (subscription product) ---
const consumers = { ...state.market.consumers };
let subscriptionRevenue = 0;
if (chatProduct?.isActive && modelQuality > 0) {
const price = chatProduct.pricing.subscriptionPrice;
const fairPrice = 20 + modelQuality * 80;
const priceRatio = price / Math.max(1, fairPrice);
const priceAttractiveness = Math.max(0, Math.min(1, 1 - (priceRatio - 1) * 0.8));
// --- Logistic growth with era-based market cap ---
const eraCapBase = MARKET_SIZE_CAP[state.meta.currentEra] ?? 100_000_000;
const effectiveCap = eraCapBase
* (1 + modelQuality * MARKET_CAP_QUALITY_BONUS)
* (1 + (state.reputation.score / 100) * MARKET_CAP_REPUTATION_BONUS);
const saturationFactor = Math.max(0, 1 - consumers.totalSubscribers / effectiveCap);
const growthRate = (CONSUMER_BASE_GROWTH + modelQuality * CONSUMER_QUALITY_GROWTH_MULTIPLIER)
* priceAttractiveness * saturationFactor;
const priceChurnMultiplier = priceRatio > 1 ? 1 + (priceRatio - 1) * 3 : 1;
const churnRate = CONSUMER_BASE_CHURN * (1 + (1 - consumers.satisfaction) * 2) * priceChurnMultiplier;
consumers.growthRatePerTick = growthRate;
consumers.churnRatePerTick = churnRate;
const newSubs = consumers.totalSubscribers * growthRate;
const lostSubs = consumers.totalSubscribers * churnRate;
consumers.totalSubscribers = Math.max(0, Math.min(
effectiveCap,
consumers.totalSubscribers + newSubs - lostSubs,
));
if (consumers.totalSubscribers < 100 && modelQuality > 0.1 && priceRatio < 3) {
consumers.totalSubscribers += 5 + modelQuality * 20;
}
// --- Satisfaction from demand/capacity ratio (current tick) ---
const consumerDemand = consumers.totalSubscribers * CONSUMER_TOKENS_PER_SUBSCRIBER;
let demandCapacityRatio: number;
if (currentTickCapacity > 0) {
demandCapacityRatio = consumerDemand / currentTickCapacity;
} else {
demandCapacityRatio = consumerDemand > 0 ? 10 : 0;
}
let headroomBonus = 0;
let overloadPenalty = 0;
if (demandCapacityRatio <= 1) {
headroomBonus = (1 - demandCapacityRatio) * 0.2;
} else {
overloadPenalty = Math.min(1, Math.pow(demandCapacityRatio - 1, OVERLOAD_PENALTY_EXPONENT));
}
const networkLatencyPenalty = state.infrastructure.networkLatencyPenalty *
NETWORK_DEGRADATION.satisfactionPenaltyPerLatency;
consumers.satisfaction = Math.min(1, Math.max(0,
0.3 + modelQuality * 0.5 + headroomBonus - overloadPenalty - networkLatencyPenalty,
));
consumers.viralCoefficient = modelQuality > 0.5 ? 1 + (modelQuality - 0.5) * 2 : 0;
subscriptionRevenue = consumers.totalSubscribers * (chatProduct.pricing.subscriptionPrice / 86400);
// --- Overload policy ---
const policy = state.market.overloadPolicy;
if (policy.degradeQualityUnderLoad && demandCapacityRatio > 0.85) {
consumers.satisfaction = Math.max(0, consumers.satisfaction - 0.02);
}
if (policy.prioritizeEnterprise && demandCapacityRatio > 0.9) {
consumers.satisfaction = Math.max(0, consumers.satisfaction - 0.01);
}
}
// --- B2B API market ---
const enterprise = { ...state.market.enterprise };
let apiRevenue = 0;
let organicApiTokens = 0;
if (textApi?.isActive && modelQuality > 0) {
const reputationFactor = state.reputation.score / 100;
const qualityFactor = modelQuality;
const priceFactor = Math.max(0.1, 1 - (textApi.pricing.outputTokenPrice / 20));
organicApiTokens = Math.floor(
qualityFactor * reputationFactor * priceFactor * 500 * (1 + state.meta.tickCount * 0.0001),
);
let contractTokens = 0;
for (const contract of enterprise.activeContracts) {
contractTokens += contract.tokensPerTick;
apiRevenue += (contract.tokensPerTick / 1_000_000) * contract.pricePerMToken;
}
apiRevenue += (organicApiTokens / 1_000_000) * textApi.pricing.outputTokenPrice;
enterprise.totalApiCallsPerTick = (organicApiTokens + contractTokens) / API_TOKENS_PER_REQUEST;
}
const totalTokenDemand = organicApiTokens +
consumers.totalSubscribers * CONSUMER_TOKENS_PER_SUBSCRIBER +
enterprise.activeContracts.reduce((s, c) => s + c.tokensPerTick, 0);
// --- Open source effects ---
const openSourceCount = state.market.openSourcedModels.length;
if (openSourceCount > 0) {
const growthBoost = 1 + openSourceCount * OPEN_SOURCE_TALENT_ATTRACTION;
consumers.totalSubscribers *= growthBoost > 1 ? 1 + (growthBoost - 1) * 0.01 : 1;
apiRevenue *= 1 - openSourceCount * OPEN_SOURCE_REVENUE_PENALTY * 0.3;
const eraCapBase = MARKET_SIZE_CAP[state.meta.currentEra] ?? 100_000_000;
const effectiveCap = eraCapBase
* (1 + modelQuality * MARKET_CAP_QUALITY_BONUS)
* (1 + (state.reputation.score / 100) * MARKET_CAP_REPUTATION_BONUS);
consumers.totalSubscribers = Math.min(effectiveCap, consumers.totalSubscribers);
}
const subscriberHistory = [...(state.market.subscriberHistory || [])];
if (state.meta.tickCount % 60 === 0) {
subscriberHistory.push({ tick: state.meta.tickCount, subscribers: consumers.totalSubscribers });
if (subscriberHistory.length > 500) subscriberHistory.shift();
}
return {
marketState: {
...state.market,
consumers,
enterprise,
subscriberHistory,
},
apiRevenue: Math.max(0, apiRevenue),
subscriptionRevenue,
totalTokenDemand,
};
export type { MarketTickResult } from './market/index';
export function processMarket(state: GameState, currentTickCapacity: number) {
return processMarketV2(state, currentTickCapacity);
}