History: AI-driven alternate-history simulator
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>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user