diff --git a/.gitea/workflows/balance-check.yml b/.gitea/workflows/balance-check.yml index 681aa36..499ab70 100644 --- a/.gitea/workflows/balance-check.yml +++ b/.gitea/workflows/balance-check.yml @@ -54,7 +54,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Run multi-simulation (5 runs) - run: pnpm --filter @ai-tycoon/game-simulation multirun -- --runs 5 --parallel 2 --strategy greedy --ticks 28800 --no-timeseries + run: pnpm --filter @ai-tycoon/game-simulation multirun -- --runs 5 --parallel 2 --strategy persona --ticks 28800 --no-timeseries - name: Interpret results if: always() diff --git a/apps/web/src/pages/ResearchPage.tsx b/apps/web/src/pages/ResearchPage.tsx index fd1f2db..6a30e1c 100644 --- a/apps/web/src/pages/ResearchPage.tsx +++ b/apps/web/src/pages/ResearchPage.tsx @@ -1,7 +1,7 @@ import { FlaskConical, Lock, Check, Play, ListOrdered, X } from 'lucide-react'; import { TutorialHint } from '@/components/game/TutorialHint'; import { useGameStore } from '@/store'; -import { formatDuration, formatPercent, formatNumber } from '@ai-tycoon/shared'; +import { formatDuration, formatPercent, formatNumber, formatMoney } from '@ai-tycoon/shared'; import { TECH_TREE, getAvailableResearch } from '@ai-tycoon/game-engine'; import type { ResearchNode } from '@ai-tycoon/shared'; @@ -44,6 +44,7 @@ export function ResearchPage() { totalTicks: node.cost.ticks, allocatedResearchers: state.talent.departments.research.headcount, allocatedCompute: node.cost.compute, + moneySpent: 0, }); }; @@ -165,7 +166,7 @@ export function ResearchPage() {

{node.description}

- {formatDuration(node.cost.ticks)} · {formatNumber(node.cost.compute)} compute + {formatMoney(node.cost.money)} · {formatDuration(node.cost.ticks)} · {formatNumber(node.cost.compute)} compute {node.cost.researchPoints > 0 && ` · ${node.cost.researchPoints} RP`}
{canStart && ( diff --git a/packages/game-engine/src/data/techTree.ts b/packages/game-engine/src/data/techTree.ts index c5321f1..689fcc1 100644 --- a/packages/game-engine/src/data/techTree.ts +++ b/packages/game-engine/src/data/techTree.ts @@ -9,7 +9,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'infrastructure', prerequisites: [], - cost: { researchPoints: 0, compute: 5, ticks: 60 }, + cost: { researchPoints: 0, compute: 5, ticks: 150, money: 2250 }, effects: [{ type: 'cost_reduction', target: 'energy', value: 0.25 }], }, { @@ -19,7 +19,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'infrastructure', prerequisites: [], - cost: { researchPoints: 0, compute: 5, ticks: 60 }, + cost: { researchPoints: 0, compute: 5, ticks: 150, money: 2250 }, effects: [{ type: 'cost_reduction', target: 'failure_rate', value: 0.5 }], }, { @@ -29,7 +29,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'infrastructure', prerequisites: [], - cost: { researchPoints: 0, compute: 10, ticks: 90 }, + cost: { researchPoints: 0, compute: 10, ticks: 225, money: 3375 }, effects: [{ type: 'unlock_rack', target: 'a100', value: 1 }], }, { @@ -39,7 +39,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['advanced-gpu-arch'], - cost: { researchPoints: 2, compute: 40, ticks: 240 }, + cost: { researchPoints: 2, compute: 40, ticks: 600, money: 30000 }, effects: [{ type: 'unlock_rack', target: 'h100', value: 1 }], }, { @@ -49,7 +49,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'infrastructure', prerequisites: ['next-gen-gpu'], - cost: { researchPoints: 5, compute: 200, ticks: 480 }, + cost: { researchPoints: 5, compute: 200, ticks: 1200, money: 240000 }, effects: [{ type: 'unlock_rack', target: 'b200', value: 1 }], }, { @@ -59,7 +59,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'agi', category: 'infrastructure', prerequisites: ['frontier-compute'], - cost: { researchPoints: 10, compute: 500, ticks: 900 }, + cost: { researchPoints: 10, compute: 500, ticks: 2250, money: 1125000 }, effects: [{ type: 'unlock_rack', target: 'custom', value: 1 }], }, { @@ -69,7 +69,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['advanced-gpu-arch'], - cost: { researchPoints: 2, compute: 30, ticks: 200 }, + cost: { researchPoints: 2, compute: 30, ticks: 500, money: 25000 }, effects: [{ type: 'unlock_rack', target: 'amd', value: 1 }], }, { @@ -79,7 +79,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['quantization'], - cost: { researchPoints: 2, compute: 20, ticks: 150 }, + cost: { researchPoints: 2, compute: 20, ticks: 375, money: 18750 }, effects: [{ type: 'unlock_rack', target: 'inference', value: 1 }], }, { @@ -89,7 +89,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'agi', category: 'infrastructure', prerequisites: ['frontier-compute'], - cost: { researchPoints: 8, compute: 400, ticks: 720 }, + cost: { researchPoints: 8, compute: 400, ticks: 1800, money: 900000 }, effects: [{ type: 'unlock_rack', target: 'gb200-nvl72', value: 1 }], }, { @@ -99,7 +99,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['advanced-cooling'], - cost: { researchPoints: 2, compute: 25, ticks: 180 }, + cost: { researchPoints: 2, compute: 25, ticks: 450, money: 22500 }, effects: [{ type: 'unlock_feature', target: 'liquid-cooling', value: 1 }], }, { @@ -109,7 +109,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'infrastructure', prerequisites: ['liquid-cooling-tech'], - cost: { researchPoints: 5, compute: 100, ticks: 400 }, + cost: { researchPoints: 5, compute: 100, ticks: 1000, money: 200000 }, effects: [{ type: 'unlock_feature', target: 'immersion-cooling', value: 1 }], }, { @@ -119,7 +119,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['network-engineering-i'], - cost: { researchPoints: 3, compute: 40, ticks: 240 }, + cost: { researchPoints: 3, compute: 40, ticks: 600, money: 30000 }, effects: [{ type: 'unlock_feature', target: 'infiniband', value: 1 }], }, { @@ -129,7 +129,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'infrastructure', prerequisites: ['advanced-cooling'], - cost: { researchPoints: 1, compute: 15, ticks: 120 }, + cost: { researchPoints: 1, compute: 15, ticks: 300, money: 4500 }, effects: [{ type: 'unlock_dc_tier', target: 'medium', value: 1 }], }, { @@ -139,7 +139,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['dc-engineering-ii'], - cost: { researchPoints: 3, compute: 60, ticks: 300 }, + cost: { researchPoints: 3, compute: 60, ticks: 750, money: 37500 }, effects: [{ type: 'unlock_dc_tier', target: 'large', value: 1 }], }, { @@ -149,7 +149,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'infrastructure', prerequisites: ['dc-engineering-iii'], - cost: { researchPoints: 6, compute: 150, ticks: 600 }, + cost: { researchPoints: 6, compute: 150, ticks: 1500, money: 300000 }, effects: [{ type: 'unlock_dc_tier', target: 'mega', value: 1 }], }, { @@ -159,7 +159,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'infrastructure', prerequisites: ['redundancy-protocols'], - cost: { researchPoints: 1, compute: 10, ticks: 90 }, + cost: { researchPoints: 1, compute: 10, ticks: 225, money: 3375 }, effects: [{ type: 'cost_reduction', target: 'test_failure_rate', value: 0.25 }], }, { @@ -169,7 +169,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['redundancy-protocols'], - cost: { researchPoints: 2, compute: 20, ticks: 150 }, + cost: { researchPoints: 2, compute: 20, ticks: 375, money: 18750 }, effects: [{ type: 'cost_reduction', target: 'network_failure_rate', value: 0.4 }], }, { @@ -179,7 +179,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'infrastructure', prerequisites: ['network-engineering-i'], - cost: { researchPoints: 4, compute: 80, ticks: 360 }, + cost: { researchPoints: 4, compute: 80, ticks: 900, money: 180000 }, effects: [{ type: 'cost_reduction', target: 'network_failure_rate', value: 0.5 }], }, { @@ -189,7 +189,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['network-engineering-i'], - cost: { researchPoints: 3, compute: 40, ticks: 240 }, + cost: { researchPoints: 3, compute: 40, ticks: 600, money: 30000 }, effects: [{ type: 'efficiency_boost', target: 'network_uplinks', value: 1 }], }, { @@ -199,7 +199,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'infrastructure', prerequisites: ['network-engineering-ii'], - cost: { researchPoints: 5, compute: 100, ticks: 400 }, + cost: { researchPoints: 5, compute: 100, ticks: 1000, money: 200000 }, effects: [{ type: 'efficiency_boost', target: 'network_repair_speed', value: 0.4 }], }, { @@ -209,7 +209,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'agi', category: 'infrastructure', prerequisites: ['network-fast-repair'], - cost: { researchPoints: 8, compute: 250, ticks: 600 }, + cost: { researchPoints: 8, compute: 250, ticks: 1500, money: 750000 }, effects: [{ type: 'efficiency_boost', target: 'network_hot_standby', value: 5 }], }, { @@ -219,7 +219,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['dc-engineering-ii'], - cost: { researchPoints: 2, compute: 25, ticks: 180 }, + cost: { researchPoints: 2, compute: 25, ticks: 450, money: 22500 }, effects: [{ type: 'efficiency_boost', target: 'pipeline_speed', value: 0.2 }], }, { @@ -229,7 +229,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'infrastructure', prerequisites: ['advanced-gpu-arch'], - cost: { researchPoints: 2, compute: 30, ticks: 180 }, + cost: { researchPoints: 2, compute: 30, ticks: 450, money: 22500 }, effects: [{ type: 'efficiency_boost', target: 'training_speed', value: 0.2 }], }, @@ -241,7 +241,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'efficiency', prerequisites: [], - cost: { researchPoints: 0, compute: 8, ticks: 75 }, + cost: { researchPoints: 0, compute: 8, ticks: 188, money: 2820 }, effects: [{ type: 'efficiency_boost', target: 'inference', value: 0.15 }], }, { @@ -251,7 +251,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'efficiency', prerequisites: ['quantization'], - cost: { researchPoints: 2, compute: 25, ticks: 180 }, + cost: { researchPoints: 2, compute: 25, ticks: 450, money: 22500 }, effects: [{ type: 'capability_boost', target: 'all', value: 5 }], }, { @@ -261,7 +261,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'efficiency', prerequisites: ['quantization'], - cost: { researchPoints: 2, compute: 20, ticks: 150 }, + cost: { researchPoints: 2, compute: 20, ticks: 375, money: 18750 }, effects: [{ type: 'efficiency_boost', target: 'tokens_per_flop', value: 0.3 }], }, @@ -273,7 +273,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'generation', prerequisites: [], - cost: { researchPoints: 0, compute: 10, ticks: 90 }, + cost: { researchPoints: 0, compute: 10, ticks: 225, money: 3375 }, effects: [{ type: 'capability_boost', target: 'all', value: 10 }], }, { @@ -284,7 +284,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'reasoning', prerequisites: ['transformer-v2'], - cost: { researchPoints: 3, compute: 40, ticks: 240 }, + cost: { researchPoints: 3, compute: 40, ticks: 720, money: 36000 }, effects: [{ type: 'capability_boost', target: 'reasoning', value: 15 }], }, { @@ -295,7 +295,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'coding', prerequisites: ['transformer-v2'], - cost: { researchPoints: 3, compute: 35, ticks: 210 }, + cost: { researchPoints: 3, compute: 35, ticks: 735, money: 36750 }, effects: [{ type: 'capability_boost', target: 'coding', value: 15 }], }, { @@ -306,7 +306,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'creative', prerequisites: ['transformer-v2'], - cost: { researchPoints: 3, compute: 30, ticks: 210 }, + cost: { researchPoints: 3, compute: 30, ticks: 735, money: 36750 }, effects: [{ type: 'capability_boost', target: 'creative', value: 15 }], }, { @@ -317,7 +317,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'multimodal', prerequisites: ['transformer-v2'], - cost: { researchPoints: 4, compute: 50, ticks: 300 }, + cost: { researchPoints: 4, compute: 50, ticks: 1050, money: 52500 }, effects: [ { type: 'capability_boost', target: 'multimodal', value: 20 }, { type: 'unlock_product_line', target: 'image', value: 1 }, @@ -331,7 +331,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'agents', prerequisites: ['reasoning-enhancement', 'code-generation'], - cost: { researchPoints: 6, compute: 100, ticks: 480 }, + cost: { researchPoints: 6, compute: 100, ticks: 1680, money: 336000 }, effects: [ { type: 'capability_boost', target: 'agents', value: 20 }, { type: 'unlock_product_line', target: 'agents', value: 1 }, @@ -346,7 +346,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'safety', prerequisites: [], - cost: { researchPoints: 0, compute: 8, ticks: 90 }, + cost: { researchPoints: 0, compute: 8, ticks: 270, money: 4050 }, effects: [ { type: 'safety_boost', target: 'models', value: 10 }, { type: 'capability_boost', target: 'reputation', value: 5 }, @@ -359,7 +359,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'safety', prerequisites: ['alignment-research'], - cost: { researchPoints: 3, compute: 40, ticks: 240 }, + cost: { researchPoints: 3, compute: 40, ticks: 720, money: 36000 }, effects: [ { type: 'safety_boost', target: 'models', value: 10 }, { type: 'capability_boost', target: 'reputation', value: 5 }, @@ -372,7 +372,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'safety', prerequisites: ['interpretability'], - cost: { researchPoints: 5, compute: 80, ticks: 420 }, + cost: { researchPoints: 5, compute: 80, ticks: 1260, money: 252000 }, effects: [ { type: 'safety_boost', target: 'models', value: 15 }, { type: 'capability_boost', target: 'reputation', value: 10 }, @@ -388,7 +388,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'coding', prerequisites: ['code-generation'], - cost: { researchPoints: 2, compute: 20, ticks: 150 }, + cost: { researchPoints: 2, compute: 20, ticks: 525, money: 26250 }, effects: [{ type: 'unlock_product_line', target: 'code-assistant', value: 1 }], }, { @@ -398,7 +398,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'efficiency', prerequisites: [], - cost: { researchPoints: 0, compute: 3, ticks: 45 }, + cost: { researchPoints: 0, compute: 3, ticks: 158, money: 2370 }, effects: [{ type: 'unlock_feature', target: 'developer-relations', value: 1 }], }, { @@ -408,7 +408,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'efficiency', prerequisites: [], - cost: { researchPoints: 0, compute: 3, ticks: 45 }, + cost: { researchPoints: 0, compute: 3, ticks: 112, money: 1680 }, effects: [{ type: 'unlock_feature', target: 'enterprise-sales', value: 1 }], }, { @@ -418,7 +418,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'efficiency', prerequisites: ['developer-relations'], - cost: { researchPoints: 2, compute: 15, ticks: 120 }, + cost: { researchPoints: 2, compute: 15, ticks: 300, money: 15000 }, effects: [{ type: 'efficiency_boost', target: 'sdk_coverage', value: 0.3 }], }, { @@ -429,7 +429,7 @@ export const TECH_TREE: ResearchNode[] = [ category: 'specialization', branch: 'agents', prerequisites: ['agentic-architecture'], - cost: { researchPoints: 4, compute: 60, ticks: 300 }, + cost: { researchPoints: 4, compute: 60, ticks: 1050, money: 210000 }, effects: [{ type: 'unlock_product_line', target: 'agents-platform', value: 1 }], }, @@ -441,7 +441,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'efficiency', prerequisites: ['inference-optimization'], - cost: { researchPoints: 2, compute: 25, ticks: 150 }, + cost: { researchPoints: 2, compute: 25, ticks: 375, money: 18750 }, effects: [{ type: 'unlock_feature', target: 'request-routing', value: 1 }], }, { @@ -451,7 +451,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'efficiency', prerequisites: ['request-routing'], - cost: { researchPoints: 3, compute: 30, ticks: 180 }, + cost: { researchPoints: 3, compute: 30, ticks: 450, money: 22500 }, effects: [{ type: 'unlock_feature', target: 'priority-queues', value: 1 }], }, { @@ -461,7 +461,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'scaleup', category: 'efficiency', prerequisites: ['inference-optimization'], - cost: { researchPoints: 2, compute: 20, ticks: 120 }, + cost: { researchPoints: 2, compute: 20, ticks: 300, money: 15000 }, effects: [{ type: 'unlock_feature', target: 'request-batching', value: 1 }], }, { @@ -471,7 +471,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'bigtech', category: 'efficiency', prerequisites: ['request-routing'], - cost: { researchPoints: 4, compute: 60, ticks: 300 }, + cost: { researchPoints: 4, compute: 60, ticks: 750, money: 150000 }, effects: [{ type: 'efficiency_boost', target: 'auto_scaling', value: 0.2 }], }, @@ -483,7 +483,7 @@ export const TECH_TREE: ResearchNode[] = [ era: 'startup', category: 'efficiency', prerequisites: [], - cost: { researchPoints: 0, compute: 5, ticks: 60 }, + cost: { researchPoints: 0, compute: 5, ticks: 150, money: 2250 }, effects: [{ type: 'efficiency_boost', target: 'data_quality', value: 0.2 }], }, ]; diff --git a/packages/game-engine/src/systems/economySystem.ts b/packages/game-engine/src/systems/economySystem.ts index 6bbdfbe..daaf349 100644 --- a/packages/game-engine/src/systems/economySystem.ts +++ b/packages/game-engine/src/systems/economySystem.ts @@ -1,5 +1,6 @@ import type { GameState, EconomyState, InfrastructureState } from '@ai-tycoon/shared'; import { FINANCIAL_SNAPSHOT_INTERVAL, MAX_FINANCIAL_HISTORY, REGULATION_COMPLIANCE_PER_CAPABILITY } from '@ai-tycoon/shared'; +import { TECH_TREE } from '../data/techTree'; import type { MarketTickResult } from './marketSystem'; export function processEconomy( @@ -27,7 +28,16 @@ export function processEconomy( const complianceCost = bestCapability > 30 ? bestCapability * REGULATION_COMPLIANCE_PER_CAPABILITY * (1 + eraIdx * 0.5) / 100 : 0; const devRelExpenses = state.market.developerEcosystem.devRelSpending; - const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost + devRelExpenses + extraCosts; + + let researchExpenses = 0; + if (state.research.activeResearch) { + const node = TECH_TREE.find(n => n.id === state.research.activeResearch!.researchId); + if (node) { + researchExpenses = node.cost.money / node.cost.ticks; + } + } + + const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost + devRelExpenses + researchExpenses + extraCosts; const money = state.economy.money + revenue - expenses; diff --git a/packages/game-engine/src/systems/researchSystem.ts b/packages/game-engine/src/systems/researchSystem.ts index 2c6b730..9692c43 100644 --- a/packages/game-engine/src/systems/researchSystem.ts +++ b/packages/game-engine/src/systems/researchSystem.ts @@ -30,6 +30,7 @@ function promoteFromQueue( totalTicks: node.cost.ticks, allocatedResearchers: state.talent.departments.research.headcount, allocatedCompute: node.cost.compute, + moneySpent: 0, }; return { @@ -52,6 +53,10 @@ export function processResearch(state: GameState, compute: ComputeState): Resear const newProgress = active.progressTicks + speedMultiplier; + const node = TECH_TREE.find(n => n.id === active.researchId); + const moneyPerTick = node ? node.cost.money / node.cost.ticks : 0; + const newMoneySpent = (active.moneySpent ?? 0) + moneyPerTick; + if (newProgress >= active.totalTicks) { const completedResearch = { ...state.research, @@ -72,7 +77,7 @@ export function processResearch(state: GameState, compute: ComputeState): Resear return { research: { ...state.research, - activeResearch: { ...active, progressTicks: newProgress }, + activeResearch: { ...active, progressTicks: newProgress, moneySpent: newMoneySpent }, }, researchCompleted: null, }; diff --git a/packages/game-engine/src/systems/talentSystem.test.ts b/packages/game-engine/src/systems/talentSystem.test.ts index 0211458..4b3f710 100644 --- a/packages/game-engine/src/systems/talentSystem.test.ts +++ b/packages/game-engine/src/systems/talentSystem.test.ts @@ -53,8 +53,9 @@ describe('processTalent', () => { expect(result.totalSalaryPerTick).toBe(70); }); - it('adds 1% of department budget per tick', () => { + it('adds 1% of department budget per tick scaled by era', () => { const state = createTestState({ + meta: { currentEra: 'bigtech' }, talent: { departments: { research: { id: 'research', headcount: 0, budget: 10_000, effectiveness: 0.5, morale: 0.8 }, @@ -66,10 +67,28 @@ describe('processTalent', () => { }, }); const result = processTalent(state); - // 10000 * 0.01 + 5000 * 0.01 = 100 + 50 = 150 + // bigtech multiplier = 1.0: 10000 * 0.01 + 5000 * 0.01 = 150 expect(result.totalSalaryPerTick).toBe(150); }); + it('applies startup era discount to budget costs', () => { + const state = createTestState({ + meta: { currentEra: 'startup' }, + talent: { + departments: { + research: { id: 'research', headcount: 0, budget: 10_000, effectiveness: 0.5, morale: 0.8 }, + engineering: { id: 'engineering', headcount: 0, budget: 5_000, effectiveness: 0.5, morale: 0.8 }, + operations: { id: 'operations', headcount: 0, budget: 0, effectiveness: 0.5, morale: 0.8 }, + sales: { id: 'sales', headcount: 0, budget: 0, effectiveness: 0.5, morale: 0.8 }, + }, + keyHires: [], + }, + }); + const result = processTalent(state); + // startup multiplier = 0.2: (10000 + 5000) * 0.01 * 0.2 = 30 + expect(result.totalSalaryPerTick).toBe(30); + }); + it('adds key hire salaries to total', () => { const state = createTestState({ talent: { @@ -110,6 +129,7 @@ describe('processTalent', () => { it('combines headcount salary, budget cost, and key hire salary', () => { const state = createTestState({ + meta: { currentEra: 'bigtech' }, talent: { departments: { research: { id: 'research', headcount: 4, budget: 2_000, effectiveness: 0.5, morale: 0.8 }, @@ -133,7 +153,7 @@ describe('processTalent', () => { }); const result = processTalent(state); // headcount: (4 + 6) * 5 = 50 - // budget: 2000 * 0.01 + 3000 * 0.01 = 20 + 30 = 50 + // budget (bigtech 1.0x): 2000 * 0.01 + 3000 * 0.01 = 50 // key hires: 15 // total = 50 + 50 + 15 = 115 expect(result.totalSalaryPerTick).toBe(115); diff --git a/packages/game-engine/src/systems/talentSystem.ts b/packages/game-engine/src/systems/talentSystem.ts index d70080f..b67e34e 100644 --- a/packages/game-engine/src/systems/talentSystem.ts +++ b/packages/game-engine/src/systems/talentSystem.ts @@ -1,14 +1,16 @@ import type { GameState, TalentState } from '@ai-tycoon/shared'; +import { ERA_BUDGET_COST_MULTIPLIER } from '@ai-tycoon/shared'; const SALARY_PER_HEADCOUNT_PER_TICK = 5; export function processTalent(state: GameState): TalentState { const departments = { ...state.talent.departments }; + const budgetMultiplier = ERA_BUDGET_COST_MULTIPLIER[state.meta.currentEra] ?? 1.0; let totalSalary = 0; for (const [id, dept] of Object.entries(departments)) { totalSalary += dept.headcount * SALARY_PER_HEADCOUNT_PER_TICK; - totalSalary += dept.budget * 0.01; + totalSalary += dept.budget * 0.01 * budgetMultiplier; } for (const hire of state.talent.keyHires) { diff --git a/packages/game-simulation/src/simulate.ts b/packages/game-simulation/src/simulate.ts index c97c698..81b420b 100644 --- a/packages/game-simulation/src/simulate.ts +++ b/packages/game-simulation/src/simulate.ts @@ -1,6 +1,7 @@ import { runSimulation } from './runner'; import { GreedyStrategy } from './strategies/greedy'; import { RandomStrategy } from './strategies/random'; +import { PersonaStrategy } from './strategies/persona'; import { printConsoleReport, generateJsonReport } from './analysis/report'; import { writeFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; @@ -27,7 +28,9 @@ const jsonOutput = hasFlag('json'); const verbose = hasFlag('verbose'); const csvOutput = hasFlag('csv'); -const strategy = strategyName === 'random' ? new RandomStrategy() : new GreedyStrategy(); +const strategy = strategyName === 'random' ? new RandomStrategy() + : strategyName === 'persona' ? new PersonaStrategy(seed ?? 42) + : new GreedyStrategy(); console.log(`Running ${strategyName} simulation: ${totalTicks.toLocaleString()} ticks, interval ${decisionInterval}${seed !== undefined ? `, seed ${seed}` : ''}...`); diff --git a/packages/game-simulation/src/strategies/greedy.ts b/packages/game-simulation/src/strategies/greedy.ts index 903a0a1..fb3c5a3 100644 --- a/packages/game-simulation/src/strategies/greedy.ts +++ b/packages/game-simulation/src/strategies/greedy.ts @@ -316,14 +316,18 @@ export class GreedyStrategy implements Strategy { return pb - pa; }); - const best = sorted[0]; - actions.startResearch(state, { - researchId: best.id, - progressTicks: 0, - totalTicks: best.cost.ticks, - allocatedResearchers: 0, - allocatedCompute: 0, - }); + for (const candidate of sorted) { + if (!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 { diff --git a/packages/game-simulation/src/strategies/persona.ts b/packages/game-simulation/src/strategies/persona.ts new file mode 100644 index 0000000..c4e4155 --- /dev/null +++ b/packages/game-simulation/src/strategies/persona.ts @@ -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 = { + '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 { + const priorities = { ...BASE_RESEARCH_PRIORITY }; + + const boostSets: [Set, 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 = { + 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; + + 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 = { + 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 = { + 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; + } + } + } + } + } + } +} diff --git a/packages/game-simulation/src/strategies/random.ts b/packages/game-simulation/src/strategies/random.ts index ab05f66..e13640d 100644 --- a/packages/game-simulation/src/strategies/random.ts +++ b/packages/game-simulation/src/strategies/random.ts @@ -48,6 +48,7 @@ export class RandomStrategy implements Strategy { totalTicks: pick.cost.ticks, allocatedResearchers: 0, allocatedCompute: 0, + moneySpent: 0, })); } } diff --git a/packages/game-simulation/src/worker.ts b/packages/game-simulation/src/worker.ts index e902568..481627c 100644 --- a/packages/game-simulation/src/worker.ts +++ b/packages/game-simulation/src/worker.ts @@ -2,6 +2,7 @@ import { runSimulation } from './runner'; import { generateJsonReport } from './analysis/report'; import { GreedyStrategy } from './strategies/greedy'; import { RandomStrategy } from './strategies/random'; +import { PersonaStrategy } from './strategies/persona'; const args = process.argv.slice(2); @@ -16,7 +17,9 @@ const seed = parseInt(getArg('seed', '0'), 10); const runId = parseInt(getArg('run-id', '1'), 10); const decisionInterval = 60; -const strategy = strategyName === 'random' ? new RandomStrategy() : new GreedyStrategy(); +const strategy = strategyName === 'random' ? new RandomStrategy() + : strategyName === 'persona' ? new PersonaStrategy(seed) + : new GreedyStrategy(); process.stderr.write(`[Run #${runId}] Starting (seed ${seed}, ${totalTicks} ticks, ${strategyName})...\n`); diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts index 553dc28..15bec14 100644 --- a/packages/shared/src/constants/gameBalance.ts +++ b/packages/shared/src/constants/gameBalance.ts @@ -1032,3 +1032,10 @@ export const COMPETITOR_PRODUCT_THRESHOLDS = { export const COMPETITOR_CATCHUP_SHARE_THRESHOLD = 0.05; export const COMPETITOR_CATCHUP_PRICE_CUT = 0.3; + +export const ERA_BUDGET_COST_MULTIPLIER: Record = { + startup: 0.2, + scaleup: 0.6, + bigtech: 1.0, + agi: 1.5, +}; diff --git a/packages/shared/src/types/research.ts b/packages/shared/src/types/research.ts index 8ac9950..b58d18f 100644 --- a/packages/shared/src/types/research.ts +++ b/packages/shared/src/types/research.ts @@ -13,6 +13,7 @@ export interface ActiveResearch { totalTicks: number; allocatedResearchers: number; allocatedCompute: number; + moneySpent: number; } export interface ResearchNode { @@ -27,6 +28,7 @@ export interface ResearchNode { researchPoints: number; compute: number; ticks: number; + money: number; }; effects: ResearchEffect[]; }