diff --git a/packages/game-simulation/src/multirun.ts b/packages/game-simulation/src/multirun.ts index cfcc05c..524ceaa 100644 --- a/packages/game-simulation/src/multirun.ts +++ b/packages/game-simulation/src/multirun.ts @@ -1,4 +1,4 @@ -import { exec } from 'node:child_process'; +import { spawn } from 'node:child_process'; import { writeFileSync } from 'node:fs'; import { resolve as pathResolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -74,20 +74,30 @@ interface WorkerResult { 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 cmd = `npx tsx "${workerPath}" --strategy ${strategyName} --ticks ${totalTicks} --seed ${seed} --run-id ${runId}`; - exec(cmd, { maxBuffer: 200 * 1024 * 1024 }, (error, stdout, stderr) => { - if (stderr) process.stderr.write(stderr); - if (error) { - reject(new Error(`Run #${runId} (seed ${seed}) failed: ${error.message}`)); + 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 { - const result = JSON.parse(stdout) as WorkerResult; - resolve(result); + 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}`)); + }); }); } diff --git a/packages/game-simulation/src/runner.ts b/packages/game-simulation/src/runner.ts index 881b3ab..471b5b9 100644 --- a/packages/game-simulation/src/runner.ts +++ b/packages/game-simulation/src/runner.ts @@ -14,6 +14,7 @@ export interface SimulationConfig { seed?: number; verbose?: boolean; silent?: boolean; + onProgress?: (tick: number, totalTicks: number, era: string) => void; } export interface EraTransition { @@ -139,8 +140,11 @@ export function runSimulation(config: SimulationConfig): SimulationResult { allMetrics.push(collectMetrics(state)); } - if (!config.silent && tick % progressInterval === 0) { - printProgress(tick, config.totalTicks, state, startTime); + if (tick % progressInterval === 0) { + if (!config.silent) { + printProgress(tick, config.totalTicks, state, startTime); + } + config.onProgress?.(tick, config.totalTicks, state.meta.currentEra); } } diff --git a/packages/game-simulation/src/worker.ts b/packages/game-simulation/src/worker.ts index 481627c..f4f9d22 100644 --- a/packages/game-simulation/src/worker.ts +++ b/packages/game-simulation/src/worker.ts @@ -23,7 +23,18 @@ const strategy = strategyName === 'random' ? new RandomStrategy() process.stderr.write(`[Run #${runId}] Starting (seed ${seed}, ${totalTicks} ticks, ${strategyName})...\n`); -const result = runSimulation({ totalTicks, decisionInterval, strategy, seed, verbose: false, silent: true }); +const result = runSimulation({ + totalTicks, + decisionInterval, + strategy, + seed, + verbose: false, + silent: true, + onProgress: (tick, total, era) => { + const pct = ((tick / total) * 100).toFixed(0); + process.stderr.write(`[Run #${runId}] ${pct}% (tick ${tick.toLocaleString()}/${total.toLocaleString()}) — ${era}\n`); + }, +}); const report = generateJsonReport(result, { totalTicks, decisionInterval, strategy, seed }); const output = {