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,173 @@
|
||||
import { useState } from 'react';
|
||||
import { Plus, Server, Cpu, MapPin } from 'lucide-react';
|
||||
import { useGameStore } from '@/store';
|
||||
import { formatMoney, formatNumber, formatPercent, GPU_CONFIGS, LOCATION_CONFIGS } from '@ai-tycoon/shared';
|
||||
import type { GpuType, LocationId } from '@ai-tycoon/shared';
|
||||
|
||||
export function InfrastructurePage() {
|
||||
const dataCenters = useGameStore((s) => s.infrastructure.dataCenters);
|
||||
const gpuPrices = useGameStore((s) => s.infrastructure.gpuMarketPrices);
|
||||
const money = useGameStore((s) => s.economy.money);
|
||||
const era = useGameStore((s) => s.meta.currentEra);
|
||||
const buildDataCenter = useGameStore((s) => s.buildDataCenter);
|
||||
const buyGpu = useGameStore((s) => s.buyGpu);
|
||||
|
||||
const [showNewDC, setShowNewDC] = useState(false);
|
||||
const [newDCName, setNewDCName] = useState('');
|
||||
const [newDCLocation, setNewDCLocation] = useState<LocationId>('us-west');
|
||||
|
||||
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
|
||||
const currentEraIdx = eraOrder.indexOf(era);
|
||||
|
||||
const availableLocations = Object.values(LOCATION_CONFIGS).filter(
|
||||
loc => eraOrder.indexOf(loc.availableAt) <= currentEraIdx,
|
||||
);
|
||||
|
||||
const availableGpus = Object.values(GPU_CONFIGS).filter(
|
||||
gpu => eraOrder.indexOf(gpu.availableAt) <= currentEraIdx,
|
||||
);
|
||||
|
||||
const handleBuildDC = () => {
|
||||
if (!newDCName.trim()) return;
|
||||
buildDataCenter(newDCName.trim(), newDCLocation);
|
||||
setNewDCName('');
|
||||
setShowNewDC(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold">Infrastructure</h2>
|
||||
<button
|
||||
onClick={() => setShowNewDC(true)}
|
||||
className="flex items-center gap-2 bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg transition-colors text-sm"
|
||||
>
|
||||
<Plus size={16} />
|
||||
New Data Center
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showNewDC && (
|
||||
<div className="bg-surface-900 border border-accent/30 rounded-xl p-4 space-y-4">
|
||||
<h3 className="font-semibold">Build New Data Center</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-surface-400 mb-1">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newDCName}
|
||||
onChange={(e) => setNewDCName(e.target.value)}
|
||||
placeholder="DC-West-01"
|
||||
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-surface-400 mb-1">Location</label>
|
||||
<select
|
||||
value={newDCLocation}
|
||||
onChange={(e) => setNewDCLocation(e.target.value as LocationId)}
|
||||
className="w-full bg-surface-800 border border-surface-600 rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-accent/50"
|
||||
>
|
||||
{availableLocations.map(loc => (
|
||||
<option key={loc.id} value={loc.id}>{loc.name} (Energy: {loc.energyCostMultiplier}x)</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-surface-400">Cost: {formatMoney(10_000)}</span>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setShowNewDC(false)} className="px-4 py-2 rounded text-sm text-surface-400 hover:text-surface-200">Cancel</button>
|
||||
<button
|
||||
onClick={handleBuildDC}
|
||||
disabled={money < 10_000 || !newDCName.trim()}
|
||||
className="px-4 py-2 rounded bg-accent hover:bg-accent-dark text-white text-sm disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Build
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{dataCenters.length === 0 ? (
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-8 text-center text-surface-500">
|
||||
<Server size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>No data centers yet. Build your first one to start hosting AI models.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{dataCenters.map(dc => (
|
||||
<div key={dc.id} className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{dc.name}</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-surface-400">
|
||||
<MapPin size={14} />
|
||||
{LOCATION_CONFIGS[dc.location].name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right text-sm">
|
||||
<div className="text-surface-400">Uptime: <span className="text-surface-200">{formatPercent(dc.currentUptime)}</span></div>
|
||||
<div className="text-surface-400">Cost: <span className="text-danger">{formatMoney(dc.energyCostPerTick + dc.maintenanceCostPerTick)}/s</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<h4 className="text-xs text-surface-400 uppercase mb-2">GPUs</h4>
|
||||
{dc.gpus.length === 0 ? (
|
||||
<p className="text-sm text-surface-500">No GPUs installed</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{dc.gpus.map(inv => (
|
||||
<div key={inv.type} className="bg-surface-800 rounded-lg p-2 text-sm">
|
||||
<div className="font-medium">{GPU_CONFIGS[inv.type].name}</div>
|
||||
<div className="text-surface-400 text-xs">
|
||||
{inv.healthyCount}/{inv.count} healthy · {formatNumber(inv.healthyCount * GPU_CONFIGS[inv.type].flopsPerUnit)} FLOPS
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-xs text-surface-400 uppercase mb-2">Buy GPUs</h4>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{availableGpus.map(gpu => (
|
||||
<button
|
||||
key={gpu.type}
|
||||
onClick={() => buyGpu(dc.id, gpu.type, 1)}
|
||||
disabled={money < gpuPrices[gpu.type]}
|
||||
className="flex items-center gap-2 bg-surface-800 hover:bg-surface-700 border border-surface-600 rounded-lg px-3 py-2 text-sm disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Cpu size={14} />
|
||||
{gpu.name}
|
||||
<span className="text-surface-400">{formatMoney(gpuPrices[gpu.type])}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
|
||||
<h3 className="text-sm font-medium text-surface-400 mb-3">GPU Market Prices</h3>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{availableGpus.map(gpu => (
|
||||
<div key={gpu.type} className="flex items-center justify-between bg-surface-800 rounded-lg p-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{gpu.name}</div>
|
||||
<div className="text-xs text-surface-400">{formatNumber(gpu.flopsPerUnit)} FLOPS/unit</div>
|
||||
</div>
|
||||
<div className="text-sm font-mono">{formatMoney(gpuPrices[gpu.type])}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user