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:
+13
-2
@@ -24,6 +24,7 @@ import {
|
||||
} from '../shared/protocol';
|
||||
import { DEFAULT_MAX_TOKENS, type LlmClient } from './llm';
|
||||
import { ADVISOR_MARKER } from './stub';
|
||||
import { regionNamesFor } from './regions';
|
||||
|
||||
const VALID_EVENT_TYPES = new Set<string>(EVENT_TYPES);
|
||||
const VALID_RELATIONS = new Set<string>(RELATION_STATUSES);
|
||||
@@ -53,11 +54,12 @@ export function buildSystemPrompt(preset: Preset): string {
|
||||
' ],',
|
||||
' "stateChanges": [{"nation":"<nationId>","stat":"<military|economy|stability|population>","op":"<set|add>","value":<0-100>}],',
|
||||
' "relations": [{"a":"<nationId>","b":"<nationId>","set":"<ally|neutral|rival|hostile|war>"}],',
|
||||
' "territoryChanges": [{"nation":"<nationId>","gain":["<ISO_A3>"],"lose":["<ISO_A3>"]}]',
|
||||
' "territoryChanges": [{"nation":"<nationId>","gain":["<regionId>"],"lose":["<regionId>"]}]',
|
||||
'}',
|
||||
'',
|
||||
'HARD CONSTRAINTS',
|
||||
'- Use only nation ids and ISO_A3 codes that exist in the provided state.',
|
||||
'- Use only nation ids and sub-region ids (e.g. `FR-57`) that exist in the provided state.',
|
||||
'- A region id (e.g. `FR-57`) identifies a single sub-region — the `regions` table below lists each id with its name and country; a nation\'s border can change one region at a time.',
|
||||
'- Prefer 1-4 events per turn; keep each text 1-3 sentences.',
|
||||
'- Keep your internal reasoning brief (a few sentences) and go straight to the JSON result; do not over-deliberate or re-derive facts already present in the state.',
|
||||
'- Keep every stat value between 0 and 100.',
|
||||
@@ -106,6 +108,15 @@ export function buildUserPrompt(state: GameState, items: RoundItem[]): string {
|
||||
outcome: e.outcome,
|
||||
})),
|
||||
},
|
||||
// A name lookup for every region id referenced anywhere in the state, so the
|
||||
// model can map an id it is considering (e.g. `FR-57`) to a place it
|
||||
// recognizes. This is a top-level reference table keyed by sub-region id;
|
||||
// each nation's own `territory` still carries raw ids (via nationForPrompt,
|
||||
// unchanged) so the deterministic stub can parse them. When the region geo
|
||||
// is not loaded the table is empty and the model falls back to the raw ids.
|
||||
regions: regionNamesFor(
|
||||
Object.values(state.nations).flatMap((n) => n.territory),
|
||||
),
|
||||
playerActions,
|
||||
pending: items,
|
||||
roundInstruction,
|
||||
|
||||
Reference in New Issue
Block a user