Game balance audit: wire research effects, rework capability formula, fix dead systems
CI / build-and-push (push) Successful in 32s

- Create researchBonuses utility to aggregate tech tree effects into all game systems
  (infrastructure energy costs, compute efficiency, training speed, model capability, reputation)
- Rework model capability from sqrt(compute) to 4-pillar formula (params + compute + data + research)
- Make context window affect benchmarks and inference speed
- Add MoE tradeoffs: 1.5x VRAM, 0.8x training speed
- Enforce research point costs as a gate for unlocking research
- Add real consequences to data contamination events (reputation hit, legal costs)
- Scale talent costs from $0.03 to $5/tick per headcount
- Scale compliance costs 100x to be meaningful
- Rework competitor acquisition: cheaper but grants headcount, RP, and reputation
- Remove dead code: sfxVolume, autoSaveInterval, notificationsEnabled,
  FAST_FORWARD_BATCH_SIZE, CHINCHILLA_OPTIMAL_RATIO

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-25 09:36:31 -04:00
parent 8d650fefae
commit 00e790591e
14 changed files with 205 additions and 54 deletions
+21 -4
View File
@@ -42,7 +42,7 @@ import {
} from '@ai-tycoon/shared'; } from '@ai-tycoon/shared';
import { import {
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary, emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
BENCHMARKS, BENCHMARKS, TECH_TREE,
} from '@ai-tycoon/game-engine'; } from '@ai-tycoon/game-engine';
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine'; import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
@@ -1117,8 +1117,15 @@ export const useGameStore = create<Store>()(
startResearch: (research) => set((s) => { startResearch: (research) => set((s) => {
if (s.research.activeResearch) return s; if (s.research.activeResearch) return s;
const node = TECH_TREE.find(n => n.id === research.researchId);
const rpCost = node?.cost.researchPoints ?? 0;
if (rpCost > s.research.researchPoints) return s;
return { return {
research: { ...s.research, activeResearch: research }, research: {
...s.research,
activeResearch: research,
researchPoints: s.research.researchPoints - rpCost,
},
}; };
}), }),
@@ -1202,8 +1209,9 @@ export const useGameStore = create<Store>()(
acquireCompetitor: (competitorId) => set((s) => { acquireCompetitor: (competitorId) => set((s) => {
const rival = s.competitors.rivals.find(r => r.id === competitorId); const rival = s.competitors.rivals.find(r => r.id === competitorId);
if (!rival || rival.status === 'acquired') return s; if (!rival || rival.status === 'acquired') return s;
const cost = rival.estimatedRevenue * 500 + rival.estimatedCapability * 100_000; const cost = rival.estimatedRevenue * 50 + rival.estimatedCapability * 20_000;
if (s.economy.money < cost) return s; if (s.economy.money < cost) return s;
const rpGain = Math.floor(rival.estimatedCapability / 15);
return { return {
economy: { ...s.economy, money: s.economy.money - cost }, economy: { ...s.economy, money: s.economy.money - cost },
competitors: { competitors: {
@@ -1217,9 +1225,18 @@ export const useGameStore = create<Store>()(
departments: { departments: {
...s.talent.departments, ...s.talent.departments,
research: { ...s.talent.departments.research, headcount: s.talent.departments.research.headcount + 5 }, research: { ...s.talent.departments.research, headcount: s.talent.departments.research.headcount + 5 },
engineering: { ...s.talent.departments.engineering, headcount: s.talent.departments.engineering.headcount + 3 }, engineering: { ...s.talent.departments.engineering, headcount: s.talent.departments.engineering.headcount + 5 },
sales: { ...s.talent.departments.sales, headcount: s.talent.departments.sales.headcount + 3 },
}, },
}, },
research: {
...s.research,
researchPoints: s.research.researchPoints + rpGain,
},
reputation: {
...s.reputation,
publicPerception: Math.min(100, s.reputation.publicPerception + 5),
},
}; };
}), }),
+1 -1
View File
@@ -94,7 +94,7 @@ When the player returns after being away:
- Elapsed ticks = `min((now - lastTick) / interval, MAX_OFFLINE_TICKS)` - Elapsed ticks = `min((now - lastTick) / interval, MAX_OFFLINE_TICKS)`
- Max offline cap: 24 hours (86,400 ticks) - Max offline cap: 24 hours (86,400 ticks)
- Ticks process in batches of `FAST_FORWARD_BATCH_SIZE` (100) with reduced fidelity (`OFFLINE_EFFICIENCY = 0.8`) - Ticks process with reduced fidelity (`OFFLINE_EFFICIENCY = 0.8`)
- A progress bar shows catch-up progress - A progress bar shows catch-up progress
- A summary screen reports what happened while away - A summary screen reports what happened while away
+2
View File
@@ -2,6 +2,8 @@ export { GameEngine } from './engine';
export { processTick, setAchievementDefinitions } from './tick'; export { processTick, setAchievementDefinitions } from './tick';
export type { TickNotification } from './tick'; export type { TickNotification } from './tick';
export { getAvailableResearch, getResearchNode } from './systems/researchSystem'; export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
export { getResearchBonuses } from './systems/researchBonuses';
export type { ResearchBonuses } from './systems/researchBonuses';
export { emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary } from './systems/infrastructureSystem'; export { emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary } from './systems/infrastructureSystem';
export { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem'; export { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem';
export { TECH_TREE } from './data/techTree'; export { TECH_TREE } from './data/techTree';
@@ -1,5 +1,6 @@
import type { GameState, ComputeState, InfrastructureState } from '@ai-tycoon/shared'; import type { GameState, ComputeState, InfrastructureState } from '@ai-tycoon/shared';
import { FLOPS_TO_TOKENS_MULTIPLIER } from '@ai-tycoon/shared'; import { FLOPS_TO_TOKENS_MULTIPLIER } from '@ai-tycoon/shared';
import type { ResearchBonuses } from './researchBonuses';
export interface CapacityResult { export interface CapacityResult {
totalFlops: number; totalFlops: number;
@@ -13,20 +14,19 @@ export interface CapacityResult {
tokensPerSecondCapacity: number; tokensPerSecondCapacity: number;
} }
export function computeCapacity(state: GameState, infrastructure: InfrastructureState): CapacityResult { export function computeCapacity(state: GameState, infrastructure: InfrastructureState, researchBonuses?: ResearchBonuses): CapacityResult {
const { totalTrainingFlops, totalInferenceFlops, totalVramGB } = infrastructure; const { totalTrainingFlops, totalInferenceFlops, totalVramGB } = infrastructure;
const trainingAllocation = state.compute.trainingAllocation; const trainingAllocation = state.compute.trainingAllocation;
const inferenceAllocation = 1 - trainingAllocation; const inferenceAllocation = 1 - trainingAllocation;
// Training hardware can do inference at ~50% efficiency
// Inference hardware can do training at ~30% efficiency (no NVLink, poor scaling)
const effectiveTrainingFlops = const effectiveTrainingFlops =
totalTrainingFlops * trainingAllocation + totalTrainingFlops * trainingAllocation +
totalInferenceFlops * trainingAllocation * 0.3; totalInferenceFlops * trainingAllocation * 0.3;
const inferenceBoost = 1 + (researchBonuses?.tokensPerFlopBonus ?? 0) + (researchBonuses?.inferenceEfficiencyBonus ?? 0);
const effectiveInferenceFlops = const effectiveInferenceFlops =
totalInferenceFlops * inferenceAllocation + (totalInferenceFlops * inferenceAllocation +
totalTrainingFlops * inferenceAllocation * 0.5; totalTrainingFlops * inferenceAllocation * 0.5) * inferenceBoost;
const tokensPerSecondCapacity = effectiveInferenceFlops * FLOPS_TO_TOKENS_MULTIPLIER; const tokensPerSecondCapacity = effectiveInferenceFlops * FLOPS_TO_TOKENS_MULTIPLIER;
@@ -24,6 +24,7 @@ import {
estimateNetworkSlots, estimateNetworkSlots,
} from '@ai-tycoon/shared'; } from '@ai-tycoon/shared';
import type { TickNotification } from '../tick'; import type { TickNotification } from '../tick';
import type { ResearchBonuses } from './researchBonuses';
export interface InfraTickResult { export interface InfraTickResult {
infrastructure: InfrastructureState; infrastructure: InfrastructureState;
@@ -463,7 +464,7 @@ function computeInterconnectMultiplier(
// --- Main Infrastructure Tick --- // --- Main Infrastructure Tick ---
export function processInfrastructure(state: GameState): InfraTickResult { export function processInfrastructure(state: GameState, researchBonuses?: ResearchBonuses): InfraTickResult {
const notifications: TickNotification[] = []; const notifications: TickNotification[] = [];
let repairCosts = 0; let repairCosts = 0;
@@ -587,7 +588,9 @@ export function processInfrastructure(state: GameState): InfraTickResult {
continue; continue;
} }
const speed = stageSpeed(cohort.stage, engEff, opsEff); const baseSpeed = stageSpeed(cohort.stage, engEff, opsEff);
const pipelineBonus = cohort.stage !== 'repair' ? (researchBonuses?.pipelineSpeedBonus ?? 0) : 0;
const speed = baseSpeed * (1 + pipelineBonus);
const newProgress = cohort.stageProgress + speed; const newProgress = cohort.stageProgress + speed;
if (newProgress < cohort.stageTotal) { if (newProgress < cohort.stageTotal) {
@@ -728,8 +731,9 @@ export function processInfrastructure(state: GameState): InfraTickResult {
} }
const pue = COOLING_TYPE_CONFIGS[dc.coolingType].pueMultiplier; const pue = COOLING_TYPE_CONFIGS[dc.coolingType].pueMultiplier;
const energyReduction = researchBonuses?.energyCostReduction ?? 0;
const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP) const energyCostPerTick = (tierConfig.baseEnergyCostPerTick + usedPowerKW * BASE_ENERGY_COST_PER_FLOP)
* location.energyCostMultiplier * pue; * location.energyCostMultiplier * pue * (1 - energyReduction);
const maintenanceCostPerTick = totalRacksInDc * BASE_MAINTENANCE_PER_RACK; const maintenanceCostPerTick = totalRacksInDc * BASE_MAINTENANCE_PER_RACK;
const currentUptime = totalRacksInDc > 0 ? effectiveComputeRacks / totalRacksInDc : 1; const currentUptime = totalRacksInDc > 0 ? effectiveComputeRacks / totalRacksInDc : 1;
+60 -23
View File
@@ -21,16 +21,21 @@ import {
DISTILLATION_BASE_RETENTION, DISTILLATION_BASE_RETENTION,
QUANTIZATION_TICKS, QUANTIZATION_TICKS,
} from '@ai-tycoon/shared'; } from '@ai-tycoon/shared';
import type { ResearchBonuses } from './researchBonuses';
export interface ModelTickResult { export interface ModelTickResult {
modelsState: ModelsState; modelsState: ModelsState;
completedModels: BaseModel[]; completedModels: BaseModel[];
notifications: { title: string; message: string; type: 'success' | 'warning' | 'info' }[]; notifications: { title: string; message: string; type: 'success' | 'warning' | 'info' | 'danger' }[];
reputationHit: number;
legalCosts: number;
} }
export function processModels(state: GameState): ModelTickResult { export function processModels(state: GameState, researchBonuses?: ResearchBonuses): ModelTickResult {
const completedModels: BaseModel[] = []; const completedModels: BaseModel[] = [];
const notifications: ModelTickResult['notifications'] = []; const notifications: ModelTickResult['notifications'] = [];
let totalReputationHit = 0;
let totalLegalCosts = 0;
let baseModels = [...state.models.baseModels]; let baseModels = [...state.models.baseModels];
let families = [...state.models.families]; let families = [...state.models.families];
@@ -40,7 +45,8 @@ export function processModels(state: GameState): ModelTickResult {
state.talent.departments.research.effectiveness; state.talent.departments.research.effectiveness;
const engineerBoost = state.talent.departments.engineering.headcount * const engineerBoost = state.talent.departments.engineering.headcount *
state.talent.departments.engineering.effectiveness; state.talent.departments.engineering.effectiveness;
const speedMultiplier = 1 + (researcherBoost + engineerBoost) * 0.05; const trainingResearchBonus = researchBonuses?.trainingSpeedBonus ?? 0;
const speedMultiplier = (1 + (researcherBoost + engineerBoost) * 0.05) * (1 + trainingResearchBonus);
const updatedPipelines: TrainingPipeline[] = []; const updatedPipelines: TrainingPipeline[] = [];
@@ -51,7 +57,8 @@ export function processModels(state: GameState): ModelTickResult {
} }
const generation = families.find(f => f.id === pipeline.familyId)?.generation ?? 1; const generation = families.find(f => f.id === pipeline.familyId)?.generation ?? 1;
const requiredVram = VRAM_REQUIREMENTS_BY_GENERATION[generation] ?? 0; const moeVramMultiplier = pipeline.architecture.type === 'moe' ? 1.5 : 1.0;
const requiredVram = (VRAM_REQUIREMENTS_BY_GENERATION[generation] ?? 0) * moeVramMultiplier;
if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) { if (requiredVram > 0 && state.compute.totalVramGB < requiredVram) {
updatedPipelines.push({ ...pipeline, status: 'stalled' }); updatedPipelines.push({ ...pipeline, status: 'stalled' });
continue; continue;
@@ -62,7 +69,8 @@ export function processModels(state: GameState): ModelTickResult {
if (pipeline.currentStage === 'pretraining') { if (pipeline.currentStage === 'pretraining') {
const stage = { ...pipeline.stages.pretraining }; const stage = { ...pipeline.stages.pretraining };
const newProgress = stage.progressTicks + speedMultiplier; const moeSpeedPenalty = pipeline.architecture.type === 'moe' ? 0.8 : 1.0;
const newProgress = stage.progressTicks + speedMultiplier * moeSpeedPenalty;
const events = generateTrainingEvents(pipeline, state); const events = generateTrainingEvents(pipeline, state);
let tickDelay = 0; let tickDelay = 0;
@@ -81,7 +89,10 @@ export function processModels(state: GameState): ModelTickResult {
tickDelay += event.impact.ticksDelayed ?? 0; tickDelay += event.impact.ticksDelayed ?? 0;
notifications.push({ title: 'Hardware Failure', message: `${pipeline.modelName}: GPU failure during training. Recovering from checkpoint.`, type: 'warning' }); notifications.push({ title: 'Hardware Failure', message: `${pipeline.modelName}: GPU failure during training. Recovering from checkpoint.`, type: 'warning' });
} else if (event.type === 'data_contamination') { } else if (event.type === 'data_contamination') {
notifications.push({ title: 'Data Contamination', message: `${pipeline.modelName}: Copyright concerns detected in training data.`, type: 'warning' }); totalReputationHit += event.impact.reputationHit ?? 0;
totalLegalCosts += event.impact.legalCost ?? 0;
const costStr = event.impact.legalCost ? ` Legal costs: $${(event.impact.legalCost).toLocaleString()}` : '';
notifications.push({ title: 'Data Contamination', message: `${pipeline.modelName}: Copyright concerns detected in training data.${costStr}`, type: 'danger' });
} }
} }
@@ -101,7 +112,7 @@ export function processModels(state: GameState): ModelTickResult {
updated.currentStage = 'alignment'; updated.currentStage = 'alignment';
notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' }); notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
} else { } else {
const model = createBaseModel(updated, state); const model = createBaseModel(updated, state, researchBonuses);
baseModels = [...baseModels, model]; baseModels = [...baseModels, model];
families = families.map(f => families = families.map(f =>
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f, f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
@@ -123,7 +134,7 @@ export function processModels(state: GameState): ModelTickResult {
updated.currentStage = 'alignment'; updated.currentStage = 'alignment';
notifications.push({ title: 'SFT Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' }); notifications.push({ title: 'SFT Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' });
} else { } else {
const model = createBaseModel(updated, state); const model = createBaseModel(updated, state, researchBonuses);
baseModels = [...baseModels, model]; baseModels = [...baseModels, model];
families = families.map(f => families = families.map(f =>
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f, f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
@@ -141,7 +152,7 @@ export function processModels(state: GameState): ModelTickResult {
stage.isComplete = true; stage.isComplete = true;
stage.progressTicks = stage.totalTicks; stage.progressTicks = stage.totalTicks;
const model = createBaseModel(updated, state); const model = createBaseModel(updated, state, researchBonuses);
baseModels = [...baseModels, model]; baseModels = [...baseModels, model];
families = families.map(f => families = families.map(f =>
f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f, f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f,
@@ -195,6 +206,8 @@ export function processModels(state: GameState): ModelTickResult {
}, },
completedModels, completedModels,
notifications, notifications,
reputationHit: totalReputationHit,
legalCosts: totalLegalCosts,
}; };
} }
@@ -262,12 +275,13 @@ function generateTrainingEvents(pipeline: TrainingPipeline, state: GameState): T
? state.data.ownedDatasets.reduce((sum, d) => sum + d.legalRisk, 0) / state.data.ownedDatasets.length ? state.data.ownedDatasets.reduce((sum, d) => sum + d.legalRisk, 0) / state.data.ownedDatasets.length
: 0; : 0;
if (Math.random() < baseProbability * (hasDataPipeline ? 0.25 : 0.5) * avgLegalRisk) { if (Math.random() < baseProbability * (hasDataPipeline ? 0.25 : 0.5) * avgLegalRisk) {
const legalCost = 5000 + Math.floor(Math.random() * 15000);
events.push({ events.push({
id: uuid(), type: 'data_contamination', tick: state.meta.tickCount, id: uuid(), type: 'data_contamination', tick: state.meta.tickCount,
severity: 'moderate', severity: avgLegalRisk > 0.4 ? 'major' : 'moderate',
description: 'Copyright holders identified content in training data.', description: 'Copyright holders identified content in training data.',
resolved: true, resolved: true,
impact: {}, impact: { reputationHit: 5, legalCost },
}); });
} }
@@ -288,36 +302,61 @@ function generateTrainingEvents(pipeline: TrainingPipeline, state: GameState): T
function createBaseModel( function createBaseModel(
pipeline: TrainingPipeline, pipeline: TrainingPipeline,
state: GameState, state: GameState,
researchBonuses?: ResearchBonuses,
): BaseModel { ): BaseModel {
const { architecture, dataMix } = pipeline; const { architecture, dataMix } = pipeline;
const compute = pipeline.stages.pretraining.computeAllocated; const compute = pipeline.stages.pretraining.computeAllocated;
const dataTokens = pipeline.stages.pretraining.targetTokens; const dataTokens = pipeline.stages.pretraining.targetTokens;
const params = architecture.totalParameters;
const computeFactor = Math.sqrt(compute) * 5; // Pillar 1: Parameters (0-30) — larger models have higher ceiling
const dataFactor = Math.log10(1 + dataTokens / 1e8) * 10; const paramFactor = Math.min(30, Math.log2(1 + params) * 4.5);
const researchBonus = state.research.completedResearch.length * 3;
const efficiencyBonus = state.research.completedResearch.filter(r => r.includes('efficiency')).length * 5;
let rawCapability = Math.min(95, computeFactor + dataFactor + researchBonus + efficiencyBonus); // Pillar 2: Compute (0-25) — compute relative to parameter count (Chinchilla scaling)
const computePerParam = compute / Math.max(1, params);
const computeFactor = Math.min(25, Math.sqrt(computePerParam) * 8);
// Pillar 3: Data (0-20) — token count with quality multiplier
const dataQualityMultiplier = 1 + (researchBonuses?.dataQualityBonus ?? 0);
const dataFactor = Math.min(20, Math.log10(1 + dataTokens / 1e8) * 8 * dataQualityMultiplier);
// Pillar 4: Research (0-20) — accumulated research knowledge
const capabilityResearchBonus = researchBonuses?.globalCapabilityBonus ?? 0;
const researchFactor = Math.min(20, capabilityResearchBonus + state.research.completedResearch.length * 0.5);
let rawCapability = Math.min(95, paramFactor + computeFactor + dataFactor + researchFactor);
if (architecture.type === 'moe') { if (architecture.type === 'moe') {
rawCapability = Math.min(98, rawCapability * MOE_CAPABILITY_MULTIPLIER); rawCapability = Math.min(98, rawCapability * MOE_CAPABILITY_MULTIPLIER);
} }
// MoE tradeoff: total params need full VRAM even though only active params run
// This is enforced in the UI/store when checking VRAM requirements
const researcherQuality = state.talent.departments.research.effectiveness; const researcherQuality = state.talent.departments.research.effectiveness;
const contextBonus = Math.log2(Math.max(1, architecture.contextWindow / 4)) * 3;
const contextPenalty = Math.max(0, Math.log2(architecture.contextWindow / 8)) * 2;
const capabilities: ModelCapabilities = { const capabilities: ModelCapabilities = {
reasoning: clamp(rawCapability * (0.6 + dataMix.scientific * 0.5 + dataMix.code * 0.3) * (1 + researcherQuality * 0.2)), reasoning: clamp(rawCapability * (0.6 + dataMix.scientific * 0.5 + dataMix.code * 0.3) * (1 + researcherQuality * 0.2)),
coding: clamp(rawCapability * (0.5 + dataMix.code * 1.0)), coding: clamp(rawCapability * (0.5 + dataMix.code * 1.0)),
creative: clamp(rawCapability * (0.4 + dataMix.books * 0.6 + dataMix.conversation * 0.3)), creative: clamp(rawCapability * (0.4 + dataMix.books * 0.6 + dataMix.conversation * 0.3)),
math: clamp(rawCapability * (0.3 + dataMix.scientific * 0.7 + dataMix.code * 0.2)), math: clamp(rawCapability * (0.3 + dataMix.scientific * 0.7 + dataMix.code * 0.2)),
knowledge: clamp(rawCapability * (0.5 + dataMix.web * 0.3 + dataMix.books * 0.3)), knowledge: clamp(rawCapability * (0.5 + dataMix.web * 0.3 + dataMix.books * 0.3) + contextBonus * 0.3),
multimodal: clamp(rawCapability * (dataMix.images * 0.5 + dataMix.video * 0.4 + dataMix.audio * 0.2)), multimodal: clamp(rawCapability * (dataMix.images * 0.5 + dataMix.video * 0.4 + dataMix.audio * 0.2)),
agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2)), agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2) + contextBonus * 0.5),
speed: Math.max(1, 100 - architecture.totalParameters * 0.3 + efficiencyBonus * 2 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)), speed: Math.max(1, 100 - params * 0.3 - contextPenalty + (researchBonuses?.inferenceEfficiencyBonus ?? 0) * 20 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)),
contextUtilization: Math.min(100, architecture.contextWindow * 0.4), contextUtilization: Math.min(100, architecture.contextWindow * 0.4),
}; };
if (researchBonuses) {
capabilities.reasoning = clamp(capabilities.reasoning + researchBonuses.reasoningBonus);
capabilities.coding = clamp(capabilities.coding + researchBonuses.codingBonus);
capabilities.creative = clamp(capabilities.creative + researchBonuses.creativeBonus);
capabilities.multimodal = clamp(capabilities.multimodal + researchBonuses.multimodalBonus);
capabilities.agents = clamp(capabilities.agents + researchBonuses.agentsBonus);
}
const breakthroughBonuses: Partial<Record<keyof ModelCapabilities, number>> = {}; const breakthroughBonuses: Partial<Record<keyof ModelCapabilities, number>> = {};
for (const event of pipeline.events) { for (const event of pipeline.events) {
if ((event.type === 'breakthrough' || event.type === 'emergent_capability') && event.impact.capabilityDomain && event.impact.capabilityBonus) { if ((event.type === 'breakthrough' || event.type === 'emergent_capability') && event.impact.capabilityDomain && event.impact.capabilityBonus) {
@@ -347,10 +386,8 @@ function createBaseModel(
} }
} }
const safetyResearch = state.research.completedResearch.filter( const safetyResearchBonus = researchBonuses?.safetyBonus ?? 0;
r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional'), let overallSafety = Math.min(100, 30 + safetyResearchBonus + Math.random() * 10);
).length;
let overallSafety = Math.min(100, 30 + safetyResearch * 15 + Math.random() * 10);
let refusalRate = overallSafety > 60 ? 0.1 : 0.03; let refusalRate = overallSafety > 60 ? 0.1 : 0.03;
if (pipeline.stages.alignment?.isComplete) { if (pipeline.stages.alignment?.isComplete) {
@@ -5,13 +5,14 @@ import {
SAFETY_INCIDENT_REPUTATION_HIT, SAFETY_INCIDENT_REPUTATION_HIT,
LOW_SAFETY_THRESHOLD, LOW_SAFETY_THRESHOLD,
} from '@ai-tycoon/shared'; } from '@ai-tycoon/shared';
import type { ResearchBonuses } from './researchBonuses';
export interface ReputationTickResult { export interface ReputationTickResult {
reputation: ReputationState; reputation: ReputationState;
safetyIncident: boolean; safetyIncident: boolean;
} }
export function processReputation(state: GameState): ReputationState & { _safetyIncident?: boolean } { export function processReputation(state: GameState, researchBonuses?: ResearchBonuses): ReputationState & { _safetyIncident?: boolean } {
let { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation; let { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation;
let safetyIncident = false; let safetyIncident = false;
@@ -40,6 +41,9 @@ export function processReputation(state: GameState): ReputationState & { _safety
.reduce((sum, d) => sum + d.morale, 0) / 4; .reduce((sum, d) => sum + d.morale, 0) / 4;
employeeSatisfaction = talentMorale; employeeSatisfaction = talentMorale;
const reputationResearchBonus = researchBonuses?.reputationBonus ?? 0;
publicPerception = Math.min(100, publicPerception + reputationResearchBonus * 0.1);
const score = Math.round( const score = Math.round(
safetyRecord * 0.3 + safetyRecord * 0.3 +
publicPerception * 0.3 + publicPerception * 0.3 +
@@ -0,0 +1,80 @@
import { TECH_TREE } from '../data/techTree';
export interface ResearchBonuses {
energyCostReduction: number;
pipelineSpeedBonus: number;
trainingSpeedBonus: number;
inferenceEfficiencyBonus: number;
tokensPerFlopBonus: number;
dataQualityBonus: number;
sdkCoverageBonus: number;
globalCapabilityBonus: number;
reasoningBonus: number;
codingBonus: number;
creativeBonus: number;
multimodalBonus: number;
agentsBonus: number;
reputationBonus: number;
safetyBonus: number;
}
export function getResearchBonuses(completedResearch: string[]): ResearchBonuses {
const bonuses: ResearchBonuses = {
energyCostReduction: 0,
pipelineSpeedBonus: 0,
trainingSpeedBonus: 0,
inferenceEfficiencyBonus: 0,
tokensPerFlopBonus: 0,
dataQualityBonus: 0,
sdkCoverageBonus: 0,
globalCapabilityBonus: 0,
reasoningBonus: 0,
codingBonus: 0,
creativeBonus: 0,
multimodalBonus: 0,
agentsBonus: 0,
reputationBonus: 0,
safetyBonus: 0,
};
for (const id of completedResearch) {
const node = TECH_TREE.find(n => n.id === id);
if (!node) continue;
for (const effect of node.effects) {
switch (effect.type) {
case 'efficiency_boost':
switch (effect.target) {
case 'training_speed': bonuses.trainingSpeedBonus += effect.value; break;
case 'inference': bonuses.inferenceEfficiencyBonus += effect.value; break;
case 'tokens_per_flop': bonuses.tokensPerFlopBonus += effect.value; break;
case 'pipeline_speed': bonuses.pipelineSpeedBonus += effect.value; break;
case 'data_quality': bonuses.dataQualityBonus += effect.value; break;
case 'sdk_coverage': bonuses.sdkCoverageBonus += effect.value; break;
}
break;
case 'capability_boost':
switch (effect.target) {
case 'all': bonuses.globalCapabilityBonus += effect.value; break;
case 'reasoning': bonuses.reasoningBonus += effect.value; break;
case 'coding': bonuses.codingBonus += effect.value; break;
case 'creative': bonuses.creativeBonus += effect.value; break;
case 'multimodal': bonuses.multimodalBonus += effect.value; break;
case 'agents': bonuses.agentsBonus += effect.value; break;
case 'reputation': bonuses.reputationBonus += effect.value; break;
}
break;
case 'cost_reduction':
if (effect.target === 'energy') bonuses.energyCostReduction += effect.value;
break;
case 'safety_boost':
bonuses.safetyBonus += effect.value;
break;
}
}
}
return bonuses;
}
@@ -46,6 +46,7 @@ export function getAvailableResearch(state: GameState): typeof TECH_TREE {
if (state.research.activeResearch?.researchId === node.id) return false; if (state.research.activeResearch?.researchId === node.id) return false;
if (eraOrder.indexOf(node.era) > currentEraIdx) return false; if (eraOrder.indexOf(node.era) > currentEraIdx) return false;
if (node.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false; if (node.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false;
if (node.cost.researchPoints > state.research.researchPoints) return false;
return true; return true;
}); });
} }
@@ -1,6 +1,6 @@
import type { GameState, TalentState } from '@ai-tycoon/shared'; import type { GameState, TalentState } from '@ai-tycoon/shared';
const SALARY_PER_HEADCOUNT_PER_TICK = 0.03; const SALARY_PER_HEADCOUNT_PER_TICK = 5;
export function processTalent(state: GameState): TalentState { export function processTalent(state: GameState): TalentState {
const departments = { ...state.talent.departments }; const departments = { ...state.talent.departments };
@@ -8,11 +8,11 @@ export function processTalent(state: GameState): TalentState {
let totalSalary = 0; let totalSalary = 0;
for (const [id, dept] of Object.entries(departments)) { for (const [id, dept] of Object.entries(departments)) {
totalSalary += dept.headcount * SALARY_PER_HEADCOUNT_PER_TICK; totalSalary += dept.headcount * SALARY_PER_HEADCOUNT_PER_TICK;
totalSalary += dept.budget / 2592000; totalSalary += dept.budget * 0.01;
} }
for (const hire of state.talent.keyHires) { for (const hire of state.talent.keyHires) {
totalSalary += hire.salary / 2592000; totalSalary += hire.salary;
} }
return { return {
+17 -5
View File
@@ -12,6 +12,7 @@ import { processData } from './systems/dataSystem';
import { checkEraTransition } from './systems/eraSystem'; import { checkEraTransition } from './systems/eraSystem';
import { processAchievements } from './systems/achievementSystem'; import { processAchievements } from './systems/achievementSystem';
import { computeValuation } from './systems/fundingSystem'; import { computeValuation } from './systems/fundingSystem';
import { getResearchBonuses } from './systems/researchBonuses';
export interface TickResult { export interface TickResult {
state: Partial<GameState>; state: Partial<GameState>;
@@ -32,13 +33,14 @@ export function setAchievementDefinitions(defs: AchievementDefinition[]) {
export function processTick(state: GameState): Partial<GameState> { export function processTick(state: GameState): Partial<GameState> {
const notifications: TickNotification[] = []; const notifications: TickNotification[] = [];
const researchBonuses = getResearchBonuses(state.research.completedResearch);
const infraResult = processInfrastructure(state); const infraResult = processInfrastructure(state, researchBonuses);
const infrastructure = infraResult.infrastructure; const infrastructure = infraResult.infrastructure;
notifications.push(...infraResult.notifications); notifications.push(...infraResult.notifications);
const stateWithInfra = { ...state, infrastructure }; const stateWithInfra = { ...state, infrastructure };
const modelResult = processModels(stateWithInfra); const modelResult = processModels(stateWithInfra, researchBonuses);
for (const completed of modelResult.completedModels) { for (const completed of modelResult.completedModels) {
notifications.push({ notifications.push({
@@ -51,7 +53,7 @@ export function processTick(state: GameState): Partial<GameState> {
const stateWithModels = { ...stateWithInfra, models: modelResult.modelsState }; const stateWithModels = { ...stateWithInfra, models: modelResult.modelsState };
const capacity = computeCapacity(state, infrastructure); const capacity = computeCapacity(state, infrastructure, researchBonuses);
const market = processMarket(stateWithModels, capacity.tokensPerSecondCapacity); const market = processMarket(stateWithModels, capacity.tokensPerSecondCapacity);
const compute = finalizeCompute(capacity, market.totalTokenDemand); const compute = finalizeCompute(capacity, market.totalTokenDemand);
@@ -67,7 +69,7 @@ export function processTick(state: GameState): Partial<GameState> {
}); });
} }
const reputationResult = processReputation(stateWithTalent); const reputationResult = processReputation(stateWithTalent, researchBonuses);
const { _safetyIncident, ...reputation } = reputationResult; const { _safetyIncident, ...reputation } = reputationResult;
if (_safetyIncident) { if (_safetyIncident) {
notifications.push({ notifications.push({
@@ -76,7 +78,17 @@ export function processTick(state: GameState): Partial<GameState> {
type: 'danger', type: 'danger',
}); });
} }
const economy = processEconomy(stateWithTalent, market, infrastructure, infraResult.repairCosts); if (modelResult.reputationHit > 0) {
reputation.publicPerception = Math.max(0, reputation.publicPerception - modelResult.reputationHit);
reputation.score = Math.round(
reputation.safetyRecord * 0.3 +
reputation.publicPerception * 0.3 +
reputation.employeeSatisfaction * 0.2 +
reputation.regulatoryStanding * 0.2,
);
}
const extraCosts = infraResult.repairCosts + modelResult.legalCosts;
const economy = processEconomy(stateWithTalent, market, infrastructure, extraCosts);
const data = processData(stateWithTalent); const data = processData(stateWithTalent);
const competitors = processCompetitors(stateWithTalent); const competitors = processCompetitors(stateWithTalent);
+1 -3
View File
@@ -5,7 +5,6 @@ import type { ConsumerTierId, ApiTierId, SeasonalPhase, EnterprisePipelineStage,
export const TICK_INTERVAL_MS = 1000; export const TICK_INTERVAL_MS = 1000;
export const MAX_OFFLINE_TICKS = 86_400; export const MAX_OFFLINE_TICKS = 86_400;
export const OFFLINE_EFFICIENCY = 0.8; export const OFFLINE_EFFICIENCY = 0.8;
export const FAST_FORWARD_BATCH_SIZE = 100;
export const AUTO_SAVE_INTERVAL_TICKS = 60; export const AUTO_SAVE_INTERVAL_TICKS = 60;
export const FINANCIAL_SNAPSHOT_INTERVAL = 60; export const FINANCIAL_SNAPSHOT_INTERVAL = 60;
export const MAX_FINANCIAL_HISTORY = 1000; export const MAX_FINANCIAL_HISTORY = 1000;
@@ -30,7 +29,6 @@ export const SFT_TIME_FRACTION = 0.10;
export const SFT_COMPUTE_FRACTION = 0.06; export const SFT_COMPUTE_FRACTION = 0.06;
export const ALIGNMENT_TIME_FRACTION = 0.08; export const ALIGNMENT_TIME_FRACTION = 0.08;
export const ALIGNMENT_COMPUTE_FRACTION = 0.04; export const ALIGNMENT_COMPUTE_FRACTION = 0.04;
export const CHINCHILLA_OPTIMAL_RATIO = 20;
export const MAX_CONCURRENT_TRAINING: Record<string, number> = { export const MAX_CONCURRENT_TRAINING: Record<string, number> = {
startup: 1, scaleup: 2, bigtech: 4, agi: 8, startup: 1, scaleup: 2, bigtech: 4, agi: 8,
@@ -774,7 +772,7 @@ export const OPEN_SOURCE_TALENT_ATTRACTION = 0.15;
export const OPEN_SOURCE_REVENUE_PENALTY = 0.10; export const OPEN_SOURCE_REVENUE_PENALTY = 0.10;
export const REGULATION_COMPLIANCE_BASE_COST = 0; export const REGULATION_COMPLIANCE_BASE_COST = 0;
export const REGULATION_COMPLIANCE_PER_CAPABILITY = 0.5; export const REGULATION_COMPLIANCE_PER_CAPABILITY = 50;
export const SAFETY_INCIDENT_PROBABILITY_BASE = 0.0002; export const SAFETY_INCIDENT_PROBABILITY_BASE = 0.0002;
export const SAFETY_INCIDENT_REPUTATION_HIT = 15; export const SAFETY_INCIDENT_REPUTATION_HIT = 15;
export const LOW_SAFETY_THRESHOLD = 40; export const LOW_SAFETY_THRESHOLD = 40;
-6
View File
@@ -43,19 +43,13 @@ export type Era = 'startup' | 'scaleup' | 'bigtech' | 'agi';
export type GameSpeed = 1 | 2 | 5; export type GameSpeed = 1 | 2 | 5;
export interface GameSettings { export interface GameSettings {
autoSaveInterval: number;
notificationsEnabled: boolean;
soundEnabled: boolean; soundEnabled: boolean;
musicVolume: number; musicVolume: number;
sfxVolume: number;
} }
export const INITIAL_SETTINGS: GameSettings = { export const INITIAL_SETTINGS: GameSettings = {
autoSaveInterval: 60,
notificationsEnabled: true,
soundEnabled: true, soundEnabled: true,
musicVolume: 0.5, musicVolume: 0.5,
sfxVolume: 0.7,
}; };
export const SAVE_VERSION = 7; export const SAVE_VERSION = 7;
+2
View File
@@ -87,6 +87,8 @@ export interface TrainingEvent {
progressLost?: number; progressLost?: number;
capabilityBonus?: number; capabilityBonus?: number;
capabilityDomain?: keyof ModelCapabilities; capabilityDomain?: keyof ModelCapabilities;
reputationHit?: number;
legalCost?: number;
}; };
} }