feat(abilities): add disasters & pandemics lever (v0.41.0, #63)
CI / build + unit + e2e (pull_request) Successful in 1m55s
CI / build + unit + e2e (pull_request) Successful in 1m55s
Add the 9th modular LLM ability: a turn-transient `TurnResult.disasters`
lever letting the LLM declare natural/man-made disasters (earthquake /
flood / volcanic / wildfire / famine / plague / epidemic) on a nation or
a region, applying a DETERMINISTIC (type, severity) stat dip (no hidden
randomness). Like the hybrid layer it owns no GameState field, needs no
db.ts change, and leaves no durable list — an ongoing plague/epidemic is
simply re-declared each turn it persists.
- shared/protocol.ts: DisasterType / DisasterSeverity / DisasterEvent /
DisastersOps, and `TurnResult.disasters` (between `ideology` and
`markers`; required, default `{ events: [] }`).
- server/abilities/disasters.ts: the ability module (validate / apply /
schemaLine / constraint / isMeaningful / inert stub / empty payload; no
stateFields). `DISASTER_EFFECTS` is the deterministic (type, severity)
dip table (population never touched). Target resolution mirrors hybrid
(a region key hits its SOLE owner; 0/2+ owners dropped) but has NO
`from` field, so there is no self-target drop. Dips clamp to 0-100,
mirror to the target's regions (regionStats), and re-derive the target
nation's stats (same 0-region / no-entry guards).
- Registered last in ABILITIES (after ideology). System-prompt goldens
regenerated (one schema line + one constraint bullet each); the
user.txt golden stays byte-identical (payload() is {}).
- New server/abilities/disasters.test.ts (33 tests); registry.test.ts
updated to the 9-ability contract.
- Secondary cleanups from the v0.40 refactor: AGENTS.md (abilities/ in
the Layout tree + a "how to add a new ability" bullet + a disasters
bullet), delete the orphaned validateTakeovers JSDoc, fold both inline
`as unknown` casts into the top-level ./abilities import, and de-dup
`str` (now imported from ./abilities/shared).
Version bumped 0.40.0 -> 0.41.0.
This commit is contained in:
@@ -2,6 +2,22 @@
|
||||
|
||||
All notable changes to **History** are listed here, newest first.
|
||||
|
||||
## v0.41.0 — 2026-10-16 09:07
|
||||
- **#63: Disasters & pandemics — a `TurnResult.disasters` lever letting the LLM declare natural/man-made disasters (earthquake / flood / volcanic / wildfire / famine / plague / epidemic) on a nation or a region, with a deterministic stat dip (stability / economy / military).** It is a **turn-transient** feature, exactly like the v0.34 `hybrid` layer: **no new `GameState` field, no `db.ts` change, no durable list** — the ops are consumed inside `applyTurnResult` and leave nothing behind. It lands as the **9th ability** of the v0.40.0 modular system, in a new `server/abilities/disasters.ts` module, **registered last** in the `ABILITIES` registry (after `ideology`, before the loop appends `markers` / `nationOps`).
|
||||
- **Types (`shared/protocol.ts`).** New `DisasterType` (`'earthquake' | 'flood' | 'volcanic' | 'wildfire' | 'famine' | 'plague' | 'epidemic'`), `DisasterSeverity` (`'minor' | 'moderate' | 'severe'`), `DisasterEvent` (`{ type; target; severity; detail? }` — `target` is a nation ref **or** a region key), and `DisastersOps` (`{ events: DisasterEvent[] }`); `TurnResult.disasters` is **required**, placed between `ideology` and `markers`, default `{ events: [] }`.
|
||||
- **Ability module (`server/abilities/disasters.ts`).** `field: 'disasters'`; `default()` / `stub()` → a **fresh** `{ events: [] }` each call; `validate` wraps the exported `validateDisasterEvents` (defensive, never throws — accepts the `{ events: [...] }` object or a bare array; each event needs a valid `type`, a valid `severity`, and a non-empty `target` (trimmed, ≤120) — anything else is dropped, with a per-turn cap); `apply` runs `applyDisasterEvent` per event; `schemaLine()` / `constraint()` supply the prompt text; `payload()` → `{}` (no user-payload keys — targets resolve from the existing ref table); `isMeaningful` = `events.length > 0`; **no** `stateFields` (turn-transient, no durable `GameState` field).
|
||||
- **Deterministic effects (`DISASTER_EFFECTS`).** A pure `(type, severity) → stat-dip` table — **no hidden randomness**. `population` is never touched. Earthquake and famine dip **both** `stability` and `economy` (−2 / −4 / −7 for minor / moderate / severe); flood dips `economy` (−2 / −3 / −6) and `stability` (−1 / −2 / −4); volcanic dips `economy` (−2 / −4 / −7) and `stability` (−2 / −3 / −6); wildfire dips **only** `economy` (−2 / −4 / −7); plague dips `stability` (−3 / −5 / −9) and `military` (−2 / −3 / −6); epidemic dips `stability` (−2 / −4 / −7) and `military` (−1 / −2 / −4).
|
||||
- **Apply semantics (`applyDisasterEvent`).** The `target` is resolved exactly like a hybrid op (`resolveHybridTarget`): a resolvable nation ref hits every region the nation owns; a region key hits its **sole** owner; a 0-owner or 2+-owner region key is **dropped** (never thrown). The one deliberate difference from hybrid: a disaster has **no `from` field**, so there is **no self-target drop** — a nation may suffer a disaster it itself declared. The dip is clamped to 0–100, **mirrored to every region the target owns** (`regionStats`), and the target's derived `stats` is **re-derived** afterwards (the same 0-region / no-entry guards as hybrid / economy / industry — a 0-region nation keeps its directly-mutated value). It never moves territory (sole ownership is untouched). The **stub never** declares a disaster (`{ events: [] }`), so e2e (CI, `LLM_STUB=1`) is unaffected and the path is covered by unit tests only.
|
||||
- **Registry & prompt.** `disasters` is added last in `ABILITIES`; the system prompt gains the `disasters` schema line and its constraint bullet ("at most 1–2 per turn, each narrated in `events`"); the user prompt is unchanged (`payload()` is `{}`). The two committed system-prompt **goldens** (`system-no-tool.txt`, `system-tool.txt`) are regenerated (each gains exactly the new `disasters` schema line + constraint bullet); the **`user.txt` golden stays byte-identical** (no new user-payload keys — verified by checksum).
|
||||
- **Tests.** New `server/abilities/disasters.test.ts` (**33 tests**): the type/severity predicate sets; `validateDisasterEvents` (shape / leniency / drops / cap); `applyDisasterEvent` exact dips (nation target, single-region target, region gated by `validKeys`, clamping at 0, 0-owner / 2+-owner region drops, unknown-key drop, unknown / ambiguous ref drops, case-insensitive name resolve, 0-region direct-dip persistence, missing-`regionStats`-entry skip); and the ability const contract (field, no `stateFields`, fresh `default()`/`stub()`, `isMeaningful`, `payload() {}`, `validate` wrapping, `apply` ctx wiring). `registry.test.ts` is updated for the **9-ability** contract (order, fields, `fullDefaultTurn()`, `meaningfulFor`, re-export pin); the goldens are regenerated (verified idempotent).
|
||||
- **Secondary cleanups from the v0.40.0 refactor (folded into this release):**
|
||||
- **AGENTS.md** — `server/abilities/` is added to the Layout tree (the per-ability files, tests, and goldens); Conventions gains a "Modular LLM abilities — how to add a new one (v0.40.0, #67)" bullet and a "Disasters & pandemics (v0.41.0, #63)" bullet.
|
||||
- The **orphaned `validateTakeovers` JSDoc** (left over after the v0.40 refactor removed the function body) is deleted.
|
||||
- Both **inline `as unknown` double-casts** are folded into the single top-level `./abilities` import (`AbilityCtx` + `UserPromptCtx` types).
|
||||
- **`str()` is de-duplicated** — imported from `./abilities/shared` instead of the local copy.
|
||||
- **Guarantees.** No client (`src/`) changes, no new runtime deps, `shared/` keeps zero runtime deps, and no `@turf`/`@types/geojson` in any `.ts`. All unit tests green (additive); e2e (CI, `LLM_STUB=1`) unaffected (the stub is disasters-inert).
|
||||
- **Version** bumped `0.40.0 → 0.41.0`.
|
||||
|
||||
## v0.40.0 — 2026-10-16 07:14
|
||||
- **#67: Modular LLM-ability system — the 8 per-feature LLM abilities are extracted from `server/engine.ts` into an ordered `ABILITIES` registry (`server/abilities/`).** A pure, **behavior-preserving** refactor: each per-ability concern (validate / schema-line / constraint / payload / meaningfulness / apply / stub-default / durable-state-field) moves into one file per ability, assembled into a single ordered registry that the engine iterates uniformly. **Nothing observable changes** — the system prompt, user prompt, `applyTurnResult` world-state mutations, and the deterministic stub are all byte-for-byte identical to v0.39.0 (pinned by new golden fixtures + registry tests), and `server/engine.ts` re-exports every name its existing callers import, so `server/index.ts` and all tests work with zero edits.
|
||||
- **New `server/abilities/` package.**
|
||||
|
||||
Reference in New Issue
Block a user