102e05c8ba
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>
107 lines
4.3 KiB
TypeScript
107 lines
4.3 KiB
TypeScript
import type { GameState, CompetitorState, Competitor } from '@ai-tycoon/shared';
|
|
import {
|
|
COMPETITOR_PRODUCT_THRESHOLDS,
|
|
COMPETITOR_CATCHUP_SHARE_THRESHOLD,
|
|
COMPETITOR_CATCHUP_PRICE_CUT,
|
|
FRESHNESS_DECAY_RATE,
|
|
} from '@ai-tycoon/shared';
|
|
|
|
function updateCompetitorProducts(rival: Competitor): Competitor['products'] {
|
|
const cap = rival.estimatedCapability;
|
|
const p = rival.products;
|
|
const pricing = rival.pricingStrategy;
|
|
|
|
const baseChatPrice = 20 * (1 + pricing.premiumPositioning * 0.5 - pricing.aggressiveness * 0.3);
|
|
const baseApiOut = 3.0 * (1 + pricing.premiumPositioning * 0.3 - pricing.aggressiveness * 0.4);
|
|
|
|
return {
|
|
hasFreeTier: cap >= COMPETITOR_PRODUCT_THRESHOLDS.freeTierAndChat,
|
|
chatPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.freeTierAndChat
|
|
? Math.max(5, baseChatPrice) : p.chatPrice,
|
|
apiInputPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
|
|
? Math.max(0.2, baseApiOut * 0.33) : p.apiInputPrice,
|
|
apiOutputPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
|
|
? Math.max(0.5, baseApiOut) : p.apiOutputPrice,
|
|
hasCodeAssistant: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant,
|
|
codeAssistantPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.apiAndCodeAssistant
|
|
? Math.max(10, 20 * (1 - pricing.aggressiveness * 0.3)) : 0,
|
|
hasAgentsPlatform: cap >= COMPETITOR_PRODUCT_THRESHOLDS.agentsPlatform,
|
|
agentsPlatformPrice: cap >= COMPETITOR_PRODUCT_THRESHOLDS.agentsPlatform
|
|
? Math.max(50, 100 * (1 - pricing.aggressiveness * 0.2)) : 0,
|
|
};
|
|
}
|
|
|
|
export function processCompetitors(state: GameState): CompetitorState {
|
|
const tick = state.meta.tickCount;
|
|
const rivals = state.competitors.rivals.map(rival => {
|
|
if (rival.status !== 'active') return rival;
|
|
|
|
const updated = { ...rival };
|
|
|
|
updated.modelFreshness = Math.max(0, updated.modelFreshness - FRESHNESS_DECAY_RATE);
|
|
|
|
const ecoGrowth = rival.personality.openSourceTendency * 0.1 + rival.personality.marketingFocus * 0.05;
|
|
updated.developerEcosystemScore = Math.min(100,
|
|
updated.developerEcosystemScore + ecoGrowth * 0.01,
|
|
);
|
|
|
|
const shares = Object.values(updated.marketShares);
|
|
let minShare = shares[0];
|
|
for (let i = 1; i < shares.length; i++) {
|
|
if (shares[i] < minShare) minShare = shares[i];
|
|
}
|
|
if (minShare < COMPETITOR_CATCHUP_SHARE_THRESHOLD) {
|
|
updated.pricingStrategy = {
|
|
...updated.pricingStrategy,
|
|
aggressiveness: Math.min(1, updated.pricingStrategy.aggressiveness + COMPETITOR_CATCHUP_PRICE_CUT * 0.1),
|
|
};
|
|
}
|
|
|
|
if (tick < rival.nextMilestoneAtTick) {
|
|
updated.products = updateCompetitorProducts(updated);
|
|
return updated;
|
|
}
|
|
|
|
const { personality } = rival;
|
|
const capGrowth = (2 + personality.researchFocus * 5 + personality.riskTolerance * 3) *
|
|
(1 + tick * 0.00005);
|
|
const revenueGrowth = rival.estimatedRevenue * (0.02 + personality.marketingFocus * 0.03);
|
|
const userGrowth = rival.estimatedUsers * (0.01 + personality.marketingFocus * 0.02);
|
|
|
|
updated.estimatedCapability = Math.min(95, rival.estimatedCapability + capGrowth);
|
|
updated.estimatedRevenue = rival.estimatedRevenue + revenueGrowth + 50;
|
|
updated.estimatedUsers = Math.floor(rival.estimatedUsers + userGrowth + 100);
|
|
|
|
const repChange = personality.safetyFocus > 0.6
|
|
? 1
|
|
: personality.riskTolerance > 0.7 ? -1 : 0;
|
|
updated.reputation = Math.min(100, Math.max(0, rival.reputation + repChange));
|
|
|
|
const modelNames = [
|
|
'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon',
|
|
'Nova', 'Quantum', 'Nexus', 'Apex', 'Zenith',
|
|
];
|
|
const modelIdx = Math.floor(updated.estimatedCapability / 10);
|
|
updated.latestModelName = `${rival.name.split(' ')[0]}-${modelNames[Math.min(modelIdx, modelNames.length - 1)]}`;
|
|
|
|
updated.modelFreshness = 1.0;
|
|
updated.lastModelReleaseTick = tick;
|
|
|
|
updated.products = updateCompetitorProducts(updated);
|
|
|
|
const milestoneInterval = 200 + Math.floor(Math.random() * 200);
|
|
updated.nextMilestoneAtTick = tick + milestoneInterval;
|
|
|
|
return updated;
|
|
});
|
|
|
|
let industryBenchmark = state.models.bestDeployedModelScore;
|
|
for (const r of rivals) {
|
|
if (r.status === 'active' && r.estimatedCapability > industryBenchmark) {
|
|
industryBenchmark = r.estimatedCapability;
|
|
}
|
|
}
|
|
|
|
return { rivals, industryBenchmark };
|
|
}
|