feat: sub-region-level territory — ownership, LLM and map at region granularity (v0.19.0)

Move territory from whole-country (ISO_A3) to individual sub-region id
granularity (PR 3 of #16):

- new server/regions.ts (turf-free): loadRegions (boot), expandTerritory
  (game creation), migrateTerritoryToRegions (legacy games on db.load),
  regionNamesFor / regionLabel (LLM prompt), graceful ISO_A3 fallback
- game creation expands each preset nation's ISO_A3 list to its sub-region
  ids; pre-v0.19 saved games migrate transparently on read (idempotent)
- LLM contract: territoryChanges gain/lose sub-region ids (one region at a
  time; gain moves out of the prior owner; lost-without-gainer = unowned);
  user prompt carries a top-level regions reference table
  ({id: 'name (parent)'} over the state's territory ids)
- WorldMap: choropleth fill per sub-region (countries can split between
  nations), ownership via properties.regionId through the shared
  mapFeatureOwner, flash lights exactly the changed features
  (featuresByRegionIds)
- deterministic stub: one region-level move per non-empty round (player
  annexes the lexicographically-first region of the first other nation)
- tests: new server/regions.test.ts (real geo + fallback + seams);
  map/engine tests re-pinned to region ids; scripts/regions.test.ts stays green

Measured full wwii-1939 user prompt (411 regions): ~24.7k chars / ~6.2k
tokens — far under the 80k frontier limit, so the full regions table is
always sent (no frontier-limited mode needed).

Part of #16
This commit is contained in:
2026-10-13 22:07:18 +00:00
parent 9c29779572
commit fdf6eb236a
14 changed files with 698 additions and 162 deletions
+9 -2
View File
@@ -26,8 +26,15 @@ export const RELATION_STATUSES: RelationStatus[] = [
];
/**
* A single nation in the world. `territory` holds ISO_A3 codes; the map colors
* each of those regions with this nation's stable color (see protocol.nationColor).
* A single nation in the world. `territory` holds **sub-region ids** (e.g.
* `FR-57`) — one entry per individually-ownable sub-region, at sub-region
* granularity rather than whole-country ISO_A3. The map colors each sub-region
* with this nation's stable color (see protocol.nationColor). At scenario start
* a nation owns **all** sub-regions of the countries it holds (the server
* expands each ISO_A3 to its sub-regions; see server/regions.expandTerritory),
* so no region starts unowned. A sub-region that is in **no** nation's list is
* unowned (no-man's-land) — the map renders it grey and it can be gained or
* lost region-by-region by the LLM.
*/
export interface Nation {
id: string;