// The NAMED-LEADERS ability (v0.38, #61) as a modular LLM-ability (v0.40.0, // #67). The model may change a nation's head of state: an `assassination` // (a dramatic, destabilizing blow — a −15 stability dip, a "military" event, // only valid when the target already has a leader), a `death` (a natural- // causes succession — a quieter −3 dip, a "political" event, also only valid // for a nation that has a leader), or an `appointed` change (sets or creates // a leader and is always valid — a neutral 0 effect, a "political" event). // On a valid change the target's `leader` is set, the DETERMINISTIC stability // dip (`LEADER_STABILITY_EFFECT[cause]`) is applied to its `stats.stability` // (clamped) and mirrored to every region the nation owns (`regionStats`), the // nation's derived stats are re-derived afterwards (the same 0-region / // no-entry guards), and a narration event is appended. The replacement never // moves territory (sole ownership is untouched). All bodies are moved // VERBATIM from the pre-refactor engine.ts. import { deriveNationStats, makeEvent } from '../../shared/protocol'; import { clampStat, LEADER_STABILITY_EFFECT } from '../../shared/protocol'; import type { GameState } from '../../shared/types'; import type { LeaderCause, LeaderChange } from '../../shared/protocol'; import { resolveNationRef, str } from './shared'; import type { Ability } from './types'; /** * (v0.38, #61) Validate the raw `leaders` object of a turn into a safe * `LeaderChange[]`. Defensive like the rest of the parser — never throws. * Accepts a `{ changes: [...] }` object or a bare array (lenient). Each change * needs a non-empty `target` (a nation ref, capped at 120 chars), a non-empty * `name` (the new leader's name, capped at 120 chars), and a `cause` that is * one of the three known literals (assassination / death / appointed). Refs * stay raw here (resolved against the real state at apply time, like the wars * / hybrid / nuclear ops); a leaderless assassination/death is dropped at * apply time, never thrown. */ export function validateLeaderOps(raw: unknown): LeaderChange[] { const out: LeaderChange[] = []; const list = Array.isArray(raw) ? raw : raw && typeof raw === 'object' ? (raw as Record).changes : undefined; if (!Array.isArray(list)) return out; for (const e of list) { if (!e || typeof e !== 'object') continue; const s = e as Record; const target = str(s.target, 120); const name = str(s.name, 120); if (!target || !name) continue; const cause = typeof s.cause === 'string' ? s.cause : ''; if (cause === 'assassination' || cause === 'death' || cause === 'appointed') { out.push({ target, name, cause: cause as LeaderCause }); } } return out; } /** * (v0.38, #61) Apply a single leader change to `next` (in place). The target * nation is resolved by the shared resolver (id first, then a case-insensitive * unique name; an unknown or ambiguous ref is dropped, never thrown). A GATE * then applies: an `assassination` or `death` requires the target to already * have a leader (a leaderless target is dropped); an `appointed` change is * always valid (it creates the first leader). On a valid change the target's * `leader` is set to the new name and a DETERMINISTIC stability dip * (`LEADER_STABILITY_EFFECT[cause]`) is applied to its `stats.stability` * (clamped 0–100) and mirrored to every region the nation owns * (`regionStats`); the nation's derived stats are re-derived afterwards (the * same 0-region / no-entry guards as the hybrid/economy/industry effects, so a * 0-region nation keeps its directly-mutated value). Finally a narration event * is appended: a dramatic `military` event for an assassination, a `political` * succession event for a death or an appointment. The replacement never moves * territory (sole ownership is untouched). */ function applyLeaderChange( next: GameState, op: LeaderChange, geoOf?: (key: string) => { area: number; population: number } | undefined, actingTurn?: number, ): void { const turn = actingTurn ?? next.turn; const id = resolveNationRef(next, op.target); if (id === null) return; // unknown / ambiguous target: drop, never throw const n = next.nations[id]; // GATE: assassination / death require an existing leader; an appointment is // always valid (it creates the first leader). if (op.cause !== 'appointed' && !n.leader) return; const prevName = n.leader?.name; n.leader = { name: op.name }; const delta = LEADER_STABILITY_EFFECT[op.cause]; if (delta !== 0) { n.stats.stability = clampStat(n.stats.stability + delta); for (const key of n.territory) { const rs = next.regionStats[key]; if (!rs) continue; rs.stability = clampStat(rs.stability + delta); } // Re-derive the nation's aggregate stats (the main recompute ran earlier // this turn, so without this the dip would be washed out next turn). if (n.territory.length > 0 && n.territory.some((k) => next.regionStats[k])) { n.stats = deriveNationStats(n.territory, next.regionStats, geoOf); } } const title = op.cause === 'assassination' ? `${prevName ?? 'The leader'} of ${n.name} is assassinated` : op.cause === 'death' ? `${prevName ?? 'The leader'} of ${n.name} dies` : `${n.name} appoints ${op.name}`; const text = op.cause === 'assassination' ? `${n.name}'s leader ${prevName ?? ''} was assassinated; ${op.name} takes power amid turmoil.` : op.cause === 'death' ? `${n.name}'s leader ${prevName ?? ''} has died; ${op.name} succeeds to power.` : `${n.name} has appointed ${op.name} as its new leader.`; next.events.push( makeEvent( { nation: n.id, type: op.cause === 'assassination' ? 'military' : 'political', title, text, outcome: 'success', }, turn, next.date, ), ); } export const leaders: Ability<'leaders'> = { field: 'leaders', default: () => ({ changes: [] }), // The low-level validator returns a bare list; the field's shape is the // `{ changes }` object (mirrors the historical `result.leaders = { changes: ... }`). validate: (raw) => ({ changes: validateLeaderOps(raw) }), apply: (state, data, ctx) => { // Placed after the nuclear block (both are the "heavier" levers) and // before the markers block (markers recompute their owner from the FINAL // post-claim ownership; leaders only touch stats, so ordering w.r.t. // markers is immaterial, but keeping the levers grouped is clearer). Each // change sets the target's leader, applies a deterministic stability dip // by cause, and appends a narration event. A leaderless assassination/ // death is dropped. for (const op of data.changes) applyLeaderChange(state, op, ctx.geoOf, ctx.actingTurn); }, schemaLine: () => ' "leaders": {"changes":[{"target":"","name":"","cause":""}]},', constraint: () => '- Leaders ("leaders") name each nation\'s current head of state and change it. Each change is {"target":"","name":"","cause":""}: "assassination" kills the current leader (a dramatic, destabilizing blow — a −15 stability dip, a "military" event) and is only valid when the target already has a leader; "death" is a natural-causes succession (a quieter −3 dip, a "political" event), also only valid for a nation that has a leader; "appointed" sets (or creates) a leader and is always valid (a neutral 0 effect, a "political" event). Propose at most 1-2 changes per turn and narrate each one in "events". This is the HEAVY leader lever — use the lighter "hybrid" {"type":"assassinate"} op (a −5 stability dip, no leader replacement) for a mere hit on a leader or region without changing who is in charge. The user message\'s "nations" field lists each nation\'s current "leader" (omitted when a nation has none).', isMeaningful: (data) => data.changes.length > 0, stub: () => ({ changes: [] }), payload: () => ({}), };