From 00e790591eecfaec3e0e5327ece690ffc3454432 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 25 Apr 2026 09:36:31 -0400 Subject: [PATCH] Game balance audit: wire research effects, rework capability formula, fix dead systems - 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 --- apps/web/src/store/index.ts | 25 +++++- docs/architecture.md | 2 +- packages/game-engine/src/index.ts | 2 + .../game-engine/src/systems/computeSystem.ts | 10 +-- .../src/systems/infrastructureSystem.ts | 10 ++- .../game-engine/src/systems/modelSystem.ts | 83 ++++++++++++++----- .../src/systems/reputationSystem.ts | 6 +- .../src/systems/researchBonuses.ts | 80 ++++++++++++++++++ .../game-engine/src/systems/researchSystem.ts | 1 + .../game-engine/src/systems/talentSystem.ts | 6 +- packages/game-engine/src/tick.ts | 22 +++-- packages/shared/src/constants/gameBalance.ts | 4 +- packages/shared/src/types/gameState.ts | 6 -- packages/shared/src/types/models.ts | 2 + 14 files changed, 205 insertions(+), 54 deletions(-) create mode 100644 packages/game-engine/src/systems/researchBonuses.ts diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts index 87b5b30..f1c33c9 100644 --- a/apps/web/src/store/index.ts +++ b/apps/web/src/store/index.ts @@ -42,7 +42,7 @@ import { } from '@ai-tycoon/shared'; import { emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary, - BENCHMARKS, + BENCHMARKS, TECH_TREE, } from '@ai-tycoon/game-engine'; import { INITIAL_RIVALS } from '@ai-tycoon/game-engine'; @@ -1117,8 +1117,15 @@ export const useGameStore = create()( startResearch: (research) => set((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 { - research: { ...s.research, activeResearch: research }, + research: { + ...s.research, + activeResearch: research, + researchPoints: s.research.researchPoints - rpCost, + }, }; }), @@ -1202,8 +1209,9 @@ export const useGameStore = create()( acquireCompetitor: (competitorId) => set((s) => { const rival = s.competitors.rivals.find(r => r.id === competitorId); 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; + const rpGain = Math.floor(rival.estimatedCapability / 15); return { economy: { ...s.economy, money: s.economy.money - cost }, competitors: { @@ -1217,9 +1225,18 @@ export const useGameStore = create()( departments: { ...s.talent.departments, 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), + }, }; }), diff --git a/docs/architecture.md b/docs/architecture.md index 610f8d4..bac63fd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -94,7 +94,7 @@ When the player returns after being away: - Elapsed ticks = `min((now - lastTick) / interval, MAX_OFFLINE_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 summary screen reports what happened while away diff --git a/packages/game-engine/src/index.ts b/packages/game-engine/src/index.ts index 67caf4e..f3504c1 100644 --- a/packages/game-engine/src/index.ts +++ b/packages/game-engine/src/index.ts @@ -2,6 +2,8 @@ export { GameEngine } from './engine'; export { processTick, setAchievementDefinitions } from './tick'; export type { TickNotification } from './tick'; 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 { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem'; export { TECH_TREE } from './data/techTree'; diff --git a/packages/game-engine/src/systems/computeSystem.ts b/packages/game-engine/src/systems/computeSystem.ts index d56fab8..e1d521a 100644 --- a/packages/game-engine/src/systems/computeSystem.ts +++ b/packages/game-engine/src/systems/computeSystem.ts @@ -1,5 +1,6 @@ import type { GameState, ComputeState, InfrastructureState } from '@ai-tycoon/shared'; import { FLOPS_TO_TOKENS_MULTIPLIER } from '@ai-tycoon/shared'; +import type { ResearchBonuses } from './researchBonuses'; export interface CapacityResult { totalFlops: number; @@ -13,20 +14,19 @@ export interface CapacityResult { 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 trainingAllocation = state.compute.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 = totalTrainingFlops * trainingAllocation + totalInferenceFlops * trainingAllocation * 0.3; + const inferenceBoost = 1 + (researchBonuses?.tokensPerFlopBonus ?? 0) + (researchBonuses?.inferenceEfficiencyBonus ?? 0); const effectiveInferenceFlops = - totalInferenceFlops * inferenceAllocation + - totalTrainingFlops * inferenceAllocation * 0.5; + (totalInferenceFlops * inferenceAllocation + + totalTrainingFlops * inferenceAllocation * 0.5) * inferenceBoost; const tokensPerSecondCapacity = effectiveInferenceFlops * FLOPS_TO_TOKENS_MULTIPLIER; diff --git a/packages/game-engine/src/systems/infrastructureSystem.ts b/packages/game-engine/src/systems/infrastructureSystem.ts index f1fa0ed..b2d81dc 100644 --- a/packages/game-engine/src/systems/infrastructureSystem.ts +++ b/packages/game-engine/src/systems/infrastructureSystem.ts @@ -24,6 +24,7 @@ import { estimateNetworkSlots, } from '@ai-tycoon/shared'; import type { TickNotification } from '../tick'; +import type { ResearchBonuses } from './researchBonuses'; export interface InfraTickResult { infrastructure: InfrastructureState; @@ -463,7 +464,7 @@ function computeInterconnectMultiplier( // --- Main Infrastructure Tick --- -export function processInfrastructure(state: GameState): InfraTickResult { +export function processInfrastructure(state: GameState, researchBonuses?: ResearchBonuses): InfraTickResult { const notifications: TickNotification[] = []; let repairCosts = 0; @@ -587,7 +588,9 @@ export function processInfrastructure(state: GameState): InfraTickResult { 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; if (newProgress < cohort.stageTotal) { @@ -728,8 +731,9 @@ export function processInfrastructure(state: GameState): InfraTickResult { } const pue = COOLING_TYPE_CONFIGS[dc.coolingType].pueMultiplier; + const energyReduction = researchBonuses?.energyCostReduction ?? 0; 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 currentUptime = totalRacksInDc > 0 ? effectiveComputeRacks / totalRacksInDc : 1; diff --git a/packages/game-engine/src/systems/modelSystem.ts b/packages/game-engine/src/systems/modelSystem.ts index 22e7184..f3c51f6 100644 --- a/packages/game-engine/src/systems/modelSystem.ts +++ b/packages/game-engine/src/systems/modelSystem.ts @@ -21,16 +21,21 @@ import { DISTILLATION_BASE_RETENTION, QUANTIZATION_TICKS, } from '@ai-tycoon/shared'; +import type { ResearchBonuses } from './researchBonuses'; export interface ModelTickResult { modelsState: ModelsState; 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 notifications: ModelTickResult['notifications'] = []; + let totalReputationHit = 0; + let totalLegalCosts = 0; let baseModels = [...state.models.baseModels]; let families = [...state.models.families]; @@ -40,7 +45,8 @@ export function processModels(state: GameState): ModelTickResult { state.talent.departments.research.effectiveness; const engineerBoost = state.talent.departments.engineering.headcount * 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[] = []; @@ -51,7 +57,8 @@ export function processModels(state: GameState): ModelTickResult { } 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) { updatedPipelines.push({ ...pipeline, status: 'stalled' }); continue; @@ -62,7 +69,8 @@ export function processModels(state: GameState): ModelTickResult { if (pipeline.currentStage === '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); let tickDelay = 0; @@ -81,7 +89,10 @@ export function processModels(state: GameState): ModelTickResult { tickDelay += event.impact.ticksDelayed ?? 0; notifications.push({ title: 'Hardware Failure', message: `${pipeline.modelName}: GPU failure during training. Recovering from checkpoint.`, type: 'warning' }); } 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'; notifications.push({ title: 'Pre-training Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' }); } else { - const model = createBaseModel(updated, state); + const model = createBaseModel(updated, state, researchBonuses); baseModels = [...baseModels, model]; families = families.map(f => f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f, @@ -123,7 +134,7 @@ export function processModels(state: GameState): ModelTickResult { updated.currentStage = 'alignment'; notifications.push({ title: 'SFT Complete', message: `${pipeline.modelName}: Moving to alignment.`, type: 'info' }); } else { - const model = createBaseModel(updated, state); + const model = createBaseModel(updated, state, researchBonuses); baseModels = [...baseModels, model]; families = families.map(f => f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f, @@ -141,7 +152,7 @@ export function processModels(state: GameState): ModelTickResult { stage.isComplete = true; stage.progressTicks = stage.totalTicks; - const model = createBaseModel(updated, state); + const model = createBaseModel(updated, state, researchBonuses); baseModels = [...baseModels, model]; families = families.map(f => f.id === pipeline.familyId ? { ...f, baseModelId: model.id } : f, @@ -195,6 +206,8 @@ export function processModels(state: GameState): ModelTickResult { }, completedModels, 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 : 0; if (Math.random() < baseProbability * (hasDataPipeline ? 0.25 : 0.5) * avgLegalRisk) { + const legalCost = 5000 + Math.floor(Math.random() * 15000); events.push({ 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.', resolved: true, - impact: {}, + impact: { reputationHit: 5, legalCost }, }); } @@ -288,36 +302,61 @@ function generateTrainingEvents(pipeline: TrainingPipeline, state: GameState): T function createBaseModel( pipeline: TrainingPipeline, state: GameState, + researchBonuses?: ResearchBonuses, ): BaseModel { const { architecture, dataMix } = pipeline; const compute = pipeline.stages.pretraining.computeAllocated; const dataTokens = pipeline.stages.pretraining.targetTokens; + const params = architecture.totalParameters; - const computeFactor = Math.sqrt(compute) * 5; - const dataFactor = Math.log10(1 + dataTokens / 1e8) * 10; - const researchBonus = state.research.completedResearch.length * 3; - const efficiencyBonus = state.research.completedResearch.filter(r => r.includes('efficiency')).length * 5; + // Pillar 1: Parameters (0-30) — larger models have higher ceiling + const paramFactor = Math.min(30, Math.log2(1 + params) * 4.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') { 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 contextBonus = Math.log2(Math.max(1, architecture.contextWindow / 4)) * 3; + const contextPenalty = Math.max(0, Math.log2(architecture.contextWindow / 8)) * 2; const capabilities: ModelCapabilities = { 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)), 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)), - 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)), - agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2)), - speed: Math.max(1, 100 - architecture.totalParameters * 0.3 + efficiencyBonus * 2 + (architecture.type === 'moe' ? MOE_SPEED_MULTIPLIER * 10 : 0)), + agents: clamp(rawCapability * (0.2 + dataMix.code * 0.3 + dataMix.conversation * 0.2) + contextBonus * 0.5), + 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), }; + 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> = {}; for (const event of pipeline.events) { 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( - r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional'), - ).length; - let overallSafety = Math.min(100, 30 + safetyResearch * 15 + Math.random() * 10); + const safetyResearchBonus = researchBonuses?.safetyBonus ?? 0; + let overallSafety = Math.min(100, 30 + safetyResearchBonus + Math.random() * 10); let refusalRate = overallSafety > 60 ? 0.1 : 0.03; if (pipeline.stages.alignment?.isComplete) { diff --git a/packages/game-engine/src/systems/reputationSystem.ts b/packages/game-engine/src/systems/reputationSystem.ts index 2c8b2af..10d429d 100644 --- a/packages/game-engine/src/systems/reputationSystem.ts +++ b/packages/game-engine/src/systems/reputationSystem.ts @@ -5,13 +5,14 @@ import { SAFETY_INCIDENT_REPUTATION_HIT, LOW_SAFETY_THRESHOLD, } from '@ai-tycoon/shared'; +import type { ResearchBonuses } from './researchBonuses'; export interface ReputationTickResult { reputation: ReputationState; 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 safetyIncident = false; @@ -40,6 +41,9 @@ export function processReputation(state: GameState): ReputationState & { _safety .reduce((sum, d) => sum + d.morale, 0) / 4; employeeSatisfaction = talentMorale; + const reputationResearchBonus = researchBonuses?.reputationBonus ?? 0; + publicPerception = Math.min(100, publicPerception + reputationResearchBonus * 0.1); + const score = Math.round( safetyRecord * 0.3 + publicPerception * 0.3 + diff --git a/packages/game-engine/src/systems/researchBonuses.ts b/packages/game-engine/src/systems/researchBonuses.ts new file mode 100644 index 0000000..163fa6b --- /dev/null +++ b/packages/game-engine/src/systems/researchBonuses.ts @@ -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; +} diff --git a/packages/game-engine/src/systems/researchSystem.ts b/packages/game-engine/src/systems/researchSystem.ts index 9f2a9c7..3f8e106 100644 --- a/packages/game-engine/src/systems/researchSystem.ts +++ b/packages/game-engine/src/systems/researchSystem.ts @@ -46,6 +46,7 @@ export function getAvailableResearch(state: GameState): typeof TECH_TREE { if (state.research.activeResearch?.researchId === node.id) return false; if (eraOrder.indexOf(node.era) > currentEraIdx) 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; }); } diff --git a/packages/game-engine/src/systems/talentSystem.ts b/packages/game-engine/src/systems/talentSystem.ts index 89d74fb..d70080f 100644 --- a/packages/game-engine/src/systems/talentSystem.ts +++ b/packages/game-engine/src/systems/talentSystem.ts @@ -1,6 +1,6 @@ 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 { const departments = { ...state.talent.departments }; @@ -8,11 +8,11 @@ export function processTalent(state: GameState): TalentState { let totalSalary = 0; for (const [id, dept] of Object.entries(departments)) { totalSalary += dept.headcount * SALARY_PER_HEADCOUNT_PER_TICK; - totalSalary += dept.budget / 2592000; + totalSalary += dept.budget * 0.01; } for (const hire of state.talent.keyHires) { - totalSalary += hire.salary / 2592000; + totalSalary += hire.salary; } return { diff --git a/packages/game-engine/src/tick.ts b/packages/game-engine/src/tick.ts index 7c55b0e..ce150da 100644 --- a/packages/game-engine/src/tick.ts +++ b/packages/game-engine/src/tick.ts @@ -12,6 +12,7 @@ import { processData } from './systems/dataSystem'; import { checkEraTransition } from './systems/eraSystem'; import { processAchievements } from './systems/achievementSystem'; import { computeValuation } from './systems/fundingSystem'; +import { getResearchBonuses } from './systems/researchBonuses'; export interface TickResult { state: Partial; @@ -32,13 +33,14 @@ export function setAchievementDefinitions(defs: AchievementDefinition[]) { export function processTick(state: GameState): Partial { const notifications: TickNotification[] = []; + const researchBonuses = getResearchBonuses(state.research.completedResearch); - const infraResult = processInfrastructure(state); + const infraResult = processInfrastructure(state, researchBonuses); const infrastructure = infraResult.infrastructure; notifications.push(...infraResult.notifications); const stateWithInfra = { ...state, infrastructure }; - const modelResult = processModels(stateWithInfra); + const modelResult = processModels(stateWithInfra, researchBonuses); for (const completed of modelResult.completedModels) { notifications.push({ @@ -51,7 +53,7 @@ export function processTick(state: GameState): Partial { const stateWithModels = { ...stateWithInfra, models: modelResult.modelsState }; - const capacity = computeCapacity(state, infrastructure); + const capacity = computeCapacity(state, infrastructure, researchBonuses); const market = processMarket(stateWithModels, capacity.tokensPerSecondCapacity); const compute = finalizeCompute(capacity, market.totalTokenDemand); @@ -67,7 +69,7 @@ export function processTick(state: GameState): Partial { }); } - const reputationResult = processReputation(stateWithTalent); + const reputationResult = processReputation(stateWithTalent, researchBonuses); const { _safetyIncident, ...reputation } = reputationResult; if (_safetyIncident) { notifications.push({ @@ -76,7 +78,17 @@ export function processTick(state: GameState): Partial { 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 competitors = processCompetitors(stateWithTalent); diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts index 5f61a97..25dac65 100644 --- a/packages/shared/src/constants/gameBalance.ts +++ b/packages/shared/src/constants/gameBalance.ts @@ -5,7 +5,6 @@ import type { ConsumerTierId, ApiTierId, SeasonalPhase, EnterprisePipelineStage, export const TICK_INTERVAL_MS = 1000; export const MAX_OFFLINE_TICKS = 86_400; export const OFFLINE_EFFICIENCY = 0.8; -export const FAST_FORWARD_BATCH_SIZE = 100; export const AUTO_SAVE_INTERVAL_TICKS = 60; export const FINANCIAL_SNAPSHOT_INTERVAL = 60; 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 ALIGNMENT_TIME_FRACTION = 0.08; export const ALIGNMENT_COMPUTE_FRACTION = 0.04; -export const CHINCHILLA_OPTIMAL_RATIO = 20; export const MAX_CONCURRENT_TRAINING: Record = { 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 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_REPUTATION_HIT = 15; export const LOW_SAFETY_THRESHOLD = 40; diff --git a/packages/shared/src/types/gameState.ts b/packages/shared/src/types/gameState.ts index 671ae09..6c3451b 100644 --- a/packages/shared/src/types/gameState.ts +++ b/packages/shared/src/types/gameState.ts @@ -43,19 +43,13 @@ export type Era = 'startup' | 'scaleup' | 'bigtech' | 'agi'; export type GameSpeed = 1 | 2 | 5; export interface GameSettings { - autoSaveInterval: number; - notificationsEnabled: boolean; soundEnabled: boolean; musicVolume: number; - sfxVolume: number; } export const INITIAL_SETTINGS: GameSettings = { - autoSaveInterval: 60, - notificationsEnabled: true, soundEnabled: true, musicVolume: 0.5, - sfxVolume: 0.7, }; export const SAVE_VERSION = 7; diff --git a/packages/shared/src/types/models.ts b/packages/shared/src/types/models.ts index 40a6601..5631e55 100644 --- a/packages/shared/src/types/models.ts +++ b/packages/shared/src/types/models.ts @@ -87,6 +87,8 @@ export interface TrainingEvent { progressLost?: number; capabilityBonus?: number; capabilityDomain?: keyof ModelCapabilities; + reputationHit?: number; + legalCost?: number; }; }