The single source of truth now keys ownership by region key (one per owned sub-region) instead of a parent-country ISO_A3: - shared/types.ts, shared/protocol.ts: TerritoryChange / RegionMap / regionOwner doc flips; mapFeatureOwner now reads the feature's stamped properties.regionKey (never the repacked numeric feature.id, nor the sub-region id — the #6 guard). - server/regions.ts (new, turf-free): loads region-keys.json + regions.geo.json once at boot; expands a nation's ISO_A3 list to its sub-regions' keys, builds the LLM 'regions' reference table (key -> "name (parent)"), and exposes the set of valid keys. Degrades gracefully when the files are missing. - server/engine.ts: buildSystemPrompt instructs the model to use region keys; buildUserPrompt gains an optional regionReference table; applyTurnResult gains an optional validKeys set so hallucinated keys are ignored. - server/index.ts: loads region data at boot; expands each nation's country list to region keys at game creation; the jump route passes the reference + valid keys through runTurn. The stub is unchanged (it emits an empty, region-key-conformant territoryChanges).
146 lines
4.1 KiB
TypeScript
146 lines
4.1 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 **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<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;
|
||
}
|