PR 1 of 2 for the per-region stats work: stats now live at region-key granularity
(GameState.regionStats); Nation.stats is derived as the area×population-weighted median
of a nation's owned regions; the LLM lever (per-region stats in the prompt) is PR 2 and is not in this change. Not deployed — production updates when PR 2 ships.
What changed
Data: new committed public/region-geo.json (1,800 entries: area km² @2dp + integer
population per region key) and public/ne-admin0-pop.json (Natural Earth admin-0 POP_EST,
249 codes — the small committed source; the 13 MB raw file is never committed). Population is
spread from each country's POP_EST to its sub-regions proportional to area by
largest-remainder, so per-country totals are preserved exactly (verified: 0 mismatches /
177 countries; global total 7,654,092,021). scripts/build-region-geo.mjs + scripts/extract-admin0-pop.mjs (new npm scripts build:region-geo / extract:admin0-pop).
Types/protocol:RegionStats + required GameState.regionStats (shared/types.ts);
pure helpers in shared/protocol.ts: weightedMedian, deriveNationStats, seedRegionStats, migrateRegionStats.
Server:server/regions.ts loads the geo manifest (probes public/ then dist/,
optional/degrading) with geoOf/hasGeo; game creation seeds regionStats; db.load
migrates legacy rows (v0.28+ rows load unchanged — no drift); applyTurnResult maintains the
table (release deletes, claim re-inits from the gainer) inside the load-bearing order
events → relations → territory → stateChanges → recompute → markers → nationOps, and
recomputes every nation's derived stats (0-region guard).
No behavior drift: the prompt is unchanged, the stub is untouched, and existing unit +
e2e assertions pass unchanged (identical-value seeding ⇒ weighted median = original;
0-region guard). Tests: 225 (was 189) incl. new server/protocol.test.ts and scripts/region-geo.test.ts.
Verification
npm run typecheck / lint / test (225 passed) / build all green locally; region-geo.json
ne-admin0-pop.json land in dist/ at build.
e2e is the CI source of truth (LLM_STUB=1).
Fixes #46
**PR 1 of 2** for the per-region stats work: stats now live at region-key granularity
(`GameState.regionStats`); `Nation.stats` is derived as the area×population-weighted median
of a nation's owned regions; the LLM lever (per-region stats in the prompt) is PR 2 and is
**not** in this change. **Not deployed** — production updates when PR 2 ships.
## What changed
- **Data:** new committed `public/region-geo.json` (1,800 entries: area km² @2dp + integer
population per region key) and `public/ne-admin0-pop.json` (Natural Earth admin-0 `POP_EST`,
249 codes — the small committed source; the 13 MB raw file is never committed). Population is
spread from each country's `POP_EST` to its sub-regions proportional to area by
largest-remainder, so per-country totals are preserved exactly (verified: 0 mismatches /
177 countries; global total 7,654,092,021). `scripts/build-region-geo.mjs` +
`scripts/extract-admin0-pop.mjs` (new npm scripts `build:region-geo` / `extract:admin0-pop`).
- **Types/protocol:** `RegionStats` + required `GameState.regionStats` (`shared/types.ts`);
pure helpers in `shared/protocol.ts`: `weightedMedian`, `deriveNationStats`,
`seedRegionStats`, `migrateRegionStats`.
- **Server:** `server/regions.ts` loads the geo manifest (probes `public/` then `dist/`,
optional/degrading) with `geoOf`/`hasGeo`; game creation seeds `regionStats`; `db.load`
migrates legacy rows (v0.28+ rows load unchanged — no drift); `applyTurnResult` maintains the
table (release deletes, claim re-inits from the gainer) inside the load-bearing order
events → relations → territory → stateChanges → recompute → markers → nationOps, and
recomputes every nation's derived stats (0-region guard).
- **No behavior drift:** the prompt is unchanged, the stub is untouched, and existing unit +
e2e assertions pass unchanged (identical-value seeding ⇒ weighted median = original;
0-region guard). Tests: 225 (was 189) incl. new `server/protocol.test.ts` and
`scripts/region-geo.test.ts`.
## Verification
- `npm run typecheck` / `lint` / `test` (225 passed) / `build` all green locally; `region-geo.json`
+ `ne-admin0-pop.json` land in `dist/` at build.
- e2e is the CI source of truth (`LLM_STUB=1`).
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.
Fixes #46
PR 1 of 2 for the per-region stats work: stats now live at region-key granularity
(
GameState.regionStats);Nation.statsis derived as the area×population-weighted medianof a nation's owned regions; the LLM lever (per-region stats in the prompt) is PR 2 and is
not in this change. Not deployed — production updates when PR 2 ships.
What changed
public/region-geo.json(1,800 entries: area km² @2dp + integerpopulation per region key) and
public/ne-admin0-pop.json(Natural Earth admin-0POP_EST,249 codes — the small committed source; the 13 MB raw file is never committed). Population is
spread from each country's
POP_ESTto its sub-regions proportional to area bylargest-remainder, so per-country totals are preserved exactly (verified: 0 mismatches /
177 countries; global total 7,654,092,021).
scripts/build-region-geo.mjs+scripts/extract-admin0-pop.mjs(new npm scriptsbuild:region-geo/extract:admin0-pop).RegionStats+ requiredGameState.regionStats(shared/types.ts);pure helpers in
shared/protocol.ts:weightedMedian,deriveNationStats,seedRegionStats,migrateRegionStats.server/regions.tsloads the geo manifest (probespublic/thendist/,optional/degrading) with
geoOf/hasGeo; game creation seedsregionStats;db.loadmigrates legacy rows (v0.28+ rows load unchanged — no drift);
applyTurnResultmaintains thetable (release deletes, claim re-inits from the gainer) inside the load-bearing order
events → relations → territory → stateChanges → recompute → markers → nationOps, and
recomputes every nation's derived stats (0-region guard).
e2e assertions pass unchanged (identical-value seeding ⇒ weighted median = original;
0-region guard). Tests: 225 (was 189) incl. new
server/protocol.test.tsandscripts/region-geo.test.ts.Verification
npm run typecheck/lint/test(225 passed) /buildall green locally;region-geo.jsonne-admin0-pop.jsonland indist/at build.LLM_STUB=1).