feat: #80 + #81 alliances & space program — 13th & 14th abilities (v0.45.0)
CI / build + unit + e2e (pull_request) Successful in 2m20s

#80: TurnResult.alliances — form/break durable mutual-defense pacts (GameState.alliances; pair relation → ally/neutral, an active war overrides).
#81: TurnResult.space — launch/deorbit satellites (GameState.satellites; +1 stability per active satellite per turn).
Both appended last to the ABILITIES registry (entries 13 & 14, after contested); durable in SQLite via the stateFields loop, mirrored to regionStats, stats re-derived. System + user prompt goldens updated; 738 unit tests passing.
This commit is contained in:
2026-10-17 16:54:28 +00:00
parent 0a6c5aef9c
commit 283f9e0306
26 changed files with 1493 additions and 49 deletions
+123
View File
@@ -830,6 +830,108 @@ export interface ContestedRegion {
parties: string[];
}
// --- Alliances / mutual-defense treaties (v0.45.0, #80) ----------------------
//
// A durable, symmetric pact between two distinct nations. The LLM proposes
// `form` / `break` ops each turn; the engine applies them directly
// (`applyTurnResult` → the alliances ability) and persists the active pacts in
// `GameState.alliances`. A pair holds at most one pact (re-forming is a
// no-op), and an active war between the pair overrides the `ally` relation —
// the pact still exists but the `war` relation stays in force until the war
// ends.
/** One active alliance: which two nations are bound, and when the pact was formed. */
export interface Alliance {
/** Stable id (`al1`, `al2`, …) assigned by the engine on formation. */
id: string;
/** The two party nation ids (sorted, distinct). */
parties: string[];
/** The turn number the pact was formed on. */
formedTurn: number;
/** The in-game date the pact took effect on (`YYYY-MM-DD`). */
formedDate: string;
}
/** A single "form a new alliance between a and b" op. */
export interface AllianceFormOp {
/** One party (a nation id or name, resolved via `resolveNationRef`). */
a: string;
/** The other party (a nation id or name, distinct from `a`). */
b: string;
}
/** A single "break the alliance between a and b" op. */
export interface AllianceBreakOp {
/** One party (a nation id or name). */
a: string;
/** The other party (a nation id or name). */
b: string;
}
/** The alliances block of a `TurnResult`. Required; default `{ form: [], break: [] }`. */
export interface AllianceOps {
/** New pacts to form this turn. */
form: AllianceFormOp[];
/** Existing pacts to end this turn. */
break: AllianceBreakOp[];
}
// --- Space program / satellites (v0.45.0, #81) -------------------------------
//
// Durable per-turn space ops: the LLM launches satellites over regions it
// owns and deorbits active ones by id. Each active satellite grants its owner
// a small deterministic +1 stability per committed turn (mirrored to its
// regions, re-derived), stacked per satellite. Launches/deorbits never move
// borders — sole ownership is untouched.
/** The kind of satellite a nation can put into orbit. */
export type SatelliteType = 'recon' | 'comms' | 'navigation' | 'weather';
/** All satellite types, in canonical order (the LLM contract + validator). */
export const SATELLITE_TYPES: SatelliteType[] = ['recon', 'comms', 'navigation', 'weather'];
/** True when `t` is one of the four known satellite types. */
export function isSatelliteType(t: unknown): t is SatelliteType {
return typeof t === 'string' && SATELLITE_TYPES.includes(t as SatelliteType);
}
/** A single "launch a satellite" op: `by` puts a satellite of `type` over `regionKey` (a region it owns). */
export interface SatelliteLaunchOp {
/** The launching nation (a nation id or name, resolved via `resolveNationRef`). */
by: string;
/** The region key the satellite orbits over (must be a valid key owned by `by`). */
regionKey: string;
/** The satellite's type (one of `SATELLITE_TYPES`). */
type: SatelliteType;
}
/** A single "deorbit a satellite" op: the satellite's stable id. */
export type SatelliteDeorbitOp = string;
/** The space block of a `TurnResult`. Required; default `{ launch: [], deorbit: [] }`. */
export interface SpaceOps {
/** New satellites to launch this turn. */
launch: SatelliteLaunchOp[];
/** Satellite ids to deorbit (remove) this turn. */
deorbit: SatelliteDeorbitOp[];
}
/** One active satellite: who launched it, over which region, of what type, and when. */
export interface Satellite {
/** Stable id (`s1`, `s2`, …) assigned by the engine on launch. */
id: string;
/** The launching nation's id (resolved). */
by: string;
/** The region key the satellite orbits over. */
regionKey: string;
/** The satellite's type (one of `SATELLITE_TYPES`). */
type: SatelliteType;
/** The turn number the satellite was launched on. */
launchedTurn: number;
/** The in-game date the satellite was launched on (`YYYY-MM-DD`). */
launchedDate: string;
}
/** The exact JSON object the world-simulator LLM must return for a turn. */
export interface TurnResult {
date: string;
@@ -1052,6 +1154,27 @@ export interface TurnResult {
* default `{ open: [], resolve: [] }`.
*/
contested: ContestedOps;
/**
* (v0.45.0, #80) Alliances / mutual-defense treaties this turn: `form`
* declares a new pact between two nations (a symmetric alliance entry plus
* an `ally` relation, unless they are at war — the pact still forms but the
* `war` relation keeps overriding); `break` ends an existing pact and sets
* the pair back to `neutral` (unless an active war still binds them). A pair
* holds at most one pact (re-forming is a no-op). Required;
* default `{ form: [], break: [] }`.
*/
alliances: AllianceOps;
/**
* (v0.45.0, #81) Space program / satellites this turn: `launch` puts a new
* satellite (one of the four `SatelliteType`s) into orbit over a region the
* acting nation owns, stamped with the turn and date; `deorbit` removes an
* active satellite by its stable id. Every satellite its owner holds grants
* the owner a small deterministic +1 stability per committed turn (mirrored
* to its regions, re-derived), so a nation with several satellites stacks
* the bonus. Launches/deorbits never move borders — sole ownership is
* untouched. Required; default `{ launch: [], deorbit: [] }`.
*/
space: SpaceOps;
/**
* (v0.27, #44) Map-marker ops this turn. `add` places new named markers
* (the engine positions each at its region's centroid + a small