// 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 **region keys** (see * `public/region-keys.json`) — one per owned sub-region, so the choropleth can * color individual sub-regions, not whole countries (v0.19, #16 P1). The map * resolves each sub-region's owner by its region key (see protocol.mapFeatureOwner). */ export interface Nation { id: string; name: string; type: NationType; stats: NationStats; territory: string[]; relations: Record; } /** 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; 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; /** * (v0.22, #16 P3) Region keys that became **unowned** — "no man's land" — * during play: a region that was owned and was released to no owner by a * committed turn (the LLM narrates the cause in that turn's events). This is * deliberately distinct from the many regions a preset never assigns, which * are also unowned but were never *released* mid-game; only the keys in this * list render as the distinct visible no-man's-land grey (see * protocol.regionFillStyle). A region leaves the list when any nation * re-claims it (an ordinary `gain`). Always `[]` on a fresh game — no * man's land can only arise mid-game. Rows saved before v0.22 load with * this defaulted to `[]` (see server/db.ts). */ unowned: string[]; } /** * A scenario the player can start. One JSON file per preset (presets/.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; }