Add game-simulation package with multi-run balance testing, fix stalled-pipeline trap
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:
@@ -0,0 +1,113 @@
|
||||
import type { SimulationMetrics } from '../strategies/types';
|
||||
|
||||
export type TrackedMetric = 'revenue' | 'subscribers' | 'developers'
|
||||
| 'bestModelCapability' | 'reputation' | 'totalFlops';
|
||||
|
||||
const TRACKED_METRICS: TrackedMetric[] = [
|
||||
'revenue', 'subscribers', 'developers',
|
||||
'bestModelCapability', 'reputation', 'totalFlops',
|
||||
];
|
||||
|
||||
export interface StagnationAlert {
|
||||
metric: TrackedMetric;
|
||||
startTick: number;
|
||||
endTick: number;
|
||||
durationTicks: number;
|
||||
stuckValue: number;
|
||||
era: string;
|
||||
}
|
||||
|
||||
export interface ExponentialAlert {
|
||||
metric: TrackedMetric;
|
||||
tick: number;
|
||||
growthRate: number;
|
||||
consecutiveSamples: number;
|
||||
}
|
||||
|
||||
export interface GrowthRateResult {
|
||||
stagnations: StagnationAlert[];
|
||||
exponentialAlerts: ExponentialAlert[];
|
||||
}
|
||||
|
||||
function getMetricValue(m: SimulationMetrics, metric: TrackedMetric): number {
|
||||
return m[metric] as number;
|
||||
}
|
||||
|
||||
export function analyzeGrowthRates(
|
||||
metrics: SimulationMetrics[],
|
||||
stagnationWindowSamples = 20,
|
||||
stagnationThreshold = 0.01,
|
||||
): GrowthRateResult {
|
||||
const stagnations: StagnationAlert[] = [];
|
||||
const exponentialAlerts: ExponentialAlert[] = [];
|
||||
|
||||
for (const metric of TRACKED_METRICS) {
|
||||
const growthRates: number[] = [];
|
||||
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
const prev = getMetricValue(metrics[i - 1], metric);
|
||||
const curr = getMetricValue(metrics[i], metric);
|
||||
if (prev > 0) {
|
||||
growthRates.push((curr - prev) / prev);
|
||||
} else {
|
||||
growthRates.push(curr > 0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
let stagnationStart: number | null = null;
|
||||
let flatCount = 0;
|
||||
|
||||
for (let i = 0; i < growthRates.length; i++) {
|
||||
if (Math.abs(growthRates[i]) < stagnationThreshold) {
|
||||
if (stagnationStart === null) stagnationStart = i;
|
||||
flatCount++;
|
||||
} else {
|
||||
if (flatCount >= stagnationWindowSamples && stagnationStart !== null) {
|
||||
const startIdx = stagnationStart + 1;
|
||||
const endIdx = i + 1;
|
||||
stagnations.push({
|
||||
metric,
|
||||
startTick: metrics[startIdx]?.tick ?? 0,
|
||||
endTick: metrics[endIdx]?.tick ?? metrics[metrics.length - 1]?.tick ?? 0,
|
||||
durationTicks: (metrics[endIdx]?.tick ?? 0) - (metrics[startIdx]?.tick ?? 0),
|
||||
stuckValue: getMetricValue(metrics[startIdx], metric),
|
||||
era: metrics[startIdx]?.era ?? 'unknown',
|
||||
});
|
||||
}
|
||||
stagnationStart = null;
|
||||
flatCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (flatCount >= stagnationWindowSamples && stagnationStart !== null) {
|
||||
const startIdx = stagnationStart + 1;
|
||||
stagnations.push({
|
||||
metric,
|
||||
startTick: metrics[startIdx]?.tick ?? 0,
|
||||
endTick: metrics[metrics.length - 1]?.tick ?? 0,
|
||||
durationTicks: (metrics[metrics.length - 1]?.tick ?? 0) - (metrics[startIdx]?.tick ?? 0),
|
||||
stuckValue: getMetricValue(metrics[startIdx], metric),
|
||||
era: metrics[startIdx]?.era ?? 'unknown',
|
||||
});
|
||||
}
|
||||
|
||||
let expCount = 0;
|
||||
for (let i = 0; i < growthRates.length; i++) {
|
||||
if (growthRates[i] > 0.10) {
|
||||
expCount++;
|
||||
if (expCount >= 5) {
|
||||
exponentialAlerts.push({
|
||||
metric,
|
||||
tick: metrics[i + 1]?.tick ?? 0,
|
||||
growthRate: growthRates[i],
|
||||
consecutiveSamples: expCount,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
expCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { stagnations, exponentialAlerts };
|
||||
}
|
||||
Reference in New Issue
Block a user