Part 1 of #16 — region-level ownership foundation.
Flips internal territory ownership from whole countries (ISO_A3) to individual sub-regions,
addressed by stable, human-readable region keys (<name-slug>-<4char>, e.g. moselle-rygy, ain-lqtt, western-alun).
What changed
Manifest: scripts/build-region-keys.mjs + public/region-keys.json — 1,796
deterministic, globally-unique region keys derived from each sub-region's name slug + a
4-char hash of its stable id (salted on within-country name collisions).
Contract flip (shared/): Nation.territory, RegionMap, TerritoryChange, regionOwner, and mapFeatureOwner now key by region key. mapFeatureOwner reads the
feature's stamped properties.regionKey (never the repacked numeric feature.id, never the
raw sub-region id — the #6 guard, now region-keyed).
Server (server/): new turf-free regions.ts loads the manifest + geo once at boot,
expands each nation's country list to its sub-regions' keys at game creation, builds the LLM regions reference table (key → " (parent)"), and exposes the valid-key set; engine's runTurn/applyTurnResult/buildUserPrompt gain optional region-aware params so
existing unit tests keep passing and hallucinated keys are ignored. The deterministic stub is
unchanged.
Client: WorldMap resolves owner/flash/hover/click per region key and stamps properties.regionKey; NationPanel shows region names in the territory chips; the dossier's
selected-region label drops the now-unused parent code.
Notes
Supersedes the abandoned feat/sub-region-territory branch, which prototyped raw
feature-id keys (DEU-03). This branch (fresh off master) uses the slugged,
human-readable key scheme instead — do not merge feat/sub-region-territory.
⚠ Breaking: pre-v0.19 saved games are dropped.territory now holds region keys, not
ISO_A3 codes, so old saves can't be read. Deploy = docker compose down -v (drop the history-data volume) then up -d --build; no migration.
#16 stays open — this references it but does not close it: P2 (LLM tools/function-calling
for reliable region ops) and the no-man's-land rename/split/merge phases follow on top of this
foundation.
Verification
Typecheck, lint, and the unit suite (now 71 tests / 7 files) pass; production build clean. The
format-agnostic e2e suite (legend/stats/dates) is covered by CI.
Part 1 of #16 — region-level ownership foundation.
Flips internal territory ownership from whole countries (ISO_A3) to individual sub-regions,
addressed by stable, human-readable region keys (`<name-slug>-<4char>`, e.g. `moselle-rygy`,
`ain-lqtt`, `western-alun`).
## What changed
- **Manifest**: `scripts/build-region-keys.mjs` + `public/region-keys.json` — 1,796
deterministic, globally-unique region keys derived from each sub-region's name slug + a
4-char hash of its stable id (salted on within-country name collisions).
- **Contract flip** (`shared/`): `Nation.territory`, `RegionMap`, `TerritoryChange`,
`regionOwner`, and `mapFeatureOwner` now key by region key. `mapFeatureOwner` reads the
feature's stamped `properties.regionKey` (never the repacked numeric `feature.id`, never the
raw sub-region id — the #6 guard, now region-keyed).
- **Server** (`server/`): new turf-free `regions.ts` loads the manifest + geo once at boot,
expands each nation's country list to its sub-regions' keys at game creation, builds the LLM
`regions` reference table (key → "<name> (parent)"), and exposes the valid-key set;
`engine`'s `runTurn`/`applyTurnResult`/`buildUserPrompt` gain optional region-aware params so
existing unit tests keep passing and hallucinated keys are ignored. The deterministic stub is
unchanged.
- **Client**: `WorldMap` resolves owner/flash/hover/click per region key and stamps
`properties.regionKey`; `NationPanel` shows region names in the territory chips; the dossier's
selected-region label drops the now-unused parent code.
## Notes
- **Supersedes the abandoned `feat/sub-region-territory` branch**, which prototyped raw
feature-id keys (`DEU-03`). This branch (fresh off `master`) uses the slugged,
human-readable key scheme instead — do not merge `feat/sub-region-territory`.
- **⚠ Breaking: pre-v0.19 saved games are dropped.** `territory` now holds region keys, not
ISO_A3 codes, so old saves can't be read. Deploy = `docker compose down -v` (drop the
`history-data` volume) then `up -d --build`; no migration.
- **#16 stays open** — this references it but does not close it: P2 (LLM tools/function-calling
for reliable region ops) and the no-man's-land rename/split/merge phases follow on top of this
foundation.
## Verification
Typecheck, lint, and the unit suite (now 71 tests / 7 files) pass; production build clean. The
format-agnostic e2e suite (legend/stats/dates) is covered by CI.
Deterministic <name-slug>-<4char> key per sub-region: name via NFKD strip +
fold table (ə ø ð đ œ ł) + a-z0-9 collapse; 4-char from FNV-1a of the stable
feature id, salted on within-slug collision so all 1,796 keys are unique.
Builtins only (no @turf). Idempotent (byte-identical on re-run).
The single source of truth now keys ownership by region key (one per owned
sub-region) instead of a parent-country ISO_A3:
- shared/types.ts, shared/protocol.ts: TerritoryChange / RegionMap / regionOwner
doc flips; mapFeatureOwner now reads the feature's stamped properties.regionKey
(never the repacked numeric feature.id, nor the sub-region id — the #6 guard).
- server/regions.ts (new, turf-free): loads region-keys.json + regions.geo.json
once at boot; expands a nation's ISO_A3 list to its sub-regions' keys, builds
the LLM 'regions' reference table (key -> "name (parent)"), and exposes the
set of valid keys. Degrades gracefully when the files are missing.
- server/engine.ts: buildSystemPrompt instructs the model to use region keys;
buildUserPrompt gains an optional regionReference table; applyTurnResult gains
an optional validKeys set so hallucinated keys are ignored.
- server/index.ts: loads region data at boot; expands each nation's country list
to region keys at game creation; the jump route passes the reference + valid
keys through runTurn.
The stub is unchanged (it emits an empty, region-key-conformant territoryChanges).
- src/lib/regions.ts (new): cached, turf-free same-origin fetches of
/region-keys.json (feature id -> key) and /regions.geo.json (id -> name) so
WorldMap and NationPanel share one fetch each.
- WorldMap.tsx: stamps each feature's properties.regionKey (from the manifest,
by its stable id) and resolves owner / flash / hover / click from that key via
the shared mapFeatureOwner; the per-region flash lights the exact sub-region
whose owner changed; click carries only the sub-region name.
- NationPanel.tsx: territory chips show the region's human name (falling back
to the raw key) with the key in the title / data-attr.
- App.tsx: the selected-region dossier label drops the now-unused parent code.
- map.test.ts: rewrite the fixture for region keys; the #6 regression guard now
proves the owner resolves via properties.regionKey while the repacked numeric
feature.id and the sub-region regionId do not.
- regions.test.ts (new): the loader sees 1,796 valid keys; FRA expands to its
42 sub-region keys (all valid, all labelled '… (FRA)'); allTerritoryKeys
unions correctly; and with no data it degrades to the raw input.
- engine.test.ts: buildUserPrompt includes a regions table only when provided;
applyTurnResult with a validKeys set applies real keys and ignores bogus ones,
and without one applies all (the legacy/unit path).
Version 0.19.0 + changelog. Breaking: pre-v0.19 saved games are dropped
(territory now holds region keys, not ISO_A3 codes) — deploy wipes the
history-data volume.
kamilkrzywda
scheduled this pull request to auto merge when all checks succeed 2026-10-14 11:16:41 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part 1 of #16 — region-level ownership foundation.
Flips internal territory ownership from whole countries (ISO_A3) to individual sub-regions,
addressed by stable, human-readable region keys (
<name-slug>-<4char>, e.g.moselle-rygy,ain-lqtt,western-alun).What changed
scripts/build-region-keys.mjs+public/region-keys.json— 1,796deterministic, globally-unique region keys derived from each sub-region's name slug + a
4-char hash of its stable id (salted on within-country name collisions).
shared/):Nation.territory,RegionMap,TerritoryChange,regionOwner, andmapFeatureOwnernow key by region key.mapFeatureOwnerreads thefeature's stamped
properties.regionKey(never the repacked numericfeature.id, never theraw sub-region id — the #6 guard, now region-keyed).
server/): new turf-freeregions.tsloads the manifest + geo once at boot,expands each nation's country list to its sub-regions' keys at game creation, builds the LLM
regionsreference table (key → " (parent)"), and exposes the valid-key set;engine'srunTurn/applyTurnResult/buildUserPromptgain optional region-aware params soexisting unit tests keep passing and hallucinated keys are ignored. The deterministic stub is
unchanged.
WorldMapresolves owner/flash/hover/click per region key and stampsproperties.regionKey;NationPanelshows region names in the territory chips; the dossier'sselected-region label drops the now-unused parent code.
Notes
feat/sub-region-territorybranch, which prototyped rawfeature-id keys (
DEU-03). This branch (fresh offmaster) uses the slugged,human-readable key scheme instead — do not merge
feat/sub-region-territory.territorynow holds region keys, notISO_A3 codes, so old saves can't be read. Deploy =
docker compose down -v(drop thehistory-datavolume) thenup -d --build; no migration.for reliable region ops) and the no-man's-land rename/split/merge phases follow on top of this
foundation.
Verification
Typecheck, lint, and the unit suite (now 71 tests / 7 files) pass; production build clean. The
format-agnostic e2e suite (legend/stats/dates) is covered by CI.