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'} - -
-
- -
- $ - { 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'} - -
-
-
- - { 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} - /> -
-
- - { 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 => ( + + ))} +
+ + {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' && ( + + )} + {tierId === 'free' && ( + Always On + )} +
+ +
{formatNumber(tier.developerCount)}
+
developers
+ + {tierId !== 'free' ? ( +
+
+ + { 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} + /> +
+
+
+ + { setApiTierPrice(tierId, 'inputTokenPrice', Number(e.target.value)); feedback.trigger(); }} + className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50" + min={0} + step={0.1} + /> +
+
+ + { setApiTierPrice(tierId, 'outputTokenPrice', Number(e.target.value)); feedback.trigger(); }} + className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50" + min={0} + step={0.1} + /> +
+
+
+ ) : ( +
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' && ( + + )} + {tierId === 'free' && ( + Always On + )} +
+ +
{formatNumber(tier.userCount)}
+
users
+ + {tierId !== 'free' && ( +
+ + { 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} +

+
+
+ + { 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.

+ ) : ( +
+ + + + + + + + + + + + + + + {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 ( + + + + + + + + + + + ); + })} + +
CustomerSegmentTokens/s$/M tokSLASatisfactionRemainingRenewal
{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 +
+
+ + + + + + + + + + + + + + + + + + + + + {competitors.filter(r => r.status === 'active').map(r => ( + + + + + + + + + ))} + +
CompetitorConsumerDeveloperEnterpriseFreshnessDev 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)} +
{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 ? ( + + ) : ( + + Research Required + + )} +
+ + {codeAssistant.isUnlocked ? ( + <> +
+
+
Seats
+
{formatNumber(codeAssistant.seats)}
+
+
+
Quality
+
{codeAssistant.qualityScore.toFixed(0)}
+
+
+
Revenue/s
+
+ {formatMoney(codeAssistant.seats * codeAssistant.pricePerSeat / 86400)} +
+
+
+
+ + { 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 ? ( + + ) : ( + + Research Required + + )} +
+ + {agentsPlatform.isUnlocked ? ( + <> +
+
+
Seats
+
{formatNumber(agentsPlatform.seats)}
+
+
+
Quality
+
{agentsPlatform.qualityScore.toFixed(0)}
+
+
+
Revenue/s
+
+ {formatMoney(agentsPlatform.seats * agentsPlatform.pricePerSeat / 86400)} +
+
+
+
+ + { 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,