# 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) ```bash npm install npm run dev # Express on :4321 + Vite on :5173 (proxies /api and /sse) ``` Open . 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`: ```bash LLM_STUB=1 npm run dev ``` ## Quickstart (Docker + Traefik) ```bash docker compose up -d --build ``` The single container listens on **4321** and is routed by the shared Traefik to ****. Game state persists on the `history-data` volume at `/app/data`. To make a deployed instance playable without a live model: ```bash 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.parse`s, 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 a `502` toast. - **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.ts` defines the LLM `TurnResult`, the client↔server messages, and the pure helpers (`nationColor`, `clampStat`, `addDays`, …). See [AGENTS.md](./AGENTS.md) for the full layout, conventions, and gotchas.