Pax Historia-style sim where the LLM is the world engine.
- React 19 + Vite 6 + Tailwind v4 + shadcn/ui frontend
- Express single-port backend (/api + /sse + built client + SPA fallback)
- SQLite (better-sqlite3) persistence, one row per game
- MapLibre choropleth map (OpenFreeMap dark + ISO_A3 regions)
- Defensive turn loop: strip fences, balanced-{} extract, parse, clamp 0-100,
retry-once nudge, generic 'unclear' fallback; never crashes on bad output
- 5 seed presets (wwii-1939, coldwar-1955, modern-2025, no-wwii-1940, aegis-2400)
- LLM client over native fetch w/ injectable transport (mesh-bot pattern);
LLM_STUB=1 deterministic transport for tests/e2e
- Unit (vitest, 19) + deterministic Playwright e2e
- Multi-stage node:22-slim Docker + Traefik (history.dev.kkhost.pl:4321)
Co-Authored-By: OpenCode <opencode@users.noreply.github.com>
57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
import { Button } from './ui/button';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
|
import StatBar from './StatBar';
|
|
import { cn } from '@/lib/utils';
|
|
import type { Preset } from '@shared/types';
|
|
|
|
interface Props {
|
|
preset: Preset;
|
|
loading?: boolean;
|
|
onPick: (nationId: string) => void;
|
|
onBack: () => void;
|
|
}
|
|
|
|
/** Pick which nation to lead, with a short world-context blurb above. */
|
|
export default function NationSelect({ preset, loading, onPick, onBack }: Props) {
|
|
return (
|
|
<div className="mx-auto max-w-6xl px-6 py-8">
|
|
<Button variant="ghost" size="sm" onClick={onBack} className="mb-4">
|
|
← Back to scenarios
|
|
</Button>
|
|
<h1 className="text-3xl font-bold tracking-tight">{preset.title}</h1>
|
|
<p
|
|
className="mt-3 max-w-3xl whitespace-pre-line text-sm leading-relaxed text-muted-foreground"
|
|
data-testid="world-background"
|
|
>
|
|
{preset.worldBackground}
|
|
</p>
|
|
|
|
<h2 className="mb-4 mt-8 text-lg font-semibold">Choose your nation</h2>
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{preset.nations.map((n) => (
|
|
<Card key={n.id} className="flex flex-col" data-testid={`nation-card-${n.id}`}>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{n.name}</CardTitle>
|
|
<CardDescription>
|
|
{n.type} · {n.territory.length} region{n.territory.length === 1 ? '' : 's'}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="mt-auto space-y-1.5">
|
|
<StatBar label="Military" value={n.stats.military} />
|
|
<StatBar label="Economy" value={n.stats.economy} />
|
|
<StatBar label="Stability" value={n.stats.stability} />
|
|
<Button
|
|
className={cn('w-full mt-3')}
|
|
onClick={() => onPick(n.id)}
|
|
disabled={loading}
|
|
>
|
|
Play as {n.name}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|