Files
AIHostingTycoon/packages/game-engine/src/systems/infrastructureSystem.ts
T
josh 24278297f0
CI / build-and-push (push) Successful in 39s
Add rack decommission pipeline stage
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>
2026-04-24 20:15:48 -04:00

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,
};
}