Files
History/shared/types.ts
T
kamilkrzywda fdf6eb236a 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
2026-10-13 22:07:18 +00:00

151 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}