diff --git a/apps/web/src/components/dev/EventTriggersTab.tsx b/apps/web/src/components/dev/EventTriggersTab.tsx
index 672b4e3..268c6a0 100644
--- a/apps/web/src/components/dev/EventTriggersTab.tsx
+++ b/apps/web/src/components/dev/EventTriggersTab.tsx
@@ -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),
},
},
}));
diff --git a/apps/web/src/components/dev/StateInspectionTab.tsx b/apps/web/src/components/dev/StateInspectionTab.tsx
index 5cc6472..7cab60e 100644
--- a/apps/web/src/components/dev/StateInspectionTab.tsx
+++ b/apps/web/src/components/dev/StateInspectionTab.tsx
@@ -75,10 +75,9 @@ export function StateInspectionTab() {
diff --git a/apps/web/src/components/game/CompanyStatsCard.tsx b/apps/web/src/components/game/CompanyStatsCard.tsx
index d88b0b9..6f871dc 100644
--- a/apps/web/src/components/game/CompanyStatsCard.tsx
+++ b/apps/web/src/components/game/CompanyStatsCard.tsx
@@ -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);
diff --git a/apps/web/src/pages/DashboardPage.tsx b/apps/web/src/pages/DashboardPage.tsx
index c85c8fe..fc7ab4a 100644
--- a/apps/web/src/pages/DashboardPage.tsx
+++ b/apps/web/src/pages/DashboardPage.tsx
@@ -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')}
/>
diff --git a/apps/web/src/pages/FinancePage.tsx b/apps/web/src/pages/FinancePage.tsx
index 4bf2363..b0a32fc 100644
--- a/apps/web/src/pages/FinancePage.tsx
+++ b/apps/web/src/pages/FinancePage.tsx
@@ -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();
diff --git a/apps/web/src/pages/MarketPage.tsx b/apps/web/src/pages/MarketPage.tsx
index b31552d..b78dc1e 100644
--- a/apps/web/src/pages/MarketPage.tsx
+++ b/apps/web/src/pages/MarketPage.tsx
@@ -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 (
-
-
Market
-
-
- Adjust pricing to balance growth and revenue. Watch customer satisfaction — low scores increase churn. High system load means you need more inference capacity.
-
-
-
-
-
-
- Subscribers
-
-
{formatNumber(consumers.totalSubscribers)}
-
- Growth: {formatPercent(consumers.growthRatePerTick)}/s
- {' '}Churn: {formatPercent(consumers.churnRatePerTick)}/s
-
-
-
-
-
- Satisfaction
-
-
{formatPercent(consumers.satisfaction)}
-
-
0.7 ? 'bg-success' : consumers.satisfaction > 0.4 ? 'bg-warning' : 'bg-danger'}`}
- style={{ width: `${consumers.satisfaction * 100}%` }}
- />
-
-
+
+
- Load
+ Inference Load
{formatPercent(inferenceUtil)}
@@ -97,85 +68,18 @@ export function MarketPage() {
-
- Market Saturation
-
-
{formatPercent(saturation)}
-
- Cap: {formatNumber(effectiveCap)} ({currentEra})
-
-
-
0.9 ? 'bg-danger' : saturation > 0.7 ? 'bg-warning' : 'bg-accent'}`}
- style={{ width: `${Math.min(100, saturation * 100)}%` }}
- />
+
+ Subscribers
+
{formatNumber(useGameStore.getState().market.consumerTiers.totalUsers)}
-
-
-
- {chatProduct && (
-
-
-
Chat Product Pricing
-
- {chatProduct.isActive ? 'Active' : 'Inactive'}
-
-
-
-
- Monthly Subscription Price
-
-
- $
- { 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}
- />
- /month
-
-
+
+
+
+ Satisfaction
- )}
-
- {textApi && (
-
-
-
API Pricing
-
- {textApi.isActive ? 'Active' : 'Inactive'}
-
-
-
-
-
Input ($/M tokens)
-
{ 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}
- />
-
-
- Output ($/M tokens)
- { 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}
- />
-
-
-
- )}
+
{formatPercent(useGameStore.getState().market.consumerTiers.satisfaction)}
+
@@ -233,25 +137,44 @@ export function MarketPage() {
-
-
-
Enterprise Contracts
- {enterprise.activeContracts.length === 0 ? (
-
No enterprise contracts yet. Improve your model quality and reputation to attract enterprise customers.
- ) : (
-
- {enterprise.activeContracts.map(c => (
-
-
-
{c.customerName}
-
{formatNumber(c.tokensPerTick)} tok/s · SLA: {formatPercent(c.slaUptime)}
-
-
{formatMoney(c.pricePerMToken)}/M tok
-
- ))}
-
- )}
-
+
+ );
+}
+
+export function MarketPage() {
+ const [activeTab, setActiveTab] = useState
('overview');
+
+ return (
+
+
Market
+
+
+ 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.
+
+
+
+ {TABS.map(tab => (
+ 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}
+
+ ))}
+
+
+ {activeTab === 'overview' &&
}
+ {activeTab === 'consumer' &&
}
+ {activeTab === 'api' &&
}
+ {activeTab === 'enterprise' &&
}
+ {activeTab === 'ecosystem' &&
}
+ {activeTab === 'products' &&
}
+ {activeTab === 'settings' &&
}
);
}
diff --git a/apps/web/src/pages/market/ApiTiersPanel.tsx b/apps/web/src/pages/market/ApiTiersPanel.tsx
new file mode 100644
index 0000000..cd136d9
--- /dev/null
+++ b/apps/web/src/pages/market/ApiTiersPanel.tsx
@@ -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 = {
+ 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>(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 (
+
+
+
+
+ API Tiers
+
+
+ Developers: {formatNumber(apiTiers.totalDevelopers)}
+ Tokens/s: {formatNumber(apiTiers.totalTokensPerTick)}
+
+
+
+
+ {TIER_ORDER.map(tierId => {
+ const tier = apiTiers.tiers[tierId];
+ return (
+
+
+
{tier.config.name}
+ {tierId !== 'free' && (
+ { 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'}
+
+ )}
+ {tierId === 'free' && (
+ Always On
+ )}
+
+
+
{formatNumber(tier.developerCount)}
+
developers
+
+ {tierId !== 'free' ? (
+
+
+
+ Monthly Fee ($)
+ {feedback.show && }
+
+ { 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}
+ />
+
+
+
+ ) : (
+
Free tier attracts developers to your ecosystem
+ )}
+
+
+
+ Rate Limit
+ {formatNumber(tier.config.rateLimit)} req/min
+
+
+ Tokens/s
+ {formatNumber(tier.tokensPerTick)}
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/pages/market/ConsumerTiersPanel.tsx b/apps/web/src/pages/market/ConsumerTiersPanel.tsx
new file mode 100644
index 0000000..fde73de
--- /dev/null
+++ b/apps/web/src/pages/market/ConsumerTiersPanel.tsx
@@ -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 = {
+ 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>(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 (
+
+
+
+
+ Consumer Subscriptions
+
+
+ Total: {formatNumber(consumerTiers.totalUsers)}
+ Satisfaction: 0.7 ? 'text-success' : consumerTiers.satisfaction > 0.4 ? 'text-warning' : 'text-danger'}`}>{formatPercent(consumerTiers.satisfaction)}
+
+
+
+
+ {TIER_ORDER.map(tierId => {
+ const tier = consumerTiers.tiers[tierId];
+ const revenue = tierId === 'free' ? 0 : tier.userCount * tier.config.price / 86400;
+ return (
+
+
+
{tier.config.name}
+ {tierId !== 'free' && (
+ { 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'}
+
+ )}
+ {tierId === 'free' && (
+ Always On
+ )}
+
+
+
{formatNumber(tier.userCount)}
+
users
+
+ {tierId !== 'free' && (
+
+
+ Price ($/mo)
+ {feedback.show && }
+
+ { 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}
+ />
+
+ )}
+
+
+
+ Tokens/mo
+ {formatNumber(tier.config.tokenAllowance)}
+
+
+ Churn
+ {formatPercent(tier.churnRate)}/t
+
+ {tierId !== 'free' && (
+
+ Revenue/s
+ {formatMoney(revenue)}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/web/src/pages/market/DeveloperEcosystemPanel.tsx b/apps/web/src/pages/market/DeveloperEcosystemPanel.tsx
new file mode 100644
index 0000000..2c33c95
--- /dev/null
+++ b/apps/web/src/pages/market/DeveloperEcosystemPanel.tsx
@@ -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>(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 (
+
+
+
+
+ Developer Ecosystem
+
+
+ Ecosystem Score: {devEco.ecosystemScore.toFixed(1)} /100
+
+
+
+
+ {metrics.map(m => (
+
+
{m.label}
+
{m.value}
+
{m.sub}
+
+ ))}
+
+
+
+
+
Flywheel Effects
+
+
+ Startups using your API
+ {formatNumber(devEco.startupsAdopted)}
+
+
+ Enterprise referrals generated
+ {formatNumber(devEco.enterpriseReferrals)}
+
+
+ Open-source contributions
+ {devEco.openSourceContributions}
+
+
+
+ Free tier generosity attracts devs, who build startups on your API, which generates enterprise referrals.
+
+
+
+
+
+ Dev-Rel Budget
+ {feedback.show && Applied }
+
+
+
+ Spending ($/tick)
+ { 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}
+ />
+
+
+
Dev-rel spending improves documentation quality and community engagement.
+
Current cost: {formatMoney(devEco.devRelSpending)}/s
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/market/EnterprisePipelinePanel.tsx b/apps/web/src/pages/market/EnterprisePipelinePanel.tsx
new file mode 100644
index 0000000..0fc38dc
--- /dev/null
+++ b/apps/web/src/pages/market/EnterprisePipelinePanel.tsx
@@ -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 = {
+ lead: 'Leads',
+ qualification: 'Qualification',
+ poc: 'POC',
+ negotiation: 'Negotiation',
+};
+const STAGE_COLORS: Record = {
+ lead: 'bg-surface-600',
+ qualification: 'bg-blue-600',
+ poc: 'bg-purple-600',
+ negotiation: 'bg-orange-600',
+};
+
+const SEGMENT_BADGES: Record = {
+ 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 (
+
+
+
+
+ Enterprise Pipeline
+
+
+ Leads: {enterprise.pipeline.length}
+ Contracts: {enterprise.activeContracts.length}
+ Lead Rate: {enterprise.leadGenerationRate.toFixed(3)}/t
+
+
+
+
+ {leadsByStage.map(({ stage, leads }) => (
+
+
+
+
{STAGE_LABELS[stage]}
+
{leads.length}
+
+
+ {leads.length === 0 && (
+
No leads
+ )}
+ {leads.map(lead => (
+
+
+ {lead.companyName}
+
+ {SEGMENT_BADGES[lead.segment].label}
+
+
+
+ {formatMoney(lead.dealValue)}/yr
+ Win: {formatPercent(lead.winProbability)}
+
+
+ ))}
+
+
+ ))}
+
+
+
+
+
Active Contracts
+ {totalSlaViolations > 0 && (
+
+ {totalSlaViolations} SLA violations
+
+ )}
+
+ {enterprise.activeContracts.length === 0 ? (
+
No active contracts. Build your sales team and model quality to attract enterprise customers.
+ ) : (
+
+
+
+
+ Customer
+ Segment
+ Tokens/s
+ $/M tok
+ SLA
+ Satisfaction
+ Remaining
+ Renewal
+
+
+
+ {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 (
+
+ {c.customerName}
+
+
+ {SEGMENT_BADGES[c.segment].label}
+
+
+ {formatNumber(c.tokensPerTick)}
+ {formatMoney(c.pricePerMToken)}
+
+ = c.slaUptime ? 'text-success' : 'text-danger'}>
+ {formatPercent(uptime)}
+
+
+ {formatPercent(c.satisfaction)}
+ {formatNumber(remaining)}t
+ {formatPercent(c.renewalProbability)}
+
+ );
+ })}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/pages/market/MarketOverviewPanel.tsx b/apps/web/src/pages/market/MarketOverviewPanel.tsx
new file mode 100644
index 0000000..55170ab
--- /dev/null
+++ b/apps/web/src/pages/market/MarketOverviewPanel.tsx
@@ -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 = {
+ consumer: 'Consumer',
+ developer: 'Developer',
+ enterprise: 'Enterprise',
+ government: 'Government',
+};
+
+const SEGMENT_COLORS: Record = {
+ consumer: 'bg-orange-500',
+ developer: 'bg-blue-500',
+ enterprise: 'bg-purple-500',
+ government: 'bg-green-500',
+};
+
+const SEASON_LABELS: Record = {
+ 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 (
+
+
+
+
+
+ Market Share by Segment
+
+
+ {segments.map(([id, seg]) => {
+ const playerShare = seg.shares.find(s => s.playerId === 'player');
+ const share = playerShare?.sharePercent ?? 0;
+ return (
+
+
+ {SEGMENT_LABELS[id]}
+
+ {formatPercent(share)} · {formatNumber(playerShare?.customers ?? 0)} customers
+
+
+
+ {seg.shares
+ .filter(s => s.sharePercent > 0.001)
+ .sort((a, b) => b.sharePercent - a.sharePercent)
+ .map((s, i) => (
+
+ ))}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+ Season
+
+
{SEASON_LABELS[seasonalPhase] ?? seasonalPhase}
+
+ Demand multiplier: {formatPercent(seasonalMultiplier)}
+
+
+
+
+
+
+ Technology Pressure
+
+
+ Market Quality Baseline
+ {(obsolescence.marketQualityBaseline * 100).toFixed(1)}
+
+
+ Your Best Model
+ {bestScore.toFixed(1)}
+
+
+ Model Freshness
+ {formatPercent(obsolescence.playerModelFreshness)}
+
+ {obsolescence.newModelBoostRemaining > 0 && (
+
New model boost active!
+ )}
+ {bestScore / 100 < obsolescence.marketQualityBaseline && (
+
Below market baseline — losing attractiveness
+ )}
+
+
+
+
+
+
+
+ Competitive Landscape
+
+
+
+
+
+ Competitor
+ Consumer
+ Developer
+ Enterprise
+ Freshness
+ Dev Eco
+
+
+
+
+ You
+
+ {formatPercent(tam.segments.consumer.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
+
+
+ {formatPercent(tam.segments.developer.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
+
+
+ {formatPercent(tam.segments.enterprise.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
+
+
+ {formatPercent(obsolescence.playerModelFreshness)}
+
+ —
+
+ {competitors.filter(r => r.status === 'active').map(r => (
+
+ {r.name}
+ {formatPercent(r.marketShares.consumer)}
+ {formatPercent(r.marketShares.developer)}
+ {formatPercent(r.marketShares.enterprise)}
+ {formatPercent(r.modelFreshness)}
+ {r.developerEcosystemScore.toFixed(0)}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/market/ProductLinesPanel.tsx b/apps/web/src/pages/market/ProductLinesPanel.tsx
new file mode 100644
index 0000000..2dc967b
--- /dev/null
+++ b/apps/web/src/pages/market/ProductLinesPanel.tsx
@@ -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>(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 (
+
+
Product Lines
+
+
+
+
+
+
+
Code Assistant
+
+ {codeAssistant.isUnlocked ? (
+
{ 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'}
+
+ ) : (
+
+ Research Required
+
+ )}
+
+
+ {codeAssistant.isUnlocked ? (
+ <>
+
+
+
Seats
+
{formatNumber(codeAssistant.seats)}
+
+
+
Quality
+
{codeAssistant.qualityScore.toFixed(0)}
+
+
+
Revenue/s
+
+ {formatMoney(codeAssistant.seats * codeAssistant.pricePerSeat / 86400)}
+
+
+
+
+
+ Price ($/seat/mo)
+ {feedback.show && }
+
+ { 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}
+ />
+
+ >
+ ) : (
+
+ Research "Code Assistant Product" to unlock. Requires the code-generation research.
+
+ )}
+
+
+
+
+
+
+
AI Agents Platform
+
+ {agentsPlatform.isUnlocked ? (
+
{ 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'}
+
+ ) : (
+
+ Research Required
+
+ )}
+
+
+ {agentsPlatform.isUnlocked ? (
+ <>
+
+
+
Seats
+
{formatNumber(agentsPlatform.seats)}
+
+
+
Quality
+
{agentsPlatform.qualityScore.toFixed(0)}
+
+
+
Revenue/s
+
+ {formatMoney(agentsPlatform.seats * agentsPlatform.pricePerSeat / 86400)}
+
+
+
+
+
+ Price ($/seat/mo)
+ {feedback.show && }
+
+ { 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}
+ />
+
+ >
+ ) : (
+
+ Research "AI Agents Platform" to unlock. Requires the agentic-architecture research.
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts
index 9c2758e..559a91d 100644
--- a/apps/web/src/store/index.ts
+++ b/apps/web/src/store/index.ts
@@ -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) => 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) => void;
}
@@ -1205,6 +1215,126 @@ export const useGameStore = create()(
};
}),
+ 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 = {};
for (const key of Object.keys(partial) as (keyof GameState)[]) {
@@ -1234,7 +1364,7 @@ export const useGameStore = create()(
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,
diff --git a/packages/game-engine/src/data/competitors.ts b/packages/game-engine/src/data/competitors.ts
index 76d5cca..90f301c 100644
--- a/packages/game-engine/src/data/competitors.ts
+++ b/packages/game-engine/src/data/competitors.ts
@@ -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 },
},
];
diff --git a/packages/game-engine/src/data/enterpriseNames.ts b/packages/game-engine/src/data/enterpriseNames.ts
new file mode 100644
index 0000000..d108d5d
--- /dev/null
+++ b/packages/game-engine/src/data/enterpriseNames.ts
@@ -0,0 +1,31 @@
+import type { EnterpriseSegment } from '@ai-tycoon/shared';
+
+export const ENTERPRISE_NAMES: Record = {
+ 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',
+ ],
+};
diff --git a/packages/game-engine/src/data/techTree.ts b/packages/game-engine/src/data/techTree.ts
index c2eb182..6997b87 100644
--- a/packages/game-engine/src/data/techTree.ts
+++ b/packages/game-engine/src/data/techTree.ts
@@ -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',
diff --git a/packages/game-engine/src/systems/competitorSystem.ts b/packages/game-engine/src/systems/competitorSystem.ts
index ee5f7fe..c1a39a4 100644
--- a/packages/game-engine/src/systems/competitorSystem.ts
+++ b/packages/game-engine/src/systems/competitorSystem.ts
@@ -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 = [
diff --git a/packages/game-engine/src/systems/dataSystem.ts b/packages/game-engine/src/systems/dataSystem.ts
index c1a03e8..a0835a4 100644
--- a/packages/game-engine/src/systems/dataSystem.ts
+++ b/packages/game-engine/src/systems/dataSystem.ts
@@ -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);
diff --git a/packages/game-engine/src/systems/economySystem.ts b/packages/game-engine/src/systems/economySystem.ts
index 21387ce..6bbdfbe 100644
--- a/packages/game-engine/src/systems/economySystem.ts
+++ b/packages/game-engine/src/systems/economySystem.ts
@@ -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;
diff --git a/packages/game-engine/src/systems/fundingSystem.ts b/packages/game-engine/src/systems/fundingSystem.ts
index 943ea05..ca43ebf 100644
--- a/packages/game-engine/src/systems/fundingSystem.ts
+++ b/packages/game-engine/src/systems/fundingSystem.ts
@@ -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);
}
diff --git a/packages/game-engine/src/systems/market/apiTierSystem.ts b/packages/game-engine/src/systems/market/apiTierSystem.ts
new file mode 100644
index 0000000..8370e4d
--- /dev/null
+++ b/packages/game-engine/src/systems/market/apiTierSystem.ts
@@ -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 = {
+ 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,
+ };
+}
diff --git a/packages/game-engine/src/systems/market/consumerTierSystem.ts b/packages/game-engine/src/systems/market/consumerTierSystem.ts
new file mode 100644
index 0000000..3e79e48
--- /dev/null
+++ b/packages/game-engine/src/systems/market/consumerTierSystem.ts
@@ -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 = {
+ 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,
+ };
+}
diff --git a/packages/game-engine/src/systems/market/developerEcosystem.ts b/packages/game-engine/src/systems/market/developerEcosystem.ts
new file mode 100644
index 0000000..e690b69
--- /dev/null
+++ b/packages/game-engine/src/systems/market/developerEcosystem.ts
@@ -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;
+}
diff --git a/packages/game-engine/src/systems/market/enterprisePipeline.ts b/packages/game-engine/src/systems/market/enterprisePipeline.ts
new file mode 100644
index 0000000..71e520a
--- /dev/null
+++ b/packages/game-engine/src/systems/market/enterprisePipeline.ts
@@ -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 {
+ 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 = {
+ 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,
+ };
+}
diff --git a/packages/game-engine/src/systems/market/index.ts b/packages/game-engine/src/systems/market/index.ts
new file mode 100644
index 0000000..29775e5
--- /dev/null
+++ b/packages/game-engine/src/systems/market/index.ts
@@ -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();
+ 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,
+ };
+}
diff --git a/packages/game-engine/src/systems/market/obsolescenceSystem.ts b/packages/game-engine/src/systems/market/obsolescenceSystem.ts
new file mode 100644
index 0000000..c56fc87
--- /dev/null
+++ b/packages/game-engine/src/systems/market/obsolescenceSystem.ts
@@ -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,
+ };
+}
diff --git a/packages/game-engine/src/systems/market/productLines.ts b/packages/game-engine/src/systems/market/productLines.ts
new file mode 100644
index 0000000..2286d6d
--- /dev/null
+++ b/packages/game-engine/src/systems/market/productLines.ts
@@ -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,
+ };
+}
diff --git a/packages/game-engine/src/systems/market/seasonalSystem.ts b/packages/game-engine/src/systems/market/seasonalSystem.ts
new file mode 100644
index 0000000..85e764e
--- /dev/null
+++ b/packages/game-engine/src/systems/market/seasonalSystem.ts
@@ -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,
+ },
+ };
+}
diff --git a/packages/game-engine/src/systems/market/tamSystem.ts b/packages/game-engine/src/systems/market/tamSystem.ts
new file mode 100644
index 0000000..0da53b0
--- /dev/null
+++ b/packages/game-engine/src/systems/market/tamSystem.ts
@@ -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();
+ 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;
+
+ 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 };
+}
diff --git a/packages/game-engine/src/systems/marketSystem.ts b/packages/game-engine/src/systems/marketSystem.ts
index 3fc9ccf..e357c84 100644
--- a/packages/game-engine/src/systems/marketSystem.ts
+++ b/packages/game-engine/src/systems/marketSystem.ts
@@ -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();
- 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);
}
diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts
index 2ec1a98..5f61a97 100644
--- a/packages/shared/src/constants/gameBalance.ts
+++ b/packages/shared/src/constants/gameBalance.ts
@@ -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> = {
+ 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> = {
+ 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 = {
+ 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 = {
+ 'free->plus': 0.002,
+ 'plus->pro': 0.0008,
+ 'pro->team': 0.0003,
+};
+
+export const TIER_CHURN_RATES: Record = {
+ 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 = {
+ 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 = {
+ free: 0.0003,
+ payg: 0.001,
+ scale: 0.0005,
+ 'enterprise-api': 0.0003,
+};
+
+export const API_CONVERSION_RATES: Record = {
+ 'free->payg': 0.003,
+ 'payg->scale': 0.001,
+ 'scale->enterprise-api': 0.0004,
+};
+
+export const API_TOKENS_PER_DEVELOPER_PER_TICK: Record = {
+ 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 = {
+ lead: 300,
+ qualification: 200,
+ poc: 400,
+ negotiation: 300,
+};
+
+export const PIPELINE_TRANSITION_RATES: Record = {
+ '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 = {
+ 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 = {
+ startup: 0.95,
+ 'mid-market': 0.97,
+ enterprise: 0.99,
+ government: 0.995,
+};
+
+export const ENTERPRISE_CAPABILITY_REQUIREMENTS: Record = {
+ startup: 15,
+ 'mid-market': 30,
+ enterprise: 50,
+ government: 45,
+};
+
+export const ENTERPRISE_TOKENS_PER_TICK: Record = {
+ 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 = {
+ 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> = {
+ 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 = {
+ 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;
diff --git a/packages/shared/src/types/competitors.ts b/packages/shared/src/types/competitors.ts
index 1c30918..a68daed 100644
--- a/packages/shared/src/types/competitors.ts
+++ b/packages/shared/src/types/competitors.ts
@@ -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;
}
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,
diff --git a/packages/shared/src/types/gameState.ts b/packages/shared/src/types/gameState.ts
index e12afb0..671ae09 100644
--- a/packages/shared/src/types/gameState.ts
+++ b/packages/shared/src/types/gameState.ts
@@ -58,4 +58,4 @@ export const INITIAL_SETTINGS: GameSettings = {
sfxVolume: 0.7,
};
-export const SAVE_VERSION = 6;
+export const SAVE_VERSION = 7;
diff --git a/packages/shared/src/types/market.ts b/packages/shared/src/types/market.ts
index e8f508f..c061ada 100644
--- a/packages/shared/src/types/market.ts
+++ b/packages/shared/src/types/market.ts
@@ -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;
+}
+
+// --- 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;
+ 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;
+ 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,