Initial scaffold: AI Tycoon monorepo with core game loop
Turborepo monorepo with three packages: - packages/shared: TypeScript types for all 14 game systems + balance constants + formatting utils - packages/game-engine: Pure TS simulation engine with tick processor, economy, infrastructure, compute, research, market, and reputation systems - apps/web: React + Vite + Tailwind + Zustand frontend with sidebar dashboard layout, new game screen, dashboard with charts, infrastructure management, and model training pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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, EventState, AchievementState,
|
||||
DataCenter, GpuType, GpuInventory, TrainingJob,
|
||||
} 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_EVENTS, INITIAL_ACHIEVEMENTS,
|
||||
GPU_CONFIGS,
|
||||
} from '@ai-tycoon/shared';
|
||||
|
||||
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
|
||||
| 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'settings';
|
||||
|
||||
interface UIState {
|
||||
activePage: ActivePage;
|
||||
notifications: GameNotification[];
|
||||
}
|
||||
|
||||
export interface GameNotification {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
type: 'info' | 'success' | 'warning' | 'danger';
|
||||
tick: number;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
interface Actions {
|
||||
setActivePage: (page: ActivePage) => void;
|
||||
addNotification: (n: Omit<GameNotification, 'id' | 'read'>) => void;
|
||||
dismissNotification: (id: string) => void;
|
||||
startNewGame: (companyName: string) => void;
|
||||
setGameSpeed: (speed: GameSpeed) => void;
|
||||
togglePause: () => void;
|
||||
setTrainingAllocation: (ratio: number) => void;
|
||||
buyGpu: (dataCenterId: string, gpuType: GpuType, count: number) => void;
|
||||
buildDataCenter: (name: string, location: DataCenter['location']) => void;
|
||||
startTraining: (job: Omit<TrainingJob, 'progressTicks'>) => void;
|
||||
deployModel: (modelId: string, productLineId: string) => void;
|
||||
setProductPricing: (productLineId: string, field: string, value: number) => void;
|
||||
toggleProductLine: (productLineId: string) => 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,
|
||||
events: INITIAL_EVENTS,
|
||||
achievements: INITIAL_ACHIEVEMENTS,
|
||||
};
|
||||
|
||||
export const useGameStore = create<Store>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...initialGameState,
|
||||
activePage: 'dashboard' as ActivePage,
|
||||
notifications: [],
|
||||
|
||||
setActivePage: (page) => set({ activePage: page }),
|
||||
|
||||
addNotification: (n) => set((s) => ({
|
||||
notifications: [
|
||||
{ ...n, id: crypto.randomUUID(), read: false },
|
||||
...s.notifications.slice(0, 49),
|
||||
],
|
||||
})),
|
||||
|
||||
dismissNotification: (id) => set((s) => ({
|
||||
notifications: s.notifications.map(n =>
|
||||
n.id === id ? { ...n, read: true } : n,
|
||||
),
|
||||
})),
|
||||
|
||||
startNewGame: (companyName) => set({
|
||||
...initialGameState,
|
||||
meta: {
|
||||
...initialGameState.meta,
|
||||
companyName,
|
||||
isPaused: false,
|
||||
createdAt: Date.now(),
|
||||
lastTickTimestamp: Date.now(),
|
||||
},
|
||||
activePage: 'dashboard',
|
||||
notifications: [],
|
||||
}),
|
||||
|
||||
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 },
|
||||
})),
|
||||
|
||||
buyGpu: (dataCenterId, gpuType, count) => set((s) => {
|
||||
const price = s.infrastructure.gpuMarketPrices[gpuType] * count;
|
||||
if (s.economy.money < price) return s;
|
||||
|
||||
const dataCenters = s.infrastructure.dataCenters.map(dc => {
|
||||
if (dc.id !== dataCenterId) return dc;
|
||||
const existingIdx = dc.gpus.findIndex(g => g.type === gpuType);
|
||||
let gpus: GpuInventory[];
|
||||
if (existingIdx >= 0) {
|
||||
gpus = dc.gpus.map((g, i) =>
|
||||
i === existingIdx
|
||||
? { ...g, count: g.count + count, healthyCount: g.healthyCount + count }
|
||||
: g,
|
||||
);
|
||||
} else {
|
||||
gpus = [...dc.gpus, { type: gpuType, count, healthyCount: count, failedCount: 0 }];
|
||||
}
|
||||
return { ...dc, gpus };
|
||||
});
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - price },
|
||||
infrastructure: { ...s.infrastructure, dataCenters },
|
||||
};
|
||||
}),
|
||||
|
||||
buildDataCenter: (name, location) => set((s) => {
|
||||
const buildCost = 10_000;
|
||||
if (s.economy.money < buildCost) return s;
|
||||
|
||||
const dc: DataCenter = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
location,
|
||||
gpus: [],
|
||||
maxCapacity: 100,
|
||||
coolingLevel: 0.5,
|
||||
redundancyLevel: 0.3,
|
||||
currentUptime: 1,
|
||||
energyCostPerTick: 0,
|
||||
maintenanceCostPerTick: 0,
|
||||
};
|
||||
|
||||
return {
|
||||
economy: { ...s.economy, money: s.economy.money - buildCost },
|
||||
infrastructure: {
|
||||
...s.infrastructure,
|
||||
dataCenters: [...s.infrastructure.dataCenters, dc],
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
startTraining: (job) => set((s) => ({
|
||||
models: {
|
||||
...s.models,
|
||||
activeTraining: { ...job, progressTicks: 0 },
|
||||
},
|
||||
})),
|
||||
|
||||
deployModel: (modelId, productLineId) => set((s) => ({
|
||||
models: {
|
||||
...s.models,
|
||||
trainedModels: s.models.trainedModels.map(m =>
|
||||
m.id === modelId ? { ...m, isDeployed: true } : m,
|
||||
),
|
||||
productLines: s.models.productLines.map(pl =>
|
||||
pl.id === productLineId ? { ...pl, modelId, isActive: true } : pl,
|
||||
),
|
||||
},
|
||||
})),
|
||||
|
||||
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,
|
||||
),
|
||||
},
|
||||
})),
|
||||
|
||||
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',
|
||||
partialize: (state) => {
|
||||
const { activePage, notifications, ...rest } = state;
|
||||
return rest;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user