Map (WorldMap): - Keep the near-black OpenFreeMap `dark` basemap (verified HTTP 200; the suggested dark-matter / dark-outdoors / liberty-dark variants 404 on that host) so the choropleth nation colors pop - Stable per-nation fills at 0.8 opacity, thin subtle borders, and a brighter thicker amber border for the player nation so "this is me" is always obvious - Hover brightens a region and shows a color-chipped owner + key stats popup; click opens a full dossier popup and focuses the left panel on that nation - Transient, self-cleaning flash (pulsing amber outline) over regions whose ownership changed this turn - Bottom-left legend auto-generated from the region map; per-scenario initial camera (Europe for the WWII scenarios, wider for Cold War / Modern / Aegis) Command-center UI: - New header bar: scenario title, prominent turn + date, the player nation (name + color chip), and a live Idle / "Simulating…" status indicator - 3-column operations-console grid: nation dossier (left) / dominant map (center) / command deck + event feed (right); stacks map-first on mobile - EventFeed: one card per event with a category icon, a colored outcome badge (success/failure/partial/neutral), a nation-colored accent edge, and the turn + date; new events slide in and flash at the top - NationPanel: value-colored animated stat bars (red → green), territory as color-coded region chips, and diplomacy as a list with a colored indicator per relation (ally/rival/war/neutral) - ActionPanel: a "Your move" composer with one prominent Send button and a spinner + "turns can take a few seconds" hint while a turn resolves - sonner error toasts: a 502 / model-down surfaces as "AI temporarily unavailable — try again" Live LLM rewire (server, previously uncommitted): - Default endpoint https://ai.kkhost.pl/v1 (vLLM, no API key); reasoning model qwen3.8-27b; always send a generous max_tokens (4000) so it finishes its chain-of-thought before emitting content; read only `content`, ignore `reasoning`. docker-compose + AGENTS.md updated to match. Tests: the deterministic Playwright e2e gains assertions for the dossier stat bars, the map legend (all nations), and the event-card type icon + outcome badge; it stays fully deterministic under LLM_STUB=1.
History
A self-contained, AI-driven alternate-history simulator in the spirit of Pax Historia — the
LLM is the world engine. Pick one of five seed scenarios, choose a nation to lead, then take
actions, send diplomatic messages, or consult an advisor. Every move is sent to the model, which
returns a structured TurnResult (events + stat / relation / territory deltas) that the engine
validates, clamps, and applies to the world state. A MapLibre choropleth renders who owns what;
everything persists to SQLite and streams over SSE on a single port.
┌────────────┐ /api + /sse ┌──────────────────────────────┐ OpenAI chat ┌──────────────┐
│ React 19 │ ───────────────▶│ Express (one process/port) │ ───────────────▶│ LLM / stub │
│ client │ (same-origin) │ /api · /sse · static · SPA │ ◀───────────────│ 27B model │
└────────────┘ │ SQLite (better-sqlite3) │ TurnResult └──────────────┘
└──────────────────────────────┘
The five scenarios
| Id | Name | Year | Nations |
|---|---|---|---|
wwii-1939 |
World War II | 1939 | 7 (Allies vs Axis) |
coldwar-1955 |
Cold War | 1955 | 7 (bloc + non-aligned) |
modern-2025 |
Modern World | 2025 | 8 (great powers) |
no-wwii-1940 |
No World War II | 1940 | 6 (divided Europe) |
aegis-2400 |
Aegis | 2400 | 6 (far-future star systems) |
Quickstart (dev)
npm install
npm run dev # Express on :4321 + Vite on :5173 (proxies /api and /sse)
Open http://localhost:5173.
The dev server talks to a live LLM by default (LLM_API_BASE / LLM_MODEL). To run fully
offline with the deterministic stub, set LLM_STUB=1:
LLM_STUB=1 npm run dev
Quickstart (Docker + Traefik)
docker compose up -d --build
The single container listens on 4321 and is routed by the shared Traefik to
https://history.dev.kkhost.pl. Game state persists on the history-data volume at
/app/data. To make a deployed instance playable without a live model:
docker compose up -d --build --force-recreate # after uncommenting LLM_STUB=1 in docker-compose.yml
Configuration (env)
| Var | Default | Notes |
|---|---|---|
LLM_API_BASE |
http://192.168.1.70:1111/v1 |
OpenAI-compatible base URL |
LLM_MODEL |
qwen3.8-27b |
model id sent in the request body |
LLM_TIMEOUT_MS |
180000 |
a local 27B model can be slow |
LLM_STUB |
(unset = live) | 1 → deterministic stub transport (no model required) |
PORT |
4321 |
the single server port (client + /api + /sse) |
DATA_DIR |
./data |
where history.db lives |
CLIENT_DIST |
(auto) | built client to serve (auto-found next to the bundle) |
VITE_PORT |
5173 |
dev-only: Vite client port (e2e uses it to dodge collisions) |
HISTORY_API_PORT |
4321 |
dev-only: the port the Vite proxy targets |
Scripts
| Command | What it does |
|---|---|
npm run dev |
Express (tsx watch, :4321) + Vite (:5173) concurrently |
npm run build |
vite build → dist/, then tsup server/index.ts → server/dist/index.js |
npm start |
run the compiled server (node server/dist/index.js) |
npm test |
vitest unit tests in server/ (19) |
npm run typecheck |
tsc --noEmit (strict) across the whole tree |
npm run lint |
ESLint (flat config) |
npm run e2e |
Playwright, fully deterministic (LLM_STUB=1) |
Design notes
- Server is authoritative. All world state lives in SQLite; the client only renders state and
sends intents (
turn/jump/advisor). The engine never trusts raw model output — it strips fences, extracts the first balanced{…},JSON.parses, ignores invalid ops, clamps stats to 0–100, retries once with a nudge, and if still unusable emits a generic "unclear" event and advances. Bad model output never crashes; only a network failure (model down) surfaces as a502toast. - No hardcoded origins. The client uses same-origin relative paths, so the identical build runs in dev (Vite proxy), in Docker, and behind Traefik without change.
- One source of truth.
shared/protocol.tsdefines the LLMTurnResult, the client↔server messages, and the pure helpers (nationColor,clampStat,addDays, …).
See AGENTS.md for the full layout, conventions, and gotchas.