Files
AIHostingTycoon/apps/web/src/store/index.ts
T
josh 901db02a6b
CI / build-and-push (push) Successful in 28s
Replace decorative overload policy with real serving pipeline and dedicated Serving page
The old overload policy had dead controls (maxQueueDepth, rateLimitPerCustomer never read)
and trivial flat penalties. This replaces it with a full serving pipeline where deployed
models form a fleet, requests route through priority/degradation logic, and policy choices
create meaningful strategic tradeoffs.

New serving pipeline: fleet building from deployed models (size/quant/MoE multipliers),
demand categorization by 5 priority tiers, enterprise capacity reservation, priority-ordered
serving with overflow behaviors (queue/reject/degrade), auto-degradation to faster models
under load, and Batch API to fill idle capacity at discounted rates.

4 new research nodes gate features progressively: Intelligent Request Routing, Priority
Queue System, Request Batching, and Auto-Scaling. New dedicated Serving page with pipeline
metrics, model fleet utilization, and research-gated policy controls.

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

1453 lines
53 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, AlignmentMethod, SizeTier,
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,
QUANTIZATION_TICKS,
SFT_TIME_FRACTION, ALIGNMENT_TIME_FRACTION,
SIZE_TIER_MAP, SIZE_TIER_LABELS,
POINT_RELEASE_TIME_FRACTION, POINT_RELEASE_MAX_VERSION,
} from '@ai-tycoon/shared';
import {
emptyDCNetworkSummary, emptyCampusNetworkSummary, emptyClusterNetworkSummary,
BENCHMARKS, TECH_TREE, onModelDeployed,
} from '@ai-tycoon/game-engine';
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
| 'market' | 'serving' | '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;
}
type ModelsTab = 'overview' | 'train' | 'models' | 'benchmarks' | 'products';
interface UIState {
activePage: ActivePage;
notifications: GameNotification[];
infraNav: InfraNav;
modelsTab: ModelsTab;
}
export interface GameNotification {
id: string;
title: string;
message: string;
type: 'info' | 'success' | 'warning' | 'danger';
tick: number;
read: boolean;
action?: { label: string; page?: ActivePage; modelsTab?: ModelsTab };
}
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;
setModelsTab: (tab: ModelsTab) => 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: {
familyId?: string;
familyName?: string;
architecture: ModelArchitecture;
dataMix: DataMixAllocation;
allocatedComputeFraction: number;
targetTokens: number;
totalTicks: number;
sftSpecializations: SFTSpecialization[];
alignmentMethod: AlignmentMethod;
alignmentSafetyWeight: number;
isPointRelease?: boolean;
sourceModelId?: string;
}) => void;
startPointRelease: (baseModelId: 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,
modelsTab: 'overview' as ModelsTab,
setActivePage: (page) => set({ activePage: page }),
setInfraNav: (nav) => set({ infraNav: nav }),
setModelsTab: (tab) => set({ modelsTab: tab }),
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) => {
let created = false;
let toastName = '';
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;
created = true;
let familyId: string;
let updatedFamilies = [...s.models.families];
if (config.familyId) {
familyId = config.familyId;
} else {
familyId = uuid();
const generation = s.models.families.length + 1;
const family: ModelFamily = {
id: familyId,
name: config.familyName ?? 'Model',
generation,
baseModelIds: [],
variants: [],
createdAtTick: s.meta.tickCount,
};
updatedFamilies = [...updatedFamilies, family];
}
const sizeTier: SizeTier = SIZE_TIER_MAP[config.architecture.totalParameters] ?? 'small';
const familyName = config.familyName ?? updatedFamilies.find(f => f.id === familyId)?.name ?? 'Model';
const version = config.isPointRelease && config.sourceModelId
? (() => {
const src = s.models.baseModels.find(m => m.id === config.sourceModelId);
return src ? Math.round((src.version + 0.1) * 10) / 10 : 1.0;
})()
: 1.0;
const modelName = `${familyName} ${SIZE_TIER_LABELS[sizeTier]} v${version.toFixed(1)}`;
toastName = modelName;
const baseTotalTicks = config.isPointRelease
? Math.ceil(config.totalTicks * POINT_RELEASE_TIME_FRACTION)
: config.totalTicks;
const pipeline: TrainingPipeline = {
id: uuid(),
familyId,
modelName,
architecture: config.architecture,
dataMix: config.dataMix,
currentStage: 'pretraining',
stages: {
pretraining: {
targetTokens: config.targetTokens,
processedTokens: 0,
computeAllocated: 0,
progressTicks: 0,
totalTicks: baseTotalTicks,
lossValue: 10,
chinchillaRatio: config.targetTokens / (config.architecture.totalParameters * 1e9),
isComplete: false,
},
sft: {
specializations: config.sftSpecializations,
progressTicks: 0,
totalTicks: Math.ceil(baseTotalTicks * SFT_TIME_FRACTION),
isComplete: false,
},
alignment: {
method: config.alignmentMethod,
safetyWeight: config.alignmentSafetyWeight,
helpfulnessWeight: 1 - config.alignmentSafetyWeight,
progressTicks: 0,
totalTicks: Math.ceil(baseTotalTicks * ALIGNMENT_TIME_FRACTION),
isComplete: false,
},
},
status: 'active',
allocatedComputeFraction: config.allocatedComputeFraction,
events: [],
startedAtTick: s.meta.tickCount,
sizeTier,
isPointRelease: config.isPointRelease ?? false,
sourceModelId: config.sourceModelId ?? null,
};
return {
models: {
...s.models,
families: updatedFamilies,
activeTrainingPipelines: [...s.models.activeTrainingPipelines, pipeline],
},
};
});
if (created) {
get().addNotification({ title: 'Training Started', message: `${toastName} training has begun.`, type: 'info', tick: get().meta.tickCount });
set({ modelsTab: 'overview' as ModelsTab });
}
},
startPointRelease: (baseModelId) => {
const s = get();
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return;
if (base.version >= POINT_RELEASE_MAX_VERSION) return;
const family = s.models.families.find(f => f.id === base.familyId);
if (!family) return;
get().startTrainingPipeline({
familyId: base.familyId,
architecture: base.architecture,
dataMix: base.dataMix,
allocatedComputeFraction: 1.0,
targetTokens: base.architecture.totalParameters * 20e9,
totalTicks: Math.ceil(base.architecture.totalParameters * 2 + 60),
sftSpecializations: base.sftSpecializations,
alignmentMethod: base.alignmentMethod ?? 'rlhf',
alignmentSafetyWeight: 0.5,
isPointRelease: true,
sourceModelId: baseModelId,
});
},
createQuantization: (baseModelId, level, variantName) => {
let created = false;
set((s) => {
const base = s.models.baseModels.find(m => m.id === baseModelId);
if (!base) return s;
created = true;
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] } };
});
if (created) {
get().addNotification({ title: 'Quantization Started', message: `${variantName} quantization in progress.`, type: 'info', tick: get().meta.tickCount });
set({ modelsTab: 'overview' as ModelsTab });
}
},
startEvaluation: (modelId, benchmarkIds) => {
let created = false;
set((s) => {
const benchmarks = BENCHMARKS.filter(b => benchmarkIds.includes(b.id));
if (benchmarks.length === 0) return s;
created = true;
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] } };
});
if (created) {
get().addNotification({ title: 'Evaluation Started', message: `${benchmarkIds.length} benchmark${benchmarkIds.length > 1 ? 's' : ''} queued.`, type: 'info', tick: get().meta.tickCount });
set({ modelsTab: 'overview' as ModelsTab });
}
},
deployModel: (modelId) => {
const modelName = get().models.baseModels.find(m => m.id === modelId)?.name ?? 'Model';
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,
})),
},
market: {
...s.market,
obsolescence: onModelDeployed(s.market.obsolescence, s.meta.tickCount),
},
}));
get().addNotification({ title: 'Model Deployed', message: `${modelName} is now serving all product lines.`, type: 'success', tick: get().meta.tickCount });
set({ modelsTab: 'products' as ModelsTab });
},
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,
),
},
}));
get().addNotification({ title: 'Variant Deployed', message: 'Variant is now live.', type: 'success', tick: get().meta.tickCount });
set({ modelsTab: 'products' as ModelsTab });
},
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;
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,
researchPoints: s.research.researchPoints - rpCost,
},
};
}),
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) => {
let opened = false;
set((s) => {
if (s.market.openSourcedModels.includes(modelId)) return s;
opened = true;
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),
},
};
});
if (opened) {
get().addNotification({ title: 'Model Open Sourced', message: 'Reputation boosted! Competitors may benefit.', type: 'success', tick: get().meta.tickCount });
}
},
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 * 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: {
...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 + 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),
},
};
}),
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, modelsTab, ...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;
},
},
),
);