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
151 lines
4.4 KiB
TypeScript
151 lines
4.4 KiB
TypeScript
// Domain types — the single source of truth shared by the frontend (src/), the
|
||
// backend (server/), and the LLM prompt contract. No runtime dependencies here.
|
||
|
||
/** A nation's 0–100 status indices. Every value is clamped to 0–100 by the engine. */
|
||
export interface NationStats {
|
||
military: number;
|
||
economy: number;
|
||
stability: number;
|
||
population: number;
|
||
}
|
||
|
||
export const STAT_KEYS = ['military', 'economy', 'stability', 'population'] as const;
|
||
export type StatKey = (typeof STAT_KEYS)[number];
|
||
|
||
export type NationType = 'major' | 'minor' | 'power' | 'alliance' | 'faction';
|
||
|
||
/** Direction of a bilateral diplomatic relationship. */
|
||
export type RelationStatus = 'ally' | 'neutral' | 'rival' | 'hostile' | 'war';
|
||
|
||
export const RELATION_STATUSES: RelationStatus[] = [
|
||
'ally',
|
||
'neutral',
|
||
'rival',
|
||
'hostile',
|
||
'war',
|
||
];
|
||
|
||
/**
|
||
* A single nation in the world. `territory` holds **sub-region ids** (e.g.
|
||
* `FR-57`) — one entry per individually-ownable sub-region, at sub-region
|
||
* granularity rather than whole-country ISO_A3. The map colors each sub-region
|
||
* with this nation's stable color (see protocol.nationColor). At scenario start
|
||
* a nation owns **all** sub-regions of the countries it holds (the server
|
||
* expands each ISO_A3 to its sub-regions; see server/regions.expandTerritory),
|
||
* so no region starts unowned. A sub-region that is in **no** nation's list is
|
||
* unowned (no-man's-land) — the map renders it grey and it can be gained or
|
||
* lost region-by-region by the LLM.
|
||
*/
|
||
export interface Nation {
|
||
id: string;
|
||
name: string;
|
||
type: NationType;
|
||
stats: NationStats;
|
||
territory: string[];
|
||
relations: Record<string, RelationStatus>;
|
||
}
|
||
|
||
/** Categories a narrative event can take. */
|
||
export type EventType =
|
||
| 'military'
|
||
| 'diplomatic'
|
||
| 'economic'
|
||
| 'political'
|
||
| 'social'
|
||
| 'disaster'
|
||
| 'scientific'
|
||
| 'cultural';
|
||
|
||
export const EVENT_TYPES: EventType[] = [
|
||
'military',
|
||
'diplomatic',
|
||
'economic',
|
||
'political',
|
||
'social',
|
||
'disaster',
|
||
'scientific',
|
||
'cultural',
|
||
];
|
||
|
||
export type EventOutcome = 'success' | 'failure' | 'partial' | 'neutral';
|
||
|
||
/** A narrative event the simulation emitted on some turn. */
|
||
export interface EventEntry {
|
||
id: string;
|
||
turn: number;
|
||
date: string;
|
||
nation: string; // nation id the event concerns
|
||
type: EventType;
|
||
title: string;
|
||
text: string;
|
||
outcome: EventOutcome;
|
||
}
|
||
|
||
/**
|
||
* A single item queued in the current round, waiting to be committed by a
|
||
* "Jump forward". `kind: 'action'` is a free-text player move; `kind:
|
||
* 'diplomacy'` is a message to a specific nation (`to`).
|
||
*/
|
||
export type RoundItem = {
|
||
kind: 'action' | 'diplomacy';
|
||
text: string;
|
||
to?: string;
|
||
};
|
||
|
||
/**
|
||
* Monotonic revision counter for a game, bumped by the server on every
|
||
* mutation (queue add/remove, turn commit) and persisted with the state.
|
||
* Clients use it to tell a fresh state from a stale one: an SSE `state`/`turn`
|
||
* snapshot (e.g. the on-connect resync) that arrives *after* a fresher REST
|
||
* response carries a lower version and must be ignored, never applied
|
||
* (see the client's "never go backwards" rule in src/App.tsx). Legacy games
|
||
* saved before this field existed are treated as version 0 by consumers.
|
||
*/
|
||
export type GameVersion = number;
|
||
|
||
/** The full, persisted state of one game. Saved to SQLite by id. */
|
||
export interface GameState {
|
||
id: string;
|
||
presetId: string;
|
||
presetTitle: string;
|
||
playerNationId: string;
|
||
turn: number;
|
||
date: string;
|
||
nations: Record<string, Nation>;
|
||
events: EventEntry[];
|
||
/**
|
||
* Actions + diplomatic messages queued this round but not yet committed.
|
||
* Adding an item never advances the turn; a "Jump forward" processes the whole
|
||
* batch in a single turn and clears this back to `[]`.
|
||
*/
|
||
pending: RoundItem[];
|
||
/**
|
||
* Monotonic revision counter (see GameVersion). Bumped on every mutation so
|
||
* clients can reject stale SSE snapshots; see the "never go backwards" rule
|
||
* in src/App.tsx. Starts at 1 on game creation.
|
||
*/
|
||
version: GameVersion;
|
||
}
|
||
|
||
/**
|
||
* A scenario the player can start. One JSON file per preset (presets/<id>.json).
|
||
* `worldBackground` + `simulationRules` become the LLM system prompt.
|
||
*/
|
||
export interface Preset {
|
||
id: string;
|
||
title: string;
|
||
startDate: string;
|
||
summary: string;
|
||
worldBackground: string;
|
||
simulationRules: string[];
|
||
nations: Nation[];
|
||
}
|
||
|
||
/** The lightweight manifest entry used to render the preset picker cards. */
|
||
export interface PresetMeta {
|
||
id: string;
|
||
title: string;
|
||
startDate: string;
|
||
summary: string;
|
||
}
|