Switch map to sub-national regions (v0.12.0)
CI / build + unit + e2e (pull_request) Successful in 1m18s

Part of #16
This commit is contained in:
2026-10-12 01:07:33 +00:00
parent 344b5b4a5e
commit 06cdbf3d81
7 changed files with 284 additions and 143 deletions
+25 -16
View File
@@ -27,8 +27,10 @@ renders territorial ownership; the whole thing persists to SQLite and streams ov
- Frontend: React 19 + Vite 6 + Tailwind CSS v4 (`@tailwindcss/vite`, no config file) + shadcn/ui
- Backend: Node 22 + Express — one process serving the built client, `/api` (REST), and `/sse`
- Persistence: `better-sqlite3` (one row per game, full `GameState` as JSON)
- Map: MapLibre GL (`maplibre-gl`) — remote OpenFreeMap `dark` basemap + a country choropleth
from `public/countries.geo.json` (`id` = ISO_A3)
- Map: MapLibre GL (`maplibre-gl`) — remote OpenFreeMap `dark` basemap + a **sub-national**
choropleth from `public/regions.geo.json` (2,597 sub-regions; feature `id` = `<ISO_A3>-<nn>`,
`properties.parent` = the country's ISO_A3). Ownership and the LLM stay at **country**
granularity, so a sub-region inherits its parent country's owner color (see #16).
- The LLM client is a thin OpenAI chat-completions wrapper over native `fetch` with an
**injectable transport** (mirrors `mesh-bot/src/llm`). A deterministic **stub** transport
(`LLM_STUB=1`) is used by all tests/e2e.
@@ -48,7 +50,8 @@ history/
├── index.html
├── presets/ # 5 scenario JSON files + index.json (manifest)
├── public/
│ └── countries.geo.json # 177 features, id=ISO_A3 (committed; Vite copies to dist/)
│ ├── countries.geo.json # 177 countries, id=ISO_A3 (input to the region generator)
│ └── regions.geo.json # 2,597 sub-regions, id=<ISO_A3>-<nn>, parent=ISO_A3 (the choropleth)
├── shared/ # NO runtime deps — the single source of truth
│ ├── types.ts # domain types (Nation, GameState, RoundItem, Preset, …)
│ └── protocol.ts # LLM TurnResult schema + AddAction/AddDiplomacy/RemoveQueueItem/Jump request types + pure helpers
@@ -140,17 +143,22 @@ history/
(model down) surfaces as an HTTP 502 to the client (shown as a toast).
- **Stable per-nation colors.** `shared.nationColor(id)` hashes a nation id → hue, shared by the
server's `RegionMap` and any client coloring so the choropleth always agrees.
- **Map click → dossier target (owner resolved from the RegionMap).** Clicking a map region does
NOT open a popup — it switches the dossier to that region's owning nation
(`setViewingId(ownerId)` in `GameScreen`); the manual "Inspect" selector and a map click both set
the same target and stay in sync. The owning nation is resolved from the authoritative `RegionMap`
by the feature's stable ISO_A3 code, which `applyRegions` stamps into `properties.regionId`
(properties survive into the rendered features) and the shared `regionOwner()` helper looks up.
It must **never** be read from the feature's top-level `id`: MapLibre repacks a GeoJSON source's
`id` into a numeric index when it stores the source, so at click/hover time `feature.id` is a
number, never the ISO_A3 (that repacking was the "no owner" bug, #6). Unowned regions (no
`RegionMap` entry) leave the current dossier target unchanged — never a broken dossier. The
hover path (a non-closeable tooltip with owner + key stats) uses the same resolution.
- **Map click → dossier target (owner resolved from the RegionMap, via the parent country).**
Clicking a sub-region does NOT open a popup — it switches the dossier to that sub-region's
**parent country's** owning nation (`focusNation(ownerId)` in `GameScreen`, which also records
which region was clicked for a small dossier label); the manual "Inspect" selector and a map click
both set the same target and stay in sync (a manual pick clears the label). Ownership is resolved
from the authoritative `RegionMap` (keyed by the **parent country's** ISO_A3) via the feature's
`properties.parent`, using the shared `mapFeatureOwner()` helper (`regionId` still holds the
sub-region's own id, e.g. `FRA-03`). It must **never** be resolved from the feature's top-level
`id` (MapLibre repacks it into a numeric index — the "no owner" bug, #6) nor from the sub-region
`id` (not a `RegionMap` key). Sub-regions of a country with no `RegionMap` entry (unowned) leave
the current dossier target unchanged — never a broken dossier. The hover path (a non-closeable
tooltip showing the sub-region name + parent country + owner + key stats) uses the same resolution.
**PR 2 of #16:** the map renders `regions.geo.json` (sub-national) but ownership/territory and the
LLM stay at country level — a nation owns whole countries, so all its sub-regions share the country's
color, and a whole-country transfer flips all its sub-regions at once (PR 3 will make sub-regions
individually ownable).
- **Deterministic stub.** `server/stub.ts` answers the model contract deterministically per
queued round item: each `action` item → one military/success event, each `diplomacy` item → one
diplomatic/partial event, an empty round → one political "The world moves on" event. The player
@@ -239,5 +247,6 @@ history/
caught.
- **`better-sqlite3`** is the only native module; the Docker build relies on its prebuilt binary
(works on `node:22-slim`/glibc). It's the sole `--external` in the tsup server bundle.
- The `dist/` client is committed to the build output only; `public/countries.geo.json` is copied
into `dist/` by Vite at build time, so the prod image serves it from `dist/` (no separate copy).
- The `dist/` client is committed to the build output only; the `public/` geo files
(`regions.geo.json` — the choropleth — and `countries.geo.json`) are copied into `dist/` by Vite
at build time, so the prod image serves them from `dist/` (no separate copy).