Add bulk fill and staggered retrofit at campus/cluster level
CI / build-and-push (push) Successful in 40s

Campus level: "Fill All DCs" instantly fills all operational DCs with
selected SKU in one click. "Retrofit Campus" queues a staggered retrofit
with configurable concurrency (1/10%/25%/custom) so only a fraction of
DCs go offline at a time, preserving capacity during the upgrade.

Cluster level: "Fill All DCs" fills across all campuses in one action.

The game engine automatically advances the retrofit queue each tick,
promoting pending DCs as active ones complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:26:14 -04:00
parent 02791f9500
commit 4a318c36ad
4 changed files with 840 additions and 45 deletions
@@ -1,6 +1,7 @@
import type {
GameState, InfrastructureState, Cluster, Campus, DataCenter,
DeploymentCohort, NetworkHealthState, PipelineStage,
DeploymentCohort, NetworkHealthState, PipelineStage, RackSkuId,
CampusRetrofitQueue,
} from '@ai-tycoon/shared';
import {
LOCATION_CONFIGS,
@@ -442,7 +443,74 @@ export function processInfrastructure(state: GameState): InfraTickResult {
};
});
return { ...campus, dataCenters };
// Process campus retrofit queue
let finalDCs = dataCenters;
let updatedQueue: CampusRetrofitQueue | null = campus.retrofitQueue ?? null;
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),
];
}
// 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);
updatedQueue.pendingDCIds = updatedQueue.pendingDCIds.slice(toStart.length);
updatedQueue.activeDCIds = [...updatedQueue.activeDCIds, ...toStart];
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));
return {
...dc,
status: 'retrofitting' as const,
deploymentCohorts: [],
retrofitState: {
fromSkuId: dc.rackSkuId as RackSkuId,
toSkuId: updatedQueue!.targetSkuId,
phase: 'decommissioning' as const,
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',
message: `All DCs in ${campus.name} have been retrofitted to ${RACK_SKU_CONFIGS[updatedQueue.targetSkuId].name}!`,
type: 'success',
});
updatedQueue = null;
}
}
return { ...campus, dataCenters: finalDCs, retrofitQueue: updatedQueue };
});
return { ...cluster, campuses };
@@ -18,6 +18,15 @@ export interface Cluster {
export type CampusStatus = 'constructing' | 'operational';
export interface CampusRetrofitQueue {
targetSkuId: RackSkuId;
maxConcurrent: number;
pendingDCIds: string[];
activeDCIds: string[];
completedDCIds: string[];
skippedDCIds: string[];
}
export interface Campus {
id: string;
name: string;
@@ -27,6 +36,7 @@ export interface Campus {
status: CampusStatus;
constructionProgress: number;
constructionTotal: number;
retrofitQueue: CampusRetrofitQueue | null;
}
// --- Data Center ---