|
|
|
@@ -0,0 +1,599 @@
|
|
|
|
|
import type { GameState, Era, RackSkuId } from '@ai-tycoon/shared';
|
|
|
|
|
import {
|
|
|
|
|
RACK_SKU_CONFIGS, DC_TIER_CONFIGS, COOLING_ORDER,
|
|
|
|
|
PARAMETER_OPTIONS, DEFAULT_DATA_MIX,
|
|
|
|
|
MAX_CONCURRENT_TRAINING, PRETRAINING_BASE_TICKS,
|
|
|
|
|
CLUSTER_COST_CONFIG, LOCATION_CONFIGS, maxComputeRacks,
|
|
|
|
|
VRAM_REQUIREMENTS_BY_GENERATION,
|
|
|
|
|
} from '@ai-tycoon/shared';
|
|
|
|
|
import {
|
|
|
|
|
canRaiseFunding, getNextFundingRound, getAvailableResearch, TECH_TREE,
|
|
|
|
|
} from '@ai-tycoon/game-engine';
|
|
|
|
|
import * as actions from '../actions';
|
|
|
|
|
import type { Strategy, SimulationMetrics } from './types';
|
|
|
|
|
|
|
|
|
|
const ERA_ORDER: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
|
|
|
|
|
|
|
|
|
|
interface PersonaProfile {
|
|
|
|
|
riskTolerance: number;
|
|
|
|
|
researchFocus: number;
|
|
|
|
|
talentBias: { research: number; engineering: number; operations: number; sales: number };
|
|
|
|
|
modelAmbition: number;
|
|
|
|
|
pricingAggression: number;
|
|
|
|
|
infraStrategy: number;
|
|
|
|
|
expansionTiming: number;
|
|
|
|
|
safetyWeight: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hashDimension(seed: number, dimensionIndex: number): number {
|
|
|
|
|
let h = (seed + dimensionIndex * 0x9E3779B9) | 0;
|
|
|
|
|
h = Math.imul(h ^ (h >>> 16), 0x45D9F3B);
|
|
|
|
|
h = Math.imul(h ^ (h >>> 13), 0x45D9F3B);
|
|
|
|
|
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function lerp(min: number, max: number, t: number): number {
|
|
|
|
|
return min + (max - min) * t;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generatePersona(seed: number): PersonaProfile {
|
|
|
|
|
const riskTolerance = lerp(0.3, 1.0, hashDimension(seed, 0));
|
|
|
|
|
const researchFocus = Math.floor(hashDimension(seed, 1) * 5);
|
|
|
|
|
const modelAmbition = lerp(0.4, 1.0, hashDimension(seed, 2));
|
|
|
|
|
const pricingAggression = lerp(0.5, 2.0, hashDimension(seed, 3));
|
|
|
|
|
const infraStrategy = Math.floor(hashDimension(seed, 4) * 3);
|
|
|
|
|
const expansionTiming = lerp(0.5, 1.5, hashDimension(seed, 5));
|
|
|
|
|
const safetyWeight = lerp(0.3, 1.0, hashDimension(seed, 6));
|
|
|
|
|
|
|
|
|
|
const rawWeights = [
|
|
|
|
|
hashDimension(seed, 7),
|
|
|
|
|
hashDimension(seed, 8),
|
|
|
|
|
hashDimension(seed, 9),
|
|
|
|
|
hashDimension(seed, 10),
|
|
|
|
|
];
|
|
|
|
|
const floor = 0.1;
|
|
|
|
|
const floored = rawWeights.map(w => floor + w * (1 - 4 * floor));
|
|
|
|
|
const sum = floored.reduce((a, b) => a + b, 0);
|
|
|
|
|
const normalized = floored.map(w => w / sum);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
riskTolerance,
|
|
|
|
|
researchFocus,
|
|
|
|
|
talentBias: {
|
|
|
|
|
research: normalized[0],
|
|
|
|
|
engineering: normalized[1],
|
|
|
|
|
operations: normalized[2],
|
|
|
|
|
sales: normalized[3],
|
|
|
|
|
},
|
|
|
|
|
modelAmbition,
|
|
|
|
|
pricingAggression,
|
|
|
|
|
infraStrategy,
|
|
|
|
|
expansionTiming,
|
|
|
|
|
safetyWeight,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BASE_RESEARCH_PRIORITY: Record<string, number> = {
|
|
|
|
|
'advanced-cooling': 200,
|
|
|
|
|
'dc-engineering-ii': 190,
|
|
|
|
|
'advanced-gpu-arch': 180,
|
|
|
|
|
'alignment-research': 250,
|
|
|
|
|
'transformer-v2': 165,
|
|
|
|
|
'quantization': 160,
|
|
|
|
|
'data-pipeline': 155,
|
|
|
|
|
'developer-relations': 150,
|
|
|
|
|
'enterprise-sales': 175,
|
|
|
|
|
'redundancy-protocols': 140,
|
|
|
|
|
'quality-assurance': 130,
|
|
|
|
|
'liquid-cooling-tech': 120,
|
|
|
|
|
'next-gen-gpu': 115,
|
|
|
|
|
'distributed-training': 110,
|
|
|
|
|
'inference-optimization': 105,
|
|
|
|
|
'dc-engineering-iii': 100,
|
|
|
|
|
'code-generation': 170,
|
|
|
|
|
'reasoning-enhancement': 90,
|
|
|
|
|
'amd-ecosystem': 85,
|
|
|
|
|
'infiniband-networking': 80,
|
|
|
|
|
'distillation': 75,
|
|
|
|
|
'inference-specialization': 70,
|
|
|
|
|
'sdk-platform': 65,
|
|
|
|
|
'request-batching': 60,
|
|
|
|
|
'request-routing': 55,
|
|
|
|
|
'code-assistant-product': 168,
|
|
|
|
|
'creative-systems': 45,
|
|
|
|
|
'multimodal-fusion': 40,
|
|
|
|
|
'network-engineering-i': 35,
|
|
|
|
|
'rapid-deployment': 30,
|
|
|
|
|
'priority-queues': 25,
|
|
|
|
|
'interpretability': 180,
|
|
|
|
|
'immersion-cooling-tech': 18,
|
|
|
|
|
'frontier-compute': 16,
|
|
|
|
|
'dc-engineering-iv': 14,
|
|
|
|
|
'network-engineering-ii': 12,
|
|
|
|
|
'agentic-architecture': 88,
|
|
|
|
|
'constitutional-ai': 160,
|
|
|
|
|
'network-redundancy': 6,
|
|
|
|
|
'auto-scaling': 5,
|
|
|
|
|
'agents-platform-product': 86,
|
|
|
|
|
'network-fast-repair': 3,
|
|
|
|
|
'rack-scale-compute': 2,
|
|
|
|
|
'custom-silicon': 1,
|
|
|
|
|
'network-hot-standby': 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const INFRA_RESEARCH = new Set([
|
|
|
|
|
'advanced-cooling', 'dc-engineering-ii', 'dc-engineering-iii', 'dc-engineering-iv',
|
|
|
|
|
'liquid-cooling-tech', 'immersion-cooling-tech', 'redundancy-protocols',
|
|
|
|
|
'network-engineering-i', 'network-engineering-ii', 'network-redundancy',
|
|
|
|
|
'network-fast-repair', 'network-hot-standby', 'rapid-deployment',
|
|
|
|
|
'distributed-training', 'inference-optimization', 'quantization',
|
|
|
|
|
'infiniband-networking', 'rack-scale-compute', 'custom-silicon',
|
|
|
|
|
'frontier-compute', 'auto-scaling',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const SAFETY_RESEARCH = new Set([
|
|
|
|
|
'alignment-research', 'interpretability', 'constitutional-ai',
|
|
|
|
|
'quality-assurance', 'enterprise-sales',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const CAPABILITY_RESEARCH = new Set([
|
|
|
|
|
'transformer-v2', 'code-generation', 'reasoning-enhancement',
|
|
|
|
|
'creative-systems', 'multimodal-fusion', 'agentic-architecture',
|
|
|
|
|
'advanced-gpu-arch', 'next-gen-gpu', 'amd-ecosystem',
|
|
|
|
|
'distillation', 'inference-specialization',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const PRODUCT_RESEARCH = new Set([
|
|
|
|
|
'developer-relations', 'enterprise-sales', 'code-assistant-product',
|
|
|
|
|
'agents-platform-product', 'sdk-platform', 'request-batching',
|
|
|
|
|
'request-routing', 'priority-queues', 'data-pipeline',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
function buildResearchPriority(profile: PersonaProfile): Record<string, number> {
|
|
|
|
|
const priorities = { ...BASE_RESEARCH_PRIORITY };
|
|
|
|
|
|
|
|
|
|
const boostSets: [Set<string>, number][] = [
|
|
|
|
|
[INFRA_RESEARCH, 100],
|
|
|
|
|
[SAFETY_RESEARCH, 150],
|
|
|
|
|
[CAPABILITY_RESEARCH, 120],
|
|
|
|
|
[PRODUCT_RESEARCH, 130],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (profile.researchFocus <= 3) {
|
|
|
|
|
const [targetSet, boost] = boostSets[profile.researchFocus];
|
|
|
|
|
for (const id of targetSet) {
|
|
|
|
|
if (id in priorities) priorities[id] += boost;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// focus=4 is balanced — uses base priorities as-is
|
|
|
|
|
|
|
|
|
|
return priorities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const TALENT_TOTALS: Record<Era, number> = {
|
|
|
|
|
startup: 15,
|
|
|
|
|
scaleup: 32,
|
|
|
|
|
bigtech: 60,
|
|
|
|
|
agi: 116,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function getOperationalDCs(state: GameState) {
|
|
|
|
|
const results: { dcId: string; coolingType: string; rackSkuId: string | null }[] = [];
|
|
|
|
|
for (const cluster of state.infrastructure.clusters) {
|
|
|
|
|
for (const campus of cluster.campuses) {
|
|
|
|
|
for (const dc of campus.dataCenters) {
|
|
|
|
|
if (dc.status === 'operational') {
|
|
|
|
|
results.push({ dcId: dc.id, coolingType: dc.coolingType, rackSkuId: dc.rackSkuId });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class PersonaStrategy implements Strategy {
|
|
|
|
|
name: string;
|
|
|
|
|
private profile: PersonaProfile;
|
|
|
|
|
private researchPriority: Record<string, number>;
|
|
|
|
|
|
|
|
|
|
constructor(seed: number) {
|
|
|
|
|
this.profile = generatePersona(seed);
|
|
|
|
|
this.name = `persona-${seed}`;
|
|
|
|
|
this.researchPriority = buildResearchPriority(this.profile);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
decide(state: GameState, _metrics: SimulationMetrics[]): void {
|
|
|
|
|
this.tryRaiseFunding(state);
|
|
|
|
|
this.tryBuildInfrastructure(state);
|
|
|
|
|
this.tryDeployRacks(state);
|
|
|
|
|
this.tryDeployModels(state);
|
|
|
|
|
this.tryOpenSourceModel(state);
|
|
|
|
|
this.cancelStalledTraining(state);
|
|
|
|
|
this.tryStartTraining(state);
|
|
|
|
|
this.tryEnableRevenue(state);
|
|
|
|
|
this.tryStartResearch(state);
|
|
|
|
|
this.tryHireTalent(state);
|
|
|
|
|
this.tryUpgradeInfra(state);
|
|
|
|
|
this.tryExpandInfra(state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cashSafe(state: GameState, cost: number, baseRunway = 100): boolean {
|
|
|
|
|
const runway = Math.round(baseRunway * this.profile.riskTolerance);
|
|
|
|
|
return state.economy.money - cost > state.economy.expensesPerTick * runway;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getBestAffordableSku(state: GameState): RackSkuId | null {
|
|
|
|
|
const era = state.meta.currentEra;
|
|
|
|
|
const completed = state.research.completedResearch;
|
|
|
|
|
|
|
|
|
|
const eligible = (Object.entries(RACK_SKU_CONFIGS) as [RackSkuId, typeof RACK_SKU_CONFIGS[RackSkuId]][])
|
|
|
|
|
.filter(([, sku]) => {
|
|
|
|
|
if (ERA_ORDER.indexOf(era) < ERA_ORDER.indexOf(sku.era)) return false;
|
|
|
|
|
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => completed.includes(r))) return false;
|
|
|
|
|
if (state.economy.money < sku.baseCost) return false;
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const strat = this.profile.infraStrategy;
|
|
|
|
|
if (strat === 1) {
|
|
|
|
|
eligible.sort((a, b) => b[1].trainingFlops - a[1].trainingFlops);
|
|
|
|
|
} else if (strat === 2) {
|
|
|
|
|
eligible.sort((a, b) =>
|
|
|
|
|
(b[1].inferenceFlops / b[1].baseCost) - (a[1].inferenceFlops / a[1].baseCost),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
eligible.sort((a, b) =>
|
|
|
|
|
(b[1].trainingFlops / b[1].baseCost) - (a[1].trainingFlops / a[1].baseCost),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return eligible.length > 0 ? eligible[0][0] : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private pickModelParams(state: GameState): number {
|
|
|
|
|
const vram = state.infrastructure.totalVramGB;
|
|
|
|
|
const era = state.meta.currentEra;
|
|
|
|
|
|
|
|
|
|
const maxByEra: Record<Era, number> = {
|
|
|
|
|
startup: 7,
|
|
|
|
|
scaleup: 70,
|
|
|
|
|
bigtech: 300,
|
|
|
|
|
agi: 1400,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const vramPerBillion = 2;
|
|
|
|
|
const maxByVram = Math.floor(vram / vramPerBillion);
|
|
|
|
|
const eraCap = Math.floor(maxByEra[era] * this.profile.modelAmbition);
|
|
|
|
|
const cap = Math.min(eraCap, maxByVram);
|
|
|
|
|
|
|
|
|
|
let best = PARAMETER_OPTIONS[0];
|
|
|
|
|
for (const p of PARAMETER_OPTIONS) {
|
|
|
|
|
if (p <= cap) best = p;
|
|
|
|
|
}
|
|
|
|
|
return best;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryRaiseFunding(state: GameState): void {
|
|
|
|
|
const { canRaise, nextRound } = canRaiseFunding(state);
|
|
|
|
|
if (!canRaise || !nextRound) return;
|
|
|
|
|
|
|
|
|
|
if (this.profile.riskTolerance < 0.5) {
|
|
|
|
|
const lowOnCash = state.economy.money < state.economy.expensesPerTick * 300;
|
|
|
|
|
if (!lowOnCash) return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
actions.raiseFunding(state, nextRound);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryBuildInfrastructure(state: GameState): void {
|
|
|
|
|
if (state.infrastructure.clusters.length === 0) {
|
|
|
|
|
actions.buildCluster(state, 'Primary', 'us-west');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const cluster of state.infrastructure.clusters) {
|
|
|
|
|
if (cluster.status !== 'operational') continue;
|
|
|
|
|
|
|
|
|
|
if (cluster.campuses.length === 0) {
|
|
|
|
|
actions.buildCampus(state, 'Campus-1', cluster.id, 'small');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const campus of cluster.campuses) {
|
|
|
|
|
if (campus.status !== 'operational') continue;
|
|
|
|
|
|
|
|
|
|
if (campus.dataCenters.length === 0) {
|
|
|
|
|
actions.buildDataCenter(state, 'DC-1', campus.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryDeployRacks(state: GameState): void {
|
|
|
|
|
const skuId = this.getBestAffordableSku(state);
|
|
|
|
|
if (!skuId) return;
|
|
|
|
|
|
|
|
|
|
const sku = RACK_SKU_CONFIGS[skuId];
|
|
|
|
|
const operationalDCs = getOperationalDCs(state);
|
|
|
|
|
|
|
|
|
|
for (const { dcId, coolingType, rackSkuId } of operationalDCs) {
|
|
|
|
|
if (rackSkuId !== null && rackSkuId !== skuId) continue;
|
|
|
|
|
|
|
|
|
|
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(coolingType as typeof sku.requiredCooling);
|
|
|
|
|
if (!coolingOk) continue;
|
|
|
|
|
|
|
|
|
|
if (!this.cashSafe(state, sku.baseCost, 50)) break;
|
|
|
|
|
actions.fillDCToCapacity(state, dcId, skuId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryDeployModels(state: GameState): void {
|
|
|
|
|
const undeployed = state.models.baseModels
|
|
|
|
|
.filter(m => !m.isDeployed)
|
|
|
|
|
.sort((a, b) => b.rawCapability - a.rawCapability);
|
|
|
|
|
|
|
|
|
|
if (undeployed.length > 0) {
|
|
|
|
|
actions.deployModel(state, undeployed[0].id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryOpenSourceModel(state: GameState): void {
|
|
|
|
|
if (this.profile.safetyWeight < 0.7) return;
|
|
|
|
|
if (state.market.openSourcedModels.length > 0) return;
|
|
|
|
|
const deployed = state.models.baseModels.filter(m => m.isDeployed);
|
|
|
|
|
if (deployed.length > 0) {
|
|
|
|
|
actions.openSourceModel(state, deployed[0].id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private cancelStalledTraining(state: GameState): void {
|
|
|
|
|
const threshold = Math.round(500 * this.profile.safetyWeight);
|
|
|
|
|
const stalledPipelines = state.models.activeTrainingPipelines.filter(
|
|
|
|
|
p => p.status === 'stalled',
|
|
|
|
|
);
|
|
|
|
|
for (const pipeline of stalledPipelines) {
|
|
|
|
|
const stalledTicks = state.meta.tickCount - pipeline.startedAtTick;
|
|
|
|
|
if (stalledTicks < threshold) continue;
|
|
|
|
|
const gen = state.models.families.find(f => f.id === pipeline.familyId)?.generation ?? 1;
|
|
|
|
|
const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[gen] ?? 0;
|
|
|
|
|
if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) {
|
|
|
|
|
state.models.activeTrainingPipelines = state.models.activeTrainingPipelines.filter(
|
|
|
|
|
p => p.id !== pipeline.id,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryStartTraining(state: GameState): void {
|
|
|
|
|
const activeCount = state.models.activeTrainingPipelines.filter(
|
|
|
|
|
p => p.status === 'active' || p.status === 'stalled',
|
|
|
|
|
).length;
|
|
|
|
|
const maxSlots = MAX_CONCURRENT_TRAINING[state.meta.currentEra] ?? 1;
|
|
|
|
|
if (activeCount >= maxSlots) return;
|
|
|
|
|
|
|
|
|
|
if (state.infrastructure.totalVramGB <= 0) return;
|
|
|
|
|
|
|
|
|
|
const gen = state.models.families.length + 1;
|
|
|
|
|
const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[gen] ?? 0;
|
|
|
|
|
if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) return;
|
|
|
|
|
|
|
|
|
|
const params = this.pickModelParams(state);
|
|
|
|
|
const trainingFlops = state.infrastructure.totalTrainingFlops;
|
|
|
|
|
const totalTicks = trainingFlops > 0
|
|
|
|
|
? Math.max(30, Math.ceil(PRETRAINING_BASE_TICKS / (1 + trainingFlops * 0.1)))
|
|
|
|
|
: PRETRAINING_BASE_TICKS;
|
|
|
|
|
const targetTokens = params * 20e9;
|
|
|
|
|
|
|
|
|
|
const hasCodeGen = state.research.completedResearch.includes('code-generation');
|
|
|
|
|
const sftSpecs: ('general' | 'code')[] = hasCodeGen ? ['general', 'code'] : ['general'];
|
|
|
|
|
|
|
|
|
|
const hasAlignment = state.research.completedResearch.includes('alignment-research');
|
|
|
|
|
|
|
|
|
|
const useMoE = this.profile.modelAmbition >= 0.7 && params > 30;
|
|
|
|
|
const archType = useMoE ? 'moe' as const : 'dense' as const;
|
|
|
|
|
const activeParams = useMoE ? Math.floor(params / 4) : params;
|
|
|
|
|
|
|
|
|
|
actions.startTrainingPipeline(state, {
|
|
|
|
|
familyName: `SimCorp-${gen}`,
|
|
|
|
|
architecture: {
|
|
|
|
|
type: archType,
|
|
|
|
|
totalParameters: params,
|
|
|
|
|
activeParameters: activeParams,
|
|
|
|
|
contextWindow: 32,
|
|
|
|
|
vocabularySize: 32000,
|
|
|
|
|
...(useMoE ? { expertCount: 8, expertTopK: 2 } : {}),
|
|
|
|
|
},
|
|
|
|
|
dataMix: { ...DEFAULT_DATA_MIX },
|
|
|
|
|
allocatedComputeFraction: 1.0,
|
|
|
|
|
targetTokens,
|
|
|
|
|
totalTicks,
|
|
|
|
|
sftSpecializations: sftSpecs,
|
|
|
|
|
alignmentMethod: hasAlignment ? 'rlhf' : 'dpo',
|
|
|
|
|
alignmentSafetyWeight: this.profile.safetyWeight,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryEnableRevenue(state: GameState): void {
|
|
|
|
|
if (state.models.bestDeployedModelScore <= 0) return;
|
|
|
|
|
const score = state.models.bestDeployedModelScore;
|
|
|
|
|
const pa = this.profile.pricingAggression;
|
|
|
|
|
|
|
|
|
|
const ct = state.market.consumerTiers.tiers;
|
|
|
|
|
if (!ct.free.config.isActive) actions.toggleConsumerTier(state, 'free');
|
|
|
|
|
if (!ct.plus.config.isActive) actions.toggleConsumerTier(state, 'plus');
|
|
|
|
|
if (!ct.pro.config.isActive && score >= Math.round(20 / pa)) {
|
|
|
|
|
actions.toggleConsumerTier(state, 'pro');
|
|
|
|
|
}
|
|
|
|
|
if (!ct.team.config.isActive && score >= Math.round(30 / pa)) {
|
|
|
|
|
actions.toggleConsumerTier(state, 'team');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const at = state.market.apiTiers.tiers;
|
|
|
|
|
if (!at.free.config.isActive) actions.toggleApiTier(state, 'free');
|
|
|
|
|
if (!at.payg.config.isActive) actions.toggleApiTier(state, 'payg');
|
|
|
|
|
if (!at.scale.config.isActive && score >= Math.round(25 / pa)) {
|
|
|
|
|
actions.toggleApiTier(state, 'scale');
|
|
|
|
|
}
|
|
|
|
|
if (!at['enterprise-api'].config.isActive && score >= Math.round(40 / pa)) {
|
|
|
|
|
actions.toggleApiTier(state, 'enterprise-api');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.research.completedResearch.includes('code-assistant-product')
|
|
|
|
|
&& !state.market.codeAssistant.isActive) {
|
|
|
|
|
actions.toggleCodeAssistant(state);
|
|
|
|
|
actions.setCodeAssistantPrice(state, Math.round(20 * pa));
|
|
|
|
|
}
|
|
|
|
|
if (state.research.completedResearch.includes('agents-platform-product')
|
|
|
|
|
&& !state.market.agentsPlatform.isActive) {
|
|
|
|
|
actions.toggleAgentsPlatform(state);
|
|
|
|
|
actions.setAgentsPlatformPrice(state, Math.round(50 * pa));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryStartResearch(state: GameState): void {
|
|
|
|
|
if (state.research.activeResearch) return;
|
|
|
|
|
|
|
|
|
|
const available = getAvailableResearch(state);
|
|
|
|
|
if (available.length === 0) return;
|
|
|
|
|
|
|
|
|
|
const sorted = [...available].sort((a, b) => {
|
|
|
|
|
const pa = this.researchPriority[a.id] ?? 0;
|
|
|
|
|
const pb = this.researchPriority[b.id] ?? 0;
|
|
|
|
|
return pb - pa;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (const candidate of sorted) {
|
|
|
|
|
if (!this.cashSafe(state, candidate.cost.money, 50)) continue;
|
|
|
|
|
actions.startResearch(state, {
|
|
|
|
|
researchId: candidate.id,
|
|
|
|
|
progressTicks: 0,
|
|
|
|
|
totalTicks: candidate.cost.ticks,
|
|
|
|
|
allocatedResearchers: 0,
|
|
|
|
|
allocatedCompute: 0,
|
|
|
|
|
moneySpent: 0,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryHireTalent(state: GameState): void {
|
|
|
|
|
const era = state.meta.currentEra;
|
|
|
|
|
const total = TALENT_TOTALS[era];
|
|
|
|
|
const bias = this.profile.talentBias;
|
|
|
|
|
const targets: Record<string, number> = {
|
|
|
|
|
research: Math.max(1, Math.round(total * bias.research)),
|
|
|
|
|
engineering: Math.max(1, Math.round(total * bias.engineering)),
|
|
|
|
|
operations: Math.max(1, Math.round(total * bias.operations)),
|
|
|
|
|
sales: Math.max(1, Math.round(total * bias.sales)),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const depts = state.talent.departments;
|
|
|
|
|
for (const [dept, target] of Object.entries(targets)) {
|
|
|
|
|
const current = depts[dept as keyof typeof depts].headcount;
|
|
|
|
|
if (current < target) {
|
|
|
|
|
const needed = Math.min(target - current, 3);
|
|
|
|
|
const cost = needed * 2000;
|
|
|
|
|
if (this.cashSafe(state, cost, 200)) {
|
|
|
|
|
actions.hireDepartment(state, dept as actions.DepartmentId, needed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryUpgradeInfra(state: GameState): void {
|
|
|
|
|
for (const cluster of state.infrastructure.clusters) {
|
|
|
|
|
for (const campus of cluster.campuses) {
|
|
|
|
|
for (const dc of campus.dataCenters) {
|
|
|
|
|
if (dc.status !== 'operational') continue;
|
|
|
|
|
|
|
|
|
|
if (dc.coolingType === 'air'
|
|
|
|
|
&& state.research.completedResearch.includes('liquid-cooling-tech')
|
|
|
|
|
&& this.cashSafe(state, 500_000)) {
|
|
|
|
|
actions.upgradeCoolingType(state, dc.id, 'liquid');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dc.coolingType === 'liquid'
|
|
|
|
|
&& state.research.completedResearch.includes('immersion-cooling-tech')
|
|
|
|
|
&& this.cashSafe(state, 1_000_000)) {
|
|
|
|
|
actions.upgradeCoolingType(state, dc.id, 'immersion');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dc.networkFabric === 'ethernet-100g'
|
|
|
|
|
&& this.cashSafe(state, 200_000)) {
|
|
|
|
|
actions.upgradeNetworkFabric(state, dc.id, 'ethernet-400g');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dc.networkFabric === 'ethernet-400g'
|
|
|
|
|
&& state.research.completedResearch.includes('infiniband-networking')
|
|
|
|
|
&& this.cashSafe(state, 500_000)) {
|
|
|
|
|
actions.upgradeNetworkFabric(state, dc.id, 'infiniband-ndr');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private tryExpandInfra(state: GameState): void {
|
|
|
|
|
const era = state.meta.currentEra;
|
|
|
|
|
const timing = this.profile.expansionTiming;
|
|
|
|
|
|
|
|
|
|
for (const cluster of state.infrastructure.clusters) {
|
|
|
|
|
if (cluster.status !== 'operational') continue;
|
|
|
|
|
|
|
|
|
|
for (const campus of cluster.campuses) {
|
|
|
|
|
if (campus.status !== 'operational') continue;
|
|
|
|
|
|
|
|
|
|
const fillRatio = campus.dataCenters.length > 0
|
|
|
|
|
? campus.dataCenters.filter(dc => {
|
|
|
|
|
if (dc.status !== 'operational') return true;
|
|
|
|
|
const tierConfig = DC_TIER_CONFIGS[dc.tier];
|
|
|
|
|
const mc = maxComputeRacks(tierConfig.rackSlots, dc.tier);
|
|
|
|
|
const existing = dc.computeRacksOnline + actions.pipelineCount(dc);
|
|
|
|
|
return existing >= mc;
|
|
|
|
|
}).length / campus.dataCenters.length
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
if (fillRatio >= timing * 0.8 && campus.dataCenters.length > 0) {
|
|
|
|
|
const tierConfig = DC_TIER_CONFIGS[campus.dcTier];
|
|
|
|
|
if (this.cashSafe(state, tierConfig.baseCost, 300)) {
|
|
|
|
|
actions.addDCsToCampus(state, campus.id, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expansionDelay = Math.round((timing - 1.0) * 3000);
|
|
|
|
|
const scaleupTick = ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf('scaleup')
|
|
|
|
|
? state.meta.tickCount
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
if (ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf('scaleup') && scaleupTick > expansionDelay) {
|
|
|
|
|
const targetTier = state.research.completedResearch.includes('dc-engineering-iii') ? 'large' as const
|
|
|
|
|
: state.research.completedResearch.includes('dc-engineering-ii') ? 'medium' as const
|
|
|
|
|
: 'small' as const;
|
|
|
|
|
|
|
|
|
|
for (const cluster of state.infrastructure.clusters) {
|
|
|
|
|
if (cluster.status !== 'operational') continue;
|
|
|
|
|
|
|
|
|
|
const hasHighTierCampus = cluster.campuses.some(c => c.dcTier === targetTier);
|
|
|
|
|
if (!hasHighTierCampus && this.cashSafe(state, 2_000_000, 300)) {
|
|
|
|
|
actions.buildCampus(state, `${targetTier}-Campus`, cluster.id, targetTier);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf('scaleup') && scaleupTick > expansionDelay) {
|
|
|
|
|
const usedLocations = new Set(state.infrastructure.clusters.map(c => c.locationId));
|
|
|
|
|
const candidates: ('eu-north' | 'us-east')[] = ['eu-north', 'us-east'];
|
|
|
|
|
for (const loc of candidates) {
|
|
|
|
|
if (!usedLocations.has(loc)) {
|
|
|
|
|
const locConfig = LOCATION_CONFIGS[loc];
|
|
|
|
|
if (ERA_ORDER.indexOf(era) >= ERA_ORDER.indexOf(locConfig.availableAt)) {
|
|
|
|
|
if (this.cashSafe(state, CLUSTER_COST_CONFIG.baseCost, 500)) {
|
|
|
|
|
actions.buildCluster(state, `Cluster-${loc}`, loc);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|