Game balance audit: wire research effects, rework capability formula, fix dead systems
CI / build-and-push (push) Successful in 32s
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:
@@ -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),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user