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() {

Dashboard

- {dataCenters.length === 0 && ( + {totalDCs === 0 && ( - Welcome to AI Tycoon! Start by building a data center in the Infrastructure tab, then order racks to begin training your first AI model. + Welcome to AI Tycoon! Start by building a cluster in the Infrastructure tab, then add a campus and data center to deploy racks and train your first AI model. )} - {dataCenters.length > 0 && trainedModels.length === 0 && !activeTraining && ( + {totalDCs > 0 && trainedModels.length === 0 && !activeTraining && ( You have compute available! Head to the Models tab to allocate compute for training and start your first model. @@ -58,7 +58,7 @@ export function DashboardPage() { useGameStore.getState().setActivePage('infrastructure')} @@ -194,7 +194,7 @@ export function DashboardPage() {
- {dataCenters.length === 0 && ( + {totalDCs === 0 && (

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 ? ( + + ) : ( + {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 (
- - - - - - - - - - - - {sortedRacks.map(rack => { - const sku = RACK_SKU_CONFIGS[rack.skuId]; - const isSelected = selected.has(rack.id); - return ( - - - - - - - - ); - })} - -
- 0} - onChange={toggleAll} - className="accent-accent" - /> -
- toggleSelect(rack.id)} - className="accent-accent" - /> - {sku.name}{formatNumber(sku.flopsPerRack)}{sku.powerDrawKW}kW - {rack.isHealthy ? ( - - - Healthy - - ) : ( - - - Faulted - - )} -
- - {selected.size > 0 && ( -
- - {selected.size} rack{selected.size !== 1 ? 's' : ''} selected - - ({formatNumber(decomImpact.flops)} FLOPS · {decomImpact.power}kW) - - - -
- )} - - {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 ( -
-
- - - - - - - - - - - - {allSkus.map(sku => { - const isSelected = selectedSku === sku.id; - return ( - sku.available && handleSelectSku(sku.id)} - > - - - - - - - - ); - })} - -
- SKUGPUsFLOPSPowerCost
- {sku.available ? ( - handleSelectSku(sku.id)} - className="accent-accent" - /> - ) : ( - - )} - -
{sku.name}
- {sku.lockReason &&
{sku.lockReason}
} -
{sku.gpuCount}{formatNumber(sku.flopsPerRack)}{sku.powerDrawKW}kW{formatMoney(sku.baseCost)}
- - {selectedSku && ( -
- Quantity: -
- - { - 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} - /> - -
- {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)} -
-
- - - - ) : ( -
- 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

+ +
+ + {clusters.length === 0 && ( + + Build your first cluster to establish a regional presence. Your first cluster is free! + + )} + + {clusters.length > 0 && ( +
+ + + + + +
+ )} + +
+ {clusters.map(cluster => ( -
+ + {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

+ +
+
+ + 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" /> +
+ +
+ + +
+ + {alreadyExists &&

You already have a cluster in this region.

} + +
+
Cost:{isFirst ? 'Free' : formatMoney(cost)}
+
Build Time:{isFirst ? 'Instant' : `${CLUSTER_COST_CONFIG.buildTimeTicks}s`}
+
+ +
+ + +
+
+
+
+ ); +} + +// ─── 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

+
+
+ +
+ + {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 => ( + + ))} +
+ + {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

+ +
+
+ + 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" /> +
+ +
+ + +
+ +
+
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
+
+ +
+ + +
+
+
+
+ ); +} + +// ─── 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

+
+
+
+
-
- - + {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 => ( + + ))}
- {expanded && ( + {showAddDC && ( +
setShowAddDC(false)}> +
e.stopPropagation()}> +

Add Data Center{bulkCount > 1 ? 's' : ''}

+
+
+ + 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" /> +
+
+ + 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)}
+
+
+ + +
+
+
+
+ )} +
+ ); +} + +// ─── 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} +
+ +
+
+ {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 => ( - ))}
- {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 => ( + + ))} +
+ + ) : ( +
+ This DC runs {sku!.name}. Available: {availableSlots} compute slots. +
+ )} + + {(dc.rackSkuId || selectedSku) && availableSlots > 0 && ( +
+
+ + 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
+
+ ); + })()} + +
+ + +
+
+ )} +
)} - {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 => ( + + ))} +
+ + )} +
)} + {/* Upgrades Tab */} {activeTab === 'upgrades' && ( -
-
-
-
-
-
+ ); + })} +
+ )} + + {/* 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} +
-
- - -
-

- 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

-
-
- - 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 - /> -
-
- - -
-
- - -
-
- -
-
-
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 -
-
-
- -
- - -
-
- ); -} - -// ─── Page ────────────────────────────────────────────────────── - -export function InfrastructurePage() { - const dataCenters = useGameStore((s) => s.infrastructure.dataCenters); - const [showNewDC, setShowNewDC] = useState(false); - - return ( -
-
-

Infrastructure

- -
- - - 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';