Overhaul market system with shared TAM competition, multi-tier pricing, enterprise pipeline, and developer ecosystem
CI / build-and-push (push) Successful in 42s
CI / build-and-push (push) Successful in 42s
Replaces the simplified single-subscriber market with a full competitive simulation: shared TAM with softmax market shares across 4 segments, multi-tier consumer subscriptions (Free/Plus/Pro/Team) and API tiers (Free/PAYG/Scale/Enterprise), enterprise sales pipeline (Lead→Qualification→POC→Negotiation→Active→Renewal) with SLA tracking, developer ecosystem flywheel, technology obsolescence pressure, seasonal demand cycles, and two new product lines (Code Assistant, AI Agents Platform). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,9 +90,9 @@ function triggerMarketBoom(multiplier: number) {
|
|||||||
useGameStore.setState((s) => ({
|
useGameStore.setState((s) => ({
|
||||||
market: {
|
market: {
|
||||||
...s.market,
|
...s.market,
|
||||||
consumers: {
|
consumerTiers: {
|
||||||
...s.market.consumers,
|
...s.market.consumerTiers,
|
||||||
totalSubscribers: Math.round(s.market.consumers.totalSubscribers * multiplier),
|
totalUsers: Math.round(s.market.consumerTiers.totalUsers * multiplier),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -75,10 +75,9 @@ export function StateInspectionTab() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Market">
|
<Section title="Market">
|
||||||
<Stat label="Subscribers" value={formatNumber(market.consumers.totalSubscribers)} />
|
<Stat label="Subscribers" value={formatNumber(market.consumerTiers.totalUsers)} />
|
||||||
<Stat label="Satisfaction" value={formatPercent(market.consumers.satisfaction)} />
|
<Stat label="Satisfaction" value={formatPercent(market.consumerTiers.satisfaction)} />
|
||||||
<Stat label="Growth/tick" value={market.consumers.growthRatePerTick.toFixed(4)} />
|
<Stat label="Viral Coeff" value={market.consumerTiers.viralCoefficient.toFixed(4)} />
|
||||||
<Stat label="Churn/tick" value={market.consumers.churnRatePerTick.toFixed(4)} />
|
|
||||||
<Stat label="Contracts" value={market.enterprise.activeContracts.length} />
|
<Stat label="Contracts" value={market.enterprise.activeContracts.length} />
|
||||||
<Stat label="API tok/tick" value={formatNumber(market.enterprise.totalApiCallsPerTick)} />
|
<Stat label="API tok/tick" value={formatNumber(market.enterprise.totalApiCallsPerTick)} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function CompanyStatsCard({ onClose }: { onClose: () => void }) {
|
|||||||
const money = useGameStore((s) => s.economy.money);
|
const money = useGameStore((s) => s.economy.money);
|
||||||
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
|
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
|
||||||
const valuation = useGameStore((s) => s.economy.funding.valuation);
|
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 models = useGameStore((s) => s.models.baseModels.length);
|
||||||
const bestModel = useGameStore((s) => s.models.bestDeployedModelScore);
|
const bestModel = useGameStore((s) => s.models.bestDeployedModelScore);
|
||||||
const reputation = useGameStore((s) => s.reputation.score);
|
const reputation = useGameStore((s) => s.reputation.score);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export function DashboardPage() {
|
|||||||
const totalDCs = useGameStore((s) => s.infrastructure.totalDataCenterCount);
|
const totalDCs = useGameStore((s) => s.infrastructure.totalDataCenterCount);
|
||||||
const baseModels = useGameStore((s) => s.models.baseModels);
|
const baseModels = useGameStore((s) => s.models.baseModels);
|
||||||
const activePipelines = useGameStore((s) => s.models.activeTrainingPipelines);
|
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 reputation = useGameStore((s) => s.reputation.score);
|
||||||
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
|
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
|
||||||
const financialHistory = useGameStore((s) => s.economy.financialHistory);
|
const financialHistory = useGameStore((s) => s.economy.financialHistory);
|
||||||
@@ -75,7 +75,7 @@ export function DashboardPage() {
|
|||||||
icon={Users}
|
icon={Users}
|
||||||
label="Subscribers"
|
label="Subscribers"
|
||||||
value={formatNumber(subscribers)}
|
value={formatNumber(subscribers)}
|
||||||
subValue={`Satisfaction: ${formatPercent(useGameStore.getState().market.consumers.satisfaction)}`}
|
subValue={`Satisfaction: ${formatPercent(useGameStore.getState().market.consumerTiers.satisfaction)}`}
|
||||||
color="text-orange-400"
|
color="text-orange-400"
|
||||||
onClick={() => useGameStore.getState().setActivePage('market')}
|
onClick={() => useGameStore.getState().setActivePage('market')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export function FinancePage() {
|
|||||||
const talent = useGameStore((s) => s.talent);
|
const talent = useGameStore((s) => s.talent);
|
||||||
const raiseFunding = useGameStore((s) => s.raiseFunding);
|
const raiseFunding = useGameStore((s) => s.raiseFunding);
|
||||||
const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
|
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 reputationScore = useGameStore((s) => s.reputation.score);
|
||||||
|
|
||||||
const state = useGameStore.getState();
|
const state = useGameStore.getState();
|
||||||
|
|||||||
@@ -2,10 +2,27 @@ import { useState, useEffect, useRef, useCallback } from 'react';
|
|||||||
import { useGameStore } from '@/store';
|
import { useGameStore } from '@/store';
|
||||||
import {
|
import {
|
||||||
formatNumber, formatMoney, formatPercent,
|
formatNumber, formatMoney, formatPercent,
|
||||||
MARKET_SIZE_CAP, MARKET_CAP_QUALITY_BONUS, MARKET_CAP_REPUTATION_BONUS,
|
|
||||||
} from '@ai-tycoon/shared';
|
} 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 { 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() {
|
function useAppliedFeedback() {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
@@ -28,67 +45,21 @@ function AppliedBadge({ visible }: { visible: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarketPage() {
|
function SettingsPanel() {
|
||||||
const consumers = useGameStore((s) => s.market.consumers);
|
|
||||||
const enterprise = useGameStore((s) => s.market.enterprise);
|
|
||||||
const overloadPolicy = useGameStore((s) => s.market.overloadPolicy);
|
const overloadPolicy = useGameStore((s) => s.market.overloadPolicy);
|
||||||
const productLines = useGameStore((s) => s.models.productLines);
|
|
||||||
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
|
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
|
||||||
const tokensCapacity = useGameStore((s) => s.compute.tokensPerSecondCapacity);
|
const tokensCapacity = useGameStore((s) => s.compute.tokensPerSecondCapacity);
|
||||||
const tokensDemand = useGameStore((s) => s.compute.tokensPerSecondDemand);
|
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 setOverloadPolicy = useGameStore((s) => s.setOverloadPolicy);
|
||||||
const pricingFeedback = useAppliedFeedback();
|
|
||||||
const policyFeedback = 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
<h2 className="text-2xl font-bold">Market</h2>
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
|
||||||
<TutorialHint id="market-intro">
|
|
||||||
Adjust pricing to balance growth and revenue. Watch customer satisfaction — low scores increase churn. High system load means you need more inference capacity.
|
|
||||||
</TutorialHint>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-4">
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Users size={16} className="text-orange-400" />
|
|
||||||
<span className="text-xs text-surface-400 uppercase">Subscribers</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold font-mono">{formatNumber(consumers.totalSubscribers)}</div>
|
|
||||||
<div className="text-xs text-surface-400 mt-1">
|
|
||||||
Growth: {formatPercent(consumers.growthRatePerTick)}/s
|
|
||||||
{' '}Churn: {formatPercent(consumers.churnRatePerTick)}/s
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Shield size={16} className="text-green-400" />
|
|
||||||
<span className="text-xs text-surface-400 uppercase">Satisfaction</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold font-mono">{formatPercent(consumers.satisfaction)}</div>
|
|
||||||
<div className="h-1.5 bg-surface-800 rounded-full mt-2 overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={`h-full rounded-full ${consumers.satisfaction > 0.7 ? 'bg-success' : consumers.satisfaction > 0.4 ? 'bg-warning' : 'bg-danger'}`}
|
|
||||||
style={{ width: `${consumers.satisfaction * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Zap size={16} className="text-blue-400" />
|
<Zap size={16} className="text-blue-400" />
|
||||||
<span className="text-xs text-surface-400 uppercase">Load</span>
|
<span className="text-xs text-surface-400 uppercase">Inference Load</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold font-mono">{formatPercent(inferenceUtil)}</div>
|
<div className="text-2xl font-bold font-mono">{formatPercent(inferenceUtil)}</div>
|
||||||
<div className="text-xs text-surface-400 mt-1">
|
<div className="text-xs text-surface-400 mt-1">
|
||||||
@@ -97,87 +68,20 @@ export function MarketPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<TrendingUp size={16} className="text-purple-400" />
|
<Users size={16} className="text-orange-400" />
|
||||||
<span className="text-xs text-surface-400 uppercase">Market Saturation</span>
|
<span className="text-xs text-surface-400 uppercase">Subscribers</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-2xl font-bold font-mono">{formatPercent(saturation)}</div>
|
<div className="text-2xl font-bold font-mono">{formatNumber(useGameStore.getState().market.consumerTiers.totalUsers)}</div>
|
||||||
<div className="text-xs text-surface-400 mt-1">
|
|
||||||
Cap: {formatNumber(effectiveCap)} ({currentEra})
|
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 bg-surface-800 rounded-full mt-2 overflow-hidden">
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
<div
|
<div className="flex items-center gap-2 mb-2">
|
||||||
className={`h-full rounded-full ${saturation > 0.9 ? 'bg-danger' : saturation > 0.7 ? 'bg-warning' : 'bg-accent'}`}
|
<Shield size={16} className="text-green-400" />
|
||||||
style={{ width: `${Math.min(100, saturation * 100)}%` }}
|
<span className="text-xs text-surface-400 uppercase">Satisfaction</span>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-2xl font-bold font-mono">{formatPercent(useGameStore.getState().market.consumerTiers.satisfaction)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
{chatProduct && (
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="font-semibold">Chat Product Pricing</h3>
|
|
||||||
<span className={`text-xs px-2 py-1 rounded-full ${chatProduct.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}>
|
|
||||||
{chatProduct.isActive ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-surface-400 mb-1">
|
|
||||||
Monthly Subscription Price <AppliedBadge visible={pricingFeedback.show} />
|
|
||||||
</label>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm">$</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={chatProduct.pricing.subscriptionPrice}
|
|
||||||
onChange={(e) => { setProductPricing(chatProduct.id, 'subscriptionPrice', Number(e.target.value)); pricingFeedback.trigger(); }}
|
|
||||||
className="w-24 bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
|
||||||
min={0}
|
|
||||||
step={5}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-surface-400">/month</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{textApi && (
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="font-semibold">API Pricing</h3>
|
|
||||||
<span className={`text-xs px-2 py-1 rounded-full ${textApi.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}>
|
|
||||||
{textApi.isActive ? 'Active' : 'Inactive'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-3">
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-surface-400 mb-1">Input ($/M tokens) <AppliedBadge visible={pricingFeedback.show} /></label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={textApi.pricing.inputTokenPrice}
|
|
||||||
onChange={(e) => { setProductPricing(textApi.id, 'inputTokenPrice', Number(e.target.value)); pricingFeedback.trigger(); }}
|
|
||||||
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
|
||||||
min={0}
|
|
||||||
step={0.5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs text-surface-400 mb-1">Output ($/M tokens)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
value={textApi.pricing.outputTokenPrice}
|
|
||||||
onChange={(e) => { setProductPricing(textApi.id, 'outputTokenPrice', Number(e.target.value)); pricingFeedback.trigger(); }}
|
|
||||||
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
|
||||||
min={0}
|
|
||||||
step={0.5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
|
||||||
<h3 className="font-semibold flex items-center gap-2">
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
<Settings2 size={16} />
|
<Settings2 size={16} />
|
||||||
@@ -233,25 +137,44 @@ export function MarketPage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3">
|
);
|
||||||
<h3 className="font-semibold">Enterprise Contracts</h3>
|
}
|
||||||
{enterprise.activeContracts.length === 0 ? (
|
|
||||||
<p className="text-sm text-surface-500">No enterprise contracts yet. Improve your model quality and reputation to attract enterprise customers.</p>
|
export function MarketPage() {
|
||||||
) : (
|
const [activeTab, setActiveTab] = useState<MarketTab>('overview');
|
||||||
<div className="space-y-2">
|
|
||||||
{enterprise.activeContracts.map(c => (
|
return (
|
||||||
<div key={c.id} className="bg-surface-800 rounded-lg p-3 flex items-center justify-between">
|
<div className="space-y-4">
|
||||||
<div>
|
<h2 className="text-2xl font-bold">Market</h2>
|
||||||
<div className="text-sm font-medium">{c.customerName}</div>
|
|
||||||
<div className="text-xs text-surface-400">{formatNumber(c.tokensPerTick)} tok/s · SLA: {formatPercent(c.slaUptime)}</div>
|
<TutorialHint id="market-intro">
|
||||||
</div>
|
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.
|
||||||
<div className="text-sm font-mono text-success">{formatMoney(c.pricePerMToken)}/M tok</div>
|
</TutorialHint>
|
||||||
</div>
|
|
||||||
))}
|
<div className="flex gap-1 border-b border-surface-700 pb-px">
|
||||||
</div>
|
{TABS.map(tab => (
|
||||||
)}
|
<button
|
||||||
</div>
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`px-4 py-2 text-sm rounded-t-lg transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'bg-surface-800 text-surface-100 border-b-2 border-accent'
|
||||||
|
: 'text-surface-400 hover:text-surface-200 hover:bg-surface-800/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeTab === 'overview' && <MarketOverviewPanel />}
|
||||||
|
{activeTab === 'consumer' && <ConsumerTiersPanel />}
|
||||||
|
{activeTab === 'api' && <ApiTiersPanel />}
|
||||||
|
{activeTab === 'enterprise' && <EnterprisePipelinePanel />}
|
||||||
|
{activeTab === 'ecosystem' && <DeveloperEcosystemPanel />}
|
||||||
|
{activeTab === 'products' && <ProductLinesPanel />}
|
||||||
|
{activeTab === 'settings' && <SettingsPanel />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useGameStore } from '@/store';
|
||||||
|
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
||||||
|
import type { ApiTierId } from '@ai-tycoon/shared';
|
||||||
|
import { Code, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
const TIER_ORDER: ApiTierId[] = ['free', 'payg', 'scale', 'enterprise-api'];
|
||||||
|
const TIER_COLORS: Record<ApiTierId, string> = {
|
||||||
|
free: 'border-surface-500',
|
||||||
|
payg: 'border-green-500',
|
||||||
|
scale: 'border-blue-500',
|
||||||
|
'enterprise-api': 'border-purple-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
function useAppliedFeedback() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const trigger = useCallback(() => {
|
||||||
|
setShow(true);
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => setShow(false), 1200);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => clearTimeout(timerRef.current), []);
|
||||||
|
return { show, trigger };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiTiersPanel() {
|
||||||
|
const apiTiers = useGameStore((s) => s.market.apiTiers);
|
||||||
|
const setApiTierPrice = useGameStore((s) => s.setApiTierPrice);
|
||||||
|
const toggleApiTier = useGameStore((s) => s.toggleApiTier);
|
||||||
|
const feedback = useAppliedFeedback();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Code size={16} className="text-blue-400" />
|
||||||
|
<span className="text-sm font-semibold">API Tiers</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-surface-400">
|
||||||
|
<span>Developers: <span className="font-mono text-surface-200">{formatNumber(apiTiers.totalDevelopers)}</span></span>
|
||||||
|
<span>Tokens/s: <span className="font-mono text-surface-200">{formatNumber(apiTiers.totalTokensPerTick)}</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{TIER_ORDER.map(tierId => {
|
||||||
|
const tier = apiTiers.tiers[tierId];
|
||||||
|
return (
|
||||||
|
<div key={tierId} className={`bg-surface-900 border-t-2 ${TIER_COLORS[tierId]} border border-surface-700 rounded-xl p-4 space-y-3`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-semibold text-sm">{tier.config.name}</h4>
|
||||||
|
{tierId !== 'free' && (
|
||||||
|
<button
|
||||||
|
onClick={() => { toggleApiTier(tierId); feedback.trigger(); }}
|
||||||
|
className={`text-[10px] px-2 py-0.5 rounded-full ${tier.config.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
|
||||||
|
>
|
||||||
|
{tier.config.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{tierId === 'free' && (
|
||||||
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-surface-700 text-surface-400">Always On</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-2xl font-bold font-mono">{formatNumber(tier.developerCount)}</div>
|
||||||
|
<div className="text-xs text-surface-400">developers</div>
|
||||||
|
|
||||||
|
{tierId !== 'free' ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-surface-400 mb-1">
|
||||||
|
Monthly Fee ($)
|
||||||
|
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={tier.config.monthlyFee}
|
||||||
|
onChange={(e) => { setApiTierPrice(tierId, 'monthlyFee', Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={0}
|
||||||
|
step={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-surface-400 mb-1">In $/M</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={tier.config.inputTokenPrice}
|
||||||
|
onChange={(e) => { setApiTierPrice(tierId, 'inputTokenPrice', Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-surface-400 mb-1">Out $/M</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={tier.config.outputTokenPrice}
|
||||||
|
onChange={(e) => { setApiTierPrice(tierId, 'outputTokenPrice', Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-surface-500">Free tier attracts developers to your ecosystem</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-xs space-y-1 text-surface-400">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Rate Limit</span>
|
||||||
|
<span className="font-mono">{formatNumber(tier.config.rateLimit)} req/min</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Tokens/s</span>
|
||||||
|
<span className="font-mono">{formatNumber(tier.tokensPerTick)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useGameStore } from '@/store';
|
||||||
|
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
||||||
|
import type { ConsumerTierId } from '@ai-tycoon/shared';
|
||||||
|
import { Users, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
const TIER_ORDER: ConsumerTierId[] = ['free', 'plus', 'pro', 'team'];
|
||||||
|
const TIER_COLORS: Record<ConsumerTierId, string> = {
|
||||||
|
free: 'border-surface-500',
|
||||||
|
plus: 'border-blue-500',
|
||||||
|
pro: 'border-purple-500',
|
||||||
|
team: 'border-orange-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
function useAppliedFeedback() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const trigger = useCallback(() => {
|
||||||
|
setShow(true);
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => setShow(false), 1200);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => clearTimeout(timerRef.current), []);
|
||||||
|
return { show, trigger };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConsumerTiersPanel() {
|
||||||
|
const consumerTiers = useGameStore((s) => s.market.consumerTiers);
|
||||||
|
const setConsumerTierPrice = useGameStore((s) => s.setConsumerTierPrice);
|
||||||
|
const toggleConsumerTier = useGameStore((s) => s.toggleConsumerTier);
|
||||||
|
const feedback = useAppliedFeedback();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users size={16} className="text-orange-400" />
|
||||||
|
<span className="text-sm font-semibold">Consumer Subscriptions</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-surface-400">
|
||||||
|
<span>Total: <span className="font-mono text-surface-200">{formatNumber(consumerTiers.totalUsers)}</span></span>
|
||||||
|
<span>Satisfaction: <span className={`font-mono ${consumerTiers.satisfaction > 0.7 ? 'text-success' : consumerTiers.satisfaction > 0.4 ? 'text-warning' : 'text-danger'}`}>{formatPercent(consumerTiers.satisfaction)}</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{TIER_ORDER.map(tierId => {
|
||||||
|
const tier = consumerTiers.tiers[tierId];
|
||||||
|
const revenue = tierId === 'free' ? 0 : tier.userCount * tier.config.price / 86400;
|
||||||
|
return (
|
||||||
|
<div key={tierId} className={`bg-surface-900 border-t-2 ${TIER_COLORS[tierId]} border border-surface-700 rounded-xl p-4 space-y-3`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-semibold text-sm">{tier.config.name}</h4>
|
||||||
|
{tierId !== 'free' && (
|
||||||
|
<button
|
||||||
|
onClick={() => { toggleConsumerTier(tierId); feedback.trigger(); }}
|
||||||
|
className={`text-[10px] px-2 py-0.5 rounded-full ${tier.config.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
|
||||||
|
>
|
||||||
|
{tier.config.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{tierId === 'free' && (
|
||||||
|
<span className="text-[10px] px-2 py-0.5 rounded-full bg-surface-700 text-surface-400">Always On</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-2xl font-bold font-mono">{formatNumber(tier.userCount)}</div>
|
||||||
|
<div className="text-xs text-surface-400">users</div>
|
||||||
|
|
||||||
|
{tierId !== 'free' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-surface-400 mb-1">
|
||||||
|
Price ($/mo)
|
||||||
|
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={tier.config.price}
|
||||||
|
onChange={(e) => { setConsumerTierPrice(tierId, Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={1}
|
||||||
|
step={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="text-xs space-y-1 text-surface-400">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Tokens/mo</span>
|
||||||
|
<span className="font-mono">{formatNumber(tier.config.tokenAllowance)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Churn</span>
|
||||||
|
<span className="font-mono">{formatPercent(tier.churnRate)}/t</span>
|
||||||
|
</div>
|
||||||
|
{tierId !== 'free' && (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span>Revenue/s</span>
|
||||||
|
<span className="font-mono text-success">{formatMoney(revenue)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useGameStore } from '@/store';
|
||||||
|
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
||||||
|
import { Boxes, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
function useAppliedFeedback() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const trigger = useCallback(() => {
|
||||||
|
setShow(true);
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => setShow(false), 1200);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => clearTimeout(timerRef.current), []);
|
||||||
|
return { show, trigger };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeveloperEcosystemPanel() {
|
||||||
|
const devEco = useGameStore((s) => s.market.developerEcosystem);
|
||||||
|
const setDevRelSpending = useGameStore((s) => s.setDevRelSpending);
|
||||||
|
const feedback = useAppliedFeedback();
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{ label: 'Community Size', value: formatNumber(devEco.communitySize), sub: `Growth: ${devEco.communityGrowthRate.toFixed(2)}/t` },
|
||||||
|
{ label: 'Active Developers', value: formatNumber(devEco.activeDevelopers), sub: `${devEco.communitySize > 0 ? formatPercent(devEco.activeDevelopers / devEco.communitySize) : '0%'} active` },
|
||||||
|
{ label: 'SDK Coverage', value: formatPercent(devEco.sdkCoverage), sub: 'Hire engineers to improve' },
|
||||||
|
{ label: 'Doc Quality', value: formatPercent(devEco.documentationQuality), sub: 'Increase dev-rel spending' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Boxes size={16} className="text-green-400" />
|
||||||
|
<span className="text-sm font-semibold">Developer Ecosystem</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-surface-400">
|
||||||
|
Ecosystem Score: <span className="font-mono text-surface-200 text-sm">{devEco.ecosystemScore.toFixed(1)}</span>/100
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{metrics.map(m => (
|
||||||
|
<div key={m.label} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase mb-1">{m.label}</div>
|
||||||
|
<div className="text-lg font-bold font-mono">{m.value}</div>
|
||||||
|
<div className="text-[10px] text-surface-500 mt-0.5">{m.sub}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<h4 className="text-sm font-semibold mb-3">Flywheel Effects</h4>
|
||||||
|
<div className="space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between text-surface-300">
|
||||||
|
<span>Startups using your API</span>
|
||||||
|
<span className="font-mono">{formatNumber(devEco.startupsAdopted)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-surface-300">
|
||||||
|
<span>Enterprise referrals generated</span>
|
||||||
|
<span className="font-mono">{formatNumber(devEco.enterpriseReferrals)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-surface-300">
|
||||||
|
<span>Open-source contributions</span>
|
||||||
|
<span className="font-mono">{devEco.openSourceContributions}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 p-2 bg-surface-800 rounded text-[10px] text-surface-400">
|
||||||
|
Free tier generosity attracts devs, who build startups on your API, which generates enterprise referrals.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<h4 className="text-sm font-semibold mb-3">
|
||||||
|
Dev-Rel Budget
|
||||||
|
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-2 animate-pulse text-[10px]"><Check size={8} /> Applied</span>}
|
||||||
|
</h4>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-surface-400 mb-1">Spending ($/tick)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={devEco.devRelSpending}
|
||||||
|
onChange={(e) => { setDevRelSpending(Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-surface-500 space-y-1">
|
||||||
|
<p>Dev-rel spending improves documentation quality and community engagement.</p>
|
||||||
|
<p>Current cost: <span className="font-mono text-surface-300">{formatMoney(devEco.devRelSpending)}/s</span></p>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-surface-800 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-green-500"
|
||||||
|
style={{ width: `${Math.min(100, devEco.ecosystemScore)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import { useGameStore } from '@/store';
|
||||||
|
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
||||||
|
import type { EnterprisePipelineStage, EnterpriseSegment } from '@ai-tycoon/shared';
|
||||||
|
import { Building2, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
const STAGE_ORDER: EnterprisePipelineStage[] = ['lead', 'qualification', 'poc', 'negotiation'];
|
||||||
|
const STAGE_LABELS: Record<EnterprisePipelineStage, string> = {
|
||||||
|
lead: 'Leads',
|
||||||
|
qualification: 'Qualification',
|
||||||
|
poc: 'POC',
|
||||||
|
negotiation: 'Negotiation',
|
||||||
|
};
|
||||||
|
const STAGE_COLORS: Record<EnterprisePipelineStage, string> = {
|
||||||
|
lead: 'bg-surface-600',
|
||||||
|
qualification: 'bg-blue-600',
|
||||||
|
poc: 'bg-purple-600',
|
||||||
|
negotiation: 'bg-orange-600',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEGMENT_BADGES: Record<EnterpriseSegment, { label: string; color: string }> = {
|
||||||
|
startup: { label: 'Startup', color: 'bg-green-500/20 text-green-400' },
|
||||||
|
'mid-market': { label: 'Mid-Market', color: 'bg-blue-500/20 text-blue-400' },
|
||||||
|
enterprise: { label: 'Enterprise', color: 'bg-purple-500/20 text-purple-400' },
|
||||||
|
government: { label: 'Gov', color: 'bg-yellow-500/20 text-yellow-400' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EnterprisePipelinePanel() {
|
||||||
|
const enterprise = useGameStore((s) => s.market.enterprise);
|
||||||
|
const tickCount = useGameStore((s) => s.meta.tickCount);
|
||||||
|
|
||||||
|
const leadsByStage = STAGE_ORDER.map(stage => ({
|
||||||
|
stage,
|
||||||
|
leads: enterprise.pipeline.filter(l => l.stage === stage),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalContractValue = enterprise.activeContracts.reduce((sum, c) => sum + c.pricePerMToken * c.tokensPerTick, 0);
|
||||||
|
const totalSlaViolations = enterprise.activeContracts.reduce((sum, c) => sum + c.slaViolations, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 size={16} className="text-purple-400" />
|
||||||
|
<span className="text-sm font-semibold">Enterprise Pipeline</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-xs text-surface-400">
|
||||||
|
<span>Leads: <span className="font-mono text-surface-200">{enterprise.pipeline.length}</span></span>
|
||||||
|
<span>Contracts: <span className="font-mono text-surface-200">{enterprise.activeContracts.length}</span></span>
|
||||||
|
<span>Lead Rate: <span className="font-mono text-surface-200">{enterprise.leadGenerationRate.toFixed(3)}/t</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-4 gap-3">
|
||||||
|
{leadsByStage.map(({ stage, leads }) => (
|
||||||
|
<div key={stage} className="bg-surface-900 border border-surface-700 rounded-xl p-3">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className={`w-2 h-2 rounded-full ${STAGE_COLORS[stage]}`} />
|
||||||
|
<span className="text-xs font-semibold">{STAGE_LABELS[stage]}</span>
|
||||||
|
<span className="text-xs text-surface-500 ml-auto">{leads.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 max-h-40 overflow-y-auto">
|
||||||
|
{leads.length === 0 && (
|
||||||
|
<div className="text-xs text-surface-600 italic">No leads</div>
|
||||||
|
)}
|
||||||
|
{leads.map(lead => (
|
||||||
|
<div key={lead.id} className="bg-surface-800 rounded px-2 py-1.5 text-xs">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium text-surface-200 truncate">{lead.companyName}</span>
|
||||||
|
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${SEGMENT_BADGES[lead.segment].color}`}>
|
||||||
|
{SEGMENT_BADGES[lead.segment].label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-surface-400 mt-0.5">
|
||||||
|
<span>{formatMoney(lead.dealValue)}/yr</span>
|
||||||
|
<span>Win: {formatPercent(lead.winProbability)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-sm font-semibold">Active Contracts</span>
|
||||||
|
{totalSlaViolations > 0 && (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-warning">
|
||||||
|
<AlertTriangle size={12} /> {totalSlaViolations} SLA violations
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{enterprise.activeContracts.length === 0 ? (
|
||||||
|
<p className="text-xs text-surface-500">No active contracts. Build your sales team and model quality to attract enterprise customers.</p>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-surface-400 border-b border-surface-700">
|
||||||
|
<th className="text-left py-1.5 pr-3">Customer</th>
|
||||||
|
<th className="text-left py-1.5 px-2">Segment</th>
|
||||||
|
<th className="text-right py-1.5 px-2">Tokens/s</th>
|
||||||
|
<th className="text-right py-1.5 px-2">$/M tok</th>
|
||||||
|
<th className="text-right py-1.5 px-2">SLA</th>
|
||||||
|
<th className="text-right py-1.5 px-2">Satisfaction</th>
|
||||||
|
<th className="text-right py-1.5 px-2">Remaining</th>
|
||||||
|
<th className="text-right py-1.5 pl-2">Renewal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{enterprise.activeContracts.map(c => {
|
||||||
|
const remaining = Math.max(0, c.startTick + c.durationTicks - tickCount);
|
||||||
|
const uptime = c.totalTicks > 0 ? c.uptimeTicks / c.totalTicks : 1;
|
||||||
|
return (
|
||||||
|
<tr key={c.id} className="border-b border-surface-800">
|
||||||
|
<td className="py-1.5 pr-3 font-medium text-surface-200">{c.customerName}</td>
|
||||||
|
<td className="py-1.5 px-2">
|
||||||
|
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${SEGMENT_BADGES[c.segment].color}`}>
|
||||||
|
{SEGMENT_BADGES[c.segment].label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-right py-1.5 px-2 font-mono">{formatNumber(c.tokensPerTick)}</td>
|
||||||
|
<td className="text-right py-1.5 px-2 font-mono text-success">{formatMoney(c.pricePerMToken)}</td>
|
||||||
|
<td className="text-right py-1.5 px-2 font-mono">
|
||||||
|
<span className={uptime >= c.slaUptime ? 'text-success' : 'text-danger'}>
|
||||||
|
{formatPercent(uptime)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-right py-1.5 px-2 font-mono">{formatPercent(c.satisfaction)}</td>
|
||||||
|
<td className="text-right py-1.5 px-2 font-mono">{formatNumber(remaining)}t</td>
|
||||||
|
<td className="text-right py-1.5 pl-2 font-mono">{formatPercent(c.renewalProbability)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { useGameStore } from '@/store';
|
||||||
|
import { formatNumber, formatPercent } from '@ai-tycoon/shared';
|
||||||
|
import type { TAMSegmentId } from '@ai-tycoon/shared';
|
||||||
|
import { Globe, TrendingUp, Clock, Thermometer } from 'lucide-react';
|
||||||
|
|
||||||
|
const SEGMENT_LABELS: Record<TAMSegmentId, string> = {
|
||||||
|
consumer: 'Consumer',
|
||||||
|
developer: 'Developer',
|
||||||
|
enterprise: 'Enterprise',
|
||||||
|
government: 'Government',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEGMENT_COLORS: Record<TAMSegmentId, string> = {
|
||||||
|
consumer: 'bg-orange-500',
|
||||||
|
developer: 'bg-blue-500',
|
||||||
|
enterprise: 'bg-purple-500',
|
||||||
|
government: 'bg-green-500',
|
||||||
|
};
|
||||||
|
|
||||||
|
const SEASON_LABELS: Record<string, string> = {
|
||||||
|
q1: 'Q1 — Slow Start',
|
||||||
|
q2: 'Q2 — Baseline',
|
||||||
|
q3: 'Q3 — Summer Dip',
|
||||||
|
q4: 'Q4 — Budget Surge',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MarketOverviewPanel() {
|
||||||
|
const tam = useGameStore((s) => s.market.tam);
|
||||||
|
const seasonalPhase = useGameStore((s) => s.market.seasonalPhase);
|
||||||
|
const seasonalMultiplier = useGameStore((s) => s.market.seasonalMultiplier);
|
||||||
|
const obsolescence = useGameStore((s) => s.market.obsolescence);
|
||||||
|
const bestScore = useGameStore((s) => s.models.bestDeployedModelScore);
|
||||||
|
const competitors = useGameStore((s) => s.competitors.rivals);
|
||||||
|
|
||||||
|
const segments = Object.entries(tam.segments) as [TAMSegmentId, typeof tam.segments.consumer][];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Globe size={16} className="text-accent" />
|
||||||
|
<span className="text-sm font-semibold">Market Share by Segment</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{segments.map(([id, seg]) => {
|
||||||
|
const playerShare = seg.shares.find(s => s.playerId === 'player');
|
||||||
|
const share = playerShare?.sharePercent ?? 0;
|
||||||
|
return (
|
||||||
|
<div key={id}>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-surface-300">{SEGMENT_LABELS[id]}</span>
|
||||||
|
<span className="font-mono">
|
||||||
|
{formatPercent(share)} · {formatNumber(playerShare?.customers ?? 0)} customers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-surface-800 rounded-full overflow-hidden flex">
|
||||||
|
{seg.shares
|
||||||
|
.filter(s => s.sharePercent > 0.001)
|
||||||
|
.sort((a, b) => b.sharePercent - a.sharePercent)
|
||||||
|
.map((s, i) => (
|
||||||
|
<div
|
||||||
|
key={s.playerId}
|
||||||
|
className={`h-full ${s.playerId === 'player' ? SEGMENT_COLORS[id] : `bg-surface-${600 - i * 100}`}`}
|
||||||
|
style={{ width: `${s.sharePercent * 100}%` }}
|
||||||
|
title={`${s.playerId}: ${formatPercent(s.sharePercent)}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Clock size={16} className="text-yellow-400" />
|
||||||
|
<span className="text-sm font-semibold">Season</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold">{SEASON_LABELS[seasonalPhase] ?? seasonalPhase}</div>
|
||||||
|
<div className="text-xs text-surface-400 mt-1">
|
||||||
|
Demand multiplier: {formatPercent(seasonalMultiplier)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Thermometer size={16} className="text-red-400" />
|
||||||
|
<span className="text-sm font-semibold">Technology Pressure</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-surface-400">Market Quality Baseline</span>
|
||||||
|
<span className="font-mono">{(obsolescence.marketQualityBaseline * 100).toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-surface-400">Your Best Model</span>
|
||||||
|
<span className="font-mono">{bestScore.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs mb-1">
|
||||||
|
<span className="text-surface-400">Model Freshness</span>
|
||||||
|
<span className="font-mono">{formatPercent(obsolescence.playerModelFreshness)}</span>
|
||||||
|
</div>
|
||||||
|
{obsolescence.newModelBoostRemaining > 0 && (
|
||||||
|
<div className="text-xs text-success mt-1">New model boost active!</div>
|
||||||
|
)}
|
||||||
|
{bestScore / 100 < obsolescence.marketQualityBaseline && (
|
||||||
|
<div className="text-xs text-danger mt-1">Below market baseline — losing attractiveness</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<TrendingUp size={16} className="text-purple-400" />
|
||||||
|
<span className="text-sm font-semibold">Competitive Landscape</span>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-surface-400 border-b border-surface-700">
|
||||||
|
<th className="text-left py-2 pr-4">Competitor</th>
|
||||||
|
<th className="text-right py-2 px-2">Consumer</th>
|
||||||
|
<th className="text-right py-2 px-2">Developer</th>
|
||||||
|
<th className="text-right py-2 px-2">Enterprise</th>
|
||||||
|
<th className="text-right py-2 px-2">Freshness</th>
|
||||||
|
<th className="text-right py-2 pl-2">Dev Eco</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="border-b border-surface-800 text-accent">
|
||||||
|
<td className="py-2 pr-4 font-medium">You</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">
|
||||||
|
{formatPercent(tam.segments.consumer.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">
|
||||||
|
{formatPercent(tam.segments.developer.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">
|
||||||
|
{formatPercent(tam.segments.enterprise.shares.find(s => s.playerId === 'player')?.sharePercent ?? 0)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">
|
||||||
|
{formatPercent(obsolescence.playerModelFreshness)}
|
||||||
|
</td>
|
||||||
|
<td className="text-right py-2 pl-2 font-mono">—</td>
|
||||||
|
</tr>
|
||||||
|
{competitors.filter(r => r.status === 'active').map(r => (
|
||||||
|
<tr key={r.id} className="border-b border-surface-800">
|
||||||
|
<td className="py-2 pr-4 font-medium text-surface-300">{r.name}</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.marketShares.consumer)}</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.marketShares.developer)}</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.marketShares.enterprise)}</td>
|
||||||
|
<td className="text-right py-2 px-2 font-mono">{formatPercent(r.modelFreshness)}</td>
|
||||||
|
<td className="text-right py-2 pl-2 font-mono">{r.developerEcosystemScore.toFixed(0)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useGameStore } from '@/store';
|
||||||
|
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
|
||||||
|
import { Wrench, Bot, Check, Lock } from 'lucide-react';
|
||||||
|
|
||||||
|
function useAppliedFeedback() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
const trigger = useCallback(() => {
|
||||||
|
setShow(true);
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = setTimeout(() => setShow(false), 1200);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => () => clearTimeout(timerRef.current), []);
|
||||||
|
return { show, trigger };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProductLinesPanel() {
|
||||||
|
const codeAssistant = useGameStore((s) => s.market.codeAssistant);
|
||||||
|
const agentsPlatform = useGameStore((s) => s.market.agentsPlatform);
|
||||||
|
const setCodeAssistantPrice = useGameStore((s) => s.setCodeAssistantPrice);
|
||||||
|
const toggleCodeAssistant = useGameStore((s) => s.toggleCodeAssistant);
|
||||||
|
const setAgentsPlatformPrice = useGameStore((s) => s.setAgentsPlatformPrice);
|
||||||
|
const toggleAgentsPlatform = useGameStore((s) => s.toggleAgentsPlatform);
|
||||||
|
const feedback = useAppliedFeedback();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<span className="text-sm font-semibold">Product Lines</span>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className={`bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3 ${!codeAssistant.isUnlocked ? 'opacity-60' : ''}`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wrench size={16} className="text-cyan-400" />
|
||||||
|
<h4 className="font-semibold text-sm">Code Assistant</h4>
|
||||||
|
</div>
|
||||||
|
{codeAssistant.isUnlocked ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { toggleCodeAssistant(); feedback.trigger(); }}
|
||||||
|
className={`text-[10px] px-2 py-0.5 rounded-full ${codeAssistant.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
|
||||||
|
>
|
||||||
|
{codeAssistant.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1 text-[10px] text-surface-500">
|
||||||
|
<Lock size={10} /> Research Required
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{codeAssistant.isUnlocked ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase">Seats</div>
|
||||||
|
<div className="text-lg font-bold font-mono">{formatNumber(codeAssistant.seats)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase">Quality</div>
|
||||||
|
<div className="text-lg font-bold font-mono">{codeAssistant.qualityScore.toFixed(0)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase">Revenue/s</div>
|
||||||
|
<div className="text-lg font-bold font-mono text-success">
|
||||||
|
{formatMoney(codeAssistant.seats * codeAssistant.pricePerSeat / 86400)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-surface-400 mb-1">
|
||||||
|
Price ($/seat/mo)
|
||||||
|
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={codeAssistant.pricePerSeat}
|
||||||
|
onChange={(e) => { setCodeAssistantPrice(Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={5}
|
||||||
|
step={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-surface-500">
|
||||||
|
Research "Code Assistant Product" to unlock. Requires the code-generation research.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`bg-surface-900 border border-surface-700 rounded-xl p-4 space-y-3 ${!agentsPlatform.isUnlocked ? 'opacity-60' : ''}`}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Bot size={16} className="text-orange-400" />
|
||||||
|
<h4 className="font-semibold text-sm">AI Agents Platform</h4>
|
||||||
|
</div>
|
||||||
|
{agentsPlatform.isUnlocked ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { toggleAgentsPlatform(); feedback.trigger(); }}
|
||||||
|
className={`text-[10px] px-2 py-0.5 rounded-full ${agentsPlatform.isActive ? 'bg-success/20 text-success' : 'bg-surface-700 text-surface-400'}`}
|
||||||
|
>
|
||||||
|
{agentsPlatform.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1 text-[10px] text-surface-500">
|
||||||
|
<Lock size={10} /> Research Required
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agentsPlatform.isUnlocked ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase">Seats</div>
|
||||||
|
<div className="text-lg font-bold font-mono">{formatNumber(agentsPlatform.seats)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase">Quality</div>
|
||||||
|
<div className="text-lg font-bold font-mono">{agentsPlatform.qualityScore.toFixed(0)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-[10px] text-surface-400 uppercase">Revenue/s</div>
|
||||||
|
<div className="text-lg font-bold font-mono text-success">
|
||||||
|
{formatMoney(agentsPlatform.seats * agentsPlatform.pricePerSeat / 86400)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] text-surface-400 mb-1">
|
||||||
|
Price ($/seat/mo)
|
||||||
|
{feedback.show && <span className="inline-flex items-center gap-0.5 text-success ml-1 animate-pulse"><Check size={8} /></span>}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={agentsPlatform.pricePerSeat}
|
||||||
|
onChange={(e) => { setAgentsPlatformPrice(Number(e.target.value)); feedback.trigger(); }}
|
||||||
|
className="w-full bg-surface-800 border border-surface-600 rounded px-2 py-1 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||||
|
min={10}
|
||||||
|
step={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-surface-500">
|
||||||
|
Research "AI Agents Platform" to unlock. Requires the agentic-architecture research.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+131
-1
@@ -16,6 +16,7 @@ import type {
|
|||||||
ModelArchitecture,
|
ModelArchitecture,
|
||||||
SFTSpecialization, QuantizationLevel, VariantCreationJob,
|
SFTSpecialization, QuantizationLevel, VariantCreationJob,
|
||||||
EvalJob,
|
EvalJob,
|
||||||
|
ConsumerTierId, ApiTierId,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
INITIAL_SETTINGS, SAVE_VERSION,
|
INITIAL_SETTINGS, SAVE_VERSION,
|
||||||
@@ -125,6 +126,15 @@ interface Actions {
|
|||||||
openSourceModel: (modelId: string) => void;
|
openSourceModel: (modelId: string) => void;
|
||||||
setOverloadPolicy: (policy: Partial<OverloadPolicy>) => void;
|
setOverloadPolicy: (policy: Partial<OverloadPolicy>) => void;
|
||||||
acquireCompetitor: (competitorId: string) => void;
|
acquireCompetitor: (competitorId: string) => void;
|
||||||
|
setConsumerTierPrice: (tierId: ConsumerTierId, price: number) => void;
|
||||||
|
toggleConsumerTier: (tierId: ConsumerTierId) => void;
|
||||||
|
setApiTierPrice: (tierId: ApiTierId, field: 'monthlyFee' | 'inputTokenPrice' | 'outputTokenPrice', value: number) => void;
|
||||||
|
toggleApiTier: (tierId: ApiTierId) => void;
|
||||||
|
setDevRelSpending: (amount: number) => void;
|
||||||
|
setCodeAssistantPrice: (price: number) => void;
|
||||||
|
toggleCodeAssistant: () => void;
|
||||||
|
setAgentsPlatformPrice: (price: number) => void;
|
||||||
|
toggleAgentsPlatform: () => void;
|
||||||
updateState: (partial: Partial<GameState>) => void;
|
updateState: (partial: Partial<GameState>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1205,6 +1215,126 @@ export const useGameStore = create<Store>()(
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
setConsumerTierPrice: (tierId, price) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
consumerTiers: {
|
||||||
|
...s.market.consumerTiers,
|
||||||
|
tiers: {
|
||||||
|
...s.market.consumerTiers.tiers,
|
||||||
|
[tierId]: {
|
||||||
|
...s.market.consumerTiers.tiers[tierId],
|
||||||
|
config: { ...s.market.consumerTiers.tiers[tierId].config, price },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
toggleConsumerTier: (tierId) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
consumerTiers: {
|
||||||
|
...s.market.consumerTiers,
|
||||||
|
tiers: {
|
||||||
|
...s.market.consumerTiers.tiers,
|
||||||
|
[tierId]: {
|
||||||
|
...s.market.consumerTiers.tiers[tierId],
|
||||||
|
config: {
|
||||||
|
...s.market.consumerTiers.tiers[tierId].config,
|
||||||
|
isActive: !s.market.consumerTiers.tiers[tierId].config.isActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
setApiTierPrice: (tierId, field, value) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
apiTiers: {
|
||||||
|
...s.market.apiTiers,
|
||||||
|
tiers: {
|
||||||
|
...s.market.apiTiers.tiers,
|
||||||
|
[tierId]: {
|
||||||
|
...s.market.apiTiers.tiers[tierId],
|
||||||
|
config: { ...s.market.apiTiers.tiers[tierId].config, [field]: value },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
toggleApiTier: (tierId) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
apiTiers: {
|
||||||
|
...s.market.apiTiers,
|
||||||
|
tiers: {
|
||||||
|
...s.market.apiTiers.tiers,
|
||||||
|
[tierId]: {
|
||||||
|
...s.market.apiTiers.tiers[tierId],
|
||||||
|
config: {
|
||||||
|
...s.market.apiTiers.tiers[tierId].config,
|
||||||
|
isActive: !s.market.apiTiers.tiers[tierId].config.isActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
setDevRelSpending: (amount) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
developerEcosystem: {
|
||||||
|
...s.market.developerEcosystem,
|
||||||
|
devRelSpending: amount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
setCodeAssistantPrice: (price) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
codeAssistant: {
|
||||||
|
...s.market.codeAssistant,
|
||||||
|
pricePerSeat: price,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
toggleCodeAssistant: () => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
codeAssistant: {
|
||||||
|
...s.market.codeAssistant,
|
||||||
|
isActive: !s.market.codeAssistant.isActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
setAgentsPlatformPrice: (price) => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
agentsPlatform: {
|
||||||
|
...s.market.agentsPlatform,
|
||||||
|
pricePerSeat: price,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
|
toggleAgentsPlatform: () => set((s) => ({
|
||||||
|
market: {
|
||||||
|
...s.market,
|
||||||
|
agentsPlatform: {
|
||||||
|
...s.market.agentsPlatform,
|
||||||
|
isActive: !s.market.agentsPlatform.isActive,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
|
||||||
updateState: (partial) => set((s) => {
|
updateState: (partial) => set((s) => {
|
||||||
const newState: Partial<Store> = {};
|
const newState: Partial<Store> = {};
|
||||||
for (const key of Object.keys(partial) as (keyof GameState)[]) {
|
for (const key of Object.keys(partial) as (keyof GameState)[]) {
|
||||||
@@ -1234,7 +1364,7 @@ export const useGameStore = create<Store>()(
|
|||||||
notifications: [{
|
notifications: [{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
title: 'Save Reset',
|
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,
|
type: 'info' as const,
|
||||||
tick: 0,
|
tick: 0,
|
||||||
read: false,
|
read: false,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import type { Competitor } from '@ai-tycoon/shared';
|
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[] = [
|
export const INITIAL_RIVALS: Competitor[] = [
|
||||||
// ── Safety-first lab (Anthropic parody) ──────────────────────────────
|
|
||||||
{
|
{
|
||||||
id: 'competitor_prometheus',
|
id: 'competitor_prometheus',
|
||||||
name: 'Prometheus AI',
|
name: 'Prometheus AI',
|
||||||
@@ -26,9 +21,23 @@ export const INITIAL_RIVALS: Competitor[] = [
|
|||||||
latestModelName: 'Aegis-1',
|
latestModelName: 'Aegis-1',
|
||||||
completedMilestones: [],
|
completedMilestones: [],
|
||||||
nextMilestoneAtTick: 300,
|
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',
|
id: 'competitor_nexus',
|
||||||
name: 'Nexus Labs',
|
name: 'Nexus Labs',
|
||||||
@@ -49,9 +58,23 @@ export const INITIAL_RIVALS: Competitor[] = [
|
|||||||
latestModelName: 'Blitz-0.9',
|
latestModelName: 'Blitz-0.9',
|
||||||
completedMilestones: [],
|
completedMilestones: [],
|
||||||
nextMilestoneAtTick: 300,
|
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',
|
id: 'competitor_titan',
|
||||||
name: 'Titan Computing',
|
name: 'Titan Computing',
|
||||||
@@ -72,5 +95,20 @@ export const INITIAL_RIVALS: Competitor[] = [
|
|||||||
latestModelName: 'Colossus 2.0',
|
latestModelName: 'Colossus 2.0',
|
||||||
completedMilestones: [],
|
completedMilestones: [],
|
||||||
nextMilestoneAtTick: 300,
|
nextMilestoneAtTick: 300,
|
||||||
|
products: {
|
||||||
|
hasFreeTier: true,
|
||||||
|
chatPrice: 20,
|
||||||
|
apiInputPrice: 1.0,
|
||||||
|
apiOutputPrice: 3.0,
|
||||||
|
hasCodeAssistant: false,
|
||||||
|
codeAssistantPrice: 0,
|
||||||
|
hasAgentsPlatform: false,
|
||||||
|
agentsPlatformPrice: 0,
|
||||||
|
},
|
||||||
|
pricingStrategy: { aggressiveness: 0.5, premiumPositioning: 0.5 },
|
||||||
|
modelFreshness: 0.9,
|
||||||
|
lastModelReleaseTick: 0,
|
||||||
|
developerEcosystemScore: 45,
|
||||||
|
marketShares: { consumer: 0.35, developer: 0.30, enterprise: 0.40, government: 0.50 },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { EnterpriseSegment } from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export const ENTERPRISE_NAMES: Record<EnterpriseSegment, string[]> = {
|
||||||
|
startup: [
|
||||||
|
'PixelForge', 'NovaByte', 'CloudSpark', 'DataLeap', 'VectorVault',
|
||||||
|
'SynthWave AI', 'CodePilot', 'BrightLoop', 'Innova Labs', 'ScaleKit',
|
||||||
|
'DeepRoot', 'FluxPoint', 'MindBridge', 'HyperNode', 'NeuralSeed',
|
||||||
|
'LatticeAI', 'StreamForge', 'QuantumLeaf', 'VeloCity AI', 'ArcStack',
|
||||||
|
],
|
||||||
|
'mid-market': [
|
||||||
|
'Meridian Health', 'Cascade Logistics', 'PrimeRetail Group', 'Atlas Financial',
|
||||||
|
'TerraForm Solutions', 'NorthStar Analytics', 'BridgePoint Media', 'Keystone Legal',
|
||||||
|
'Orion Pharmaceuticals', 'Summit Education', 'ClearView Insurance', 'Horizon Dynamics',
|
||||||
|
'PeakWare Systems', 'CorePath Consulting', 'BlueStar Manufacturing', 'Vertex Commerce',
|
||||||
|
'RiverStone Banking', 'Zenith Telecom', 'IronGate Security', 'ElmBridge Tech',
|
||||||
|
],
|
||||||
|
enterprise: [
|
||||||
|
'GlobalBank Corp', 'TransContinental Airlines', 'MegaRetail Holdings', 'United Telecom',
|
||||||
|
'Pinnacle Pharmaceuticals', 'Imperial Energy', 'Vanguard Insurance', 'Sterling Automotive',
|
||||||
|
'Apex Media Group', 'Continental Healthcare', 'Sovereign Financial', 'Atlas Industries',
|
||||||
|
'Dominion Logistics', 'Pacific Semiconductor', 'NorthAm Rail Systems', 'OceanGate Maritime',
|
||||||
|
'Crown Broadcasting', 'Titan Manufacturing', 'Citadel Consulting', 'Granite Resources',
|
||||||
|
],
|
||||||
|
government: [
|
||||||
|
'Dept. of National Intelligence', 'Federal Research Authority', 'National Health Service AI',
|
||||||
|
'Defense Advanced Projects', 'Central Tax Administration', 'State Education Bureau',
|
||||||
|
'Environmental Protection AI', 'National Cybersecurity Center', 'Federal Aviation Systems',
|
||||||
|
'Dept. of Energy Computing', 'National Weather Service', 'Census Analytics Division',
|
||||||
|
'Judicial Analytics Office', 'Immigration Processing AI', 'Veterans Affairs Digital',
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -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 ===
|
// === DATA ===
|
||||||
{
|
{
|
||||||
id: 'data-pipeline',
|
id: 'data-pipeline',
|
||||||
|
|||||||
@@ -1,44 +1,99 @@
|
|||||||
import type { GameState, CompetitorState } from '@ai-tycoon/shared';
|
import type { GameState, CompetitorState, Competitor } from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
COMPETITOR_PRODUCT_THRESHOLDS,
|
||||||
|
COMPETITOR_CATCHUP_SHARE_THRESHOLD,
|
||||||
|
COMPETITOR_CATCHUP_PRICE_CUT,
|
||||||
|
FRESHNESS_DECAY_RATE,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
function updateCompetitorProducts(rival: Competitor): Competitor['products'] {
|
||||||
|
const cap = rival.estimatedCapability;
|
||||||
|
const p = rival.products;
|
||||||
|
const pricing = rival.pricingStrategy;
|
||||||
|
|
||||||
|
const baseChatPrice = 20 * (1 + pricing.premiumPositioning * 0.5 - pricing.aggressiveness * 0.3);
|
||||||
|
const baseApiOut = 3.0 * (1 + pricing.premiumPositioning * 0.3 - pricing.aggressiveness * 0.4);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasFreeTier: cap >= COMPETITOR_PRODUCT_THRESHOLDS.freeTierAndChat,
|
||||||
|
chatPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.freeTierAndChat
|
||||||
|
? Math.max(5, baseChatPrice) : p.chatPrice,
|
||||||
|
apiInputPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
|
||||||
|
? Math.max(0.2, baseApiOut * 0.33) : p.apiInputPrice,
|
||||||
|
apiOutputPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
|
||||||
|
? Math.max(0.5, baseApiOut) : p.apiOutputPrice,
|
||||||
|
hasCodeAssistant: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant,
|
||||||
|
codeAssistantPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
|
||||||
|
? Math.max(10, 20 * (1 - pricing.aggressiveness * 0.3)) : 0,
|
||||||
|
hasAgentsPlatform: cap >= COMPETITOR_PRODUCT_THRESHOLDS.agentsPlatform,
|
||||||
|
agentsPlatformPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.agentsPlatform
|
||||||
|
? Math.max(50, 100 * (1 - pricing.aggressiveness * 0.2)) : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function processCompetitors(state: GameState): CompetitorState {
|
export function processCompetitors(state: GameState): CompetitorState {
|
||||||
const tick = state.meta.tickCount;
|
const tick = state.meta.tickCount;
|
||||||
const rivals = state.competitors.rivals.map(rival => {
|
const rivals = state.competitors.rivals.map(rival => {
|
||||||
if (rival.status !== 'active') return 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 { personality } = rival;
|
||||||
const capGrowth = (2 + personality.researchFocus * 5 + personality.riskTolerance * 3) *
|
const capGrowth = (2 + personality.researchFocus * 5 + personality.riskTolerance * 3) *
|
||||||
(1 + tick * 0.00005);
|
(1 + tick * 0.00005);
|
||||||
const revenueGrowth = rival.estimatedRevenue * (0.02 + personality.marketingFocus * 0.03);
|
const revenueGrowth = rival.estimatedRevenue * (0.02 + personality.marketingFocus * 0.03);
|
||||||
const userGrowth = rival.estimatedUsers * (0.01 + personality.marketingFocus * 0.02);
|
const userGrowth = rival.estimatedUsers * (0.01 + personality.marketingFocus * 0.02);
|
||||||
|
|
||||||
const newCapability = Math.min(95, rival.estimatedCapability + capGrowth);
|
updated.estimatedCapability = Math.min(95, rival.estimatedCapability + capGrowth);
|
||||||
const newRevenue = rival.estimatedRevenue + revenueGrowth + 50;
|
updated.estimatedRevenue = rival.estimatedRevenue + revenueGrowth + 50;
|
||||||
const newUsers = rival.estimatedUsers + userGrowth + 100;
|
updated.estimatedUsers = Math.floor(rival.estimatedUsers + userGrowth + 100);
|
||||||
|
|
||||||
const repChange = personality.safetyFocus > 0.6
|
const repChange = personality.safetyFocus > 0.6
|
||||||
? 1
|
? 1
|
||||||
: personality.riskTolerance > 0.7 ? -1 : 0;
|
: personality.riskTolerance > 0.7 ? -1 : 0;
|
||||||
|
updated.reputation = Math.min(100, Math.max(0, rival.reputation + repChange));
|
||||||
|
|
||||||
const modelNames = [
|
const modelNames = [
|
||||||
'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',
|
'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',
|
||||||
'Nova', 'Quantum', 'Nexus', 'Apex', 'Zenith',
|
'Nova', 'Quantum', 'Nexus', 'Apex', 'Zenith',
|
||||||
];
|
];
|
||||||
const modelIdx = Math.floor(newCapability / 10);
|
const modelIdx = Math.floor(updated.estimatedCapability / 10);
|
||||||
const latestModelName = `${rival.name.split(' ')[0]}-${modelNames[Math.min(modelIdx, modelNames.length - 1)]}`;
|
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);
|
const milestoneInterval = 200 + Math.floor(Math.random() * 200);
|
||||||
|
updated.nextMilestoneAtTick = tick + milestoneInterval;
|
||||||
|
|
||||||
return {
|
return updated;
|
||||||
...rival,
|
|
||||||
estimatedCapability: newCapability,
|
|
||||||
estimatedRevenue: newRevenue,
|
|
||||||
estimatedUsers: Math.floor(newUsers),
|
|
||||||
reputation: Math.min(100, Math.max(0, rival.reputation + repChange)),
|
|
||||||
latestModelName,
|
|
||||||
nextMilestoneAtTick: tick + milestoneInterval,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const allCaps = [
|
const allCaps = [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { GameState, DataState } from '@ai-tycoon/shared';
|
import type { GameState, DataState } from '@ai-tycoon/shared';
|
||||||
|
|
||||||
export function processData(state: GameState): DataState {
|
export function processData(state: GameState): DataState {
|
||||||
const subscribers = state.market.consumers.totalSubscribers;
|
const subscribers = state.market.consumerTiers.totalUsers;
|
||||||
const userDataRate = subscribers * 0.5;
|
const userDataRate = subscribers * 0.5;
|
||||||
|
|
||||||
const partnershipTokens = state.data.partnerships.reduce((sum, p) => sum + p.tokensPerTick, 0);
|
const partnershipTokens = state.data.partnerships.reduce((sum, p) => sum + p.tokensPerTick, 0);
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ export function processEconomy(
|
|||||||
const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra);
|
const 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 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;
|
const money = state.economy.money + revenue - expenses;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function canRaiseFunding(state: GameState): { canRaise: boolean; nextRoun
|
|||||||
if (reqs.minRevenue && state.economy.totalRevenue < reqs.minRevenue) {
|
if (reqs.minRevenue && state.economy.totalRevenue < reqs.minRevenue) {
|
||||||
return { canRaise: false, nextRound, reason: `Need $${reqs.minRevenue.toLocaleString()} total revenue` };
|
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` };
|
return { canRaise: false, nextRound, reason: `Need ${reqs.minUsers.toLocaleString()} subscribers` };
|
||||||
}
|
}
|
||||||
if (reqs.minReputation && state.reputation.score < reqs.minReputation) {
|
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 {
|
export function computeValuation(state: GameState): number {
|
||||||
const revenueMultiple = state.economy.revenuePerTick * 86400 * 365;
|
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;
|
const capabilityValue = Math.pow(state.models.bestDeployedModelScore, 2) * 1000;
|
||||||
return Math.max(100_000, revenueMultiple * 10 + subscriberValue + capabilityValue);
|
return Math.max(100_000, revenueMultiple * 10 + subscriberValue + capabilityValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import type { ApiTierState, ApiTierId, DeveloperEcosystem } from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
API_TIER_ORDER,
|
||||||
|
API_CONVERSION_RATES,
|
||||||
|
API_TIER_CHURN_RATES,
|
||||||
|
API_TOKENS_PER_DEVELOPER_PER_TICK,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export interface ApiTickResult {
|
||||||
|
apiTiers: ApiTierState;
|
||||||
|
apiRevenue: number;
|
||||||
|
totalApiTokenDemand: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processApiTiers(
|
||||||
|
tiers: ApiTierState,
|
||||||
|
playerDevCustomers: number,
|
||||||
|
modelQuality: number,
|
||||||
|
seasonalApiMultiplier: number,
|
||||||
|
ecosystem: DeveloperEcosystem,
|
||||||
|
): ApiTickResult {
|
||||||
|
const updated: ApiTierState = {
|
||||||
|
tiers: { ...tiers.tiers },
|
||||||
|
totalDevelopers: 0,
|
||||||
|
totalTokensPerTick: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const id of API_TIER_ORDER) {
|
||||||
|
updated.tiers[id] = { ...tiers.tiers[id], config: { ...tiers.tiers[id].config } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelQuality <= 0) {
|
||||||
|
return { apiTiers: updated, apiRevenue: 0, totalApiTokenDemand: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFreeDevelopers = playerDevCustomers * 0.1 * seasonalApiMultiplier;
|
||||||
|
const freeGrowth = (targetFreeDevelopers - updated.tiers.free.developerCount) * 0.03;
|
||||||
|
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount + freeGrowth);
|
||||||
|
const freeChurn = updated.tiers.free.developerCount * API_TIER_CHURN_RATES.free;
|
||||||
|
updated.tiers.free.developerCount = Math.max(0, updated.tiers.free.developerCount - freeChurn);
|
||||||
|
updated.tiers.free.churnRate = API_TIER_CHURN_RATES.free;
|
||||||
|
|
||||||
|
const prevTierMap: Record<ApiTierId, ApiTierId | null> = {
|
||||||
|
free: null,
|
||||||
|
payg: 'free',
|
||||||
|
scale: 'payg',
|
||||||
|
'enterprise-api': 'scale',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const id of API_TIER_ORDER) {
|
||||||
|
if (id === 'free') continue;
|
||||||
|
const tier = updated.tiers[id];
|
||||||
|
if (!tier.config.isActive) continue;
|
||||||
|
|
||||||
|
const prevId = prevTierMap[id];
|
||||||
|
if (!prevId) continue;
|
||||||
|
const prevTier = updated.tiers[prevId];
|
||||||
|
|
||||||
|
const convKey = `${prevId}->${id}`;
|
||||||
|
const baseRate = API_CONVERSION_RATES[convKey] ?? 0;
|
||||||
|
const ecosystemBoost = 1 + ecosystem.ecosystemScore / 200;
|
||||||
|
const convRate = baseRate * Math.max(0.1, modelQuality) * ecosystemBoost * seasonalApiMultiplier;
|
||||||
|
|
||||||
|
const converting = prevTier.developerCount * convRate;
|
||||||
|
prevTier.developerCount = Math.max(0, prevTier.developerCount - converting);
|
||||||
|
tier.developerCount += converting;
|
||||||
|
|
||||||
|
tier.churnRate = API_TIER_CHURN_RATES[id];
|
||||||
|
const churned = tier.developerCount * tier.churnRate;
|
||||||
|
tier.developerCount = Math.max(0, tier.developerCount - churned);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalDevelopers = 0;
|
||||||
|
let totalTokens = 0;
|
||||||
|
let apiRevenue = 0;
|
||||||
|
|
||||||
|
for (const id of API_TIER_ORDER) {
|
||||||
|
const tier = updated.tiers[id];
|
||||||
|
totalDevelopers += tier.developerCount;
|
||||||
|
|
||||||
|
const tokensPerDev = API_TOKENS_PER_DEVELOPER_PER_TICK[id];
|
||||||
|
tier.tokensPerTick = tier.developerCount * tokensPerDev;
|
||||||
|
totalTokens += tier.tokensPerTick;
|
||||||
|
|
||||||
|
apiRevenue += tier.developerCount * (tier.config.monthlyFee / 86400);
|
||||||
|
apiRevenue += (tier.tokensPerTick / 1_000_000) * tier.config.outputTokenPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.totalDevelopers = totalDevelopers;
|
||||||
|
updated.totalTokensPerTick = totalTokens;
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiTiers: updated,
|
||||||
|
apiRevenue: Math.max(0, apiRevenue),
|
||||||
|
totalApiTokenDemand: totalTokens,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import type { ConsumerTierState, ConsumerTierId } from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
CONSUMER_TIER_ORDER,
|
||||||
|
CONVERSION_RATES,
|
||||||
|
TIER_CHURN_RATES,
|
||||||
|
FREE_TIER_ADOPTION_RATE,
|
||||||
|
CONSUMER_TOKENS_PER_SUBSCRIBER,
|
||||||
|
OVERLOAD_PENALTY_EXPONENT,
|
||||||
|
NETWORK_DEGRADATION,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export interface ConsumerTickResult {
|
||||||
|
consumerTiers: ConsumerTierState;
|
||||||
|
subscriptionRevenue: number;
|
||||||
|
totalConsumerTokenDemand: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processConsumerTiers(
|
||||||
|
tiers: ConsumerTierState,
|
||||||
|
playerConsumerCustomers: number,
|
||||||
|
modelQuality: number,
|
||||||
|
seasonalConsumerMultiplier: number,
|
||||||
|
demandCapacityRatio: number,
|
||||||
|
networkLatencyPenalty: number,
|
||||||
|
overloadPolicy: { degradeQualityUnderLoad: boolean; prioritizeEnterprise: boolean },
|
||||||
|
): ConsumerTickResult {
|
||||||
|
const updated = {
|
||||||
|
tiers: { ...tiers.tiers },
|
||||||
|
totalUsers: 0,
|
||||||
|
satisfaction: tiers.satisfaction,
|
||||||
|
viralCoefficient: tiers.viralCoefficient,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const id of CONSUMER_TIER_ORDER) {
|
||||||
|
updated.tiers[id] = { ...tiers.tiers[id], config: { ...tiers.tiers[id].config } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelQuality <= 0) {
|
||||||
|
return { consumerTiers: updated, subscriptionRevenue: 0, totalConsumerTokenDemand: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const qualityFactor = Math.max(0.1, modelQuality);
|
||||||
|
const freeAdoption = playerConsumerCustomers * FREE_TIER_ADOPTION_RATE * seasonalConsumerMultiplier;
|
||||||
|
const targetFreeUsers = Math.max(updated.tiers.free.userCount, freeAdoption);
|
||||||
|
|
||||||
|
const freeGrowth = (targetFreeUsers - updated.tiers.free.userCount) * 0.05;
|
||||||
|
updated.tiers.free.userCount = Math.max(0, updated.tiers.free.userCount + freeGrowth);
|
||||||
|
updated.tiers.free.churnRate = TIER_CHURN_RATES.free;
|
||||||
|
const freeChurn = updated.tiers.free.userCount * TIER_CHURN_RATES.free;
|
||||||
|
updated.tiers.free.userCount = Math.max(0, updated.tiers.free.userCount - freeChurn);
|
||||||
|
|
||||||
|
const prevTierMap: Record<ConsumerTierId, ConsumerTierId | null> = {
|
||||||
|
free: null,
|
||||||
|
plus: 'free',
|
||||||
|
pro: 'plus',
|
||||||
|
team: 'pro',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const id of CONSUMER_TIER_ORDER) {
|
||||||
|
if (id === 'free') continue;
|
||||||
|
const tier = updated.tiers[id];
|
||||||
|
if (!tier.config.isActive) continue;
|
||||||
|
if (modelQuality * 100 < tier.config.requiredModelQuality) continue;
|
||||||
|
|
||||||
|
const prevId = prevTierMap[id];
|
||||||
|
if (!prevId) continue;
|
||||||
|
const prevTier = updated.tiers[prevId];
|
||||||
|
|
||||||
|
const conversionKey = `${prevId}->${id}`;
|
||||||
|
const baseRate = CONVERSION_RATES[conversionKey] ?? 0;
|
||||||
|
const priceAttr = tier.config.price > 0
|
||||||
|
? Math.max(0.1, 1 - tier.config.price / 100)
|
||||||
|
: 1;
|
||||||
|
const convRate = baseRate * qualityFactor * priceAttr * seasonalConsumerMultiplier;
|
||||||
|
|
||||||
|
const converting = prevTier.userCount * convRate;
|
||||||
|
prevTier.userCount = Math.max(0, prevTier.userCount - converting);
|
||||||
|
tier.userCount += converting;
|
||||||
|
tier.conversionRateFromBelow = convRate;
|
||||||
|
|
||||||
|
tier.churnRate = TIER_CHURN_RATES[id];
|
||||||
|
const churnMultiplier = 1 + (1 - updated.satisfaction) * 2;
|
||||||
|
const churned = tier.userCount * tier.churnRate * churnMultiplier;
|
||||||
|
tier.userCount = Math.max(0, tier.userCount - churned);
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalUsers = 0;
|
||||||
|
let subscriptionRevenue = 0;
|
||||||
|
let totalTokenDemand = 0;
|
||||||
|
|
||||||
|
for (const id of CONSUMER_TIER_ORDER) {
|
||||||
|
const tier = updated.tiers[id];
|
||||||
|
totalUsers += tier.userCount;
|
||||||
|
subscriptionRevenue += tier.userCount * (tier.config.price / 86400);
|
||||||
|
totalTokenDemand += tier.userCount * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.totalUsers = totalUsers;
|
||||||
|
|
||||||
|
let headroomBonus = 0;
|
||||||
|
let overloadPenalty = 0;
|
||||||
|
if (demandCapacityRatio <= 1) {
|
||||||
|
headroomBonus = (1 - demandCapacityRatio) * 0.2;
|
||||||
|
} else {
|
||||||
|
overloadPenalty = Math.min(1, Math.pow(demandCapacityRatio - 1, OVERLOAD_PENALTY_EXPONENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
const netLatencyPenalty = networkLatencyPenalty * NETWORK_DEGRADATION.satisfactionPenaltyPerLatency;
|
||||||
|
updated.satisfaction = Math.min(1, Math.max(0,
|
||||||
|
0.3 + modelQuality * 0.5 + headroomBonus - overloadPenalty - netLatencyPenalty,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (overloadPolicy.degradeQualityUnderLoad && demandCapacityRatio > 0.85) {
|
||||||
|
updated.satisfaction = Math.max(0, updated.satisfaction - 0.02);
|
||||||
|
}
|
||||||
|
if (overloadPolicy.prioritizeEnterprise && demandCapacityRatio > 0.9) {
|
||||||
|
updated.satisfaction = Math.max(0, updated.satisfaction - 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.viralCoefficient = modelQuality > 0.5 ? 1 + (modelQuality - 0.5) * 2 : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
consumerTiers: updated,
|
||||||
|
subscriptionRevenue,
|
||||||
|
totalConsumerTokenDemand: totalTokenDemand,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { DeveloperEcosystem } from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
BASE_DEV_GROWTH,
|
||||||
|
FREE_TIER_DEV_MULTIPLIER,
|
||||||
|
OPEN_SOURCE_DEV_BOOST,
|
||||||
|
DEV_REL_EFFECTIVENESS,
|
||||||
|
SDK_GROWTH_BONUS,
|
||||||
|
DEV_ECOSYSTEM_WEIGHTS,
|
||||||
|
STARTUP_ADOPTION_PER_DEV,
|
||||||
|
ENTERPRISE_REFERRAL_PER_STARTUP,
|
||||||
|
TAM_BASE_SIZES,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
import type { Era } from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export function processDeveloperEcosystem(
|
||||||
|
eco: DeveloperEcosystem,
|
||||||
|
openSourceCount: number,
|
||||||
|
apiFreeTierDevs: number,
|
||||||
|
apiTotalDevs: number,
|
||||||
|
engineeringHeadcount: number,
|
||||||
|
era: Era,
|
||||||
|
): DeveloperEcosystem {
|
||||||
|
const updated = { ...eco };
|
||||||
|
|
||||||
|
const growthRate =
|
||||||
|
BASE_DEV_GROWTH +
|
||||||
|
apiFreeTierDevs * FREE_TIER_DEV_MULTIPLIER +
|
||||||
|
openSourceCount * OPEN_SOURCE_DEV_BOOST +
|
||||||
|
updated.devRelSpending * DEV_REL_EFFECTIVENESS +
|
||||||
|
updated.sdkCoverage * SDK_GROWTH_BONUS;
|
||||||
|
|
||||||
|
updated.communityGrowthRate = growthRate;
|
||||||
|
updated.communitySize = Math.max(0, updated.communitySize + updated.communitySize * growthRate);
|
||||||
|
|
||||||
|
if (updated.communitySize < 10 && apiTotalDevs > 0) {
|
||||||
|
updated.communitySize += 1 + apiTotalDevs * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
updated.activeDevelopers = apiTotalDevs;
|
||||||
|
updated.openSourceContributions = openSourceCount;
|
||||||
|
|
||||||
|
const sdkTarget = Math.min(1, engineeringHeadcount / 50);
|
||||||
|
updated.sdkCoverage += (sdkTarget - updated.sdkCoverage) * 0.005;
|
||||||
|
updated.sdkCoverage = Math.min(1, Math.max(0, updated.sdkCoverage));
|
||||||
|
|
||||||
|
const docTarget = Math.min(1, updated.devRelSpending / 500);
|
||||||
|
updated.documentationQuality += (docTarget - updated.documentationQuality) * 0.003;
|
||||||
|
updated.documentationQuality = Math.min(1, Math.max(0, updated.documentationQuality));
|
||||||
|
|
||||||
|
const eraCap = TAM_BASE_SIZES[era].developer;
|
||||||
|
const communityNorm = Math.min(1, updated.communitySize / Math.max(1, eraCap * 0.1));
|
||||||
|
const activeRatio = updated.communitySize > 0
|
||||||
|
? Math.min(1, updated.activeDevelopers / updated.communitySize)
|
||||||
|
: 0;
|
||||||
|
const osNorm = Math.min(1, openSourceCount / 5);
|
||||||
|
|
||||||
|
updated.ecosystemScore = (
|
||||||
|
DEV_ECOSYSTEM_WEIGHTS.communitySize * communityNorm +
|
||||||
|
DEV_ECOSYSTEM_WEIGHTS.activeRatio * activeRatio +
|
||||||
|
DEV_ECOSYSTEM_WEIGHTS.sdkCoverage * updated.sdkCoverage +
|
||||||
|
DEV_ECOSYSTEM_WEIGHTS.docQuality * updated.documentationQuality +
|
||||||
|
DEV_ECOSYSTEM_WEIGHTS.openSource * osNorm
|
||||||
|
) * 100;
|
||||||
|
|
||||||
|
updated.startupsAdopted = Math.floor(updated.activeDevelopers * STARTUP_ADOPTION_PER_DEV);
|
||||||
|
updated.enterpriseReferrals = Math.floor(updated.startupsAdopted * ENTERPRISE_REFERRAL_PER_STARTUP);
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import type {
|
||||||
|
EnterpriseState,
|
||||||
|
EnterpriseLead,
|
||||||
|
EnterpriseContract,
|
||||||
|
EnterpriseSegment,
|
||||||
|
EnterprisePipelineStage,
|
||||||
|
DeveloperEcosystem,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
BASE_LEAD_RATE,
|
||||||
|
LEAD_EXPIRY_TICKS,
|
||||||
|
PIPELINE_STAGE_TIMEOUTS,
|
||||||
|
PIPELINE_TRANSITION_RATES,
|
||||||
|
SLA_PENALTY_FRACTION,
|
||||||
|
CONTRACT_DURATION_BY_SEGMENT,
|
||||||
|
ENTERPRISE_DEAL_VALUES,
|
||||||
|
ENTERPRISE_SLA_REQUIREMENTS,
|
||||||
|
ENTERPRISE_CAPABILITY_REQUIREMENTS,
|
||||||
|
ENTERPRISE_TOKENS_PER_TICK,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
import { ENTERPRISE_NAMES } from '../../data/enterpriseNames';
|
||||||
|
|
||||||
|
let leadIdCounter = 0;
|
||||||
|
|
||||||
|
function generateLeadId(): string {
|
||||||
|
return `lead_${Date.now()}_${++leadIdCounter}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomInRange(min: number, max: number): number {
|
||||||
|
return min + Math.random() * (max - min);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickSegment(reputation: number): EnterpriseSegment {
|
||||||
|
const roll = Math.random();
|
||||||
|
if (reputation > 70 && roll < 0.15) return 'government';
|
||||||
|
if (reputation > 50 && roll < 0.35) return 'enterprise';
|
||||||
|
if (roll < 0.6) return 'mid-market';
|
||||||
|
return 'startup';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickCompanyName(segment: EnterpriseSegment, existingNames: Set<string>): string {
|
||||||
|
const pool = ENTERPRISE_NAMES[segment];
|
||||||
|
const available = pool.filter(n => !existingNames.has(n));
|
||||||
|
if (available.length === 0) return `${segment}-client-${Math.floor(Math.random() * 9999)}`;
|
||||||
|
return available[Math.floor(Math.random() * available.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnterprisePipelineResult {
|
||||||
|
enterprise: EnterpriseState;
|
||||||
|
contractRevenue: number;
|
||||||
|
slaPenalties: number;
|
||||||
|
contractTokenDemand: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processEnterprisePipeline(
|
||||||
|
ent: EnterpriseState,
|
||||||
|
reputation: number,
|
||||||
|
modelCapability: number,
|
||||||
|
safetyScore: number,
|
||||||
|
salesHeadcount: number,
|
||||||
|
salesEffectiveness: number,
|
||||||
|
devEcosystem: DeveloperEcosystem,
|
||||||
|
seasonalEntMultiplier: number,
|
||||||
|
currentTick: number,
|
||||||
|
demandCapacityRatio: number,
|
||||||
|
): EnterprisePipelineResult {
|
||||||
|
const pipeline = [...ent.pipeline];
|
||||||
|
const activeContracts = [...ent.activeContracts];
|
||||||
|
|
||||||
|
const effectiveSales = salesHeadcount > 0
|
||||||
|
? Math.min(1, salesHeadcount * salesEffectiveness / Math.max(1, pipeline.length))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// --- Lead generation ---
|
||||||
|
const leadRate = BASE_LEAD_RATE
|
||||||
|
* (1 + reputation / 100)
|
||||||
|
* (1 + devEcosystem.startupsAdopted * 0.001)
|
||||||
|
* (effectiveSales > 0 ? effectiveSales : 0.1)
|
||||||
|
* seasonalEntMultiplier;
|
||||||
|
|
||||||
|
if (Math.random() < leadRate && pipeline.length < 20) {
|
||||||
|
const existingNames = new Set([
|
||||||
|
...pipeline.map(l => l.companyName),
|
||||||
|
...activeContracts.map(c => c.customerName),
|
||||||
|
]);
|
||||||
|
const segment = pickSegment(reputation);
|
||||||
|
const vals = ENTERPRISE_DEAL_VALUES[segment];
|
||||||
|
const toks = ENTERPRISE_TOKENS_PER_TICK[segment];
|
||||||
|
|
||||||
|
pipeline.push({
|
||||||
|
id: generateLeadId(),
|
||||||
|
companyName: pickCompanyName(segment, existingNames),
|
||||||
|
segment,
|
||||||
|
stage: 'lead',
|
||||||
|
enteredStageAtTick: currentTick,
|
||||||
|
dealValue: randomInRange(vals.min, vals.max),
|
||||||
|
tokensPerTick: randomInRange(toks.min, toks.max),
|
||||||
|
requiredCapability: ENTERPRISE_CAPABILITY_REQUIREMENTS[segment],
|
||||||
|
requiredSlaUptime: ENTERPRISE_SLA_REQUIREMENTS[segment],
|
||||||
|
requiredSafetyScore: segment === 'government' ? 60 : 30,
|
||||||
|
winProbability: 0.5,
|
||||||
|
expiresAtTick: currentTick + LEAD_EXPIRY_TICKS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Pipeline progression ---
|
||||||
|
const stageOrder: EnterprisePipelineStage[] = ['lead', 'qualification', 'poc', 'negotiation'];
|
||||||
|
const nextStageMap: Record<EnterprisePipelineStage, EnterprisePipelineStage | 'active'> = {
|
||||||
|
lead: 'qualification',
|
||||||
|
qualification: 'poc',
|
||||||
|
poc: 'negotiation',
|
||||||
|
negotiation: 'active',
|
||||||
|
};
|
||||||
|
|
||||||
|
const survivingLeads: EnterpriseLead[] = [];
|
||||||
|
const newContracts: EnterpriseContract[] = [];
|
||||||
|
|
||||||
|
for (const lead of pipeline) {
|
||||||
|
if (currentTick > lead.expiresAtTick) continue;
|
||||||
|
|
||||||
|
const timeout = PIPELINE_STAGE_TIMEOUTS[lead.stage];
|
||||||
|
if (currentTick - lead.enteredStageAtTick > timeout) continue;
|
||||||
|
|
||||||
|
const transKey = `${lead.stage}->${nextStageMap[lead.stage]}`;
|
||||||
|
const baseRate = PIPELINE_TRANSITION_RATES[transKey] ?? 0;
|
||||||
|
|
||||||
|
let transitionProb = baseRate * effectiveSales;
|
||||||
|
|
||||||
|
if (lead.stage === 'qualification') {
|
||||||
|
transitionProb *= modelCapability >= lead.requiredCapability ? 1 : 0.1;
|
||||||
|
} else if (lead.stage === 'poc') {
|
||||||
|
transitionProb *= Math.max(0.2, 1 - Math.max(0, demandCapacityRatio - 0.9) * 5);
|
||||||
|
} else if (lead.stage === 'negotiation') {
|
||||||
|
transitionProb *= Math.max(0.3, 1 - (lead.dealValue / 10_000_000) * 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lead.stage === 'qualification' && safetyScore < lead.requiredSafetyScore) {
|
||||||
|
transitionProb *= 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.random() < transitionProb) {
|
||||||
|
const nextStage = nextStageMap[lead.stage];
|
||||||
|
if (nextStage === 'active') {
|
||||||
|
const duration = CONTRACT_DURATION_BY_SEGMENT[lead.segment];
|
||||||
|
const pricePerMToken = (lead.dealValue / duration) / (lead.tokensPerTick / 1_000_000);
|
||||||
|
newContracts.push({
|
||||||
|
id: lead.id,
|
||||||
|
customerName: lead.companyName,
|
||||||
|
segment: lead.segment,
|
||||||
|
tokensPerTick: lead.tokensPerTick,
|
||||||
|
pricePerMToken: Math.max(0.1, pricePerMToken),
|
||||||
|
slaUptime: lead.requiredSlaUptime,
|
||||||
|
startTick: currentTick,
|
||||||
|
durationTicks: duration,
|
||||||
|
satisfaction: 0.7,
|
||||||
|
renewalProbability: 0.5,
|
||||||
|
slaViolations: 0,
|
||||||
|
slaPenaltiesPaid: 0,
|
||||||
|
uptimeTicks: 0,
|
||||||
|
totalTicks: 0,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
survivingLeads.push({
|
||||||
|
...lead,
|
||||||
|
stage: nextStage,
|
||||||
|
enteredStageAtTick: currentTick,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
survivingLeads.push(lead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Active contracts: SLA, satisfaction, renewal ---
|
||||||
|
let contractRevenue = 0;
|
||||||
|
let slaPenalties = 0;
|
||||||
|
let contractTokenDemand = 0;
|
||||||
|
const survivingContracts: EnterpriseContract[] = [];
|
||||||
|
|
||||||
|
for (const contract of [...activeContracts, ...newContracts]) {
|
||||||
|
const updated = { ...contract };
|
||||||
|
updated.totalTicks++;
|
||||||
|
|
||||||
|
if (demandCapacityRatio <= (1 / updated.slaUptime)) {
|
||||||
|
updated.uptimeTicks++;
|
||||||
|
} else {
|
||||||
|
updated.slaViolations++;
|
||||||
|
const penalty = updated.pricePerMToken * (updated.tokensPerTick / 1_000_000) * SLA_PENALTY_FRACTION;
|
||||||
|
slaPenalties += penalty;
|
||||||
|
updated.slaPenaltiesPaid += penalty;
|
||||||
|
updated.satisfaction = Math.max(0, updated.satisfaction - 0.005);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.totalTicks > 0 && updated.slaViolations === 0) {
|
||||||
|
updated.satisfaction = Math.min(1, updated.satisfaction + 0.001);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tickRevenue = (updated.tokensPerTick / 1_000_000) * updated.pricePerMToken;
|
||||||
|
contractRevenue += tickRevenue;
|
||||||
|
contractTokenDemand += updated.tokensPerTick;
|
||||||
|
|
||||||
|
if (currentTick >= updated.startTick + updated.durationTicks) {
|
||||||
|
const renewalProb = updated.satisfaction * 0.6 + 0.3 - (updated.slaViolations * 0.01);
|
||||||
|
if (Math.random() < Math.max(0, renewalProb)) {
|
||||||
|
updated.startTick = currentTick;
|
||||||
|
updated.totalTicks = 0;
|
||||||
|
updated.uptimeTicks = 0;
|
||||||
|
updated.slaViolations = 0;
|
||||||
|
updated.slaPenaltiesPaid = 0;
|
||||||
|
survivingContracts.push(updated);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
survivingContracts.push(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enterprise: {
|
||||||
|
...ent,
|
||||||
|
pipeline: survivingLeads,
|
||||||
|
activeContracts: survivingContracts,
|
||||||
|
totalApiCallsPerTick: contractTokenDemand / ent.averageTokensPerCall,
|
||||||
|
leadGenerationRate: leadRate,
|
||||||
|
},
|
||||||
|
contractRevenue,
|
||||||
|
slaPenalties,
|
||||||
|
contractTokenDemand,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import type { GameState, MarketState, BenchmarkResult, Competitor } from '@ai-tycoon/shared';
|
||||||
|
import { CONSUMER_TOKENS_PER_SUBSCRIBER } from '@ai-tycoon/shared';
|
||||||
|
import { BENCHMARKS } from '../../data/benchmarks';
|
||||||
|
import { computeSeasonal } from './seasonalSystem';
|
||||||
|
import { updateObsolescence } from './obsolescenceSystem';
|
||||||
|
import { buildPlayerProfile, buildCompetitorProfile, computeMarketShares, updateTAMGrowth } from './tamSystem';
|
||||||
|
import { processConsumerTiers } from './consumerTierSystem';
|
||||||
|
import { processApiTiers } from './apiTierSystem';
|
||||||
|
import { processProductLines } from './productLines';
|
||||||
|
import { processDeveloperEcosystem } from './developerEcosystem';
|
||||||
|
import { processEnterprisePipeline } from './enterprisePipeline';
|
||||||
|
|
||||||
|
export interface MarketTickResult {
|
||||||
|
marketState: MarketState;
|
||||||
|
apiRevenue: number;
|
||||||
|
subscriptionRevenue: number;
|
||||||
|
totalTokenDemand: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSegmentQuality(
|
||||||
|
segment: 'consumer' | 'enterprise' | 'developer' | 'research',
|
||||||
|
benchmarkResults: BenchmarkResult[],
|
||||||
|
fallbackScore: number,
|
||||||
|
): number {
|
||||||
|
if (benchmarkResults.length === 0) return fallbackScore / 100;
|
||||||
|
|
||||||
|
const bestByBenchmark = new Map<string, number>();
|
||||||
|
for (const r of benchmarkResults) {
|
||||||
|
const prev = bestByBenchmark.get(r.benchmarkId) ?? 0;
|
||||||
|
if (r.score > prev) bestByBenchmark.set(r.benchmarkId, r.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
let weightedSum = 0;
|
||||||
|
let totalWeight = 0;
|
||||||
|
for (const bench of BENCHMARKS) {
|
||||||
|
const score = bestByBenchmark.get(bench.id);
|
||||||
|
if (score == null) continue;
|
||||||
|
const weight = bench.marketRelevance[segment];
|
||||||
|
weightedSum += (score / 100) * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalWeight === 0) return fallbackScore / 100;
|
||||||
|
return weightedSum / totalWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processMarketV2(state: GameState, currentTickCapacity: number): MarketTickResult {
|
||||||
|
const consumerQuality = getSegmentQuality('consumer', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
||||||
|
const enterpriseQuality = getSegmentQuality('enterprise', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
||||||
|
const modelQuality = state.models.benchmarkResults.length > 0
|
||||||
|
? (consumerQuality + enterpriseQuality) / 2
|
||||||
|
: state.models.bestDeployedModelScore / 100;
|
||||||
|
|
||||||
|
// --- Seasonal ---
|
||||||
|
const seasonal = computeSeasonal(state.meta.tickCount);
|
||||||
|
|
||||||
|
// --- Obsolescence ---
|
||||||
|
const obsolescence = updateObsolescence(
|
||||||
|
state.market.obsolescence,
|
||||||
|
state.meta.currentEra,
|
||||||
|
state.meta.tickCount,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Developer Ecosystem ---
|
||||||
|
const freeApiDevs = state.market.apiTiers.tiers.free.developerCount;
|
||||||
|
const totalApiDevs = state.market.apiTiers.totalDevelopers;
|
||||||
|
const engineeringCount = state.talent.departments.engineering.headcount;
|
||||||
|
|
||||||
|
const devEcosystem = processDeveloperEcosystem(
|
||||||
|
state.market.developerEcosystem,
|
||||||
|
state.market.openSourcedModels.length,
|
||||||
|
freeApiDevs,
|
||||||
|
totalApiDevs,
|
||||||
|
engineeringCount,
|
||||||
|
state.meta.currentEra,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- TAM & Market Shares ---
|
||||||
|
const chatProduct = state.models.productLines.find(p => p.type === 'chat-product');
|
||||||
|
const textApi = state.models.productLines.find(p => p.type === 'text-api');
|
||||||
|
|
||||||
|
const chatPrice = chatProduct?.pricing.subscriptionPrice ?? 20;
|
||||||
|
const apiOutPrice = textApi?.pricing.outputTokenPrice ?? 3;
|
||||||
|
|
||||||
|
const hasAnyFreeTier = state.market.consumerTiers.tiers.free.config.isActive
|
||||||
|
|| state.market.apiTiers.tiers.free.config.isActive;
|
||||||
|
|
||||||
|
const playerProfile = buildPlayerProfile(
|
||||||
|
modelQuality,
|
||||||
|
chatPrice,
|
||||||
|
apiOutPrice,
|
||||||
|
state.reputation.score,
|
||||||
|
devEcosystem,
|
||||||
|
obsolescence,
|
||||||
|
hasAnyFreeTier,
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeRivals = state.competitors.rivals.filter(r => r.status === 'active');
|
||||||
|
const competitorProfiles = activeRivals.map(buildCompetitorProfile);
|
||||||
|
const allProfiles = [playerProfile, ...competitorProfiles];
|
||||||
|
|
||||||
|
let tam = updateTAMGrowth(state.market.tam, state.meta.currentEra);
|
||||||
|
tam = computeMarketShares(tam, allProfiles, obsolescence.marketQualityBaseline);
|
||||||
|
|
||||||
|
const playerConsumerCustomers = tam.segments.consumer.shares.find(s => s.playerId === 'player')?.customers ?? 0;
|
||||||
|
const playerDevCustomers = tam.segments.developer.shares.find(s => s.playerId === 'player')?.customers ?? 0;
|
||||||
|
const playerEntCustomers = tam.segments.enterprise.shares.find(s => s.playerId === 'player')?.customers ?? 0;
|
||||||
|
|
||||||
|
// --- Consumer Tiers ---
|
||||||
|
const consumerDemandEstimate = state.market.consumerTiers.totalUsers * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
||||||
|
const demandCapacityRatio = currentTickCapacity > 0
|
||||||
|
? consumerDemandEstimate / currentTickCapacity
|
||||||
|
: consumerDemandEstimate > 0 ? 10 : 0;
|
||||||
|
|
||||||
|
const consumerResult = processConsumerTiers(
|
||||||
|
state.market.consumerTiers,
|
||||||
|
playerConsumerCustomers,
|
||||||
|
modelQuality,
|
||||||
|
seasonal.multipliers.consumer,
|
||||||
|
demandCapacityRatio,
|
||||||
|
state.infrastructure.networkLatencyPenalty,
|
||||||
|
state.market.overloadPolicy,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- API Tiers ---
|
||||||
|
const apiResult = processApiTiers(
|
||||||
|
state.market.apiTiers,
|
||||||
|
playerDevCustomers,
|
||||||
|
modelQuality,
|
||||||
|
seasonal.multipliers.api,
|
||||||
|
devEcosystem,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Product Lines ---
|
||||||
|
const productResult = processProductLines(
|
||||||
|
state.market.codeAssistant,
|
||||||
|
state.market.agentsPlatform,
|
||||||
|
state.models.benchmarkResults,
|
||||||
|
playerDevCustomers,
|
||||||
|
playerEntCustomers,
|
||||||
|
seasonal.multipliers.consumer,
|
||||||
|
seasonal.multipliers.enterprise,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Enterprise Pipeline ---
|
||||||
|
const salesDept = state.talent.departments.sales;
|
||||||
|
const salesHeadcount = salesDept.headcount;
|
||||||
|
const salesEffectiveness = salesDept.effectiveness;
|
||||||
|
|
||||||
|
const enterpriseResult = processEnterprisePipeline(
|
||||||
|
state.market.enterprise,
|
||||||
|
state.reputation.score,
|
||||||
|
state.models.bestDeployedModelScore,
|
||||||
|
state.models.bestDeployedSafetyScore,
|
||||||
|
salesHeadcount,
|
||||||
|
salesEffectiveness,
|
||||||
|
devEcosystem,
|
||||||
|
seasonal.multipliers.enterprise,
|
||||||
|
state.meta.tickCount,
|
||||||
|
demandCapacityRatio,
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Aggregate revenue ---
|
||||||
|
const subscriptionRevenue = consumerResult.subscriptionRevenue
|
||||||
|
+ productResult.codeAssistantRevenue
|
||||||
|
+ productResult.agentsPlatformRevenue;
|
||||||
|
|
||||||
|
const apiRevenue = apiResult.apiRevenue
|
||||||
|
+ enterpriseResult.contractRevenue
|
||||||
|
- enterpriseResult.slaPenalties;
|
||||||
|
|
||||||
|
const totalTokenDemand = consumerResult.totalConsumerTokenDemand
|
||||||
|
+ apiResult.totalApiTokenDemand
|
||||||
|
+ enterpriseResult.contractTokenDemand
|
||||||
|
+ productResult.codeAssistantTokenDemand
|
||||||
|
+ productResult.agentsPlatformTokenDemand;
|
||||||
|
|
||||||
|
// --- Subscriber history ---
|
||||||
|
const subscriberHistory = [...(state.market.subscriberHistory || [])];
|
||||||
|
if (state.meta.tickCount % 60 === 0) {
|
||||||
|
subscriberHistory.push({ tick: state.meta.tickCount, subscribers: consumerResult.consumerTiers.totalUsers });
|
||||||
|
if (subscriberHistory.length > 500) subscriberHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Open source effects ---
|
||||||
|
const openSourceCount = state.market.openSourcedModels.length;
|
||||||
|
if (openSourceCount > 0) {
|
||||||
|
const revenueReduction = openSourceCount * 0.10 * 0.3;
|
||||||
|
const adjustedApiRevenue = apiRevenue * (1 - revenueReduction);
|
||||||
|
return {
|
||||||
|
marketState: {
|
||||||
|
...state.market,
|
||||||
|
tam,
|
||||||
|
consumerTiers: consumerResult.consumerTiers,
|
||||||
|
apiTiers: apiResult.apiTiers,
|
||||||
|
codeAssistant: productResult.codeAssistant,
|
||||||
|
agentsPlatform: productResult.agentsPlatform,
|
||||||
|
enterprise: enterpriseResult.enterprise,
|
||||||
|
developerEcosystem: devEcosystem,
|
||||||
|
seasonalPhase: seasonal.phase,
|
||||||
|
seasonalMultiplier: seasonal.multipliers.consumer,
|
||||||
|
obsolescence,
|
||||||
|
subscriberHistory,
|
||||||
|
},
|
||||||
|
apiRevenue: Math.max(0, adjustedApiRevenue),
|
||||||
|
subscriptionRevenue,
|
||||||
|
totalTokenDemand,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
marketState: {
|
||||||
|
...state.market,
|
||||||
|
tam,
|
||||||
|
consumerTiers: consumerResult.consumerTiers,
|
||||||
|
apiTiers: apiResult.apiTiers,
|
||||||
|
codeAssistant: productResult.codeAssistant,
|
||||||
|
agentsPlatform: productResult.agentsPlatform,
|
||||||
|
enterprise: enterpriseResult.enterprise,
|
||||||
|
developerEcosystem: devEcosystem,
|
||||||
|
seasonalPhase: seasonal.phase,
|
||||||
|
seasonalMultiplier: seasonal.multipliers.consumer,
|
||||||
|
obsolescence,
|
||||||
|
subscriberHistory,
|
||||||
|
},
|
||||||
|
apiRevenue: Math.max(0, apiRevenue),
|
||||||
|
subscriptionRevenue,
|
||||||
|
totalTokenDemand,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { ObsolescenceState, Era } from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
OBSOLESCENCE_BASELINE_GROWTH,
|
||||||
|
OBSOLESCENCE_ERA_ACCELERATOR,
|
||||||
|
FRESHNESS_DECAY_RATE,
|
||||||
|
NEW_MODEL_BOOST_TICKS,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export function updateObsolescence(
|
||||||
|
obs: ObsolescenceState,
|
||||||
|
era: Era,
|
||||||
|
currentTick: number,
|
||||||
|
): ObsolescenceState {
|
||||||
|
const accelerator = OBSOLESCENCE_ERA_ACCELERATOR[era];
|
||||||
|
const newBaseline = obs.marketQualityBaseline + OBSOLESCENCE_BASELINE_GROWTH * accelerator;
|
||||||
|
|
||||||
|
const ticksSinceRelease = currentTick - obs.lastModelReleaseTick;
|
||||||
|
const freshness = obs.lastModelReleaseTick > 0
|
||||||
|
? Math.max(0, 1 - ticksSinceRelease * FRESHNESS_DECAY_RATE)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const boostRemaining = Math.max(0, obs.newModelBoostRemaining - 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...obs,
|
||||||
|
marketQualityBaseline: newBaseline,
|
||||||
|
playerModelFreshness: freshness,
|
||||||
|
newModelBoostRemaining: boostRemaining,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onModelDeployed(obs: ObsolescenceState, tick: number): ObsolescenceState {
|
||||||
|
return {
|
||||||
|
...obs,
|
||||||
|
playerModelFreshness: 1.0,
|
||||||
|
lastModelReleaseTick: tick,
|
||||||
|
newModelBoostRemaining: NEW_MODEL_BOOST_TICKS,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import type { CodeAssistantState, AgentsPlatformState, BenchmarkResult } from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
CODE_ASSISTANT_MIN_CODING_SCORE,
|
||||||
|
CODE_ASSISTANT_BASE_ADOPTION_RATE,
|
||||||
|
CODE_ASSISTANT_CHURN_RATE,
|
||||||
|
AGENTS_PLATFORM_MIN_AGENTS_SCORE,
|
||||||
|
AGENTS_PLATFORM_BASE_ADOPTION_RATE,
|
||||||
|
AGENTS_PLATFORM_CHURN_RATE,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
import { BENCHMARKS } from '../../data/benchmarks';
|
||||||
|
|
||||||
|
function getBenchmarkScore(benchmarkId: string, results: BenchmarkResult[]): number {
|
||||||
|
let best = 0;
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.benchmarkId === benchmarkId && r.score > best) best = r.score;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCodingScore(results: BenchmarkResult[]): number {
|
||||||
|
const codeBench = BENCHMARKS.find(b => b.id === 'codeforce');
|
||||||
|
if (!codeBench) return 0;
|
||||||
|
return getBenchmarkScore(codeBench.id, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgentsScore(results: BenchmarkResult[]): number {
|
||||||
|
const agentBench = BENCHMARKS.find(b => b.id === 'agentarena');
|
||||||
|
if (!agentBench) return 0;
|
||||||
|
return getBenchmarkScore(agentBench.id, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductLineResult {
|
||||||
|
codeAssistant: CodeAssistantState;
|
||||||
|
agentsPlatform: AgentsPlatformState;
|
||||||
|
codeAssistantRevenue: number;
|
||||||
|
agentsPlatformRevenue: number;
|
||||||
|
codeAssistantTokenDemand: number;
|
||||||
|
agentsPlatformTokenDemand: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processProductLines(
|
||||||
|
ca: CodeAssistantState,
|
||||||
|
ap: AgentsPlatformState,
|
||||||
|
benchmarkResults: BenchmarkResult[],
|
||||||
|
playerDevCustomers: number,
|
||||||
|
playerEntCustomers: number,
|
||||||
|
seasonalConsumerMult: number,
|
||||||
|
seasonalEntMult: number,
|
||||||
|
): ProductLineResult {
|
||||||
|
const updatedCA = { ...ca };
|
||||||
|
const updatedAP = { ...ap };
|
||||||
|
let caRevenue = 0;
|
||||||
|
let apRevenue = 0;
|
||||||
|
|
||||||
|
// --- Code Assistant ---
|
||||||
|
updatedCA.qualityScore = getCodingScore(benchmarkResults);
|
||||||
|
if (updatedCA.isUnlocked && updatedCA.isActive && updatedCA.qualityScore >= CODE_ASSISTANT_MIN_CODING_SCORE) {
|
||||||
|
const qualityFactor = updatedCA.qualityScore / 100;
|
||||||
|
const priceAttr = Math.max(0.1, 1 - updatedCA.pricePerSeat / 50);
|
||||||
|
const targetSeats = playerDevCustomers * 0.05 * qualityFactor;
|
||||||
|
const growth = CODE_ASSISTANT_BASE_ADOPTION_RATE * qualityFactor * priceAttr * seasonalConsumerMult;
|
||||||
|
const churn = CODE_ASSISTANT_CHURN_RATE * (1 + (1 - qualityFactor) * 2);
|
||||||
|
|
||||||
|
updatedCA.seats = Math.max(0, updatedCA.seats + updatedCA.seats * growth - updatedCA.seats * churn);
|
||||||
|
if (updatedCA.seats < 10 && targetSeats > 10) {
|
||||||
|
updatedCA.seats += targetSeats * 0.01;
|
||||||
|
}
|
||||||
|
updatedCA.satisfaction = Math.min(1, 0.3 + qualityFactor * 0.5);
|
||||||
|
caRevenue = updatedCA.seats * (updatedCA.pricePerSeat / 86400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Agents Platform ---
|
||||||
|
updatedAP.qualityScore = getAgentsScore(benchmarkResults);
|
||||||
|
if (updatedAP.isUnlocked && updatedAP.isActive && updatedAP.qualityScore >= AGENTS_PLATFORM_MIN_AGENTS_SCORE) {
|
||||||
|
const qualityFactor = updatedAP.qualityScore / 100;
|
||||||
|
const priceAttr = Math.max(0.1, 1 - updatedAP.pricePerSeat / 250);
|
||||||
|
const targetSeats = playerEntCustomers * 0.02 * qualityFactor;
|
||||||
|
const growth = AGENTS_PLATFORM_BASE_ADOPTION_RATE * qualityFactor * priceAttr * seasonalEntMult;
|
||||||
|
const churn = AGENTS_PLATFORM_CHURN_RATE * (1 + (1 - qualityFactor) * 2);
|
||||||
|
|
||||||
|
updatedAP.seats = Math.max(0, updatedAP.seats + updatedAP.seats * growth - updatedAP.seats * churn);
|
||||||
|
if (updatedAP.seats < 5 && targetSeats > 5) {
|
||||||
|
updatedAP.seats += targetSeats * 0.01;
|
||||||
|
}
|
||||||
|
updatedAP.satisfaction = Math.min(1, 0.3 + qualityFactor * 0.5);
|
||||||
|
apRevenue = updatedAP.seats * (updatedAP.pricePerSeat / 86400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const caTokenDemand = updatedCA.seats * 2;
|
||||||
|
const apTokenDemand = updatedAP.seats * 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
codeAssistant: updatedCA,
|
||||||
|
agentsPlatform: updatedAP,
|
||||||
|
codeAssistantRevenue: caRevenue,
|
||||||
|
agentsPlatformRevenue: apRevenue,
|
||||||
|
codeAssistantTokenDemand: caTokenDemand,
|
||||||
|
agentsPlatformTokenDemand: apTokenDemand,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { SeasonalPhase } from '@ai-tycoon/shared';
|
||||||
|
import { SEASONAL_CYCLE_TICKS, SEASONAL_MULTIPLIERS } from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export interface SeasonalResult {
|
||||||
|
phase: SeasonalPhase;
|
||||||
|
multipliers: { consumer: number; api: number; enterprise: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
const PHASES: SeasonalPhase[] = ['q1', 'q2', 'q3', 'q4'];
|
||||||
|
|
||||||
|
export function computeSeasonal(tickCount: number): SeasonalResult {
|
||||||
|
const positionInCycle = tickCount % SEASONAL_CYCLE_TICKS;
|
||||||
|
const quarterLength = SEASONAL_CYCLE_TICKS / 4;
|
||||||
|
const phaseIndex = Math.min(3, Math.floor(positionInCycle / quarterLength));
|
||||||
|
const phase = PHASES[phaseIndex];
|
||||||
|
const raw = SEASONAL_MULTIPLIERS[phase];
|
||||||
|
return {
|
||||||
|
phase,
|
||||||
|
multipliers: {
|
||||||
|
consumer: raw.consumer,
|
||||||
|
api: raw.api,
|
||||||
|
enterprise: raw.enterprise,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import type {
|
||||||
|
TotalAddressableMarket,
|
||||||
|
TAMSegmentId,
|
||||||
|
MarketShareEntry,
|
||||||
|
Competitor,
|
||||||
|
Era,
|
||||||
|
ObsolescenceState,
|
||||||
|
DeveloperEcosystem,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
import {
|
||||||
|
TAM_BASE_SIZES,
|
||||||
|
TAM_GROWTH_PER_TICK,
|
||||||
|
SHARE_TEMPERATURE,
|
||||||
|
SHARE_MIGRATION_SPEED,
|
||||||
|
ATTRACTIVENESS_WEIGHTS,
|
||||||
|
OBSOLESCENCE_PENALTY_WEIGHT,
|
||||||
|
NEW_MODEL_BOOST_VALUE,
|
||||||
|
} from '@ai-tycoon/shared';
|
||||||
|
|
||||||
|
export interface ParticipantProfile {
|
||||||
|
id: string;
|
||||||
|
qualityScore: number;
|
||||||
|
priceScore: number;
|
||||||
|
reputation: number;
|
||||||
|
ecosystemScore: number;
|
||||||
|
freshness: number;
|
||||||
|
hasFreeTier: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPlayerProfile(
|
||||||
|
qualityScore: number,
|
||||||
|
chatPrice: number,
|
||||||
|
apiOutputPrice: number,
|
||||||
|
reputation: number,
|
||||||
|
ecosystem: DeveloperEcosystem,
|
||||||
|
obsolescence: ObsolescenceState,
|
||||||
|
hasFreeTier: boolean,
|
||||||
|
): ParticipantProfile {
|
||||||
|
const avgPrice = (chatPrice + apiOutputPrice) / 2;
|
||||||
|
const priceScore = Math.max(0, Math.min(1, 1 - avgPrice / 100));
|
||||||
|
|
||||||
|
let freshness = obsolescence.playerModelFreshness;
|
||||||
|
if (obsolescence.newModelBoostRemaining > 0) {
|
||||||
|
freshness = Math.min(1, freshness + NEW_MODEL_BOOST_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'player',
|
||||||
|
qualityScore: Math.min(1, qualityScore),
|
||||||
|
priceScore,
|
||||||
|
reputation,
|
||||||
|
ecosystemScore: ecosystem.ecosystemScore,
|
||||||
|
freshness,
|
||||||
|
hasFreeTier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCompetitorProfile(c: Competitor): ParticipantProfile {
|
||||||
|
const avgPrice = (c.products.chatPrice + c.products.apiOutputPrice) / 2;
|
||||||
|
const priceScore = Math.max(0, Math.min(1, 1 - avgPrice / 100));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: c.id,
|
||||||
|
qualityScore: Math.min(1, c.estimatedCapability / 100),
|
||||||
|
priceScore,
|
||||||
|
reputation: c.reputation,
|
||||||
|
ecosystemScore: c.developerEcosystemScore,
|
||||||
|
freshness: c.modelFreshness,
|
||||||
|
hasFreeTier: c.products.hasFreeTier,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeAttractiveness(
|
||||||
|
p: ParticipantProfile,
|
||||||
|
segment: TAMSegmentId,
|
||||||
|
qualityBaseline: number,
|
||||||
|
): number {
|
||||||
|
const w = ATTRACTIVENESS_WEIGHTS[segment];
|
||||||
|
let score =
|
||||||
|
w.quality * p.qualityScore +
|
||||||
|
w.price * p.priceScore +
|
||||||
|
w.reputation * (p.reputation / 100) +
|
||||||
|
w.ecosystem * (p.ecosystemScore / 100) +
|
||||||
|
w.freshness * p.freshness +
|
||||||
|
w.freeTier * (p.hasFreeTier ? 1 : 0);
|
||||||
|
|
||||||
|
const qualityGap = qualityBaseline / 100 - p.qualityScore;
|
||||||
|
if (qualityGap > 0) {
|
||||||
|
score -= qualityGap * OBSOLESCENCE_PENALTY_WEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0.01, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
function softmaxShares(scores: number[]): number[] {
|
||||||
|
const maxScore = Math.max(...scores);
|
||||||
|
const exps = scores.map(s => Math.exp((s - maxScore) * SHARE_TEMPERATURE));
|
||||||
|
const sumExp = exps.reduce((a, b) => a + b, 0);
|
||||||
|
return exps.map(e => e / sumExp);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeMarketShares(
|
||||||
|
tam: TotalAddressableMarket,
|
||||||
|
participants: ParticipantProfile[],
|
||||||
|
qualityBaseline: number,
|
||||||
|
): TotalAddressableMarket {
|
||||||
|
const segments = { ...tam.segments };
|
||||||
|
const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government'];
|
||||||
|
|
||||||
|
for (const segId of segmentIds) {
|
||||||
|
const seg = segments[segId];
|
||||||
|
const scores = participants.map(p => computeAttractiveness(p, segId, qualityBaseline));
|
||||||
|
const targetShares = softmaxShares(scores);
|
||||||
|
|
||||||
|
const oldShareMap = new Map<string, MarketShareEntry>();
|
||||||
|
for (const entry of seg.shares) {
|
||||||
|
oldShareMap.set(entry.playerId, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newShares: MarketShareEntry[] = participants.map((p, i) => {
|
||||||
|
const old = oldShareMap.get(p.id);
|
||||||
|
const oldShare = old?.sharePercent ?? 0;
|
||||||
|
const migratedShare = oldShare + (targetShares[i] - oldShare) * SHARE_MIGRATION_SPEED;
|
||||||
|
return {
|
||||||
|
playerId: p.id,
|
||||||
|
sharePercent: migratedShare,
|
||||||
|
customers: Math.floor(migratedShare * seg.totalSize),
|
||||||
|
attractivenessScore: scores[i],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalShare = newShares.reduce((s, e) => s + e.sharePercent, 0);
|
||||||
|
if (totalShare > 0) {
|
||||||
|
for (const entry of newShares) {
|
||||||
|
entry.sharePercent /= totalShare;
|
||||||
|
entry.customers = Math.floor(entry.sharePercent * seg.totalSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segments[segId] = { ...seg, shares: newShares };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { segments };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTAMGrowth(tam: TotalAddressableMarket, era: Era): TotalAddressableMarket {
|
||||||
|
const baseSizes = TAM_BASE_SIZES[era];
|
||||||
|
const segments = { ...tam.segments };
|
||||||
|
const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government'];
|
||||||
|
|
||||||
|
for (const segId of segmentIds) {
|
||||||
|
const seg = segments[segId];
|
||||||
|
const base = baseSizes[segId];
|
||||||
|
const grown = seg.totalSize + seg.totalSize * TAM_GROWTH_PER_TICK;
|
||||||
|
segments[segId] = {
|
||||||
|
...seg,
|
||||||
|
totalSize: Math.max(base, grown),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { segments };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeTAM(era: Era, competitors: Competitor[]): TotalAddressableMarket {
|
||||||
|
const baseSizes = TAM_BASE_SIZES[era];
|
||||||
|
const segmentIds: TAMSegmentId[] = ['consumer', 'developer', 'enterprise', 'government'];
|
||||||
|
const segments = {} as Record<TAMSegmentId, { totalSize: number; shares: MarketShareEntry[] }>;
|
||||||
|
|
||||||
|
for (const segId of segmentIds) {
|
||||||
|
const shares: MarketShareEntry[] = [
|
||||||
|
{ playerId: 'player', sharePercent: 0.05, customers: 0, attractivenessScore: 0 },
|
||||||
|
...competitors.map(c => ({
|
||||||
|
playerId: c.id,
|
||||||
|
sharePercent: c.marketShares[segId] ?? 0.1,
|
||||||
|
customers: 0,
|
||||||
|
attractivenessScore: 0,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const totalShare = shares.reduce((s, e) => s + e.sharePercent, 0);
|
||||||
|
for (const entry of shares) {
|
||||||
|
entry.sharePercent /= totalShare;
|
||||||
|
entry.customers = Math.floor(entry.sharePercent * baseSizes[segId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
segments[segId] = { totalSize: baseSizes[segId], shares };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { segments };
|
||||||
|
}
|
||||||
@@ -1,194 +1,8 @@
|
|||||||
import type { GameState, MarketState, BenchmarkResult } from '@ai-tycoon/shared';
|
import type { GameState } from '@ai-tycoon/shared';
|
||||||
import {
|
import { processMarketV2 } from './market/index';
|
||||||
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';
|
|
||||||
|
|
||||||
export interface MarketTickResult {
|
export type { MarketTickResult } from './market/index';
|
||||||
marketState: MarketState;
|
|
||||||
apiRevenue: number;
|
export function processMarket(state: GameState, currentTickCapacity: number) {
|
||||||
subscriptionRevenue: number;
|
return processMarketV2(state, currentTickCapacity);
|
||||||
totalTokenDemand: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSegmentQuality(
|
|
||||||
segment: 'consumer' | 'enterprise' | 'developer' | 'research',
|
|
||||||
benchmarkResults: BenchmarkResult[],
|
|
||||||
fallbackScore: number,
|
|
||||||
): number {
|
|
||||||
if (benchmarkResults.length === 0) return fallbackScore / 100;
|
|
||||||
|
|
||||||
const bestByBenchmark = new Map<string, number>();
|
|
||||||
for (const r of benchmarkResults) {
|
|
||||||
const prev = bestByBenchmark.get(r.benchmarkId) ?? 0;
|
|
||||||
if (r.score > prev) bestByBenchmark.set(r.benchmarkId, r.score);
|
|
||||||
}
|
|
||||||
|
|
||||||
let weightedSum = 0;
|
|
||||||
let totalWeight = 0;
|
|
||||||
for (const bench of BENCHMARKS) {
|
|
||||||
const score = bestByBenchmark.get(bench.id);
|
|
||||||
if (score == null) continue;
|
|
||||||
const weight = bench.marketRelevance[segment];
|
|
||||||
weightedSum += (score / 100) * weight;
|
|
||||||
totalWeight += weight;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalWeight === 0) return fallbackScore / 100;
|
|
||||||
return weightedSum / totalWeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processMarket(state: GameState, currentTickCapacity: number): MarketTickResult {
|
|
||||||
const consumerQuality = getSegmentQuality('consumer', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
|
||||||
const enterpriseQuality = getSegmentQuality('enterprise', state.models.benchmarkResults, state.models.bestDeployedModelScore);
|
|
||||||
const modelQuality = state.models.benchmarkResults.length > 0
|
|
||||||
? (consumerQuality + enterpriseQuality) / 2
|
|
||||||
: state.models.bestDeployedModelScore / 100;
|
|
||||||
const chatProduct = state.models.productLines.find(p => p.type === 'chat-product');
|
|
||||||
const textApi = state.models.productLines.find(p => p.type === 'text-api');
|
|
||||||
|
|
||||||
// --- Consumer market (subscription product) ---
|
|
||||||
const consumers = { ...state.market.consumers };
|
|
||||||
let subscriptionRevenue = 0;
|
|
||||||
|
|
||||||
if (chatProduct?.isActive && modelQuality > 0) {
|
|
||||||
const price = chatProduct.pricing.subscriptionPrice;
|
|
||||||
const fairPrice = 20 + modelQuality * 80;
|
|
||||||
const priceRatio = price / Math.max(1, fairPrice);
|
|
||||||
const priceAttractiveness = Math.max(0, Math.min(1, 1 - (priceRatio - 1) * 0.8));
|
|
||||||
|
|
||||||
// --- Logistic growth with era-based market cap ---
|
|
||||||
const eraCapBase = MARKET_SIZE_CAP[state.meta.currentEra] ?? 100_000_000;
|
|
||||||
const effectiveCap = eraCapBase
|
|
||||||
* (1 + modelQuality * MARKET_CAP_QUALITY_BONUS)
|
|
||||||
* (1 + (state.reputation.score / 100) * MARKET_CAP_REPUTATION_BONUS);
|
|
||||||
|
|
||||||
const saturationFactor = Math.max(0, 1 - consumers.totalSubscribers / effectiveCap);
|
|
||||||
|
|
||||||
const growthRate = (CONSUMER_BASE_GROWTH + modelQuality * CONSUMER_QUALITY_GROWTH_MULTIPLIER)
|
|
||||||
* priceAttractiveness * saturationFactor;
|
|
||||||
const priceChurnMultiplier = priceRatio > 1 ? 1 + (priceRatio - 1) * 3 : 1;
|
|
||||||
const churnRate = CONSUMER_BASE_CHURN * (1 + (1 - consumers.satisfaction) * 2) * priceChurnMultiplier;
|
|
||||||
|
|
||||||
consumers.growthRatePerTick = growthRate;
|
|
||||||
consumers.churnRatePerTick = churnRate;
|
|
||||||
|
|
||||||
const newSubs = consumers.totalSubscribers * growthRate;
|
|
||||||
const lostSubs = consumers.totalSubscribers * churnRate;
|
|
||||||
consumers.totalSubscribers = Math.max(0, Math.min(
|
|
||||||
effectiveCap,
|
|
||||||
consumers.totalSubscribers + newSubs - lostSubs,
|
|
||||||
));
|
|
||||||
|
|
||||||
if (consumers.totalSubscribers < 100 && modelQuality > 0.1 && priceRatio < 3) {
|
|
||||||
consumers.totalSubscribers += 5 + modelQuality * 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Satisfaction from demand/capacity ratio (current tick) ---
|
|
||||||
const consumerDemand = consumers.totalSubscribers * CONSUMER_TOKENS_PER_SUBSCRIBER;
|
|
||||||
let demandCapacityRatio: number;
|
|
||||||
if (currentTickCapacity > 0) {
|
|
||||||
demandCapacityRatio = consumerDemand / currentTickCapacity;
|
|
||||||
} else {
|
|
||||||
demandCapacityRatio = consumerDemand > 0 ? 10 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let headroomBonus = 0;
|
|
||||||
let overloadPenalty = 0;
|
|
||||||
if (demandCapacityRatio <= 1) {
|
|
||||||
headroomBonus = (1 - demandCapacityRatio) * 0.2;
|
|
||||||
} else {
|
|
||||||
overloadPenalty = Math.min(1, Math.pow(demandCapacityRatio - 1, OVERLOAD_PENALTY_EXPONENT));
|
|
||||||
}
|
|
||||||
|
|
||||||
const networkLatencyPenalty = state.infrastructure.networkLatencyPenalty *
|
|
||||||
NETWORK_DEGRADATION.satisfactionPenaltyPerLatency;
|
|
||||||
consumers.satisfaction = Math.min(1, Math.max(0,
|
|
||||||
0.3 + modelQuality * 0.5 + headroomBonus - overloadPenalty - networkLatencyPenalty,
|
|
||||||
));
|
|
||||||
|
|
||||||
consumers.viralCoefficient = modelQuality > 0.5 ? 1 + (modelQuality - 0.5) * 2 : 0;
|
|
||||||
|
|
||||||
subscriptionRevenue = consumers.totalSubscribers * (chatProduct.pricing.subscriptionPrice / 86400);
|
|
||||||
|
|
||||||
// --- Overload policy ---
|
|
||||||
const policy = state.market.overloadPolicy;
|
|
||||||
if (policy.degradeQualityUnderLoad && demandCapacityRatio > 0.85) {
|
|
||||||
consumers.satisfaction = Math.max(0, consumers.satisfaction - 0.02);
|
|
||||||
}
|
|
||||||
if (policy.prioritizeEnterprise && demandCapacityRatio > 0.9) {
|
|
||||||
consumers.satisfaction = Math.max(0, consumers.satisfaction - 0.01);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- B2B API market ---
|
|
||||||
const enterprise = { ...state.market.enterprise };
|
|
||||||
let apiRevenue = 0;
|
|
||||||
let organicApiTokens = 0;
|
|
||||||
|
|
||||||
if (textApi?.isActive && modelQuality > 0) {
|
|
||||||
const reputationFactor = state.reputation.score / 100;
|
|
||||||
const qualityFactor = modelQuality;
|
|
||||||
const priceFactor = Math.max(0.1, 1 - (textApi.pricing.outputTokenPrice / 20));
|
|
||||||
|
|
||||||
organicApiTokens = Math.floor(
|
|
||||||
qualityFactor * reputationFactor * priceFactor * 500 * (1 + state.meta.tickCount * 0.0001),
|
|
||||||
);
|
|
||||||
|
|
||||||
let contractTokens = 0;
|
|
||||||
for (const contract of enterprise.activeContracts) {
|
|
||||||
contractTokens += contract.tokensPerTick;
|
|
||||||
apiRevenue += (contract.tokensPerTick / 1_000_000) * contract.pricePerMToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
apiRevenue += (organicApiTokens / 1_000_000) * textApi.pricing.outputTokenPrice;
|
|
||||||
enterprise.totalApiCallsPerTick = (organicApiTokens + contractTokens) / API_TOKENS_PER_REQUEST;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalTokenDemand = organicApiTokens +
|
|
||||||
consumers.totalSubscribers * CONSUMER_TOKENS_PER_SUBSCRIBER +
|
|
||||||
enterprise.activeContracts.reduce((s, c) => s + c.tokensPerTick, 0);
|
|
||||||
|
|
||||||
// --- Open source effects ---
|
|
||||||
const openSourceCount = state.market.openSourcedModels.length;
|
|
||||||
if (openSourceCount > 0) {
|
|
||||||
const growthBoost = 1 + openSourceCount * OPEN_SOURCE_TALENT_ATTRACTION;
|
|
||||||
consumers.totalSubscribers *= growthBoost > 1 ? 1 + (growthBoost - 1) * 0.01 : 1;
|
|
||||||
apiRevenue *= 1 - openSourceCount * OPEN_SOURCE_REVENUE_PENALTY * 0.3;
|
|
||||||
|
|
||||||
const eraCapBase = MARKET_SIZE_CAP[state.meta.currentEra] ?? 100_000_000;
|
|
||||||
const effectiveCap = eraCapBase
|
|
||||||
* (1 + modelQuality * MARKET_CAP_QUALITY_BONUS)
|
|
||||||
* (1 + (state.reputation.score / 100) * MARKET_CAP_REPUTATION_BONUS);
|
|
||||||
consumers.totalSubscribers = Math.min(effectiveCap, consumers.totalSubscribers);
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscriberHistory = [...(state.market.subscriberHistory || [])];
|
|
||||||
if (state.meta.tickCount % 60 === 0) {
|
|
||||||
subscriberHistory.push({ tick: state.meta.tickCount, subscribers: consumers.totalSubscribers });
|
|
||||||
if (subscriberHistory.length > 500) subscriberHistory.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
marketState: {
|
|
||||||
...state.market,
|
|
||||||
consumers,
|
|
||||||
enterprise,
|
|
||||||
subscriberHistory,
|
|
||||||
},
|
|
||||||
apiRevenue: Math.max(0, apiRevenue),
|
|
||||||
subscriptionRevenue,
|
|
||||||
totalTokenDemand,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { DCTier, DCTierConfig, RackSkuId, RackSkuConfig, SwitchTier, SwitchTierConfig, CampusTierCost, ClusterCostConfig, CoolingType, CoolingTypeConfig, NetworkFabric, NetworkFabricConfig } from '../types/infrastructure';
|
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 TICK_INTERVAL_MS = 1000;
|
||||||
export const MAX_OFFLINE_TICKS = 86_400;
|
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_PROBABILITY_BASE = 0.0002;
|
||||||
export const SAFETY_INCIDENT_REPUTATION_HIT = 15;
|
export const SAFETY_INCIDENT_REPUTATION_HIT = 15;
|
||||||
export const LOW_SAFETY_THRESHOLD = 40;
|
export const LOW_SAFETY_THRESHOLD = 40;
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// MARKET SYSTEM v2 — Shared TAM, Tiered Products, Enterprise Pipeline
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
// --- Shared TAM ---
|
||||||
|
|
||||||
|
export const TAM_BASE_SIZES: Record<Era, Record<TAMSegmentId, number>> = {
|
||||||
|
startup: { consumer: 50_000, developer: 5_000, enterprise: 500, government: 50 },
|
||||||
|
scaleup: { consumer: 5_000_000, developer: 200_000, enterprise: 5_000, government: 500 },
|
||||||
|
bigtech: { consumer: 50_000_000, developer: 2_000_000, enterprise: 50_000, government: 5_000 },
|
||||||
|
agi: { consumer: 500_000_000, developer: 20_000_000, enterprise: 200_000, government: 20_000 },
|
||||||
|
};
|
||||||
|
export const TAM_GROWTH_PER_TICK = 0.0001;
|
||||||
|
export const SHARE_TEMPERATURE = 4.0;
|
||||||
|
export const SHARE_MIGRATION_SPEED = 0.03;
|
||||||
|
|
||||||
|
// --- Attractiveness Weights ---
|
||||||
|
|
||||||
|
export const ATTRACTIVENESS_WEIGHTS: Record<TAMSegmentId, Record<string, number>> = {
|
||||||
|
consumer: { quality: 0.30, price: 0.35, reputation: 0.15, ecosystem: 0.05, freshness: 0.10, freeTier: 0.05 },
|
||||||
|
developer: { quality: 0.25, price: 0.25, reputation: 0.10, ecosystem: 0.25, freshness: 0.10, freeTier: 0.05 },
|
||||||
|
enterprise: { quality: 0.35, price: 0.15, reputation: 0.25, ecosystem: 0.10, freshness: 0.10, freeTier: 0.05 },
|
||||||
|
government: { quality: 0.25, price: 0.10, reputation: 0.35, ecosystem: 0.05, freshness: 0.10, freeTier: 0.15 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Consumer Tier Defaults ---
|
||||||
|
|
||||||
|
export const CONSUMER_TIER_DEFAULTS: Record<ConsumerTierId, { price: number; tokenAllowance: number; requiredQuality: number }> = {
|
||||||
|
free: { price: 0, tokenAllowance: 5_000, requiredQuality: 0 },
|
||||||
|
plus: { price: 20, tokenAllowance: 50_000, requiredQuality: 20 },
|
||||||
|
pro: { price: 50, tokenAllowance: 200_000, requiredQuality: 40 },
|
||||||
|
team: { price: 30, tokenAllowance: 100_000, requiredQuality: 30 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONSUMER_TIER_ORDER: ConsumerTierId[] = ['free', 'plus', 'pro', 'team'];
|
||||||
|
|
||||||
|
export const CONVERSION_RATES: Record<string, number> = {
|
||||||
|
'free->plus': 0.002,
|
||||||
|
'plus->pro': 0.0008,
|
||||||
|
'pro->team': 0.0003,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIER_CHURN_RATES: Record<ConsumerTierId, number> = {
|
||||||
|
free: 0.0005,
|
||||||
|
plus: 0.001,
|
||||||
|
pro: 0.0006,
|
||||||
|
team: 0.0004,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FREE_TIER_ADOPTION_RATE = 0.05;
|
||||||
|
|
||||||
|
// --- API Tier Defaults ---
|
||||||
|
|
||||||
|
export const API_TIER_DEFAULTS: Record<ApiTierId, { monthlyFee: number; inputPrice: number; outputPrice: number; rateLimit: number }> = {
|
||||||
|
free: { monthlyFee: 0, inputPrice: 0, outputPrice: 0, rateLimit: 10 },
|
||||||
|
payg: { monthlyFee: 0, inputPrice: 1.0, outputPrice: 3.0, rateLimit: 100 },
|
||||||
|
scale: { monthlyFee: 500, inputPrice: 0.8, outputPrice: 2.4, rateLimit: 1000 },
|
||||||
|
'enterprise-api': { monthlyFee: 5000, inputPrice: 0.6, outputPrice: 1.8, rateLimit: 10000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_TIER_ORDER: ApiTierId[] = ['free', 'payg', 'scale', 'enterprise-api'];
|
||||||
|
|
||||||
|
export const API_TIER_CHURN_RATES: Record<ApiTierId, number> = {
|
||||||
|
free: 0.0003,
|
||||||
|
payg: 0.001,
|
||||||
|
scale: 0.0005,
|
||||||
|
'enterprise-api': 0.0003,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_CONVERSION_RATES: Record<string, number> = {
|
||||||
|
'free->payg': 0.003,
|
||||||
|
'payg->scale': 0.001,
|
||||||
|
'scale->enterprise-api': 0.0004,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const API_TOKENS_PER_DEVELOPER_PER_TICK: Record<ApiTierId, number> = {
|
||||||
|
free: 0.5,
|
||||||
|
payg: 5,
|
||||||
|
scale: 50,
|
||||||
|
'enterprise-api': 200,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Code Assistant ---
|
||||||
|
|
||||||
|
export const CODE_ASSISTANT_MIN_CODING_SCORE = 40;
|
||||||
|
export const CODE_ASSISTANT_DEFAULT_PRICE = 20;
|
||||||
|
export const CODE_ASSISTANT_BASE_ADOPTION_RATE = 0.003;
|
||||||
|
export const CODE_ASSISTANT_CHURN_RATE = 0.0008;
|
||||||
|
|
||||||
|
// --- Agents Platform ---
|
||||||
|
|
||||||
|
export const AGENTS_PLATFORM_MIN_AGENTS_SCORE = 50;
|
||||||
|
export const AGENTS_PLATFORM_DEFAULT_PRICE = 100;
|
||||||
|
export const AGENTS_PLATFORM_BASE_ADOPTION_RATE = 0.002;
|
||||||
|
export const AGENTS_PLATFORM_CHURN_RATE = 0.0005;
|
||||||
|
|
||||||
|
// --- Enterprise Pipeline ---
|
||||||
|
|
||||||
|
export const BASE_LEAD_RATE = 0.005;
|
||||||
|
export const LEAD_EXPIRY_TICKS = 600;
|
||||||
|
|
||||||
|
export const PIPELINE_STAGE_TIMEOUTS: Record<EnterprisePipelineStage, number> = {
|
||||||
|
lead: 300,
|
||||||
|
qualification: 200,
|
||||||
|
poc: 400,
|
||||||
|
negotiation: 300,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PIPELINE_TRANSITION_RATES: Record<string, number> = {
|
||||||
|
'lead->qualification': 0.02,
|
||||||
|
'qualification->poc': 0.015,
|
||||||
|
'poc->negotiation': 0.01,
|
||||||
|
'negotiation->active': 0.008,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SLA_PENALTY_FRACTION = 0.02;
|
||||||
|
export const CONTRACT_BASE_DURATION_TICKS = 2400;
|
||||||
|
|
||||||
|
export const ENTERPRISE_DEAL_VALUES: Record<EnterpriseSegment, { min: number; max: number }> = {
|
||||||
|
startup: { min: 5_000, max: 50_000 },
|
||||||
|
'mid-market': { min: 50_000, max: 500_000 },
|
||||||
|
enterprise: { min: 500_000, max: 5_000_000 },
|
||||||
|
government: { min: 1_000_000, max: 10_000_000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ENTERPRISE_SLA_REQUIREMENTS: Record<EnterpriseSegment, number> = {
|
||||||
|
startup: 0.95,
|
||||||
|
'mid-market': 0.97,
|
||||||
|
enterprise: 0.99,
|
||||||
|
government: 0.995,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ENTERPRISE_CAPABILITY_REQUIREMENTS: Record<EnterpriseSegment, number> = {
|
||||||
|
startup: 15,
|
||||||
|
'mid-market': 30,
|
||||||
|
enterprise: 50,
|
||||||
|
government: 45,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ENTERPRISE_TOKENS_PER_TICK: Record<EnterpriseSegment, { min: number; max: number }> = {
|
||||||
|
startup: { min: 50, max: 500 },
|
||||||
|
'mid-market': { min: 500, max: 5_000 },
|
||||||
|
enterprise: { min: 5_000, max: 50_000 },
|
||||||
|
government: { min: 2_000, max: 20_000 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONTRACT_DURATION_BY_SEGMENT: Record<EnterpriseSegment, number> = {
|
||||||
|
startup: 1200,
|
||||||
|
'mid-market': 2400,
|
||||||
|
enterprise: 4800,
|
||||||
|
government: 7200,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Developer Ecosystem ---
|
||||||
|
|
||||||
|
export const BASE_DEV_GROWTH = 0.001;
|
||||||
|
export const FREE_TIER_DEV_MULTIPLIER = 0.0005;
|
||||||
|
export const OPEN_SOURCE_DEV_BOOST = 0.05;
|
||||||
|
export const DEV_REL_EFFECTIVENESS = 0.00001;
|
||||||
|
export const SDK_GROWTH_BONUS = 0.01;
|
||||||
|
|
||||||
|
export const DEV_ECOSYSTEM_WEIGHTS = {
|
||||||
|
communitySize: 0.30,
|
||||||
|
activeRatio: 0.20,
|
||||||
|
sdkCoverage: 0.20,
|
||||||
|
docQuality: 0.15,
|
||||||
|
openSource: 0.15,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STARTUP_ADOPTION_PER_DEV = 0.005;
|
||||||
|
export const ENTERPRISE_REFERRAL_PER_STARTUP = 0.01;
|
||||||
|
|
||||||
|
// --- Seasonal Demand ---
|
||||||
|
|
||||||
|
export const SEASONAL_CYCLE_TICKS = 360;
|
||||||
|
|
||||||
|
export const SEASONAL_MULTIPLIERS: Record<SeasonalPhase, Record<string, number>> = {
|
||||||
|
q1: { consumer: 0.90, api: 0.85, enterprise: 0.75 },
|
||||||
|
q2: { consumer: 1.00, api: 1.00, enterprise: 1.00 },
|
||||||
|
q3: { consumer: 0.95, api: 0.95, enterprise: 0.85 },
|
||||||
|
q4: { consumer: 1.10, api: 1.05, enterprise: 1.40 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Technology Obsolescence ---
|
||||||
|
|
||||||
|
export const OBSOLESCENCE_BASELINE_GROWTH = 0.01;
|
||||||
|
|
||||||
|
export const OBSOLESCENCE_ERA_ACCELERATOR: Record<Era, number> = {
|
||||||
|
startup: 1,
|
||||||
|
scaleup: 1.5,
|
||||||
|
bigtech: 2.5,
|
||||||
|
agi: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FRESHNESS_DECAY_RATE = 0.001;
|
||||||
|
export const NEW_MODEL_BOOST_VALUE = 0.15;
|
||||||
|
export const NEW_MODEL_BOOST_TICKS = 200;
|
||||||
|
export const OBSOLESCENCE_PENALTY_WEIGHT = 0.5;
|
||||||
|
|
||||||
|
// --- Competitor Market Behavior ---
|
||||||
|
|
||||||
|
export const COMPETITOR_PRODUCT_THRESHOLDS = {
|
||||||
|
freeTierAndChat: 20,
|
||||||
|
apiAndCodeAssistant: 40,
|
||||||
|
proTierAndEnterprise: 60,
|
||||||
|
agentsPlatform: 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const COMPETITOR_CATCHUP_SHARE_THRESHOLD = 0.05;
|
||||||
|
export const COMPETITOR_CATCHUP_PRICE_CUT = 0.3;
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ export interface Competitor {
|
|||||||
latestModelName: string;
|
latestModelName: string;
|
||||||
completedMilestones: string[];
|
completedMilestones: string[];
|
||||||
nextMilestoneAtTick: number;
|
nextMilestoneAtTick: number;
|
||||||
|
|
||||||
|
products: CompetitorProducts;
|
||||||
|
pricingStrategy: CompetitorPricing;
|
||||||
|
modelFreshness: number;
|
||||||
|
lastModelReleaseTick: number;
|
||||||
|
developerEcosystemScore: number;
|
||||||
|
marketShares: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CompetitorArchetype = 'safety-first' | 'move-fast' | 'big-tech' | 'open-source' | 'stealth-startup';
|
export type CompetitorArchetype = 'safety-first' | 'move-fast' | 'big-tech' | 'open-source' | 'stealth-startup';
|
||||||
@@ -29,6 +36,22 @@ export interface CompetitorPersonality {
|
|||||||
riskTolerance: number;
|
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 = {
|
export const INITIAL_COMPETITORS: CompetitorState = {
|
||||||
rivals: [],
|
rivals: [],
|
||||||
industryBenchmark: 0,
|
industryBenchmark: 0,
|
||||||
|
|||||||
@@ -58,4 +58,4 @@ export const INITIAL_SETTINGS: GameSettings = {
|
|||||||
sfxVolume: 0.7,
|
sfxVolume: 0.7,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SAVE_VERSION = 6;
|
export const SAVE_VERSION = 7;
|
||||||
|
|||||||
@@ -1,48 +1,177 @@
|
|||||||
export interface MarketState {
|
import type { Era } from './gameState';
|
||||||
consumers: ConsumerMarket;
|
|
||||||
enterprise: EnterpriseMarket;
|
// --- Seasonal ---
|
||||||
overloadPolicy: OverloadPolicy;
|
|
||||||
openSourcedModels: string[];
|
export type SeasonalPhase = 'q1' | 'q2' | 'q3' | 'q4';
|
||||||
subscriberHistory: { tick: number; subscribers: number }[];
|
|
||||||
|
// --- TAM (Total Addressable Market) ---
|
||||||
|
|
||||||
|
export type TAMSegmentId = 'consumer' | 'developer' | 'enterprise' | 'government';
|
||||||
|
|
||||||
|
export interface TAMSegment {
|
||||||
|
totalSize: number;
|
||||||
|
shares: MarketShareEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsumerMarket {
|
export interface MarketShareEntry {
|
||||||
totalSubscribers: number;
|
playerId: string;
|
||||||
churnRatePerTick: number;
|
sharePercent: number;
|
||||||
growthRatePerTick: number;
|
customers: number;
|
||||||
|
attractivenessScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TotalAddressableMarket {
|
||||||
|
segments: Record<TAMSegmentId, TAMSegment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Consumer Tiers ---
|
||||||
|
|
||||||
|
export type ConsumerTierId = 'free' | 'plus' | 'pro' | 'team';
|
||||||
|
|
||||||
|
export interface ConsumerTierConfig {
|
||||||
|
id: ConsumerTierId;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
tokenAllowance: number;
|
||||||
|
requiredModelQuality: number;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsumerTierRuntime {
|
||||||
|
config: ConsumerTierConfig;
|
||||||
|
userCount: number;
|
||||||
|
conversionRateFromBelow: number;
|
||||||
|
churnRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConsumerTierState {
|
||||||
|
tiers: Record<ConsumerTierId, ConsumerTierRuntime>;
|
||||||
|
totalUsers: number;
|
||||||
satisfaction: number;
|
satisfaction: number;
|
||||||
viralCoefficient: number;
|
viralCoefficient: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseMarket {
|
// --- API Tiers ---
|
||||||
activeContracts: EnterpriseContract[];
|
|
||||||
pendingRFPs: EnterpriseRFP[];
|
export type ApiTierId = 'free' | 'payg' | 'scale' | 'enterprise-api';
|
||||||
totalApiCallsPerTick: number;
|
|
||||||
averageTokensPerCall: number;
|
export interface ApiTierConfig {
|
||||||
|
id: ApiTierId;
|
||||||
|
name: string;
|
||||||
|
monthlyFee: number;
|
||||||
|
inputTokenPrice: number;
|
||||||
|
outputTokenPrice: number;
|
||||||
|
rateLimit: number;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiTierRuntime {
|
||||||
|
config: ApiTierConfig;
|
||||||
|
developerCount: number;
|
||||||
|
tokensPerTick: number;
|
||||||
|
churnRate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiTierState {
|
||||||
|
tiers: Record<ApiTierId, ApiTierRuntime>;
|
||||||
|
totalDevelopers: number;
|
||||||
|
totalTokensPerTick: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- New Product Lines ---
|
||||||
|
|
||||||
|
export interface CodeAssistantState {
|
||||||
|
isUnlocked: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
pricePerSeat: number;
|
||||||
|
seats: number;
|
||||||
|
qualityScore: number;
|
||||||
|
satisfaction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentsPlatformState {
|
||||||
|
isUnlocked: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
pricePerSeat: number;
|
||||||
|
seats: number;
|
||||||
|
qualityScore: number;
|
||||||
|
satisfaction: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Enterprise Pipeline ---
|
||||||
|
|
||||||
|
export type EnterprisePipelineStage = 'lead' | 'qualification' | 'poc' | 'negotiation';
|
||||||
|
|
||||||
|
export type EnterpriseSegment = 'startup' | 'mid-market' | 'enterprise' | 'government';
|
||||||
|
|
||||||
|
export interface EnterpriseLead {
|
||||||
|
id: string;
|
||||||
|
companyName: string;
|
||||||
|
segment: EnterpriseSegment;
|
||||||
|
stage: EnterprisePipelineStage;
|
||||||
|
enteredStageAtTick: number;
|
||||||
|
dealValue: number;
|
||||||
|
tokensPerTick: number;
|
||||||
|
requiredCapability: number;
|
||||||
|
requiredSlaUptime: number;
|
||||||
|
requiredSafetyScore: number;
|
||||||
|
winProbability: number;
|
||||||
|
expiresAtTick: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseContract {
|
export interface EnterpriseContract {
|
||||||
id: string;
|
id: string;
|
||||||
customerName: string;
|
customerName: string;
|
||||||
segment: 'startup' | 'mid-market' | 'enterprise' | 'government';
|
segment: EnterpriseSegment;
|
||||||
tokensPerTick: number;
|
tokensPerTick: number;
|
||||||
pricePerMToken: number;
|
pricePerMToken: number;
|
||||||
slaUptime: number;
|
slaUptime: number;
|
||||||
startTick: number;
|
startTick: number;
|
||||||
durationTicks: number;
|
durationTicks: number;
|
||||||
satisfaction: number;
|
satisfaction: number;
|
||||||
|
renewalProbability: number;
|
||||||
|
slaViolations: number;
|
||||||
|
slaPenaltiesPaid: number;
|
||||||
|
uptimeTicks: number;
|
||||||
|
totalTicks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseRFP {
|
export interface EnterpriseState {
|
||||||
id: string;
|
pipeline: EnterpriseLead[];
|
||||||
customerName: string;
|
activeContracts: EnterpriseContract[];
|
||||||
segment: 'startup' | 'mid-market' | 'enterprise' | 'government';
|
totalApiCallsPerTick: number;
|
||||||
requiredCapability: number;
|
averageTokensPerCall: number;
|
||||||
offeredPricePerMToken: number;
|
leadGenerationRate: number;
|
||||||
requiredSlaUptime: number;
|
|
||||||
expiresAtTick: 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 {
|
export interface OverloadPolicy {
|
||||||
maxQueueDepth: number;
|
maxQueueDepth: number;
|
||||||
rateLimitPerCustomer: number;
|
rateLimitPerCustomer: number;
|
||||||
@@ -50,19 +179,140 @@ export interface OverloadPolicy {
|
|||||||
prioritizeEnterprise: boolean;
|
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 = {
|
export const INITIAL_MARKET: MarketState = {
|
||||||
consumers: {
|
tam: {
|
||||||
totalSubscribers: 0,
|
segments: {
|
||||||
churnRatePerTick: 0.001,
|
consumer: makeInitialTAMSegment(),
|
||||||
growthRatePerTick: 0,
|
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,
|
satisfaction: 0.5,
|
||||||
viralCoefficient: 0,
|
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: {
|
enterprise: {
|
||||||
|
pipeline: [],
|
||||||
activeContracts: [],
|
activeContracts: [],
|
||||||
pendingRFPs: [],
|
|
||||||
totalApiCallsPerTick: 0,
|
totalApiCallsPerTick: 0,
|
||||||
averageTokensPerCall: 500,
|
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: {
|
overloadPolicy: {
|
||||||
maxQueueDepth: 100,
|
maxQueueDepth: 100,
|
||||||
|
|||||||
Reference in New Issue
Block a user