Files
AIHostingTycoon/packages/game-engine/src/systems/competitorSystem.ts
T
josh 102e05c8ba
Balance Check / balance-simulation (push) Failing after 11m32s
Balance Check / multi-run-balance (push) Failing after 23m46s
CI / build-and-push (push) Successful in 1m20s
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>
2026-04-26 06:11:26 -04:00

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 };
}