// Client <-> server protocol types + a few shared pure helpers (single source of // truth). The LLM is asked to return exactly the `TurnResult` shape; the engine // validates/clamps it before applying it to a GameState. import type { EventEntry, EventOutcome, EventType, GameState, Nation, RelationStatus, StatKey, } from './types'; /** A numeric change to one of a nation's 0–100 stats. */ export type StateChange = | { nation: string; stat: StatKey; op: 'set'; value: number } | { nation: string; stat: StatKey; op: 'add'; value: number }; /** Set the bilateral relation between nations `a` and `b` to `set` (symmetric). */ export interface RelationChange { a: string; b: string; set: RelationStatus; } /** A nation gains/loses ISO_A3 regions this turn. */ export interface TerritoryChange { nation: string; gain: string[]; lose: string[]; } /** One narrative event the LLM proposes for this turn. */ export interface TurnEventSpec { nation: string; type: EventType; title: string; text: string; outcome: EventOutcome; } /** The exact JSON object the world-simulator LLM must return for a turn. */ export interface TurnResult { date: string; events: TurnEventSpec[]; stateChanges: StateChange[]; relations: RelationChange[]; territoryChanges: TerritoryChange[]; } /** Per-region map payload: ISO_A3 -> owning nation + its stable color. */ export interface RegionOwner { color: string; owner: string; ownerId: string; } export type RegionMap = Record; /** Server -> client Server-Sent Events payload (see server/index.ts /sse). */ export type SseMessage = | { type: 'state'; state: GameState } | { type: 'turn'; state: GameState; result: TurnResult } | { type: 'error'; message: string }; // --- API request / response shapes (REST) --- export interface CreateGameRequest { presetId: string; playerNationId: string; } export interface CreateGameResponse { game: GameState; } /** Queue a free-text player action onto the pending round (no turn advance). */ export interface AddActionRequest { text: string; } export interface AddActionResponse { game: GameState; } /** Queue a diplomatic message onto the pending round (no turn advance). */ export interface AddDiplomacyRequest { to: string; message: string; } export interface AddDiplomacyResponse { game: GameState; } /** * "Jump forward" — commit the whole pending round to the model in a single * turn. The body is empty; everything being committed lives in `game.pending`. */ export type JumpRequest = Record; export interface TurnResponse { state: GameState; result: TurnResult; } export interface AdvisorRequest { /** Optional prompt to steer the brainstorm. */ prompt?: string; } export interface AdvisorResponse { suggestions: string[]; } export interface GameSummary { id: string; presetId: string; presetTitle: string; playerNationId: string; turn: number; date: string; } /** * Deterministic, stable per-nation color for the choropleth. Hashes the nation id * to a hue; sat/light tuned for the dark command-center theme. Pure + shared so * the server's RegionMap and any client-side coloring always agree. */ export function nationColor(id: string): string { let h = 2166136261; for (let i = 0; i < id.length; i++) { h = Math.imul(h ^ id.charCodeAt(i), 16777619) >>> 0; } const hue = h % 360; return `hsl(${hue} 58% 46%)`; } /** Rebuild the full event list entry shape from a TurnEventSpec + turn/date. */ export function makeEvent( spec: TurnEventSpec, turn: number, date: string, ): EventEntry { return { id: `${turn}-${spec.nation}-${spec.type}-${Math.random().toString(36).slice(2, 8)}`, turn, date, nation: spec.nation, type: spec.type, title: spec.title, text: spec.text, outcome: spec.outcome, }; } /** Defensive helpers used by both the engine (server) and tests. */ export function clampStat(v: number): number { if (!Number.isFinite(v)) return 0; return Math.max(0, Math.min(100, Math.round(v))); } /** True if the string looks like a `YYYY-MM-DD` date. */ export function isDateLike(s: string): boolean { return /^\d{4}-\d{2}-\d{2}$/.test(s) && !Number.isNaN(Date.parse(s)); } /** Advance a `YYYY-MM-DD` string by N days (UTC). Invalid input returns as-is. */ export function addDays(date: string, days: number): string { if (!isDateLike(date)) return date; const d = new Date(`${date}T00:00:00Z`); d.setUTCDate(d.getUTCDate() + days); return d.toISOString().slice(0, 10); } export function deepClone(v: T): T { return JSON.parse(JSON.stringify(v)) as T; } /** The subset of a Nation the prompt needs (keeps the LLM context small). */ export function nationForPrompt(n: Nation) { return { id: n.id, name: n.name, type: n.type, stats: n.stats, territory: n.territory, relations: n.relations, }; }