Add game-simulation package with multi-run balance testing, fix stalled-pipeline trap
Balance Check / balance-simulation (push) Failing after 11m32s
Balance Check / multi-run-balance (push) Failing after 23m46s
CI / build-and-push (push) Successful in 1m20s

Adds a full simulation harness (game-simulation package) with greedy/random strategies,
36-metric diagnostics, multi-run orchestration via child processes, and a statistical
interpreter. Includes 2.3x engine performance optimizations (research bonus caching,
per-DC dirty tracking, reduced allocations in tick pipeline, single-pass loops).

Fixes a critical balance bug where training pipelines stalled on insufficient VRAM would
permanently block training slots — the engine never re-checked stalled pipelines, and the
greedy strategy didn't pre-check VRAM requirements. This caused 20-25% of seeds to get
stuck in Scale-up era. All three fixes (engine un-stalling, strategy VRAM pre-check,
stalled pipeline cancellation) bring pass rate from 75% to 100% across 20 random seeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 06:11:26 -04:00
parent 283c7c7932
commit 102e05c8ba
51 changed files with 4294 additions and 132 deletions
@@ -0,0 +1,436 @@
import type { GameState, Era, RackSkuId } from '@ai-tycoon/shared';
import {
RACK_SKU_CONFIGS, DC_TIER_CONFIGS, COOLING_ORDER,
PARAMETER_OPTIONS, DEFAULT_DATA_MIX,
MAX_CONCURRENT_TRAINING, PRETRAINING_BASE_TICKS,
CLUSTER_COST_CONFIG, LOCATION_CONFIGS, maxComputeRacks,
VRAM_REQUIREMENTS_BY_GENERATION,
} from '@ai-tycoon/shared';
import {
canRaiseFunding, getNextFundingRound, getAvailableResearch, TECH_TREE,
} from '@ai-tycoon/game-engine';
import * as actions from '../actions';
import type { Strategy, SimulationMetrics } from './types';
const ERA_ORDER: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
const TALENT_TARGETS: Record<Era, Record<string, number>> = {
startup: { research: 5, engineering: 5, operations: 2, sales: 3 },
scaleup: { research: 10, engineering: 10, operations: 4, sales: 8 },
bigtech: { research: 20, engineering: 20, operations: 8, sales: 12 },
agi: { research: 40, engineering: 40, operations: 16, sales: 20 },
};
const RESEARCH_PRIORITY: Record<string, number> = {
'advanced-cooling': 200,
'dc-engineering-ii': 190,
'advanced-gpu-arch': 180,
'alignment-research': 250,
'transformer-v2': 165,
'quantization': 160,
'data-pipeline': 155,
'developer-relations': 150,
'enterprise-sales': 175,
'redundancy-protocols': 140,
'quality-assurance': 130,
'liquid-cooling-tech': 120,
'next-gen-gpu': 115,
'distributed-training': 110,
'inference-optimization': 105,
'dc-engineering-iii': 100,
'code-generation': 170,
'reasoning-enhancement': 90,
'amd-ecosystem': 85,
'infiniband-networking': 80,
'distillation': 75,
'inference-specialization': 70,
'sdk-platform': 65,
'request-batching': 60,
'request-routing': 55,
'code-assistant-product': 168,
'creative-systems': 45,
'multimodal-fusion': 40,
'network-engineering-i': 35,
'rapid-deployment': 30,
'priority-queues': 25,
'interpretability': 180,
'immersion-cooling-tech': 18,
'frontier-compute': 16,
'dc-engineering-iv': 14,
'network-engineering-ii': 12,
'agentic-architecture': 88,
'constitutional-ai': 160,
'network-redundancy': 6,
'auto-scaling': 5,
'agents-platform-product': 86,
'network-fast-repair': 3,
'rack-scale-compute': 2,
'custom-silicon': 1,
'network-hot-standby': 0,
};
function cashSafe(state: GameState, cost: number, runway = 100): boolean {
return state.economy.money - cost > state.economy.expensesPerTick * runway;
}
function getBestAffordableSku(state: GameState): RackSkuId | null {
const era = state.meta.currentEra;
const completed = state.research.completedResearch;
const eligible = (Object.entries(RACK_SKU_CONFIGS) as [RackSkuId, typeof RACK_SKU_CONFIGS[RackSkuId]][])
.filter(([, sku]) => {
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(sku.era)) return false;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => completed.includes(r))) return false;
if (state.economy.money < sku.baseCost) return false;
return true;
})
.sort((a, b) => (b[1].trainingFlops / b[1].baseCost) - (a[1].trainingFlops / a[1].baseCost));
return eligible.length > 0 ? eligible[0][0] : null;
}
function getOperationalDCs(state: GameState) {
const results: { dcId: string; coolingType: string; rackSkuId: string | null }[] = [];
for (const cluster of state.infrastructure.clusters) {
for (const campus of cluster.campuses) {
for (const dc of campus.dataCenters) {
if (dc.status === 'operational') {
results.push({ dcId: dc.id, coolingType: dc.coolingType, rackSkuId: dc.rackSkuId });
}
}
}
}
return results;
}
function pickModelParams(state: GameState): number {
const vram = state.infrastructure.totalVramGB;
const era = state.meta.currentEra;
const maxByEra: Record<Era, number> = {
startup: 7,
scaleup: 70,
bigtech: 300,
agi: 1400,
};
const vramPerBillion = 2;
const maxByVram = Math.floor(vram / vramPerBillion);
const cap = Math.min(maxByEra[era], maxByVram);
let best = PARAMETER_OPTIONS[0];
for (const p of PARAMETER_OPTIONS) {
if (p <= cap) best = p;
}
return best;
}
export class GreedyStrategy implements Strategy {
name = 'greedy';
decide(state: GameState, _metrics: SimulationMetrics[]): void {
this.tryRaiseFunding(state);
this.tryBuildInfrastructure(state);
this.tryDeployRacks(state);
this.tryDeployModels(state);
this.tryOpenSourceModel(state);
this.cancelStalledTraining(state);
this.tryStartTraining(state);
this.tryEnableRevenue(state);
this.tryStartResearch(state);
this.tryHireTalent(state);
this.tryUpgradeInfra(state);
this.tryExpandInfra(state);
}
private tryRaiseFunding(state: GameState): void {
const { canRaise, nextRound } = canRaiseFunding(state);
if (canRaise && nextRound) {
actions.raiseFunding(state, nextRound);
}
}
private tryBuildInfrastructure(state: GameState): void {
if (state.infrastructure.clusters.length === 0) {
actions.buildCluster(state, 'Primary', 'us-west');
}
for (const cluster of state.infrastructure.clusters) {
if (cluster.status !== 'operational') continue;
if (cluster.campuses.length === 0) {
actions.buildCampus(state, 'Campus-1', cluster.id, 'small');
}
for (const campus of cluster.campuses) {
if (campus.status !== 'operational') continue;
if (campus.dataCenters.length === 0) {
actions.buildDataCenter(state, 'DC-1', campus.id);
}
}
}
}
private tryDeployRacks(state: GameState): void {
const skuId = getBestAffordableSku(state);
if (!skuId) return;
const sku = RACK_SKU_CONFIGS[skuId];
const operationalDCs = getOperationalDCs(state);
for (const { dcId, coolingType, rackSkuId } of operationalDCs) {
if (rackSkuId !== null && rackSkuId !== skuId) continue;
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(coolingType as typeof sku.requiredCooling);
if (!coolingOk) continue;
if (!cashSafe(state, sku.baseCost, 50)) break;
actions.fillDCToCapacity(state, dcId, skuId);
}
}
private tryDeployModels(state: GameState): void {
const undeployed = state.models.baseModels
.filter(m => !m.isDeployed)
.sort((a, b) => b.rawCapability - a.rawCapability);
if (undeployed.length > 0) {
actions.deployModel(state, undeployed[0].id);
}
}
private tryOpenSourceModel(state: GameState): void {
if (state.market.openSourcedModels.length > 0) return;
const deployed = state.models.baseModels.filter(m => m.isDeployed);
if (deployed.length > 0) {
actions.openSourceModel(state, deployed[0].id);
}
}
private cancelStalledTraining(state: GameState): void {
const stalledPipelines = state.models.activeTrainingPipelines.filter(
p => p.status === 'stalled',
);
for (const pipeline of stalledPipelines) {
const stalledTicks = state.meta.tickCount - pipeline.startedAtTick;
if (stalledTicks < 500) continue;
const gen = state.models.families.find(f => f.id === pipeline.familyId)?.generation ?? 1;
const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[gen] ?? 0;
if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) {
state.models.activeTrainingPipelines = state.models.activeTrainingPipelines.filter(
p => p.id !== pipeline.id,
);
}
}
}
private tryStartTraining(state: GameState): void {
const activeCount = state.models.activeTrainingPipelines.filter(
p => p.status === 'active' || p.status === 'stalled',
).length;
const maxSlots = MAX_CONCURRENT_TRAINING[state.meta.currentEra] ?? 1;
if (activeCount >= maxSlots) return;
if (state.infrastructure.totalVramGB <= 0) return;
const gen = state.models.families.length + 1;
const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[gen] ?? 0;
if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) return;
const params = pickModelParams(state);
const trainingFlops = state.infrastructure.totalTrainingFlops;
const totalTicks = trainingFlops > 0
? Math.max(30, Math.ceil(PRETRAINING_BASE_TICKS / (1 + trainingFlops * 0.1)))
: PRETRAINING_BASE_TICKS;
const targetTokens = params * 20e9;
const hasCodeGen = state.research.completedResearch.includes('code-generation');
const sftSpecs: ('general' | 'code')[] = hasCodeGen ? ['general', 'code'] : ['general'];
const hasAlignment = state.research.completedResearch.includes('alignment-research');
actions.startTrainingPipeline(state, {
familyName: `SimCorp-${gen}`,
architecture: {
type: 'dense',
totalParameters: params,
activeParameters: params,
contextWindow: 32,
vocabularySize: 32000,
},
dataMix: { ...DEFAULT_DATA_MIX },
allocatedComputeFraction: 1.0,
targetTokens,
totalTicks,
sftSpecializations: sftSpecs,
alignmentMethod: hasAlignment ? 'rlhf' : 'dpo',
alignmentSafetyWeight: 0.75,
});
}
private tryEnableRevenue(state: GameState): void {
if (state.models.bestDeployedModelScore <= 0) return;
const ct = state.market.consumerTiers.tiers;
if (!ct.free.config.isActive) actions.toggleConsumerTier(state, 'free');
if (!ct.plus.config.isActive) actions.toggleConsumerTier(state, 'plus');
if (!ct.pro.config.isActive && state.models.bestDeployedModelScore >= 20) {
actions.toggleConsumerTier(state, 'pro');
}
if (!ct.team.config.isActive && state.models.bestDeployedModelScore >= 30) {
actions.toggleConsumerTier(state, 'team');
}
const at = state.market.apiTiers.tiers;
if (!at.free.config.isActive) actions.toggleApiTier(state, 'free');
if (!at.payg.config.isActive) actions.toggleApiTier(state, 'payg');
if (!at.scale.config.isActive && state.models.bestDeployedModelScore >= 25) {
actions.toggleApiTier(state, 'scale');
}
if (!at['enterprise-api'].config.isActive && state.models.bestDeployedModelScore >= 40) {
actions.toggleApiTier(state, 'enterprise-api');
}
if (state.research.completedResearch.includes('code-assistant-product')
&& !state.market.codeAssistant.isActive) {
actions.toggleCodeAssistant(state);
actions.setCodeAssistantPrice(state, 20);
}
if (state.research.completedResearch.includes('agents-platform-product')
&& !state.market.agentsPlatform.isActive) {
actions.toggleAgentsPlatform(state);
actions.setAgentsPlatformPrice(state, 50);
}
}
private tryStartResearch(state: GameState): void {
if (state.research.activeResearch) return;
const available = getAvailableResearch(state);
if (available.length === 0) return;
const sorted = [...available].sort((a, b) => {
const pa = RESEARCH_PRIORITY[a.id] ?? 0;
const pb = RESEARCH_PRIORITY[b.id] ?? 0;
return pb - pa;
});
const best = sorted[0];
actions.startResearch(state, {
researchId: best.id,
progressTicks: 0,
totalTicks: best.cost.ticks,
allocatedResearchers: 0,
allocatedCompute: 0,
});
}
private tryHireTalent(state: GameState): void {
const targets = TALENT_TARGETS[state.meta.currentEra];
const depts = state.talent.departments;
for (const [dept, target] of Object.entries(targets)) {
const current = depts[dept as keyof typeof depts].headcount;
if (current < target) {
const needed = Math.min(target - current, 3);
const cost = needed * 2000;
if (cashSafe(state, cost, 200)) {
actions.hireDepartment(state, dept as actions.DepartmentId, needed);
}
}
}
}
private tryUpgradeInfra(state: GameState): void {
for (const cluster of state.infrastructure.clusters) {
for (const campus of cluster.campuses) {
for (const dc of campus.dataCenters) {
if (dc.status !== 'operational') continue;
if (dc.coolingType === 'air'
&& state.research.completedResearch.includes('liquid-cooling-tech')
&& cashSafe(state, 500_000)) {
actions.upgradeCoolingType(state, dc.id, 'liquid');
}
if (dc.coolingType === 'liquid'
&& state.research.completedResearch.includes('immersion-cooling-tech')
&& cashSafe(state, 1_000_000)) {
actions.upgradeCoolingType(state, dc.id, 'immersion');
}
if (dc.networkFabric === 'ethernet-100g'
&& cashSafe(state, 200_000)) {
actions.upgradeNetworkFabric(state, dc.id, 'ethernet-400g');
}
if (dc.networkFabric === 'ethernet-400g'
&& state.research.completedResearch.includes('infiniband-networking')
&& cashSafe(state, 500_000)) {
actions.upgradeNetworkFabric(state, dc.id, 'infiniband-ndr');
}
}
}
}
}
private tryExpandInfra(state: GameState): void {
const era = state.meta.currentEra;
for (const cluster of state.infrastructure.clusters) {
if (cluster.status !== 'operational') continue;
for (const campus of cluster.campuses) {
if (campus.status !== 'operational') continue;
const allFull = campus.dataCenters.length > 0 && campus.dataCenters.every(dc => {
if (dc.status !== 'operational') return true;
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const mc = maxComputeRacks(tierConfig.rackSlots, dc.tier);
const existing = dc.computeRacksOnline + actions.pipelineCount(dc);
return existing >= mc;
});
if (allFull && campus.dataCenters.length > 0) {
const tierConfig = DC_TIER_CONFIGS[campus.dcTier];
if (cashSafe(state, tierConfig.baseCost, 300)) {
actions.addDCsToCampus(state, campus.id, 1);
}
}
}
}
if (ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf('scaleup')) {
const targetTier = state.research.completedResearch.includes('dc-engineering-iii') ? 'large' as const
: state.research.completedResearch.includes('dc-engineering-ii') ? 'medium' as const
: 'small' as const;
for (const cluster of state.infrastructure.clusters) {
if (cluster.status !== 'operational') continue;
const hasHighTierCampus = cluster.campuses.some(c => c.dcTier === targetTier);
if (!hasHighTierCampus && cashSafe(state, 2_000_000, 300)) {
actions.buildCampus(state, `${targetTier}-Campus`, cluster.id, targetTier);
}
}
}
if (ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf('scaleup')) {
const usedLocations = new Set(state.infrastructure.clusters.map(c => c.locationId));
const candidates: ('eu-north' | 'us-east')[] = ['eu-north', 'us-east'];
for (const loc of candidates) {
if (!usedLocations.has(loc)) {
const locConfig = LOCATION_CONFIGS[loc];
if (ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf(locConfig.availableAt)) {
if (cashSafe(state, CLUSTER_COST_CONFIG.baseCost, 500)) {
actions.buildCluster(state, `Cluster-${loc}`, loc);
break;
}
}
}
}
}
}
}
@@ -0,0 +1,97 @@
import type { GameState } from '@ai-tycoon/shared';
import { RACK_SKU_CONFIGS, DEFAULT_DATA_MIX, PARAMETER_OPTIONS } from '@ai-tycoon/shared';
import { canRaiseFunding, getNextFundingRound, getAvailableResearch } from '@ai-tycoon/game-engine';
import * as actions from '../actions';
import type { Strategy, SimulationMetrics } from './types';
export class RandomStrategy implements Strategy {
name = 'random';
decide(state: GameState, _metrics: SimulationMetrics[]): void {
const candidates: (() => void)[] = [];
const { canRaise, nextRound } = canRaiseFunding(state);
if (canRaise && nextRound) {
candidates.push(() => actions.raiseFunding(state, nextRound));
}
if (state.infrastructure.clusters.length === 0) {
candidates.push(() => actions.buildCluster(state, 'Cluster-1', 'us-west'));
}
for (const cluster of state.infrastructure.clusters) {
if (cluster.status !== 'operational') continue;
if (cluster.campuses.length < 3) {
candidates.push(() => actions.buildCampus(state, 'Campus', cluster.id, 'small'));
}
for (const campus of cluster.campuses) {
if (campus.status !== 'operational') continue;
if (campus.dataCenters.length < 5) {
candidates.push(() => actions.buildDataCenter(state, 'DC', campus.id));
}
for (const dc of campus.dataCenters) {
if (dc.status !== 'operational') continue;
const skuIds = Object.keys(RACK_SKU_CONFIGS) as (keyof typeof RACK_SKU_CONFIGS)[];
const randomSku = skuIds[Math.floor(Math.random() * skuIds.length)];
candidates.push(() => actions.fillDCToCapacity(state, dc.id, randomSku));
}
}
}
if (!state.research.activeResearch) {
const available = getAvailableResearch(state);
if (available.length > 0) {
const pick = available[Math.floor(Math.random() * available.length)];
candidates.push(() => actions.startResearch(state, {
researchId: pick.id,
progressTicks: 0,
totalTicks: pick.cost.ticks,
allocatedResearchers: 0,
allocatedCompute: 0,
}));
}
}
const undeployed = state.models.baseModels.filter(m => !m.isDeployed);
if (undeployed.length > 0) {
candidates.push(() => actions.deployModel(state, undeployed[0].id));
}
if (state.infrastructure.totalVramGB > 0) {
const params = PARAMETER_OPTIONS[Math.floor(Math.random() * PARAMETER_OPTIONS.length)];
candidates.push(() => actions.startTrainingPipeline(state, {
familyName: `Rand-${state.models.families.length + 1}`,
architecture: {
type: 'dense',
totalParameters: params,
activeParameters: params,
contextWindow: 32,
vocabularySize: 32000,
},
dataMix: { ...DEFAULT_DATA_MIX },
allocatedComputeFraction: 1.0,
targetTokens: params * 20e9,
totalTicks: Math.ceil(params * 2 + 60),
sftSpecializations: ['general'],
alignmentMethod: 'dpo',
alignmentSafetyWeight: 0.5,
}));
}
const depts: actions.DepartmentId[] = ['research', 'engineering', 'operations', 'sales'];
const dept = depts[Math.floor(Math.random() * depts.length)];
candidates.push(() => actions.hireDepartment(state, dept, 1));
if (state.models.bestDeployedModelScore > 0) {
candidates.push(() => {
actions.toggleConsumerTier(state, 'free');
actions.toggleApiTier(state, 'free');
});
}
if (candidates.length > 0) {
const pick = Math.floor(Math.random() * candidates.length);
candidates[pick]();
}
}
}
@@ -0,0 +1,60 @@
import type { GameState } from '@ai-tycoon/shared';
export interface SimulationMetrics {
tick: number;
era: string;
money: number;
revenue: number;
totalRevenue: number;
expensesPerTick: number;
bestModelCapability: number;
reputation: number;
subscribers: number;
developers: number;
totalFlops: number;
totalTrainingFlops: number;
researchCount: number;
headcount: number;
modelsDeployed: number;
// Reputation breakdown
safetyRecord: number;
publicPerception: number;
employeeSatisfaction: number;
regulatoryStanding: number;
// Cash flow
netCashFlow: number;
// Infrastructure utilization
tokensPerSecondCapacity: number;
tokensPerSecondDemand: number;
inferenceUtilization: number;
// Training pipeline
activeTrainingPipelines: number;
bestPipelineProgress: number;
// Revenue breakdown
subscriptionRevenue: number;
apiTokenRevenue: number;
enterpriseRevenue: number;
// Talent breakdown
researchHeadcount: number;
engineeringHeadcount: number;
operationsHeadcount: number;
salesHeadcount: number;
// Feature activation counts
completedResearchIds: string[];
activeConsumerTiers: number;
activeApiTiers: number;
enterpriseContracts: number;
fundingRoundsCompleted: number;
}
export interface Strategy {
name: string;
decide(state: GameState, metrics: SimulationMetrics[]): void;
}