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,91 @@
|
||||
import { Badge } from './ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import StatBar from './StatBar';
|
||||
import { nationColor } from '@shared/protocol';
|
||||
import { RELATION_COLORS } from '@/lib/colors';
|
||||
import type { Nation } from '@shared/types';
|
||||
|
||||
interface Props {
|
||||
nation: Nation;
|
||||
nations: Record<string, Nation>;
|
||||
/** True when this is the nation the player is leading. */
|
||||
isPlayer: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single nation's dossier: identity, the four 0–100 stat bars (colored by the
|
||||
* nation's stable choropleth color), territory count, and its diplomatic posture
|
||||
* toward every other nation. Used for the player (highlighted) or any inspected
|
||||
* nation.
|
||||
*/
|
||||
export default function NationPanel({ nation, nations, isPlayer }: Props) {
|
||||
const color = nationColor(nation.id);
|
||||
const relations = Object.entries(nation.relations).sort(([a], [b]) => a.localeCompare(b));
|
||||
const regions = nation.territory;
|
||||
|
||||
return (
|
||||
<Card data-testid="nation-panel" className={isPlayer ? 'gap-4 border-primary/40' : 'gap-4'}>
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="inline-block size-3.5 rounded-full" style={{ background: color }} />
|
||||
<div className="space-y-0.5">
|
||||
<CardTitle className="text-lg">{nation.name}</CardTitle>
|
||||
<CardDescription>
|
||||
{nation.type} · {regions.length} region{regions.length === 1 ? '' : 's'}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={isPlayer ? 'default' : 'secondary'} className="shrink-0">
|
||||
{isPlayer ? 'Your nation' : 'Other'}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid gap-x-6 gap-y-2 sm:grid-cols-2">
|
||||
<StatBar label="Military" value={nation.stats.military} color={color} testId={isPlayer ? 'stat-military' : undefined} />
|
||||
<StatBar label="Economy" value={nation.stats.economy} color={color} testId={isPlayer ? 'stat-economy' : undefined} />
|
||||
<StatBar label="Stability" value={nation.stats.stability} color={color} testId={isPlayer ? 'stat-stability' : undefined} />
|
||||
<StatBar label="Population" value={nation.stats.population} color={color} testId={isPlayer ? 'stat-population' : undefined} />
|
||||
</div>
|
||||
|
||||
{regions.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">Territory</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{regions.map((code) => (
|
||||
<span
|
||||
key={code}
|
||||
className="rounded border px-1.5 py-0.5 text-xs tabular-nums"
|
||||
style={{ borderColor: `${color}66`, color }}
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{relations.length > 0 && (
|
||||
<div>
|
||||
<h3 className="mb-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">Diplomacy</h3>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{relations.map(([id, status]) => {
|
||||
const other = nations[id];
|
||||
const c = RELATION_COLORS[status] ?? '#94a3b8';
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className="rounded-md border px-2 py-0.5 text-xs"
|
||||
style={{ borderColor: `${c}66`, color: c }}
|
||||
>
|
||||
{other?.name ?? id}: {status}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user