Files
History/README.md
2026-10-11 14:36:35 +00:00

107 lines
5.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 queue
actions and diplomatic messages into a pending round; a single "Jump forward" sends the whole batch
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 <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`:
```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
**<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:
```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`); runs in CI on every PR |
## Change process
Changes follow a mandatory PR-based workflow (full rules in [AGENTS.md](./AGENTS.md#change-process-mandatory)):
work on a feature branch off `master` (no direct pushes), add a `CHANGELOG.md` entry and bump the
`package.json` version, open a PR to `master`, and let **CI pass** (typecheck → lint → unit → build → e2e)
before auto-merging. After merge, tag the version and deploy manually (`docker compose up -d --build`).
## Design notes
- **Server is authoritative.** All world state lives in SQLite; the client only renders state and
sends intents (`action` / `diplomacy` / `jump` / `advisor`). Actions and diplomatic messages are
queued into `game.pending` (no LLM call, no turn advance); a single `jump` ("Jump forward")
commits the whole batch in one LLM turn. 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.