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