Files
AIHostingTycoon/apps/web/src/store/index.ts
T
josh 8d650fefae
CI / build-and-push (push) Successful in 28s
Comprehensive UX audit fixes: navigation, feedback, affordances, and accessibility
Address 18 issues across high/medium/low impact tiers identified in a full
interface review. Key changes: Models page decomposed into tabs, confirmation
dialogs for irreversible actions (deploy/open-source/acquire), chart Y-axes
made visible, hash router extended for Market tab persistence, collapsible
sidebar, keyboard navigation shortcuts (g+key chords), notification bulk
actions, achievement progress bars, and ARIA label improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-25 09:05:26 -04:00

1388 lines
50 KiB
TypeScript

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type {
GameState, Era, GameSpeed, GameSettings,
EconomyState, InfrastructureState, ComputeState,
ResearchState, ModelsState, MarketState,
CompetitorState, TalentState, DataState,
ReputationState, AchievementState,
Cluster, Campus, DataCenter, DCTier, RackSkuId,
ActiveResearch, OwnedDataset, LocationId,
DeploymentCohort, PipelineStage,
CampusRetrofitQueue,
CoolingType, NetworkFabric,
FundingRoundType, OverloadPolicy,
TrainingPipeline, ModelFamily, DataMixAllocation,
ModelArchitecture,
SFTSpecialization, QuantizationLevel, VariantCreationJob,
EvalJob,
ConsumerTierId, ApiTierId,
} from '@ai-tycoon/shared';
import {
INITIAL_SETTINGS, SAVE_VERSION,
INITIAL_ECONOMY, INITIAL_INFRASTRUCTURE, INITIAL_COMPUTE,
INITIAL_RESEARCH, INITIAL_MODELS, INITIAL_MARKET,
INITIAL_COMPETITORS, INITIAL_TALENT, INITIAL_DATA,
INITIAL_REPUTATION, INITIAL_ACHIEVEMENTS,
DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
PIPELINE_ORDER_BASE_TICKS, DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
CLUSTER_COST_CONFIG, CAMPUS_TIER_COSTS, FIRST_CAMPUS_BUILD_TICKS,
COHORT_SCALE_FACTOR,
FUNDING_ROUNDS,
OPEN_SOURCE_REPUTATION_BOOST,
LOCATION_CONFIGS,
estimateNetworkSlots, maxComputeRacks,
uuid,
COOLING_TYPE_CONFIGS, COOLING_ORDER, NETWORK_FABRIC_CONFIGS, FABRIC_ORDER,
DEFAULT_DATA_MIX,
MAX_CONCURRENT_TRAINING,
DISTILLATION_TIME_FRACTION, DISTILLATION_COMPUTE_FRACTION,
FINETUNE_TIME_FRACTION, FINETUNE_COMPUTE_FRACTION,
QUANTIZATION_TICKS,
} from '@ai-tycoon/shared';
import {
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
BENCHMARKS,
} from '@ai-tycoon/game-engine';
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
| 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'settings';
export type InfraNavLevel = 'clusters' | 'cluster' | 'campus' | 'datacenter';
export interface InfraNav {
level: InfraNavLevel;
clusterId?: string;
campusId?: string;
datacenterId?: string;
}
interface UIState {
activePage: ActivePage;
notifications: GameNotification[];
infraNav: InfraNav;
}
export interface GameNotification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'danger';
tick: number;
read: boolean;
}
function emptyDC(): Pick<DataCenter, 'networkSummary' | 'effectiveComputeRacks' | 'usedSlots' | 'usedPowerKW' | 'energyCostPerTick' | 'maintenanceCostPerTick' | 'currentUptime'> {
return {
networkSummary: emptyDCNetworkSummary(),
effectiveComputeRacks: 0,
usedSlots: 0, usedPowerKW: 0,
energyCostPerTick: 0, maintenanceCostPerTick: 0,
currentUptime: 1,
};
}
interface Actions {
setActivePage: (page: ActivePage) => void;
setInfraNav: (nav: InfraNav) => void;
addNotification: (n: Omit<GameNotification, 'id' | 'read'>) => void;
dismissNotification: (id: string) => void;
removeNotification: (id: string) => void;
clearAllNotifications: () => void;
markAllNotificationsRead: () => void;
startNewGame: (companyName: string) => void;
setGameSpeed: (speed: GameSpeed) => void;
togglePause: () => void;
setTrainingAllocation: (ratio: number) => void;
buildCluster: (name: string, locationId: LocationId) => void;
buildCampus: (name: string, clusterId: string, dcTier: DCTier) => void;
buildDataCenter: (name: string, campusId: string) => void;
deployRacks: (dataCenterId: string, skuId: RackSkuId, quantity: number) => void;
fillDCToCapacity: (dataCenterId: string, skuId: RackSkuId) => void;
addDCsToCampus: (campusId: string, count: number) => void;
retrofitDC: (dataCenterId: string, newSkuId: RackSkuId) => void;
cancelRetrofit: (dataCenterId: string) => void;
fillCampusToCapacity: (campusId: string, skuId: RackSkuId) => void;
fillClusterToCapacity: (clusterId: string, skuId: RackSkuId) => void;
startCampusRetrofit: (campusId: string, targetSkuId: RackSkuId, maxConcurrent: number) => void;
cancelCampusRetrofit: (campusId: string) => void;
upgradeDataCenter: (dataCenterId: string, upgrade: 'cooling' | 'redundancy') => void;
upgradeCoolingType: (dataCenterId: string, targetCooling: CoolingType) => void;
upgradeNetworkFabric: (dataCenterId: string, targetFabric: NetworkFabric) => void;
startTrainingPipeline: (config: { modelName: string; architecture: ModelArchitecture; dataMix: DataMixAllocation; allocatedComputeFraction: number; targetTokens: number; totalTicks: number }) => void;
configureSFT: (pipelineId: string, specializations: import('@ai-tycoon/shared').SFTSpecialization[]) => void;
configureAlignment: (pipelineId: string, method: import('@ai-tycoon/shared').AlignmentMethod, safetyWeight: number) => void;
createDistillation: (baseModelId: string, targetParameters: number, variantName: string) => void;
createFineTune: (baseModelId: string, specialization: SFTSpecialization, variantName: string) => void;
createQuantization: (baseModelId: string, level: QuantizationLevel, variantName: string) => void;
startEvaluation: (modelId: string, benchmarkIds: string[]) => void;
deployModel: (modelId: string) => void;
deployVariant: (familyId: string, variantId: string) => void;
setProductPricing: (productLineId: string, field: string, value: number) => void;
toggleProductLine: (productLineId: string) => void;
startResearch: (research: ActiveResearch) => void;
hireDepartment: (departmentId: string, count: number) => void;
purchaseDataset: (dataset: OwnedDataset, cost: number) => void;
raiseFunding: (roundType: FundingRoundType) => void;
openSourceModel: (modelId: string) => void;
setOverloadPolicy: (policy: Partial<OverloadPolicy>) => void;
acquireCompetitor: (competitorId: string) => void;
setConsumerTierPrice: (tierId: ConsumerTierId, price: number) => void;
toggleConsumerTier: (tierId: ConsumerTierId) => void;
setApiTierPrice: (tierId: ApiTierId, field: 'monthlyFee' | 'inputTokenPrice' | 'outputTokenPrice', value: number) => void;
toggleApiTier: (tierId: ApiTierId) => void;
setDevRelSpending: (amount: number) => void;
setCodeAssistantPrice: (price: number) => void;
toggleCodeAssistant: () => void;
setAgentsPlatformPrice: (price: number) => void;
toggleAgentsPlatform: () => void;
updateState: (partial: Partial<GameState>) => void;
}
type Store = GameState & UIState & Actions;
const initialGameState: GameState = {
meta: {
saveVersion: SAVE_VERSION,
companyName: '',
currentEra: 'startup',
tickCount: 0,
lastTickTimestamp: Date.now(),
gameSpeed: 1,
isPaused: true,
createdAt: Date.now(),
totalPlayTime: 0,
settings: INITIAL_SETTINGS,
},
economy: INITIAL_ECONOMY,
infrastructure: INITIAL_INFRASTRUCTURE,
compute: INITIAL_COMPUTE,
research: INITIAL_RESEARCH,
models: INITIAL_MODELS,
market: INITIAL_MARKET,
competitors: INITIAL_COMPETITORS,
talent: INITIAL_TALENT,
data: INITIAL_DATA,
reputation: INITIAL_REPUTATION,
achievements: INITIAL_ACHIEVEMENTS,
};
// --- Helper: find entities in the hierarchy ---
function findCluster(infra: InfrastructureState, clusterId: string): Cluster | undefined {
return infra.clusters.find(c => c.id === clusterId);
}
function findCampusInCluster(cluster: Cluster, campusId: string): Campus | undefined {
return cluster.campuses.find(c => c.id === campusId);
}
function findCampus(infra: InfrastructureState, campusId: string): { cluster: Cluster; campus: Campus } | undefined {
for (const cluster of infra.clusters) {
const campus = cluster.campuses.find(c => c.id === campusId);
if (campus) return { cluster, campus };
}
return undefined;
}
function findDC(infra: InfrastructureState, dcId: string): { cluster: Cluster; campus: Campus; dc: DataCenter } | undefined {
for (const cluster of infra.clusters) {
for (const campus of cluster.campuses) {
const dc = campus.dataCenters.find(d => d.id === dcId);
if (dc) return { cluster, campus, dc };
}
}
return undefined;
}
function updateDCInInfra(infra: InfrastructureState, dcId: string, updater: (dc: DataCenter) => DataCenter): InfrastructureState {
return {
...infra,
clusters: infra.clusters.map(cluster => ({
...cluster,
campuses: cluster.campuses.map(campus => ({
...campus,
dataCenters: campus.dataCenters.map(dc =>
dc.id === dcId ? updater(dc) : dc,
),
})),
})),
};
}
function updateClusterInInfra(infra: InfrastructureState, clusterId: string, updater: (cluster: Cluster) => Cluster): InfrastructureState {
return {
...infra,
clusters: infra.clusters.map(cluster =>
cluster.id === clusterId ? updater(cluster) : cluster,
),
};
}
export function computeFillForDC(
dc: DataCenter,
skuId: RackSkuId,
availableMoney: number,
): { qty: number; cost: number } {
if (dc.status !== 'operational') return { qty: 0, cost: 0 };
if (dc.rackSkuId !== null && dc.rackSkuId !== skuId) return { qty: 0, cost: 0 };
const sku = RACK_SKU_CONFIGS[skuId];
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) return { qty: 0, cost: 0 };
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
const existingCompute = dc.computeRacksOnline + pipelineCount;
const available = maxCompute - existingCompute;
if (available <= 0) return { qty: 0, cost: 0 };
const affordableQty = Math.floor(availableMoney / sku.baseCost);
const powerLimit = Math.floor((tierConfig.powerBudgetKW - dc.computeRacksOnline * sku.powerDrawKW) / sku.powerDrawKW);
const qty = Math.min(available, affordableQty, Math.max(0, powerLimit));
if (qty <= 0) return { qty: 0, cost: 0 };
return { qty, cost: sku.baseCost * qty };
}
function startRetrofitOnDC(dc: DataCenter, targetSkuId: RackSkuId): DataCenter {
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
const totalRacks = dc.computeRacksOnline + pipelineCount;
if (totalRacks <= 0) return dc;
const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId!];
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacks));
return {
...dc,
status: 'retrofitting' as const,
deploymentCohorts: [],
retrofitState: {
fromSkuId: dc.rackSkuId!,
toSkuId: targetSkuId,
phase: 'decommissioning' as const,
progress: 0,
total: decommTicks,
racksRemaining: totalRacks,
},
};
}
function updateCampusInInfra(infra: InfrastructureState, campusId: string, updater: (campus: Campus) => Campus): InfrastructureState {
return {
...infra,
clusters: infra.clusters.map(cluster => ({
...cluster,
campuses: cluster.campuses.map(campus =>
campus.id === campusId ? updater(campus) : campus,
),
})),
};
}
export const useGameStore = create<Store>()(
persist(
(set, get) => ({
...initialGameState,
activePage: 'dashboard' as ActivePage,
notifications: [],
infraNav: { level: 'clusters' } as InfraNav,
setActivePage: (page) => set({ activePage: page }),
setInfraNav: (nav) => set({ infraNav: nav }),
addNotification: (n) => set((s) => ({
notifications: [
{ ...n, id: uuid(), read: false },
...s.notifications.slice(0, 49),
],
})),
dismissNotification: (id) => set((s) => ({
notifications: s.notifications.map(n =>
n.id === id ? { ...n, read: true } : n,
),
})),
removeNotification: (id) => set((s) => ({
notifications: s.notifications.filter(n => n.id !== id),
})),
clearAllNotifications: () => set({ notifications: [] }),
markAllNotificationsRead: () => set((s) => ({
notifications: s.notifications.map(n => ({ ...n, read: true })),
})),
startNewGame: (companyName) => set({
...initialGameState,
meta: {
...initialGameState.meta,
companyName,
isPaused: false,
createdAt: Date.now(),
lastTickTimestamp: Date.now(),
},
competitors: {
rivals: INITIAL_RIVALS,
industryBenchmark: 0,
},
activePage: 'dashboard',
notifications: [],
infraNav: { level: 'clusters' },
}),
setGameSpeed: (speed) => set((s) => ({
meta: { ...s.meta, gameSpeed: speed },
})),
togglePause: () => set((s) => ({
meta: { ...s.meta, isPaused: !s.meta.isPaused },
})),
setTrainingAllocation: (ratio) => set((s) => ({
compute: { ...s.compute, trainingAllocation: ratio, inferenceAllocation: 1 - ratio },
})),
// --- Infrastructure: Cluster ---
buildCluster: (name, locationId) => set((s) => {
const loc = LOCATION_CONFIGS[locationId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(loc.availableAt)) return s;
const existingInRegion = s.infrastructure.clusters.find(c => c.locationId === locationId);
if (existingInRegion) return s;
const isFirst = s.infrastructure.clusters.length === 0;
const cost = isFirst ? 0 : CLUSTER_COST_CONFIG.baseCost;
if (s.economy.money < cost) return s;
const cluster: Cluster = {
id: uuid(),
name,
locationId,
campuses: [],
status: isFirst ? 'operational' : 'constructing',
constructionProgress: isFirst ? 0 : 0,
constructionTotal: isFirst ? 0 : CLUSTER_COST_CONFIG.buildTimeTicks,
networkSummary: emptyClusterNetworkSummary(),
};
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: {
...s.infrastructure,
clusters: [...s.infrastructure.clusters, cluster],
},
};
}),
// --- Infrastructure: Campus ---
buildCampus: (name, clusterId, dcTier) => set((s) => {
const cluster = findCluster(s.infrastructure, clusterId);
if (!cluster || cluster.status !== 'operational') return s;
const tierConfig = DC_TIER_CONFIGS[dcTier];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(tierConfig.requiredEra)) return s;
if (tierConfig.requiredResearch && !s.research.completedResearch.includes(tierConfig.requiredResearch)) return s;
const campusCost = CAMPUS_TIER_COSTS[dcTier];
const isFirstCampus = s.infrastructure.clusters.every(c => c.campuses.length === 0);
const cost = isFirstCampus ? 0 : campusCost.baseCost;
if (s.economy.money < cost) return s;
const buildTime = isFirstCampus ? FIRST_CAMPUS_BUILD_TICKS : campusCost.buildTimeTicks;
const campus: Campus = {
id: uuid(),
name,
clusterId,
dcTier,
dataCenters: [],
status: 'constructing',
constructionProgress: 0,
constructionTotal: buildTime,
retrofitQueue: null,
networkSummary: emptyCampusNetworkSummary(),
};
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: {
...s.infrastructure,
clusters: s.infrastructure.clusters.map(c =>
c.id === clusterId
? { ...c, campuses: [...c.campuses, campus] }
: c,
),
},
};
}),
// --- Infrastructure: Data Center ---
buildDataCenter: (name, campusId) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || found.campus.status !== 'operational') return s;
const tierConfig = DC_TIER_CONFIGS[found.campus.dcTier];
if (s.economy.money < tierConfig.baseCost) return s;
const isFirstDC = s.infrastructure.clusters.every(cl =>
cl.campuses.every(ca => ca.dataCenters.length === 0),
);
const buildTime = isFirstDC ? tierConfig.firstBuildTimeTicks : tierConfig.buildTimeTicks;
const dc: DataCenter = {
id: uuid(),
name,
campusId,
tier: found.campus.dcTier,
status: 'constructing',
constructionProgress: 0,
constructionTotal: buildTime,
rackSkuId: null,
computeRacksOnline: 0,
computeRacksFailed: 0,
...emptyDC(),
deploymentCohorts: [],
retrofitState: null,
coolingLevel: 0,
redundancyLevel: 0,
coolingType: 'air' as CoolingType,
networkFabric: 'ethernet-100g' as NetworkFabric,
dcTrainingFlops: 0,
dcInferenceFlops: 0,
dcTotalVramGB: 0,
};
return {
economy: { ...s.economy, money: s.economy.money - tierConfig.baseCost },
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
dataCenters: [...campus.dataCenters, dc],
})),
};
}),
// --- Infrastructure: Deploy Racks ---
deployRacks: (dataCenterId, skuId, quantity) => set((s) => {
if (quantity <= 0) return s;
const found = findDC(s.infrastructure, dataCenterId);
if (!found || found.dc.status !== 'operational') return s;
const dc = found.dc;
if (dc.rackSkuId !== null && dc.rackSkuId !== skuId) return s;
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) return s;
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
const existingCompute = dc.computeRacksOnline + pipelineCount;
const available = maxCompute - existingCompute;
const actualQty = Math.min(quantity, available);
if (actualQty <= 0) return s;
const totalNetSlots = estimateNetworkSlots(existingCompute + actualQty, dc.tier);
const totalSlotsNeeded = existingCompute + actualQty + totalNetSlots;
if (totalSlotsNeeded > tierConfig.rackSlots) return s;
const powerNeeded = (existingCompute + actualQty) * sku.powerDrawKW;
if (powerNeeded > tierConfig.powerBudgetKW) return s;
const totalCost = sku.baseCost * actualQty;
if (s.economy.money < totalCost) return s;
const baseTicks = PIPELINE_ORDER_BASE_TICKS;
const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * actualQty));
const cohort: DeploymentCohort = {
id: uuid(),
count: actualQty,
skuId,
stage: 'ordered',
stageProgress: 0,
stageTotal: scaledTicks,
repairCount: 0,
};
return {
economy: { ...s.economy, money: s.economy.money - totalCost },
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
rackSkuId: skuId,
deploymentCohorts: [...d.deploymentCohorts, cohort],
})),
};
}),
fillDCToCapacity: (dataCenterId, skuId) => {
const s = get();
const found = findDC(s.infrastructure, dataCenterId);
if (!found || found.dc.status !== 'operational') return;
const dc = found.dc;
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const maxCompute = maxComputeRacks(tierConfig.rackSlots, dc.tier);
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
const existingCompute = dc.computeRacksOnline + pipelineCount;
const available = maxCompute - existingCompute;
if (available <= 0) return;
const sku = RACK_SKU_CONFIGS[skuId];
const affordableQty = Math.floor(s.economy.money / sku.baseCost);
const powerLimit = Math.floor((tierConfig.powerBudgetKW - dc.computeRacksOnline * sku.powerDrawKW) / sku.powerDrawKW);
const qty = Math.min(available, affordableQty, powerLimit);
if (qty <= 0) return;
get().deployRacks(dataCenterId, skuId, qty);
},
addDCsToCampus: (campusId, count) => set((s) => {
if (count <= 0) return s;
const found = findCampus(s.infrastructure, campusId);
if (!found || found.campus.status !== 'operational') return s;
const tierConfig = DC_TIER_CONFIGS[found.campus.dcTier];
const totalCost = tierConfig.baseCost * count;
if (s.economy.money < totalCost) return s;
const newDCs: DataCenter[] = [];
for (let i = 0; i < count; i++) {
newDCs.push({
id: uuid(),
name: `${found.campus.name}-DC-${found.campus.dataCenters.length + i + 1}`,
campusId,
tier: found.campus.dcTier,
status: 'constructing',
constructionProgress: 0,
constructionTotal: tierConfig.buildTimeTicks,
rackSkuId: null,
computeRacksOnline: 0,
computeRacksFailed: 0,
...emptyDC(),
deploymentCohorts: [],
retrofitState: null,
coolingLevel: 0,
redundancyLevel: 0,
coolingType: 'air' as CoolingType,
networkFabric: 'ethernet-100g' as NetworkFabric,
dcTrainingFlops: 0,
dcInferenceFlops: 0,
dcTotalVramGB: 0,
});
}
return {
economy: { ...s.economy, money: s.economy.money - totalCost },
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
dataCenters: [...campus.dataCenters, ...newDCs],
})),
};
}),
// --- Infrastructure: Retrofit ---
retrofitDC: (dataCenterId, newSkuId) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found || found.dc.status !== 'operational') return s;
const dc = found.dc;
if (!dc.rackSkuId || dc.rackSkuId === newSkuId) return s;
const sku = RACK_SKU_CONFIGS[newSkuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) return s;
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
const totalRacksToRetrofit = dc.computeRacksOnline + pipelineCount;
if (totalRacksToRetrofit <= 0) return s;
const oldSku = RACK_SKU_CONFIGS[dc.rackSkuId];
const decommTicks = Math.ceil(oldSku.pipelineTimeTicks.installation * (1 + COHORT_SCALE_FACTOR * totalRacksToRetrofit));
return {
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
status: 'retrofitting' as const,
deploymentCohorts: [],
retrofitState: {
fromSkuId: d.rackSkuId!,
toSkuId: newSkuId,
phase: 'decommissioning' as const,
progress: 0,
total: decommTicks,
racksRemaining: totalRacksToRetrofit,
},
})),
};
}),
cancelRetrofit: (dataCenterId) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found || found.dc.status !== 'retrofitting') return s;
return {
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
status: 'operational' as const,
retrofitState: null,
})),
};
}),
// --- Infrastructure: Bulk Actions ---
fillCampusToCapacity: (campusId, skuId) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || found.campus.status !== 'operational') return s;
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
let remainingMoney = s.economy.money;
const dcUpdates = new Map<string, DeploymentCohort>();
for (const dc of found.campus.dataCenters) {
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) continue;
const { qty, cost } = computeFillForDC(dc, skuId, remainingMoney);
if (qty <= 0) continue;
const baseTicks = PIPELINE_ORDER_BASE_TICKS;
const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * qty));
dcUpdates.set(dc.id, {
id: uuid(),
count: qty,
skuId,
stage: 'ordered' as PipelineStage,
stageProgress: 0,
stageTotal: scaledTicks,
repairCount: 0,
});
remainingMoney -= cost;
}
if (dcUpdates.size === 0) return s;
return {
economy: { ...s.economy, money: remainingMoney },
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
dataCenters: campus.dataCenters.map(dc => {
const cohort = dcUpdates.get(dc.id);
if (!cohort) return dc;
return { ...dc, rackSkuId: skuId, deploymentCohorts: [...dc.deploymentCohorts, cohort] };
}),
})),
};
}),
fillClusterToCapacity: (clusterId, skuId) => set((s) => {
const cluster = findCluster(s.infrastructure, clusterId);
if (!cluster || cluster.status !== 'operational') return s;
const sku = RACK_SKU_CONFIGS[skuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
let remainingMoney = s.economy.money;
const allDcUpdates = new Map<string, DeploymentCohort>();
for (const campus of cluster.campuses) {
if (campus.status !== 'operational') continue;
for (const dc of campus.dataCenters) {
const coolingOk = COOLING_ORDER.indexOf(sku.requiredCooling) <= COOLING_ORDER.indexOf(dc.coolingType);
if (!coolingOk) continue;
const { qty, cost } = computeFillForDC(dc, skuId, remainingMoney);
if (qty <= 0) continue;
const baseTicks = PIPELINE_ORDER_BASE_TICKS;
const scaledTicks = Math.ceil(baseTicks * (1 + COHORT_SCALE_FACTOR * qty));
allDcUpdates.set(dc.id, {
id: uuid(),
count: qty,
skuId,
stage: 'ordered' as PipelineStage,
stageProgress: 0,
stageTotal: scaledTicks,
repairCount: 0,
});
remainingMoney -= cost;
}
}
if (allDcUpdates.size === 0) return s;
return {
economy: { ...s.economy, money: remainingMoney },
infrastructure: updateClusterInInfra(s.infrastructure, clusterId, (cl) => ({
...cl,
campuses: cl.campuses.map(campus => ({
...campus,
dataCenters: campus.dataCenters.map(dc => {
const cohort = allDcUpdates.get(dc.id);
if (!cohort) return dc;
return { ...dc, rackSkuId: skuId, deploymentCohorts: [...dc.deploymentCohorts, cohort] };
}),
})),
})),
};
}),
startCampusRetrofit: (campusId, targetSkuId, maxConcurrent) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || found.campus.status !== 'operational') return s;
if (found.campus.retrofitQueue) return s;
const sku = RACK_SKU_CONFIGS[targetSkuId];
const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi'];
if (eraOrder.indexOf(s.meta.currentEra) < eraOrder.indexOf(sku.era)) return s;
if (sku.requiredResearch.length > 0 && !sku.requiredResearch.every(r => s.research.completedResearch.includes(r))) return s;
const eligible: string[] = [];
const skipped: string[] = [];
for (const dc of found.campus.dataCenters) {
if (dc.status !== 'operational' || !dc.rackSkuId || dc.rackSkuId === targetSkuId) {
skipped.push(dc.id);
continue;
}
const pipelineCount = dc.deploymentCohorts.filter(c => c.stage !== 'decommission').reduce((sum, c) => sum + c.count, 0);
if (dc.computeRacksOnline + pipelineCount <= 0) {
skipped.push(dc.id);
continue;
}
eligible.push(dc.id);
}
if (eligible.length === 0) return s;
const concurrent = Math.max(1, Math.min(maxConcurrent, eligible.length));
const toStartNow = eligible.slice(0, concurrent);
const pending = eligible.slice(concurrent);
const queue: CampusRetrofitQueue = {
targetSkuId,
maxConcurrent: concurrent,
pendingDCIds: pending,
activeDCIds: toStartNow,
completedDCIds: [],
skippedDCIds: skipped,
};
return {
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
retrofitQueue: queue,
dataCenters: campus.dataCenters.map(dc => {
if (!toStartNow.includes(dc.id)) return dc;
return startRetrofitOnDC(dc, targetSkuId);
}),
})),
};
}),
cancelCampusRetrofit: (campusId) => set((s) => {
const found = findCampus(s.infrastructure, campusId);
if (!found || !found.campus.retrofitQueue) return s;
const queue = found.campus.retrofitQueue;
if (queue.activeDCIds.length === 0) {
return {
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
retrofitQueue: null,
})),
};
}
return {
infrastructure: updateCampusInInfra(s.infrastructure, campusId, (campus) => ({
...campus,
retrofitQueue: { ...queue, pendingDCIds: [] },
})),
};
}),
// --- Infrastructure: Upgrades ---
upgradeDataCenter: (dataCenterId, upgrade) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found || found.dc.status !== 'operational') return s;
const dc = found.dc;
const tierConfig = DC_TIER_CONFIGS[dc.tier];
const cost = tierConfig.baseCost * DC_UPGRADE_COST_FRACTION;
if (s.economy.money < cost) return s;
const currentLevel = upgrade === 'cooling' ? dc.coolingLevel : dc.redundancyLevel;
if (currentLevel >= 1.0) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
[upgrade === 'cooling' ? 'coolingLevel' : 'redundancyLevel']:
Math.min(1.0, currentLevel + DC_UPGRADE_INCREMENT),
})),
};
}),
upgradeCoolingType: (dataCenterId, targetCooling) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found) return s;
const { dc } = found;
if (dc.status !== 'operational') return s;
const currentIdx = COOLING_ORDER.indexOf(dc.coolingType);
const targetIdx = COOLING_ORDER.indexOf(targetCooling);
if (targetIdx <= currentIdx) return s;
// Research gates
if (targetCooling === 'liquid' && !s.research.completedResearch.includes('liquid-cooling-tech')) return s;
if (targetCooling === 'immersion' && !s.research.completedResearch.includes('immersion-cooling-tech')) return s;
const cost = COOLING_TYPE_CONFIGS[targetCooling].upgradeCost[dc.tier];
if (s.economy.money < cost) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
coolingType: targetCooling,
})),
};
}),
upgradeNetworkFabric: (dataCenterId, targetFabric) => set((s) => {
const found = findDC(s.infrastructure, dataCenterId);
if (!found) return s;
const { dc } = found;
if (dc.status !== 'operational') return s;
const currentIdx = FABRIC_ORDER.indexOf(dc.networkFabric);
const targetIdx = FABRIC_ORDER.indexOf(targetFabric);
if (targetIdx <= currentIdx) return s;
// InfiniBand requires research
if ((targetFabric === 'infiniband-ndr' || targetFabric === 'infiniband-xdr')
&& !s.research.completedResearch.includes('infiniband-networking')) return s;
const cost = NETWORK_FABRIC_CONFIGS[targetFabric].upgradeCost[dc.tier];
if (s.economy.money < cost) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
infrastructure: updateDCInInfra(s.infrastructure, dataCenterId, (d) => ({
...d,
networkFabric: targetFabric,
})),
};
}),
// --- Non-infrastructure actions (unchanged) ---
startTrainingPipeline: (config) => set((s) => {
const activeCount = s.models.activeTrainingPipelines.filter(p => p.status === 'active' || p.status === 'stalled').length;
const maxSlots = MAX_CONCURRENT_TRAINING[s.meta.currentEra] ?? 1;
if (activeCount >= maxSlots) return s;
const familyId = uuid();
const pipelineId = uuid();
const generation = s.models.families.length + 1;
const family: ModelFamily = {
id: familyId,
name: config.modelName,
generation,
baseModelId: null,
variants: [],
createdAtTick: s.meta.tickCount,
};
const pipeline: TrainingPipeline = {
id: pipelineId,
familyId,
modelName: config.modelName,
architecture: config.architecture,
dataMix: config.dataMix,
currentStage: 'pretraining',
stages: {
pretraining: {
targetTokens: config.targetTokens,
processedTokens: 0,
computeAllocated: 0,
progressTicks: 0,
totalTicks: config.totalTicks,
lossValue: 10,
chinchillaRatio: config.targetTokens / (config.architecture.totalParameters * 1e9),
isComplete: false,
},
sft: null,
alignment: null,
},
status: 'active',
allocatedComputeFraction: config.allocatedComputeFraction,
events: [],
startedAtTick: s.meta.tickCount,
};
return {
models: {
...s.models,
families: [...s.models.families, family],
activeTrainingPipelines: [...s.models.activeTrainingPipelines, pipeline],
},
};
}),
configureSFT: (pipelineId, specializations) => set((s) => ({
models: {
...s.models,
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
p.id === pipelineId ? {
...p,
stages: {
...p.stages,
sft: {
specializations,
progressTicks: 0,
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.10),
isComplete: false,
},
},
} : p,
),
},
})),
configureAlignment: (pipelineId, method, safetyWeight) => set((s) => ({
models: {
...s.models,
activeTrainingPipelines: s.models.activeTrainingPipelines.map(p =>
p.id === pipelineId ? {
...p,
stages: {
...p.stages,
alignment: {
method,
safetyWeight,
helpfulnessWeight: 1 - safetyWeight,
progressTicks: 0,
totalTicks: Math.ceil(p.stages.pretraining.totalTicks * 0.08),
isComplete: false,
},
},
} : p,
),
},
})),
createDistillation: (baseModelId, targetParameters, variantName) => set((s) => {
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return s;
const job: VariantCreationJob = {
id: uuid(),
familyId: base.familyId,
baseModelId,
jobType: 'distillation',
config: { targetParameters, targetArchitecture: base.architecture.type, variantName },
progressTicks: 0,
totalTicks: Math.ceil(base.trainingCostTotal > 0 ? DISTILLATION_TIME_FRACTION * 120 : 30),
allocatedComputeFraction: DISTILLATION_COMPUTE_FRACTION,
status: 'active',
};
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
}),
createFineTune: (baseModelId, specialization, variantName) => set((s) => {
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return s;
const job: VariantCreationJob = {
id: uuid(),
familyId: base.familyId,
baseModelId,
jobType: 'fine-tuning',
config: { specialization, datasetIds: [], variantName },
progressTicks: 0,
totalTicks: Math.ceil(FINETUNE_TIME_FRACTION * 120),
allocatedComputeFraction: FINETUNE_COMPUTE_FRACTION,
status: 'active',
};
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
}),
createQuantization: (baseModelId, level, variantName) => set((s) => {
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return s;
const job: VariantCreationJob = {
id: uuid(),
familyId: base.familyId,
baseModelId,
jobType: 'quantization',
config: { level, variantName },
progressTicks: 0,
totalTicks: QUANTIZATION_TICKS,
allocatedComputeFraction: 0,
status: 'active',
};
return { models: { ...s.models, variantJobs: [...s.models.variantJobs, job] } };
}),
startEvaluation: (modelId, benchmarkIds) => set((s) => {
const benchmarks = BENCHMARKS.filter(b => benchmarkIds.includes(b.id));
if (benchmarks.length === 0) return s;
const totalTicks = benchmarks.reduce((sum, b) => sum + b.ticksToRun, 0);
const computeCost = benchmarks.reduce((sum, b) => sum + b.computeCost, 0);
const job: EvalJob = {
id: uuid(),
modelId,
benchmarkIds,
progressTicks: 0,
totalTicks,
computeAllocated: computeCost,
status: 'active',
results: [],
};
return { models: { ...s.models, evalJobs: [...s.models.evalJobs, job] } };
}),
deployModel: (modelId) => set((s) => ({
models: {
...s.models,
baseModels: s.models.baseModels.map(m =>
m.id === modelId ? { ...m, isDeployed: true } : m,
),
productLines: s.models.productLines.map(pl => ({
...pl, modelId, isActive: true,
})),
},
})),
deployVariant: (familyId, variantId) => set((s) => ({
models: {
...s.models,
families: s.models.families.map(f =>
f.id === familyId
? { ...f, variants: f.variants.map(v => v.id === variantId ? { ...v, isDeployed: true } : v) }
: f,
),
},
})),
setProductPricing: (productLineId, field, value) => set((s) => ({
models: {
...s.models,
productLines: s.models.productLines.map(pl =>
pl.id === productLineId
? { ...pl, pricing: { ...pl.pricing, [field]: value } }
: pl,
),
},
})),
toggleProductLine: (productLineId) => set((s) => ({
models: {
...s.models,
productLines: s.models.productLines.map(pl =>
pl.id === productLineId ? { ...pl, isActive: !pl.isActive } : pl,
),
},
})),
startResearch: (research) => set((s) => {
if (s.research.activeResearch) return s;
return {
research: { ...s.research, activeResearch: research },
};
}),
hireDepartment: (departmentId, count) => set((s) => {
const costPerHire = 2000;
const totalCost = costPerHire * count;
if (s.economy.money < totalCost) return s;
return {
economy: { ...s.economy, money: s.economy.money - totalCost },
talent: {
...s.talent,
departments: {
...s.talent.departments,
[departmentId]: {
...s.talent.departments[departmentId as keyof typeof s.talent.departments],
headcount: s.talent.departments[departmentId as keyof typeof s.talent.departments].headcount + count,
},
},
},
};
}),
purchaseDataset: (dataset, cost) => set((s) => {
if (s.economy.money < cost) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
data: {
...s.data,
ownedDatasets: [...s.data.ownedDatasets, dataset],
totalTrainingTokens: s.data.totalTrainingTokens + dataset.sizeTokens,
},
};
}),
raiseFunding: (roundType) => set((s) => {
const config = FUNDING_ROUNDS[roundType];
if (!config) return s;
const amount = config.amount;
const dilution = config.dilution;
return {
economy: {
...s.economy,
money: s.economy.money + amount,
funding: {
...s.economy.funding,
totalRaised: s.economy.funding.totalRaised + amount,
founderEquity: s.economy.funding.founderEquity * (1 - dilution),
completedRounds: [
...s.economy.funding.completedRounds,
{ type: roundType, amount, dilution, completedAtTick: s.meta.tickCount },
],
isPublic: roundType === 'ipo',
},
},
};
}),
openSourceModel: (modelId) => set((s) => {
if (s.market.openSourcedModels.includes(modelId)) return s;
return {
market: {
...s.market,
openSourcedModels: [...s.market.openSourcedModels, modelId],
},
reputation: {
...s.reputation,
score: Math.min(100, s.reputation.score + OPEN_SOURCE_REPUTATION_BOOST),
publicPerception: Math.min(100, s.reputation.publicPerception + OPEN_SOURCE_REPUTATION_BOOST),
},
};
}),
setOverloadPolicy: (policy) => set((s) => ({
market: {
...s.market,
overloadPolicy: { ...s.market.overloadPolicy, ...policy },
},
})),
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;
if (s.economy.money < cost) return s;
return {
economy: { ...s.economy, money: s.economy.money - cost },
competitors: {
...s.competitors,
rivals: s.competitors.rivals.map(r =>
r.id === competitorId ? { ...r, status: 'acquired' as const } : r,
),
},
talent: {
...s.talent,
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 },
},
},
};
}),
setConsumerTierPrice: (tierId, price) => set((s) => ({
market: {
...s.market,
consumerTiers: {
...s.market.consumerTiers,
tiers: {
...s.market.consumerTiers.tiers,
[tierId]: {
...s.market.consumerTiers.tiers[tierId],
config: { ...s.market.consumerTiers.tiers[tierId].config, price },
},
},
},
},
})),
toggleConsumerTier: (tierId) => set((s) => ({
market: {
...s.market,
consumerTiers: {
...s.market.consumerTiers,
tiers: {
...s.market.consumerTiers.tiers,
[tierId]: {
...s.market.consumerTiers.tiers[tierId],
config: {
...s.market.consumerTiers.tiers[tierId].config,
isActive: !s.market.consumerTiers.tiers[tierId].config.isActive,
},
},
},
},
},
})),
setApiTierPrice: (tierId, field, value) => set((s) => ({
market: {
...s.market,
apiTiers: {
...s.market.apiTiers,
tiers: {
...s.market.apiTiers.tiers,
[tierId]: {
...s.market.apiTiers.tiers[tierId],
config: { ...s.market.apiTiers.tiers[tierId].config, [field]: value },
},
},
},
},
})),
toggleApiTier: (tierId) => set((s) => ({
market: {
...s.market,
apiTiers: {
...s.market.apiTiers,
tiers: {
...s.market.apiTiers.tiers,
[tierId]: {
...s.market.apiTiers.tiers[tierId],
config: {
...s.market.apiTiers.tiers[tierId].config,
isActive: !s.market.apiTiers.tiers[tierId].config.isActive,
},
},
},
},
},
})),
setDevRelSpending: (amount) => set((s) => ({
market: {
...s.market,
developerEcosystem: {
...s.market.developerEcosystem,
devRelSpending: amount,
},
},
})),
setCodeAssistantPrice: (price) => set((s) => ({
market: {
...s.market,
codeAssistant: {
...s.market.codeAssistant,
pricePerSeat: price,
},
},
})),
toggleCodeAssistant: () => set((s) => ({
market: {
...s.market,
codeAssistant: {
...s.market.codeAssistant,
isActive: !s.market.codeAssistant.isActive,
},
},
})),
setAgentsPlatformPrice: (price) => set((s) => ({
market: {
...s.market,
agentsPlatform: {
...s.market.agentsPlatform,
pricePerSeat: price,
},
},
})),
toggleAgentsPlatform: () => set((s) => ({
market: {
...s.market,
agentsPlatform: {
...s.market.agentsPlatform,
isActive: !s.market.agentsPlatform.isActive,
},
},
})),
updateState: (partial) => set((s) => {
const newState: Partial<Store> = {};
for (const key of Object.keys(partial) as (keyof GameState)[]) {
const value = partial[key];
const current = s[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value) && typeof current === 'object' && current !== null) {
(newState as Record<string, unknown>)[key] = { ...current, ...value };
} else {
(newState as Record<string, unknown>)[key] = value;
}
}
return newState;
}),
}),
{
name: 'ai-tycoon-save',
version: SAVE_VERSION,
partialize: (state) => {
const { activePage, notifications, infraNav, ...rest } = state;
return rest;
},
migrate: (_persisted, version) => {
if (version < SAVE_VERSION) {
return {
...initialGameState,
activePage: 'dashboard' as const,
notifications: [{
id: uuid(),
title: 'Save Reset',
message: 'Your save was reset due to a major market system overhaul — shared TAM competition, multi-tier pricing, enterprise pipeline, developer ecosystem, and technology obsolescence!',
type: 'info' as const,
tick: 0,
read: false,
}],
infraNav: { level: 'clusters' },
} as unknown as Store;
}
return _persisted as Store;
},
},
),
);