import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; import { resolve as pathResolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { randomInt } from 'node:crypto'; import { cpus } from 'node:os'; import type { SimulationMetrics } from './strategies/types'; const __dirname = dirname(fileURLToPath(import.meta.url)); const args = process.argv.slice(2); function getArg(name: string, defaultValue: string): string { const idx = args.indexOf(`--${name}`); return idx !== -1 && args[idx + 1] ? args[idx + 1] : defaultValue; } function hasFlag(name: string): boolean { return args.includes(`--${name}`); } const totalRuns = parseInt(getArg('runs', '0'), 10); const parallel = Math.min(totalRuns, parseInt(getArg('parallel', String(Math.max(1, cpus().length - 1))), 10)); const strategyName = getArg('strategy', 'greedy'); const totalTicks = parseInt(getArg('ticks', '28800'), 10); const outDir = pathResolve(getArg('out', pathResolve(__dirname, '..'))); const baseSeedStr = getArg('seed', ''); const baseSeed = baseSeedStr ? parseInt(baseSeedStr, 10) : null; const noTimeseries = hasFlag('no-timeseries'); if (totalRuns <= 0) { console.error('Usage: multirun --runs [--parallel

] [--strategy ] [--ticks ] [--out

] [--seed ] [--no-timeseries]'); process.exit(1); } function deriveSeeds(count: number, base: number | null): number[] { if (base === null) { return Array.from({ length: count }, () => randomInt(1, 2_147_483_647)); } // Deterministic derivation from base seed const seeds: number[] = []; for (let i = 0; i < count; i++) { let h = (base + i * 0x9E3779B9) | 0; h = Math.imul(h ^ (h >>> 16), 0x45D9F3B); h = Math.imul(h ^ (h >>> 13), 0x45D9F3B); h = (h ^ (h >>> 16)) >>> 0; seeds.push(h || 1); } return seeds; } interface WorkerResult { runId: number; seed: number; passed: boolean; failureReasons: string[]; wallTimeMs: number; eraTransitions: Array<{ from: string; to: string; tick: number; wallTime: string }>; finalMetrics: SimulationMetrics | null; featureUtilization: { coverageByCategory: Record; unusedFeatures: string[]; revenueStreamDiversity: number; }; systemInterconnections: { connections: Array<{ from: string; to: string; score: number; evidence: string; diagnosis: string; events: number; eventLabel: string }>; overallScore: number; }; cashFlow: { bankruptcyRisks: number; minCash: { amount: number; tick: number }; peakCash: { amount: number; tick: number }; }; sanityChecks: { passed: boolean; errorCount: number }; perEraSummary: Array<{ era: string; durationTicks: number; bottleneckAtExit: string | null }>; serving: { meanUtilization: number; pctOverloaded: number; pctUnderloaded: number; peakUtilization: number; }; lateGameRevenueGrowthRate: number; metrics: SimulationMetrics[]; } function spawnWorker(runId: number, seed: number): Promise { return new Promise((resolve, reject) => { const workerPath = new URL('./worker.ts', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1'); const tsxBin = pathResolve(__dirname, '..', 'node_modules', '.bin', 'tsx'); const child = spawn(tsxBin, [workerPath, '--strategy', strategyName, '--ticks', String(totalTicks), '--seed', String(seed), '--run-id', String(runId)], { stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; child.stdout.on('data', (chunk: Buffer) => { stdout += chunk; }); child.stderr.on('data', (chunk: Buffer) => { process.stderr.write(chunk); }); child.on('close', (code) => { if (code !== 0) { reject(new Error(`Run #${runId} (seed ${seed}) exited with code ${code}`)); return; } try { resolve(JSON.parse(stdout) as WorkerResult); } catch (e) { reject(new Error(`Run #${runId} (seed ${seed}) produced invalid JSON: ${(e as Error).message}`)); } }); child.on('error', (err) => { reject(new Error(`Run #${runId} (seed ${seed}) failed to spawn: ${err.message}`)); }); }); } function buildSummaryCsv(results: WorkerResult[]): string { const interconnectionKeys = results[0]?.systemInterconnections.connections.map( c => `interconnection_${c.from}_${c.to}`.replace(/\s+/g, ''), ) ?? []; const headers = [ 'runId', 'seed', 'passed', 'wallTimeMs', 'finalEra', 'finalMoney', 'finalRevenue', 'finalTotalRevenue', 'finalCapability', 'finalReputation', 'finalSubscribers', 'finalDevelopers', 'finalHeadcount', 'finalResearchCount', 'finalModelsDeployed', 'revenueStreamDiversity', 'featureUtilization_research', 'featureUtilization_infrastructure', 'featureUtilization_revenue', 'featureUtilization_talent', 'featureUtilization_model', 'featureUtilization_funding', 'interconnection_overall', ...interconnectionKeys, 'eraTransition_scaleup', 'eraTransition_bigtech', 'eraTransition_agi', 'bankruptcyRisks', 'sanityErrors', 'failureReasons', 'duration_startup', 'duration_scaleup', 'duration_bigtech', 'duration_agi', 'bottleneck_scaleup', 'bottleneck_bigtech', 'bottleneck_agi', 'servingMeanUtil', 'servingPctOverloaded', 'servingPctUnderloaded', 'servingPeakUtil', 'cashMinAmount', 'cashMinTick', 'cashPeakAmount', 'cashPeakTick', 'lateGameRevenueGrowthRate', 'unusedFeatures', ]; const rows = results.map(r => { const fm = r.finalMetrics; const fu = r.featureUtilization; const ic = r.systemInterconnections; const eraMap: Record = { scaleup: '', bigtech: '', agi: '', }; for (const t of r.eraTransitions) { if (t.to === 'scaleup') eraMap.scaleup = t.tick; if (t.to === 'bigtech') eraMap.bigtech = t.tick; if (t.to === 'agi') eraMap.agi = t.tick; } const icScores = ic.connections.map(c => c.score); const durationMap: Record = { startup: '', scaleup: '', bigtech: '', agi: '' }; const bottleneckMap: Record = { scaleup: '', bigtech: '', agi: '' }; for (const es of r.perEraSummary) { durationMap[es.era] = es.durationTicks; if (es.bottleneckAtExit) bottleneckMap[es.era] = es.bottleneckAtExit; } return [ r.runId, r.seed, r.passed ? 1 : 0, r.wallTimeMs, fm?.era ?? '', fm?.money ?? '', fm?.revenue ?? '', fm?.totalRevenue ?? '', fm?.bestModelCapability ?? '', fm?.reputation ?? '', fm?.subscribers ?? '', fm?.developers ?? '', fm?.headcount ?? '', fm?.researchCount ?? '', fm?.modelsDeployed ?? '', fu.revenueStreamDiversity, fu.coverageByCategory['research']?.percent ?? '', fu.coverageByCategory['infrastructure']?.percent ?? '', fu.coverageByCategory['revenue']?.percent ?? '', fu.coverageByCategory['talent']?.percent ?? '', fu.coverageByCategory['model']?.percent ?? '', fu.coverageByCategory['funding']?.percent ?? '', ic.overallScore, ...icScores, eraMap.scaleup, eraMap.bigtech, eraMap.agi, r.cashFlow.bankruptcyRisks, r.sanityChecks.errorCount, `"${r.failureReasons.join('; ').replace(/"/g, '""')}"`, durationMap.startup, durationMap.scaleup, durationMap.bigtech, durationMap.agi, bottleneckMap.scaleup, bottleneckMap.bigtech, bottleneckMap.agi, r.serving.meanUtilization, r.serving.pctOverloaded, r.serving.pctUnderloaded, r.serving.peakUtilization, r.cashFlow.minCash.amount, r.cashFlow.minCash.tick, r.cashFlow.peakCash.amount, r.cashFlow.peakCash.tick, r.lateGameRevenueGrowthRate, `"${fu.unusedFeatures.join(';').replace(/"/g, '""')}"`, ].join(','); }); return [headers.join(','), ...rows].join('\n'); } function buildTimeseriesCsv(results: WorkerResult[]): string { if (results.length === 0 || results[0].metrics.length === 0) return ''; const sampleKeys = Object.keys(results[0].metrics[0]).filter(k => k !== 'completedResearchIds') as (keyof SimulationMetrics)[]; const headers = ['runId', 'seed', ...sampleKeys]; const rows: string[] = []; for (const r of results) { for (const m of r.metrics) { const values = sampleKeys.map(k => { const v = m[k]; return typeof v === 'number' ? v : String(v); }); rows.push([r.runId, r.seed, ...values].join(',')); } } return [headers.join(','), ...rows].join('\n'); } async function main() { const seeds = deriveSeeds(totalRuns, baseSeed); const startTime = Date.now(); console.log(`=== Multi-Run Simulation ===`); console.log(`Runs: ${totalRuns} | Parallel: ${parallel} | Strategy: ${strategyName} | Ticks: ${totalTicks.toLocaleString()}`); console.log(`Seeds: [${seeds.join(', ')}]`); if (baseSeed !== null) console.log(`Base seed: ${baseSeed} (deterministic)`); console.log(''); const results: WorkerResult[] = []; let completed = 0; const pending = seeds.map((seed, i) => ({ runId: i + 1, seed })); const active = new Set>(); async function runOne(runId: number, seed: number): Promise { try { const result = await spawnWorker(runId, seed); results.push(result); completed++; const status = result.passed ? 'PASSED' : 'FAILED'; const reasons = !result.passed && result.failureReasons.length > 0 ? ` [${result.failureReasons.join('; ')}]` : ''; console.log(`[${completed}/${totalRuns}] Run #${runId} (seed ${seed}) completed in ${(result.wallTimeMs / 1000).toFixed(1)}s — ${status}${reasons}`); } catch (e) { completed++; console.error(`[${completed}/${totalRuns}] Run #${runId} (seed ${seed}) ERROR: ${(e as Error).message}`); } } for (const job of pending) { if (active.size >= parallel) { await Promise.race(active); } const p = runOne(job.runId, job.seed).then(() => { active.delete(p); }); active.add(p); } await Promise.all(active); results.sort((a, b) => a.runId - b.runId); const totalTime = ((Date.now() - startTime) / 1000).toFixed(1); const passCount = results.filter(r => r.passed).length; console.log(''); console.log(`=== Complete ===`); console.log(`${passCount}/${results.length} passed | Total wall time: ${totalTime}s`); const summaryCsv = buildSummaryCsv(results); const summaryPath = pathResolve(outDir, 'multirun-summary.csv'); writeFileSync(summaryPath, summaryCsv); console.log(`Summary CSV: ${summaryPath}`); if (!noTimeseries) { const tsCsv = buildTimeseriesCsv(results); const tsPath = pathResolve(outDir, 'multirun-timeseries.csv'); writeFileSync(tsPath, tsCsv); console.log(`Time-series CSV: ${tsPath}`); } const failedSeeds = results.filter(r => !r.passed).map(r => r.seed); if (failedSeeds.length > 0) { console.log(`\nFailed seeds (for reproduction): ${failedSeeds.join(', ')}`); } } main().catch(e => { console.error(e); process.exit(1); });