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,79 @@
|
||||
import type { GameState, RackSkuId } from '@ai-tycoon/shared';
|
||||
import { RACK_SKU_CONFIGS } from '@ai-tycoon/shared';
|
||||
import type { SimulationMetrics } from '../strategies/types';
|
||||
|
||||
export interface DeadZone {
|
||||
startTick: number;
|
||||
endTick: number;
|
||||
durationTicks: number;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function getCheapestSkuCost(state: GameState): number {
|
||||
const era = state.meta.currentEra;
|
||||
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
const eraIdx = eraOrder.indexOf(era);
|
||||
|
||||
let cheapest = Infinity;
|
||||
for (const [, sku] of Object.entries(RACK_SKU_CONFIGS)) {
|
||||
if (eraOrder.indexOf(sku.era) <= eraIdx) {
|
||||
cheapest = Math.min(cheapest, sku.baseCost);
|
||||
}
|
||||
}
|
||||
return cheapest === Infinity ? 100_000 : cheapest;
|
||||
}
|
||||
|
||||
export function detectDeadZones(
|
||||
metrics: SimulationMetrics[],
|
||||
cheapestSkuCost: number,
|
||||
windowSize = 10,
|
||||
revenueTolerance = 0.02,
|
||||
capabilityTolerance = 0.02,
|
||||
): DeadZone[] {
|
||||
const zones: DeadZone[] = [];
|
||||
let zoneStart: number | null = null;
|
||||
|
||||
for (let i = windowSize; i < metrics.length; i++) {
|
||||
const current = metrics[i];
|
||||
const past = metrics[i - windowSize];
|
||||
|
||||
const revFlat = past.revenue > 0
|
||||
? Math.abs(current.revenue - past.revenue) / past.revenue < revenueTolerance
|
||||
: current.revenue === 0;
|
||||
|
||||
const capFlat = past.bestModelCapability > 0
|
||||
? Math.abs(current.bestModelCapability - past.bestModelCapability) / past.bestModelCapability < capabilityTolerance
|
||||
: current.bestModelCapability === 0;
|
||||
|
||||
const isStuck = revFlat && capFlat && current.money < cheapestSkuCost * 2;
|
||||
|
||||
if (isStuck) {
|
||||
if (zoneStart === null) zoneStart = past.tick;
|
||||
} else {
|
||||
if (zoneStart !== null) {
|
||||
const endTick = metrics[i - 1].tick;
|
||||
zones.push({
|
||||
startTick: zoneStart,
|
||||
endTick,
|
||||
durationTicks: endTick - zoneStart,
|
||||
description: 'revenue flat, no affordable upgrades',
|
||||
});
|
||||
zoneStart = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (zoneStart !== null) {
|
||||
const endTick = metrics[metrics.length - 1].tick;
|
||||
zones.push({
|
||||
startTick: zoneStart,
|
||||
endTick,
|
||||
durationTicks: endTick - zoneStart,
|
||||
description: 'revenue flat, no affordable upgrades (ongoing)',
|
||||
});
|
||||
}
|
||||
|
||||
return zones;
|
||||
}
|
||||
|
||||
export { getCheapestSkuCost };
|
||||
Reference in New Issue
Block a user