24278297f0
CI / build-and-push (push) Successful in 39s
Racks can now be marked for decommission from the DC view. The rack leaves production immediately (freeing slot and power), enters the pipeline as a timed decommission order, and is removed when complete. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
288 lines
8.6 KiB
TypeScript
288 lines
8.6 KiB
TypeScript
import type { GameState, InfrastructureState, DataCenter, RackOrder, Rack, PipelineStage } from '@ai-tycoon/shared';
|
|
import {
|
|
LOCATION_CONFIGS,
|
|
RACK_SKU_CONFIGS,
|
|
DC_TIER_CONFIGS,
|
|
BASE_ENERGY_COST_PER_FLOP,
|
|
BASE_MAINTENANCE_PER_RACK,
|
|
COOLING_FAILURE_REDUCTION,
|
|
REDUNDANCY_FAILURE_REDUCTION,
|
|
RACK_REPAIR_BASE_TICKS,
|
|
} from '@ai-tycoon/shared';
|
|
import type { TickNotification } from '../tick';
|
|
|
|
export interface InfraTickResult {
|
|
infrastructure: InfrastructureState;
|
|
notifications: TickNotification[];
|
|
repairCosts: number;
|
|
}
|
|
|
|
const PIPELINE_ADVANCE_ORDER: PipelineStage[] = [
|
|
'ordered', 'manufacturing', 'receiving', 'installation', 'testing',
|
|
];
|
|
|
|
function nextStage(stage: PipelineStage): PipelineStage | 'production' {
|
|
const idx = PIPELINE_ADVANCE_ORDER.indexOf(stage);
|
|
if (idx === -1 || idx === PIPELINE_ADVANCE_ORDER.length - 1) return 'production';
|
|
return PIPELINE_ADVANCE_ORDER[idx + 1];
|
|
}
|
|
|
|
function stageTotal(stage: PipelineStage, order: RackOrder): number {
|
|
const sku = RACK_SKU_CONFIGS[order.skuId];
|
|
const timings = sku.pipelineTimeTicks;
|
|
switch (stage) {
|
|
case 'manufacturing': return timings.manufacturing;
|
|
case 'receiving': return timings.receiving;
|
|
case 'installation': return timings.installation;
|
|
case 'testing': return timings.testing;
|
|
case 'repair': return RACK_REPAIR_BASE_TICKS;
|
|
case 'decommission': return timings.installation;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
function stageSpeed(stage: PipelineStage, engEff: number, opsEff: number): number {
|
|
switch (stage) {
|
|
case 'manufacturing': return 1 + engEff * 0.1;
|
|
case 'installation':
|
|
case 'testing':
|
|
case 'decommission': return 1 + opsEff * 0.1;
|
|
case 'repair': return 1 + opsEff * 0.05;
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
export function processInfrastructure(state: GameState): InfraTickResult {
|
|
const notifications: TickNotification[] = [];
|
|
let repairCosts = 0;
|
|
|
|
const engEff = state.talent.departments.engineering.effectiveness;
|
|
const opsEff = state.talent.departments.operations.effectiveness;
|
|
|
|
const qaResearchBonus = state.research.completedResearch.includes('quality-assurance') ? 0.25 : 0;
|
|
|
|
// --- Phase 1: Advance DC Construction ---
|
|
const dataCenters: DataCenter[] = state.infrastructure.dataCenters.map(dc => {
|
|
if (dc.status !== 'constructing') return { ...dc };
|
|
|
|
const newProgress = dc.constructionProgress + 1;
|
|
if (newProgress >= dc.constructionTotal) {
|
|
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 };
|
|
});
|
|
|
|
// --- Phase 2: Advance Rack Pipeline ---
|
|
const rackPipeline: RackOrder[] = [];
|
|
const newRacks: Rack[] = [];
|
|
|
|
for (const order of state.infrastructure.rackPipeline) {
|
|
const speed = stageSpeed(order.stage, engEff, opsEff);
|
|
const newProgress = order.stageProgress + speed;
|
|
|
|
if (newProgress < order.stageTotal) {
|
|
rackPipeline.push({ ...order, stageProgress: newProgress });
|
|
continue;
|
|
}
|
|
|
|
if (order.stage === 'decommission') {
|
|
const sku = RACK_SKU_CONFIGS[order.skuId];
|
|
notifications.push({
|
|
title: 'Rack Decommissioned',
|
|
message: `${sku.name} rack has been fully decommissioned.`,
|
|
type: 'info',
|
|
});
|
|
continue;
|
|
}
|
|
|
|
if (order.stage === 'repair') {
|
|
const total = stageTotal('testing', order);
|
|
rackPipeline.push({
|
|
...order,
|
|
stage: 'testing',
|
|
stageProgress: 0,
|
|
stageTotal: total,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const next = nextStage(order.stage);
|
|
|
|
if (next === 'production') {
|
|
const sku = RACK_SKU_CONFIGS[order.skuId];
|
|
const dc = dataCenters.find(d => d.id === order.dataCenterId);
|
|
const cooling = dc?.coolingLevel ?? 0;
|
|
|
|
const effectiveFailRate = sku.testFailureRate
|
|
* (1 - cooling * COOLING_FAILURE_REDUCTION)
|
|
* (1 - opsEff * 0.2)
|
|
* (1 - qaResearchBonus);
|
|
|
|
if (Math.random() < effectiveFailRate) {
|
|
const repairCost = sku.baseCost * sku.repairCostFraction;
|
|
repairCosts += repairCost;
|
|
rackPipeline.push({
|
|
...order,
|
|
stage: 'repair',
|
|
stageProgress: 0,
|
|
stageTotal: RACK_REPAIR_BASE_TICKS,
|
|
repairCount: order.repairCount + 1,
|
|
});
|
|
notifications.push({
|
|
title: 'Rack Failed Testing',
|
|
message: `${sku.name} rack failed QA (attempt ${order.repairCount + 1}). Repair cost: $${repairCost.toLocaleString()}`,
|
|
type: 'warning',
|
|
});
|
|
} else {
|
|
newRacks.push({
|
|
id: order.id,
|
|
skuId: order.skuId,
|
|
dataCenterId: order.dataCenterId,
|
|
isHealthy: true,
|
|
});
|
|
notifications.push({
|
|
title: 'Rack Online',
|
|
message: `${sku.name} rack is now in production at ${dc?.name ?? 'data center'}.`,
|
|
type: 'success',
|
|
});
|
|
}
|
|
} else {
|
|
const total = stageTotal(next, order);
|
|
rackPipeline.push({
|
|
...order,
|
|
stage: next,
|
|
stageProgress: 0,
|
|
stageTotal: total,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add newly completed racks to their data centers
|
|
for (const rack of newRacks) {
|
|
const dcIdx = dataCenters.findIndex(d => d.id === rack.dataCenterId);
|
|
if (dcIdx !== -1) {
|
|
dataCenters[dcIdx] = {
|
|
...dataCenters[dcIdx],
|
|
racks: [...dataCenters[dcIdx].racks, rack],
|
|
};
|
|
}
|
|
}
|
|
|
|
// --- Phase 3: Production Failures ---
|
|
for (let dcIdx = 0; dcIdx < dataCenters.length; dcIdx++) {
|
|
const dc = dataCenters[dcIdx];
|
|
if (dc.status !== 'operational') continue;
|
|
|
|
const updatedRacks: Rack[] = [];
|
|
for (const rack of dc.racks) {
|
|
if (!rack.isHealthy) {
|
|
updatedRacks.push(rack);
|
|
continue;
|
|
}
|
|
|
|
const sku = RACK_SKU_CONFIGS[rack.skuId];
|
|
const effectiveRate = sku.productionFailureRate
|
|
* (1 - dc.coolingLevel * COOLING_FAILURE_REDUCTION)
|
|
* (1 - dc.redundancyLevel * REDUNDANCY_FAILURE_REDUCTION);
|
|
|
|
if (Math.random() < effectiveRate) {
|
|
updatedRacks.push({ ...rack, isHealthy: false });
|
|
const repairCost = sku.baseCost * sku.repairCostFraction;
|
|
repairCosts += repairCost;
|
|
|
|
rackPipeline.push({
|
|
id: rack.id,
|
|
skuId: rack.skuId,
|
|
dataCenterId: dc.id,
|
|
stage: 'repair',
|
|
stageProgress: 0,
|
|
stageTotal: RACK_REPAIR_BASE_TICKS,
|
|
totalCost: repairCost,
|
|
repairCount: 0,
|
|
});
|
|
|
|
notifications.push({
|
|
title: 'Rack Failure',
|
|
message: `${sku.name} rack failed in ${dc.name}. Sent for repair.`,
|
|
type: 'danger',
|
|
});
|
|
} else {
|
|
updatedRacks.push(rack);
|
|
}
|
|
}
|
|
|
|
// Remove failed racks from the DC (they're now in the repair pipeline)
|
|
dataCenters[dcIdx] = {
|
|
...dc,
|
|
racks: updatedRacks.filter(r => r.isHealthy),
|
|
};
|
|
}
|
|
|
|
// --- Phase 4: Compute Aggregates ---
|
|
let totalFlops = 0;
|
|
let totalUptime = 0;
|
|
let totalRackCount = 0;
|
|
let dcWithRacks = 0;
|
|
|
|
for (let dcIdx = 0; dcIdx < dataCenters.length; dcIdx++) {
|
|
const dc = dataCenters[dcIdx];
|
|
if (dc.status !== 'operational') continue;
|
|
|
|
const location = LOCATION_CONFIGS[dc.location];
|
|
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
|
|
|
let dcFlops = 0;
|
|
let usedPowerKW = 0;
|
|
const healthyCount = dc.racks.length;
|
|
const totalInDc = dc.racks.length;
|
|
|
|
for (const rack of dc.racks) {
|
|
const sku = RACK_SKU_CONFIGS[rack.skuId];
|
|
dcFlops += sku.flopsPerRack;
|
|
usedPowerKW += sku.powerDrawKW;
|
|
}
|
|
|
|
const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id && o.stage !== 'decommission').length;
|
|
const usedSlots = totalInDc + pipelineRacksForDc;
|
|
|
|
const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
|
|
* location.energyCostMultiplier;
|
|
const maintenanceCostPerTick = totalInDc * BASE_MAINTENANCE_PER_RACK;
|
|
|
|
const currentUptime = totalInDc > 0 ? healthyCount / totalInDc : 1;
|
|
|
|
totalFlops += dcFlops;
|
|
totalRackCount += totalInDc;
|
|
if (totalInDc > 0) {
|
|
totalUptime += currentUptime;
|
|
dcWithRacks++;
|
|
}
|
|
|
|
dataCenters[dcIdx] = {
|
|
...dataCenters[dcIdx],
|
|
usedSlots,
|
|
usedPowerKW,
|
|
energyCostPerTick,
|
|
maintenanceCostPerTick,
|
|
currentUptime,
|
|
};
|
|
}
|
|
|
|
return {
|
|
infrastructure: {
|
|
dataCenters,
|
|
rackPipeline,
|
|
totalFlops,
|
|
totalUptime: dcWithRacks > 0 ? totalUptime / dcWithRacks : 1,
|
|
totalRackCount,
|
|
},
|
|
notifications,
|
|
repairCosts,
|
|
};
|
|
}
|