Fix compute utilization bug and add subscriber saturation cap
CI / build-and-push (push) Successful in 34s

Three intertwined fixes:

1. Zero-capacity utilization: when inference allocation was 0%, the
   guard clause returned 0% utilization instead of 100%, so the market
   system never penalized satisfaction and subscribers never churned.

2. Stale compute in market: restructured tick order so capacity is
   computed before market runs, giving satisfaction calculations
   current-tick demand/capacity ratio instead of previous tick's.

3. Subscriber growth: replaced pure compound growth (reached billions
   in minutes) with logistic saturation curve. Era-based market caps:
   startup 10K, scaleup 1M, bigtech 20M, agi 100M. Quality and
   reputation expand the effective cap.

Also tuned FLOPS-to-tokens multiplier (10 → 26) for balanced
demand/capacity feel across all eras, and added market saturation
indicator to the Market page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 20:50:26 -04:00
parent a36617f9e3
commit 900d1d5190
5 changed files with 134 additions and 41 deletions
+33 -2
View File
@@ -1,5 +1,8 @@
import { useGameStore } from '@/store';
import { formatNumber, formatMoney, formatPercent } from '@ai-tycoon/shared';
import {
formatNumber, formatMoney, formatPercent,
MARKET_SIZE_CAP, MARKET_CAP_QUALITY_BONUS, MARKET_CAP_REPUTATION_BONUS,
} from '@ai-tycoon/shared';
import { Users, Zap, Shield, TrendingUp, Settings2 } from 'lucide-react';
export function MarketPage() {
@@ -10,9 +13,21 @@ export function MarketPage() {
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
const tokensCapacity = useGameStore((s) => s.compute.tokensPerSecondCapacity);
const tokensDemand = useGameStore((s) => s.compute.tokensPerSecondDemand);
const currentEra = useGameStore((s) => s.meta.currentEra);
const reputationScore = useGameStore((s) => s.reputation.score);
const deployedModels = useGameStore((s) => s.models.trainedModels.filter(m => m.isDeployed));
const setProductPricing = useGameStore((s) => s.setProductPricing);
const setOverloadPolicy = useGameStore((s) => s.setOverloadPolicy);
const bestQuality = deployedModels.length > 0
? Math.max(...deployedModels.map(m => m.benchmarkScore)) / 100
: 0;
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');
@@ -20,7 +35,7 @@ export function MarketPage() {
<div className="space-y-6">
<h2 className="text-2xl font-bold">Market</h2>
<div className="grid grid-cols-3 gap-4">
<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" />
@@ -55,6 +70,22 @@ export function MarketPage() {
{formatNumber(tokensDemand)} / {formatNumber(tokensCapacity)} tok/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">
<TrendingUp size={16} className="text-purple-400" />
<span className="text-xs text-surface-400 uppercase">Market Saturation</span>
</div>
<div className="text-2xl font-bold font-mono">{formatPercent(saturation)}</div>
<div className="text-xs text-surface-400 mt-1">
Cap: {formatNumber(effectiveCap)} ({currentEra})
</div>
<div className="h-1.5 bg-surface-800 rounded-full mt-2 overflow-hidden">
<div
className={`h-full rounded-full ${saturation > 0.9 ? 'bg-danger' : saturation > 0.7 ? 'bg-warning' : 'bg-accent'}`}
style={{ width: `${Math.min(100, saturation * 100)}%` }}
/>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4">