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() {
- {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[];
}