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,357 @@
|
||||
import type { SimulationMetrics } from '../strategies/types';
|
||||
import type { TickNotification } from '@ai-tycoon/game-engine';
|
||||
|
||||
export interface SystemConnection {
|
||||
from: string;
|
||||
to: string;
|
||||
score: number;
|
||||
evidence: string;
|
||||
events: number;
|
||||
}
|
||||
|
||||
export interface InterconnectionResult {
|
||||
connections: SystemConnection[];
|
||||
overallScore: number;
|
||||
weakLinks: SystemConnection[];
|
||||
deadLinks: SystemConnection[];
|
||||
}
|
||||
|
||||
function findMetricAtTick(metrics: SimulationMetrics[], tick: number): SimulationMetrics | undefined {
|
||||
for (let i = metrics.length - 1; i >= 0; i--) {
|
||||
if (metrics[i].tick <= tick) return metrics[i];
|
||||
}
|
||||
return metrics[0];
|
||||
}
|
||||
|
||||
function findMetricAfterTick(metrics: SimulationMetrics[], tick: number, windowTicks: number): SimulationMetrics | undefined {
|
||||
const target = tick + windowTicks;
|
||||
for (const m of metrics) {
|
||||
if (m.tick >= target) return m;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function measureDelta(
|
||||
metrics: SimulationMetrics[],
|
||||
eventTicks: number[],
|
||||
getter: (m: SimulationMetrics) => number,
|
||||
windowTicks = 300,
|
||||
): { totalDelta: number; events: number } {
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of eventTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, windowTicks);
|
||||
if (before && after) {
|
||||
totalDelta += after[getter.name as keyof SimulationMetrics] !== undefined
|
||||
? getter(after) - getter(before)
|
||||
: 0;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
return { totalDelta, events };
|
||||
}
|
||||
|
||||
function scoreFromDelta(totalDelta: number, events: number, scale: number): number {
|
||||
if (events === 0) return 0;
|
||||
const avgDelta = totalDelta / events;
|
||||
return Math.min(10, Math.max(0, Math.round((avgDelta / scale) * 10)));
|
||||
}
|
||||
|
||||
function detectResearchCompletionTicks(metrics: SimulationMetrics[]): number[] {
|
||||
const ticks: number[] = [];
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
if (metrics[i].researchCount > metrics[i - 1].researchCount) {
|
||||
ticks.push(metrics[i].tick);
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
function detectModelDeploymentTicks(metrics: SimulationMetrics[]): number[] {
|
||||
const ticks: number[] = [];
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
if (metrics[i].modelsDeployed > metrics[i - 1].modelsDeployed) {
|
||||
ticks.push(metrics[i].tick);
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
function detectFundingTicks(metrics: SimulationMetrics[]): number[] {
|
||||
const ticks: number[] = [];
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
if (metrics[i].fundingRoundsCompleted > metrics[i - 1].fundingRoundsCompleted) {
|
||||
ticks.push(metrics[i].tick);
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
function detectHiringSpikes(metrics: SimulationMetrics[]): number[] {
|
||||
const ticks: number[] = [];
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
if (metrics[i].headcount - metrics[i - 1].headcount >= 3) {
|
||||
ticks.push(metrics[i].tick);
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
function detectComputeGrowthTicks(metrics: SimulationMetrics[]): number[] {
|
||||
const ticks: number[] = [];
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
const prev = metrics[i - 1].totalFlops;
|
||||
const curr = metrics[i].totalFlops;
|
||||
if (prev > 0 && (curr - prev) / prev > 0.1) {
|
||||
ticks.push(metrics[i].tick);
|
||||
}
|
||||
}
|
||||
return ticks;
|
||||
}
|
||||
|
||||
export function analyzeSystemInterconnections(
|
||||
metrics: SimulationMetrics[],
|
||||
_notifications: TickNotification[],
|
||||
): InterconnectionResult {
|
||||
const connections: SystemConnection[] = [];
|
||||
|
||||
if (metrics.length < 10) {
|
||||
return { connections: [], overallScore: 0, weakLinks: [], deadLinks: [] };
|
||||
}
|
||||
|
||||
const researchTicks = detectResearchCompletionTicks(metrics);
|
||||
const deployTicks = detectModelDeploymentTicks(metrics);
|
||||
const fundingTicks = detectFundingTicks(metrics);
|
||||
const hiringTicks = detectHiringSpikes(metrics);
|
||||
const computeGrowthTicks = detectComputeGrowthTicks(metrics);
|
||||
|
||||
// Research → Capability
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of researchTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 600);
|
||||
if (before && after) {
|
||||
totalDelta += after.bestModelCapability - before.bestModelCapability;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 ? scoreFromDelta(totalDelta, events, 5) : 0;
|
||||
connections.push({
|
||||
from: 'Research', to: 'Model Capability', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} research completions, avg capability delta: ${(totalDelta / events).toFixed(1)}`
|
||||
: 'No research completions observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Research → Infrastructure
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of researchTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 600);
|
||||
if (before && after) {
|
||||
totalDelta += after.totalFlops - before.totalFlops;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 && totalDelta > 0 ? Math.min(10, Math.round((totalDelta / events / 100) * 10)) : 0;
|
||||
connections.push({
|
||||
from: 'Research', to: 'Infrastructure', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} research completions, avg FLOPS delta: ${(totalDelta / events).toFixed(0)}`
|
||||
: 'No research completions observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Talent → Training (hiring → more training pipelines or faster progress)
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of hiringTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 300);
|
||||
if (before && after) {
|
||||
totalDelta += after.bestModelCapability - before.bestModelCapability;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 ? scoreFromDelta(totalDelta, events, 3) : 0;
|
||||
connections.push({
|
||||
from: 'Talent', to: 'Training', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} hiring spikes, avg capability change: ${(totalDelta / events).toFixed(1)}`
|
||||
: 'No significant hiring events observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Talent → Enterprise
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of hiringTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 600);
|
||||
if (before && after) {
|
||||
totalDelta += after.enterpriseContracts - before.enterpriseContracts;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 && totalDelta > 0 ? Math.min(10, Math.round((totalDelta / events) * 5)) : 0;
|
||||
connections.push({
|
||||
from: 'Talent', to: 'Enterprise', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} hiring spikes, avg enterprise contract delta: ${(totalDelta / events).toFixed(1)}`
|
||||
: 'No hiring events observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Infrastructure → Revenue
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of computeGrowthTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 300);
|
||||
if (before && after && before.revenue > 0) {
|
||||
totalDelta += (after.revenue - before.revenue) / before.revenue;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 ? Math.min(10, Math.max(0, Math.round((totalDelta / events) * 20))) : 0;
|
||||
connections.push({
|
||||
from: 'Infrastructure', to: 'Revenue', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} compute growth events, avg revenue growth: ${((totalDelta / events) * 100).toFixed(1)}%`
|
||||
: 'No compute growth events observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Models → Revenue
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of deployTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 300);
|
||||
if (before && after) {
|
||||
const revDelta = before.revenue > 0
|
||||
? (after.revenue - before.revenue) / before.revenue
|
||||
: (after.revenue > 0 ? 1 : 0);
|
||||
totalDelta += revDelta;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 ? Math.min(10, Math.max(0, Math.round((totalDelta / events) * 10))) : 0;
|
||||
connections.push({
|
||||
from: 'Models', to: 'Revenue', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} model deployments, avg revenue change: ${((totalDelta / events) * 100).toFixed(1)}%`
|
||||
: 'No model deployments observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Models → Enterprise
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of deployTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 600);
|
||||
if (before && after) {
|
||||
totalDelta += after.enterpriseContracts - before.enterpriseContracts;
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 && totalDelta > 0 ? Math.min(10, Math.round((totalDelta / events) * 5)) : 0;
|
||||
connections.push({
|
||||
from: 'Models', to: 'Enterprise', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} deployments, avg enterprise contract delta: ${(totalDelta / events).toFixed(1)}`
|
||||
: 'No model deployments observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Reputation → Era gates
|
||||
{
|
||||
let score = 0;
|
||||
const eraChanges = [];
|
||||
for (let i = 1; i < metrics.length; i++) {
|
||||
if (metrics[i].era !== metrics[i - 1].era) {
|
||||
eraChanges.push({ tick: metrics[i].tick, repBefore: metrics[i - 1].reputation });
|
||||
}
|
||||
}
|
||||
if (eraChanges.length > 0) {
|
||||
let bindingCount = 0;
|
||||
for (const ec of eraChanges) {
|
||||
const before = findMetricAtTick(metrics, ec.tick - 120);
|
||||
if (before && (ec.repBefore - before.reputation) > 1) bindingCount++;
|
||||
}
|
||||
score = Math.min(10, Math.round((bindingCount / eraChanges.length) * 10));
|
||||
}
|
||||
connections.push({
|
||||
from: 'Reputation', to: 'Era Gates', score, events: eraChanges.length,
|
||||
evidence: eraChanges.length > 0
|
||||
? `${eraChanges.length} era transitions observed`
|
||||
: 'No era transitions observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Funding → Growth
|
||||
{
|
||||
let totalDelta = 0;
|
||||
let events = 0;
|
||||
for (const tick of fundingTicks) {
|
||||
const before = findMetricAtTick(metrics, tick);
|
||||
const after = findMetricAfterTick(metrics, tick, 600);
|
||||
if (before && after) {
|
||||
const growthBefore = before.revenue;
|
||||
const growthAfter = after.revenue;
|
||||
totalDelta += growthBefore > 0
|
||||
? (growthAfter - growthBefore) / growthBefore
|
||||
: (growthAfter > 0 ? 1 : 0);
|
||||
events++;
|
||||
}
|
||||
}
|
||||
const score = events > 0 ? Math.min(10, Math.max(0, Math.round((totalDelta / events) * 10))) : 0;
|
||||
connections.push({
|
||||
from: 'Funding', to: 'Growth', score, events,
|
||||
evidence: events > 0
|
||||
? `${events} funding rounds, avg revenue growth: ${((totalDelta / events) * 100).toFixed(1)}%`
|
||||
: 'No funding rounds observed',
|
||||
});
|
||||
}
|
||||
|
||||
// Compute → Serving quality (utilization tracking)
|
||||
{
|
||||
let wellUtilizedCount = 0;
|
||||
let totalSamples = 0;
|
||||
for (const m of metrics) {
|
||||
if (m.tokensPerSecondCapacity > 0) {
|
||||
totalSamples++;
|
||||
const util = m.inferenceUtilization;
|
||||
if (util > 0.2 && util < 0.95) wellUtilizedCount++;
|
||||
}
|
||||
}
|
||||
const score = totalSamples > 0 ? Math.min(10, Math.round((wellUtilizedCount / totalSamples) * 10)) : 0;
|
||||
connections.push({
|
||||
from: 'Compute', to: 'Serving', score, events: totalSamples,
|
||||
evidence: totalSamples > 0
|
||||
? `${wellUtilizedCount}/${totalSamples} samples with healthy utilization (20-95%)`
|
||||
: 'No compute capacity observed',
|
||||
});
|
||||
}
|
||||
|
||||
const overallScore = connections.length > 0
|
||||
? Math.round(connections.reduce((sum, c) => sum + c.score, 0) / connections.length * 10) / 10
|
||||
: 0;
|
||||
|
||||
const weakLinks = connections.filter(c => c.score > 0 && c.score < 3);
|
||||
const deadLinks = connections.filter(c => c.score === 0);
|
||||
|
||||
return { connections, overallScore, weakLinks, deadLinks };
|
||||
}
|
||||
Reference in New Issue
Block a user