feat: #80 + #81 alliances & space program — 13th & 14th abilities (v0.45.0)
CI / build + unit + e2e (pull_request) Successful in 2m20s
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:
@@ -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
|
||||
|
||||
@@ -465,6 +465,10 @@ export function worldDraftToGameState(
|
||||
seaZones: [],
|
||||
// (v0.44.0, #66) no contested regions on a fresh state.
|
||||
contested: [],
|
||||
// (v0.45.0, #80) no alliances on a fresh state.
|
||||
alliances: [],
|
||||
// (v0.45.0, #81) no satellites on a fresh state.
|
||||
satellites: [],
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
+29
-1
@@ -3,7 +3,12 @@
|
||||
// (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, ContestedRegion } from './protocol';
|
||||
import type {
|
||||
Alliance,
|
||||
NationOp,
|
||||
ContestedRegion,
|
||||
Satellite,
|
||||
} from './protocol';
|
||||
|
||||
/** A nation's 0–100 status indices. Every value is clamped to 0–100 by the engine. */
|
||||
export interface NationStats {
|
||||
@@ -486,6 +491,29 @@ export interface GameState {
|
||||
* with this defaulted to `[]` (see server/db.ts) — additive, no data wipe.
|
||||
*/
|
||||
contested: ContestedRegion[];
|
||||
/**
|
||||
* (v0.45.0, #80) Active alliances / mutual-defense treaties (see {@link
|
||||
* Alliance}): one entry per pact — which two nations are bound and when the
|
||||
* pact was formed. Declared/ended by a turn's `alliances.form` /
|
||||
* `alliances.break` ops (a `form` op also sets the pair's relation to
|
||||
* `ally` unless an active war overrides it; a `break` op sets it back to
|
||||
* `neutral` unless an active war still binds them). A pair holds at most one
|
||||
* pact (re-forming is a no-op). Always `[]` on a fresh game; rows saved
|
||||
* before v0.45 load with this defaulted to `[]` (see server/db.ts) —
|
||||
* additive, no data wipe.
|
||||
*/
|
||||
alliances: Alliance[];
|
||||
/**
|
||||
* (v0.45.0, #81) Active satellites (see {@link Satellite}): one entry per
|
||||
* satellite in orbit — which nation launched it, over which region, of what
|
||||
* type, and when. Declared/deorbited by a turn's `space.launch` /
|
||||
* `space.deorbit` ops (a launch requires `by` to own `regionKey`; each
|
||||
* active satellite grants its owner +1 stability per committed turn,
|
||||
* mirrored to its regions and re-derived). Always `[]` on a fresh game;
|
||||
* rows saved before v0.45 load with this defaulted to `[]` (see
|
||||
* server/db.ts) — additive, no data wipe.
|
||||
*/
|
||||
satellites: Satellite[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user