Rework network to 6-tier Clos topology with individual switch entities
CI / build-and-push (push) Successful in 31s
CI / build-and-push (push) Successful in 31s
Replace aggregate network health stats with a full 6-tier Clos topology (ToR → T1 → T2 → T3 → T4 → T5) where every switch is an individually tracked entity with uplinks, repair pipelines, and failure cascades. Key mechanics: - Bottleneck bandwidth model (min along path) affects FLOPS and satisfaction - Rackdown on full disconnect → racks re-enter testing pipeline on recovery - Binomial failure sampling per tier, dirty-flag cascade optimization - Flat switch registry for performance at scale - Three new research nodes: network-redundancy, fast-repair, hot-standby Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
||||
formatMoney, formatNumber, formatPercent,
|
||||
LOCATION_CONFIGS, DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
|
||||
CAMPUS_TIER_COSTS, CLUSTER_COST_CONFIG, FIRST_CAMPUS_BUILD_TICKS,
|
||||
networkSlotsRequired, maxComputeRacks,
|
||||
estimateNetworkSlots, maxComputeRacks,
|
||||
SWITCH_TIER_CONFIGS,
|
||||
DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type {
|
||||
@@ -26,12 +27,14 @@ const ERA_ORDER: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
|
||||
const STAGE_LABELS: Record<PipelineStage, string> = {
|
||||
ordered: 'Ordered', manufacturing: 'Mfg', receiving: 'Recv',
|
||||
installation: 'Install', testing: 'Testing', repair: 'Repair', decommission: 'Decom',
|
||||
installation: 'Install', testing: 'Testing', repair: 'Repair',
|
||||
'network-down': 'Net Down', decommission: 'Decom',
|
||||
};
|
||||
|
||||
const STAGE_COLORS: Record<PipelineStage, string> = {
|
||||
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',
|
||||
installation: 'bg-violet-500', testing: 'bg-amber-500', repair: 'bg-danger',
|
||||
'network-down': 'bg-red-600', decommission: 'bg-surface-500',
|
||||
};
|
||||
|
||||
// ─── Shared Components ──────────────────────────────────────────
|
||||
@@ -112,7 +115,7 @@ function Breadcrumb({ nav }: { nav: InfraNav }) {
|
||||
|
||||
function DeploymentProgressBar({ dc }: { dc: DataCenter }) {
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineRacks = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((s, c) => s + c.count, 0);
|
||||
const totalTarget = dc.computeRacksOnline + pipelineRacks;
|
||||
const pct = totalTarget > 0 ? (dc.computeRacksOnline / totalTarget) * 100 : 0;
|
||||
@@ -157,23 +160,27 @@ function CohortStageBreakdown({ cohorts }: { cohorts: DeploymentCohort[] }) {
|
||||
}
|
||||
|
||||
function NetworkHealthIndicator({ dc }: { dc: DataCenter }) {
|
||||
const nh = dc.networkHealth;
|
||||
if (nh.tier1Required === 0) return null;
|
||||
const ns = dc.networkSummary;
|
||||
if (ns.switchIds.length === 0) return null;
|
||||
|
||||
const allHealthy = nh.tier1Healthy === nh.tier1Required
|
||||
&& nh.tier2Healthy === nh.tier2Required
|
||||
&& nh.tier3Healthy === nh.tier3Required;
|
||||
const hasDisconnected = ns.racksDisconnected > 0;
|
||||
const hasDegraded = ns.racksDegraded > 0;
|
||||
const coreDown = (ns.healthyByTier?.t3 ?? 0) < (ns.totalByTier?.t3 ?? 0);
|
||||
|
||||
const color = nh.tier3Healthy < nh.tier3Required ? 'text-danger'
|
||||
: !allHealthy ? 'text-amber-400'
|
||||
const color = coreDown ? 'text-danger'
|
||||
: hasDisconnected ? 'text-danger'
|
||||
: hasDegraded ? 'text-amber-400'
|
||||
: 'text-green-400';
|
||||
|
||||
const bwPct = Math.round(ns.averageBandwidth * 100);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 text-xs ${color}`}>
|
||||
<Network size={12} />
|
||||
<span>
|
||||
{nh.tier3Healthy < nh.tier3Required ? 'Core Down'
|
||||
: !allHealthy ? `${nh.racksDisconnected} disconnected`
|
||||
{coreDown ? 'Core Down'
|
||||
: hasDisconnected ? `${ns.racksDisconnected} disconnected`
|
||||
: hasDegraded ? `${bwPct}% bandwidth`
|
||||
: 'Healthy'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -661,7 +668,7 @@ function FillAllDCsModal({ campus, money, era, research, onConfirm, onClose }: {
|
||||
const { qty, cost } = computeFillForDC(dc, selectedSku, remaining);
|
||||
if (qty === 0) {
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const isFull = maxCompute - (dc.computeRacksOnline + pipelineCount) <= 0;
|
||||
return { dc, qty: 0, cost: 0, reason: isFull ? 'Already full' : 'No budget' };
|
||||
@@ -929,7 +936,7 @@ function CampusDetailView({ clusterId, campusId }: { clusterId: string; campusId
|
||||
const hasRetrofitQueue = !!campus.retrofitQueue;
|
||||
|
||||
const fillableDCs = operationalDCs.filter(dc => {
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
return maxCompute - (dc.computeRacksOnline + pipelineCount) > 0;
|
||||
});
|
||||
@@ -1124,12 +1131,12 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
|
||||
if (!dc || !cluster) return <div className="text-surface-400">Data center not found.</div>;
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((s, c) => s + c.count, 0);
|
||||
const existingCompute = dc.computeRacksOnline + pipelineCount;
|
||||
const availableSlots = maxCompute - existingCompute;
|
||||
const sku = dc.rackSkuId ? RACK_SKU_CONFIGS[dc.rackSkuId] : null;
|
||||
const netSlots = networkSlotsRequired(existingCompute);
|
||||
const netSlots = estimateNetworkSlots(existingCompute, dc.tier);
|
||||
|
||||
const availableSkus = Object.values(RACK_SKU_CONFIGS).filter(s => {
|
||||
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(s.era)) return false;
|
||||
@@ -1257,7 +1264,7 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
|
||||
{(() => {
|
||||
const skuToUse = dc.rackSkuId ?? selectedSku!;
|
||||
const skuConfig = RACK_SKU_CONFIGS[skuToUse];
|
||||
const newNetSlots = networkSlotsRequired(existingCompute + deployQty);
|
||||
const newNetSlots = estimateNetworkSlots(existingCompute + deployQty, dc.tier);
|
||||
const addedNet = newNetSlots - netSlots;
|
||||
const totalCost = skuConfig.baseCost * deployQty;
|
||||
return (
|
||||
@@ -1358,24 +1365,51 @@ function DataCenterDetailView({ clusterId, campusId, datacenterId }: {
|
||||
<p className="text-sm text-surface-400">No racks online. Deploy racks to see network topology.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ 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 => (
|
||||
<div key={tier.label} className="flex items-center justify-between p-3 border border-surface-600 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{tier.label}</div>
|
||||
<div className="text-xs text-surface-400">{tier.desc}</div>
|
||||
</div>
|
||||
<div className={`text-sm font-mono ${tier.healthy < tier.required ? 'text-danger' : 'text-green-400'}`}>
|
||||
{tier.healthy} / {tier.required}
|
||||
</div>
|
||||
{/* Bandwidth gauge */}
|
||||
<div className="p-3 border border-surface-600 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium">Bandwidth</span>
|
||||
<span className={`text-sm font-mono ${dc.networkSummary.averageBandwidth < 0.8 ? 'text-warning' : dc.networkSummary.averageBandwidth < 0.5 ? 'text-danger' : 'text-green-400'}`}>
|
||||
{formatPercent(dc.networkSummary.averageBandwidth)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{dc.networkHealth.racksDisconnected > 0 && (
|
||||
<div className="w-full bg-surface-900 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${dc.networkSummary.averageBandwidth >= 0.8 ? 'bg-green-500' : dc.networkSummary.averageBandwidth >= 0.5 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${dc.networkSummary.averageBandwidth * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1 text-xs text-surface-500">
|
||||
<span>Effective FLOPS: {formatPercent(dc.networkSummary.effectiveFlopsFraction)}</span>
|
||||
<span>{dc.networkSummary.racksDegraded} degraded</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Per-tier switch health */}
|
||||
{(['tor', 't1', 't2', 't3'] as const).map(tier => {
|
||||
const total = dc.networkSummary.totalByTier[tier] ?? 0;
|
||||
if (total === 0) return null;
|
||||
const healthy = dc.networkSummary.healthyByTier[tier] ?? 0;
|
||||
const failed = total - healthy;
|
||||
const config = SWITCH_TIER_CONFIGS[tier];
|
||||
return (
|
||||
<div key={tier} className="flex items-center justify-between p-3 border border-surface-600 rounded-lg">
|
||||
<div>
|
||||
<div className="font-medium text-sm">{config.name}</div>
|
||||
<div className="text-xs text-surface-400">
|
||||
{tier === 'tor' ? '1 per rack (embedded)' : `Fan-out ${config.fanOut}, ${config.uplinkCount} uplinks`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-sm font-mono ${failed > 0 ? 'text-danger' : 'text-green-400'}`}>
|
||||
{healthy} / {total}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{dc.networkSummary.racksDisconnected > 0 && (
|
||||
<div className="text-sm text-danger flex items-center gap-2 p-2">
|
||||
<Activity size={14} /> {dc.networkHealth.racksDisconnected} compute racks disconnected due to network failures
|
||||
<Activity size={14} /> {dc.networkSummary.racksDisconnected} compute racks disconnected due to network failures
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+21
-22
@@ -9,7 +9,7 @@ import type {
|
||||
Cluster, Campus, DataCenter, DCTier, RackSkuId, TrainingJob,
|
||||
ActiveResearch, OwnedDataset, LocationId,
|
||||
DeploymentCohort, PipelineStage,
|
||||
NetworkHealthState, CampusRetrofitQueue,
|
||||
CampusRetrofitQueue,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
|
||||
import {
|
||||
@@ -25,9 +25,12 @@ import {
|
||||
FUNDING_ROUNDS,
|
||||
OPEN_SOURCE_REPUTATION_BOOST,
|
||||
LOCATION_CONFIGS,
|
||||
networkSlotsRequired, maxComputeRacks,
|
||||
estimateNetworkSlots, maxComputeRacks,
|
||||
uuid,
|
||||
} from '@ai-tycoon/shared';
|
||||
import {
|
||||
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
|
||||
} from '@ai-tycoon/game-engine';
|
||||
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
|
||||
|
||||
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
|
||||
@@ -57,8 +60,14 @@ export interface GameNotification {
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
function emptyNetworkHealth(): NetworkHealthState {
|
||||
return { tier1Required: 0, tier1Healthy: 0, tier2Required: 0, tier2Healthy: 0, tier3Required: 0, tier3Healthy: 0, racksDisconnected: 0 };
|
||||
function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks' | 'usedSlots' | 'usedPowerKW' | 'energyCostPerTick' | 'maintenanceCostPerTick' | 'currentUptime'> {
|
||||
return {
|
||||
networkSummary: emptyDCNetworkSummary(),
|
||||
effectiveComputeRacks: 0,
|
||||
usedSlots: 0, usedPowerKW: 0,
|
||||
energyCostPerTick: 0, maintenanceCostPerTick: 0,
|
||||
currentUptime: 1,
|
||||
};
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
@@ -189,7 +198,7 @@ export function computeFillForDC(
|
||||
|
||||
const sku = RACK_SKU_CONFIGS[skuId];
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const existingCompute = dc.computeRacksOnline + pipelineCount;
|
||||
const available = maxCompute - existingCompute;
|
||||
@@ -319,6 +328,7 @@ export const useGameStore = create<Store>()(
|
||||
status: isFirst ? 'operational' : 'constructing',
|
||||
constructionProgress: isFirst ? 0 : 0,
|
||||
constructionTotal: isFirst ? 0 : CLUSTER_COST_CONFIG.buildTimeTicks,
|
||||
networkSummary: emptyClusterNetworkSummary(),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -358,6 +368,7 @@ export const useGameStore = create<Store>()(
|
||||
constructionProgress: 0,
|
||||
constructionTotal: buildTime,
|
||||
retrofitQueue: null,
|
||||
networkSummary: emptyCampusNetworkSummary(),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -398,17 +409,11 @@ export const useGameStore = create<Store>()(
|
||||
rackSkuId: null,
|
||||
computeRacksOnline: 0,
|
||||
computeRacksFailed: 0,
|
||||
networkHealth: emptyNetworkHealth(),
|
||||
...emptyDC(),
|
||||
deploymentCohorts: [],
|
||||
retrofitState: null,
|
||||
coolingLevel: 0,
|
||||
redundancyLevel: 0,
|
||||
effectiveComputeRacks: 0,
|
||||
usedSlots: 0,
|
||||
usedPowerKW: 0,
|
||||
energyCostPerTick: 0,
|
||||
maintenanceCostPerTick: 0,
|
||||
currentUptime: 1,
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -437,14 +442,14 @@ export const useGameStore = create<Store>()(
|
||||
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
|
||||
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const existingCompute = dc.computeRacksOnline + pipelineCount;
|
||||
const available = maxCompute - existingCompute;
|
||||
const actualQty = Math.min(quantity, available);
|
||||
if (actualQty <= 0) return s;
|
||||
|
||||
const totalNetSlots = networkSlotsRequired(existingCompute + actualQty);
|
||||
const totalNetSlots = estimateNetworkSlots(existingCompute + actualQty, dc.tier);
|
||||
const totalSlotsNeeded = existingCompute + actualQty + totalNetSlots;
|
||||
if (totalSlotsNeeded > tierConfig.rackSlots) return s;
|
||||
|
||||
@@ -484,7 +489,7 @@ export const useGameStore = create<Store>()(
|
||||
|
||||
const dc = found.dc;
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
|
||||
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const existingCompute = dc.computeRacksOnline + pipelineCount;
|
||||
const available = maxCompute - existingCompute;
|
||||
@@ -522,17 +527,11 @@ export const useGameStore = create<Store>()(
|
||||
rackSkuId: null,
|
||||
computeRacksOnline: 0,
|
||||
computeRacksFailed: 0,
|
||||
networkHealth: emptyNetworkHealth(),
|
||||
...emptyDC(),
|
||||
deploymentCohorts: [],
|
||||
retrofitState: null,
|
||||
coolingLevel: 0,
|
||||
redundancyLevel: 0,
|
||||
effectiveComputeRacks: 0,
|
||||
usedSlots: 0,
|
||||
usedPowerKW: 0,
|
||||
energyCostPerTick: 0,
|
||||
maintenanceCostPerTick: 0,
|
||||
currentUptime: 1,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,36 @@ export const TECH_TREE: ResearchNode[] = [
|
||||
cost: { researchPoints: 4, compute: 80, ticks: 360 },
|
||||
effects: [{ type: 'cost_reduction', target: 'network_failure_rate', value: 0.5 }],
|
||||
},
|
||||
{
|
||||
id: 'network-redundancy',
|
||||
name: 'Network Redundancy',
|
||||
description: 'Additional uplink per switch at all tiers — reduces disconnect risk from single switch failures.',
|
||||
era: 'scaleup',
|
||||
category: 'infrastructure',
|
||||
prerequisites: ['network-engineering-i'],
|
||||
cost: { researchPoints: 3, compute: 40, ticks: 240 },
|
||||
effects: [{ type: 'efficiency_boost', target: 'network_uplinks', value: 1 }],
|
||||
},
|
||||
{
|
||||
id: 'network-fast-repair',
|
||||
name: 'Fast Network Repair',
|
||||
description: 'Hot-swap switch modules and pre-staged spares reduce network repair time by 40%.',
|
||||
era: 'bigtech',
|
||||
category: 'infrastructure',
|
||||
prerequisites: ['network-engineering-ii'],
|
||||
cost: { researchPoints: 5, compute: 100, ticks: 400 },
|
||||
effects: [{ type: 'efficiency_boost', target: 'network_repair_speed', value: 0.4 }],
|
||||
},
|
||||
{
|
||||
id: 'network-hot-standby',
|
||||
name: 'Hot Standby Switches',
|
||||
description: 'Automated failover with standby switches — failed switches auto-replace in 5 ticks.',
|
||||
era: 'agi',
|
||||
category: 'infrastructure',
|
||||
prerequisites: ['network-fast-repair'],
|
||||
cost: { researchPoints: 8, compute: 250, ticks: 600 },
|
||||
effects: [{ type: 'efficiency_boost', target: 'network_hot_standby', value: 5 }],
|
||||
},
|
||||
{
|
||||
id: 'rapid-deployment',
|
||||
name: 'Rapid Deployment',
|
||||
|
||||
@@ -2,6 +2,7 @@ export { GameEngine } from './engine';
|
||||
export { processTick, setAchievementDefinitions } from './tick';
|
||||
export type { TickNotification } from './tick';
|
||||
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
|
||||
export { emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary } from './systems/infrastructureSystem';
|
||||
export { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem';
|
||||
export { TECH_TREE } from './data/techTree';
|
||||
export { INITIAL_RIVALS } from './data/competitors';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type {
|
||||
GameState, InfrastructureState, Cluster, Campus, DataCenter,
|
||||
DeploymentCohort, NetworkHealthState, PipelineStage, RackSkuId,
|
||||
CampusRetrofitQueue,
|
||||
DeploymentCohort, PipelineStage, RackSkuId, NetworkSwitch,
|
||||
SwitchTier, DCNetworkSummary, CampusNetworkSummary, ClusterNetworkSummary,
|
||||
CampusRetrofitQueue, DCTier,
|
||||
} from '@ai-tycoon/shared';
|
||||
import {
|
||||
LOCATION_CONFIGS,
|
||||
@@ -12,10 +13,13 @@ import {
|
||||
COOLING_FAILURE_REDUCTION,
|
||||
REDUNDANCY_FAILURE_REDUCTION,
|
||||
RACK_REPAIR_BASE_TICKS,
|
||||
NETWORK_TOPOLOGY,
|
||||
COHORT_SCALE_FACTOR,
|
||||
PIPELINE_ORDER_BASE_TICKS,
|
||||
networkSlotsRequired,
|
||||
SWITCH_TIER_CONFIGS,
|
||||
T3_COUNT_PER_DC_TIER,
|
||||
SWITCH_REPAIR_COST_FRACTION,
|
||||
NETWORK_DEGRADATION,
|
||||
estimateNetworkSlots,
|
||||
} from '@ai-tycoon/shared';
|
||||
import type { TickNotification } from '../tick';
|
||||
|
||||
@@ -25,6 +29,8 @@ export interface InfraTickResult {
|
||||
repairCosts: number;
|
||||
}
|
||||
|
||||
// --- Pipeline helpers ---
|
||||
|
||||
const PIPELINE_ADVANCE_ORDER: PipelineStage[] = [
|
||||
'ordered', 'manufacturing', 'receiving', 'installation', 'testing',
|
||||
];
|
||||
@@ -47,6 +53,7 @@ function cohortStageTotal(stage: PipelineStage, skuId: string, count: number): n
|
||||
case 'testing': base = timings.testing; break;
|
||||
case 'repair': base = RACK_REPAIR_BASE_TICKS; break;
|
||||
case 'decommission': base = timings.installation; break;
|
||||
case 'network-down': base = 0; break;
|
||||
default: base = 0;
|
||||
}
|
||||
return Math.ceil(base * (1 + COHORT_SCALE_FACTOR * count));
|
||||
@@ -59,6 +66,7 @@ function stageSpeed(stage: PipelineStage, engEff: number, opsEff: number): numbe
|
||||
case 'testing':
|
||||
case 'decommission': return 1 + opsEff * 0.1;
|
||||
case 'repair': return 1 + opsEff * 0.05;
|
||||
case 'network-down': return 0;
|
||||
default: return 1;
|
||||
}
|
||||
}
|
||||
@@ -72,66 +80,363 @@ function binomialSample(n: number, p: number): number {
|
||||
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;
|
||||
// --- Network Topology Construction ---
|
||||
|
||||
let switchIdCounter = 0;
|
||||
|
||||
function createSwitch(
|
||||
tier: SwitchTier,
|
||||
dcId: string | null,
|
||||
campusId: string | null,
|
||||
clusterId: string | null,
|
||||
): NetworkSwitch {
|
||||
const config = SWITCH_TIER_CONFIGS[tier];
|
||||
return {
|
||||
tier1Required: tier1,
|
||||
tier1Healthy: tier1,
|
||||
tier2Required: tier2,
|
||||
tier2Healthy: tier2,
|
||||
tier3Required: tier3,
|
||||
tier3Healthy: tier3,
|
||||
racksDisconnected: 0,
|
||||
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 processNetworkFailures(
|
||||
nh: NetworkHealthState,
|
||||
computeRacksOnline: number,
|
||||
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 {
|
||||
return {
|
||||
switchIds: [], networkRackCount: 0,
|
||||
totalByTier: {}, healthyByTier: {},
|
||||
racksDisconnected: 0, racksDegraded: 0,
|
||||
averageBandwidth: 1, effectiveFlopsFraction: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function emptyCampusNetworkSummary(): CampusNetworkSummary {
|
||||
return { switchIds: [], totalT4: 0, healthyT4: 0, crossDCBandwidth: 1 };
|
||||
}
|
||||
|
||||
export function emptyClusterNetworkSummary(): ClusterNetworkSummary {
|
||||
return { switchIds: [], totalT5: 0, healthyT5: 0, crossCampusBandwidth: 1 };
|
||||
}
|
||||
|
||||
export function buildDCTopology(
|
||||
computeRackCount: number,
|
||||
dcTier: DCTier,
|
||||
dcId: string,
|
||||
registry: Record<string, NetworkSwitch>,
|
||||
): 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 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);
|
||||
}
|
||||
|
||||
export function expandDCTopology(
|
||||
existing: DCNetworkSummary,
|
||||
newRackCount: number,
|
||||
dcTier: DCTier,
|
||||
dcId: string,
|
||||
registry: Record<string, NetworkSwitch>,
|
||||
): 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<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 {
|
||||
switchIds, networkRackCount, totalByTier, healthyByTier,
|
||||
racksDisconnected: disconnected, racksDegraded: degraded,
|
||||
averageBandwidth: avgBW, effectiveFlopsFraction: avgBW,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Network Tick (failure rolls + repair) ---
|
||||
|
||||
function processNetworkTick(
|
||||
registry: Record<string, NetworkSwitch>,
|
||||
networkResearchBonus: number,
|
||||
): { networkHealth: NetworkHealthState; racksDisconnected: number } {
|
||||
if (computeRacksOnline <= 0) {
|
||||
return { networkHealth: nh, racksDisconnected: 0 };
|
||||
opsEff: number,
|
||||
repairSpeedBonus: number,
|
||||
hotStandbyTicks: number,
|
||||
redundancyBonus: number,
|
||||
): { switchRepairCosts: number; notifications: TickNotification[]; dirty: boolean } {
|
||||
const notifications: TickNotification[] = [];
|
||||
let switchRepairCosts = 0;
|
||||
let dirty = false;
|
||||
|
||||
const healthyByTier: Partial<Record<SwitchTier, NetworkSwitch[]>> = {};
|
||||
const repairing: NetworkSwitch[] = [];
|
||||
const failed: 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);
|
||||
} else if (sw.status === 'failed') {
|
||||
failed.push(sw);
|
||||
}
|
||||
}
|
||||
|
||||
let racksDisconnected = 0;
|
||||
const tiers: SwitchTier[] = ['tor', 't1', 't2', 't3', 't4', 't5'];
|
||||
const newlyFailed: NetworkSwitch[] = [];
|
||||
|
||||
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;
|
||||
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 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);
|
||||
switchRepairCosts += SWITCH_TIER_CONFIGS[tier].baseCost * SWITCH_REPAIR_COST_FRACTION;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
racksDisconnected = Math.min(racksDisconnected, computeRacksOnline);
|
||||
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;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
networkHealth: {
|
||||
...nh,
|
||||
tier1Healthy,
|
||||
tier2Healthy,
|
||||
tier3Healthy,
|
||||
racksDisconnected,
|
||||
},
|
||||
racksDisconnected,
|
||||
};
|
||||
if (dirty) {
|
||||
for (const sw of Object.values(registry)) {
|
||||
if (sw.uplinkIds.length === 0) 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) {
|
||||
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, dirty };
|
||||
}
|
||||
|
||||
// --- Main Infrastructure Tick ---
|
||||
|
||||
export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
const notifications: TickNotification[] = [];
|
||||
let repairCosts = 0;
|
||||
@@ -142,6 +447,20 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
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);
|
||||
const repairSpeedBonus = state.research.completedResearch.includes('network-fast-repair') ? 0.4 : 0;
|
||||
const hotStandbyTicks = state.research.completedResearch.includes('network-hot-standby') ? 5 : 0;
|
||||
const redundancyBonus = state.research.completedResearch.includes('network-redundancy') ? 1 : 0;
|
||||
|
||||
// Clone switch registry for mutable operations this tick
|
||||
const registry: Record<string, NetworkSwitch> = {};
|
||||
for (const [id, sw] of Object.entries(state.infrastructure.switchRegistry)) {
|
||||
registry[id] = { ...sw, uplinkIds: [...sw.uplinkIds], downlinkIds: [...sw.downlinkIds] };
|
||||
}
|
||||
|
||||
// Process network failures/repairs globally
|
||||
const netResult = processNetworkTick(registry, networkResearchBonus, opsEff, repairSpeedBonus, hotStandbyTicks, redundancyBonus);
|
||||
repairCosts += netResult.switchRepairCosts;
|
||||
notifications.push(...netResult.notifications);
|
||||
|
||||
let totalFlops = 0;
|
||||
let totalUptime = 0;
|
||||
@@ -149,9 +468,10 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
let totalComputeRackCount = 0;
|
||||
let totalDataCenterCount = 0;
|
||||
let dcWithRacks = 0;
|
||||
let globalLatencyPenalty = 0;
|
||||
let latencyDCCount = 0;
|
||||
|
||||
const clusters: Cluster[] = state.infrastructure.clusters.map(cluster => {
|
||||
// Advance cluster construction
|
||||
if (cluster.status === 'constructing') {
|
||||
const newProgress = cluster.constructionProgress + 1;
|
||||
if (newProgress >= cluster.constructionTotal) {
|
||||
@@ -160,36 +480,26 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
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: cluster.constructionTotal, status: 'operational' as const };
|
||||
}
|
||||
return { ...cluster, constructionProgress: newProgress };
|
||||
}
|
||||
|
||||
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 };
|
||||
notifications.push({ title: 'Campus Ready', message: `Campus ${campus.name} is now operational!`, type: 'success' });
|
||||
return { ...campus, constructionProgress: campus.constructionTotal, status: 'operational' as const };
|
||||
}
|
||||
return { ...campus, constructionProgress: newProgress };
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
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 };
|
||||
@@ -205,8 +515,9 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
|
||||
if (rs.progress >= rs.total) {
|
||||
if (rs.phase === 'decommissioning') {
|
||||
const installSku = RACK_SKU_CONFIGS[rs.toSkuId];
|
||||
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,
|
||||
@@ -221,31 +532,16 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
stageTotal: installTotal,
|
||||
repairCount: 0,
|
||||
}],
|
||||
retrofitState: {
|
||||
...rs,
|
||||
phase: 'installing' as const,
|
||||
progress: 0,
|
||||
total: installTotal,
|
||||
},
|
||||
networkHealth: computeNetworkHealth(0),
|
||||
retrofitState: { ...rs, phase: 'installing' as const, progress: 0, total: installTotal },
|
||||
networkSummary: emptyDCNetworkSummary(),
|
||||
effectiveComputeRacks: 0,
|
||||
usedSlots: 0,
|
||||
usedPowerKW: 0,
|
||||
currentUptime: 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,
|
||||
};
|
||||
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 };
|
||||
@@ -254,9 +550,14 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
// Process deployment cohorts
|
||||
const updatedCohorts: DeploymentCohort[] = [];
|
||||
let racksJustOnlined = 0;
|
||||
let racksFailedTesting = 0;
|
||||
|
||||
for (const cohort of dc.deploymentCohorts) {
|
||||
// network-down cohorts don't progress via speed — handled separately below
|
||||
if (cohort.stage === 'network-down') {
|
||||
updatedCohorts.push(cohort);
|
||||
continue;
|
||||
}
|
||||
|
||||
const speed = stageSpeed(cohort.stage, engEff, opsEff);
|
||||
const newProgress = cohort.stageProgress + speed;
|
||||
|
||||
@@ -265,18 +566,11 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cohort.stage === 'decommission') {
|
||||
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,
|
||||
});
|
||||
updatedCohorts.push({ ...cohort, stage: 'testing', stageProgress: 0, stageTotal: testTotal });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -291,94 +585,91 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
|
||||
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,
|
||||
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,
|
||||
});
|
||||
updatedCohorts.push({ ...cohort, stage: next, stageProgress: 0, stageTotal: total });
|
||||
}
|
||||
}
|
||||
|
||||
computeRacksOnline += racksJustOnlined;
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
networkSummary = expandDCTopology(networkSummary, racksJustOnlined, dc.tier, dc.id, registry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Production failures (statistical)
|
||||
// Production failures
|
||||
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;
|
||||
const repairCost = sku.baseCost * sku.repairCostFraction * prodFailures;
|
||||
dcRepairCosts += repairCost;
|
||||
|
||||
dcRepairCosts += sku.baseCost * sku.repairCostFraction * prodFailures;
|
||||
updatedCohorts.push({
|
||||
id: `prodfail-${dc.id}-${Date.now()}`,
|
||||
count: prodFailures,
|
||||
skuId: dc.rackSkuId,
|
||||
stage: 'repair',
|
||||
stageProgress: 0,
|
||||
count: prodFailures, skuId: dc.rackSkuId,
|
||||
stage: 'repair', stageProgress: 0,
|
||||
stageTotal: cohortStageTotal('repair', dc.rackSkuId, prodFailures),
|
||||
repairCount: 0,
|
||||
});
|
||||
|
||||
|
||||
networkSummary = shrinkDCTopology(networkSummary, prodFailures, dc.tier, registry);
|
||||
}
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
// Recompute DC network summary after failures/repairs
|
||||
if (netResult.dirty && networkSummary.switchIds.length > 0) {
|
||||
networkSummary = buildDCSummary(
|
||||
networkSummary.switchIds, networkSummary.networkRackCount, registry,
|
||||
);
|
||||
}
|
||||
|
||||
const effectiveComputeRacks = computeRacksOnline - racksDisconnected;
|
||||
// Rackdown: detect recovery (previously disconnected racks now have connectivity)
|
||||
const prevDisconnected = dc.networkSummary.racksDisconnected;
|
||||
const currDisconnected = networkSummary.racksDisconnected;
|
||||
|
||||
// Compute aggregates for this DC
|
||||
if (currDisconnected < prevDisconnected && dc.rackSkuId) {
|
||||
const recovered = prevDisconnected - currDisconnected;
|
||||
computeRacksOnline -= recovered;
|
||||
networkSummary = shrinkDCTopology(networkSummary, recovered, dc.tier, registry);
|
||||
updatedCohorts.push({
|
||||
id: `netrecovery-${dc.id}-${Date.now()}`,
|
||||
count: recovered, skuId: dc.rackSkuId,
|
||||
stage: 'testing', stageProgress: 0,
|
||||
stageTotal: cohortStageTotal('testing', dc.rackSkuId, recovered),
|
||||
repairCount: 0,
|
||||
});
|
||||
// Recompute summary after shrink
|
||||
networkSummary = buildDCSummary(
|
||||
networkSummary.switchIds, networkSummary.networkRackCount, registry,
|
||||
);
|
||||
}
|
||||
|
||||
// Compute DC aggregates
|
||||
const effectiveComputeRacks = Math.max(0,
|
||||
computeRacksOnline - networkSummary.racksDisconnected);
|
||||
const location = LOCATION_CONFIGS[cluster.locationId];
|
||||
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
||||
const pipelineRacks = updatedCohorts
|
||||
@@ -388,7 +679,7 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
.filter(c => c.stage === 'repair')
|
||||
.reduce((sum, c) => sum + c.count, 0);
|
||||
const totalRacksInDc = computeRacksOnline + pipelineRacks;
|
||||
const netSlots = networkSlotsRequired(computeRacksOnline + pipelineRacks);
|
||||
const netSlots = networkSummary.networkRackCount;
|
||||
const usedSlots = computeRacksOnline + pipelineRacks + netSlots;
|
||||
|
||||
let usedPowerKW = 0;
|
||||
@@ -396,36 +687,33 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
if (dc.rackSkuId && computeRacksOnline > 0) {
|
||||
const sku = RACK_SKU_CONFIGS[dc.rackSkuId];
|
||||
usedPowerKW = computeRacksOnline * sku.powerDrawKW;
|
||||
dcFlops = effectiveComputeRacks * sku.flopsPerRack;
|
||||
dcFlops = effectiveComputeRacks * sku.flopsPerRack * networkSummary.effectiveFlopsFraction;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Latency penalty from bandwidth degradation
|
||||
if (networkSummary.averageBandwidth < 1 && computeRacksOnline > 0) {
|
||||
const penalty = (1 - networkSummary.averageBandwidth) * NETWORK_DEGRADATION.bandwidthToLatencyPenalty;
|
||||
globalLatencyPenalty += penalty;
|
||||
latencyDCCount++;
|
||||
}
|
||||
|
||||
totalFlops += dcFlops;
|
||||
totalRackCount += totalRacksInDc + netSlots;
|
||||
totalComputeRackCount += totalRacksInDc;
|
||||
totalDataCenterCount++;
|
||||
if (totalRacksInDc > 0) {
|
||||
totalUptime += currentUptime;
|
||||
dcWithRacks++;
|
||||
}
|
||||
if (totalRacksInDc > 0) { totalUptime += currentUptime; dcWithRacks++; }
|
||||
|
||||
return {
|
||||
...dc,
|
||||
computeRacksOnline,
|
||||
computeRacksFailed,
|
||||
computeRacksOnline, computeRacksFailed,
|
||||
deploymentCohorts: updatedCohorts,
|
||||
networkHealth,
|
||||
effectiveComputeRacks,
|
||||
usedSlots,
|
||||
usedPowerKW,
|
||||
energyCostPerTick,
|
||||
maintenanceCostPerTick,
|
||||
currentUptime,
|
||||
networkSummary, effectiveComputeRacks,
|
||||
usedSlots, usedPowerKW, energyCostPerTick, maintenanceCostPerTick, currentUptime,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -436,22 +724,16 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
if (updatedQueue && updatedQueue.pendingDCIds.length + updatedQueue.activeDCIds.length > 0) {
|
||||
updatedQueue = { ...updatedQueue };
|
||||
|
||||
// Detect DCs that just completed retrofit (were active, now operational)
|
||||
const newlyCompleted = finalDCs.filter(
|
||||
dc => updatedQueue!.activeDCIds.includes(dc.id) && dc.status === 'operational',
|
||||
);
|
||||
|
||||
if (newlyCompleted.length > 0) {
|
||||
updatedQueue.activeDCIds = updatedQueue.activeDCIds.filter(
|
||||
id => !newlyCompleted.some(dc => dc.id === id),
|
||||
);
|
||||
updatedQueue.completedDCIds = [
|
||||
...updatedQueue.completedDCIds,
|
||||
...newlyCompleted.map(dc => dc.id),
|
||||
];
|
||||
updatedQueue.completedDCIds = [...updatedQueue.completedDCIds, ...newlyCompleted.map(dc => dc.id)];
|
||||
}
|
||||
|
||||
// Promote DCs from pending to active
|
||||
const slotsAvailable = updatedQueue.maxConcurrent - updatedQueue.activeDCIds.length;
|
||||
if (slotsAvailable > 0 && updatedQueue.pendingDCIds.length > 0) {
|
||||
const toStart = updatedQueue.pendingDCIds.slice(0, slotsAvailable);
|
||||
@@ -461,31 +743,28 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
finalDCs = finalDCs.map(dc => {
|
||||
if (!toStart.includes(dc.id)) return dc;
|
||||
if (dc.status !== 'operational' || !dc.rackSkuId) return dc;
|
||||
|
||||
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
|
||||
const totalRacks = dc.computeRacksOnline + pipelineCount;
|
||||
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,
|
||||
deploymentCohorts: [],
|
||||
networkSummary: emptyDCNetworkSummary(),
|
||||
retrofitState: {
|
||||
fromSkuId: dc.rackSkuId as RackSkuId,
|
||||
toSkuId: updatedQueue!.targetSkuId,
|
||||
phase: 'decommissioning' as const,
|
||||
progress: 0,
|
||||
total: decommTicks,
|
||||
racksRemaining: totalRacks,
|
||||
progress: 0, total: decommTicks, racksRemaining: totalRacks,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Check if queue is complete
|
||||
if (updatedQueue.pendingDCIds.length === 0 && updatedQueue.activeDCIds.length === 0) {
|
||||
notifications.push({
|
||||
title: 'Campus Retrofit Complete',
|
||||
@@ -502,14 +781,18 @@ export function processInfrastructure(state: GameState): InfraTickResult {
|
||||
return { ...cluster, campuses };
|
||||
});
|
||||
|
||||
const avgLatencyPenalty = latencyDCCount > 0 ? globalLatencyPenalty / latencyDCCount : 0;
|
||||
|
||||
return {
|
||||
infrastructure: {
|
||||
clusters,
|
||||
switchRegistry: registry,
|
||||
totalFlops,
|
||||
totalUptime: dcWithRacks > 0 ? totalUptime / dcWithRacks : 1,
|
||||
totalRackCount,
|
||||
totalComputeRackCount,
|
||||
totalDataCenterCount,
|
||||
networkLatencyPenalty: avgLatencyPenalty,
|
||||
},
|
||||
notifications,
|
||||
repairCosts,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
OPEN_SOURCE_REVENUE_PENALTY,
|
||||
OPEN_SOURCE_TALENT_ATTRACTION,
|
||||
MARKET_SIZE_CAP,
|
||||
NETWORK_DEGRADATION,
|
||||
MARKET_CAP_QUALITY_BONUS,
|
||||
MARKET_CAP_REPUTATION_BONUS,
|
||||
OVERLOAD_PENALTY_EXPONENT,
|
||||
@@ -83,8 +84,10 @@ export function processMarket(state: GameState, currentTickCapacity: number): Ma
|
||||
overloadPenalty = Math.min(1, Math.pow(demandCapacityRatio - 1, OVERLOAD_PENALTY_EXPONENT));
|
||||
}
|
||||
|
||||
const networkLatencyPenalty = state.infrastructure.networkLatencyPenalty *
|
||||
NETWORK_DEGRADATION.satisfactionPenaltyPerLatency;
|
||||
consumers.satisfaction = Math.min(1, Math.max(0,
|
||||
0.3 + modelQuality * 0.5 + headroomBonus - overloadPenalty,
|
||||
0.3 + modelQuality * 0.5 + headroomBonus - overloadPenalty - networkLatencyPenalty,
|
||||
));
|
||||
|
||||
consumers.viralCoefficient = modelQuality > 0.5 ? 1 + (modelQuality - 0.5) * 2 : 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DCTier, DCTierConfig, RackSkuId, RackSkuConfig, NetworkTopologyConfig, CampusTierCost, ClusterCostConfig } from '../types/infrastructure';
|
||||
import type { DCTier, DCTierConfig, RackSkuId, RackSkuConfig, SwitchTier, SwitchTierConfig, CampusTierCost, ClusterCostConfig } from '../types/infrastructure';
|
||||
|
||||
export const TICK_INTERVAL_MS = 1000;
|
||||
export const MAX_OFFLINE_TICKS = 86_400;
|
||||
@@ -123,19 +123,92 @@ export const CLUSTER_COST_CONFIG: ClusterCostConfig = {
|
||||
buildTimeTicks: 600,
|
||||
};
|
||||
|
||||
// --- Network Topology ---
|
||||
// --- Network Topology (6-Tier Clos) ---
|
||||
|
||||
export const NETWORK_TOPOLOGY: NetworkTopologyConfig = {
|
||||
tier1PerCompute: 24,
|
||||
tier2PerTier1: 6,
|
||||
tier3PerDC: 2,
|
||||
tier1FailureRate: 0.0001,
|
||||
tier2FailureRate: 0.00005,
|
||||
tier3FailureRate: 0.00002,
|
||||
tier1BlastRadius: 24,
|
||||
tier2BlastRadiusMultiplier: 6,
|
||||
export const SWITCH_TIER_CONFIGS: Record<SwitchTier, SwitchTierConfig> = {
|
||||
tor: {
|
||||
tier: 'tor', name: 'Top-of-Rack',
|
||||
baseCost: 0, uplinkCount: 2, fanOut: 1,
|
||||
failureRatePerTick: 0.00005, repairBaseTicks: 15,
|
||||
switchesPerNetworkRack: 0, powerDrawKW: 0,
|
||||
},
|
||||
t1: {
|
||||
tier: 't1', name: 'Tier-1 Aggregation',
|
||||
baseCost: 8_000, uplinkCount: 2, fanOut: 24,
|
||||
failureRatePerTick: 0.0001, repairBaseTicks: 25,
|
||||
switchesPerNetworkRack: 4, powerDrawKW: 0.5,
|
||||
},
|
||||
t2: {
|
||||
tier: 't2', name: 'Tier-2 Spine',
|
||||
baseCost: 25_000, uplinkCount: 4, fanOut: 6,
|
||||
failureRatePerTick: 0.00008, repairBaseTicks: 40,
|
||||
switchesPerNetworkRack: 2, powerDrawKW: 1.0,
|
||||
},
|
||||
t3: {
|
||||
tier: 't3', name: 'Tier-3 Core',
|
||||
baseCost: 80_000, uplinkCount: 4, fanOut: 0,
|
||||
failureRatePerTick: 0.00004, repairBaseTicks: 60,
|
||||
switchesPerNetworkRack: 1, powerDrawKW: 2.0,
|
||||
},
|
||||
t4: {
|
||||
tier: 't4', name: 'Tier-4 Campus',
|
||||
baseCost: 200_000, uplinkCount: 4, fanOut: 0,
|
||||
failureRatePerTick: 0.00002, repairBaseTicks: 90,
|
||||
switchesPerNetworkRack: 0, powerDrawKW: 3.0,
|
||||
},
|
||||
t5: {
|
||||
tier: 't5', name: 'Tier-5 Cluster',
|
||||
baseCost: 500_000, uplinkCount: 0, fanOut: 0,
|
||||
failureRatePerTick: 0.00001, repairBaseTicks: 120,
|
||||
switchesPerNetworkRack: 0, powerDrawKW: 5.0,
|
||||
},
|
||||
};
|
||||
|
||||
export const NETWORK_RACK_COST = 5_000;
|
||||
|
||||
export const T3_COUNT_PER_DC_TIER: Record<DCTier, number> = {
|
||||
small: 2, medium: 2, large: 3, mega: 4,
|
||||
};
|
||||
|
||||
export const T4_COUNT_PER_CAMPUS: Record<DCTier, number> = {
|
||||
small: 2, medium: 2, large: 3, mega: 4,
|
||||
};
|
||||
|
||||
export const T5_COUNT_PER_CLUSTER = 2;
|
||||
|
||||
export const NETWORK_DEGRADATION = {
|
||||
bandwidthToLatencyPenalty: 0.3,
|
||||
satisfactionPenaltyPerLatency: 0.05,
|
||||
};
|
||||
|
||||
export const SWITCH_REPAIR_COST_FRACTION = 0.3;
|
||||
|
||||
export function estimateNetworkSlots(computeRacks: number, dcTier: DCTier): number {
|
||||
if (computeRacks <= 0) return 0;
|
||||
const t1Count = Math.ceil(computeRacks / SWITCH_TIER_CONFIGS.t1.fanOut);
|
||||
const t2Count = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t2.fanOut);
|
||||
const t3Count = T3_COUNT_PER_DC_TIER[dcTier];
|
||||
const t1Racks = Math.ceil(t1Count / SWITCH_TIER_CONFIGS.t1.switchesPerNetworkRack);
|
||||
const t2Racks = Math.ceil(t2Count / SWITCH_TIER_CONFIGS.t2.switchesPerNetworkRack);
|
||||
const t3Racks = Math.ceil(t3Count / SWITCH_TIER_CONFIGS.t3.switchesPerNetworkRack);
|
||||
return t1Racks + t2Racks + t3Racks;
|
||||
}
|
||||
|
||||
export function maxComputeRacks(totalSlots: number, dcTier: DCTier): number {
|
||||
if (totalSlots <= 2) return 0;
|
||||
let lo = 0;
|
||||
let hi = totalSlots;
|
||||
while (lo < hi) {
|
||||
const mid = Math.ceil((lo + hi) / 2);
|
||||
if (mid + estimateNetworkSlots(mid, dcTier) <= totalSlots) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
// --- Rack SKU Configs ---
|
||||
|
||||
export const RACK_SKU_CONFIGS: Record<RackSkuId, RackSkuConfig> = {
|
||||
|
||||
@@ -58,4 +58,4 @@ export const INITIAL_SETTINGS: GameSettings = {
|
||||
sfxVolume: 0.7,
|
||||
};
|
||||
|
||||
export const SAVE_VERSION = 3;
|
||||
export const SAVE_VERSION = 4;
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Cluster {
|
||||
status: ClusterStatus;
|
||||
constructionProgress: number;
|
||||
constructionTotal: number;
|
||||
networkSummary: ClusterNetworkSummary;
|
||||
}
|
||||
|
||||
// --- Campus (holds same-tier DCs) ---
|
||||
@@ -37,6 +38,7 @@ export interface Campus {
|
||||
constructionProgress: number;
|
||||
constructionTotal: number;
|
||||
retrofitQueue: CampusRetrofitQueue | null;
|
||||
networkSummary: CampusNetworkSummary;
|
||||
}
|
||||
|
||||
// --- Data Center ---
|
||||
@@ -68,7 +70,7 @@ export interface DataCenter {
|
||||
rackSkuId: RackSkuId | null;
|
||||
computeRacksOnline: number;
|
||||
computeRacksFailed: number;
|
||||
networkHealth: NetworkHealthState;
|
||||
networkSummary: DCNetworkSummary;
|
||||
deploymentCohorts: DeploymentCohort[];
|
||||
retrofitState: RetrofitState | null;
|
||||
coolingLevel: number;
|
||||
@@ -81,50 +83,62 @@ export interface DataCenter {
|
||||
currentUptime: number;
|
||||
}
|
||||
|
||||
// --- Network Topology ---
|
||||
// --- Network Topology (6-Tier Clos) ---
|
||||
|
||||
export interface NetworkHealthState {
|
||||
tier1Required: number;
|
||||
tier1Healthy: number;
|
||||
tier2Required: number;
|
||||
tier2Healthy: number;
|
||||
tier3Required: number;
|
||||
tier3Healthy: number;
|
||||
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;
|
||||
baseCost: number;
|
||||
uplinkCount: number;
|
||||
fanOut: number;
|
||||
failureRatePerTick: number;
|
||||
repairBaseTicks: number;
|
||||
switchesPerNetworkRack: number;
|
||||
powerDrawKW: number;
|
||||
}
|
||||
|
||||
export interface DCNetworkSummary {
|
||||
switchIds: string[];
|
||||
networkRackCount: number;
|
||||
totalByTier: Partial<Record<SwitchTier, number>>;
|
||||
healthyByTier: Partial<Record<SwitchTier, number>>;
|
||||
racksDisconnected: number;
|
||||
racksDegraded: number;
|
||||
averageBandwidth: number;
|
||||
effectiveFlopsFraction: number;
|
||||
}
|
||||
|
||||
export interface NetworkTopologyConfig {
|
||||
tier1PerCompute: number;
|
||||
tier2PerTier1: number;
|
||||
tier3PerDC: number;
|
||||
tier1FailureRate: number;
|
||||
tier2FailureRate: number;
|
||||
tier3FailureRate: number;
|
||||
tier1BlastRadius: number;
|
||||
tier2BlastRadiusMultiplier: number;
|
||||
export interface CampusNetworkSummary {
|
||||
switchIds: string[];
|
||||
totalT4: number;
|
||||
healthyT4: number;
|
||||
crossDCBandwidth: 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;
|
||||
export interface ClusterNetworkSummary {
|
||||
switchIds: string[];
|
||||
totalT5: number;
|
||||
healthyT5: number;
|
||||
crossCampusBandwidth: number;
|
||||
}
|
||||
|
||||
// --- Racks ---
|
||||
@@ -137,7 +151,7 @@ export type RackSkuId =
|
||||
|
||||
export type PipelineStage =
|
||||
| 'ordered' | 'manufacturing' | 'receiving'
|
||||
| 'installation' | 'testing' | 'repair' | 'decommission';
|
||||
| 'installation' | 'testing' | 'repair' | 'network-down' | 'decommission';
|
||||
|
||||
export interface PipelineTimings {
|
||||
manufacturing: number;
|
||||
@@ -202,20 +216,24 @@ export interface ClusterCostConfig {
|
||||
|
||||
export interface InfrastructureState {
|
||||
clusters: Cluster[];
|
||||
switchRegistry: Record<string, NetworkSwitch>;
|
||||
totalFlops: number;
|
||||
totalUptime: number;
|
||||
totalRackCount: number;
|
||||
totalComputeRackCount: number;
|
||||
totalDataCenterCount: number;
|
||||
networkLatencyPenalty: number;
|
||||
}
|
||||
|
||||
export const INITIAL_INFRASTRUCTURE: InfrastructureState = {
|
||||
clusters: [],
|
||||
switchRegistry: {},
|
||||
totalFlops: 0,
|
||||
totalUptime: 1,
|
||||
totalRackCount: 0,
|
||||
totalComputeRackCount: 0,
|
||||
totalDataCenterCount: 0,
|
||||
networkLatencyPenalty: 0,
|
||||
};
|
||||
|
||||
// --- Locations ---
|
||||
|
||||
Reference in New Issue
Block a user