4881907c28
JWT-based auth (hono/jwt + bcrypt), anonymous-first flow preserved. Registration requires invite code when REQUIRE_INVITE=true. Admin user seeded on startup (admin/admin, forced password reset). Login accepts email or username. Admin invitations management page in sidebar. Regular users get invite-a-friend button when USER_INVITATIONS > 0. Frontend gate screen blocks game access for unregistered users with invite code entry, registration, login, and password reset flows. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1475 lines
54 KiB
TypeScript
1475 lines
54 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,
|
|
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,
|
|
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' | 'invitations' | 'settings';
|
|
|
|
export type InfraNavLevel = 'clusters' | 'cluster' | 'campus' | 'datacenter';
|
|
|
|
export interface InfraNav {
|
|
level: InfraNavLevel;
|
|
clusterId?: string;
|
|
campusId?: string;
|
|
datacenterId?: string;
|
|
}
|
|
|
|
type ModelsTab = 'overview' | 'train' | 'models' | '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;
|
|
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;
|
|
queueResearch: (researchId: string) => void;
|
|
removeFromResearchQueue: (researchId: string) => 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 });
|
|
}
|
|
},
|
|
|
|
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,
|
|
})),
|
|
deploymentVersion: s.models.deploymentVersion + 1,
|
|
},
|
|
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,
|
|
),
|
|
deploymentVersion: s.models.deploymentVersion + 1,
|
|
},
|
|
}));
|
|
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,
|
|
},
|
|
};
|
|
}),
|
|
|
|
queueResearch: (researchId) => set((s) => {
|
|
if (s.research.researchQueue.includes(researchId)) return s;
|
|
const node = TECH_TREE.find(n => n.id === researchId);
|
|
if (!node) return s;
|
|
const rpCost = node.cost.researchPoints ?? 0;
|
|
if (rpCost > s.research.researchPoints) return s;
|
|
return {
|
|
research: {
|
|
...s.research,
|
|
researchQueue: [...s.research.researchQueue, researchId],
|
|
researchPoints: s.research.researchPoints - rpCost,
|
|
},
|
|
};
|
|
}),
|
|
|
|
removeFromResearchQueue: (researchId) => set((s) => {
|
|
const idx = s.research.researchQueue.indexOf(researchId);
|
|
if (idx === -1) return s;
|
|
const node = TECH_TREE.find(n => n.id === researchId);
|
|
const rpRefund = node?.cost.researchPoints ?? 0;
|
|
return {
|
|
research: {
|
|
...s.research,
|
|
researchQueue: s.research.researchQueue.filter(id => id !== researchId),
|
|
researchPoints: s.research.researchPoints + rpRefund,
|
|
},
|
|
};
|
|
}),
|
|
|
|
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 === 9) {
|
|
const s = _persisted as Record<string, unknown>;
|
|
const compute = s.compute as Record<string, unknown>;
|
|
return { ...s, compute: { ...compute, computeHistory: [] } } as unknown as Store;
|
|
}
|
|
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 game update. Start fresh and build your AI empire!',
|
|
type: 'info' as const,
|
|
tick: 0,
|
|
read: false,
|
|
}],
|
|
infraNav: { level: 'clusters' },
|
|
} as unknown as Store;
|
|
}
|
|
return _persisted as Store;
|
|
},
|
|
merge: (persisted, current) => {
|
|
const p = persisted as Record<string, unknown>;
|
|
const c = current as unknown as Record<string, unknown>;
|
|
const merged = { ...c, ...p };
|
|
for (const key of Object.keys(c)) {
|
|
if (p[key] != null && typeof p[key] === 'object' && !Array.isArray(p[key])
|
|
&& typeof c[key] === 'object' && !Array.isArray(c[key])) {
|
|
merged[key] = { ...c[key] as Record<string, unknown>, ...p[key] as Record<string, unknown> };
|
|
}
|
|
}
|
|
return merged as unknown as Store;
|
|
},
|
|
},
|
|
),
|
|
);
|