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:
+12
-3
@@ -13,7 +13,8 @@ export interface StubPayload {
|
||||
turn: number;
|
||||
date: string;
|
||||
playerNationId: string;
|
||||
nations: Record<string, { stats: Record<string, number> }>;
|
||||
/** As of v0.19 territory is a list of sub-region ids (not ISO_A3 codes). */
|
||||
nations: Record<string, { stats: Record<string, number>; territory?: string[] }>;
|
||||
};
|
||||
/** The raw pending round the engine emitted (one entry per queued item). */
|
||||
pending?: RoundItem[];
|
||||
@@ -30,7 +31,12 @@ function truncate(s: string, n: number): string {
|
||||
* single political "The world moves on" event when the round is empty — so a
|
||||
* caller can predict the exact next state for assertions. It always sets the
|
||||
* player's military to (current + 5) and allies the player with the first other
|
||||
* nation, advancing the date by 2 days.
|
||||
* nation (by key order), advancing the date by 2 days. As of v0.19, when the
|
||||
* round is non-empty it also makes exactly **one** territory move: the player
|
||||
* annexes the lexicographically-first sub-region id of that first other
|
||||
* nation's territory (an empty round makes no move, and a player with nothing
|
||||
* to annex from that nation also makes no move) — so unit tests and e2e can
|
||||
* assert the exact region that changes hands.
|
||||
*/
|
||||
export function deterministicTurn(p: StubPayload): TurnResult {
|
||||
const state = p.state;
|
||||
@@ -41,6 +47,9 @@ export function deterministicTurn(p: StubPayload): TurnResult {
|
||||
? (Object.keys(state.nations).find((id) => id !== pid) ?? '')
|
||||
: 'us';
|
||||
const items = p.pending ?? [];
|
||||
// The one deterministic per-turn territory move (see the doc above).
|
||||
const otherTerritory = [...(state?.nations?.[otherId]?.territory ?? [])].sort();
|
||||
const gain = items.length > 0 && otherTerritory.length > 0 ? [otherTerritory[0]] : [];
|
||||
|
||||
const events: TurnEventSpec[] =
|
||||
items.length === 0
|
||||
@@ -76,7 +85,7 @@ export function deterministicTurn(p: StubPayload): TurnResult {
|
||||
events,
|
||||
stateChanges: [{ nation: pid, stat: 'military', op: 'set', value: mil }],
|
||||
relations: otherId ? [{ a: pid, b: otherId, set: 'ally' }] : [],
|
||||
territoryChanges: [],
|
||||
territoryChanges: gain.length > 0 ? [{ nation: pid, gain, lose: [] }] : [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user