Files
History/shared/types.ts
T
kamilkrzywda e9fa267fd0 Nation-op proposals (P4a, #16): protocol types, pendingNationOps state, engine propose/resolve (stage 1)
- shared/protocol.ts: RenameOp/SplitOp/MergeOp + NationOp union; TurnResult.nationOps (required); ConfirmNationOp/RejectNationOp request+response types
- shared/types.ts: GameState.pendingNationOps (type-only import of NationOp — no runtime cycle)
- server/engine.ts: validateNationOps parser wired into validateTurnResult; isMeaningful counts proposals; applyTurnResult appends to the surviving pending list (never applies); runTurn fallback literal; system prompt gains the nationOps JSON field + rename proposal rule (both modes); new resolveNationRef/applyNationOp/confirmNationOp/rejectNationOp (id-first then unique case-insensitive name; rename changes Nation.name only; split/merge no-op until P4b; index validation returns null for 400)
- server/db.ts: legacy rows load with pendingNationOps defaulted to []
- server/index.ts: fresh games start with pendingNationOps: []
- server/stub.ts: deterministicTurn returns nationOps: [] (the stub never proposes — unit-only coverage, matching P3)
- tests: fixture updates (engine/db/map/regions) + new suites for resolveNationRef, validateNationOps, proposal lifecycle, confirm/reject (151 tests total)
2026-10-14 12:47:58 +00:00

178 lines
6.0 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.
// (The single `import type` below erases at compile time, so no runtime
// dependency on protocol.ts is introduced — see the GameState.pendingNationOps
// doc comment.)
import type { NationOp } from './protocol';
/** 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;
/**
* (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[];
/**
* (v0.23, #16 P4a) Nation-management ops the LLM has proposed but the player
* has not yet resolved: a *pending proposal* list, as opposed to `pending`
* (the current round's queued player actions). A turn NEVER applies one —
* `applyTurnResult` appends the turn's `nationOps` to this list, and each
* proposal survives across turns until the player resolves it: confirming
* applies it (a rename changes `Nation.name` only — the id, its territory,
* its relations, and the player pointer all stay valid) and removes it;
* rejecting removes it without applying. Split/merge are declared for P4b
* and are a safe no-op on confirm until then. Always `[]` on a fresh game;
* rows saved before v0.23 load with this defaulted to `[]` (see server/db.ts).
* (The `NationOp` type lives in protocol.ts; the import there is type-only,
* so this file still has no runtime dependencies.)
*/
pendingNationOps: NationOp[];
}
/**
* 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;
}