Add bulk fill and staggered retrofit at campus/cluster level
CI / build-and-push (push) Successful in 40s
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:
+235
-1
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user