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
+235 -1
View File
@@ -9,7 +9,7 @@ import type {
Cluster, Campus, DataCenter, DCTier, RackSkuId, TrainingJob,
ActiveResearch, OwnedDataset, LocationId,
DeploymentCohort, PipelineStage,
NetworkHealthState,
NetworkHealthState, CampusRetrofitQueue,
} from '@ai-tycoon/shared';
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
import {
@@ -79,6 +79,10 @@ interface Actions {
addDCsToCampus: (campusId: string, count: number) => void;
retrofitDC: (dataCenterId: string, newSkuId: RackSkuId) => void;
cancelRetrofit: (dataCenterId: string) => void;
fillCampusToCapacity: (campusId: string, skuId: RackSkuId) => void;
fillClusterToCapacity: (clusterId: string, skuId: RackSkuId) => void;
startCampusRetrofit: (campusId: string, targetSkuId: RackSkuId, maxConcurrent: number) => void;
cancelCampusRetrofit: (campusId: string) => void;
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
deployModel: (modelId: string) => void;
@@ -166,6 +170,62 @@ function updateDCInInfra(infra: InfrastructureState, dcId: string, updater: (dc:
};
}
function updateClusterInInfra(infra: InfrastructureState, clusterId: string, updater: (cluster: Cluster) => Cluster): InfrastructureState {
return {
...infra,
clusters: infra.clusters.map(cluster =>
cluster.id === clusterId ? updater(cluster) : cluster,
),
};
}
export function computeFillForDC(
dc: DataCenter,
skuId: RackSkuId,
availableMoney: number,
): { qty: number; cost: number } {
if (dc.status !== 'operational') return { qty: 0, cost: 0 };
if (dc.rackSkuId !== null && dc.rackSkuId !== skuId) return { qty: 0, cost: 0 };
const sku = RACK_SKU_CONFIGS[skuId];
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const maxCompute = maxComputeRacks(tierConfig.rackSlots);
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;
if (available <= 0) return { qty: 0, cost: 0 };
const affordableQty = Math.floor(availableMoney / sku.baseCost);
const powerLimit = Math.floor((tierConfig.powerBudgetKW - dc.computeRacksOnline * sku.powerDrawKW) / sku.powerDrawKW);
const qty = Math.min(available, affordableQty, Math.max(0, powerLimit));
if (qty <= 0) return { qty: 0, cost: 0 };
return { qty, cost: sku.baseCost * qty };
}
function startRetrofitOnDC(dc: DataCenter, targetSkuId: RackSkuId): DataCenter {
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!];
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacks));
return {
...dc,
status: 'retrofitting' as const,
deploymentCohorts: [],
retrofitState: {
fromSkuId: dc.rackSkuId!,
toSkuId: targetSkuId,
phase: 'decommissioning' as const,
progress: 0,
total: decommTicks,
racksRemaining: totalRacks,
},
};
}
function updateCampusInInfra(infra: InfrastructureState, campusId: string, updater: (campus: Campus) => Campus): InfrastructureState {
return {
...infra,
@@ -297,6 +357,7 @@ export const useGameStore = create<Store>()(
status: 'constructing',
constructionProgress: 0,
constructionTotal: buildTime,
retrofitQueue: null,
};
return {
@@ -535,6 +596,179 @@ export const useGameStore = create<Store>()(
};
}),
// --- Infrastructure: Bulk Actions ---
fillCampusToCapacity: (campusId, skuId) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || found.campus.status !== 'operational') return s;
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
let remainingMoney = s.economy.money;
const dcUpdates = new Map<string, DeploymentCohort>();
for (const dc of found.campus.dataCenters) {
const { qty, cost } = computeFillForDC(dc, skuId, remainingMoney);
if (qty <= 0) continue;
const baseTicks = PIPELINE_ORDER_BASE_TICKS;
const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * qty));
dcUpdates.set(dc.id, {
id: uuid(),
count: qty,
skuId,
stage: 'ordered' as PipelineStage,
stageProgress: 0,
stageTotal: scaledTicks,
repairCount: 0,
});
remainingMoney -= cost;
}
if (dcUpdates.size === 0) return s;
return {
economy: { ...s.economy, money: remainingMoney },
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
dataCenters: campus.dataCenters.map(dc => {
const cohort = dcUpdates.get(dc.id);
if (!cohort) return dc;
return { ...dc, rackSkuId: skuId, deploymentCohorts: [...dc.deploymentCohorts, cohort] };
}),
})),
};
}),
fillClusterToCapacity: (clusterId, skuId) => set((s) => {
const cluster = findCluster(s.infrastructure, clusterId);
if (!cluster || cluster.status !== 'operational') return s;
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
let remainingMoney = s.economy.money;
const allDcUpdates = new Map<string, DeploymentCohort>();
for (const campus of cluster.campuses) {
if (campus.status !== 'operational') continue;
for (const dc of campus.dataCenters) {
const { qty, cost } = computeFillForDC(dc, skuId, remainingMoney);
if (qty <= 0) continue;
const baseTicks = PIPELINE_ORDER_BASE_TICKS;
const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * qty));
allDcUpdates.set(dc.id, {
id: uuid(),
count: qty,
skuId,
stage: 'ordered' as PipelineStage,
stageProgress: 0,
stageTotal: scaledTicks,
repairCount: 0,
});
remainingMoney -= cost;
}
}
if (allDcUpdates.size === 0) return s;
return {
economy: { ...s.economy, money: remainingMoney },
infrastructure: updateClusterInInfra(s.infrastructure, clusterId, (cl) => ({
...cl,
campuses: cl.campuses.map(campus => ({
...campus,
dataCenters: campus.dataCenters.map(dc => {
const cohort = allDcUpdates.get(dc.id);
if (!cohort) return dc;
return { ...dc, rackSkuId: skuId, deploymentCohorts: [...dc.deploymentCohorts, cohort] };
}),
})),
})),
};
}),
startCampusRetrofit: (campusId, targetSkuId, maxConcurrent) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || found.campus.status !== 'operational') return s;
if (found.campus.retrofitQueue) return s;
const sku = RACK_SKU_CONFIGS[targetSkuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch && !s.research.completedResearch.includes(sku.requiredResearch)) return s;
const eligible: string[] = [];
const skipped: string[] = [];
for (const dc of found.campus.dataCenters) {
if (dc.status !== 'operational' || !dc.rackSkuId || dc.rackSkuId === targetSkuId) {
skipped.push(dc.id);
continue;
}
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
if (dc.computeRacksOnline + pipelineCount <= 0) {
skipped.push(dc.id);
continue;
}
eligible.push(dc.id);
}
if (eligible.length === 0) return s;
const concurrent = Math.max(1, Math.min(maxConcurrent, eligible.length));
const toStartNow = eligible.slice(0, concurrent);
const pending = eligible.slice(concurrent);
const queue: CampusRetrofitQueue = {
targetSkuId,
maxConcurrent: concurrent,
pendingDCIds: pending,
activeDCIds: toStartNow,
completedDCIds: [],
skippedDCIds: skipped,
};
return {
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
retrofitQueue: queue,
dataCenters: campus.dataCenters.map(dc => {
if (!toStartNow.includes(dc.id)) return dc;
return startRetrofitOnDC(dc, targetSkuId);
}),
})),
};
}),
cancelCampusRetrofit: (campusId) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || !found.campus.retrofitQueue) return s;
const queue = found.campus.retrofitQueue;
if (queue.activeDCIds.length === 0) {
return {
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
retrofitQueue: null,
})),
};
}
return {
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
retrofitQueue: { ...queue, pendingDCIds: [] },
})),
};
}),
// --- Infrastructure: Upgrades ---
upgradeDataCenter: (dataCenterId, upgrade) => set((s) => {