feat(#61): named leaders — assassination / death / appointment with succession (v0.38.0)
CI / build + unit + e2e (pull_request) Successful in 2m33s

Per-nation named leaders (Nation.leader) and a required TurnResult.leaders
lever ({ changes: [{ target, name, cause }] }). Each change sets the target
nation's leader and applies a deterministic stability dip by cause
(assassination -15, death -3, appointed 0) mirrored to its regions and
re-derived into its stats, plus a narration event (military for an
assassination, political for a death/appointment). An assassination/death
requires the target to already have a leader (a leaderless target is
dropped); an appointment always creates/sets one. Unknown/ambiguous targets
are dropped, never thrown.

- shared/types.ts: optional Nation.leader
- shared/protocol.ts: LeaderCause/LeaderChange/LeaderOps, LEADER_STABILITY_EFFECT,
  TurnResult.leaders (required, between nuclear and markers), leader in nationForPrompt
- server/engine.ts: validateLeaderOps, applyLeaderChange, the applyTurnResult
  leader block (after nuclear, before markers), isMeaningful, system-prompt
  contract + constraint, validateTurnResult + runTurn fallback
- server/stub.ts: leaders always { changes: [] } (e2e unaffected)
- tests: 15 engine.test.ts leader tests + 2 db.test.ts round-trips
- version 0.37.0 -> 0.38.0, CHANGELOG entry
This commit is contained in:
2026-10-15 20:09:19 +00:00
parent b6dcd3f979
commit 8730a56170
8 changed files with 469 additions and 41 deletions
+10 -1
View File
@@ -16,9 +16,17 @@ export interface StubPayload {
// `nuclear` (v0.37.0, #60) is the per-nation nuclear capability stamped by
// the engine (its `nuclear` flag OR a `rocket` marker); the stub ignores
// it — it never arms or strikes (see deterministicTurn).
// `leader` (v0.38.0, #61) is the per-nation named leader stamped by the
// engine; the stub ignores it — it never changes a leader (see
// deterministicTurn).
nations: Record<
string,
{ stats: Record<string, number>; territory?: string[]; nuclear?: boolean }
{
stats: Record<string, number>;
territory?: string[];
nuclear?: boolean;
leader?: { name: string };
}
>;
};
/** The raw pending round the engine emitted (one entry per queued item). */
@@ -224,6 +232,7 @@ export function deterministicTurn(p: StubPayload): TurnResult {
economy: { trade: [], embargo: [] }, // the stub never touches trades/embargoes (see doc comment)
industry: { projects: [] }, // the stub never declares industry projects (see doc comment)
nuclear: { arm: [], strikes: [] }, // the stub never arms or strikes (see doc comment)
leaders: { changes: [] }, // the stub never changes leaders (see doc comment)
markers: { add: [], remove: [] }, // the stub never touches markers (see doc comment)
};
}