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:
2026-04-24 16:53:46 -04:00
commit fdc8e544ae
57 changed files with 4753 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
import { useGameStore } from '@/store';
import { formatMoney, formatNumber, formatPercent } from '@ai-tycoon/shared';
import {
DollarSign, Server, Brain, Users, TrendingUp,
TrendingDown, Minus, Cpu, Zap, Shield,
} from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Area, AreaChart } from 'recharts';
export function DashboardPage() {
const money = useGameStore((s) => s.economy.money);
const revenuePerTick = useGameStore((s) => s.economy.revenuePerTick);
const expensesPerTick = useGameStore((s) => s.economy.expensesPerTick);
const totalFlops = useGameStore((s) => s.infrastructure.totalFlops);
const dataCenters = useGameStore((s) => s.infrastructure.dataCenters);
const trainedModels = useGameStore((s) => s.models.trainedModels);
const activeTraining = useGameStore((s) => s.models.activeTraining);
const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
const reputation = useGameStore((s) => s.reputation.score);
const inferenceUtil = useGameStore((s) => s.compute.inferenceUtilization);
const financialHistory = useGameStore((s) => s.economy.financialHistory);
const era = useGameStore((s) => s.meta.currentEra);
const netIncome = revenuePerTick - expensesPerTick;
return (
<div className="space-y-6">
<h2 className="text-2xl font-bold">Dashboard</h2>
<div className="grid grid-cols-4 gap-4">
<StatCard
icon={DollarSign}
label="Cash"
value={formatMoney(money)}
subValue={`${netIncome >= 0 ? '+' : ''}${formatMoney(netIncome)}/s`}
trend={netIncome > 0 ? 'up' : netIncome < 0 ? 'down' : 'neutral'}
color="text-green-400"
/>
<StatCard
icon={Server}
label="Data Centers"
value={dataCenters.length.toString()}
subValue={`${formatNumber(totalFlops)} FLOPS`}
color="text-blue-400"
/>
<StatCard
icon={Brain}
label="Models"
value={trainedModels.length.toString()}
subValue={activeTraining ? `Training: ${Math.floor((activeTraining.progressTicks / activeTraining.totalTicks) * 100)}%` : 'Idle'}
color="text-purple-400"
/>
<StatCard
icon={Users}
label="Subscribers"
value={formatNumber(subscribers)}
subValue={`Satisfaction: ${formatPercent(useGameStore.getState().market.consumers.satisfaction)}`}
color="text-orange-400"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-surface-400 mb-4">Revenue Over Time</h3>
{financialHistory.length > 1 ? (
<ResponsiveContainer width="100%" height={200}>
<AreaChart data={financialHistory}>
<defs>
<linearGradient id="revenueGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#22c55e" stopOpacity={0.3} />
<stop offset="100%" stopColor="#22c55e" stopOpacity={0} />
</linearGradient>
</defs>
<XAxis dataKey="tick" hide />
<YAxis hide />
<Tooltip
contentStyle={{ backgroundColor: '#1e293b', border: '1px solid #334155', borderRadius: '8px' }}
labelStyle={{ color: '#94a3b8' }}
formatter={(value: number) => [formatMoney(value), 'Revenue']}
/>
<Area type="monotone" dataKey="revenue" stroke="#22c55e" fill="url(#revenueGrad)" />
</AreaChart>
</ResponsiveContainer>
) : (
<div className="h-[200px] flex items-center justify-center text-surface-500 text-sm">
No data yet start earning revenue
</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-4">System Status</h3>
<div className="space-y-4">
<StatusRow
icon={Cpu}
label="Inference Utilization"
value={formatPercent(inferenceUtil)}
bar={inferenceUtil}
barColor={inferenceUtil > 0.9 ? 'bg-danger' : inferenceUtil > 0.7 ? 'bg-warning' : 'bg-success'}
/>
<StatusRow
icon={Shield}
label="Reputation"
value={`${reputation}/100`}
bar={reputation / 100}
barColor={reputation > 70 ? 'bg-success' : reputation > 40 ? 'bg-warning' : 'bg-danger'}
/>
<StatusRow
icon={Zap}
label="Compute"
value={`${formatNumber(totalFlops)} FLOPS`}
bar={Math.min(1, totalFlops / 100)}
barColor="bg-accent"
/>
</div>
</div>
</div>
{dataCenters.length === 0 && (
<div className="bg-surface-900 border border-accent/30 rounded-xl p-6 text-center">
<h3 className="text-lg font-semibold mb-2">Get Started</h3>
<p className="text-surface-400 text-sm mb-4">
Build your first data center to start training AI models.
</p>
<button
onClick={() => useGameStore.getState().setActivePage('infrastructure')}
className="bg-accent hover:bg-accent-dark text-white font-medium px-6 py-2 rounded-lg transition-colors"
>
Build Data Center
</button>
</div>
)}
</div>
);
}
function StatCard({
icon: Icon, label, value, subValue, trend, color,
}: {
icon: typeof DollarSign;
label: string;
value: string;
subValue?: string;
trend?: 'up' | 'down' | 'neutral';
color?: string;
}) {
return (
<div className="bg-surface-900 border border-surface-700 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Icon size={16} className={color ?? 'text-surface-400'} />
<span className="text-xs text-surface-400 uppercase tracking-wider">{label}</span>
</div>
<div className="text-2xl font-bold font-mono">{value}</div>
{subValue && (
<div className={`text-xs mt-1 flex items-center gap-1 ${
trend === 'up' ? 'text-success' : trend === 'down' ? 'text-danger' : 'text-surface-400'
}`}>
{trend === 'up' && <TrendingUp size={12} />}
{trend === 'down' && <TrendingDown size={12} />}
{trend === 'neutral' && <Minus size={12} />}
{subValue}
</div>
)}
</div>
);
}
function StatusRow({
icon: Icon, label, value, bar, barColor,
}: {
icon: typeof Cpu;
label: string;
value: string;
bar: number;
barColor: string;
}) {
return (
<div>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<Icon size={14} className="text-surface-400" />
<span className="text-sm text-surface-300">{label}</span>
</div>
<span className="text-sm font-mono text-surface-200">{value}</span>
</div>
<div className="h-1.5 bg-surface-800 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
style={{ width: `${Math.min(100, bar * 100)}%` }}
/>
</div>
</div>
);
}