Replace per-switch network simulation with aggregate per-DC statistical model
Balance Check / balance-simulation (push) Successful in 48s
Balance Check / multi-run-balance (push) Successful in 1m24s
CI / build-and-push (push) Successful in 43s

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 20:06:40 -04:00
parent 57a81be769
commit 19f652b43a
4 changed files with 181 additions and 365 deletions
+2 -1
View File
@@ -162,7 +162,8 @@ function CohortStageBreakdown({ cohorts }: { cohorts: DeploymentCohort[] }) {
function NetworkHealthIndicator({ dc }: { dc: DataCenter }) { function NetworkHealthIndicator({ dc }: { dc: DataCenter }) {
const ns = dc.networkSummary; 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 hasDisconnected = ns.racksDisconnected > 0;
const hasDegraded = ns.racksDegraded > 0; const hasDegraded = ns.racksDegraded > 0;
@@ -8,10 +8,10 @@ import type { DeepPartial } from './createTestState';
function emptyDCNetwork(): DCNetworkSummary { function emptyDCNetwork(): DCNetworkSummary {
return { return {
switchIds: [],
networkRackCount: 0,
totalByTier: {}, totalByTier: {},
healthyByTier: {}, healthyByTier: {},
repairBatches: [],
networkRackCount: 0,
racksDisconnected: 0, racksDisconnected: 0,
racksDegraded: 0, racksDegraded: 0,
averageBandwidth: 1, averageBandwidth: 1,
@@ -20,11 +20,11 @@ function emptyDCNetwork(): DCNetworkSummary {
} }
function emptyCampusNetwork(): CampusNetworkSummary { function emptyCampusNetwork(): CampusNetworkSummary {
return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 }; return { totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
} }
function emptyClusterNetwork(): ClusterNetworkSummary { function emptyClusterNetwork(): ClusterNetworkSummary {
return { switchIds: [], totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 }; return { totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 };
} }
export function createTestDataCenter(overrides?: DeepPartial<DataCenter>): DataCenter { export function createTestDataCenter(overrides?: DeepPartial<DataCenter>): DataCenter {
@@ -1,8 +1,8 @@
import type { import type {
GameState, InfrastructureState, Cluster, Campus, DataCenter, GameState, InfrastructureState, Cluster, Campus, DataCenter,
DeploymentCohort, PipelineStage, RackSkuId, NetworkSwitch, DeploymentCohort, PipelineStage, RackSkuId,
SwitchTier, DCNetworkSummary, CampusNetworkSummary, ClusterNetworkSummary, SwitchTier, DCNetworkSummary, CampusNetworkSummary, ClusterNetworkSummary,
CampusRetrofitQueue, DCTier, IntraNodeInterconnect, NetworkFabric, RackSkuConfig, RepairBatch, CampusRetrofitQueue, DCTier, IntraNodeInterconnect, NetworkFabric, RackSkuConfig,
} from '@ai-tycoon/shared'; } from '@ai-tycoon/shared';
import { import {
LOCATION_CONFIGS, LOCATION_CONFIGS,
@@ -83,357 +83,202 @@ function binomialSample(n: number, p: number): number {
return base + (Math.random() < frac ? 1 : 0); return base + (Math.random() < frac ? 1 : 0);
} }
// --- Network Topology Construction --- // --- Aggregate Network Model ---
let switchIdCounter = 0; const DC_TIERS: SwitchTier[] = ['tor', 't1', 't2', 't3'];
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;
}
export function emptyDCNetworkSummary(): DCNetworkSummary { export function emptyDCNetworkSummary(): DCNetworkSummary {
return { return {
switchIds: [], networkRackCount: 0,
totalByTier: {}, healthyByTier: {}, totalByTier: {}, healthyByTier: {},
repairBatches: [], networkRackCount: 0,
racksDisconnected: 0, racksDegraded: 0, racksDisconnected: 0, racksDegraded: 0,
averageBandwidth: 1, effectiveFlopsFraction: 1, averageBandwidth: 1, effectiveFlopsFraction: 1,
}; };
} }
export function emptyCampusNetworkSummary(): CampusNetworkSummary { export function emptyCampusNetworkSummary(): CampusNetworkSummary {
return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 }; return { totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
} }
export function emptyClusterNetworkSummary(): ClusterNetworkSummary { 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, computeRackCount: number,
dcTier: DCTier, dcTier: DCTier,
dcId: string, ): Partial<Record<SwitchTier, number>> {
registry: Record<string, NetworkSwitch>, if (computeRackCount <= 0) return {};
): 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);
}
const t1Count = Math.ceil(computeRackCount / SWITCH_TIER_CONFIGS.t1.fanOut); const t1Count = Math.ceil(computeRackCount / SWITCH_TIER_CONFIGS.t1.fanOut);
const t2Count = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t2.fanOut); const t2Count = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t2.fanOut);
const t3Count = T3_COUNT_PER_DC_TIER[dcTier];
const t2s: NetworkSwitch[] = []; return { tor: computeRackCount, t1: t1Count, t2: t2Count, t3: t3Count };
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);
} }
export function expandDCTopology( export function buildDCNetworkSummary(
existing: DCNetworkSummary, computeRackCount: number,
newRackCount: number,
dcTier: DCTier, dcTier: DCTier,
dcId: string,
registry: Record<string, NetworkSwitch>,
): DCNetworkSummary { ): DCNetworkSummary {
if (newRackCount <= 0) return existing; if (computeRackCount <= 0) return emptyDCNetworkSummary();
const totalByTier = computeTopologyCounts(computeRackCount, dcTier);
const currentTorCount = existing.totalByTier?.tor ?? 0; const healthyByTier = { ...totalByTier };
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<string, NetworkSwitch>,
): 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<string, NetworkSwitch>): 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<string>();
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<string, NetworkSwitch>,
): DCNetworkSummary {
const totalByTier: Partial<Record<SwitchTier, number>> = {};
const healthyByTier: Partial<Record<SwitchTier, number>> = {};
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;
return { return {
switchIds, networkRackCount, totalByTier, healthyByTier, totalByTier, healthyByTier,
racksDisconnected: disconnected, racksDegraded: degraded, repairBatches: [],
averageBandwidth: avgBW, effectiveFlopsFraction: avgBW, 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<Record<SwitchTier, number>> = {};
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( export function shrinkDCNetwork(
registry: Record<string, NetworkSwitch>, 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<Record<SwitchTier, number>> = {};
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, networkResearchBonus: number,
opsEff: number, opsEff: number,
repairSpeedBonus: number, repairSpeedBonus: number,
hotStandbyTicks: number, hotStandbyTicks: number,
redundancyBonus: number, redundancyBonus: number,
): { switchRepairCosts: number; notifications: TickNotification[]; dirtyDCs: Set<string> } { ): { 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[] = []; const notifications: TickNotification[] = [];
let switchRepairCosts = 0; const healthyByTier = { ...summary.healthyByTier };
const dirtyDCs = new Set<string>(); let dirty = false;
const healthyByTier: Partial<Record<SwitchTier, NetworkSwitch[]>> = {}; for (const tier of DC_TIERS) {
const repairing: NetworkSwitch[] = []; const healthy = healthyByTier[tier] ?? 0;
if (healthy <= 0) continue;
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;
const rate = SWITCH_TIER_CONFIGS[tier].failureRatePerTick * (1 - networkResearchBonus); const rate = SWITCH_TIER_CONFIGS[tier].failureRatePerTick * (1 - networkResearchBonus);
const count = binomialSample(healthy.length, rate); const failed = binomialSample(healthy, rate);
if (count > 0) { if (failed > 0) {
const shuffled = [...healthy].sort(() => Math.random() - 0.5); healthyByTier[tier] = healthy - failed;
for (let i = 0; i < count; i++) { const baseRepair = SWITCH_TIER_CONFIGS[tier].repairBaseTicks;
const sw = shuffled[i]; const repairTime = hotStandbyTicks > 0
const baseRepair = SWITCH_TIER_CONFIGS[tier].repairBaseTicks; ? hotStandbyTicks
const repairTime = hotStandbyTicks > 0 : baseRepair * (1 - repairSpeedBonus);
? hotStandbyTicks summary.repairBatches.push({ tier, count: failed, ticksRemaining: repairTime });
: baseRepair * (1 - repairSpeedBonus); costs += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION * failed;
sw.status = 'repairing'; dirty = true;
sw.repairProgress = 0;
sw.repairTotal = repairTime; if (tier === 't3') {
newlyFailed.push(sw); notifications.push({ title: 'Core Network Failure', message: `Tier-3 core switch failed — potential DC disconnect!`, type: 'danger' });
if (sw.dcId) dirtyDCs.add(sw.dcId); } else if (tier === 't2') {
switchRepairCosts += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION; notifications.push({ title: 'Network Switch Failure', message: `Tier-2 spine switch failed — racks may be degraded.`, type: 'warning' });
} }
} }
} }
for (const sw of repairing) { const remainingBatches: RepairBatch[] = [];
sw.repairProgress += 1 + opsEff * 0.05; for (const batch of summary.repairBatches) {
if (sw.repairProgress >= sw.repairTotal) { const newTicks = batch.ticksRemaining - (1 + opsEff * 0.05);
sw.status = 'healthy'; if (newTicks <= 0) {
sw.repairProgress = 0; healthyByTier[batch.tier] = Math.min(
sw.repairTotal = 0; summary.totalByTier[batch.tier] ?? 0,
if (sw.dcId) dirtyDCs.add(sw.dcId); (healthyByTier[batch.tier] ?? 0) + batch.count,
);
dirty = true;
} else {
remainingBatches.push({ ...batch, ticksRemaining: newTicks });
} }
} }
if (dirtyDCs.size > 0) { if (!dirty) return { summary: { ...summary, repairBatches: remainingBatches }, costs, notifications };
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;
}
}
for (const sw of newlyFailed) { const updated: DCNetworkSummary = {
if (sw.tier === 't3') { ...summary,
notifications.push({ title: 'Core Network Failure', message: `Tier-3 core switch failed — potential DC disconnect!`, type: 'danger' }); healthyByTier,
} else if (sw.tier === 't4') { repairBatches: remainingBatches,
notifications.push({ title: 'Campus Network Failure', message: `Tier-4 campus switch failed — cross-DC degradation!`, type: 'danger' }); };
} else if (sw.tier === 't2') { return { summary: recomputeBandwidth(updated, redundancyBonus), costs, notifications };
notifications.push({ title: 'Network Switch Failure', message: `Tier-2 spine switch failed — racks may be degraded.`, type: 'warning' });
}
}
return { switchRepairCosts, notifications, dirtyDCs };
} }
// --- Interconnect Training Multiplier --- // --- 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 hotStandbyTicks = state.research.completedResearch.includes('network-hot-standby') ? 5 : 0;
const redundancyBonus = state.research.completedResearch.includes('network-redundancy') ? 1 : 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 totalFlops = 0;
let totalTrainingFlops = 0; let totalTrainingFlops = 0;
let totalInferenceFlops = 0; let totalInferenceFlops = 0;
@@ -541,8 +378,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
if (rs.progress >= rs.total) { if (rs.progress >= rs.total) {
if (rs.phase === 'decommissioning') { if (rs.phase === 'decommissioning') {
const installTotal = cohortStageTotal('installation', rs.toSkuId, rs.racksRemaining); const installTotal = cohortStageTotal('installation', rs.toSkuId, rs.racksRemaining);
// Clear DC topology on retrofit
for (const sid of dc.networkSummary.switchIds) delete registry[sid];
return { return {
...dc, ...dc,
computeRacksOnline: 0, computeRacksOnline: 0,
@@ -636,10 +471,11 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
// Expand topology for newly onlined racks // Expand topology for newly onlined racks
let networkSummary = dc.networkSummary; let networkSummary = dc.networkSummary;
if (racksJustOnlined > 0) { if (racksJustOnlined > 0) {
if (networkSummary.switchIds.length === 0) { const torTotal = networkSummary.totalByTier.tor ?? 0;
networkSummary = buildDCTopology(computeRacksOnline, dc.tier, dc.id, registry); if (torTotal === 0) {
networkSummary = buildDCNetworkSummary(computeRacksOnline, dc.tier);
} else { } 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), stageTotal: cohortStageTotal('repair', dc.rackSkuId, prodFailures),
repairCount: 0, repairCount: 0,
}); });
networkSummary = shrinkDCTopology(networkSummary, prodFailures, dc.tier, registry); networkSummary = shrinkDCNetwork(networkSummary, prodFailures, dc.tier);
} }
} }
repairCosts += dcRepairCosts; repairCosts += dcRepairCosts;
// Recompute DC network summary after failures/repairs (only if this DC's switches changed) // Process per-DC network failures and repairs (aggregate model)
if (netResult.dirtyDCs.has(dc.id) && networkSummary.switchIds.length > 0) { const netResult = processNetworkForDC(
networkSummary = buildDCSummary( networkSummary, networkResearchBonus, opsEff,
networkSummary.switchIds, networkSummary.networkRackCount, registry, 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) // Rackdown: detect recovery (previously disconnected racks now have connectivity)
const prevDisconnected = dc.networkSummary.racksDisconnected; const prevDisconnected = dc.networkSummary.racksDisconnected;
@@ -680,7 +518,7 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
if (currDisconnected < prevDisconnected && dc.rackSkuId) { if (currDisconnected < prevDisconnected && dc.rackSkuId) {
const recovered = prevDisconnected - currDisconnected; const recovered = prevDisconnected - currDisconnected;
computeRacksOnline -= recovered; computeRacksOnline -= recovered;
networkSummary = shrinkDCTopology(networkSummary, recovered, dc.tier, registry); networkSummary = shrinkDCNetwork(networkSummary, recovered, dc.tier);
updatedCohorts.push({ updatedCohorts.push({
id: `netrecovery-${dc.id}-${Date.now()}`, id: `netrecovery-${dc.id}-${Date.now()}`,
count: recovered, skuId: dc.rackSkuId, count: recovered, skuId: dc.rackSkuId,
@@ -688,10 +526,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
stageTotal: cohortStageTotal('testing', dc.rackSkuId, recovered), stageTotal: cohortStageTotal('testing', dc.rackSkuId, recovered),
repairCount: 0, repairCount: 0,
}); });
// Recompute summary after shrink
networkSummary = buildDCSummary(
networkSummary.switchIds, networkSummary.networkRackCount, registry,
);
} }
// Compute DC aggregates // Compute DC aggregates
@@ -789,8 +623,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
if (totalRacks <= 0) return dc; if (totalRacks <= 0) return dc;
const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId as RackSkuId]; const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId as RackSkuId];
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacks)); 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 { return {
...dc, ...dc,
status: 'retrofitting' as const, status: 'retrofitting' as const,
@@ -827,7 +659,6 @@ export function processInfrastructure(state: GameState, researchBonuses?: Resear
return { return {
infrastructure: { infrastructure: {
clusters, clusters,
switchRegistry: registry,
totalFlops, totalFlops,
totalTrainingFlops, totalTrainingFlops,
totalInferenceFlops, totalInferenceFlops,
+8 -24
View File
@@ -91,24 +91,6 @@ export interface DataCenter {
// --- Network Topology (6-Tier Clos) --- // --- Network Topology (6-Tier Clos) ---
export type SwitchTier = 'tor' | 't1' | 't2' | 't3' | 't4' | 't5'; 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 { export interface SwitchTierConfig {
tier: SwitchTier; tier: SwitchTier;
name: string; name: string;
@@ -121,11 +103,17 @@ export interface SwitchTierConfig {
powerDrawKW: number; powerDrawKW: number;
} }
export interface RepairBatch {
tier: SwitchTier;
count: number;
ticksRemaining: number;
}
export interface DCNetworkSummary { export interface DCNetworkSummary {
switchIds: string[];
networkRackCount: number;
totalByTier: Partial<Record<SwitchTier, number>>; totalByTier: Partial<Record<SwitchTier, number>>;
healthyByTier: Partial<Record<SwitchTier, number>>; healthyByTier: Partial<Record<SwitchTier, number>>;
repairBatches: RepairBatch[];
networkRackCount: number;
racksDisconnected: number; racksDisconnected: number;
racksDegraded: number; racksDegraded: number;
averageBandwidth: number; averageBandwidth: number;
@@ -133,14 +121,12 @@ export interface DCNetworkSummary {
} }
export interface CampusNetworkSummary { export interface CampusNetworkSummary {
switchIds: string[];
totalT4: number; totalT4: number;
healthyT4: number; healthyT4: number;
crossDCBandwidth: number; crossDCBandwidth: number;
} }
export interface ClusterNetworkSummary { export interface ClusterNetworkSummary {
switchIds: string[];
totalT5: number; totalT5: number;
healthyT5: number; healthyT5: number;
crossCampusBandwidth: number; crossCampusBandwidth: number;
@@ -262,7 +248,6 @@ export interface ClusterCostConfig {
export interface InfrastructureState { export interface InfrastructureState {
clusters: Cluster[]; clusters: Cluster[];
switchRegistry: Record<string, NetworkSwitch>;
totalFlops: number; totalFlops: number;
totalTrainingFlops: number; totalTrainingFlops: number;
totalInferenceFlops: number; totalInferenceFlops: number;
@@ -276,7 +261,6 @@ export interface InfrastructureState {
export const INITIAL_INFRASTRUCTURE: InfrastructureState = { export const INITIAL_INFRASTRUCTURE: InfrastructureState = {
clusters: [], clusters: [],
switchRegistry: {},
totalFlops: 0, totalFlops: 0,
totalTrainingFlops: 0, totalTrainingFlops: 0,
totalInferenceFlops: 0, totalInferenceFlops: 0,