416b6bfe8d
Research now costs money (drained per-tick) with ~2.5-3.5x longer durations by category. Early-game talent budget costs reduced via era multiplier (startup 0.2x → bigtech 1.0x). New seed-driven PersonaStrategy with 8 axes of variation for meaningful multi-run testing. CI multi-run switched from greedy to persona strategy. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { runSimulation } from './runner';
|
|
import { GreedyStrategy } from './strategies/greedy';
|
|
import { RandomStrategy } from './strategies/random';
|
|
import { PersonaStrategy } from './strategies/persona';
|
|
import { printConsoleReport, generateJsonReport } from './analysis/report';
|
|
import { writeFileSync } from 'node:fs';
|
|
import { resolve, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import type { SimulationMetrics } from './strategies/types';
|
|
|
|
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 strategyName = getArg('strategy', 'greedy');
|
|
const totalTicks = parseInt(getArg('ticks', '28800'), 10);
|
|
const decisionInterval = parseInt(getArg('interval', '60'), 10);
|
|
const seedStr = getArg('seed', '');
|
|
const seed = seedStr ? parseInt(seedStr, 10) : undefined;
|
|
const jsonOutput = hasFlag('json');
|
|
const verbose = hasFlag('verbose');
|
|
const csvOutput = hasFlag('csv');
|
|
|
|
const strategy = strategyName === 'random' ? new RandomStrategy()
|
|
: strategyName === 'persona' ? new PersonaStrategy(seed ?? 42)
|
|
: new GreedyStrategy();
|
|
|
|
console.log(`Running ${strategyName} simulation: ${totalTicks.toLocaleString()} ticks, interval ${decisionInterval}${seed !== undefined ? `, seed ${seed}` : ''}...`);
|
|
|
|
const result = runSimulation({ totalTicks, decisionInterval, strategy, seed, verbose });
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
if (csvOutput) {
|
|
const allMetrics = result.metrics;
|
|
if (allMetrics.length > 0) {
|
|
const scalarKeys = Object.keys(allMetrics[0]).filter(k => k !== 'completedResearchIds') as (keyof SimulationMetrics)[];
|
|
const headers = scalarKeys.join(',');
|
|
const rows = allMetrics.map(m =>
|
|
scalarKeys.map(k => {
|
|
const v = m[k];
|
|
return typeof v === 'number' ? v : String(v);
|
|
}).join(','),
|
|
);
|
|
const csv = [headers, ...rows].join('\n');
|
|
const csvPath = resolve(__dirname, '..', 'balance-metrics.csv');
|
|
writeFileSync(csvPath, csv);
|
|
console.log(`Metrics CSV written to ${csvPath}`);
|
|
}
|
|
}
|
|
|
|
if (jsonOutput) {
|
|
const report = generateJsonReport(result, { totalTicks, decisionInterval, strategy, seed });
|
|
|
|
const outPath = resolve(__dirname, '..', 'balance-report.json');
|
|
writeFileSync(outPath, JSON.stringify(report, null, 2));
|
|
console.log(`Report written to ${outPath}`);
|
|
|
|
printConsoleReport(result, { totalTicks, decisionInterval, strategy, seed }, verbose);
|
|
|
|
if (!report.passed) {
|
|
console.log('FAILED:');
|
|
for (const reason of report.failureReasons) {
|
|
console.log(` - ${reason}`);
|
|
}
|
|
process.exit(1);
|
|
} else {
|
|
console.log('PASSED: All balance checks within thresholds.');
|
|
}
|
|
} else {
|
|
printConsoleReport(result, { totalTicks, decisionInterval, strategy, seed }, verbose);
|
|
}
|