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:
+15
-1
@@ -19,6 +19,7 @@ import type { GameState } from '../shared/types';
|
||||
import { createLlmClient } from './llm';
|
||||
import { makeStubLlmClient } from './stub';
|
||||
import { askAdvisor, queueRoundItem, removeRoundItem, runTurn } from './engine';
|
||||
import { expandTerritory, loadRegions } from './regions';
|
||||
import { buildRegionMap } from './map';
|
||||
import { createPresetStore } from './presets';
|
||||
import { openDb, type Db } from './db';
|
||||
@@ -41,6 +42,10 @@ function findRoot(): string {
|
||||
}
|
||||
|
||||
const root = findRoot();
|
||||
// Load the sub-region geo once at boot (a graceful no-op, with a warning, when
|
||||
// the file is missing — the territory model then degrades to whole-country
|
||||
// ISO_A3 codes). Must run before the first game creation/load.
|
||||
loadRegions(root);
|
||||
const presetsDir = path.join(root, 'presets');
|
||||
const clientDir = process.env.CLIENT_DIST ?? path.join(root, 'dist');
|
||||
const dataDir = process.env.DATA_DIR ?? path.join(root, 'data');
|
||||
@@ -116,7 +121,16 @@ app.post('/api/games', (req: Request, res) => {
|
||||
playerNationId: nation.id,
|
||||
turn: 1,
|
||||
date: preset.startDate,
|
||||
nations: Object.fromEntries(preset.nations.map((n) => [n.id, deepClone(n)])),
|
||||
// v0.19: territory is at sub-region granularity — expand each preset
|
||||
// nation's ISO_A3 country codes into all their sub-region ids (a no-op that
|
||||
// keeps the country codes when the region geo is not loaded).
|
||||
nations: Object.fromEntries(
|
||||
preset.nations.map((n) => {
|
||||
const nation = deepClone(n);
|
||||
nation.territory = expandTerritory(nation.territory);
|
||||
return [n.id, nation];
|
||||
}),
|
||||
),
|
||||
events: [],
|
||||
pending: [],
|
||||
version: 1, // the first saved revision; every later mutation bumps it
|
||||
|
||||
Reference in New Issue
Block a user