diff --git a/apps/web/src/components/game/CompanyStatsCard.tsx b/apps/web/src/components/game/CompanyStatsCard.tsx
index 5200a31..a01eb4d 100644
--- a/apps/web/src/components/game/CompanyStatsCard.tsx
+++ b/apps/web/src/components/game/CompanyStatsCard.tsx
@@ -19,7 +19,7 @@ export function CompanyStatsCard({ onClose }: { onClose: () => void }) {
);
const reputation = useGameStore((s) => s.reputation.score);
const achievements = useGameStore((s) => s.achievements.unlocked.length);
- const dataCenters = useGameStore((s) => s.infrastructure.dataCenters.length);
+ const dataCenters = useGameStore((s) => s.infrastructure.totalDataCenterCount);
const totalRacks = useGameStore((s) => s.infrastructure.totalRackCount);
const eraLabel = era === 'startup' ? 'Startup' : era === 'scaleup' ? 'Scale-up' : era === 'bigtech' ? 'Big Tech' : 'AGI';
diff --git a/apps/web/src/pages/DashboardPage.tsx b/apps/web/src/pages/DashboardPage.tsx
index 1603a20..da60788 100644
--- a/apps/web/src/pages/DashboardPage.tsx
+++ b/apps/web/src/pages/DashboardPage.tsx
@@ -12,7 +12,7 @@ export function DashboardPage() {
const revenuePerTick = useGameStore((s) => s.economy.revenuePerTick);
const expensesPerTick = useGameStore((s) => s.economy.expensesPerTick);
const totalFlops = useGameStore((s) => s.infrastructure.totalFlops);
- const dataCenters = useGameStore((s) => s.infrastructure.dataCenters);
+ const totalDCs = useGameStore((s) => s.infrastructure.totalDataCenterCount);
const trainedModels = useGameStore((s) => s.models.trainedModels);
const activeTraining = useGameStore((s) => s.models.activeTraining);
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
@@ -27,13 +27,13 @@ export function DashboardPage() {
Get Started
diff --git a/apps/web/src/pages/FinancePage.tsx b/apps/web/src/pages/FinancePage.tsx
index 448573b..4bf2363 100644
--- a/apps/web/src/pages/FinancePage.tsx
+++ b/apps/web/src/pages/FinancePage.tsx
@@ -33,9 +33,14 @@ export function FinancePage() {
const burnRate = expensesPerTick > revenuePerTick ? expensesPerTick - revenuePerTick : 0;
const runway = burnRate > 0 ? money / burnRate : Infinity;
- const infraCosts = infrastructure.dataCenters.reduce(
- (s, dc) => s + dc.energyCostPerTick + dc.maintenanceCostPerTick, 0,
- );
+ let infraCosts = 0;
+ for (const cluster of infrastructure.clusters) {
+ for (const campus of cluster.campuses) {
+ for (const dc of campus.dataCenters) {
+ infraCosts += dc.energyCostPerTick + dc.maintenanceCostPerTick;
+ }
+ }
+ }
const talentCosts = talent.totalSalaryPerTick;
return (
diff --git a/apps/web/src/pages/InfrastructurePage.tsx b/apps/web/src/pages/InfrastructurePage.tsx
index 0985b18..13b7d33 100644
--- a/apps/web/src/pages/InfrastructurePage.tsx
+++ b/apps/web/src/pages/InfrastructurePage.tsx
@@ -1,44 +1,39 @@
-import { useState, useMemo, useCallback } from 'react';
+import { useState, useMemo } from 'react';
import {
Plus, Server, MapPin, Zap, HardDrive, Wrench,
- ChevronDown, ChevronUp, Thermometer, Shield,
- Rocket, Clock, Lock, Trash2, ArrowUpDown, Cpu, Minus,
- Activity, DollarSign,
+ ChevronRight, Thermometer, Shield,
+ Rocket, Clock, Lock, Cpu,
+ Activity, DollarSign, Globe, Building2, Layers,
+ Network, ArrowLeft, RefreshCw, ChevronDown,
} from 'lucide-react';
import { TutorialHint } from '@/components/game/TutorialHint';
import { ConfirmModal } from '@/components/common/ConfirmModal';
-import { useGameStore } from '@/store';
-import { useShallow } from 'zustand/shallow';
+import { useGameStore, type InfraNav } from '@/store';
import {
formatMoney, formatNumber, formatPercent,
LOCATION_CONFIGS, DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
+ CAMPUS_TIER_COSTS, CLUSTER_COST_CONFIG, FIRST_CAMPUS_BUILD_TICKS,
+ networkSlotsRequired, maxComputeRacks,
+ DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
+} from '@ai-tycoon/shared';
+import type {
+ DCTier, RackSkuId, LocationId, PipelineStage, Era,
+ Cluster, Campus, DataCenter, DeploymentCohort,
} from '@ai-tycoon/shared';
-import type { DCTier, DCTierConfig, RackSkuId, LocationId, RackOrder, PipelineStage, Era, Rack } from '@ai-tycoon/shared';
const ERA_ORDER: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
-const collapsedDCs = new Set();
const STAGE_LABELS: Record = {
- ordered: 'Ordered',
- manufacturing: 'Manufacturing',
- receiving: 'Receiving',
- installation: 'Installation',
- testing: 'Testing',
- repair: 'Repair',
- decommission: 'Decom',
+ ordered: 'Ordered', manufacturing: 'Mfg', receiving: 'Recv',
+ installation: 'Install', testing: 'Testing', repair: 'Repair', decommission: 'Decom',
};
const STAGE_COLORS: Record = {
- ordered: 'bg-surface-600',
- manufacturing: 'bg-blue-500',
- receiving: 'bg-cyan-500',
- installation: 'bg-violet-500',
- testing: 'bg-amber-500',
- repair: 'bg-danger',
- decommission: 'bg-surface-500',
+ ordered: 'bg-surface-600', manufacturing: 'bg-blue-500', receiving: 'bg-cyan-500',
+ installation: 'bg-violet-500', testing: 'bg-amber-500', repair: 'bg-danger', decommission: 'bg-surface-500',
};
-// ─── Fleet Summary ─────────────────────────────────────────────
+// ─── Shared Components ──────────────────────────────────────────
function FleetStat({ label, value, sub, icon: Icon }: {
label: string; value: string; sub?: string; icon: typeof Server;
@@ -55,941 +50,885 @@ function FleetStat({ label, value, sub, icon: Icon }: {
);
}
-function FleetSummary() {
- const dataCenters = useGameStore((s) => s.infrastructure.dataCenters);
- const pipeline = useGameStore((s) => s.infrastructure.rackPipeline);
-
- const stats = useMemo(() => {
- const operational = dataCenters.filter(dc => dc.status === 'operational');
- const totalRacks = operational.reduce((sum, dc) => sum + dc.racks.length, 0);
- const totalSlots = operational.reduce((sum, dc) => sum + DC_TIER_CONFIGS[dc.tier].rackSlots, 0);
- const totalFlops = operational.reduce((sum, dc) =>
- sum + dc.racks.reduce((s, r) => s + RACK_SKU_CONFIGS[r.skuId].flopsPerRack, 0), 0);
- const avgUptime = operational.length > 0
- ? operational.reduce((sum, dc) => sum + dc.currentUptime, 0) / operational.length
- : 0;
- const totalCost = operational.reduce((sum, dc) => sum + dc.energyCostPerTick + dc.maintenanceCostPerTick, 0);
- const inPipeline = pipeline.filter(o => o.stage !== 'decommission').length;
- const constructing = dataCenters.length - operational.length;
-
- return { dcCount: operational.length, constructing, totalRacks, totalSlots, totalFlops, avgUptime, totalCost, inPipeline };
- }, [dataCenters, pipeline]);
-
- if (dataCenters.length === 0) return null;
-
+function CapacityBar({ label, used, total }: { label: string; used: number; total: number }) {
+ const pct = total > 0 ? (used / total) * 100 : 0;
+ const color = pct > 90 ? 'bg-danger' : pct > 70 ? 'bg-amber-500' : 'bg-green-500';
return (
-
-
0 ? `${stats.constructing} building` : 'operational'}
- icon={Server}
- />
-
- 0 ? `${stats.inPipeline} in pipeline` : undefined}
- icon={Cpu}
- />
-
-
+
+
+ {label}
+ {formatNumber(used)} / {formatNumber(total)}
+
+
);
}
-// ─── Pipeline Kanban ───────────────────────────────────────────
+function Breadcrumb({ nav }: { nav: InfraNav }) {
+ const setNav = useGameStore((s) => s.setInfraNav);
+ const clusters = useGameStore((s) => s.infrastructure.clusters);
-interface PipelineGroup {
- key: string;
- skuId: RackSkuId;
- stage: PipelineStage;
- orders: RackOrder[];
- avgProgress: number;
- stageTotal: number;
-}
+ const crumbs: { label: string; nav: InfraNav }[] = [
+ { label: 'Infrastructure', nav: { level: 'clusters' } },
+ ];
-function PipelineKanban() {
- const pipeline = useGameStore((s) => s.infrastructure.rackPipeline);
- if (pipeline.length === 0) return null;
-
- const stages: PipelineStage[] = ['ordered', 'manufacturing', 'receiving', 'installation', 'testing', 'repair', 'decommission'];
-
- const grouped = stages.map(stage => {
- const stageOrders = pipeline.filter(o => o.stage === stage);
- const skuMap = new Map();
-
- for (const order of stageOrders) {
- if (!skuMap.has(order.skuId)) skuMap.set(order.skuId, []);
- skuMap.get(order.skuId)!.push(order);
+ if (nav.level !== 'clusters' && nav.clusterId) {
+ const cluster = clusters.find(c => c.id === nav.clusterId);
+ if (cluster) {
+ crumbs.push({ label: cluster.name, nav: { level: 'cluster', clusterId: nav.clusterId } });
}
-
- const groups: PipelineGroup[] = [];
- for (const [skuId, orders] of skuMap) {
- const avgProgress = orders.reduce((s, o) => s + (o.stageTotal > 0 ? o.stageProgress / o.stageTotal : 0), 0) / orders.length;
- groups.push({ key: `${skuId}-${stage}`, skuId, stage, orders, avgProgress, stageTotal: orders[0].stageTotal });
- }
-
- return { stage, groups, count: stageOrders.length };
- });
-
- const activeOrders = pipeline.filter(o => o.stage !== 'decommission');
- const testingOrders = pipeline.filter(o => o.stage === 'testing');
- const nearestToOnline = testingOrders.length > 0
- ? Math.min(...testingOrders.map(o => o.stageTotal - o.stageProgress))
- : null;
-
- return (
-
-
-
Rack Pipeline
-
- {activeOrders.length} order{activeOrders.length !== 1 ? 's' : ''} in pipeline
- {nearestToOnline !== null && (
-
-
- Next online in ~{nearestToOnline}s
-
- )}
-
-
-
- {grouped.map(({ stage, groups, count }) => (
-
-
- {STAGE_LABELS[stage]} ({count})
-
-
- {groups.map(group => (
-
- ))}
-
-
- ))}
-
-
- );
-}
-
-function PipelineGroupCard({ group }: { group: PipelineGroup }) {
- const sku = RACK_SKU_CONFIGS[group.skuId];
- const remaining = Math.round(group.stageTotal * (1 - group.avgProgress));
- const hasRepairs = group.orders.some(o => o.repairCount > 0);
-
- return (
-
-
- {sku.name}
- {group.orders.length > 1 && ×{group.orders.length} }
-
-
-
-
- {remaining}s
-
- {hasRepairs && (
-
-
-
- )}
-
-
- );
-}
-
-// ─── Capacity Bar ──────────────────────────────────────────────
-
-function CapacityBar({ label, used, max, unit, icon: Icon }: {
- label: string; used: number; max: number; unit: string; icon: typeof HardDrive;
-}) {
- const pct = max > 0 ? used / max : 0;
- const color = pct > 0.9 ? 'bg-danger' : pct > 0.7 ? 'bg-amber-500' : 'bg-accent';
-
- return (
-
-
- {label}
- {formatNumber(used)}/{formatNumber(max)} {unit}
-
-
-
- );
-}
-
-// ─── Rack Inventory Table ──────────────────────────────────────
-
-type SortField = 'sku' | 'flops' | 'power' | 'health';
-type SortDir = 'asc' | 'desc';
-
-function SortableHeader({ field, label, align, sortField, sortDir, onToggle }: {
- field: SortField; label: string; align?: 'right';
- sortField: SortField; sortDir: SortDir; onToggle: (f: SortField) => void;
-}) {
- return (
- onToggle(field)}
- >
-
- {label}
- {sortField === field ? (
- sortDir === 'asc' ? :
- ) : (
-
- )}
-
-
- );
-}
-
-function RackTable({ racks, dcId }: { racks: Rack[]; dcId: string }) {
- const decommissionRack = useGameStore((s) => s.decommissionRack);
- const [selected, setSelected] = useState>(new Set());
- const [sortField, setSortField] = useState('sku');
- const [sortDir, setSortDir] = useState('asc');
- const [showDecomConfirm, setShowDecomConfirm] = useState(false);
-
- const sortedRacks = useMemo(() => {
- const sorted = [...racks];
- sorted.sort((a, b) => {
- const skuA = RACK_SKU_CONFIGS[a.skuId];
- const skuB = RACK_SKU_CONFIGS[b.skuId];
- let cmp = 0;
- switch (sortField) {
- case 'sku': cmp = skuA.name.localeCompare(skuB.name); break;
- case 'flops': cmp = skuA.flopsPerRack - skuB.flopsPerRack; break;
- case 'power': cmp = skuA.powerDrawKW - skuB.powerDrawKW; break;
- case 'health': cmp = (a.isHealthy ? 1 : 0) - (b.isHealthy ? 1 : 0); break;
+ if ((nav.level === 'campus' || nav.level === 'datacenter') && nav.campusId) {
+ const campus = cluster?.campuses.find(c => c.id === nav.campusId);
+ if (campus) {
+ crumbs.push({ label: campus.name, nav: { level: 'campus', clusterId: nav.clusterId, campusId: nav.campusId } });
}
- return sortDir === 'desc' ? -cmp : cmp;
- });
- return sorted;
- }, [racks, sortField, sortDir]);
-
- const toggleSort = (field: SortField) => {
- if (sortField === field) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
- else { setSortField(field); setSortDir('asc'); }
- };
-
- const toggleSelect = (id: string) => {
- setSelected(prev => {
- const next = new Set(prev);
- if (next.has(id)) next.delete(id); else next.add(id);
- return next;
- });
- };
-
- const toggleAll = () => {
- if (selected.size === racks.length) setSelected(new Set());
- else setSelected(new Set(racks.map(r => r.id)));
- };
-
- const handleBulkDecom = () => {
- for (const rackId of selected) {
- decommissionRack(dcId, rackId);
- }
- setSelected(new Set());
- setShowDecomConfirm(false);
- };
-
- const decomImpact = useMemo(() => {
- let flops = 0, power = 0;
- for (const rackId of selected) {
- const rack = racks.find(r => r.id === rackId);
- if (rack) {
- const sku = RACK_SKU_CONFIGS[rack.skuId];
- flops += sku.flopsPerRack;
- power += sku.powerDrawKW;
+ if (nav.level === 'datacenter' && nav.datacenterId) {
+ const dc = campus?.dataCenters.find(d => d.id === nav.datacenterId);
+ if (dc) {
+ crumbs.push({ label: dc.name, nav: { level: 'datacenter', clusterId: nav.clusterId, campusId: nav.campusId, datacenterId: nav.datacenterId } });
+ }
}
}
- return { flops, power };
- }, [selected, racks]);
-
- if (racks.length === 0) {
- return (
-
-
- No racks installed yet.
-
- );
}
+ return (
+
+ {crumbs.map((crumb, i) => (
+
+ {i > 0 && }
+ {i < crumbs.length - 1 ? (
+ setNav(crumb.nav)} className="text-accent hover:underline">{crumb.label}
+ ) : (
+ {crumb.label}
+ )}
+
+ ))}
+
+ );
+}
+
+function DeploymentProgressBar({ dc }: { dc: DataCenter }) {
+ const tierConfig = DC_TIER_CONFIGS[dc.tier];
+ const maxCompute = maxComputeRacks(tierConfig.rackSlots);
+ const pipelineRacks = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((s, c) => s + c.count, 0);
+ const totalTarget = dc.computeRacksOnline + dc.computeRacksFailed + pipelineRacks;
+ const pct = totalTarget > 0 ? (dc.computeRacksOnline / totalTarget) * 100 : 0;
+
+ if (totalTarget === 0 && dc.status === 'operational') return null;
+
return (
-
-
- {selected.size > 0 && (
-
-
- {selected.size} rack{selected.size !== 1 ? 's' : ''} selected
-
- ({formatNumber(decomImpact.flops)} FLOPS · {decomImpact.power}kW)
-
-
- setShowDecomConfirm(true)}
- className="flex items-center gap-1.5 px-3 py-1.5 rounded bg-danger/20 hover:bg-danger/30 border border-danger/50 text-danger text-sm font-medium transition-colors"
- >
-
- Decommission Selected
-
-
- )}
-
- {showDecomConfirm && (
-
setShowDecomConfirm(false)}
- />
- )}
-
- );
-}
-
-// ─── Launch Racks Panel ────────────────────────────────────────
-
-function LaunchRacksPanel({ dcId, tierConfig, liveUsedSlots, liveUsedPower }: {
- dcId: string;
- tierConfig: DCTierConfig;
- liveUsedSlots: number;
- liveUsedPower: number;
-}) {
- const money = useGameStore((s) => s.economy.money);
- const era = useGameStore((s) => s.meta.currentEra);
- const completedResearch = useGameStore((s) => s.research.completedResearch);
- const orderRack = useGameStore((s) => s.orderRack);
-
- const [selectedSku, setSelectedSku] = useState(null);
- const [quantity, setQuantity] = useState(1);
- const currentEraIdx = ERA_ORDER.indexOf(era);
-
- const allSkus = useMemo(() => {
- return Object.values(RACK_SKU_CONFIGS).map(sku => {
- const eraLocked = ERA_ORDER.indexOf(sku.era) > currentEraIdx;
- const researchLocked = !!(sku.requiredResearch && !completedResearch.includes(sku.requiredResearch));
- const available = !eraLocked && !researchLocked;
- const lockReason = eraLocked
- ? `Requires ${sku.era} era`
- : researchLocked
- ? `Requires "${sku.requiredResearch}"`
- : null;
- return { ...sku, available, lockReason };
- });
- }, [currentEraIdx, completedResearch]);
-
- const selected = selectedSku ? RACK_SKU_CONFIGS[selectedSku] : null;
-
- const { maxQuantity, constraintInfo } = useMemo(() => {
- if (!selected) return { maxQuantity: 0, constraintInfo: null };
- const bySlots = tierConfig.rackSlots - liveUsedSlots;
- const byPower = Math.floor((tierConfig.powerBudgetKW - liveUsedPower) / selected.powerDrawKW);
- const byMoney = Math.floor(money / selected.baseCost);
- const max = Math.max(0, Math.min(bySlots, byPower, byMoney));
-
- let info: { text: string; color: string } | null = null;
- if (max === 0) {
- if (byMoney <= 0) info = { text: 'Insufficient funds', color: 'text-danger' };
- else if (bySlots <= 0) info = { text: 'No rack slots available', color: 'text-danger' };
- else info = { text: 'Exceeds power budget', color: 'text-danger' };
- } else {
- const limiting = max === byMoney ? 'budget' : max === bySlots ? 'slots' : max === byPower ? 'power' : null;
- if (limiting) info = { text: `Max ${max} (${limiting} limit)`, color: 'text-surface-400' };
- }
-
- return { maxQuantity: max, constraintInfo: info };
- }, [selected, tierConfig, liveUsedSlots, liveUsedPower, money]);
-
- const handleSelectSku = (skuId: RackSkuId) => {
- setSelectedSku(skuId);
- setQuantity(1);
- };
-
- const handleLaunch = () => {
- if (!selectedSku || quantity <= 0) return;
- for (let i = 0; i < quantity; i++) {
- orderRack(dcId, selectedSku);
- }
- setQuantity(1);
- setSelectedSku(null);
- };
-
- const totalCost = selected ? selected.baseCost * quantity : 0;
- const newSlots = liveUsedSlots + quantity;
- const newPower = selected ? liveUsedPower + selected.powerDrawKW * quantity : liveUsedPower;
-
- return (
-
-
-
-
- {selectedSku && (
-
-
Quantity:
-
-
setQuantity(q => Math.max(1, q - 1))}
- disabled={quantity <= 1}
- className="px-2 py-1 bg-surface-800 border border-surface-600 rounded-l hover:bg-surface-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
- >
-
-
-
{
- const v = parseInt(e.target.value) || 1;
- setQuantity(Math.max(1, Math.min(maxQuantity, v)));
- }}
- className="w-16 text-center bg-surface-800 border-y border-surface-600 py-1 text-sm font-mono focus:outline-none"
- min={1}
- max={maxQuantity}
- />
-
setQuantity(q => Math.min(maxQuantity, q + 1))}
- disabled={quantity >= maxQuantity}
- className="px-2 py-1 bg-surface-800 border border-surface-600 rounded-r hover:bg-surface-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
- >
-
-
-
- {constraintInfo && (
-
{constraintInfo.text}
- )}
-
- )}
+
+ Deployment
+ {dc.computeRacksOnline} / {totalTarget} online
-
-
-
Order Summary
-
- {selectedSku && selected ? (
- <>
-
-
- Instance
- {selected.name}
-
-
- Quantity
- {quantity}
-
-
- Total Cost
- {formatMoney(totalCost)}
-
-
-
-
-
- Slots
-
- {liveUsedSlots}/{tierConfig.rackSlots}
- → {newSlots}/{tierConfig.rackSlots}
-
-
-
- Power
-
- {liveUsedPower.toFixed(1)}kW
- → {newPower.toFixed(1)}kW
-
-
-
- FLOPS added
- +{formatNumber(selected.flopsPerRack * quantity)}
-
-
-
-
-
- Launch {quantity} Rack{quantity !== 1 ? 's' : ''}
-
- >
- ) : (
-
- Select a rack SKU to configure your order.
-
- )}
+
);
}
-// ─── Data Center Card ──────────────────────────────────────────
-
-type DCTab = 'inventory' | 'launch' | 'upgrades';
-
-function DataCenterCard({ dcId }: { dcId: string }) {
- const dc = useGameStore((s) => s.infrastructure.dataCenters.find(d => d.id === dcId))!;
- const pipelineForDc = useGameStore(useShallow((s) => s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dcId)));
- const money = useGameStore((s) => s.economy.money);
- const upgradeDataCenter = useGameStore((s) => s.upgradeDataCenter);
- const [expanded, setExpanded] = useState(!collapsedDCs.has(dcId));
- const [activeTab, setActiveTab] = useState
(dc.racks.length === 0 ? 'launch' : 'inventory');
-
- const toggleExpanded = useCallback(() => {
- setExpanded(prev => {
- const next = !prev;
- if (next) collapsedDCs.delete(dcId);
- else collapsedDCs.add(dcId);
- return next;
- });
- }, [dcId]);
-
- const tierConfig = DC_TIER_CONFIGS[dc.tier];
- const activePipeline = pipelineForDc.filter(o => o.stage !== 'decommission');
- const liveUsedSlots = dc.racks.length + activePipeline.length;
- const liveUsedPower = dc.racks.reduce((s, r) => s + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0)
- + activePipeline.reduce((s, o) => s + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0);
-
- const handleLaunchClick = useCallback(() => {
- if (!expanded) {
- collapsedDCs.delete(dcId);
- setExpanded(true);
- }
- setActiveTab('launch');
- }, [expanded, dcId]);
-
- if (dc.status === 'constructing') {
- const pct = dc.constructionTotal > 0 ? dc.constructionProgress / dc.constructionTotal : 0;
- return (
-
-
-
-
{dc.name}
-
Under Construction — {tierConfig.name}
-
-
-
- {LOCATION_CONFIGS[dc.location].name}
-
-
-
-
- {Math.round(pct * 100)}% — {dc.constructionTotal - dc.constructionProgress}s remaining
-
-
- );
+function CohortStageBreakdown({ cohorts }: { cohorts: DeploymentCohort[] }) {
+ const stages: PipelineStage[] = ['ordered', 'manufacturing', 'receiving', 'installation', 'testing', 'repair'];
+ const counts: Record = {};
+ for (const stage of stages) counts[stage] = 0;
+ for (const c of cohorts) {
+ if (c.stage in counts) counts[c.stage] += c.count;
}
- const tabs: { id: DCTab; label: string; count?: number }[] = [
- { id: 'inventory', label: 'Inventory', count: dc.racks.length },
- { id: 'launch', label: 'Launch Racks' },
- { id: 'upgrades', label: 'Upgrades' },
- ];
+ const hasAny = stages.some(s => counts[s] > 0);
+ if (!hasAny) return null;
return (
-
-
-
-
-
{dc.name}
- {dc.tier}
-
-
- {LOCATION_CONFIGS[dc.location].name}
- {dc.racks.length} rack{dc.racks.length !== 1 ? 's' : ''}
- {activePipeline.length > 0 && {activePipeline.length} in pipeline }
- Uptime: {formatPercent(dc.currentUptime)}
- Cost: {formatMoney(dc.energyCostPerTick + dc.maintenanceCostPerTick)}/s
-
+
+ {stages.map(stage => counts[stage] > 0 && (
+
+
+ {STAGE_LABELS[stage]}:
+ {counts[stage]}
-
+ ))}
+
+ );
+}
+
+function NetworkHealthIndicator({ dc }: { dc: DataCenter }) {
+ const nh = dc.networkHealth;
+ if (nh.tier1Required === 0) return null;
+
+ const allHealthy = nh.tier1Healthy === nh.tier1Required
+ && nh.tier2Healthy === nh.tier2Required
+ && nh.tier3Healthy === nh.tier3Required;
+
+ const color = nh.tier3Healthy < nh.tier3Required ? 'text-danger'
+ : !allHealthy ? 'text-amber-400'
+ : 'text-green-400';
+
+ return (
+
+
+
+ {nh.tier3Healthy < nh.tier3Required ? 'Core Down'
+ : !allHealthy ? `${nh.racksDisconnected} disconnected`
+ : 'Healthy'}
+
+
+ );
+}
+
+// ─── Clusters List View ─────────────────────────────────────────
+
+function ClustersListView() {
+ const clusters = useGameStore((s) => s.infrastructure.clusters);
+ const totalFlops = useGameStore((s) => s.infrastructure.totalFlops);
+ const totalRacks = useGameStore((s) => s.infrastructure.totalRackCount);
+ const totalUptime = useGameStore((s) => s.infrastructure.totalUptime);
+ const totalDCs = useGameStore((s) => s.infrastructure.totalDataCenterCount);
+ const setNav = useGameStore((s) => s.setInfraNav);
+
+ const [showBuild, setShowBuild] = useState(false);
+
+ return (
+
+
+
Infrastructure
+
setShowBuild(true)}
+ className="flex items-center gap-1.5 bg-accent hover:bg-accent-dark text-white rounded-lg px-4 py-2 text-sm font-medium"
+ >
+ New Cluster
+
+
+
+ {clusters.length === 0 && (
+
+ Build your first cluster to establish a regional presence. Your first cluster is free!
+
+ )}
+
+ {clusters.length > 0 && (
+
+
+
+
+
+
+
+ )}
+
+
+ {clusters.map(cluster => (
cluster.status === 'operational' && setNav({ level: 'cluster', clusterId: cluster.id })}
+ className="bg-surface-800 border border-surface-700 rounded-xl p-5 text-left hover:border-accent/50 transition-colors"
>
-
- Launch
+
+
+
+
{cluster.name}
+ {cluster.status === 'constructing' && (
+ Building...
+ )}
+
+
+
+ {LOCATION_CONFIGS[cluster.locationId].name}
+
+
+
+ {cluster.status === 'constructing' ? (
+
+
+ Construction
+ {Math.floor((cluster.constructionProgress / cluster.constructionTotal) * 100)}%
+
+
+
+ ) : (
+
+
Campuses: {cluster.campuses.length}
+
DCs: {cluster.campuses.reduce((s, c) => s + c.dataCenters.length, 0)}
+
Racks: {
+ formatNumber(cluster.campuses.reduce((s, c) => s + c.dataCenters.reduce((s2, d) => s2 + d.computeRacksOnline + d.computeRacksFailed, 0), 0))
+ }
+
+
+
+
+ )}
-
- {expanded ? : }
+ ))}
+
+
+ {showBuild &&
setShowBuild(false)} />}
+
+ );
+}
+
+// ─── Build Cluster Modal ────────────────────────────────────────
+
+function BuildClusterModal({ onClose }: { onClose: () => void }) {
+ const era = useGameStore((s) => s.meta.currentEra);
+ const money = useGameStore((s) => s.economy.money);
+ const existingClusters = useGameStore((s) => s.infrastructure.clusters);
+ const buildCluster = useGameStore((s) => s.buildCluster);
+
+ const [name, setName] = useState('');
+ const [location, setLocation] = useState
('us-west');
+
+ const isFirst = existingClusters.length === 0;
+ const cost = isFirst ? 0 : CLUSTER_COST_CONFIG.baseCost;
+ const alreadyExists = existingClusters.some(c => c.locationId === location);
+ const locationConfig = LOCATION_CONFIGS[location];
+ const eraUnlocked = ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf(locationConfig.availableAt);
+ const canBuild = name.trim() !== '' && !alreadyExists && eraUnlocked && money >= cost;
+
+ return (
+
+
e.stopPropagation()}>
+
Build New Cluster
+
+
+
+ Cluster Name
+ setName(e.target.value)} placeholder="e.g., US West Cluster"
+ className="w-full bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-sm" />
+
+
+
+ Region
+ setLocation(e.target.value as LocationId)}
+ className="w-full bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-sm">
+ {Object.values(LOCATION_CONFIGS).map(loc => {
+ const locked = ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(loc.availableAt);
+ const taken = existingClusters.some(c => c.locationId === loc.id);
+ return (
+
+ {loc.name} ({loc.energyCostMultiplier}x energy) {locked ? '🔒' : ''} {taken ? '(exists)' : ''}
+
+ );
+ })}
+
+
+
+ {alreadyExists &&
You already have a cluster in this region.
}
+
+
+
Cost: {isFirst ? 'Free' : formatMoney(cost)}
+
Build Time: {isFirst ? 'Instant' : `${CLUSTER_COST_CONFIG.buildTimeTicks}s`}
+
+
+
+ Cancel
+ { buildCluster(name.trim(), location); onClose(); }} disabled={!canBuild}
+ className="flex-1 bg-accent hover:bg-accent-dark disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg py-2 text-sm font-medium">
+ {isFirst ? 'Create Cluster' : `Build (${formatMoney(cost)})`}
+
+
+
+
+
+ );
+}
+
+// ─── Cluster Detail View ────────────────────────────────────────
+
+function ClusterDetailView({ clusterId }: { clusterId: string }) {
+ const cluster = useGameStore((s) => s.infrastructure.clusters.find(c => c.id === clusterId));
+ const setNav = useGameStore((s) => s.setInfraNav);
+ const [showBuild, setShowBuild] = useState(false);
+
+ if (!cluster) return Cluster not found.
;
+
+ const location = LOCATION_CONFIGS[cluster.locationId];
+
+ return (
+
+
+
+
+
+
{cluster.name}
+
{location.name} — {location.energyCostMultiplier}x energy cost
+
+
+
setShowBuild(true)}
+ className="flex items-center gap-1.5 bg-accent hover:bg-accent-dark text-white rounded-lg px-4 py-2 text-sm font-medium">
+ New Campus
+
+
+
+ {cluster.campuses.length === 0 && (
+
+ Build a campus to start housing data centers. All DCs on a campus share the same tier.
+
+ )}
+
+
+ {cluster.campuses.map(campus => (
+
campus.status === 'operational' && setNav({ level: 'campus', clusterId, campusId: campus.id })}
+ className="bg-surface-800 border border-surface-700 rounded-xl p-5 text-left hover:border-accent/50 transition-colors">
+
+
+
+
{campus.name}
+ {DC_TIER_CONFIGS[campus.dcTier].name}
+ {campus.status === 'constructing' && (
+ Building...
+ )}
+
+
+
+
+ {campus.status === 'constructing' ? (
+
+
+ Construction
+ {Math.floor((campus.constructionProgress / campus.constructionTotal) * 100)}%
+
+
+
+ ) : (
+
+
DCs: {campus.dataCenters.length}
+
Racks: {
+ formatNumber(campus.dataCenters.reduce((s, d) => s + d.computeRacksOnline + d.computeRacksFailed, 0))
+ }
+
FLOPS: {
+ formatNumber(campus.dataCenters.reduce((s, d) => {
+ const sku = d.rackSkuId ? RACK_SKU_CONFIGS[d.rackSkuId] : null;
+ return s + (sku ? d.effectiveComputeRacks * sku.flopsPerRack : 0);
+ }, 0))
+ }
+
+ )}
+
+ ))}
+
+
+ {showBuild &&
setShowBuild(false)} />}
+
+ );
+}
+
+// ─── Build Campus Modal ─────────────────────────────────────────
+
+function BuildCampusModal({ clusterId, onClose }: { clusterId: string; onClose: () => void }) {
+ const era = useGameStore((s) => s.meta.currentEra);
+ const money = useGameStore((s) => s.economy.money);
+ const research = useGameStore((s) => s.research.completedResearch);
+ const buildCampus = useGameStore((s) => s.buildCampus);
+ const isFirstCampus = useGameStore((s) => s.infrastructure.clusters.every(c => c.campuses.length === 0));
+
+ const [name, setName] = useState('');
+ const [tier, setTier] = useState('small');
+
+ const tierConfig = DC_TIER_CONFIGS[tier];
+ const campusCost = CAMPUS_TIER_COSTS[tier];
+ const effectiveCost = isFirstCampus ? 0 : campusCost.baseCost;
+ const eraUnlocked = ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf(tierConfig.requiredEra);
+ const researchUnlocked = !tierConfig.requiredResearch || research.includes(tierConfig.requiredResearch);
+ const canBuild = name.trim() !== '' && eraUnlocked && researchUnlocked && money >= effectiveCost;
+
+ return (
+
+
e.stopPropagation()}>
+
Build New Campus
+
+
+
+ Campus Name
+ setName(e.target.value)} placeholder="e.g., Campus Alpha"
+ className="w-full bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-sm" />
+
+
+
+ DC Tier (all DCs on this campus)
+ setTier(e.target.value as DCTier)}
+ className="w-full bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-sm">
+ {Object.values(DC_TIER_CONFIGS).map(tc => {
+ const locked = ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(tc.requiredEra)
+ || (tc.requiredResearch && !research.includes(tc.requiredResearch));
+ return (
+
+ {tc.name} ({tc.rackSlots} slots, {formatMoney(tc.baseCost)}/DC) {locked ? '🔒' : ''}
+
+ );
+ })}
+
+
+
+
+
Campus Cost: {isFirstCampus ? 'Free' : formatMoney(campusCost.baseCost)}
+
Build Time: {isFirstCampus ? `${FIRST_CAMPUS_BUILD_TICKS}s` : `${campusCost.buildTimeTicks}s`}
+
DC Slots: {tierConfig.rackSlots} racks/DC
+
DC Power: {formatNumber(tierConfig.powerBudgetKW)} kW/DC
+
+
+
+ Cancel
+ { buildCampus(name.trim(), clusterId, tier); onClose(); }} disabled={!canBuild}
+ className="flex-1 bg-accent hover:bg-accent-dark disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg py-2 text-sm font-medium">
+ Build {isFirstCampus ? '(Free)' : `(${formatMoney(effectiveCost)})`}
+
+
+
+
+
+ );
+}
+
+// ─── Campus Detail View ─────────────────────────────────────────
+
+function CampusDetailView({ clusterId, campusId }: { clusterId: string; campusId: string }) {
+ const cluster = useGameStore((s) => s.infrastructure.clusters.find(c => c.id === clusterId));
+ const campus = cluster?.campuses.find(c => c.id === campusId);
+ const setNav = useGameStore((s) => s.setInfraNav);
+ const buildDC = useGameStore((s) => s.buildDataCenter);
+ const addDCs = useGameStore((s) => s.addDCsToCampus);
+ const money = useGameStore((s) => s.economy.money);
+
+ const [showAddDC, setShowAddDC] = useState(false);
+ const [dcName, setDcName] = useState('');
+ const [bulkCount, setBulkCount] = useState(1);
+
+ if (!campus || !cluster) return Campus not found.
;
+
+ const tierConfig = DC_TIER_CONFIGS[campus.dcTier];
+
+ return (
+
+
+
+
+
+
{campus.name}
+
{tierConfig.name} campus — {campus.dataCenters.length} DCs
+
+
+
+
setShowAddDC(true)}
+ className="flex items-center gap-1.5 bg-accent hover:bg-accent-dark text-white rounded-lg px-4 py-2 text-sm font-medium">
+ Add DC
-
-
-
+ {campus.dataCenters.length === 0 && (
+
+ Add a data center to this campus. Once built, you can deploy racks to start generating compute.
+
+ )}
+
+
+ {campus.dataCenters.map(dc => (
+
(dc.status === 'operational' || dc.status === 'retrofitting') && setNav({
+ level: 'datacenter', clusterId, campusId, datacenterId: dc.id,
+ })}
+ className="bg-surface-800 border border-surface-700 rounded-xl p-4 text-left hover:border-accent/50 transition-colors">
+
+
+
+ {dc.name}
+ {dc.rackSkuId && {RACK_SKU_CONFIGS[dc.rackSkuId].name} }
+ {dc.status === 'constructing' && Building }
+ {dc.status === 'retrofitting' && Retrofitting }
+
+
+
+
+ {dc.status === 'constructing' ? (
+
+
+ Construction
+ {Math.floor((dc.constructionProgress / dc.constructionTotal) * 100)}%
+
+
+
+ ) : (
+
+
+
+ {dc.deploymentCohorts.length > 0 &&
}
+
+ Uptime: {formatPercent(dc.currentUptime)}
+ Cost: {formatMoney(dc.energyCostPerTick + dc.maintenanceCostPerTick)}/s
+
+
+ )}
+
+ ))}
- {expanded && (
+ {showAddDC && (
+
setShowAddDC(false)}>
+
e.stopPropagation()}>
+
Add Data Center{bulkCount > 1 ? 's' : ''}
+
+
+ DC Name
+ setDcName(e.target.value)} placeholder={`${campus.name}-DC-${campus.dataCenters.length + 1}`}
+ className="w-full bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-sm" />
+
+
+ Quantity
+ setBulkCount(Math.max(1, parseInt(e.target.value) || 1))}
+ className="w-full bg-surface-800 border border-surface-600 rounded-lg px-3 py-2 text-sm" />
+
+
+
Tier: {tierConfig.name}
+
Cost per DC: {formatMoney(tierConfig.baseCost)}
+
Total: {formatMoney(tierConfig.baseCost * bulkCount)}
+
+
+ setShowAddDC(false)} className="flex-1 bg-surface-700 hover:bg-surface-600 rounded-lg py-2 text-sm">Cancel
+ {
+ if (bulkCount === 1) {
+ buildDC(dcName.trim() || `${campus.name}-DC-${campus.dataCenters.length + 1}`, campusId);
+ } else {
+ addDCs(campusId, bulkCount);
+ }
+ setShowAddDC(false);
+ }}
+ disabled={money < tierConfig.baseCost * bulkCount}
+ className="flex-1 bg-accent hover:bg-accent-dark disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg py-2 text-sm font-medium">
+ Build {bulkCount > 1 ? `${bulkCount} DCs` : 'DC'} ({formatMoney(tierConfig.baseCost * bulkCount)})
+
+
+
+
+
+ )}
+
+ );
+}
+
+// ─── Data Center Detail View ────────────────────────────────────
+
+function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
+ clusterId: string; campusId: string; datacenterId: string;
+}) {
+ const cluster = useGameStore((s) => s.infrastructure.clusters.find(c => c.id === clusterId));
+ const campus = cluster?.campuses.find(c => c.id === campusId);
+ const dc = campus?.dataCenters.find(d => d.id === datacenterId);
+ const money = useGameStore((s) => s.economy.money);
+ const era = useGameStore((s) => s.meta.currentEra);
+ const research = useGameStore((s) => s.research.completedResearch);
+ const deployRacks = useGameStore((s) => s.deployRacks);
+ const fillToCapacity = useGameStore((s) => s.fillDCToCapacity);
+ const retrofitDC = useGameStore((s) => s.retrofitDC);
+ const cancelRetrofit = useGameStore((s) => s.cancelRetrofit);
+ const upgradeDataCenter = useGameStore((s) => s.upgradeDataCenter);
+
+ const [activeTab, setActiveTab] = useState<'deploy' | 'retrofit' | 'upgrades' | 'network'>('deploy');
+ const [selectedSku, setSelectedSku] = useState
(null);
+ const [deployQty, setDeployQty] = useState(10);
+ const [confirmRetrofit, setConfirmRetrofit] = useState(null);
+
+ if (!dc || !cluster) return Data center not found.
;
+
+ const tierConfig = DC_TIER_CONFIGS[dc.tier];
+ const maxCompute = maxComputeRacks(tierConfig.rackSlots);
+ const existingCompute = dc.computeRacksOnline + dc.computeRacksFailed
+ + dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((s, c) => s + c.count, 0);
+ const availableSlots = maxCompute - existingCompute;
+ const sku = dc.rackSkuId ? RACK_SKU_CONFIGS[dc.rackSkuId] : null;
+ const netSlots = networkSlotsRequired(existingCompute);
+
+ 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 (dc.rackSkuId && dc.rackSkuId !== s.id) return false;
+ return true;
+ });
+
+ const effectiveSku = selectedSku ? RACK_SKU_CONFIGS[selectedSku] : null;
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
{dc.name}
+
+ {tierConfig.name} — {sku?.name ?? 'No racks deployed'} — {LOCATION_CONFIGS[cluster.locationId].name}
+
+
+
+ {dc.status === 'retrofitting' && (
+
+ Retrofitting to {dc.retrofitState ? RACK_SKU_CONFIGS[dc.retrofitState.toSkuId].name : '...'}
+
+ )}
+
+
+ {/* Stats Grid */}
+
+
+
+
+
+
+
+ {/* Capacity Bars */}
+
+
+
+
+
+ {/* Deployment Progress */}
+
+
+ {dc.deploymentCohorts.length > 0 && (
+
+
+
+ )}
+ {dc.computeRacksFailed > 0 && (
+
+ {dc.computeRacksFailed} rack{dc.computeRacksFailed > 1 ? 's' : ''} under repair
+
+ )}
+
+
+ {/* Retrofit Progress */}
+ {dc.status === 'retrofitting' && dc.retrofitState && (
+
+
+
+
+ Retrofit: {dc.retrofitState.phase}
+
+
cancelRetrofit(datacenterId)} className="text-xs text-surface-400 hover:text-surface-200">Cancel
+
+
+ {RACK_SKU_CONFIGS[dc.retrofitState.fromSkuId].name} → {RACK_SKU_CONFIGS[dc.retrofitState.toSkuId].name}
+ {Math.floor((dc.retrofitState.progress / dc.retrofitState.total) * 100)}%
+
+
+
+ )}
+
+ {/* Tabs */}
+ {dc.status === 'operational' && (
<>
-
- {tabs.map(tab => (
-
setActiveTab(tab.id)}
- className={`px-3 py-2 text-sm font-medium border-b-2 transition-colors ${
- activeTab === tab.id
- ? 'border-accent text-accent-light'
- : 'border-transparent text-surface-400 hover:text-surface-200'
- }`}
- >
- {tab.label}
- {tab.count !== undefined && (
- ({tab.count})
- )}
+
+ {(['deploy', 'retrofit', 'upgrades', 'network'] as const).map(tab => (
+ setActiveTab(tab)}
+ className={`flex-1 py-2 text-sm rounded-md transition-colors ${activeTab === tab ? 'bg-surface-700 text-white' : 'text-surface-400 hover:text-surface-200'}`}>
+ {tab === 'deploy' ? 'Deploy Racks' : tab === 'retrofit' ? 'Retrofit' : tab === 'upgrades' ? 'Upgrades' : 'Network'}
))}
- {activeTab === 'inventory' && (
-
+ {/* Deploy Tab */}
+ {activeTab === 'deploy' && (
+
+ {dc.rackSkuId === null ? (
+ <>
+
Select a rack SKU for this data center. All racks in a DC must be the same type.
+
+ {availableSkus.map(s => (
+
+ setSelectedSku(s.id)} className="accent-accent" />
+
+
{s.name}
+
{s.flopsPerRack} FLOPS | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}
+
+
+ ))}
+
+ >
+ ) : (
+
+ This DC runs {sku!.name} . Available: {availableSlots} compute slots.
+
+ )}
+
+ {(dc.rackSkuId || selectedSku) && availableSlots > 0 && (
+
+
+ Quantity
+ setDeployQty(Math.max(1, Math.min(availableSlots, parseInt(e.target.value) || 1)))}
+ className="w-full bg-surface-900 border border-surface-600 rounded-lg px-3 py-2 text-sm" />
+
+
+ {(() => {
+ const skuToUse = dc.rackSkuId ?? selectedSku!;
+ const skuConfig = RACK_SKU_CONFIGS[skuToUse];
+ const newNetSlots = networkSlotsRequired(existingCompute + deployQty);
+ const addedNet = newNetSlots - netSlots;
+ const totalCost = skuConfig.baseCost * deployQty;
+ return (
+
+
{deployQty} compute racks ({skuConfig.name}) {formatMoney(totalCost)}
+
+ {addedNet} network racks (auto) included
+
= {existingCompute + deployQty + newNetSlots} / {tierConfig.rackSlots} total slots
+
Power: {formatNumber((existingCompute + deployQty) * skuConfig.powerDrawKW)} / {formatNumber(tierConfig.powerBudgetKW)} kW
+
+ );
+ })()}
+
+
+ deployRacks(datacenterId, dc.rackSkuId ?? selectedSku!, deployQty)}
+ disabled={money < (RACK_SKU_CONFIGS[dc.rackSkuId ?? selectedSku!].baseCost * deployQty)}
+ className="flex-1 flex items-center justify-center gap-1.5 bg-accent hover:bg-accent-dark disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg py-2 text-sm font-medium">
+ Deploy {deployQty} Racks
+
+ fillToCapacity(datacenterId, dc.rackSkuId ?? selectedSku!)}
+ disabled={availableSlots === 0}
+ className="flex items-center gap-1.5 bg-surface-700 hover:bg-surface-600 disabled:opacity-50 text-white rounded-lg px-4 py-2 text-sm">
+ Fill to Capacity
+
+
+
+ )}
+
)}
- {activeTab === 'launch' && (
-
+ {/* Retrofit Tab */}
+ {activeTab === 'retrofit' && (
+
+ {!dc.rackSkuId ? (
+
No racks deployed yet. Deploy racks first before retrofitting.
+ ) : (
+ <>
+
+ Retrofit swaps all {dc.computeRacksOnline + dc.computeRacksFailed} {sku!.name} racks to a new SKU.
+ The DC goes offline during retrofit.
+
+
+ {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;
+ return true;
+ }).map(s => (
+
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">
+
+
{s.name}
+
{s.flopsPerRack} FLOPS | {s.powerDrawKW} kW | {formatMoney(s.baseCost)}/rack
+
+
+
+ ))}
+
+ >
+ )}
+
)}
+ {/* Upgrades Tab */}
{activeTab === 'upgrades' && (
-
-
-
upgradeDataCenter(dc.id, 'cooling')}
- disabled={dc.coolingLevel >= 1.0 || money < tierConfig.baseCost * 0.25}
- className="flex-1 flex items-center gap-2 bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded-lg px-4 py-3 text-sm disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
- >
-
-
-
Cooling Level {Math.round(dc.coolingLevel * 10)}/10
-
- {dc.coolingLevel >= 1.0 ? 'Maxed out' : `Upgrade for ${formatMoney(tierConfig.baseCost * 0.25)}`}
+
+ {(['cooling', 'redundancy'] as const).map(upgrade => {
+ const level = upgrade === 'cooling' ? dc.coolingLevel : dc.redundancyLevel;
+ const cost = tierConfig.baseCost * DC_UPGRADE_COST_FRACTION;
+ const maxed = level >= 1.0;
+ return (
+
+
+ {upgrade === 'cooling' ?
:
}
+
+
{upgrade}
+
Level {Math.round(level * 10)}/10 — reduces {upgrade === 'cooling' ? 'test' : 'production'} failure rates
+
+
upgradeDataCenter(datacenterId, upgrade)}
+ disabled={maxed || money < cost}
+ className="bg-surface-700 hover:bg-surface-600 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-1.5 rounded-lg text-xs font-medium">
+ {maxed ? 'Maxed' : `Upgrade (${formatMoney(cost)})`}
+
-
-
+ )}
+
+ {/* Network Tab */}
+ {activeTab === 'network' && (
+
+
Network Topology
+ {dc.computeRacksOnline === 0 ? (
+
No racks online. Deploy racks to see network topology.
+ ) : (
+
+ {[
+ { label: 'Tier-1 (ToR)', required: dc.networkHealth.tier1Required, healthy: dc.networkHealth.tier1Healthy, desc: `1 per ${24} compute racks` },
+ { label: 'Tier-2 (Aggr)', required: dc.networkHealth.tier2Required, healthy: dc.networkHealth.tier2Healthy, desc: `1 per ${6} Tier-1 switches` },
+ { label: 'Tier-3 (Core)', required: dc.networkHealth.tier3Required, healthy: dc.networkHealth.tier3Healthy, desc: 'Redundant pair' },
+ ].map(tier => (
+
+
+
{tier.label}
+
{tier.desc}
+
+
+ {tier.healthy} / {tier.required}
+
-
-
-
upgradeDataCenter(dc.id, 'redundancy')}
- disabled={dc.redundancyLevel >= 1.0 || money < tierConfig.baseCost * 0.25}
- className="flex-1 flex items-center gap-2 bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded-lg px-4 py-3 text-sm disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
- >
-
-
-
Redundancy Level {Math.round(dc.redundancyLevel * 10)}/10
-
- {dc.redundancyLevel >= 1.0 ? 'Maxed out' : `Upgrade for ${formatMoney(tierConfig.baseCost * 0.25)}`}
+ ))}
+ {dc.networkHealth.racksDisconnected > 0 && (
+
+
{dc.networkHealth.racksDisconnected} compute racks disconnected due to network failures
-
-
-
-
-
- Cooling reduces hardware failure rates. Redundancy improves uptime during outages.
-
+ )}
+
+ )}
)}
>
)}
-
- );
-}
-// ─── Build Data Center Panel ───────────────────────────────────
-
-function BuildDCPanel({ onClose }: { onClose: () => void }) {
- const money = useGameStore((s) => s.economy.money);
- const era = useGameStore((s) => s.meta.currentEra);
- const completedResearch = useGameStore((s) => s.research.completedResearch);
- const buildDataCenter = useGameStore((s) => s.buildDataCenter);
-
- const [name, setName] = useState('');
- const [location, setLocation] = useState
('us-west');
- const [tier, setTier] = useState('small');
-
- const currentEraIdx = ERA_ORDER.indexOf(era);
-
- const availableLocations = Object.values(LOCATION_CONFIGS).filter(
- loc => ERA_ORDER.indexOf(loc.availableAt) <= currentEraIdx,
- );
-
- const tierAvailability = useMemo(() => {
- return Object.values(DC_TIER_CONFIGS).map(t => {
- const eraLocked = ERA_ORDER.indexOf(t.requiredEra) > currentEraIdx;
- const researchLocked = !!(t.requiredResearch && !completedResearch.includes(t.requiredResearch));
- const available = !eraLocked && !researchLocked;
- const lockReason = eraLocked
- ? `Requires ${t.requiredEra} era`
- : researchLocked
- ? `Requires "${t.requiredResearch}"`
- : null;
- return { config: t, available, lockReason };
- });
- }, [currentEraIdx, completedResearch]);
-
- const tierConfig = DC_TIER_CONFIGS[tier];
- const locationConfig = LOCATION_CONFIGS[location];
- const estimatedOpCost = tierConfig.baseEnergyCostPerTick * locationConfig.energyCostMultiplier;
-
- const handleBuild = () => {
- if (!name.trim()) return;
- buildDataCenter(name.trim(), location, tier);
- onClose();
- };
-
- return (
-
-
Build New Data Center
-
-
- Name
- setName(e.target.value)}
- placeholder="DC-West-01"
- 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"
- autoFocus
- />
-
-
- Location
- setLocation(e.target.value as LocationId)}
- 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"
- >
- {availableLocations.map(loc => (
- {loc.name} (Energy: {loc.energyCostMultiplier}x)
- ))}
-
-
-
- Tier
- setTier(e.target.value as DCTier)}
- 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"
- >
- {tierAvailability.map(({ config: t, available, lockReason }) => (
-
- {t.name} ({t.rackSlots} slots, {t.powerBudgetKW}kW){lockReason ? ` — ${lockReason}` : ''}
-
- ))}
-
-
-
-
-
-
-
Cost Breakdown
-
- Build cost
- {formatMoney(tierConfig.baseCost)}
-
-
- Build time
- {tierConfig.buildTimeTicks}s
-
-
- Est. operating cost
- ~{formatMoney(estimatedOpCost)}/s
-
-
-
-
Capacity
-
- Rack slots
- {tierConfig.rackSlots}
-
-
- Power budget
- {tierConfig.powerBudgetKW}kW
-
-
-
-
-
- Cancel
-
- Build ({formatMoney(tierConfig.baseCost)})
-
-
-
- );
-}
-
-// ─── Page ──────────────────────────────────────────────────────
-
-export function InfrastructurePage() {
- const dataCenters = useGameStore((s) => s.infrastructure.dataCenters);
- const [showNewDC, setShowNewDC] = useState(false);
-
- return (
-
-
-
Infrastructure
-
setShowNewDC(true)}
- className="flex items-center gap-2 bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg transition-colors text-sm"
- >
-
- New Data Center
-
-
-
-
- Choose a location and tier for your data center, then order GPU racks to add compute capacity. Racks go through a build pipeline before they come online.
-
-
-
-
- {showNewDC &&
setShowNewDC(false)} />}
-
-
-
- {dataCenters.length === 0 && !showNewDC ? (
-
-
-
No data centers yet. Build your first one to start hosting AI models.
-
- ) : (
-
- {dataCenters.map(dc => (
-
- ))}
-
+ {/* Retrofit Confirmation Modal */}
+ {confirmRetrofit && (
+ { retrofitDC(datacenterId, confirmRetrofit); setConfirmRetrofit(null); }}
+ onCancel={() => setConfirmRetrofit(null)}
+ />
+ )}
+
+ );
+}
+
+// ─── Main Page ──────────────────────────────────────────────────
+
+export function InfrastructurePage() {
+ const nav = useGameStore((s) => s.infraNav);
+
+ return (
+
+
+ {nav.level === 'clusters' && }
+ {nav.level === 'cluster' && nav.clusterId && }
+ {nav.level === 'campus' && nav.clusterId && nav.campusId && }
+ {nav.level === 'datacenter' && nav.clusterId && nav.campusId && nav.datacenterId && (
+
)}
);
diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts
index d908d41..4c5a1ed 100644
--- a/apps/web/src/store/index.ts
+++ b/apps/web/src/store/index.ts
@@ -6,8 +6,10 @@ import type {
ResearchState, ModelsState, MarketState,
CompetitorState, TalentState, DataState,
ReputationState, AchievementState,
- DataCenter, DCTier, RackSkuId, TrainingJob,
+ Cluster, Campus, DataCenter, DCTier, RackSkuId, TrainingJob,
ActiveResearch, OwnedDataset, LocationId,
+ DeploymentCohort, PipelineStage,
+ NetworkHealthState,
} from '@ai-tycoon/shared';
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
import {
@@ -18,8 +20,12 @@ import {
INITIAL_REPUTATION, INITIAL_ACHIEVEMENTS,
DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
PIPELINE_ORDER_BASE_TICKS, DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
+ CLUSTER_COST_CONFIG, CAMPUS_TIER_COSTS, FIRST_CAMPUS_BUILD_TICKS,
+ COHORT_SCALE_FACTOR,
FUNDING_ROUNDS,
OPEN_SOURCE_REPUTATION_BOOST,
+ LOCATION_CONFIGS,
+ networkSlotsRequired, maxComputeRacks,
uuid,
} from '@ai-tycoon/shared';
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
@@ -27,9 +33,19 @@ import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
| 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'settings';
+export type InfraNavLevel = 'clusters' | 'cluster' | 'campus' | 'datacenter';
+
+export interface InfraNav {
+ level: InfraNavLevel;
+ clusterId?: string;
+ campusId?: string;
+ datacenterId?: string;
+}
+
interface UIState {
activePage: ActivePage;
notifications: GameNotification[];
+ infraNav: InfraNav;
}
export interface GameNotification {
@@ -41,8 +57,13 @@ export interface GameNotification {
read: boolean;
}
+function emptyNetworkHealth(): NetworkHealthState {
+ return { tier1Required: 0, tier1Healthy: 0, tier2Required: 0, tier2Healthy: 0, tier3Required: 0, tier3Healthy: 0, racksDisconnected: 0 };
+}
+
interface Actions {
setActivePage: (page: ActivePage) => void;
+ setInfraNav: (nav: InfraNav) => void;
addNotification: (n: Omit) => void;
dismissNotification: (id: string) => void;
markAllNotificationsRead: () => void;
@@ -50,9 +71,14 @@ interface Actions {
setGameSpeed: (speed: GameSpeed) => void;
togglePause: () => void;
setTrainingAllocation: (ratio: number) => void;
- buildDataCenter: (name: string, location: LocationId, tier: DCTier) => void;
- orderRack: (dataCenterId: string, skuId: RackSkuId) => void;
- decommissionRack: (dataCenterId: string, rackId: string) => void;
+ buildCluster: (name: string, locationId: LocationId) => void;
+ buildCampus: (name: string, clusterId: string, dcTier: DCTier) => void;
+ buildDataCenter: (name: string, campusId: string) => void;
+ deployRacks: (dataCenterId: string, skuId: RackSkuId, quantity: number) => void;
+ fillDCToCapacity: (dataCenterId: string, skuId: RackSkuId) => void;
+ addDCsToCampus: (campusId: string, count: number) => void;
+ retrofitDC: (dataCenterId: string, newSkuId: RackSkuId) => void;
+ cancelRetrofit: (dataCenterId: string) => void;
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
startTraining: (job: Omit) => void;
deployModel: (modelId: string) => void;
@@ -97,15 +123,73 @@ const initialGameState: GameState = {
achievements: INITIAL_ACHIEVEMENTS,
};
+// --- Helper: find entities in the hierarchy ---
+
+function findCluster(infra: InfrastructureState, clusterId: string): Cluster | undefined {
+ return infra.clusters.find(c => c.id === clusterId);
+}
+
+function findCampusInCluster(cluster: Cluster, campusId: string): Campus | undefined {
+ return cluster.campuses.find(c => c.id === campusId);
+}
+
+function findCampus(infra: InfrastructureState, campusId: string): { cluster: Cluster; campus: Campus } | undefined {
+ for (const cluster of infra.clusters) {
+ const campus = cluster.campuses.find(c => c.id === campusId);
+ if (campus) return { cluster, campus };
+ }
+ return undefined;
+}
+
+function findDC(infra: InfrastructureState, dcId: string): { cluster: Cluster; campus: Campus; dc: DataCenter } | undefined {
+ for (const cluster of infra.clusters) {
+ for (const campus of cluster.campuses) {
+ const dc = campus.dataCenters.find(d => d.id === dcId);
+ if (dc) return { cluster, campus, dc };
+ }
+ }
+ return undefined;
+}
+
+function updateDCInInfra(infra: InfrastructureState, dcId: string, updater: (dc: DataCenter) => DataCenter): InfrastructureState {
+ return {
+ ...infra,
+ clusters: infra.clusters.map(cluster => ({
+ ...cluster,
+ campuses: cluster.campuses.map(campus => ({
+ ...campus,
+ dataCenters: campus.dataCenters.map(dc =>
+ dc.id === dcId ? updater(dc) : dc,
+ ),
+ })),
+ })),
+ };
+}
+
+function updateCampusInInfra(infra: InfrastructureState, campusId: string, updater: (campus: Campus) => Campus): InfrastructureState {
+ return {
+ ...infra,
+ clusters: infra.clusters.map(cluster => ({
+ ...cluster,
+ campuses: cluster.campuses.map(campus =>
+ campus.id === campusId ? updater(campus) : campus,
+ ),
+ })),
+ };
+}
+
export const useGameStore = create()(
persist(
(set, get) => ({
...initialGameState,
activePage: 'dashboard' as ActivePage,
notifications: [],
+ infraNav: { level: 'clusters' } as InfraNav,
setActivePage: (page) => set({ activePage: page }),
+ setInfraNav: (nav) => set({ infraNav: nav }),
+
addNotification: (n) => set((s) => ({
notifications: [
{ ...n, id: uuid(), read: false },
@@ -138,6 +222,7 @@ export const useGameStore = create()(
},
activePage: 'dashboard',
notifications: [],
+ infraNav: { level: 'clusters' },
}),
setGameSpeed: (speed) => set((s) => ({
@@ -152,121 +237,310 @@ export const useGameStore = create()(
compute: { ...s.compute, trainingAllocation: ratio, inferenceAllocation: 1 - ratio },
})),
- buildDataCenter: (name, location, tier) => set((s) => {
- const tierConfig = DC_TIER_CONFIGS[tier];
- if (s.economy.money < tierConfig.baseCost) return s;
+ // --- Infrastructure: Cluster ---
+ buildCluster: (name, locationId) => set((s) => {
+ const loc = LOCATION_CONFIGS[locationId];
+ const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
+ if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(loc.availableAt)) return s;
+
+ const existingInRegion = s.infrastructure.clusters.find(c => c.locationId === locationId);
+ if (existingInRegion) return s;
+
+ const isFirst = s.infrastructure.clusters.length === 0;
+ const cost = isFirst ? 0 : CLUSTER_COST_CONFIG.baseCost;
+ if (s.economy.money < cost) return s;
+
+ const cluster: Cluster = {
+ id: uuid(),
+ name,
+ locationId,
+ campuses: [],
+ status: isFirst ? 'operational' : 'constructing',
+ constructionProgress: isFirst ? 0 : 0,
+ constructionTotal: isFirst ? 0 : CLUSTER_COST_CONFIG.buildTimeTicks,
+ };
+
+ return {
+ economy: { ...s.economy, money: s.economy.money - cost },
+ infrastructure: {
+ ...s.infrastructure,
+ clusters: [...s.infrastructure.clusters, cluster],
+ },
+ };
+ }),
+
+ // --- Infrastructure: Campus ---
+
+ buildCampus: (name, clusterId, dcTier) => set((s) => {
+ const cluster = findCluster(s.infrastructure, clusterId);
+ if (!cluster || cluster.status !== 'operational') return s;
+
+ const tierConfig = DC_TIER_CONFIGS[dcTier];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(tierConfig.requiredEra)) return s;
if (tierConfig.requiredResearch && !s.research.completedResearch.includes(tierConfig.requiredResearch)) return s;
- const isFirstDC = s.infrastructure.dataCenters.length === 0;
+ const campusCost = CAMPUS_TIER_COSTS[dcTier];
+ const isFirstCampus = s.infrastructure.clusters.every(c => c.campuses.length === 0);
+ const cost = isFirstCampus ? 0 : campusCost.baseCost;
+ if (s.economy.money < cost) return s;
+
+ const buildTime = isFirstCampus ? FIRST_CAMPUS_BUILD_TICKS : campusCost.buildTimeTicks;
+
+ const campus: Campus = {
+ id: uuid(),
+ name,
+ clusterId,
+ dcTier,
+ dataCenters: [],
+ status: 'constructing',
+ constructionProgress: 0,
+ constructionTotal: buildTime,
+ };
+
+ return {
+ economy: { ...s.economy, money: s.economy.money - cost },
+ infrastructure: {
+ ...s.infrastructure,
+ clusters: s.infrastructure.clusters.map(c =>
+ c.id === clusterId
+ ? { ...c, campuses: [...c.campuses, campus] }
+ : c,
+ ),
+ },
+ };
+ }),
+
+ // --- Infrastructure: Data Center ---
+
+ buildDataCenter: (name, campusId) => set((s) => {
+ const found = findCampus(s.infrastructure, campusId);
+ if (!found || found.campus.status !== 'operational') return s;
+
+ const tierConfig = DC_TIER_CONFIGS[found.campus.dcTier];
+ if (s.economy.money < tierConfig.baseCost) return s;
+
+ const isFirstDC = s.infrastructure.clusters.every(cl =>
+ cl.campuses.every(ca => ca.dataCenters.length === 0),
+ );
const buildTime = isFirstDC ? tierConfig.firstBuildTimeTicks : tierConfig.buildTimeTicks;
const dc: DataCenter = {
id: uuid(),
name,
- location,
- tier,
+ campusId,
+ tier: found.campus.dcTier,
status: 'constructing',
constructionProgress: 0,
constructionTotal: buildTime,
- racks: [],
+ rackSkuId: null,
+ computeRacksOnline: 0,
+ computeRacksFailed: 0,
+ networkHealth: emptyNetworkHealth(),
+ deploymentCohorts: [],
+ retrofitState: null,
coolingLevel: 0,
redundancyLevel: 0,
- currentUptime: 1,
- energyCostPerTick: 0,
- maintenanceCostPerTick: 0,
+ effectiveComputeRacks: 0,
usedSlots: 0,
usedPowerKW: 0,
+ energyCostPerTick: 0,
+ maintenanceCostPerTick: 0,
+ currentUptime: 1,
};
return {
economy: { ...s.economy, money: s.economy.money - tierConfig.baseCost },
- infrastructure: {
- ...s.infrastructure,
- dataCenters: [...s.infrastructure.dataCenters, dc],
- },
+ infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
+ ...campus,
+ dataCenters: [...campus.dataCenters, dc],
+ })),
};
}),
- orderRack: (dataCenterId, skuId) => set((s) => {
- const sku = RACK_SKU_CONFIGS[skuId];
- if (s.economy.money < sku.baseCost) return s;
+ // --- Infrastructure: Deploy Racks ---
+ deployRacks: (dataCenterId, skuId, quantity) => set((s) => {
+ if (quantity <= 0) return s;
+
+ const found = findDC(s.infrastructure, dataCenterId);
+ if (!found || found.dc.status !== 'operational') return s;
+
+ const dc = found.dc;
+ if (dc.rackSkuId !== null && dc.rackSkuId !== skuId) return s;
+
+ 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;
- const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
- if (!dc || dc.status !== 'operational') return s;
-
const tierConfig = DC_TIER_CONFIGS[dc.tier];
- const activePipeline = s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dataCenterId && o.stage !== 'decommission');
- const actualUsedSlots = dc.racks.length + activePipeline.length;
- const pipelinePowerForDc = activePipeline
- .reduce((sum, o) => sum + RACK_SKU_CONFIGS[o.skuId].powerDrawKW, 0);
- const actualUsedPower = dc.racks.reduce((sum, r) => sum + RACK_SKU_CONFIGS[r.skuId].powerDrawKW, 0) + pipelinePowerForDc;
- if (actualUsedSlots >= tierConfig.rackSlots) return s;
- if (actualUsedPower + sku.powerDrawKW > tierConfig.powerBudgetKW) return s;
+ const maxCompute = maxComputeRacks(tierConfig.rackSlots);
+ const existingCompute = dc.computeRacksOnline + dc.computeRacksFailed
+ + dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
+ const available = maxCompute - existingCompute;
+ const actualQty = Math.min(quantity, available);
+ if (actualQty <= 0) return s;
- const order = {
+ const totalNetSlots = networkSlotsRequired(existingCompute + actualQty);
+ const totalSlotsNeeded = existingCompute + actualQty + totalNetSlots;
+ if (totalSlotsNeeded > tierConfig.rackSlots) return s;
+
+ const powerNeeded = (existingCompute + actualQty) * sku.powerDrawKW;
+ if (powerNeeded > tierConfig.powerBudgetKW) return s;
+
+ const totalCost = sku.baseCost * actualQty;
+ if (s.economy.money < totalCost) return s;
+
+ const baseTicks = PIPELINE_ORDER_BASE_TICKS;
+ const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * actualQty));
+
+ const cohort: DeploymentCohort = {
id: uuid(),
+ count: actualQty,
skuId,
- dataCenterId,
- stage: 'ordered' as const,
+ stage: 'ordered',
stageProgress: 0,
- stageTotal: PIPELINE_ORDER_BASE_TICKS,
- totalCost: sku.baseCost,
+ stageTotal: scaledTicks,
repairCount: 0,
};
return {
- economy: { ...s.economy, money: s.economy.money - sku.baseCost },
- infrastructure: {
- ...s.infrastructure,
- rackPipeline: [...s.infrastructure.rackPipeline, order],
- },
+ economy: { ...s.economy, money: s.economy.money - totalCost },
+ infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
+ ...d,
+ rackSkuId: skuId,
+ deploymentCohorts: [...d.deploymentCohorts, cohort],
+ })),
};
}),
- decommissionRack: (dataCenterId, rackId) => set((s) => {
- const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
- if (!dc || dc.status !== 'operational') return s;
+ fillDCToCapacity: (dataCenterId, skuId) => {
+ const s = get();
+ const found = findDC(s.infrastructure, dataCenterId);
+ if (!found || found.dc.status !== 'operational') return;
- const rack = dc.racks.find(r => r.id === rackId);
- if (!rack) return s;
+ const dc = found.dc;
+ const tierConfig = DC_TIER_CONFIGS[dc.tier];
+ const maxCompute = maxComputeRacks(tierConfig.rackSlots);
+ const existingCompute = dc.computeRacksOnline + dc.computeRacksFailed
+ + dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
+ const available = maxCompute - existingCompute;
+ if (available <= 0) return;
- const sku = RACK_SKU_CONFIGS[rack.skuId];
- const dataCenters = s.infrastructure.dataCenters.map(d => {
- if (d.id !== dataCenterId) return d;
- return { ...d, racks: d.racks.filter(r => r.id !== rackId) };
- });
+ const sku = RACK_SKU_CONFIGS[skuId];
+ const affordableQty = Math.floor(s.economy.money / sku.baseCost);
+ const powerLimit = Math.floor((tierConfig.powerBudgetKW - existingCompute * sku.powerDrawKW) / sku.powerDrawKW);
+ const qty = Math.min(available, affordableQty, powerLimit);
+ if (qty <= 0) return;
- const order = {
- id: rackId,
- skuId: rack.skuId,
- dataCenterId,
- stage: 'decommission' as const,
- stageProgress: 0,
- stageTotal: sku.pipelineTimeTicks.installation,
- totalCost: 0,
- repairCount: 0,
- };
+ get().deployRacks(dataCenterId, skuId, qty);
+ },
+
+ addDCsToCampus: (campusId, count) => set((s) => {
+ if (count <= 0) return s;
+
+ const found = findCampus(s.infrastructure, campusId);
+ if (!found || found.campus.status !== 'operational') return s;
+
+ const tierConfig = DC_TIER_CONFIGS[found.campus.dcTier];
+ const totalCost = tierConfig.baseCost * count;
+ if (s.economy.money < totalCost) return s;
+
+ const newDCs: DataCenter[] = [];
+ for (let i = 0; i < count; i++) {
+ newDCs.push({
+ id: uuid(),
+ name: `${found.campus.name}-DC-${found.campus.dataCenters.length + i + 1}`,
+ campusId,
+ tier: found.campus.dcTier,
+ status: 'constructing',
+ constructionProgress: 0,
+ constructionTotal: tierConfig.buildTimeTicks,
+ rackSkuId: null,
+ computeRacksOnline: 0,
+ computeRacksFailed: 0,
+ networkHealth: emptyNetworkHealth(),
+ deploymentCohorts: [],
+ retrofitState: null,
+ coolingLevel: 0,
+ redundancyLevel: 0,
+ effectiveComputeRacks: 0,
+ usedSlots: 0,
+ usedPowerKW: 0,
+ energyCostPerTick: 0,
+ maintenanceCostPerTick: 0,
+ currentUptime: 1,
+ });
+ }
return {
- infrastructure: {
- ...s.infrastructure,
- dataCenters,
- rackPipeline: [...s.infrastructure.rackPipeline, order],
- },
+ economy: { ...s.economy, money: s.economy.money - totalCost },
+ infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
+ ...campus,
+ dataCenters: [...campus.dataCenters, ...newDCs],
+ })),
};
}),
+ // --- Infrastructure: Retrofit ---
+
+ retrofitDC: (dataCenterId, newSkuId) => set((s) => {
+ const found = findDC(s.infrastructure, dataCenterId);
+ if (!found || found.dc.status !== 'operational') return s;
+
+ const dc = found.dc;
+ if (!dc.rackSkuId || dc.rackSkuId === newSkuId) return s;
+
+ 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;
+
+ const totalRacksToRetrofit = dc.computeRacksOnline + dc.computeRacksFailed;
+ if (totalRacksToRetrofit <= 0) return s;
+
+ const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId];
+ const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacksToRetrofit));
+
+ return {
+ infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
+ ...d,
+ status: 'retrofitting' as const,
+ deploymentCohorts: [],
+ retrofitState: {
+ fromSkuId: d.rackSkuId!,
+ toSkuId: newSkuId,
+ phase: 'decommissioning' as const,
+ progress: 0,
+ total: decommTicks,
+ racksRemaining: totalRacksToRetrofit,
+ },
+ })),
+ };
+ }),
+
+ cancelRetrofit: (dataCenterId) => set((s) => {
+ const found = findDC(s.infrastructure, dataCenterId);
+ if (!found || found.dc.status !== 'retrofitting') return s;
+
+ return {
+ infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
+ ...d,
+ status: 'operational' as const,
+ retrofitState: null,
+ })),
+ };
+ }),
+
+ // --- Infrastructure: Upgrades ---
+
upgradeDataCenter: (dataCenterId, upgrade) => set((s) => {
- const dc = s.infrastructure.dataCenters.find(d => d.id === dataCenterId);
- if (!dc || dc.status !== 'operational') return s;
+ const found = findDC(s.infrastructure, dataCenterId);
+ if (!found || found.dc.status !== 'operational') return s;
+ const dc = found.dc;
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const cost = tierConfig.baseCost * DC_UPGRADE_COST_FRACTION;
if (s.economy.money < cost) return s;
@@ -274,21 +548,18 @@ export const useGameStore = create()(
const currentLevel = upgrade === 'cooling' ? dc.coolingLevel : dc.redundancyLevel;
if (currentLevel >= 1.0) return s;
- const dataCenters = s.infrastructure.dataCenters.map(d => {
- if (d.id !== dataCenterId) return d;
- return {
+ return {
+ economy: { ...s.economy, money: s.economy.money - cost },
+ infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
[upgrade === 'cooling' ? 'coolingLevel' : 'redundancyLevel']:
Math.min(1.0, currentLevel + DC_UPGRADE_INCREMENT),
- };
- });
-
- return {
- economy: { ...s.economy, money: s.economy.money - cost },
- infrastructure: { ...s.infrastructure, dataCenters },
+ })),
};
}),
+ // --- Non-infrastructure actions (unchanged) ---
+
startTraining: (job) => set((s) => ({
models: {
...s.models,
@@ -463,7 +734,7 @@ export const useGameStore = create()(
name: 'ai-tycoon-save',
version: SAVE_VERSION,
partialize: (state) => {
- const { activePage, notifications, ...rest } = state;
+ const { activePage, notifications, infraNav, ...rest } = state;
return rest;
},
migrate: (_persisted, version) => {
@@ -474,11 +745,12 @@ export const useGameStore = create()(
notifications: [{
id: uuid(),
title: 'Save Reset',
- message: 'Your save was reset due to a major infrastructure overhaul. Enjoy the new rack-based system!',
+ message: 'Your save was reset due to a major infrastructure redesign — Hypercluster scale! Build clusters, campuses, and data centers.',
type: 'info' as const,
tick: 0,
read: false,
}],
+ infraNav: { level: 'clusters' },
} as unknown as Store;
}
return _persisted as Store;
diff --git a/packages/game-engine/src/data/achievements.ts b/packages/game-engine/src/data/achievements.ts
index a65af51..d5e1484 100644
--- a/packages/game-engine/src/data/achievements.ts
+++ b/packages/game-engine/src/data/achievements.ts
@@ -6,7 +6,7 @@ export const ACHIEVEMENT_DEFINITIONS: AchievementDefinition[] = [
name: 'First Steps',
description: 'Build your first data center.',
icon: 'Server',
- condition: { field: 'infrastructure.dataCenters.length', operator: 'gte', value: 1 },
+ condition: { field: 'infrastructure.totalDataCenterCount', operator: 'gte', value: 1 },
},
{
id: 'first-model',
diff --git a/packages/game-engine/src/data/techTree.ts b/packages/game-engine/src/data/techTree.ts
index e479dad..120aeb8 100644
--- a/packages/game-engine/src/data/techTree.ts
+++ b/packages/game-engine/src/data/techTree.ts
@@ -65,7 +65,7 @@ export const TECH_TREE: ResearchNode[] = [
{
id: 'dc-engineering-ii',
name: 'DC Engineering II',
- description: 'Advanced facility design unlocks Medium data centers (30 slots, 200kW).',
+ description: 'Advanced facility design unlocks Medium data centers (500 slots, 3000kW).',
era: 'startup',
category: 'infrastructure',
prerequisites: ['advanced-cooling'],
@@ -75,7 +75,7 @@ export const TECH_TREE: ResearchNode[] = [
{
id: 'dc-engineering-iii',
name: 'DC Engineering III',
- description: 'Large-scale facility design unlocks Large data centers (60 slots, 500kW).',
+ description: 'Large-scale facility design unlocks Large data centers (1000 slots, 7000kW).',
era: 'scaleup',
category: 'infrastructure',
prerequisites: ['dc-engineering-ii'],
@@ -85,7 +85,7 @@ export const TECH_TREE: ResearchNode[] = [
{
id: 'dc-engineering-iv',
name: 'DC Engineering IV',
- description: 'Mega-scale campus design unlocks Mega data centers (120 slots, 1200kW).',
+ description: 'Mega-scale campus design unlocks Mega data centers (1500 slots, 12000kW).',
era: 'bigtech',
category: 'infrastructure',
prerequisites: ['dc-engineering-iii'],
@@ -102,6 +102,36 @@ export const TECH_TREE: ResearchNode[] = [
cost: { researchPoints: 1, compute: 10, ticks: 90 },
effects: [{ type: 'cost_reduction', target: 'test_failure_rate', value: 0.25 }],
},
+ {
+ id: 'network-engineering-i',
+ name: 'Network Engineering I',
+ description: 'Improved network switching reduces Tier-1 failure rate by 40%.',
+ era: 'scaleup',
+ category: 'infrastructure',
+ prerequisites: ['redundancy-protocols'],
+ cost: { researchPoints: 2, compute: 20, ticks: 150 },
+ effects: [{ type: 'cost_reduction', target: 'network_failure_rate', value: 0.4 }],
+ },
+ {
+ id: 'network-engineering-ii',
+ name: 'Network Engineering II',
+ description: 'Spine-leaf architecture reduces all network failure rates by 50%.',
+ era: 'bigtech',
+ category: 'infrastructure',
+ prerequisites: ['network-engineering-i'],
+ cost: { researchPoints: 4, compute: 80, ticks: 360 },
+ effects: [{ type: 'cost_reduction', target: 'network_failure_rate', value: 0.5 }],
+ },
+ {
+ id: 'rapid-deployment',
+ name: 'Rapid Deployment',
+ description: 'Streamlined procurement pipelines reduce deployment times by 20%.',
+ era: 'scaleup',
+ category: 'infrastructure',
+ prerequisites: ['dc-engineering-ii'],
+ cost: { researchPoints: 2, compute: 25, ticks: 180 },
+ effects: [{ type: 'efficiency_boost', target: 'pipeline_speed', value: 0.2 }],
+ },
{
id: 'distributed-training',
name: 'Distributed Training',
diff --git a/packages/game-engine/src/systems/economySystem.ts b/packages/game-engine/src/systems/economySystem.ts
index 86c27fc..8a8969a 100644
--- a/packages/game-engine/src/systems/economySystem.ts
+++ b/packages/game-engine/src/systems/economySystem.ts
@@ -10,9 +10,14 @@ export function processEconomy(
): EconomyState {
const revenue = market.apiRevenue + market.subscriptionRevenue;
- const infraExpenses = infrastructure.dataCenters.reduce((sum, dc) => {
- return sum + dc.energyCostPerTick + dc.maintenanceCostPerTick;
- }, 0);
+ let infraExpenses = 0;
+ for (const cluster of infrastructure.clusters) {
+ for (const campus of cluster.campuses) {
+ for (const dc of campus.dataCenters) {
+ infraExpenses += dc.energyCostPerTick + dc.maintenanceCostPerTick;
+ }
+ }
+ }
const talentExpenses = state.talent.totalSalaryPerTick;
const dataExpenses = state.data.partnerships.reduce((sum, p) => sum + p.costPerTick, 0);
diff --git a/packages/game-engine/src/systems/infrastructureSystem.ts b/packages/game-engine/src/systems/infrastructureSystem.ts
index 50ef3b8..c74740d 100644
--- a/packages/game-engine/src/systems/infrastructureSystem.ts
+++ b/packages/game-engine/src/systems/infrastructureSystem.ts
@@ -1,4 +1,7 @@
-import type { GameState, InfrastructureState, DataCenter, RackOrder, Rack, PipelineStage } from '@ai-tycoon/shared';
+import type {
+ GameState, InfrastructureState, Cluster, Campus, DataCenter,
+ DeploymentCohort, NetworkHealthState, PipelineStage,
+} from '@ai-tycoon/shared';
import {
LOCATION_CONFIGS,
RACK_SKU_CONFIGS,
@@ -8,6 +11,10 @@ import {
COOLING_FAILURE_REDUCTION,
REDUNDANCY_FAILURE_REDUCTION,
RACK_REPAIR_BASE_TICKS,
+ NETWORK_TOPOLOGY,
+ COHORT_SCALE_FACTOR,
+ PIPELINE_ORDER_BASE_TICKS,
+ networkSlotsRequired,
} from '@ai-tycoon/shared';
import type { TickNotification } from '../tick';
@@ -27,18 +34,21 @@ function nextStage(stage: PipelineStage): PipelineStage | 'production' {
return PIPELINE_ADVANCE_ORDER[idx + 1];
}
-function stageTotal(stage: PipelineStage, order: RackOrder): number {
- const sku = RACK_SKU_CONFIGS[order.skuId];
+function cohortStageTotal(stage: PipelineStage, skuId: string, count: number): number {
+ const sku = RACK_SKU_CONFIGS[skuId as keyof typeof RACK_SKU_CONFIGS];
const timings = sku.pipelineTimeTicks;
+ let base: number;
switch (stage) {
- case 'manufacturing': return timings.manufacturing;
- case 'receiving': return timings.receiving;
- case 'installation': return timings.installation;
- case 'testing': return timings.testing;
- case 'repair': return RACK_REPAIR_BASE_TICKS;
- case 'decommission': return timings.installation;
- default: return 0;
+ case 'ordered': base = PIPELINE_ORDER_BASE_TICKS; break;
+ case 'manufacturing': base = timings.manufacturing; break;
+ case 'receiving': base = timings.receiving; break;
+ case 'installation': base = timings.installation; break;
+ case 'testing': base = timings.testing; break;
+ case 'repair': base = RACK_REPAIR_BASE_TICKS; break;
+ case 'decommission': base = timings.installation; break;
+ default: base = 0;
}
+ return Math.ceil(base * (1 + COHORT_SCALE_FACTOR * count));
}
function stageSpeed(stage: PipelineStage, engEff: number, opsEff: number): number {
@@ -52,235 +62,403 @@ function stageSpeed(stage: PipelineStage, engEff: number, opsEff: number): numbe
}
}
+function binomialSample(n: number, p: number): number {
+ if (n <= 0 || p <= 0) return 0;
+ if (p >= 1) return n;
+ const expected = n * p;
+ const base = Math.floor(expected);
+ const frac = expected - base;
+ return base + (Math.random() < frac ? 1 : 0);
+}
+
+function computeNetworkHealth(computeRacksOnline: number): NetworkHealthState {
+ if (computeRacksOnline <= 0) {
+ return { tier1Required: 0, tier1Healthy: 0, tier2Required: 0, tier2Healthy: 0, tier3Required: 0, tier3Healthy: 0, racksDisconnected: 0 };
+ }
+ const tier1 = Math.ceil(computeRacksOnline / NETWORK_TOPOLOGY.tier1PerCompute);
+ const tier2 = Math.ceil(tier1 / NETWORK_TOPOLOGY.tier2PerTier1);
+ const tier3 = NETWORK_TOPOLOGY.tier3PerDC;
+ return {
+ tier1Required: tier1,
+ tier1Healthy: tier1,
+ tier2Required: tier2,
+ tier2Healthy: tier2,
+ tier3Required: tier3,
+ tier3Healthy: tier3,
+ racksDisconnected: 0,
+ };
+}
+
+function processNetworkFailures(
+ nh: NetworkHealthState,
+ computeRacksOnline: number,
+ networkResearchBonus: number,
+): { networkHealth: NetworkHealthState; racksDisconnected: number } {
+ if (computeRacksOnline <= 0) {
+ return { networkHealth: nh, racksDisconnected: 0 };
+ }
+
+ let racksDisconnected = 0;
+
+ const t1Rate = NETWORK_TOPOLOGY.tier1FailureRate * (1 - networkResearchBonus);
+ const t1Failures = binomialSample(nh.tier1Required, t1Rate);
+ const tier1Healthy = nh.tier1Required - t1Failures;
+ racksDisconnected += t1Failures * NETWORK_TOPOLOGY.tier1BlastRadius;
+
+ const t2Rate = NETWORK_TOPOLOGY.tier2FailureRate * (1 - networkResearchBonus);
+ const t2Failures = binomialSample(nh.tier2Required, t2Rate);
+ const tier2Healthy = nh.tier2Required - t2Failures;
+ racksDisconnected += t2Failures * NETWORK_TOPOLOGY.tier1BlastRadius * NETWORK_TOPOLOGY.tier2BlastRadiusMultiplier;
+
+ const t3Rate = NETWORK_TOPOLOGY.tier3FailureRate * (1 - networkResearchBonus);
+ const t3Failures = binomialSample(nh.tier3Required, t3Rate);
+ const tier3Healthy = nh.tier3Required - t3Failures;
+ if (t3Failures > 0) {
+ racksDisconnected = computeRacksOnline;
+ }
+
+ racksDisconnected = Math.min(racksDisconnected, computeRacksOnline);
+
+ return {
+ networkHealth: {
+ ...nh,
+ tier1Healthy,
+ tier2Healthy,
+ tier3Healthy,
+ racksDisconnected,
+ },
+ racksDisconnected,
+ };
+}
+
export function processInfrastructure(state: GameState): InfraTickResult {
const notifications: TickNotification[] = [];
let repairCosts = 0;
const engEff = state.talent.departments.engineering.effectiveness;
const opsEff = state.talent.departments.operations.effectiveness;
-
const qaResearchBonus = state.research.completedResearch.includes('quality-assurance') ? 0.25 : 0;
+ const netResearch1 = state.research.completedResearch.includes('network-engineering-i') ? 0.4 : 0;
+ const netResearch2 = state.research.completedResearch.includes('network-engineering-ii') ? 0.5 : 0;
+ const networkResearchBonus = Math.min(0.8, netResearch1 + netResearch2);
- // --- Phase 1: Advance DC Construction ---
- const dataCenters: DataCenter[] = state.infrastructure.dataCenters.map(dc => {
- if (dc.status !== 'constructing') return { ...dc };
-
- const newProgress = dc.constructionProgress + 1;
- if (newProgress >= dc.constructionTotal) {
- notifications.push({
- title: 'Data Center Online',
- message: `${dc.name} is now operational!`,
- type: 'success',
- });
- return { ...dc, constructionProgress: dc.constructionTotal, status: 'operational' as const };
- }
- return { ...dc, constructionProgress: newProgress };
- });
-
- // --- Phase 2: Advance Rack Pipeline ---
- const rackPipeline: RackOrder[] = [];
- const newRacks: Rack[] = [];
-
- for (const order of state.infrastructure.rackPipeline) {
- const speed = stageSpeed(order.stage, engEff, opsEff);
- const newProgress = order.stageProgress + speed;
-
- if (newProgress < order.stageTotal) {
- rackPipeline.push({ ...order, stageProgress: newProgress });
- continue;
- }
-
- if (order.stage === 'decommission') {
- const sku = RACK_SKU_CONFIGS[order.skuId];
- notifications.push({
- title: 'Rack Decommissioned',
- message: `${sku.name} rack has been fully decommissioned.`,
- type: 'info',
- });
- continue;
- }
-
- if (order.stage === 'repair') {
- const total = stageTotal('testing', order);
- rackPipeline.push({
- ...order,
- stage: 'testing',
- stageProgress: 0,
- stageTotal: total,
- });
- continue;
- }
-
- const next = nextStage(order.stage);
-
- if (next === 'production') {
- const sku = RACK_SKU_CONFIGS[order.skuId];
- const dc = dataCenters.find(d => d.id === order.dataCenterId);
- const cooling = dc?.coolingLevel ?? 0;
-
- const effectiveFailRate = sku.testFailureRate
- * (1 - cooling * COOLING_FAILURE_REDUCTION)
- * (1 - opsEff * 0.2)
- * (1 - qaResearchBonus);
-
- if (Math.random() < effectiveFailRate) {
- const repairCost = sku.baseCost * sku.repairCostFraction;
- repairCosts += repairCost;
- rackPipeline.push({
- ...order,
- stage: 'repair',
- stageProgress: 0,
- stageTotal: RACK_REPAIR_BASE_TICKS,
- repairCount: order.repairCount + 1,
- });
- notifications.push({
- title: 'Rack Failed Testing',
- message: `${sku.name} rack failed QA (attempt ${order.repairCount + 1}). Repair cost: $${repairCost.toLocaleString()}`,
- type: 'warning',
- });
- } else {
- newRacks.push({
- id: order.id,
- skuId: order.skuId,
- dataCenterId: order.dataCenterId,
- isHealthy: true,
- });
- notifications.push({
- title: 'Rack Online',
- message: `${sku.name} rack is now in production at ${dc?.name ?? 'data center'}.`,
- type: 'success',
- });
- }
- } else {
- const total = stageTotal(next, order);
- rackPipeline.push({
- ...order,
- stage: next,
- stageProgress: 0,
- stageTotal: total,
- });
- }
- }
-
- // Add newly completed racks to their data centers
- for (const rack of newRacks) {
- const dcIdx = dataCenters.findIndex(d => d.id === rack.dataCenterId);
- if (dcIdx !== -1) {
- dataCenters[dcIdx] = {
- ...dataCenters[dcIdx],
- racks: [...dataCenters[dcIdx].racks, rack],
- };
- }
- }
-
- // --- Phase 3: Production Failures ---
- for (let dcIdx = 0; dcIdx < dataCenters.length; dcIdx++) {
- const dc = dataCenters[dcIdx];
- if (dc.status !== 'operational') continue;
-
- const updatedRacks: Rack[] = [];
- for (const rack of dc.racks) {
- if (!rack.isHealthy) {
- updatedRacks.push(rack);
- continue;
- }
-
- const sku = RACK_SKU_CONFIGS[rack.skuId];
- const effectiveRate = sku.productionFailureRate
- * (1 - dc.coolingLevel * COOLING_FAILURE_REDUCTION)
- * (1 - dc.redundancyLevel * REDUNDANCY_FAILURE_REDUCTION);
-
- if (Math.random() < effectiveRate) {
- updatedRacks.push({ ...rack, isHealthy: false });
- const repairCost = sku.baseCost * sku.repairCostFraction;
- repairCosts += repairCost;
-
- rackPipeline.push({
- id: rack.id,
- skuId: rack.skuId,
- dataCenterId: dc.id,
- stage: 'repair',
- stageProgress: 0,
- stageTotal: RACK_REPAIR_BASE_TICKS,
- totalCost: repairCost,
- repairCount: 0,
- });
-
- notifications.push({
- title: 'Rack Failure',
- message: `${sku.name} rack failed in ${dc.name}. Sent for repair.`,
- type: 'danger',
- });
- } else {
- updatedRacks.push(rack);
- }
- }
-
- // Remove failed racks from the DC (they're now in the repair pipeline)
- dataCenters[dcIdx] = {
- ...dc,
- racks: updatedRacks.filter(r => r.isHealthy),
- };
- }
-
- // --- Phase 4: Compute Aggregates ---
let totalFlops = 0;
let totalUptime = 0;
let totalRackCount = 0;
+ let totalComputeRackCount = 0;
+ let totalDataCenterCount = 0;
let dcWithRacks = 0;
- for (let dcIdx = 0; dcIdx < dataCenters.length; dcIdx++) {
- const dc = dataCenters[dcIdx];
- if (dc.status !== 'operational') continue;
-
- const location = LOCATION_CONFIGS[dc.location];
- const tierConfig = DC_TIER_CONFIGS[dc.tier];
-
- let dcFlops = 0;
- let usedPowerKW = 0;
- const repairingForDc = rackPipeline.filter(o => o.dataCenterId === dc.id && o.stage === 'repair').length;
- const healthyCount = dc.racks.length;
- const totalInDc = dc.racks.length + repairingForDc;
-
- for (const rack of dc.racks) {
- const sku = RACK_SKU_CONFIGS[rack.skuId];
- dcFlops += sku.flopsPerRack;
- usedPowerKW += sku.powerDrawKW;
+ const clusters: Cluster[] = state.infrastructure.clusters.map(cluster => {
+ // Advance cluster construction
+ if (cluster.status === 'constructing') {
+ const newProgress = cluster.constructionProgress + 1;
+ if (newProgress >= cluster.constructionTotal) {
+ notifications.push({
+ title: 'Cluster Online',
+ message: `${cluster.name} cluster in ${LOCATION_CONFIGS[cluster.locationId].name} is now operational!`,
+ type: 'success',
+ });
+ return { ...cluster, constructionProgress: cluster.constructionTotal, status: 'operational' as const, campuses: cluster.campuses };
+ }
+ return { ...cluster, constructionProgress: newProgress };
}
- const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id && o.stage !== 'decommission').length;
- const usedSlots = totalInDc + pipelineRacksForDc;
+ const campuses: Campus[] = cluster.campuses.map(campus => {
+ // Advance campus construction
+ if (campus.status === 'constructing') {
+ const newProgress = campus.constructionProgress + 1;
+ if (newProgress >= campus.constructionTotal) {
+ notifications.push({
+ title: 'Campus Ready',
+ message: `Campus ${campus.name} is now operational!`,
+ type: 'success',
+ });
+ return { ...campus, constructionProgress: campus.constructionTotal, status: 'operational' as const, dataCenters: campus.dataCenters };
+ }
+ return { ...campus, constructionProgress: newProgress };
+ }
- const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
- * location.energyCostMultiplier;
- const maintenanceCostPerTick = totalInDc * BASE_MAINTENANCE_PER_RACK;
+ const dataCenters: DataCenter[] = campus.dataCenters.map(dc => {
+ // Advance DC construction
+ if (dc.status === 'constructing') {
+ const newProgress = dc.constructionProgress + 1;
+ if (newProgress >= dc.constructionTotal) {
+ notifications.push({
+ title: 'Data Center Online',
+ message: `${dc.name} is now operational!`,
+ type: 'success',
+ });
+ return { ...dc, constructionProgress: dc.constructionTotal, status: 'operational' as const };
+ }
+ return { ...dc, constructionProgress: newProgress };
+ }
- const currentUptime = totalInDc > 0 ? healthyCount / totalInDc : 1;
+ let computeRacksOnline = dc.computeRacksOnline;
+ let computeRacksFailed = dc.computeRacksFailed;
+ let dcRepairCosts = 0;
- totalFlops += dcFlops;
- totalRackCount += totalInDc;
- if (totalInDc > 0) {
- totalUptime += currentUptime;
- dcWithRacks++;
- }
+ // Process retrofit
+ if (dc.status === 'retrofitting' && dc.retrofitState) {
+ const rs = { ...dc.retrofitState };
+ rs.progress += (1 + opsEff * 0.1);
- dataCenters[dcIdx] = {
- ...dataCenters[dcIdx],
- usedSlots,
- usedPowerKW,
- energyCostPerTick,
- maintenanceCostPerTick,
- currentUptime,
- };
- }
+ if (rs.progress >= rs.total) {
+ if (rs.phase === 'decommissioning') {
+ const installSku = RACK_SKU_CONFIGS[rs.toSkuId];
+ const installTotal = cohortStageTotal('installation', rs.toSkuId, rs.racksRemaining);
+ return {
+ ...dc,
+ computeRacksOnline: 0,
+ computeRacksFailed: 0,
+ rackSkuId: rs.toSkuId,
+ deploymentCohorts: [{
+ id: `retrofit-${dc.id}-${Date.now()}`,
+ count: rs.racksRemaining,
+ skuId: rs.toSkuId,
+ stage: 'installation' as PipelineStage,
+ stageProgress: 0,
+ stageTotal: installTotal,
+ repairCount: 0,
+ }],
+ retrofitState: {
+ ...rs,
+ phase: 'installing' as const,
+ progress: 0,
+ total: installTotal,
+ },
+ networkHealth: computeNetworkHealth(0),
+ effectiveComputeRacks: 0,
+ usedSlots: 0,
+ usedPowerKW: 0,
+ currentUptime: 0,
+ energyCostPerTick: DC_TIER_CONFIGS[dc.tier].baseEnergyCostPerTick * LOCATION_CONFIGS[cluster.locationId].energyCostMultiplier,
+ maintenanceCostPerTick: 0,
+ };
+ } else {
+ notifications.push({
+ title: 'Retrofit Complete',
+ message: `${dc.name} retrofit to ${RACK_SKU_CONFIGS[rs.toSkuId].name} is complete!`,
+ type: 'success',
+ });
+ return {
+ ...dc,
+ status: 'operational' as const,
+ retrofitState: null,
+ };
+ }
+ }
+ return { ...dc, retrofitState: rs };
+ }
+
+ // Process deployment cohorts
+ const updatedCohorts: DeploymentCohort[] = [];
+ let racksJustOnlined = 0;
+ let racksFailedTesting = 0;
+
+ for (const cohort of dc.deploymentCohorts) {
+ const speed = stageSpeed(cohort.stage, engEff, opsEff);
+ const newProgress = cohort.stageProgress + speed;
+
+ if (newProgress < cohort.stageTotal) {
+ updatedCohorts.push({ ...cohort, stageProgress: newProgress });
+ continue;
+ }
+
+ if (cohort.stage === 'decommission') {
+ continue;
+ }
+
+ if (cohort.stage === 'repair') {
+ const testTotal = cohortStageTotal('testing', cohort.skuId, cohort.count);
+ updatedCohorts.push({
+ ...cohort,
+ stage: 'testing',
+ stageProgress: 0,
+ stageTotal: testTotal,
+ });
+ continue;
+ }
+
+ const next = nextStage(cohort.stage);
+
+ if (next === 'production') {
+ const sku = RACK_SKU_CONFIGS[cohort.skuId];
+ const effectiveFailRate = sku.testFailureRate
+ * (1 - dc.coolingLevel * COOLING_FAILURE_REDUCTION)
+ * (1 - opsEff * 0.2)
+ * (1 - qaResearchBonus);
+
+ const failed = binomialSample(cohort.count, effectiveFailRate);
+ const passed = cohort.count - failed;
+
+ racksJustOnlined += passed;
+
+ if (failed > 0) {
+ racksFailedTesting += failed;
+ const repairCost = sku.baseCost * sku.repairCostFraction * failed;
+ dcRepairCosts += repairCost;
+
+ updatedCohorts.push({
+ id: `repair-${cohort.id}`,
+ count: failed,
+ skuId: cohort.skuId,
+ stage: 'repair',
+ stageProgress: 0,
+ stageTotal: cohortStageTotal('repair', cohort.skuId, failed),
+ repairCount: cohort.repairCount + 1,
+ });
+ }
+ } else {
+ const total = cohortStageTotal(next, cohort.skuId, cohort.count);
+ updatedCohorts.push({
+ ...cohort,
+ stage: next,
+ stageProgress: 0,
+ stageTotal: total,
+ });
+ }
+ }
+
+ computeRacksOnline += racksJustOnlined;
+
+ if (racksFailedTesting > 0) {
+ const skuName = dc.rackSkuId ? RACK_SKU_CONFIGS[dc.rackSkuId].name : 'Unknown';
+ notifications.push({
+ title: 'Racks Failed Testing',
+ message: `${dc.name}: ${racksFailedTesting} ${skuName} rack${racksFailedTesting > 1 ? 's' : ''} failed QA — repair batch created.`,
+ type: 'warning',
+ });
+ }
+
+ if (racksJustOnlined > 0 && updatedCohorts.filter(c => c.stage !== 'repair').length === 0) {
+ notifications.push({
+ title: 'Deployment Complete',
+ message: `${dc.name}: all racks deployed and online!`,
+ type: 'success',
+ });
+ }
+
+ // Production failures (statistical)
+ if (computeRacksOnline > 0 && dc.rackSkuId) {
+ const sku = RACK_SKU_CONFIGS[dc.rackSkuId];
+ const effectiveRate = sku.productionFailureRate
+ * (1 - dc.coolingLevel * COOLING_FAILURE_REDUCTION)
+ * (1 - dc.redundancyLevel * REDUNDANCY_FAILURE_REDUCTION);
+
+ const prodFailures = binomialSample(computeRacksOnline, effectiveRate);
+ if (prodFailures > 0) {
+ computeRacksOnline -= prodFailures;
+ computeRacksFailed += prodFailures;
+ const repairCost = sku.baseCost * sku.repairCostFraction * prodFailures;
+ dcRepairCosts += repairCost;
+
+ updatedCohorts.push({
+ id: `prodfail-${dc.id}-${Date.now()}`,
+ count: prodFailures,
+ skuId: dc.rackSkuId,
+ stage: 'repair',
+ stageProgress: 0,
+ stageTotal: cohortStageTotal('repair', dc.rackSkuId, prodFailures),
+ repairCount: 0,
+ });
+
+ notifications.push({
+ title: 'Production Failure',
+ message: `${dc.name}: ${prodFailures} rack${prodFailures > 1 ? 's' : ''} failed in production — sent for repair.`,
+ type: 'danger',
+ });
+ }
+ }
+
+ repairCosts += dcRepairCosts;
+
+ // Network health
+ const baseNetworkHealth = computeNetworkHealth(computeRacksOnline);
+ const { networkHealth, racksDisconnected } = processNetworkFailures(
+ baseNetworkHealth, computeRacksOnline, networkResearchBonus,
+ );
+
+ if (racksDisconnected > 0) {
+ if (networkHealth.tier3Healthy < networkHealth.tier3Required) {
+ notifications.push({
+ title: 'Core Network Failure',
+ message: `${dc.name}: Tier-3 core switch failure — entire DC disconnected!`,
+ type: 'danger',
+ });
+ } else if (racksDisconnected >= NETWORK_TOPOLOGY.tier1BlastRadius * NETWORK_TOPOLOGY.tier2BlastRadiusMultiplier) {
+ notifications.push({
+ title: 'Network Switch Failure',
+ message: `${dc.name}: Tier-2 aggregation failure — ${racksDisconnected} racks disconnected.`,
+ type: 'warning',
+ });
+ }
+ }
+
+ const effectiveComputeRacks = computeRacksOnline - racksDisconnected;
+
+ // Compute aggregates for this DC
+ const location = LOCATION_CONFIGS[cluster.locationId];
+ const tierConfig = DC_TIER_CONFIGS[dc.tier];
+ const totalRacksInDc = computeRacksOnline + computeRacksFailed;
+ const netSlots = networkSlotsRequired(computeRacksOnline);
+ const pipelineRacks = updatedCohorts
+ .filter(c => c.stage !== 'decommission' && c.stage !== 'repair')
+ .reduce((sum, c) => sum + c.count, 0);
+ const usedSlots = totalRacksInDc + netSlots + pipelineRacks;
+
+ let usedPowerKW = 0;
+ let dcFlops = 0;
+ if (dc.rackSkuId && computeRacksOnline > 0) {
+ const sku = RACK_SKU_CONFIGS[dc.rackSkuId];
+ usedPowerKW = computeRacksOnline * sku.powerDrawKW;
+ dcFlops = effectiveComputeRacks * sku.flopsPerRack;
+ }
+
+ const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
+ * location.energyCostMultiplier;
+ const maintenanceCostPerTick = totalRacksInDc * BASE_MAINTENANCE_PER_RACK;
+
+ const currentUptime = totalRacksInDc > 0 ? effectiveComputeRacks / totalRacksInDc : 1;
+
+ totalFlops += dcFlops;
+ totalRackCount += totalRacksInDc + netSlots;
+ totalComputeRackCount += totalRacksInDc;
+ totalDataCenterCount++;
+ if (totalRacksInDc > 0) {
+ totalUptime += currentUptime;
+ dcWithRacks++;
+ }
+
+ return {
+ ...dc,
+ computeRacksOnline,
+ computeRacksFailed,
+ deploymentCohorts: updatedCohorts,
+ networkHealth,
+ effectiveComputeRacks,
+ usedSlots,
+ usedPowerKW,
+ energyCostPerTick,
+ maintenanceCostPerTick,
+ currentUptime,
+ };
+ });
+
+ return { ...campus, dataCenters };
+ });
+
+ return { ...cluster, campuses };
+ });
return {
infrastructure: {
- dataCenters,
- rackPipeline,
+ clusters,
totalFlops,
totalUptime: dcWithRacks > 0 ? totalUptime / dcWithRacks : 1,
totalRackCount,
+ totalComputeRackCount,
+ totalDataCenterCount,
},
notifications,
repairCosts,
diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts
index bcf6d61..1e4b524 100644
--- a/packages/shared/src/constants/gameBalance.ts
+++ b/packages/shared/src/constants/gameBalance.ts
@@ -1,4 +1,4 @@
-import type { DCTier, DCTierConfig, RackSkuId, RackSkuConfig } from '../types/infrastructure';
+import type { DCTier, DCTierConfig, RackSkuId, RackSkuConfig, NetworkTopologyConfig, CampusTierCost, ClusterCostConfig } from '../types/infrastructure';
export const TICK_INTERVAL_MS = 1000;
export const MAX_OFFLINE_TICKS = 86_400;
@@ -9,7 +9,7 @@ export const FINANCIAL_SNAPSHOT_INTERVAL = 60;
export const MAX_FINANCIAL_HISTORY = 1000;
export const MAX_REPUTATION_HISTORY = 500;
-export const STARTING_MONEY = 50_000;
+export const STARTING_MONEY = 600_000;
export const BASE_ENERGY_COST_PER_FLOP = 0.001;
export const TRAINING_BASE_TICKS = 120;
@@ -58,53 +58,84 @@ export const DC_TIER_CONFIGS: Record = {
small: {
tier: 'small',
name: 'Small Data Center',
- rackSlots: 12,
- powerBudgetKW: 60,
- baseCost: 10_000,
- buildTimeTicks: 300,
- firstBuildTimeTicks: 10,
+ rackSlots: 200,
+ powerBudgetKW: 1_000,
+ baseCost: 500_000,
+ buildTimeTicks: 600,
+ firstBuildTimeTicks: 30,
requiredEra: 'startup',
requiredResearch: null,
- baseEnergyCostPerTick: 5,
+ baseEnergyCostPerTick: 50,
},
medium: {
tier: 'medium',
name: 'Medium Data Center',
- rackSlots: 30,
- powerBudgetKW: 200,
- baseCost: 50_000,
- buildTimeTicks: 900,
- firstBuildTimeTicks: 900,
+ rackSlots: 500,
+ powerBudgetKW: 3_000,
+ baseCost: 2_000_000,
+ buildTimeTicks: 1200,
+ firstBuildTimeTicks: 1200,
requiredEra: 'scaleup',
requiredResearch: 'dc-engineering-ii',
- baseEnergyCostPerTick: 15,
+ baseEnergyCostPerTick: 150,
},
large: {
tier: 'large',
name: 'Large Data Center',
- rackSlots: 60,
- powerBudgetKW: 500,
- baseCost: 200_000,
- buildTimeTicks: 1800,
- firstBuildTimeTicks: 1800,
+ rackSlots: 1000,
+ powerBudgetKW: 7_000,
+ baseCost: 8_000_000,
+ buildTimeTicks: 2400,
+ firstBuildTimeTicks: 2400,
requiredEra: 'bigtech',
requiredResearch: 'dc-engineering-iii',
- baseEnergyCostPerTick: 40,
+ baseEnergyCostPerTick: 400,
},
mega: {
tier: 'mega',
name: 'Mega Data Center',
- rackSlots: 120,
- powerBudgetKW: 1200,
- baseCost: 1_000_000,
+ rackSlots: 1500,
+ powerBudgetKW: 12_000,
+ baseCost: 25_000_000,
buildTimeTicks: 3600,
firstBuildTimeTicks: 3600,
requiredEra: 'agi',
requiredResearch: 'dc-engineering-iv',
- baseEnergyCostPerTick: 100,
+ baseEnergyCostPerTick: 1000,
},
};
+// --- Campus Costs (scale with DC tier) ---
+
+export const CAMPUS_TIER_COSTS: Record = {
+ small: { baseCost: 200_000, buildTimeTicks: 300 },
+ medium: { baseCost: 800_000, buildTimeTicks: 600 },
+ large: { baseCost: 3_000_000, buildTimeTicks: 900 },
+ mega: { baseCost: 10_000_000, buildTimeTicks: 1200 },
+};
+
+export const FIRST_CAMPUS_BUILD_TICKS = 15;
+
+// --- Cluster Costs (first is free) ---
+
+export const CLUSTER_COST_CONFIG: ClusterCostConfig = {
+ baseCost: 250_000,
+ buildTimeTicks: 600,
+};
+
+// --- Network Topology ---
+
+export const NETWORK_TOPOLOGY: NetworkTopologyConfig = {
+ tier1PerCompute: 24,
+ tier2PerTier1: 6,
+ tier3PerDC: 2,
+ tier1FailureRate: 0.0001,
+ tier2FailureRate: 0.00005,
+ tier3FailureRate: 0.00002,
+ tier1BlastRadius: 24,
+ tier2BlastRadiusMultiplier: 6,
+};
+
// --- Rack SKU Configs ---
export const RACK_SKU_CONFIGS: Record = {
@@ -260,13 +291,15 @@ export const REDUNDANCY_FAILURE_REDUCTION = 0.5;
export const DC_UPGRADE_COST_FRACTION = 0.25;
export const DC_UPGRADE_INCREMENT = 0.1;
+export const COHORT_SCALE_FACTOR = 0.0003;
+
export const FUNDING_ROUNDS = {
- seed: { amount: 100_000, dilution: 0.10, requirements: { minRevenue: 100, minUsers: 0, minReputation: 0 } },
- seriesA: { amount: 500_000, dilution: 0.15, requirements: { minRevenue: 500, minUsers: 100, minReputation: 20 } },
- seriesB: { amount: 2_000_000, dilution: 0.12, requirements: { minRevenue: 5_000, minUsers: 1_000, minReputation: 30 } },
- seriesC: { amount: 10_000_000, dilution: 0.10, requirements: { minRevenue: 50_000, minUsers: 10_000, minReputation: 40 } },
- seriesD: { amount: 50_000_000, dilution: 0.08, requirements: { minRevenue: 500_000, minUsers: 50_000, minReputation: 50 } },
- ipo: { amount: 200_000_000, dilution: 0.20, requirements: { minRevenue: 5_000_000, minUsers: 100_000, minReputation: 60 } },
+ seed: { amount: 500_000, dilution: 0.10, requirements: { minRevenue: 500, minUsers: 0, minReputation: 0 } },
+ seriesA: { amount: 2_000_000, dilution: 0.15, requirements: { minRevenue: 2_500, minUsers: 100, minReputation: 20 } },
+ seriesB: { amount: 10_000_000, dilution: 0.12, requirements: { minRevenue: 25_000, minUsers: 1_000, minReputation: 30 } },
+ seriesC: { amount: 50_000_000, dilution: 0.10, requirements: { minRevenue: 250_000, minUsers: 10_000, minReputation: 40 } },
+ seriesD: { amount: 200_000_000, dilution: 0.08, requirements: { minRevenue: 2_500_000, minUsers: 50_000, minReputation: 50 } },
+ ipo: { amount: 1_000_000_000, dilution: 0.20, requirements: { minRevenue: 25_000_000, minUsers: 100_000, minReputation: 60 } },
} as const;
export const OPEN_SOURCE_REPUTATION_BOOST = 8;
diff --git a/packages/shared/src/types/economy.ts b/packages/shared/src/types/economy.ts
index 95e3bcf..98b5d0b 100644
--- a/packages/shared/src/types/economy.ts
+++ b/packages/shared/src/types/economy.ts
@@ -46,7 +46,7 @@ export interface FinancialSnapshot {
}
export const INITIAL_ECONOMY: EconomyState = {
- money: 50_000,
+ money: 600_000,
totalRevenue: 0,
totalExpenses: 0,
revenuePerTick: 0,
@@ -56,7 +56,7 @@ export const INITIAL_ECONOMY: EconomyState = {
currentRound: null,
completedRounds: [],
founderEquity: 1.0,
- valuation: 100_000,
+ valuation: 1_000_000,
isPublic: false,
},
financialHistory: [],
diff --git a/packages/shared/src/types/gameState.ts b/packages/shared/src/types/gameState.ts
index 4fcb1b1..7e3017b 100644
--- a/packages/shared/src/types/gameState.ts
+++ b/packages/shared/src/types/gameState.ts
@@ -58,4 +58,4 @@ export const INITIAL_SETTINGS: GameSettings = {
sfxVolume: 0.7,
};
-export const SAVE_VERSION = 2;
+export const SAVE_VERSION = 3;
diff --git a/packages/shared/src/types/infrastructure.ts b/packages/shared/src/types/infrastructure.ts
index ec272d3..45df26b 100644
--- a/packages/shared/src/types/infrastructure.ts
+++ b/packages/shared/src/types/infrastructure.ts
@@ -1,9 +1,38 @@
import type { Era } from './gameState';
+// --- Cluster (regional container) ---
+
+export type ClusterStatus = 'constructing' | 'operational';
+
+export interface Cluster {
+ id: string;
+ name: string;
+ locationId: LocationId;
+ campuses: Campus[];
+ status: ClusterStatus;
+ constructionProgress: number;
+ constructionTotal: number;
+}
+
+// --- Campus (holds same-tier DCs) ---
+
+export type CampusStatus = 'constructing' | 'operational';
+
+export interface Campus {
+ id: string;
+ name: string;
+ clusterId: string;
+ dcTier: DCTier;
+ dataCenters: DataCenter[];
+ status: CampusStatus;
+ constructionProgress: number;
+ constructionTotal: number;
+}
+
// --- Data Center ---
export type DCTier = 'small' | 'medium' | 'large' | 'mega';
-export type DCStatus = 'constructing' | 'operational';
+export type DCStatus = 'constructing' | 'operational' | 'retrofitting';
export interface DCTierConfig {
tier: DCTier;
@@ -21,19 +50,71 @@ export interface DCTierConfig {
export interface DataCenter {
id: string;
name: string;
- location: LocationId;
+ campusId: string;
tier: DCTier;
status: DCStatus;
constructionProgress: number;
constructionTotal: number;
- racks: Rack[];
+ rackSkuId: RackSkuId | null;
+ computeRacksOnline: number;
+ computeRacksFailed: number;
+ networkHealth: NetworkHealthState;
+ deploymentCohorts: DeploymentCohort[];
+ retrofitState: RetrofitState | null;
coolingLevel: number;
redundancyLevel: number;
- currentUptime: number;
- energyCostPerTick: number;
- maintenanceCostPerTick: number;
+ effectiveComputeRacks: number;
usedSlots: number;
usedPowerKW: number;
+ energyCostPerTick: number;
+ maintenanceCostPerTick: number;
+ currentUptime: number;
+}
+
+// --- Network Topology ---
+
+export interface NetworkHealthState {
+ tier1Required: number;
+ tier1Healthy: number;
+ tier2Required: number;
+ tier2Healthy: number;
+ tier3Required: number;
+ tier3Healthy: number;
+ racksDisconnected: number;
+}
+
+export interface NetworkTopologyConfig {
+ tier1PerCompute: number;
+ tier2PerTier1: number;
+ tier3PerDC: number;
+ tier1FailureRate: number;
+ tier2FailureRate: number;
+ tier3FailureRate: number;
+ tier1BlastRadius: number;
+ tier2BlastRadiusMultiplier: number;
+}
+
+export function networkSlotsRequired(computeRacks: number): number {
+ if (computeRacks <= 0) return 0;
+ const tier1 = Math.ceil(computeRacks / 24);
+ const tier2 = Math.ceil(tier1 / 6);
+ const tier3 = 2;
+ return tier1 + tier2 + tier3;
+}
+
+export function maxComputeRacks(totalSlots: number): number {
+ if (totalSlots <= 2) return 0;
+ let lo = 0;
+ let hi = totalSlots;
+ while (lo < hi) {
+ const mid = Math.ceil((lo + hi) / 2);
+ if (mid + networkSlotsRequired(mid) <= totalSlots) {
+ lo = mid;
+ } else {
+ hi = mid - 1;
+ }
+ }
+ return lo;
}
// --- Racks ---
@@ -70,43 +151,64 @@ export interface RackSkuConfig {
repairCostFraction: number;
}
-export interface Rack {
- id: string;
- skuId: RackSkuId;
- dataCenterId: string;
- isHealthy: boolean;
-}
+// --- Deployment Cohort (batch pipeline) ---
-export interface RackOrder {
+export interface DeploymentCohort {
id: string;
+ count: number;
skuId: RackSkuId;
- dataCenterId: string;
stage: PipelineStage;
stageProgress: number;
stageTotal: number;
- totalCost: number;
repairCount: number;
}
+// --- Retrofit ---
+
+export interface RetrofitState {
+ fromSkuId: RackSkuId;
+ toSkuId: RackSkuId;
+ phase: 'decommissioning' | 'installing';
+ progress: number;
+ total: number;
+ racksRemaining: number;
+}
+
+// --- Campus Config ---
+
+export interface CampusTierCost {
+ baseCost: number;
+ buildTimeTicks: number;
+}
+
+// --- Cluster Config ---
+
+export interface ClusterCostConfig {
+ baseCost: number;
+ buildTimeTicks: number;
+}
+
// --- Infrastructure State ---
export interface InfrastructureState {
- dataCenters: DataCenter[];
- rackPipeline: RackOrder[];
+ clusters: Cluster[];
totalFlops: number;
totalUptime: number;
totalRackCount: number;
+ totalComputeRackCount: number;
+ totalDataCenterCount: number;
}
export const INITIAL_INFRASTRUCTURE: InfrastructureState = {
- dataCenters: [],
- rackPipeline: [],
+ clusters: [],
totalFlops: 0,
totalUptime: 1,
totalRackCount: 0,
+ totalComputeRackCount: 0,
+ totalDataCenterCount: 0,
};
-// --- Locations (unchanged) ---
+// --- Locations ---
export type LocationId = 'us-west' | 'us-east' | 'eu-west' | 'eu-north' | 'asia-east' | 'asia-south' | 'middle-east';