Overhaul infrastructure: replace GPU model with rack-centric system
CI / build-and-push (push) Successful in 33s

Replace flat GPU buying with a realistic data center + rack pipeline:
- 4 DC tiers (small/medium/large/mega) with construction time, dual
  capacity constraints (rack slots + power budget kW), and era/research
  gating
- 10 predefined rack SKUs from consumer GPUs through custom ASICs, each
  with unique FLOPS, power draw, cost, and pipeline timings
- 6-stage procurement pipeline (order → mfg → receive → install → test
  → production) with Kanban UI, talent-influenced speed bonuses
- Test failures (5-25% base rate) reduced by cooling, ops talent, and QA
  research; auto-repair with cost and re-test cycle
- Production failures at low per-tick rate, racks sent to repair pipeline
- Cooling and redundancy upgrades per DC (reduce failure rates)
- 4 new tech tree nodes (DC Engineering II/III/IV, Quality Assurance)
- Save version bump (1→2) with migration that resets old saves
- Updated economy system to account for rack repair costs
- Redesigned Infrastructure page with pipeline Kanban, capacity bars,
  rack ordering, and DC upgrade panels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 19:41:55 -04:00
parent 1af9408c87
commit 0005e580a7
14 changed files with 1051 additions and 295 deletions
@@ -79,11 +79,11 @@ export const ACHIEVEMENT_DEFINITIONS: AchievementDefinition[] = [
condition: { field: 'meta._eraIndex', operator: 'gte', value: 3 },
},
{
id: 'gpu-hoarder',
name: 'GPU Hoarder',
description: 'Own 100 or more GPUs across all data centers.',
id: 'rack-hoarder',
name: 'Rack Hoarder',
description: 'Have 50 or more production racks across all data centers.',
icon: 'Cpu',
condition: { field: 'infrastructure._totalGpuCount', operator: 'gte', value: 100 },
condition: { field: 'infrastructure.totalRackCount', operator: 'gte', value: 50 },
},
{
id: 'research-pioneer',
+47 -7
View File
@@ -25,32 +25,32 @@ export const TECH_TREE: ResearchNode[] = [
{
id: 'advanced-gpu-arch',
name: 'Advanced GPU Architecture',
description: 'Unlocks procurement of NVIDIA A100 datacenter GPUs.',
description: 'Unlocks procurement of NVIDIA A100 rack configurations.',
era: 'startup',
category: 'infrastructure',
prerequisites: [],
cost: { researchPoints: 0, compute: 10, ticks: 90 },
effects: [{ type: 'unlock_gpu', target: 'a100', value: 1 }],
effects: [{ type: 'unlock_rack', target: 'a100', value: 1 }],
},
{
id: 'next-gen-gpu',
name: 'Next-Gen GPU Architecture',
description: 'Unlocks procurement of NVIDIA H100 GPUs.',
description: 'Unlocks procurement of NVIDIA H100 rack configurations.',
era: 'scaleup',
category: 'infrastructure',
prerequisites: ['advanced-gpu-arch'],
cost: { researchPoints: 2, compute: 40, ticks: 240 },
effects: [{ type: 'unlock_gpu', target: 'h100', value: 1 }],
effects: [{ type: 'unlock_rack', target: 'h100', value: 1 }],
},
{
id: 'frontier-compute',
name: 'Frontier Compute',
description: 'Unlocks procurement of NVIDIA B200 GPUs.',
description: 'Unlocks procurement of NVIDIA B200 rack configurations.',
era: 'bigtech',
category: 'infrastructure',
prerequisites: ['next-gen-gpu'],
cost: { researchPoints: 5, compute: 200, ticks: 480 },
effects: [{ type: 'unlock_gpu', target: 'b200', value: 1 }],
effects: [{ type: 'unlock_rack', target: 'b200', value: 1 }],
},
{
id: 'custom-silicon',
@@ -60,7 +60,47 @@ export const TECH_TREE: ResearchNode[] = [
category: 'infrastructure',
prerequisites: ['frontier-compute'],
cost: { researchPoints: 10, compute: 500, ticks: 900 },
effects: [{ type: 'unlock_gpu', target: 'custom', value: 1 }],
effects: [{ type: 'unlock_rack', target: 'custom', value: 1 }],
},
{
id: 'dc-engineering-ii',
name: 'DC Engineering II',
description: 'Advanced facility design unlocks Medium data centers (30 slots, 200kW).',
era: 'startup',
category: 'infrastructure',
prerequisites: ['advanced-cooling'],
cost: { researchPoints: 1, compute: 15, ticks: 120 },
effects: [{ type: 'unlock_dc_tier', target: 'medium', value: 1 }],
},
{
id: 'dc-engineering-iii',
name: 'DC Engineering III',
description: 'Large-scale facility design unlocks Large data centers (60 slots, 500kW).',
era: 'scaleup',
category: 'infrastructure',
prerequisites: ['dc-engineering-ii'],
cost: { researchPoints: 3, compute: 60, ticks: 300 },
effects: [{ type: 'unlock_dc_tier', target: 'large', value: 1 }],
},
{
id: 'dc-engineering-iv',
name: 'DC Engineering IV',
description: 'Mega-scale campus design unlocks Mega data centers (120 slots, 1200kW).',
era: 'bigtech',
category: 'infrastructure',
prerequisites: ['dc-engineering-iii'],
cost: { researchPoints: 6, compute: 150, ticks: 600 },
effects: [{ type: 'unlock_dc_tier', target: 'mega', value: 1 }],
},
{
id: 'quality-assurance',
name: 'Quality Assurance',
description: 'Advanced QA processes reduce rack test failure rate by 25%.',
era: 'startup',
category: 'infrastructure',
prerequisites: ['redundancy-protocols'],
cost: { researchPoints: 1, compute: 10, ticks: 90 },
effects: [{ type: 'cost_reduction', target: 'test_failure_rate', value: 0.25 }],
},
{
id: 'distributed-training',
@@ -10,12 +10,6 @@ const ERA_INDEX: Record<string, number> = { startup: 0, scaleup: 1, bigtech: 2,
function getFieldValue(state: GameState, field: string): number {
if (field === 'meta._eraIndex') return ERA_INDEX[state.meta.currentEra] ?? 0;
if (field === 'meta._deployedModelCount') return state.models.trainedModels.filter(m => m.isDeployed).length;
if (field === 'infrastructure._totalGpuCount') {
return state.infrastructure.dataCenters.reduce(
(sum, dc) => sum + dc.gpus.reduce((s, g) => s + g.count, 0), 0,
);
}
const parts = field.split('.');
let current: unknown = state;
for (const part of parts) {
@@ -6,6 +6,7 @@ export function processEconomy(
state: GameState,
market: MarketTickResult,
infrastructure: InfrastructureState,
extraCosts: number = 0,
): EconomyState {
const revenue = market.apiRevenue + market.subscriptionRevenue;
@@ -20,7 +21,7 @@ export function processEconomy(
const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra);
const complianceCost = bestCapability > 30 ? bestCapability * REGULATION_COMPLIANCE_PER_CAPABILITY * (1 + eraIdx * 0.5) / 100 : 0;
const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost;
const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost + extraCosts;
const money = state.economy.money + revenue - expenses;
@@ -1,72 +1,275 @@
import type { GameState, InfrastructureState } from '@ai-tycoon/shared';
import type { GameState, InfrastructureState, DataCenter, RackOrder, Rack, PipelineStage } from '@ai-tycoon/shared';
import {
GPU_CONFIGS,
LOCATION_CONFIGS,
GPU_PRICE_VOLATILITY,
GPU_FAILURE_RATE_BASE,
REDUNDANCY_FAILURE_REDUCTION,
RACK_SKU_CONFIGS,
DC_TIER_CONFIGS,
BASE_ENERGY_COST_PER_FLOP,
BASE_MAINTENANCE_PER_GPU,
BASE_MAINTENANCE_PER_RACK,
COOLING_FAILURE_REDUCTION,
REDUNDANCY_FAILURE_REDUCTION,
RACK_REPAIR_BASE_TICKS,
} from '@ai-tycoon/shared';
import type { GpuType } from '@ai-tycoon/shared';
import type { TickNotification } from '../tick';
export function processInfrastructure(state: GameState): InfrastructureState {
const dataCenters = state.infrastructure.dataCenters.map(dc => {
const location = LOCATION_CONFIGS[dc.location];
export interface InfraTickResult {
infrastructure: InfrastructureState;
notifications: TickNotification[];
repairCosts: number;
}
const gpus = dc.gpus.map(inv => {
const failureRate = GPU_FAILURE_RATE_BASE * (1 - dc.redundancyLevel * REDUNDANCY_FAILURE_REDUCTION);
let newFailed = inv.failedCount;
for (let i = 0; i < inv.healthyCount; i++) {
if (Math.random() < failureRate) newFailed++;
}
const healthyCount = Math.max(0, inv.count - newFailed);
return { ...inv, healthyCount, failedCount: newFailed };
});
const PIPELINE_ADVANCE_ORDER: PipelineStage[] = [
'ordered', 'manufacturing', 'receiving', 'installation', 'testing',
];
let totalFlops = 0;
let totalPower = 0;
let totalGpuCount = 0;
for (const inv of gpus) {
const config = GPU_CONFIGS[inv.type];
totalFlops += inv.healthyCount * config.flopsPerUnit;
totalPower += inv.healthyCount * config.basePowerDraw;
totalGpuCount += inv.count;
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;
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': 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 };
}
const energyCostPerTick = totalPower * BASE_ENERGY_COST_PER_FLOP * location.energyCostMultiplier;
const maintenanceCostPerTick = totalGpuCount * BASE_MAINTENANCE_PER_GPU;
const currentUptime = totalGpuCount > 0
? gpus.reduce((s, inv) => s + inv.healthyCount, 0) / totalGpuCount
: 1;
return { ...dc, gpus, energyCostPerTick, maintenanceCostPerTick, currentUptime };
return { ...dc, constructionProgress: newProgress };
});
const gpuMarketPrices = { ...state.infrastructure.gpuMarketPrices };
for (const gpuType of Object.keys(gpuMarketPrices) as GpuType[]) {
const basePrice = GPU_CONFIGS[gpuType].basePrice;
const variation = (Math.random() - 0.5) * 2 * GPU_PRICE_VOLATILITY;
const currentPrice = gpuMarketPrices[gpuType];
const newPrice = currentPrice * (1 + variation);
gpuMarketPrices[gpuType] = Math.max(basePrice * 0.7, Math.min(basePrice * 1.5, newPrice));
// --- 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 === '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 dcCount = 0;
for (const dc of dataCenters) {
for (const inv of dc.gpus) {
totalFlops += inv.healthyCount * GPU_CONFIGS[inv.type].flopsPerUnit;
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;
}
totalUptime += dc.currentUptime;
dcCount++;
const pipelineRacksForDc = rackPipeline.filter(o => o.dataCenterId === dc.id).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 {
dataCenters,
gpuMarketPrices,
totalFlops,
totalUptime: dcCount > 0 ? totalUptime / dcCount : 1,
infrastructure: {
dataCenters,
rackPipeline,
totalFlops,
totalUptime: dcWithRacks > 0 ? totalUptime / dcWithRacks : 1,
totalRackCount,
},
notifications,
repairCosts,
};
}
+4 -2
View File
@@ -39,7 +39,9 @@ export function setAchievementDefinitions(defs: AchievementDefinition[]) {
export function processTick(state: GameState): Partial<GameState> {
const notifications: TickNotification[] = [];
const infrastructure = processInfrastructure(state);
const infraResult = processInfrastructure(state);
const infrastructure = infraResult.infrastructure;
notifications.push(...infraResult.notifications);
const stateWithInfra = { ...state, infrastructure };
const modelResult = processModels(stateWithInfra);
@@ -82,7 +84,7 @@ export function processTick(state: GameState): Partial<GameState> {
type: 'danger',
});
}
const economy = processEconomy(stateWithTalent, market, infrastructure);
const economy = processEconomy(stateWithTalent, market, infrastructure, infraResult.repairCosts);
const data = processData(stateWithTalent);
const competitors = processCompetitors(stateWithTalent);