v0.32.0 — Non-military takeovers become a visible regime change (TurnResult.takeovers) #54

Closed
opened 2026-10-15 12:58:07 +02:00 by kamilkrzywda · 0 comments
Owner

Summary

The v0.30.0 stats work made stability a soft lever: the system prompt tells the model that a nation whose stability is below 30 and that hosts a gathering marker "can suffer a non-military takeover — a coup, revolution or faction seizure of power", narrated with a political/social success event and a stability/economy shock. But the model had no field to express the regime change itself — the takeover was narrated yet the nation's name never changed, so it was invisible in the world (the dossier, the map, and the relation names all kept the old regime's name).

This issue closes that gap: a new TurnResult.takeovers field carries non-military regime changes, and the engine applies them as an automatic, in-world rename of the target nation.

1. Protocol (shared/protocol.ts)

  • New Takeover = { nationRef: string; newRegimeName: string } — the target named by its id or current name (resolved by the existing resolveNationRef), and the name of the new regime.
  • TurnResult.takeovers: Takeover[] — required (default []), placed between nationOps and markers.

2. Engine (server/engine.ts)

  • validateTakeovers(raw) — new exported defensive validator (same style as validateNationOps; never throws): non-array → []; each entry needs a non-empty, whitespace-trimmed nationRef and newRegimeName (each capped at 200 chars); anything else is dropped. nationRef resolution (id first, then case-insensitive unique name) happens at apply time — an unknown or ambiguous ref is a no-op, never a throw.
  • Apply step in applyTurnResult — placed after the per-region stats recompute and before the markers block (the rename is order-independent of territory/stats; marker owners are nation ids, not names, so the rename never disturbs the owner recompute). Each resolved takeover sets n.name = newRegimeName — only the display name changes: the stable id, territory, relations (keyed by id), stats, and playerNationId are all preserved, so a player whose nation is overthrown keeps leading it under the new regime.
  • isMeaningful — a turn that only performs a takeover is meaningful (survives the retry/nudge path).
  • buildSystemPrompt — the JSON shape gains "takeovers": [{…}] and a new HARD CONSTRAINTS bullet: a takeover is automatic, NOT player-confirmed — fire it when a low-stability nation hosting a gathering marker is overthrown (giving the v0.30 stability/gathering mechanic a concrete consequence), narrate it with a political/social success event + a stability/economy shock, and use nationOps rename only for the player-confirmed kind of name change.
  • The generic "unclear" fallback in runTurn carries takeovers: [].

3. Stub + client

  • server/stub.ts — deterministicTurn returns takeovers: [] (the stub never performs a takeover); its one-conquest behavior and every e2e assertion stay untouched (the regime-rename path is covered by unit tests only).
  • Client — no change: a takeover is just a nation name change, which the dossier / event feed / map already render (the nation keeps its stable color — keyed by id).

4. Tests + version

  • 15 new unit tests in server/engine.test.ts (apply: rename-with-preservation, player takeover, unknown ref, ambiguous ref, empty name, multiple takeovers, single version bump, case-insensitive name; isMeaningful; system-prompt contract in both modes; validateTakeovers; stub takeovers: []) + takeovers: [] added to every existing TurnResult literal.
  • Bump package.json → 0.32.0` + changelog entry.
## Summary The v0.30.0 stats work made **stability** a *soft* lever: the system prompt tells the model that a nation whose stability is below 30 **and** that hosts a `gathering` marker "can suffer a non-military takeover — a coup, revolution or faction seizure of power", narrated with a political/social success event and a stability/economy shock. But the model had **no field to express the regime change itself** — the takeover was narrated yet the nation's name never changed, so it was invisible in the world (the dossier, the map, and the relation names all kept the old regime's name). This issue closes that gap: a new **`TurnResult.takeovers`** field carries non-military regime changes, and the engine applies them as an **automatic, in-world** rename of the target nation. ### 1. Protocol (`shared/protocol.ts`) - New **`Takeover = { nationRef: string; newRegimeName: string }`** — the target named by its id or current name (resolved by the existing `resolveNationRef`), and the name of the new regime. - **`TurnResult.takeovers: Takeover[]`** — **required** (default `[]`), placed between `nationOps` and `markers`. ### 2. Engine (`server/engine.ts`) - **`validateTakeovers(raw)`** — new exported defensive validator (same style as `validateNationOps`; never throws): non-array → `[]`; each entry needs a non-empty, whitespace-trimmed `nationRef` **and** `newRegimeName` (each capped at 200 chars); anything else is dropped. `nationRef` resolution (id first, then case-insensitive unique name) happens at **apply** time — an unknown or ambiguous ref is a no-op, never a throw. - **Apply step in `applyTurnResult`** — placed **after** the per-region stats recompute and **before** the markers block (the rename is order-independent of territory/stats; marker owners are nation **ids**, not names, so the rename never disturbs the owner recompute). Each resolved takeover sets `n.name = newRegimeName` — **only the display name changes**: the stable `id`, territory, relations (keyed by id), stats, and `playerNationId` are all preserved, so a player whose nation is overthrown keeps leading it under the new regime. - **`isMeaningful`** — a turn that only performs a takeover is meaningful (survives the retry/nudge path). - **`buildSystemPrompt`** — the JSON shape gains `"takeovers": [{…}]` and a new HARD CONSTRAINTS bullet: a takeover is **automatic, NOT player-confirmed** — fire it when a low-stability nation hosting a `gathering` marker is overthrown (giving the v0.30 stability/gathering mechanic a concrete consequence), narrate it with a political/social success event + a stability/economy shock, and use `nationOps` rename only for the player-confirmed kind of name change. - The generic "unclear" fallback in `runTurn` carries `takeovers: []`. ### 3. Stub + client - **`server/stub.ts`** — `deterministicTurn` returns `takeovers: []` (the stub never performs a takeover); its one-conquest behavior and every e2e assertion stay untouched (the regime-rename path is covered by unit tests only). - **Client** — no change: a takeover is just a nation `name` change, which the dossier / event feed / map already render (the nation keeps its stable color — keyed by id). ### 4. Tests + version - 15 new unit tests in `server/engine.test.ts` (apply: rename-with-preservation, player takeover, unknown ref, ambiguous ref, empty name, multiple takeovers, single version bump, case-insensitive name; `isMeaningful`; system-prompt contract in both modes; `validateTakeovers`; stub `takeovers: []`) + `takeovers: []` added to every existing `TurnResult` literal. - Bump **`package.json` → 0.32.0`** + changelog entry.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OpenCode/History#54