Overhaul rack system with split FLOPS, VRAM, cooling, interconnect, and multi-vendor SKUs
CI / build-and-push (push) Successful in 29s

Expand from 10 to 18 rack SKUs across NVIDIA, AMD, and custom ASIC vendors, each with
distinct training vs inference FLOPS, VRAM capacity, cooling requirements, and interconnect
technology. Adds cooling hierarchy (air/liquid/immersion) that gates rack deployment, VRAM
requirements that gate model training by generation, interconnect multipliers for distributed
training scaling, and PUE-based energy cost reduction for advanced cooling. Includes save
migration from v4 to v5, 6 new research nodes, and UI updates showing split compute stats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 02:27:03 -04:00
parent 54220fca70
commit fc1f371c8c
12 changed files with 749 additions and 100 deletions
@@ -62,9 +62,14 @@ export function StateInspectionTab() {
<Section title="Compute">
<Stat label="Total FLOPS" value={formatFlops(compute.totalFlops)} />
<Stat label="Training FLOPS" value={formatFlops(compute.totalTrainingFlops)} />
<Stat label="Inference FLOPS" value={formatFlops(compute.totalInferenceFlops)} />
<Stat label="Eff. Training" value={formatFlops(compute.effectiveTrainingFlops)} />
<Stat label="Eff. Inference" value={formatFlops(compute.effectiveInferenceFlops)} />
<Stat label="VRAM" value={`${formatNumber(compute.totalVramGB)} GB`} />
<Stat label="Utilization" value={formatPercent(compute.inferenceUtilization)} />
<Stat label="Training" value={formatPercent(compute.trainingAllocation)} />
<Stat label="Inference" value={formatPercent(compute.inferenceAllocation)} />
<Stat label="Training Alloc" value={formatPercent(compute.trainingAllocation)} />
<Stat label="Inference Alloc" value={formatPercent(compute.inferenceAllocation)} />
<Stat label="Capacity" value={`${formatNumber(compute.tokensPerSecondCapacity)} tok/s`} />
<Stat label="Demand" value={`${formatNumber(compute.tokensPerSecondDemand)} tok/s`} />
</Section>
@@ -85,6 +90,9 @@ export function StateInspectionTab() {
<Stat label="Racks Failed" value={totalFailedRacks} />
<Stat label="In Pipeline" value={pipelineRacks} />
<Stat label="Total FLOPS" value={formatFlops(infrastructure.totalFlops)} />
<Stat label="Training FLOPS" value={formatFlops(infrastructure.totalTrainingFlops)} />
<Stat label="Inference FLOPS" value={formatFlops(infrastructure.totalInferenceFlops)} />
<Stat label="Total VRAM" value={`${formatNumber(infrastructure.totalVramGB)} GB`} />
</Section>
<Section title="Reputation">
+31 -11
View File
@@ -17,6 +17,7 @@ import {
estimateNetworkSlots, maxComputeRacks,
SWITCH_TIER_CONFIGS,
DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
skuTotalFlops,
} from '@ai-tycoon/shared';
import type {
DCTier, RackSkuId, LocationId, PipelineStage, Era,
@@ -357,7 +358,7 @@ function ClusterFillAllModal({ cluster, money, era, research, onConfirm, onClose
}) {
const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(s => {
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(s.era)) return false;
if (s.requiredResearch && !research.includes(s.requiredResearch)) return false;
if (s.requiredResearch.length > 0 && !s.requiredResearch.every(r => research.includes(r))) return false;
return true;
});
@@ -540,7 +541,7 @@ function ClusterDetailView({ clusterId }: { clusterId: string }) {
<div><span className="text-surface-400">FLOPS:</span> <span className="font-mono">{
formatNumber(campus.dataCenters.reduce((s, d) => {
const sku = d.rackSkuId ? RACK_SKU_CONFIGS[d.rackSkuId] : null;
return s + (sku ? d.effectiveComputeRacks * sku.flopsPerRack : 0);
return s + (sku ? d.effectiveComputeRacks * skuTotalFlops(sku) : 0);
}, 0))
}</span></div>
</div>
@@ -644,7 +645,7 @@ function FillAllDCsModal({ campus, money, era, research, onConfirm, onClose }: {
}) {
const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(s => {
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(s.era)) return false;
if (s.requiredResearch && !research.includes(s.requiredResearch)) return false;
if (s.requiredResearch.length > 0 && !s.requiredResearch.every(r => research.includes(r))) return false;
return true;
});
@@ -754,7 +755,7 @@ function RetrofitCampusModal({ campus, era, research, onConfirm, onClose }: {
const targetSkus = Object.values(RACK_SKU_CONFIGS).filter(s => {
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(s.era)) return false;
if (s.requiredResearch && !research.includes(s.requiredResearch)) return false;
if (s.requiredResearch.length > 0 && !s.requiredResearch.every(r => research.includes(r))) return false;
return true;
});
@@ -810,7 +811,7 @@ function RetrofitCampusModal({ campus, era, research, onConfirm, onClose }: {
}`}>
<div>
<div className="font-medium">{s.name}</div>
<div className="text-xs text-surface-400">{s.flopsPerRack} FLOPS | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}/rack</div>
<div className="text-xs text-surface-400">{s.trainingFlops}T / {s.inferenceFlops}I FLOPS | {s.totalVramGB}GB | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}/rack</div>
</div>
{isCurrentOnly && <span className="text-xs text-surface-500">Current</span>}
{selectedSku === s.id && <CheckCircle size={16} className="text-violet-400" />}
@@ -1140,7 +1141,7 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(s => {
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(s.era)) return false;
if (s.requiredResearch && !research.includes(s.requiredResearch)) return false;
if (s.requiredResearch.length > 0 && !s.requiredResearch.every(r => research.includes(r))) return false;
if (dc.rackSkuId && dc.rackSkuId !== s.id) return false;
return true;
});
@@ -1168,9 +1169,10 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
</div>
{/* Stats Grid */}
<div className="grid grid-cols-4 gap-3">
<div className="grid grid-cols-5 gap-3">
<FleetStat icon={Cpu} label="Online" value={formatNumber(dc.computeRacksOnline)} sub={`of ${maxCompute} max compute`} />
<FleetStat icon={Zap} label="FLOPS" value={formatNumber(sku ? dc.effectiveComputeRacks * sku.flopsPerRack : 0)} />
<FleetStat icon={Zap} label="FLOPS" value={`${formatNumber(dc.dcTrainingFlops)}T / ${formatNumber(dc.dcInferenceFlops)}I`} />
<FleetStat icon={HardDrive} label="VRAM" value={`${formatNumber(dc.dcTotalVramGB)} GB`} />
<FleetStat icon={Activity} label="Uptime" value={formatPercent(dc.currentUptime)} />
<FleetStat icon={DollarSign} label="Cost/s" value={formatMoney(dc.energyCostPerTick + dc.maintenanceCostPerTick)} />
</div>
@@ -1240,7 +1242,7 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
<input type="radio" name="sku" checked={selectedSku === s.id} onChange={() => setSelectedSku(s.id)} className="accent-accent" />
<div className="flex-1">
<div className="font-medium text-sm">{s.name}</div>
<div className="text-xs text-surface-400">{s.flopsPerRack} FLOPS | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}</div>
<div className="text-xs text-surface-400">{s.trainingFlops}T / {s.inferenceFlops}I FLOPS | {s.totalVramGB}GB | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}</div>
</div>
</label>
))}
@@ -1311,14 +1313,14 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
{Object.values(RACK_SKU_CONFIGS).filter(s => {
if (s.id === dc.rackSkuId) return false;
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(s.era)) return false;
if (s.requiredResearch && !research.includes(s.requiredResearch)) return false;
if (s.requiredResearch.length > 0 && !s.requiredResearch.every(r => research.includes(r))) return false;
return true;
}).map(s => (
<button key={s.id} onClick={() => setConfirmRetrofit(s.id)}
className="w-full flex items-center justify-between p-3 rounded-lg border border-surface-600 hover:border-accent/50 text-left">
<div>
<div className="font-medium text-sm">{s.name}</div>
<div className="text-xs text-surface-400">{s.flopsPerRack} FLOPS | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}/rack</div>
<div className="text-xs text-surface-400">{s.trainingFlops}T / {s.inferenceFlops}I FLOPS | {s.totalVramGB}GB | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}/rack</div>
</div>
<RefreshCw size={14} className="text-surface-400" />
</button>
@@ -1332,6 +1334,24 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
{/* Upgrades Tab */}
{activeTab === 'upgrades' && (
<div className="bg-surface-800 border border-surface-700 rounded-xl p-4 space-y-4">
{/* Cooling & Network Fabric */}
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center gap-3 p-3 border border-surface-600 rounded-lg">
<Thermometer size={18} className="text-cyan-400" />
<div>
<div className="text-xs text-surface-400">Cooling Type</div>
<div className="font-medium text-sm capitalize">{dc.coolingType}</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 border border-surface-600 rounded-lg">
<Network size={18} className="text-blue-400" />
<div>
<div className="text-xs text-surface-400">Network Fabric</div>
<div className="font-medium text-sm">{dc.networkFabric}</div>
</div>
</div>
</div>
{(['cooling', 'redundancy'] as const).map(upgrade => {
const level = upgrade === 'cooling' ? dc.coolingLevel : dc.redundancyLevel;
const cost = tierConfig.baseCost * DC_UPGRADE_COST_FRACTION;
+15 -2
View File
@@ -2,7 +2,7 @@ import { useState } from 'react';
import { Brain, Play, Rocket, Globe, SlidersHorizontal, ChevronDown, ChevronUp } from 'lucide-react';
import { TutorialHint } from '@/components/game/TutorialHint';
import { useGameStore } from '@/store';
import { formatNumber, formatPercent, formatDuration } from '@ai-tycoon/shared';
import { formatNumber, formatPercent, formatDuration, VRAM_REQUIREMENTS_BY_GENERATION } from '@ai-tycoon/shared';
import type { TuningPreset } from '@ai-tycoon/shared';
export function ModelsPage() {
@@ -10,6 +10,7 @@ export function ModelsPage() {
const activeTraining = useGameStore((s) => s.models.activeTraining);
const productLines = useGameStore((s) => s.models.productLines);
const totalFlops = useGameStore((s) => s.compute.totalFlops);
const totalVramGB = useGameStore((s) => s.compute.totalVramGB);
const trainingAlloc = useGameStore((s) => s.compute.trainingAllocation);
const totalData = useGameStore((s) => s.data.totalTrainingTokens);
const startTraining = useGameStore((s) => s.startTraining);
@@ -89,6 +90,14 @@ export function ModelsPage() {
<div className="text-xs text-surface-500 mt-1">
ETA: {formatDuration(activeTraining.totalTicks - activeTraining.progressTicks)}
</div>
{(() => {
const reqVram = VRAM_REQUIREMENTS_BY_GENERATION[activeTraining.generation] ?? 0;
return reqVram > 0 && totalVramGB < reqVram ? (
<p className="text-xs text-error mt-2">
Training stalled requires {formatNumber(reqVram)} GB VRAM (have {formatNumber(totalVramGB)} GB). Deploy more GPU racks.
</p>
) : null;
})()}
</div>
) : (
<div className="space-y-3">
@@ -102,11 +111,15 @@ export function ModelsPage() {
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
<div className="grid grid-cols-3 gap-3 text-sm">
<div className="grid grid-cols-4 gap-3 text-sm">
<div className="bg-surface-800 rounded-lg p-3">
<div className="text-xs text-surface-400">Training Compute</div>
<div className="font-mono">{formatNumber(trainingFlops)} FLOPS</div>
</div>
<div className="bg-surface-800 rounded-lg p-3">
<div className="text-xs text-surface-400">Available VRAM</div>
<div className="font-mono">{formatNumber(totalVramGB)} GB</div>
</div>
<div className="bg-surface-800 rounded-lg p-3">
<div className="text-xs text-surface-400">Training Data</div>
<div className="font-mono">{formatNumber(totalData)} tokens</div>
+85 -6
View File
@@ -10,6 +10,7 @@ import type {
ActiveResearch, OwnedDataset, LocationId,
DeploymentCohort, PipelineStage,
CampusRetrofitQueue,
CoolingType, NetworkFabric,
} from '@ai-tycoon/shared';
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
import {
@@ -27,6 +28,7 @@ import {
LOCATION_CONFIGS,
estimateNetworkSlots, maxComputeRacks,
uuid,
COOLING_TYPE_CONFIGS, COOLING_ORDER, NETWORK_FABRIC_CONFIGS, FABRIC_ORDER,
} from '@ai-tycoon/shared';
import {
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
@@ -93,6 +95,8 @@ interface Actions {
startCampusRetrofit: (campusId: string, targetSkuId: RackSkuId, maxConcurrent: number) => void;
cancelCampusRetrofit: (campusId: string) => void;
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
upgradeCoolingType: (dataCenterId: string, targetCooling: CoolingType) => void;
upgradeNetworkFabric: (dataCenterId: string, targetFabric: NetworkFabric) => void;
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
deployModel: (modelId: string) => void;
setProductPricing: (productLineId: string, field: string, value: number) => void;
@@ -197,6 +201,9 @@ export function computeFillForDC(
if (dc.rackSkuId !== null && dc.rackSkuId !== skuId) return { qty: 0, cost: 0 };
const sku = RACK_SKU_CONFIGS[skuId];
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) return { qty: 0, cost: 0 };
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
@@ -414,6 +421,11 @@ export const useGameStore = create<Store>()(
retrofitState: null,
coolingLevel: 0,
redundancyLevel: 0,
coolingType: 'air' as CoolingType,
networkFabric: 'ethernet-100g' as NetworkFabric,
dcTrainingFlops: 0,
dcInferenceFlops: 0,
dcTotalVramGB: 0,
};
return {
@@ -439,7 +451,10 @@ export const useGameStore = create<Store>()(
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) return s;
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
@@ -532,6 +547,11 @@ export const useGameStore = create<Store>()(
retrofitState: null,
coolingLevel: 0,
redundancyLevel: 0,
coolingType: 'air' as CoolingType,
networkFabric: 'ethernet-100g' as NetworkFabric,
dcTrainingFlops: 0,
dcInferenceFlops: 0,
dcTotalVramGB: 0,
});
}
@@ -556,7 +576,10 @@ export const useGameStore = create<Store>()(
const sku = RACK_SKU_CONFIGS[newSkuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) return s;
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
const totalRacksToRetrofit = dc.computeRacksOnline + pipelineCount;
@@ -604,12 +627,14 @@ export const useGameStore = create<Store>()(
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
let remainingMoney = s.economy.money;
const dcUpdates = new Map<string, DeploymentCohort>();
for (const dc of found.campus.dataCenters) {
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) continue;
const { qty, cost } = computeFillForDC(dc, skuId, remainingMoney);
if (qty <= 0) continue;
@@ -649,7 +674,7 @@ export const useGameStore = create<Store>()(
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
let remainingMoney = s.economy.money;
const allDcUpdates = new Map<string, DeploymentCohort>();
@@ -657,6 +682,8 @@ export const useGameStore = create<Store>()(
for (const campus of cluster.campuses) {
if (campus.status !== 'operational') continue;
for (const dc of campus.dataCenters) {
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) continue;
const { qty, cost } = computeFillForDC(dc, skuId, remainingMoney);
if (qty <= 0) continue;
@@ -701,7 +728,7 @@ export const useGameStore = create<Store>()(
const sku = RACK_SKU_CONFIGS[targetSkuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
const eligible: string[] = [];
const skipped: string[] = [];
@@ -792,6 +819,58 @@ export const useGameStore = create<Store>()(
};
}),
upgradeCoolingType: (dataCenterId, targetCooling) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found) return s;
const { dc } = found;
if (dc.status !== 'operational') return s;
const currentIdx = COOLING_ORDER.indexOf(dc.coolingType);
const targetIdx = COOLING_ORDER.indexOf(targetCooling);
if (targetIdx <= currentIdx) return s;
// Research gates
if (targetCooling === 'liquid' && !s.research.completedResearch.includes('liquid-cooling-tech')) return s;
if (targetCooling === 'immersion' && !s.research.completedResearch.includes('immersion-cooling-tech')) return s;
const cost = COOLING_TYPE_CONFIGS[targetCooling].upgradeCost[dc.tier];
if (s.economy.money < cost) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
coolingType: targetCooling,
})),
};
}),
upgradeNetworkFabric: (dataCenterId, targetFabric) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found) return s;
const { dc } = found;
if (dc.status !== 'operational') return s;
const currentIdx = FABRIC_ORDER.indexOf(dc.networkFabric);
const targetIdx = FABRIC_ORDER.indexOf(targetFabric);
if (targetIdx <= currentIdx) return s;
// InfiniBand requires research
if ((targetFabric === 'infiniband-ndr' || targetFabric === 'infiniband-xdr')
&& !s.research.completedResearch.includes('infiniband-networking')) return s;
const cost = NETWORK_FABRIC_CONFIGS[targetFabric].upgradeCost[dc.tier];
if (s.economy.money < cost) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
networkFabric: targetFabric,
})),
};
}),
// --- Non-infrastructure actions (unchanged) ---
startTraining: (job) => set((s) => ({
@@ -979,7 +1058,7 @@ export const useGameStore = create<Store>()(
notifications: [{
id: uuid(),
title: 'Save Reset',
message: 'Your save was reset due to a major infrastructure redesign — Hypercluster scale! Build clusters, campuses, and data centers.',
message: 'Your save was reset due to a major rack system overhaul — 20 SKUs with training/inference specialization, VRAM, cooling tech, interconnects, and AMD/ASIC vendors!',
type: 'info' as const,
tick: 0,
read: false,