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
@@ -90,9 +90,9 @@ function triggerMarketBoom(multiplier: number) {
useGameStore.setState((s) => ({
market: {
...s.market,
consumers: {
...s.market.consumers,
totalSubscribers: Math.round(s.market.consumers.totalSubscribers * multiplier),
consumerTiers: {
...s.market.consumerTiers,
totalUsers: Math.round(s.market.consumerTiers.totalUsers * multiplier),
},
},
}));
@@ -75,10 +75,9 @@ export function StateInspectionTab() {
</Section>
<Section title="Market">
<Stat label="Subscribers" value={formatNumber(market.consumers.totalSubscribers)} />
<Stat label="Satisfaction" value={formatPercent(market.consumers.satisfaction)} />
<Stat label="Growth/tick" value={market.consumers.growthRatePerTick.toFixed(4)} />
<Stat label="Churn/tick" value={market.consumers.churnRatePerTick.toFixed(4)} />
<Stat label="Subscribers" value={formatNumber(market.consumerTiers.totalUsers)} />
<Stat label="Satisfaction" value={formatPercent(market.consumerTiers.satisfaction)} />
<Stat label="Viral Coeff" value={market.consumerTiers.viralCoefficient.toFixed(4)} />
<Stat label="Contracts" value={market.enterprise.activeContracts.length} />
<Stat label="API tok/tick" value={formatNumber(market.enterprise.totalApiCallsPerTick)} />
</Section>
@@ -12,7 +12,7 @@ export function CompanyStatsCard({ onClose }: { onClose: () => void }) {
const money = useGameStore((s) => s.economy.money);
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
const valuation = useGameStore((s) => s.economy.funding.valuation);
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
const subscribers = useGameStore((s) => s.market.consumerTiers.totalUsers);
const models = useGameStore((s) => s.models.baseModels.length);
const bestModel = useGameStore((s) => s.models.bestDeployedModelScore);
const reputation = useGameStore((s) => s.reputation.score);
+2 -2
View File
@@ -15,7 +15,7 @@ export function DashboardPage() {
const totalDCs = useGameStore((s) => s.infrastructure.totalDataCenterCount);
const baseModels = useGameStore((s) => s.models.baseModels);
const activePipelines = useGameStore((s) => s.models.activeTrainingPipelines);
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
const subscribers = useGameStore((s) => s.market.consumerTiers.totalUsers);
const reputation = useGameStore((s) => s.reputation.score);
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
const financialHistory = useGameStore((s) => s.economy.financialHistory);
@@ -75,7 +75,7 @@ export function DashboardPage() {
icon={Users}
label="Subscribers"
value={formatNumber(subscribers)}
subValue={`Satisfaction: ${formatPercent(useGameStore.getState().market.consumers.satisfaction)}`}
subValue={`Satisfaction: ${formatPercent(useGameStore.getState().market.consumerTiers.satisfaction)}`}
color="text-orange-400"
onClick={() => useGameStore.getState().setActivePage('market')}
/>
+1 -1
View File
@@ -16,7 +16,7 @@ export function FinancePage() {
const talent = useGameStore((s) => s.talent);
const raiseFunding = useGameStore((s) => s.raiseFunding);
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
const subscribers = useGameStore((s) => s.market.consumerTiers.totalUsers);
const reputationScore = useGameStore((s) => s.reputation.score);
const state = useGameStore.getState();
+70 -147
View File
@@ -2,10 +2,27 @@ import { useState, useEffect, useRef, useCallback } from 'react';
import { useGameStore } from '@/store';
import {
formatNumber, formatMoney, formatPercent,
MARKET_SIZE_CAP, MARKET_CAP_QUALITY_BONUS, MARKET_CAP_REPUTATION_BONUS,
} from '@ai-tycoon/shared';
import { Users, Zap, Shield, TrendingUp, Settings2, Check } from 'lucide-react';
import { Users, Zap, Shield, Settings2, Check } from 'lucide-react';
import { TutorialHint } from '@/components/game/TutorialHint';
import { MarketOverviewPanel } from './market/MarketOverviewPanel';
import { ConsumerTiersPanel } from './market/ConsumerTiersPanel';
import { ApiTiersPanel } from './market/ApiTiersPanel';
import { EnterprisePipelinePanel } from './market/EnterprisePipelinePanel';
import { DeveloperEcosystemPanel } from './market/DeveloperEcosystemPanel';
import { ProductLinesPanel } from './market/ProductLinesPanel';
type MarketTab = 'overview' | 'consumer' | 'api' | 'enterprise' | 'ecosystem' | 'products' | 'settings';
const TABS: { id: MarketTab; label: string }[] = [
{ id: 'overview', label: 'Overview' },
{ id: 'consumer', label: 'Consumer' },
{ id: 'api', label: 'API' },
{ id: 'enterprise', label: 'Enterprise' },
{ id: 'ecosystem', label: 'Dev Ecosystem' },
{ id: 'products', label: 'Products' },
{ id: 'settings', label: 'Settings' },
];
function useAppliedFeedback() {
const [show, setShow] = useState(false);
@@ -28,67 +45,21 @@ function AppliedBadge({ visible }: { visible: boolean }) {
);
}
export function MarketPage() {
const consumers = useGameStore((s) => s.market.consumers);
const enterprise = useGameStore((s) => s.market.enterprise);
function SettingsPanel() {
const overloadPolicy = useGameStore((s) => s.market.overloadPolicy);
const productLines = useGameStore((s) => s.models.productLines);
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
const tokensCapacity = useGameStore((s) => s.compute.tokensPerSecondCapacity);
const tokensDemand = useGameStore((s) => s.compute.tokensPerSecondDemand);
const currentEra = useGameStore((s) => s.meta.currentEra);
const reputationScore = useGameStore((s) => s.reputation.score);
const bestQuality = useGameStore((s) => s.models.bestDeployedModelScore / 100);
const setProductPricing = useGameStore((s) => s.setProductPricing);
const setOverloadPolicy = useGameStore((s) => s.setOverloadPolicy);
const pricingFeedback = useAppliedFeedback();
const policyFeedback = useAppliedFeedback();
const eraCapBase = MARKET_SIZE_CAP[currentEra] ?? 100_000_000;
const effectiveCap = eraCapBase
* (1 + bestQuality * MARKET_CAP_QUALITY_BONUS)
* (1 + (reputationScore / 100) * MARKET_CAP_REPUTATION_BONUS);
const saturation = effectiveCap > 0 ? consumers.totalSubscribers / effectiveCap : 0;
const chatProduct = productLines.find(p => p.type === 'chat-product');
const textApi = productLines.find(p => p.type === 'text-api');
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Market</h2>
<TutorialHint id="market-intro">
Adjust pricing to balance growth and revenue. Watch customer satisfaction low scores increase churn. High system load means you need more inference capacity.
</TutorialHint>
<div className="grid grid-cols-4 gap-4">
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Users size={16} className="text-orange-400" />
<span className="text-xs text-surface-400 uppercase">Subscribers</span>
</div>
<div className="text-2xl font-bold font-mono">{formatNumber(consumers.totalSubscribers)}</div>
<div className="text-xs text-surface-400 mt-1">
Growth: {formatPercent(consumers.growthRatePerTick)}/s
{' '}Churn: {formatPercent(consumers.churnRatePerTick)}/s
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Shield size={16} className="text-green-400" />
<span className="text-xs text-surface-400 uppercase">Satisfaction</span>
</div>
<div className="text-2xl font-bold font-mono">{formatPercent(consumers.satisfaction)}</div>
<div className="h-1.5 bg-surface-800 rounded-full mt-2 overflow-hidden">
<div
className={`h-full rounded-full ${consumers.satisfaction > 0.7 ? 'bg-success' : consumers.satisfaction > 0.4 ? 'bg-warning' : 'bg-danger'}`}
style={{ width: `${consumers.satisfaction * 100}%` }}
/>
</div>
</div>
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Zap size={16} className="text-blue-400" />
<span className="text-xs text-surface-400 uppercase">Load</span>
<span className="text-xs text-surface-400 uppercase">Inference Load</span>
</div>
<div className="text-2xl font-bold font-mono">{formatPercent(inferenceUtil)}</div>
<div className="text-xs text-surface-400 mt-1">
@@ -97,85 +68,18 @@ export function MarketPage() {
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<TrendingUp size={16} className="text-purple-400" />
<span className="text-xs text-surface-400 uppercase">Market Saturation</span>
</div>
<div className="text-2xl font-bold font-mono">{formatPercent(saturation)}</div>
<div className="text-xs text-surface-400 mt-1">
Cap: {formatNumber(effectiveCap)} ({currentEra})
</div>
<div className="h-1.5 bg-surface-800 rounded-full mt-2 overflow-hidden">
<div
className={`h-full rounded-full ${saturation > 0.9 ? 'bg-danger' : saturation > 0.7 ? 'bg-warning' : 'bg-accent'}`}
style={{ width: `${Math.min(100, saturation * 100)}%` }}
/>
<Users size={16} className="text-orange-400" />
<span className="text-xs text-surface-400 uppercase">Subscribers</span>
</div>
<div className="text-2xl font-bold font-mono">{formatNumber(useGameStore.getState().market.consumerTiers.totalUsers)}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
{chatProduct && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-semibold">Chat Product Pricing</h3>
<span className={`text-xs px-2 py-1 rounded-full ${chatProduct.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}>
{chatProduct.isActive ? 'Active' : 'Inactive'}
</span>
</div>
<div>
<label className="block text-xs text-surface-400 mb-1">
Monthly Subscription Price <AppliedBadge visible={pricingFeedback.show} />
</label>
<div className="flex items-center gap-2">
<span className="text-sm">$</span>
<input
type="number"
value={chatProduct.pricing.subscriptionPrice}
onChange={(e) => { setProductPricing(chatProduct.id, 'subscriptionPrice', Number(e.target.value)); pricingFeedback.trigger(); }}
className="w-24 bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={5}
/>
<span className="text-xs text-surface-400">/month</span>
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Shield size={16} className="text-green-400" />
<span className="text-xs text-surface-400 uppercase">Satisfaction</span>
</div>
)}
{textApi && (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-semibold">API Pricing</h3>
<span className={`text-xs px-2 py-1 rounded-full ${textApi.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}>
{textApi.isActive ? 'Active' : 'Inactive'}
</span>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs text-surface-400 mb-1">Input ($/M tokens) <AppliedBadge visible={pricingFeedback.show} /></label>
<input
type="number"
value={textApi.pricing.inputTokenPrice}
onChange={(e) => { setProductPricing(textApi.id, 'inputTokenPrice', Number(e.target.value)); pricingFeedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={0.5}
/>
</div>
<div>
<label className="block text-xs text-surface-400 mb-1">Output ($/M tokens)</label>
<input
type="number"
value={textApi.pricing.outputTokenPrice}
onChange={(e) => { setProductPricing(textApi.id, 'outputTokenPrice', Number(e.target.value)); pricingFeedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={0.5}
/>
</div>
</div>
</div>
)}
<div className="text-2xl font-bold font-mono">{formatPercent(useGameStore.getState().market.consumerTiers.satisfaction)}</div>
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
@@ -233,25 +137,44 @@ export function MarketPage() {
</label>
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
<h3 className="font-semibold">Enterprise Contracts</h3>
{enterprise.activeContracts.length === 0 ? (
<p className="text-sm text-surface-500">No enterprise contracts yet. Improve your model quality and reputation to attract enterprise customers.</p>
) : (
<div className="space-y-2">
{enterprise.activeContracts.map(c => (
<div key={c.id} className="bg-surface-800 rounded-lg p-3 flex items-center justify-between">
<div>
<div className="text-sm font-medium">{c.customerName}</div>
<div className="text-xs text-surface-400">{formatNumber(c.tokensPerTick)} tok/s · SLA: {formatPercent(c.slaUptime)}</div>
</div>
<div className="text-sm font-mono text-success">{formatMoney(c.pricePerMToken)}/M tok</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
export function MarketPage() {
const [activeTab, setActiveTab] = useState<MarketTab>('overview');
return (
<div className="space-y-4">
<h2 className="text-2xl font-bold">Market</h2>
<TutorialHint id="market-intro">
Manage your multi-tier pricing, enterprise pipeline, and developer ecosystem. Watch market share you're competing against 3 AI rivals for customers across every segment.
</TutorialHint>
<div className="flex gap-1 border-b border-surface-700 pb-px">
{TABS.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm rounded-t-lg transition-colors ${
activeTab === tab.id
? 'bg-surface-800 text-surface-100 border-b-2 border-accent'
: 'text-surface-400 hover:text-surface-200 hover:bg-surface-800/50'
}`}
>
{tab.label}
</button>
))}
</div>
{activeTab === 'overview' && <MarketOverviewPanel />}
{activeTab === 'consumer' && <ConsumerTiersPanel />}
{activeTab === 'api' && <ApiTiersPanel />}
{activeTab === 'enterprise' && <EnterprisePipelinePanel />}
{activeTab === 'ecosystem' && <DeveloperEcosystemPanel />}
{activeTab === 'products' && <ProductLinesPanel />}
{activeTab === 'settings' && <SettingsPanel />}
</div>
);
}
+130
View File
@@ -0,0 +1,130 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import type { ApiTierId } from '@ai-tycoon/shared';
import { Code, Check } from 'lucide-react';
const TIER_ORDER: ApiTierId[] = ['free', 'payg', 'scale', 'enterprise-api'];
const TIER_COLORS: Record<ApiTierId, string> = {
free: 'border-surface-500',
payg: 'border-green-500',
scale: 'border-blue-500',
'enterprise-api': 'border-purple-500',
};
function useAppliedFeedback() {
const [show, setShow] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const trigger = useCallback(() => {
setShow(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setShow(false), 1200);
}, []);
useEffect(() => () => clearTimeout(timerRef.current), []);
return { show, trigger };
}
export function ApiTiersPanel() {
const apiTiers = useGameStore((s) => s.market.apiTiers);
const setApiTierPrice = useGameStore((s) => s.setApiTierPrice);
const toggleApiTier = useGameStore((s) => s.toggleApiTier);
const feedback = useAppliedFeedback();
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Code size={16} className="text-blue-400" />
<span className="text-sm font-semibold">API Tiers</span>
</div>
<div className="flex items-center gap-4 text-xs text-surface-400">
<span>Developers: <span className="font-mono text-surface-200">{formatNumber(apiTiers.totalDevelopers)}</span></span>
<span>Tokens/s: <span className="font-mono text-surface-200">{formatNumber(apiTiers.totalTokensPerTick)}</span></span>
</div>
</div>
<div className="grid grid-cols-4 gap-3">
{TIER_ORDER.map(tierId => {
const tier = apiTiers.tiers[tierId];
return (
<div key={tierId} className={`bg-surface-900 border-t-2 ${TIER_COLORS[tierId]} border border-surface-700 rounded-xl p-4 space-y-3`}>
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm">{tier.config.name}</h4>
{tierId !== 'free' && (
<button
onClick={() => { toggleApiTier(tierId); feedback.trigger(); }}
className={`text-[10px] px-2 py-0.5 rounded-full ${tier.config.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
>
{tier.config.isActive ? 'Active' : 'Inactive'}
</button>
)}
{tierId === 'free' && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-surface-700 text-surface-400">Always On</span>
)}
</div>
<div className="text-2xl font-bold font-mono">{formatNumber(tier.developerCount)}</div>
<div className="text-xs text-surface-400">developers</div>
{tierId !== 'free' ? (
<div className="space-y-2">
<div>
<label className="block text-[10px] text-surface-400 mb-1">
Monthly Fee ($)
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
</label>
<input
type="number"
value={tier.config.monthlyFee}
onChange={(e) => { setApiTierPrice(tierId, 'monthlyFee', Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={50}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[10px] text-surface-400 mb-1">In $/M</label>
<input
type="number"
value={tier.config.inputTokenPrice}
onChange={(e) => { setApiTierPrice(tierId, 'inputTokenPrice', Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={0.1}
/>
</div>
<div>
<label className="block text-[10px] text-surface-400 mb-1">Out $/M</label>
<input
type="number"
value={tier.config.outputTokenPrice}
onChange={(e) => { setApiTierPrice(tierId, 'outputTokenPrice', Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={0.1}
/>
</div>
</div>
</div>
) : (
<div className="text-xs text-surface-500">Free tier attracts developers to your ecosystem</div>
)}
<div className="text-xs space-y-1 text-surface-400">
<div className="flex justify-between">
<span>Rate Limit</span>
<span className="font-mono">{formatNumber(tier.config.rateLimit)} req/min</span>
</div>
<div className="flex justify-between">
<span>Tokens/s</span>
<span className="font-mono">{formatNumber(tier.tokensPerTick)}</span>
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,109 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import type { ConsumerTierId } from '@ai-tycoon/shared';
import { Users, Check } from 'lucide-react';
const TIER_ORDER: ConsumerTierId[] = ['free', 'plus', 'pro', 'team'];
const TIER_COLORS: Record<ConsumerTierId, string> = {
free: 'border-surface-500',
plus: 'border-blue-500',
pro: 'border-purple-500',
team: 'border-orange-500',
};
function useAppliedFeedback() {
const [show, setShow] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const trigger = useCallback(() => {
setShow(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setShow(false), 1200);
}, []);
useEffect(() => () => clearTimeout(timerRef.current), []);
return { show, trigger };
}
export function ConsumerTiersPanel() {
const consumerTiers = useGameStore((s) => s.market.consumerTiers);
const setConsumerTierPrice = useGameStore((s) => s.setConsumerTierPrice);
const toggleConsumerTier = useGameStore((s) => s.toggleConsumerTier);
const feedback = useAppliedFeedback();
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Users size={16} className="text-orange-400" />
<span className="text-sm font-semibold">Consumer Subscriptions</span>
</div>
<div className="flex items-center gap-4 text-xs text-surface-400">
<span>Total: <span className="font-mono text-surface-200">{formatNumber(consumerTiers.totalUsers)}</span></span>
<span>Satisfaction: <span className={`font-mono ${consumerTiers.satisfaction > 0.7 ? 'text-success' : consumerTiers.satisfaction > 0.4 ? 'text-warning' : 'text-danger'}`}>{formatPercent(consumerTiers.satisfaction)}</span></span>
</div>
</div>
<div className="grid grid-cols-4 gap-3">
{TIER_ORDER.map(tierId => {
const tier = consumerTiers.tiers[tierId];
const revenue = tierId === 'free' ? 0 : tier.userCount * tier.config.price / 86400;
return (
<div key={tierId} className={`bg-surface-900 border-t-2 ${TIER_COLORS[tierId]} border border-surface-700 rounded-xl p-4 space-y-3`}>
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm">{tier.config.name}</h4>
{tierId !== 'free' && (
<button
onClick={() => { toggleConsumerTier(tierId); feedback.trigger(); }}
className={`text-[10px] px-2 py-0.5 rounded-full ${tier.config.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
>
{tier.config.isActive ? 'Active' : 'Inactive'}
</button>
)}
{tierId === 'free' && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-surface-700 text-surface-400">Always On</span>
)}
</div>
<div className="text-2xl font-bold font-mono">{formatNumber(tier.userCount)}</div>
<div className="text-xs text-surface-400">users</div>
{tierId !== 'free' && (
<div>
<label className="block text-[10px] text-surface-400 mb-1">
Price ($/mo)
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
</label>
<input
type="number"
value={tier.config.price}
onChange={(e) => { setConsumerTierPrice(tierId, Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={1}
step={5}
/>
</div>
)}
<div className="text-xs space-y-1 text-surface-400">
<div className="flex justify-between">
<span>Tokens/mo</span>
<span className="font-mono">{formatNumber(tier.config.tokenAllowance)}</span>
</div>
<div className="flex justify-between">
<span>Churn</span>
<span className="font-mono">{formatPercent(tier.churnRate)}/t</span>
</div>
{tierId !== 'free' && (
<div className="flex justify-between">
<span>Revenue/s</span>
<span className="font-mono text-success">{formatMoney(revenue)}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}
@@ -0,0 +1,106 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import { Boxes, Check } from 'lucide-react';
function useAppliedFeedback() {
const [show, setShow] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const trigger = useCallback(() => {
setShow(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setShow(false), 1200);
}, []);
useEffect(() => () => clearTimeout(timerRef.current), []);
return { show, trigger };
}
export function DeveloperEcosystemPanel() {
const devEco = useGameStore((s) => s.market.developerEcosystem);
const setDevRelSpending = useGameStore((s) => s.setDevRelSpending);
const feedback = useAppliedFeedback();
const metrics = [
{ label: 'Community Size', value: formatNumber(devEco.communitySize), sub: `Growth: ${devEco.communityGrowthRate.toFixed(2)}/t` },
{ label: 'Active Developers', value: formatNumber(devEco.activeDevelopers), sub: `${devEco.communitySize > 0 ? formatPercent(devEco.activeDevelopers / devEco.communitySize) : '0%'} active` },
{ label: 'SDK Coverage', value: formatPercent(devEco.sdkCoverage), sub: 'Hire engineers to improve' },
{ label: 'Doc Quality', value: formatPercent(devEco.documentationQuality), sub: 'Increase dev-rel spending' },
];
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Boxes size={16} className="text-green-400" />
<span className="text-sm font-semibold">Developer Ecosystem</span>
</div>
<div className="text-xs text-surface-400">
Ecosystem Score: <span className="font-mono text-surface-200 text-sm">{devEco.ecosystemScore.toFixed(1)}</span>/100
</div>
</div>
<div className="grid grid-cols-4 gap-3">
{metrics.map(m => (
<div key={m.label} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
<div className="text-[10px] text-surface-400 uppercase mb-1">{m.label}</div>
<div className="text-lg font-bold font-mono">{m.value}</div>
<div className="text-[10px] text-surface-500 mt-0.5">{m.sub}</div>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<h4 className="text-sm font-semibold mb-3">Flywheel Effects</h4>
<div className="space-y-2 text-xs">
<div className="flex justify-between text-surface-300">
<span>Startups using your API</span>
<span className="font-mono">{formatNumber(devEco.startupsAdopted)}</span>
</div>
<div className="flex justify-between text-surface-300">
<span>Enterprise referrals generated</span>
<span className="font-mono">{formatNumber(devEco.enterpriseReferrals)}</span>
</div>
<div className="flex justify-between text-surface-300">
<span>Open-source contributions</span>
<span className="font-mono">{devEco.openSourceContributions}</span>
</div>
</div>
<div className="mt-3 p-2 bg-surface-800 rounded text-[10px] text-surface-400">
Free tier generosity attracts devs, who build startups on your API, which generates enterprise referrals.
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<h4 className="text-sm font-semibold mb-3">
Dev-Rel Budget
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-2 animate-pulse text-[10px]"><Check size={8} /> Applied</span>}
</h4>
<div className="space-y-3">
<div>
<label className="block text-xs text-surface-400 mb-1">Spending ($/tick)</label>
<input
type="number"
value={devEco.devRelSpending}
onChange={(e) => { setDevRelSpending(Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={0}
step={0.1}
/>
</div>
<div className="text-[10px] text-surface-500 space-y-1">
<p>Dev-rel spending improves documentation quality and community engagement.</p>
<p>Current cost: <span className="font-mono text-surface-300">{formatMoney(devEco.devRelSpending)}/s</span></p>
</div>
<div className="h-1.5 bg-surface-800 rounded-full overflow-hidden">
<div
className="h-full rounded-full bg-green-500"
style={{ width: `${Math.min(100, devEco.ecosystemScore)}%` }}
/>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,142 @@
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import type { EnterprisePipelineStage, EnterpriseSegment } from '@ai-tycoon/shared';
import { Building2, AlertTriangle } from 'lucide-react';
const STAGE_ORDER: EnterprisePipelineStage[] = ['lead', 'qualification', 'poc', 'negotiation'];
const STAGE_LABELS: Record<EnterprisePipelineStage, string> = {
lead: 'Leads',
qualification: 'Qualification',
poc: 'POC',
negotiation: 'Negotiation',
};
const STAGE_COLORS: Record<EnterprisePipelineStage, string> = {
lead: 'bg-surface-600',
qualification: 'bg-blue-600',
poc: 'bg-purple-600',
negotiation: 'bg-orange-600',
};
const SEGMENT_BADGES: Record<EnterpriseSegment, { label: string; color: string }> = {
startup: { label: 'Startup', color: 'bg-green-500/20 text-green-400' },
'mid-market': { label: 'Mid-Market', color: 'bg-blue-500/20 text-blue-400' },
enterprise: { label: 'Enterprise', color: 'bg-purple-500/20 text-purple-400' },
government: { label: 'Gov', color: 'bg-yellow-500/20 text-yellow-400' },
};
export function EnterprisePipelinePanel() {
const enterprise = useGameStore((s) => s.market.enterprise);
const tickCount = useGameStore((s) => s.meta.tickCount);
const leadsByStage = STAGE_ORDER.map(stage => ({
stage,
leads: enterprise.pipeline.filter(l => l.stage === stage),
}));
const totalContractValue = enterprise.activeContracts.reduce((sum, c) => sum + c.pricePerMToken * c.tokensPerTick, 0);
const totalSlaViolations = enterprise.activeContracts.reduce((sum, c) => sum + c.slaViolations, 0);
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Building2 size={16} className="text-purple-400" />
<span className="text-sm font-semibold">Enterprise Pipeline</span>
</div>
<div className="flex items-center gap-4 text-xs text-surface-400">
<span>Leads: <span className="font-mono text-surface-200">{enterprise.pipeline.length}</span></span>
<span>Contracts: <span className="font-mono text-surface-200">{enterprise.activeContracts.length}</span></span>
<span>Lead Rate: <span className="font-mono text-surface-200">{enterprise.leadGenerationRate.toFixed(3)}/t</span></span>
</div>
</div>
<div className="grid grid-cols-4 gap-3">
{leadsByStage.map(({ stage, leads }) => (
<div key={stage} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
<div className="flex items-center gap-2 mb-2">
<div className={`w-2 h-2 rounded-full ${STAGE_COLORS[stage]}`} />
<span className="text-xs font-semibold">{STAGE_LABELS[stage]}</span>
<span className="text-xs text-surface-500 ml-auto">{leads.length}</span>
</div>
<div className="space-y-1.5 max-h-40 overflow-y-auto">
{leads.length === 0 && (
<div className="text-xs text-surface-600 italic">No leads</div>
)}
{leads.map(lead => (
<div key={lead.id} className="bg-surface-800 rounded px-2 py-1.5 text-xs">
<div className="flex items-center justify-between">
<span className="font-medium text-surface-200 truncate">{lead.companyName}</span>
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${SEGMENT_BADGES[lead.segment].color}`}>
{SEGMENT_BADGES[lead.segment].label}
</span>
</div>
<div className="flex justify-between text-surface-400 mt-0.5">
<span>{formatMoney(lead.dealValue)}/yr</span>
<span>Win: {formatPercent(lead.winProbability)}</span>
</div>
</div>
))}
</div>
</div>
))}
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-semibold">Active Contracts</span>
{totalSlaViolations > 0 && (
<span className="flex items-center gap-1 text-xs text-warning">
<AlertTriangle size={12} /> {totalSlaViolations} SLA violations
</span>
)}
</div>
{enterprise.activeContracts.length === 0 ? (
<p className="text-xs text-surface-500">No active contracts. Build your sales team and model quality to attract enterprise customers.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-surface-400 border-b border-surface-700">
<th className="text-left py-1.5 pr-3">Customer</th>
<th className="text-left py-1.5 px-2">Segment</th>
<th className="text-right py-1.5 px-2">Tokens/s</th>
<th className="text-right py-1.5 px-2">$/M tok</th>
<th className="text-right py-1.5 px-2">SLA</th>
<th className="text-right py-1.5 px-2">Satisfaction</th>
<th className="text-right py-1.5 px-2">Remaining</th>
<th className="text-right py-1.5 pl-2">Renewal</th>
</tr>
</thead>
<tbody>
{enterprise.activeContracts.map(c => {
const remaining = Math.max(0, c.startTick + c.durationTicks - tickCount);
const uptime = c.totalTicks > 0 ? c.uptimeTicks / c.totalTicks : 1;
return (
<tr key={c.id} className="border-b border-surface-800">
<td className="py-1.5 pr-3 font-medium text-surface-200">{c.customerName}</td>
<td className="py-1.5 px-2">
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${SEGMENT_BADGES[c.segment].color}`}>
{SEGMENT_BADGES[c.segment].label}
</span>
</td>
<td className="text-right py-1.5 px-2 font-mono">{formatNumber(c.tokensPerTick)}</td>
<td className="text-right py-1.5 px-2 font-mono text-success">{formatMoney(c.pricePerMToken)}</td>
<td className="text-right py-1.5 px-2 font-mono">
<span className={uptime >= c.slaUptime ? 'text-success' : 'text-danger'}>
{formatPercent(uptime)}
</span>
</td>
<td className="text-right py-1.5 px-2 font-mono">{formatPercent(c.satisfaction)}</td>
<td className="text-right py-1.5 px-2 font-mono">{formatNumber(remaining)}t</td>
<td className="text-right py-1.5 pl-2 font-mono">{formatPercent(c.renewalProbability)}</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
@@ -0,0 +1,165 @@
import { useGameStore } from '@/store';
import { formatNumber, formatPercent } from '@ai-tycoon/shared';
import type { TAMSegmentId } from '@ai-tycoon/shared';
import { Globe, TrendingUp, Clock, Thermometer } from 'lucide-react';
const SEGMENT_LABELS: Record<TAMSegmentId, string> = {
consumer: 'Consumer',
developer: 'Developer',
enterprise: 'Enterprise',
government: 'Government',
};
const SEGMENT_COLORS: Record<TAMSegmentId, string> = {
consumer: 'bg-orange-500',
developer: 'bg-blue-500',
enterprise: 'bg-purple-500',
government: 'bg-green-500',
};
const SEASON_LABELS: Record<string, string> = {
q1: 'Q1 — Slow Start',
q2: 'Q2 — Baseline',
q3: 'Q3 — Summer Dip',
q4: 'Q4 — Budget Surge',
};
export function MarketOverviewPanel() {
const tam = useGameStore((s) => s.market.tam);
const seasonalPhase = useGameStore((s) => s.market.seasonalPhase);
const seasonalMultiplier = useGameStore((s) => s.market.seasonalMultiplier);
const obsolescence = useGameStore((s) => s.market.obsolescence);
const bestScore = useGameStore((s) => s.models.bestDeployedModelScore);
const competitors = useGameStore((s) => s.competitors.rivals);
const segments = Object.entries(tam.segments) as [TAMSegmentId, typeof tam.segments.consumer][];
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-3">
<Globe size={16} className="text-accent" />
<span className="text-sm font-semibold">Market Share by Segment</span>
</div>
<div className="space-y-3">
{segments.map(([id, seg]) => {
const playerShare = seg.shares.find(s => s.playerId === 'player');
const share = playerShare?.sharePercent ?? 0;
return (
<div key={id}>
<div className="flex justify-between text-xs mb-1">
<span className="text-surface-300">{SEGMENT_LABELS[id]}</span>
<span className="font-mono">
{formatPercent(share)} · {formatNumber(playerShare?.customers ?? 0)} customers
</span>
</div>
<div className="h-2 bg-surface-800 rounded-full overflow-hidden flex">
{seg.shares
.filter(s => s.sharePercent > 0.001)
.sort((a, b) => b.sharePercent - a.sharePercent)
.map((s, i) => (
<div
key={s.playerId}
className={`h-full ${s.playerId === 'player' ? SEGMENT_COLORS[id] : `bg-surface-${600 - i * 100}`}`}
style={{ width: `${s.sharePercent * 100}%` }}
title={`${s.playerId}: ${formatPercent(s.sharePercent)}`}
/>
))}
</div>
</div>
);
})}
</div>
</div>
<div className="space-y-4">
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Clock size={16} className="text-yellow-400" />
<span className="text-sm font-semibold">Season</span>
</div>
<div className="text-lg font-bold">{SEASON_LABELS[seasonalPhase] ?? seasonalPhase}</div>
<div className="text-xs text-surface-400 mt-1">
Demand multiplier: {formatPercent(seasonalMultiplier)}
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Thermometer size={16} className="text-red-400" />
<span className="text-sm font-semibold">Technology Pressure</span>
</div>
<div className="flex justify-between text-xs mb-1">
<span className="text-surface-400">Market Quality Baseline</span>
<span className="font-mono">{(obsolescence.marketQualityBaseline * 100).toFixed(1)}</span>
</div>
<div className="flex justify-between text-xs mb-1">
<span className="text-surface-400">Your Best Model</span>
<span className="font-mono">{bestScore.toFixed(1)}</span>
</div>
<div className="flex justify-between text-xs mb-1">
<span className="text-surface-400">Model Freshness</span>
<span className="font-mono">{formatPercent(obsolescence.playerModelFreshness)}</span>
</div>
{obsolescence.newModelBoostRemaining > 0 && (
<div className="text-xs text-success mt-1">New model boost active!</div>
)}
{bestScore / 100 < obsolescence.marketQualityBaseline && (
<div className="text-xs text-danger mt-1">Below market baseline losing attractiveness</div>
)}
</div>
</div>
</div>
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-3">
<TrendingUp size={16} className="text-purple-400" />
<span className="text-sm font-semibold">Competitive Landscape</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-surface-400 border-b border-surface-700">
<th className="text-left py-2 pr-4">Competitor</th>
<th className="text-right py-2 px-2">Consumer</th>
<th className="text-right py-2 px-2">Developer</th>
<th className="text-right py-2 px-2">Enterprise</th>
<th className="text-right py-2 px-2">Freshness</th>
<th className="text-right py-2 pl-2">Dev Eco</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-surface-800 text-accent">
<td className="py-2 pr-4 font-medium">You</td>
<td className="text-right py-2 px-2 font-mono">
{formatPercent(tam.segments.consumer.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
</td>
<td className="text-right py-2 px-2 font-mono">
{formatPercent(tam.segments.developer.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
</td>
<td className="text-right py-2 px-2 font-mono">
{formatPercent(tam.segments.enterprise.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
</td>
<td className="text-right py-2 px-2 font-mono">
{formatPercent(obsolescence.playerModelFreshness)}
</td>
<td className="text-right py-2 pl-2 font-mono"></td>
</tr>
{competitors.filter(r => r.status === 'active').map(r => (
<tr key={r.id} className="border-b border-surface-800">
<td className="py-2 pr-4 font-medium text-surface-300">{r.name}</td>
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.marketShares.consumer)}</td>
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.marketShares.developer)}</td>
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.marketShares.enterprise)}</td>
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.modelFreshness)}</td>
<td className="text-right py-2 pl-2 font-mono">{r.developerEcosystemScore.toFixed(0)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}
@@ -0,0 +1,154 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import { Wrench, Bot, Check, Lock } from 'lucide-react';
function useAppliedFeedback() {
const [show, setShow] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const trigger = useCallback(() => {
setShow(true);
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setShow(false), 1200);
}, []);
useEffect(() => () => clearTimeout(timerRef.current), []);
return { show, trigger };
}
export function ProductLinesPanel() {
const codeAssistant = useGameStore((s) => s.market.codeAssistant);
const agentsPlatform = useGameStore((s) => s.market.agentsPlatform);
const setCodeAssistantPrice = useGameStore((s) => s.setCodeAssistantPrice);
const toggleCodeAssistant = useGameStore((s) => s.toggleCodeAssistant);
const setAgentsPlatformPrice = useGameStore((s) => s.setAgentsPlatformPrice);
const toggleAgentsPlatform = useGameStore((s) => s.toggleAgentsPlatform);
const feedback = useAppliedFeedback();
return (
<div className="space-y-4">
<span className="text-sm font-semibold">Product Lines</span>
<div className="grid grid-cols-2 gap-4">
<div className={`bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3 ${!codeAssistant.isUnlocked ? 'opacity-60' : ''}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Wrench size={16} className="text-cyan-400" />
<h4 className="font-semibold text-sm">Code Assistant</h4>
</div>
{codeAssistant.isUnlocked ? (
<button
onClick={() => { toggleCodeAssistant(); feedback.trigger(); }}
className={`text-[10px] px-2 py-0.5 rounded-full ${codeAssistant.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
>
{codeAssistant.isActive ? 'Active' : 'Inactive'}
</button>
) : (
<span className="flex items-center gap-1 text-[10px] text-surface-500">
<Lock size={10} /> Research Required
</span>
)}
</div>
{codeAssistant.isUnlocked ? (
<>
<div className="grid grid-cols-3 gap-2 text-xs">
<div>
<div className="text-[10px] text-surface-400 uppercase">Seats</div>
<div className="text-lg font-bold font-mono">{formatNumber(codeAssistant.seats)}</div>
</div>
<div>
<div className="text-[10px] text-surface-400 uppercase">Quality</div>
<div className="text-lg font-bold font-mono">{codeAssistant.qualityScore.toFixed(0)}</div>
</div>
<div>
<div className="text-[10px] text-surface-400 uppercase">Revenue/s</div>
<div className="text-lg font-bold font-mono text-success">
{formatMoney(codeAssistant.seats * codeAssistant.pricePerSeat / 86400)}
</div>
</div>
</div>
<div>
<label className="block text-[10px] text-surface-400 mb-1">
Price ($/seat/mo)
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
</label>
<input
type="number"
value={codeAssistant.pricePerSeat}
onChange={(e) => { setCodeAssistantPrice(Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={5}
step={5}
/>
</div>
</>
) : (
<p className="text-xs text-surface-500">
Research "Code Assistant Product" to unlock. Requires the code-generation research.
</p>
)}
</div>
<div className={`bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3 ${!agentsPlatform.isUnlocked ? 'opacity-60' : ''}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Bot size={16} className="text-orange-400" />
<h4 className="font-semibold text-sm">AI Agents Platform</h4>
</div>
{agentsPlatform.isUnlocked ? (
<button
onClick={() => { toggleAgentsPlatform(); feedback.trigger(); }}
className={`text-[10px] px-2 py-0.5 rounded-full ${agentsPlatform.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
>
{agentsPlatform.isActive ? 'Active' : 'Inactive'}
</button>
) : (
<span className="flex items-center gap-1 text-[10px] text-surface-500">
<Lock size={10} /> Research Required
</span>
)}
</div>
{agentsPlatform.isUnlocked ? (
<>
<div className="grid grid-cols-3 gap-2 text-xs">
<div>
<div className="text-[10px] text-surface-400 uppercase">Seats</div>
<div className="text-lg font-bold font-mono">{formatNumber(agentsPlatform.seats)}</div>
</div>
<div>
<div className="text-[10px] text-surface-400 uppercase">Quality</div>
<div className="text-lg font-bold font-mono">{agentsPlatform.qualityScore.toFixed(0)}</div>
</div>
<div>
<div className="text-[10px] text-surface-400 uppercase">Revenue/s</div>
<div className="text-lg font-bold font-mono text-success">
{formatMoney(agentsPlatform.seats * agentsPlatform.pricePerSeat / 86400)}
</div>
</div>
</div>
<div>
<label className="block text-[10px] text-surface-400 mb-1">
Price ($/seat/mo)
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
</label>
<input
type="number"
value={agentsPlatform.pricePerSeat}
onChange={(e) => { setAgentsPlatformPrice(Number(e.target.value)); feedback.trigger(); }}
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
min={10}
step={10}
/>
</div>
</>
) : (
<p className="text-xs text-surface-500">
Research "AI Agents Platform" to unlock. Requires the agentic-architecture research.
</p>
)}
</div>
</div>
</div>
);
}
+131 -1
View File
@@ -16,6 +16,7 @@ import type {
ModelArchitecture,
SFTSpecialization, QuantizationLevel, VariantCreationJob,
EvalJob,
ConsumerTierId, ApiTierId,
} from '@ai-tycoon/shared';
import {
INITIAL_SETTINGS, SAVE_VERSION,
@@ -125,6 +126,15 @@ interface Actions {
openSourceModel: (modelId: string) => void;
setOverloadPolicy: (policy: Partial<OverloadPolicy>) => void;
acquireCompetitor: (competitorId: string) => void;
setConsumerTierPrice: (tierId: ConsumerTierId, price: number) => void;
toggleConsumerTier: (tierId: ConsumerTierId) => void;
setApiTierPrice: (tierId: ApiTierId, field: 'monthlyFee' | 'inputTokenPrice' | 'outputTokenPrice', value: number) => void;
toggleApiTier: (tierId: ApiTierId) => void;
setDevRelSpending: (amount: number) => void;
setCodeAssistantPrice: (price: number) => void;
toggleCodeAssistant: () => void;
setAgentsPlatformPrice: (price: number) => void;
toggleAgentsPlatform: () => void;
updateState: (partial: Partial<GameState>) => void;
}
@@ -1205,6 +1215,126 @@ export const useGameStore = create<Store>()(
};
}),
setConsumerTierPrice: (tierId, price) => set((s) => ({
market: {
...s.market,
consumerTiers: {
...s.market.consumerTiers,
tiers: {
...s.market.consumerTiers.tiers,
[tierId]: {
...s.market.consumerTiers.tiers[tierId],
config: { ...s.market.consumerTiers.tiers[tierId].config, price },
},
},
},
},
})),
toggleConsumerTier: (tierId) => set((s) => ({
market: {
...s.market,
consumerTiers: {
...s.market.consumerTiers,
tiers: {
...s.market.consumerTiers.tiers,
[tierId]: {
...s.market.consumerTiers.tiers[tierId],
config: {
...s.market.consumerTiers.tiers[tierId].config,
isActive: !s.market.consumerTiers.tiers[tierId].config.isActive,
},
},
},
},
},
})),
setApiTierPrice: (tierId, field, value) => set((s) => ({
market: {
...s.market,
apiTiers: {
...s.market.apiTiers,
tiers: {
...s.market.apiTiers.tiers,
[tierId]: {
...s.market.apiTiers.tiers[tierId],
config: { ...s.market.apiTiers.tiers[tierId].config, [field]: value },
},
},
},
},
})),
toggleApiTier: (tierId) => set((s) => ({
market: {
...s.market,
apiTiers: {
...s.market.apiTiers,
tiers: {
...s.market.apiTiers.tiers,
[tierId]: {
...s.market.apiTiers.tiers[tierId],
config: {
...s.market.apiTiers.tiers[tierId].config,
isActive: !s.market.apiTiers.tiers[tierId].config.isActive,
},
},
},
},
},
})),
setDevRelSpending: (amount) => set((s) => ({
market: {
...s.market,
developerEcosystem: {
...s.market.developerEcosystem,
devRelSpending: amount,
},
},
})),
setCodeAssistantPrice: (price) => set((s) => ({
market: {
...s.market,
codeAssistant: {
...s.market.codeAssistant,
pricePerSeat: price,
},
},
})),
toggleCodeAssistant: () => set((s) => ({
market: {
...s.market,
codeAssistant: {
...s.market.codeAssistant,
isActive: !s.market.codeAssistant.isActive,
},
},
})),
setAgentsPlatformPrice: (price) => set((s) => ({
market: {
...s.market,
agentsPlatform: {
...s.market.agentsPlatform,
pricePerSeat: price,
},
},
})),
toggleAgentsPlatform: () => set((s) => ({
market: {
...s.market,
agentsPlatform: {
...s.market.agentsPlatform,
isActive: !s.market.agentsPlatform.isActive,
},
},
})),
updateState: (partial) => set((s) => {
const newState: Partial<Store> = {};
for (const key of Object.keys(partial) as (keyof GameState)[]) {
@@ -1234,7 +1364,7 @@ export const useGameStore = create<Store>()(
notifications: [{
id: uuid(),
title: 'Save Reset',
message: 'Your save was reset due to a major model system overhaul — multi-stage training pipelines, model families with variants, benchmarks, and architecture choices!',
message: 'Your save was reset due to a major market system overhaul — shared TAM competition, multi-tier pricing, enterprise pipeline, developer ecosystem, and technology obsolescence!',
type: 'info' as const,
tick: 0,
read: false,
+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);
}
@@ -1,4 +1,6 @@
import type { DCTier, DCTierConfig, RackSkuId, RackSkuConfig, SwitchTier, SwitchTierConfig, CampusTierCost, ClusterCostConfig, CoolingType, CoolingTypeConfig, NetworkFabric, NetworkFabricConfig } from '../types/infrastructure';
import type { Era } from '../types/gameState';
import type { ConsumerTierId, ApiTierId, SeasonalPhase, EnterprisePipelineStage, EnterpriseSegment, TAMSegmentId } from '../types/market';
export const TICK_INTERVAL_MS = 1000;
export const MAX_OFFLINE_TICKS = 86_400;
@@ -776,3 +778,214 @@ export const REGULATION_COMPLIANCE_PER_CAPABILITY = 0.5;
export const SAFETY_INCIDENT_PROBABILITY_BASE = 0.0002;
export const SAFETY_INCIDENT_REPUTATION_HIT = 15;
export const LOW_SAFETY_THRESHOLD = 40;
// ========================================================================
// MARKET SYSTEM v2 — Shared TAM, Tiered Products, Enterprise Pipeline
// ========================================================================
// --- Shared TAM ---
export const TAM_BASE_SIZES: Record<Era, Record<TAMSegmentId, number>> = {
startup: { consumer: 50_000, developer: 5_000, enterprise: 500, government: 50 },
scaleup: { consumer: 5_000_000, developer: 200_000, enterprise: 5_000, government: 500 },
bigtech: { consumer: 50_000_000, developer: 2_000_000, enterprise: 50_000, government: 5_000 },
agi: { consumer: 500_000_000, developer: 20_000_000, enterprise: 200_000, government: 20_000 },
};
export const TAM_GROWTH_PER_TICK = 0.0001;
export const SHARE_TEMPERATURE = 4.0;
export const SHARE_MIGRATION_SPEED = 0.03;
// --- Attractiveness Weights ---
export const ATTRACTIVENESS_WEIGHTS: Record<TAMSegmentId, Record<string, number>> = {
consumer: { quality: 0.30, price: 0.35, reputation: 0.15, ecosystem: 0.05, freshness: 0.10, freeTier: 0.05 },
developer: { quality: 0.25, price: 0.25, reputation: 0.10, ecosystem: 0.25, freshness: 0.10, freeTier: 0.05 },
enterprise: { quality: 0.35, price: 0.15, reputation: 0.25, ecosystem: 0.10, freshness: 0.10, freeTier: 0.05 },
government: { quality: 0.25, price: 0.10, reputation: 0.35, ecosystem: 0.05, freshness: 0.10, freeTier: 0.15 },
};
// --- Consumer Tier Defaults ---
export const CONSUMER_TIER_DEFAULTS: Record<ConsumerTierId, { price: number; tokenAllowance: number; requiredQuality: number }> = {
free: { price: 0, tokenAllowance: 5_000, requiredQuality: 0 },
plus: { price: 20, tokenAllowance: 50_000, requiredQuality: 20 },
pro: { price: 50, tokenAllowance: 200_000, requiredQuality: 40 },
team: { price: 30, tokenAllowance: 100_000, requiredQuality: 30 },
};
export const CONSUMER_TIER_ORDER: ConsumerTierId[] = ['free', 'plus', 'pro', 'team'];
export const CONVERSION_RATES: Record<string, number> = {
'free->plus': 0.002,
'plus->pro': 0.0008,
'pro->team': 0.0003,
};
export const TIER_CHURN_RATES: Record<ConsumerTierId, number> = {
free: 0.0005,
plus: 0.001,
pro: 0.0006,
team: 0.0004,
};
export const FREE_TIER_ADOPTION_RATE = 0.05;
// --- API Tier Defaults ---
export const API_TIER_DEFAULTS: Record<ApiTierId, { monthlyFee: number; inputPrice: number; outputPrice: number; rateLimit: number }> = {
free: { monthlyFee: 0, inputPrice: 0, outputPrice: 0, rateLimit: 10 },
payg: { monthlyFee: 0, inputPrice: 1.0, outputPrice: 3.0, rateLimit: 100 },
scale: { monthlyFee: 500, inputPrice: 0.8, outputPrice: 2.4, rateLimit: 1000 },
'enterprise-api': { monthlyFee: 5000, inputPrice: 0.6, outputPrice: 1.8, rateLimit: 10000 },
};
export const API_TIER_ORDER: ApiTierId[] = ['free', 'payg', 'scale', 'enterprise-api'];
export const API_TIER_CHURN_RATES: Record<ApiTierId, number> = {
free: 0.0003,
payg: 0.001,
scale: 0.0005,
'enterprise-api': 0.0003,
};
export const API_CONVERSION_RATES: Record<string, number> = {
'free->payg': 0.003,
'payg->scale': 0.001,
'scale->enterprise-api': 0.0004,
};
export const API_TOKENS_PER_DEVELOPER_PER_TICK: Record<ApiTierId, number> = {
free: 0.5,
payg: 5,
scale: 50,
'enterprise-api': 200,
};
// --- Code Assistant ---
export const CODE_ASSISTANT_MIN_CODING_SCORE = 40;
export const CODE_ASSISTANT_DEFAULT_PRICE = 20;
export const CODE_ASSISTANT_BASE_ADOPTION_RATE = 0.003;
export const CODE_ASSISTANT_CHURN_RATE = 0.0008;
// --- Agents Platform ---
export const AGENTS_PLATFORM_MIN_AGENTS_SCORE = 50;
export const AGENTS_PLATFORM_DEFAULT_PRICE = 100;
export const AGENTS_PLATFORM_BASE_ADOPTION_RATE = 0.002;
export const AGENTS_PLATFORM_CHURN_RATE = 0.0005;
// --- Enterprise Pipeline ---
export const BASE_LEAD_RATE = 0.005;
export const LEAD_EXPIRY_TICKS = 600;
export const PIPELINE_STAGE_TIMEOUTS: Record<EnterprisePipelineStage, number> = {
lead: 300,
qualification: 200,
poc: 400,
negotiation: 300,
};
export const PIPELINE_TRANSITION_RATES: Record<string, number> = {
'lead->qualification': 0.02,
'qualification->poc': 0.015,
'poc->negotiation': 0.01,
'negotiation->active': 0.008,
};
export const SLA_PENALTY_FRACTION = 0.02;
export const CONTRACT_BASE_DURATION_TICKS = 2400;
export const ENTERPRISE_DEAL_VALUES: Record<EnterpriseSegment, { min: number; max: number }> = {
startup: { min: 5_000, max: 50_000 },
'mid-market': { min: 50_000, max: 500_000 },
enterprise: { min: 500_000, max: 5_000_000 },
government: { min: 1_000_000, max: 10_000_000 },
};
export const ENTERPRISE_SLA_REQUIREMENTS: Record<EnterpriseSegment, number> = {
startup: 0.95,
'mid-market': 0.97,
enterprise: 0.99,
government: 0.995,
};
export const ENTERPRISE_CAPABILITY_REQUIREMENTS: Record<EnterpriseSegment, number> = {
startup: 15,
'mid-market': 30,
enterprise: 50,
government: 45,
};
export const ENTERPRISE_TOKENS_PER_TICK: Record<EnterpriseSegment, { min: number; max: number }> = {
startup: { min: 50, max: 500 },
'mid-market': { min: 500, max: 5_000 },
enterprise: { min: 5_000, max: 50_000 },
government: { min: 2_000, max: 20_000 },
};
export const CONTRACT_DURATION_BY_SEGMENT: Record<EnterpriseSegment, number> = {
startup: 1200,
'mid-market': 2400,
enterprise: 4800,
government: 7200,
};
// --- Developer Ecosystem ---
export const BASE_DEV_GROWTH = 0.001;
export const FREE_TIER_DEV_MULTIPLIER = 0.0005;
export const OPEN_SOURCE_DEV_BOOST = 0.05;
export const DEV_REL_EFFECTIVENESS = 0.00001;
export const SDK_GROWTH_BONUS = 0.01;
export const DEV_ECOSYSTEM_WEIGHTS = {
communitySize: 0.30,
activeRatio: 0.20,
sdkCoverage: 0.20,
docQuality: 0.15,
openSource: 0.15,
};
export const STARTUP_ADOPTION_PER_DEV = 0.005;
export const ENTERPRISE_REFERRAL_PER_STARTUP = 0.01;
// --- Seasonal Demand ---
export const SEASONAL_CYCLE_TICKS = 360;
export const SEASONAL_MULTIPLIERS: Record<SeasonalPhase, Record<string, number>> = {
q1: { consumer: 0.90, api: 0.85, enterprise: 0.75 },
q2: { consumer: 1.00, api: 1.00, enterprise: 1.00 },
q3: { consumer: 0.95, api: 0.95, enterprise: 0.85 },
q4: { consumer: 1.10, api: 1.05, enterprise: 1.40 },
};
// --- Technology Obsolescence ---
export const OBSOLESCENCE_BASELINE_GROWTH = 0.01;
export const OBSOLESCENCE_ERA_ACCELERATOR: Record<Era, number> = {
startup: 1,
scaleup: 1.5,
bigtech: 2.5,
agi: 4,
};
export const FRESHNESS_DECAY_RATE = 0.001;
export const NEW_MODEL_BOOST_VALUE = 0.15;
export const NEW_MODEL_BOOST_TICKS = 200;
export const OBSOLESCENCE_PENALTY_WEIGHT = 0.5;
// --- Competitor Market Behavior ---
export const COMPETITOR_PRODUCT_THRESHOLDS = {
freeTierAndChat: 20,
apiAndCodeAssistant: 40,
proTierAndEnterprise: 60,
agentsPlatform: 80,
};
export const COMPETITOR_CATCHUP_SHARE_THRESHOLD = 0.05;
export const COMPETITOR_CATCHUP_PRICE_CUT = 0.3;
+23
View File
@@ -16,6 +16,13 @@ export interface Competitor {
latestModelName: string;
completedMilestones: string[];
nextMilestoneAtTick: number;
products: CompetitorProducts;
pricingStrategy: CompetitorPricing;
modelFreshness: number;
lastModelReleaseTick: number;
developerEcosystemScore: number;
marketShares: Record<string, number>;
}
export type CompetitorArchetype = 'safety-first' | 'move-fast' | 'big-tech' | 'open-source' | 'stealth-startup';
@@ -29,6 +36,22 @@ export interface CompetitorPersonality {
riskTolerance: number;
}
export interface CompetitorProducts {
hasFreeTier: boolean;
chatPrice: number;
apiInputPrice: number;
apiOutputPrice: number;
hasCodeAssistant: boolean;
codeAssistantPrice: number;
hasAgentsPlatform: boolean;
agentsPlatformPrice: number;
}
export interface CompetitorPricing {
aggressiveness: number;
premiumPositioning: number;
}
export const INITIAL_COMPETITORS: CompetitorState = {
rivals: [],
industryBenchmark: 0,
+1 -1
View File
@@ -58,4 +58,4 @@ export const INITIAL_SETTINGS: GameSettings = {
sfxVolume: 0.7,
};
export const SAVE_VERSION = 6;
export const SAVE_VERSION = 7;
+279 -29
View File
@@ -1,48 +1,177 @@
export interface MarketState {
consumers: ConsumerMarket;
enterprise: EnterpriseMarket;
overloadPolicy: OverloadPolicy;
openSourcedModels: string[];
subscriberHistory: { tick: number; subscribers: number }[];
import type { Era } from './gameState';
// --- Seasonal ---
export type SeasonalPhase = 'q1' | 'q2' | 'q3' | 'q4';
// --- TAM (Total Addressable Market) ---
export type TAMSegmentId = 'consumer' | 'developer' | 'enterprise' | 'government';
export interface TAMSegment {
totalSize: number;
shares: MarketShareEntry[];
}
export interface ConsumerMarket {
totalSubscribers: number;
churnRatePerTick: number;
growthRatePerTick: number;
export interface MarketShareEntry {
playerId: string;
sharePercent: number;
customers: number;
attractivenessScore: number;
}
export interface TotalAddressableMarket {
segments: Record<TAMSegmentId, TAMSegment>;
}
// --- Consumer Tiers ---
export type ConsumerTierId = 'free' | 'plus' | 'pro' | 'team';
export interface ConsumerTierConfig {
id: ConsumerTierId;
name: string;
price: number;
tokenAllowance: number;
requiredModelQuality: number;
isActive: boolean;
}
export interface ConsumerTierRuntime {
config: ConsumerTierConfig;
userCount: number;
conversionRateFromBelow: number;
churnRate: number;
}
export interface ConsumerTierState {
tiers: Record<ConsumerTierId, ConsumerTierRuntime>;
totalUsers: number;
satisfaction: number;
viralCoefficient: number;
}
export interface EnterpriseMarket {
activeContracts: EnterpriseContract[];
pendingRFPs: EnterpriseRFP[];
totalApiCallsPerTick: number;
averageTokensPerCall: number;
// --- API Tiers ---
export type ApiTierId = 'free' | 'payg' | 'scale' | 'enterprise-api';
export interface ApiTierConfig {
id: ApiTierId;
name: string;
monthlyFee: number;
inputTokenPrice: number;
outputTokenPrice: number;
rateLimit: number;
isActive: boolean;
}
export interface ApiTierRuntime {
config: ApiTierConfig;
developerCount: number;
tokensPerTick: number;
churnRate: number;
}
export interface ApiTierState {
tiers: Record<ApiTierId, ApiTierRuntime>;
totalDevelopers: number;
totalTokensPerTick: number;
}
// --- New Product Lines ---
export interface CodeAssistantState {
isUnlocked: boolean;
isActive: boolean;
pricePerSeat: number;
seats: number;
qualityScore: number;
satisfaction: number;
}
export interface AgentsPlatformState {
isUnlocked: boolean;
isActive: boolean;
pricePerSeat: number;
seats: number;
qualityScore: number;
satisfaction: number;
}
// --- Enterprise Pipeline ---
export type EnterprisePipelineStage = 'lead' | 'qualification' | 'poc' | 'negotiation';
export type EnterpriseSegment = 'startup' | 'mid-market' | 'enterprise' | 'government';
export interface EnterpriseLead {
id: string;
companyName: string;
segment: EnterpriseSegment;
stage: EnterprisePipelineStage;
enteredStageAtTick: number;
dealValue: number;
tokensPerTick: number;
requiredCapability: number;
requiredSlaUptime: number;
requiredSafetyScore: number;
winProbability: number;
expiresAtTick: number;
}
export interface EnterpriseContract {
id: string;
customerName: string;
segment: 'startup' | 'mid-market' | 'enterprise' | 'government';
segment: EnterpriseSegment;
tokensPerTick: number;
pricePerMToken: number;
slaUptime: number;
startTick: number;
durationTicks: number;
satisfaction: number;
renewalProbability: number;
slaViolations: number;
slaPenaltiesPaid: number;
uptimeTicks: number;
totalTicks: number;
}
export interface EnterpriseRFP {
id: string;
customerName: string;
segment: 'startup' | 'mid-market' | 'enterprise' | 'government';
requiredCapability: number;
offeredPricePerMToken: number;
requiredSlaUptime: number;
expiresAtTick: number;
export interface EnterpriseState {
pipeline: EnterpriseLead[];
activeContracts: EnterpriseContract[];
totalApiCallsPerTick: number;
averageTokensPerCall: number;
leadGenerationRate: number;
}
// --- Developer Ecosystem ---
export interface DeveloperEcosystem {
communitySize: number;
activeDevelopers: number;
sdkCoverage: number;
documentationQuality: number;
openSourceContributions: number;
devRelSpending: number;
communityGrowthRate: number;
ecosystemScore: number;
startupsAdopted: number;
enterpriseReferrals: number;
}
// --- Technology Obsolescence ---
export interface ObsolescenceState {
marketQualityBaseline: number;
baselineGrowthRate: number;
playerModelFreshness: number;
lastModelReleaseTick: number;
freshnessDecayRate: number;
newModelBoostRemaining: number;
}
// --- Overload Policy (kept from original) ---
export interface OverloadPolicy {
maxQueueDepth: number;
rateLimitPerCustomer: number;
@@ -50,19 +179,140 @@ export interface OverloadPolicy {
prioritizeEnterprise: boolean;
}
// --- Root Market State ---
export interface MarketState {
tam: TotalAddressableMarket;
consumerTiers: ConsumerTierState;
apiTiers: ApiTierState;
codeAssistant: CodeAssistantState;
agentsPlatform: AgentsPlatformState;
enterprise: EnterpriseState;
developerEcosystem: DeveloperEcosystem;
seasonalPhase: SeasonalPhase;
seasonalMultiplier: number;
obsolescence: ObsolescenceState;
overloadPolicy: OverloadPolicy;
openSourcedModels: string[];
subscriberHistory: { tick: number; subscribers: number }[];
}
// --- Initial State ---
function makeInitialTAMSegment(): TAMSegment {
return {
totalSize: 0,
shares: [
{ playerId: 'player', sharePercent: 0, customers: 0, attractivenessScore: 0 },
],
};
}
function makeConsumerTierRuntime(
id: ConsumerTierId,
name: string,
price: number,
tokenAllowance: number,
requiredModelQuality: number,
): ConsumerTierRuntime {
return {
config: { id, name, price, tokenAllowance, requiredModelQuality, isActive: id === 'free' },
userCount: 0,
conversionRateFromBelow: 0,
churnRate: 0,
};
}
function makeApiTierRuntime(
id: ApiTierId,
name: string,
monthlyFee: number,
inputTokenPrice: number,
outputTokenPrice: number,
rateLimit: number,
): ApiTierRuntime {
return {
config: { id, name, monthlyFee, inputTokenPrice, outputTokenPrice, rateLimit, isActive: id === 'free' },
developerCount: 0,
tokensPerTick: 0,
churnRate: 0,
};
}
export const INITIAL_MARKET: MarketState = {
consumers: {
totalSubscribers: 0,
churnRatePerTick: 0.001,
growthRatePerTick: 0,
tam: {
segments: {
consumer: makeInitialTAMSegment(),
developer: makeInitialTAMSegment(),
enterprise: makeInitialTAMSegment(),
government: makeInitialTAMSegment(),
},
},
consumerTiers: {
tiers: {
free: makeConsumerTierRuntime('free', 'Free', 0, 5_000, 0),
plus: makeConsumerTierRuntime('plus', 'Plus', 20, 50_000, 20),
pro: makeConsumerTierRuntime('pro', 'Pro', 50, 200_000, 40),
team: makeConsumerTierRuntime('team', 'Team', 30, 100_000, 30),
},
totalUsers: 0,
satisfaction: 0.5,
viralCoefficient: 0,
},
apiTiers: {
tiers: {
free: makeApiTierRuntime('free', 'Free', 0, 0, 0, 10),
payg: makeApiTierRuntime('payg', 'Pay-as-you-go', 0, 1.0, 3.0, 100),
scale: makeApiTierRuntime('scale', 'Scale', 500, 0.8, 2.4, 1000),
'enterprise-api': makeApiTierRuntime('enterprise-api', 'Enterprise API', 5000, 0.6, 1.8, 10000),
},
totalDevelopers: 0,
totalTokensPerTick: 0,
},
codeAssistant: {
isUnlocked: false,
isActive: false,
pricePerSeat: 20,
seats: 0,
qualityScore: 0,
satisfaction: 0,
},
agentsPlatform: {
isUnlocked: false,
isActive: false,
pricePerSeat: 100,
seats: 0,
qualityScore: 0,
satisfaction: 0,
},
enterprise: {
pipeline: [],
activeContracts: [],
pendingRFPs: [],
totalApiCallsPerTick: 0,
averageTokensPerCall: 500,
leadGenerationRate: 0,
},
developerEcosystem: {
communitySize: 0,
activeDevelopers: 0,
sdkCoverage: 0,
documentationQuality: 0.1,
openSourceContributions: 0,
devRelSpending: 0,
communityGrowthRate: 0,
ecosystemScore: 0,
startupsAdopted: 0,
enterpriseReferrals: 0,
},
seasonalPhase: 'q2',
seasonalMultiplier: 1.0,
obsolescence: {
marketQualityBaseline: 0,
baselineGrowthRate: 0.01,
playerModelFreshness: 0,
lastModelReleaseTick: 0,
freshnessDecayRate: 0.001,
newModelBoostRemaining: 0,
},
overloadPolicy: {
maxQueueDepth: 100,