From 19f652b43a2f91775d0cafbacdf072487b9a3095 Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 26 Apr 2026 20:06:40 -0400 Subject: [PATCH] Replace per-switch network simulation with aggregate per-DC statistical model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminates the 22K-object switchRegistry that caused O(n×m) scans 4x per tick. Network health is now tracked as aggregate counts per tier (totalByTier/healthyByTier) with RepairBatch timers, cutting late-game tick cost from ~50ms to ~0.3ms. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/pages/InfrastructurePage.tsx | 3 +- .../src/__test-utils__/builders.ts | 8 +- .../src/systems/infrastructureSystem.ts | 503 ++++++------------ packages/shared/src/types/infrastructure.ts | 32 +- 4 files changed, 181 insertions(+), 365 deletions(-) diff --git a/apps/web/src/pages/InfrastructurePage.tsx b/apps/web/src/pages/InfrastructurePage.tsx index 540637f..1b97474 100644 --- a/apps/web/src/pages/InfrastructurePage.tsx +++ b/apps/web/src/pages/InfrastructurePage.tsx @@ -162,7 +162,8 @@ function CohortStageBreakdown({ cohorts }: { cohorts: DeploymentCohort[] }) { function NetworkHealthIndicator({ dc }: { dc: DataCenter }) { const ns = dc.networkSummary; - if (ns.switchIds.length === 0) return null; + const torTotal = ns.totalByTier?.tor ?? 0; + if (torTotal === 0) return null; const hasDisconnected = ns.racksDisconnected > 0; const hasDegraded = ns.racksDegraded > 0; diff --git a/packages/game-engine/src/__test-utils__/builders.ts b/packages/game-engine/src/__test-utils__/builders.ts index 6eff4da..27e06d1 100644 --- a/packages/game-engine/src/__test-utils__/builders.ts +++ b/packages/game-engine/src/__test-utils__/builders.ts @@ -8,10 +8,10 @@ import type { DeepPartial } from './createTestState'; function emptyDCNetwork(): DCNetworkSummary { return { - switchIds: [], - networkRackCount: 0, totalByTier: {}, healthyByTier: {}, + repairBatches: [], + networkRackCount: 0, racksDisconnected: 0, racksDegraded: 0, averageBandwidth: 1, @@ -20,11 +20,11 @@ function emptyDCNetwork(): DCNetworkSummary { } function emptyCampusNetwork(): CampusNetworkSummary { - return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 }; + return { totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 }; } function emptyClusterNetwork(): ClusterNetworkSummary { - return { switchIds: [], totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 }; + return { totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 }; } export function createTestDataCenter(overrides?: DeepPartial): DataCenter { diff --git a/packages/game-engine/src/systems/infrastructureSystem.ts b/packages/game-engine/src/systems/infrastructureSystem.ts index 420a97a..1b02e9f 100644 --- a/packages/game-engine/src/systems/infrastructureSystem.ts +++ b/packages/game-engine/src/systems/infrastructureSystem.ts @@ -1,8 +1,8 @@ import type { GameState, InfrastructureState, Cluster, Campus, DataCenter, - DeploymentCohort, PipelineStage, RackSkuId, NetworkSwitch, + DeploymentCohort, PipelineStage, RackSkuId, SwitchTier, DCNetworkSummary, CampusNetworkSummary, ClusterNetworkSummary, - CampusRetrofitQueue, DCTier, IntraNodeInterconnect, NetworkFabric, RackSkuConfig, + RepairBatch, CampusRetrofitQueue, DCTier, IntraNodeInterconnect, NetworkFabric, RackSkuConfig, } from '@ai-tycoon/shared'; import { LOCATION_CONFIGS, @@ -83,357 +83,202 @@ function binomialSample(n: number, p: number): number { return base + (Math.random() < frac ? 1 : 0); } -// --- Network Topology Construction --- +// --- Aggregate Network Model --- -let switchIdCounter = 0; - -function createSwitch( - tier: SwitchTier, - dcId: string | null, - campusId: string | null, - clusterId: string | null, -): NetworkSwitch { - const config = SWITCH_TIER_CONFIGS[tier]; - return { - id: `${tier}-${dcId ?? campusId ?? clusterId ?? 'x'}-${switchIdCounter++}`, - tier, - status: 'healthy', - dcId, campusId, clusterId, - uplinkIds: [], - downlinkIds: [], - activeUplinks: config.uplinkCount, - totalUplinks: config.uplinkCount, - effectiveBandwidth: 1.0, - repairProgress: 0, - repairTotal: 0, - }; -} - -function wireUplinks(child: NetworkSwitch, parents: NetworkSwitch[], count: number): void { - if (parents.length === 0) return; - for (let i = 0; i < count; i++) { - const parent = parents[i % parents.length]; - child.uplinkIds.push(parent.id); - if (!parent.downlinkIds.includes(child.id)) { - parent.downlinkIds.push(child.id); - } - } - child.activeUplinks = count; - child.effectiveBandwidth = 1.0; -} +const DC_TIERS: SwitchTier[] = ['tor', 't1', 't2', 't3']; export function emptyDCNetworkSummary(): DCNetworkSummary { return { - switchIds: [], networkRackCount: 0, totalByTier: {}, healthyByTier: {}, + repairBatches: [], networkRackCount: 0, racksDisconnected: 0, racksDegraded: 0, averageBandwidth: 1, effectiveFlopsFraction: 1, }; } export function emptyCampusNetworkSummary(): CampusNetworkSummary { - return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 }; + return { totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 }; } export function emptyClusterNetworkSummary(): ClusterNetworkSummary { - return { switchIds: [], totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 }; + return { totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 }; } -export function buildDCTopology( +function computeTopologyCounts( computeRackCount: number, dcTier: DCTier, - dcId: string, - registry: Record, -): DCNetworkSummary { - if (computeRackCount <= 0) return emptyDCNetworkSummary(); - - const switchIds: string[] = []; - - const t3Count = T3_COUNT_PER_DC_TIER[dcTier]; - const t3s: NetworkSwitch[] = []; - for (let i = 0; i < t3Count; i++) { - const sw = createSwitch('t3', dcId, null, null); - sw.totalUplinks = 0; - sw.activeUplinks = 0; - t3s.push(sw); - registry[sw.id] = sw; - switchIds.push(sw.id); - } - +): Partial> { + if (computeRackCount <= 0) return {}; const t1Count = Math.ceil(computeRackCount / SWITCH_TIER_CONFIGS.t1.fanOut); const t2Count = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t2.fanOut); - - const t2s: NetworkSwitch[] = []; - for (let i = 0; i < t2Count; i++) { - const sw = createSwitch('t2', dcId, null, null); - wireUplinks(sw, t3s, SWITCH_TIER_CONFIGS.t2.uplinkCount); - t2s.push(sw); - registry[sw.id] = sw; - switchIds.push(sw.id); - } - - const t1s: NetworkSwitch[] = []; - for (let i = 0; i < t1Count; i++) { - const sw = createSwitch('t1', dcId, null, null); - wireUplinks(sw, t2s, SWITCH_TIER_CONFIGS.t1.uplinkCount); - t1s.push(sw); - registry[sw.id] = sw; - switchIds.push(sw.id); - } - - for (let i = 0; i < computeRackCount; i++) { - const sw = createSwitch('tor', dcId, null, null); - const primary = t1s[Math.floor(i / SWITCH_TIER_CONFIGS.t1.fanOut)]; - const altIdx = (Math.floor(i / SWITCH_TIER_CONFIGS.t1.fanOut) + 1) % t1s.length; - const alt = t1s[altIdx]; - if (t1s.length >= 2 && primary !== alt) { - wireUplinks(sw, [primary, alt], 2); - } else { - wireUplinks(sw, [primary], 2); - } - registry[sw.id] = sw; - switchIds.push(sw.id); - } - - const networkRackCount = estimateNetworkSlots(computeRackCount, dcTier); - return buildDCSummary(switchIds, networkRackCount, registry); + const t3Count = T3_COUNT_PER_DC_TIER[dcTier]; + return { tor: computeRackCount, t1: t1Count, t2: t2Count, t3: t3Count }; } -export function expandDCTopology( - existing: DCNetworkSummary, - newRackCount: number, +export function buildDCNetworkSummary( + computeRackCount: number, dcTier: DCTier, - dcId: string, - registry: Record, ): DCNetworkSummary { - if (newRackCount <= 0) return existing; - - const currentTorCount = existing.totalByTier?.tor ?? 0; - const targetTorCount = currentTorCount + newRackCount; - - const t1s = existing.switchIds.map(id => registry[id]).filter((s): s is NetworkSwitch => !!s && s.tier === 't1'); - const t2s = existing.switchIds.map(id => registry[id]).filter((s): s is NetworkSwitch => !!s && s.tier === 't2'); - const t3s = existing.switchIds.map(id => registry[id]).filter((s): s is NetworkSwitch => !!s && s.tier === 't3'); - - const newIds = [...existing.switchIds]; - - const neededT1 = Math.ceil(targetTorCount / SWITCH_TIER_CONFIGS.t1.fanOut); - const neededT2 = Math.ceil(neededT1 / SWITCH_TIER_CONFIGS.t2.fanOut); - - while (t2s.length < neededT2) { - const sw = createSwitch('t2', dcId, null, null); - wireUplinks(sw, t3s, SWITCH_TIER_CONFIGS.t2.uplinkCount); - t2s.push(sw); - registry[sw.id] = sw; - newIds.push(sw.id); - } - - while (t1s.length < neededT1) { - const sw = createSwitch('t1', dcId, null, null); - wireUplinks(sw, t2s, SWITCH_TIER_CONFIGS.t1.uplinkCount); - t1s.push(sw); - registry[sw.id] = sw; - newIds.push(sw.id); - } - - for (let i = 0; i < newRackCount; i++) { - const torIdx = currentTorCount + i; - const sw = createSwitch('tor', dcId, null, null); - const primary = t1s[Math.floor(torIdx / SWITCH_TIER_CONFIGS.t1.fanOut)]; - const altIdx = (Math.floor(torIdx / SWITCH_TIER_CONFIGS.t1.fanOut) + 1) % t1s.length; - const alt = t1s[altIdx]; - if (t1s.length >= 2 && primary !== alt) { - wireUplinks(sw, [primary, alt], 2); - } else { - wireUplinks(sw, [primary], 2); - } - registry[sw.id] = sw; - newIds.push(sw.id); - } - - const networkRackCount = estimateNetworkSlots(targetTorCount, dcTier); - return buildDCSummary(newIds, networkRackCount, registry); -} - -export function shrinkDCTopology( - existing: DCNetworkSummary, - removeCount: number, - dcTier: DCTier, - registry: Record, -): DCNetworkSummary { - if (removeCount <= 0) return existing; - - const torIds = existing.switchIds.filter(id => registry[id]?.tier === 'tor'); - const toRemove = new Set(torIds.slice(-removeCount)); - - for (const torId of toRemove) { - const tor = registry[torId]; - if (!tor) continue; - for (const upId of tor.uplinkIds) { - const parent = registry[upId]; - if (parent) parent.downlinkIds = parent.downlinkIds.filter(id => id !== torId); - } - delete registry[torId]; - } - - const remainingIds = existing.switchIds.filter(id => !toRemove.has(id)); - const remainingTors = remainingIds.filter(id => registry[id]?.tier === 'tor').length; - return buildDCSummary(remainingIds, estimateNetworkSlots(remainingTors, dcTier), registry); -} - -function computeRackBandwidth(tor: NetworkSwitch, registry: Record): number { - if (tor.status !== 'healthy') return 0; - - let minBW = tor.totalUplinks > 0 ? tor.activeUplinks / tor.totalUplinks : 1; - if (minBW === 0) return 0; - - const visited = new Set(); - let current = tor.uplinkIds.filter(id => { - const sw = registry[id]; - return sw && sw.status === 'healthy'; - }); - - while (current.length > 0) { - let tierBW = 1; - const next: string[] = []; - for (const sid of current) { - if (visited.has(sid)) continue; - visited.add(sid); - const sw = registry[sid]; - if (!sw || sw.status !== 'healthy') continue; - const bw = sw.totalUplinks > 0 ? sw.activeUplinks / sw.totalUplinks : 1; - tierBW = Math.min(tierBW, bw); - for (const upId of sw.uplinkIds) { - if (registry[upId]?.status === 'healthy') next.push(upId); - } - } - minBW = Math.min(minBW, tierBW); - if (minBW === 0) return 0; - current = next; - } - - return minBW; -} - -function buildDCSummary( - switchIds: string[], - networkRackCount: number, - registry: Record, -): DCNetworkSummary { - const totalByTier: Partial> = {}; - const healthyByTier: Partial> = {}; - let disconnected = 0; - let degraded = 0; - let bwSum = 0; - let torCount = 0; - - for (const sid of switchIds) { - const sw = registry[sid]; - if (!sw) continue; - totalByTier[sw.tier] = (totalByTier[sw.tier] ?? 0) + 1; - if (sw.status === 'healthy') healthyByTier[sw.tier] = (healthyByTier[sw.tier] ?? 0) + 1; - if (sw.tier === 'tor') { - torCount++; - const bw = computeRackBandwidth(sw, registry); - bwSum += bw; - if (bw === 0) disconnected++; - else if (bw < 1) degraded++; - } - } - - const avgBW = torCount > 0 ? bwSum / torCount : 1; + if (computeRackCount <= 0) return emptyDCNetworkSummary(); + const totalByTier = computeTopologyCounts(computeRackCount, dcTier); + const healthyByTier = { ...totalByTier }; return { - switchIds, networkRackCount, totalByTier, healthyByTier, - racksDisconnected: disconnected, racksDegraded: degraded, - averageBandwidth: avgBW, effectiveFlopsFraction: avgBW, + totalByTier, healthyByTier, + repairBatches: [], + networkRackCount: estimateNetworkSlots(computeRackCount, dcTier), + racksDisconnected: 0, racksDegraded: 0, + averageBandwidth: 1, effectiveFlopsFraction: 1, }; } -// --- Network Tick (failure rolls + repair) --- +export function expandDCNetwork( + existing: DCNetworkSummary, + addedRacks: number, + dcTier: DCTier, +): DCNetworkSummary { + if (addedRacks <= 0) return existing; + const oldTor = existing.totalByTier.tor ?? 0; + const newTor = oldTor + addedRacks; + const newTotal = computeTopologyCounts(newTor, dcTier); + const healthyByTier: Partial> = {}; + for (const tier of DC_TIERS) { + const oldTotal = existing.totalByTier[tier] ?? 0; + const oldHealthy = existing.healthyByTier[tier] ?? 0; + const added = (newTotal[tier] ?? 0) - oldTotal; + healthyByTier[tier] = oldHealthy + Math.max(0, added); + } + const summary: DCNetworkSummary = { + ...existing, + totalByTier: newTotal, + healthyByTier, + networkRackCount: estimateNetworkSlots(newTor, dcTier), + }; + return recomputeBandwidth(summary); +} -function processNetworkTick( - registry: Record, +export function shrinkDCNetwork( + existing: DCNetworkSummary, + removedRacks: number, + dcTier: DCTier, +): DCNetworkSummary { + if (removedRacks <= 0) return existing; + const oldTor = existing.totalByTier.tor ?? 0; + const newTor = Math.max(0, oldTor - removedRacks); + if (newTor === 0) return emptyDCNetworkSummary(); + const newTotal = computeTopologyCounts(newTor, dcTier); + const healthyByTier: Partial> = {}; + for (const tier of DC_TIERS) { + const nt = newTotal[tier] ?? 0; + const oh = existing.healthyByTier[tier] ?? 0; + healthyByTier[tier] = Math.min(oh, nt); + } + const repairBatches = existing.repairBatches.filter(b => { + const nt = newTotal[b.tier] ?? 0; + const nh = healthyByTier[b.tier] ?? 0; + return nh < nt; + }); + const summary: DCNetworkSummary = { + ...existing, + totalByTier: newTotal, + healthyByTier, + repairBatches, + networkRackCount: estimateNetworkSlots(newTor, dcTier), + }; + return recomputeBandwidth(summary); +} + +function computeAggregateBandwidth( + summary: DCNetworkSummary, + redundancyBonus: number, +): number { + let minBW = 1; + for (const tier of DC_TIERS) { + const total = summary.totalByTier[tier] ?? 0; + if (total === 0) continue; + const healthy = summary.healthyByTier[tier] ?? 0; + const tierBW = Math.min(1, (healthy + redundancyBonus) / total); + if (tierBW < minBW) minBW = tierBW; + } + return minBW; +} + +function recomputeBandwidth(summary: DCNetworkSummary, redundancyBonus = 0): DCNetworkSummary { + const avgBW = computeAggregateBandwidth(summary, redundancyBonus); + const torTotal = summary.totalByTier.tor ?? 0; + const torHealthy = summary.healthyByTier.tor ?? 0; + const torFailed = torTotal - torHealthy; + const disconnected = avgBW === 0 ? torTotal : torFailed; + const degraded = avgBW > 0 && avgBW < 1 ? Math.ceil(torTotal * (1 - avgBW)) - disconnected : 0; + return { + ...summary, + averageBandwidth: avgBW, + effectiveFlopsFraction: avgBW, + racksDisconnected: Math.max(0, disconnected), + racksDegraded: Math.max(0, degraded), + }; +} + +function processNetworkForDC( + summary: DCNetworkSummary, networkResearchBonus: number, opsEff: number, repairSpeedBonus: number, hotStandbyTicks: number, redundancyBonus: number, -): { switchRepairCosts: number; notifications: TickNotification[]; dirtyDCs: Set } { +): { summary: DCNetworkSummary; costs: number; notifications: TickNotification[] } { + const torTotal = summary.totalByTier.tor ?? 0; + if (torTotal === 0) return { summary, costs: 0, notifications: [] }; + + let costs = 0; const notifications: TickNotification[] = []; - let switchRepairCosts = 0; - const dirtyDCs = new Set(); + const healthyByTier = { ...summary.healthyByTier }; + let dirty = false; - const healthyByTier: Partial> = {}; - const repairing: NetworkSwitch[] = []; - - for (const sw of Object.values(registry)) { - if (sw.status === 'healthy') { - (healthyByTier[sw.tier] ??= []).push(sw); - } else if (sw.status === 'repairing') { - repairing.push(sw); - } - } - - const tiers: SwitchTier[] = ['tor', 't1', 't2', 't3', 't4', 't5']; - const newlyFailed: NetworkSwitch[] = []; - - for (const tier of tiers) { - const healthy = healthyByTier[tier]; - if (!healthy || healthy.length === 0) continue; + for (const tier of DC_TIERS) { + const healthy = healthyByTier[tier] ?? 0; + if (healthy <= 0) continue; const rate = SWITCH_TIER_CONFIGS[tier].failureRatePerTick * (1 - networkResearchBonus); - const count = binomialSample(healthy.length, rate); - if (count > 0) { - const shuffled = [...healthy].sort(() => Math.random() - 0.5); - for (let i = 0; i < count; i++) { - const sw = shuffled[i]; - const baseRepair = SWITCH_TIER_CONFIGS[tier].repairBaseTicks; - const repairTime = hotStandbyTicks > 0 - ? hotStandbyTicks - : baseRepair * (1 - repairSpeedBonus); - sw.status = 'repairing'; - sw.repairProgress = 0; - sw.repairTotal = repairTime; - newlyFailed.push(sw); - if (sw.dcId) dirtyDCs.add(sw.dcId); - switchRepairCosts += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION; + const failed = binomialSample(healthy, rate); + if (failed > 0) { + healthyByTier[tier] = healthy - failed; + const baseRepair = SWITCH_TIER_CONFIGS[tier].repairBaseTicks; + const repairTime = hotStandbyTicks > 0 + ? hotStandbyTicks + : baseRepair * (1 - repairSpeedBonus); + summary.repairBatches.push({ tier, count: failed, ticksRemaining: repairTime }); + costs += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION * failed; + dirty = true; + + if (tier === 't3') { + notifications.push({ title: 'Core Network Failure', message: `Tier-3 core switch failed — potential DC disconnect!`, type: 'danger' }); + } else if (tier === 't2') { + notifications.push({ title: 'Network Switch Failure', message: `Tier-2 spine switch failed — racks may be degraded.`, type: 'warning' }); } } } - for (const sw of repairing) { - sw.repairProgress += 1 + opsEff * 0.05; - if (sw.repairProgress >= sw.repairTotal) { - sw.status = 'healthy'; - sw.repairProgress = 0; - sw.repairTotal = 0; - if (sw.dcId) dirtyDCs.add(sw.dcId); + const remainingBatches: RepairBatch[] = []; + for (const batch of summary.repairBatches) { + const newTicks = batch.ticksRemaining - (1 + opsEff * 0.05); + if (newTicks <= 0) { + healthyByTier[batch.tier] = Math.min( + summary.totalByTier[batch.tier] ?? 0, + (healthyByTier[batch.tier] ?? 0) + batch.count, + ); + dirty = true; + } else { + remainingBatches.push({ ...batch, ticksRemaining: newTicks }); } } - if (dirtyDCs.size > 0) { - for (const sw of Object.values(registry)) { - if (sw.uplinkIds.length === 0) continue; - if (sw.dcId && !dirtyDCs.has(sw.dcId)) continue; - let active = 0; - for (const upId of sw.uplinkIds) { - if (registry[upId]?.status === 'healthy') active++; - } - sw.activeUplinks = active; - sw.effectiveBandwidth = sw.totalUplinks > 0 ? Math.min(1, (active + redundancyBonus) / sw.totalUplinks) : 1; - } - } + if (!dirty) return { summary: { ...summary, repairBatches: remainingBatches }, costs, notifications }; - for (const sw of newlyFailed) { - if (sw.tier === 't3') { - notifications.push({ title: 'Core Network Failure', message: `Tier-3 core switch failed — potential DC disconnect!`, type: 'danger' }); - } else if (sw.tier === 't4') { - notifications.push({ title: 'Campus Network Failure', message: `Tier-4 campus switch failed — cross-DC degradation!`, type: 'danger' }); - } else if (sw.tier === 't2') { - notifications.push({ title: 'Network Switch Failure', message: `Tier-2 spine switch failed — racks may be degraded.`, type: 'warning' }); - } - } - - return { switchRepairCosts, notifications, dirtyDCs }; + const updated: DCNetworkSummary = { + ...summary, + healthyByTier, + repairBatches: remainingBatches, + }; + return { summary: recomputeBandwidth(updated, redundancyBonus), costs, notifications }; } // --- Interconnect Training Multiplier --- @@ -476,14 +321,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear const hotStandbyTicks = state.research.completedResearch.includes('network-hot-standby') ? 5 : 0; const redundancyBonus = state.research.completedResearch.includes('network-redundancy') ? 1 : 0; - // Mutate registry in-place — infrastructure returns a new state anyway - const registry = state.infrastructure.switchRegistry; - - // Process network failures/repairs globally - const netResult = processNetworkTick(registry, networkResearchBonus, opsEff, repairSpeedBonus, hotStandbyTicks, redundancyBonus); - repairCosts += netResult.switchRepairCosts; - if (netResult.notifications.length > 0) notifications.push(...netResult.notifications); - let totalFlops = 0; let totalTrainingFlops = 0; let totalInferenceFlops = 0; @@ -541,8 +378,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear if (rs.progress >= rs.total) { if (rs.phase === 'decommissioning') { const installTotal = cohortStageTotal('installation', rs.toSkuId, rs.racksRemaining); - // Clear DC topology on retrofit - for (const sid of dc.networkSummary.switchIds) delete registry[sid]; return { ...dc, computeRacksOnline: 0, @@ -636,10 +471,11 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear // Expand topology for newly onlined racks let networkSummary = dc.networkSummary; if (racksJustOnlined > 0) { - if (networkSummary.switchIds.length === 0) { - networkSummary = buildDCTopology(computeRacksOnline, dc.tier, dc.id, registry); + const torTotal = networkSummary.totalByTier.tor ?? 0; + if (torTotal === 0) { + networkSummary = buildDCNetworkSummary(computeRacksOnline, dc.tier); } else { - networkSummary = expandDCTopology(networkSummary, racksJustOnlined, dc.tier, dc.id, registry); + networkSummary = expandDCNetwork(networkSummary, racksJustOnlined, dc.tier); } } @@ -660,18 +496,20 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear stageTotal: cohortStageTotal('repair', dc.rackSkuId, prodFailures), repairCount: 0, }); - networkSummary = shrinkDCTopology(networkSummary, prodFailures, dc.tier, registry); + networkSummary = shrinkDCNetwork(networkSummary, prodFailures, dc.tier); } } repairCosts += dcRepairCosts; - // Recompute DC network summary after failures/repairs (only if this DC's switches changed) - if (netResult.dirtyDCs.has(dc.id) && networkSummary.switchIds.length > 0) { - networkSummary = buildDCSummary( - networkSummary.switchIds, networkSummary.networkRackCount, registry, - ); - } + // Process per-DC network failures and repairs (aggregate model) + const netResult = processNetworkForDC( + networkSummary, networkResearchBonus, opsEff, + repairSpeedBonus, hotStandbyTicks, redundancyBonus, + ); + networkSummary = netResult.summary; + repairCosts += netResult.costs; + if (netResult.notifications.length > 0) notifications.push(...netResult.notifications); // Rackdown: detect recovery (previously disconnected racks now have connectivity) const prevDisconnected = dc.networkSummary.racksDisconnected; @@ -680,7 +518,7 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear if (currDisconnected < prevDisconnected && dc.rackSkuId) { const recovered = prevDisconnected - currDisconnected; computeRacksOnline -= recovered; - networkSummary = shrinkDCTopology(networkSummary, recovered, dc.tier, registry); + networkSummary = shrinkDCNetwork(networkSummary, recovered, dc.tier); updatedCohorts.push({ id: `netrecovery-${dc.id}-${Date.now()}`, count: recovered, skuId: dc.rackSkuId, @@ -688,10 +526,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear stageTotal: cohortStageTotal('testing', dc.rackSkuId, recovered), repairCount: 0, }); - // Recompute summary after shrink - networkSummary = buildDCSummary( - networkSummary.switchIds, networkSummary.networkRackCount, registry, - ); } // Compute DC aggregates @@ -789,8 +623,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear if (totalRacks <= 0) return dc; const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId as RackSkuId]; const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacks)); - // Clear topology on retrofit start - for (const sid of dc.networkSummary.switchIds) delete registry[sid]; return { ...dc, status: 'retrofitting' as const, @@ -827,7 +659,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear return { infrastructure: { clusters, - switchRegistry: registry, totalFlops, totalTrainingFlops, totalInferenceFlops, diff --git a/packages/shared/src/types/infrastructure.ts b/packages/shared/src/types/infrastructure.ts index 1a0be53..74068ae 100644 --- a/packages/shared/src/types/infrastructure.ts +++ b/packages/shared/src/types/infrastructure.ts @@ -91,24 +91,6 @@ export interface DataCenter { // --- Network Topology (6-Tier Clos) --- export type SwitchTier = 'tor' | 't1' | 't2' | 't3' | 't4' | 't5'; -export type SwitchStatus = 'healthy' | 'failed' | 'repairing'; - -export interface NetworkSwitch { - id: string; - tier: SwitchTier; - status: SwitchStatus; - dcId: string | null; - campusId: string | null; - clusterId: string | null; - uplinkIds: string[]; - downlinkIds: string[]; - activeUplinks: number; - totalUplinks: number; - effectiveBandwidth: number; - repairProgress: number; - repairTotal: number; -} - export interface SwitchTierConfig { tier: SwitchTier; name: string; @@ -121,11 +103,17 @@ export interface SwitchTierConfig { powerDrawKW: number; } +export interface RepairBatch { + tier: SwitchTier; + count: number; + ticksRemaining: number; +} + export interface DCNetworkSummary { - switchIds: string[]; - networkRackCount: number; totalByTier: Partial>; healthyByTier: Partial>; + repairBatches: RepairBatch[]; + networkRackCount: number; racksDisconnected: number; racksDegraded: number; averageBandwidth: number; @@ -133,14 +121,12 @@ export interface DCNetworkSummary { } export interface CampusNetworkSummary { - switchIds: string[]; totalT4: number; healthyT4: number; crossDCBandwidth: number; } export interface ClusterNetworkSummary { - switchIds: string[]; totalT5: number; healthyT5: number; crossCampusBandwidth: number; @@ -262,7 +248,6 @@ export interface ClusterCostConfig { export interface InfrastructureState { clusters: Cluster[]; - switchRegistry: Record; totalFlops: number; totalTrainingFlops: number; totalInferenceFlops: number; @@ -276,7 +261,6 @@ export interface InfrastructureState { export const INITIAL_INFRASTRUCTURE: InfrastructureState = { clusters: [], - switchRegistry: {}, totalFlops: 0, totalTrainingFlops: 0, totalInferenceFlops: 0,