feat(abilities): add migration & refugees lever (v0.42.0, #64)
CI / build + unit + e2e (pull_request) Successful in 2m12s

Add the 10th modular LLM ability: a turn-transient TurnResult.migration
lever declaring population flows between nations (migration / refugees /
diaspora / exodus). The source loses a little population + economy, the
destination gains a little (a large influx also strains its stability),
and the from<->to relation is nudged one step toward the positive end of
the ally/neutral/rival/hostile ladder (capped by kind; a war is never
touched). Deterministic per-scale tables, no hidden randomness, clamped
0-100, mirrored to owned regions, re-derived aggregates; never moves
territory. Registered last in ABILITIES; user prompt unchanged
(user.txt golden byte-identical), system goldens re-baselined. Fixes #64.
This commit is contained in:
2026-10-16 10:15:45 +00:00
parent e0f3d0313b
commit 7e4faa73d3
13 changed files with 1142 additions and 55 deletions
+80
View File
@@ -650,6 +650,63 @@ export interface DisastersOps {
events: DisasterEvent[];
}
// --- Migration & refugees (v0.42.0, #64) --------------------------------------
//
// People moving between nations each turn: ordinary migration, refugee flows
// (fleeing a war or disaster), a spreading diaspora, and an exodus (people
// leaving with no destination). Each op names a `kind`, a source `from` (the
// nation people leave), a destination `to` (required for every kind except
// `exodus`), and a `scale` (small / moderate / large). The engine applies a
// DETERMINISTIC effect chosen from the per-`scale` tables
// `MIGRATION_SOURCE_EFFECTS` / `MIGRATION_DESTINATION_EFFECTS` (defined in
// `server/abilities/migration.ts`) — no hidden randomness: the LLM chooses the
// kind and scale, the tables decide the magnitude. The source LOSES a small
// amount of population and economy, the destination GAINS a small amount of
// population and economy (a `large` influx additionally strains the
// destination's stability), both clamped to 0–100, mirrored to each side's
// regions (`regionStats`), and each side's derived `stats` re-derived
// afterwards (the same 0-region / no-entry guards as the hybrid / economy /
// disasters layers). The from↔to relation moves one step toward the positive
// end of the ally/neutral/rival/hostile ladder (`shiftRelationTowardPositive`)
// — capped at `ally` for `refugees`, `neutral` for `migration` / `diaspora`,
// and not at all for `exodus` — and a `war` relation is never touched. It is
// **turn-transient** like the hybrid / disasters layers: there is NO `GameState`
// field, no durable list, and no `db.ts` change — a continuing flow is simply
// re-declared each turn it persists. It never moves territory (sole ownership
// is untouched) and never creates or escalates a war.
/** The kind of population flow a migration op describes (v0.42.0, #64). */
export type MigrationKind = 'migration' | 'refugees' | 'diaspora' | 'exodus';
/** The size of a migration flow (v0.42.0, #64). */
export type MigrationScale = 'small' | 'moderate' | 'large';
/**
* A single migration / refugee flow (v0.42.0, #64): the `kind`, the source
* `from` (a nation id / name — the nation people leave), the destination `to`
* (a nation id / name — required for every kind except `exodus`, which has no
* destination), the `scale`, and an optional short `detail`. The deterministic
* stat effects are looked up from `MIGRATION_SOURCE_EFFECTS` /
* `MIGRATION_DESTINATION_EFFECTS` by `scale`.
*/
export interface MigrationOp {
/** The kind of flow (one of {@link MigrationKind}). */
kind: MigrationKind;
/** The source nation (by id or name) people leave. */
from: string;
/** The destination nation (by id or name). Required for every kind except `exodus`. */
to?: string;
/** How many people moved (one of {@link MigrationScale}). */
scale: MigrationScale;
/** Optional short annotation (≤300 chars). */
detail?: string;
}
/** The migration block of a `TurnResult`. Required; default `{ ops: [] }`. */
export interface MigrationOps {
ops: MigrationOp[];
}
/** The exact JSON object the world-simulator LLM must return for a turn. */
export interface TurnResult {
date: string;
@@ -819,6 +876,29 @@ export interface TurnResult {
* `{ events: [] }`.
*/
disasters: DisastersOps;
/**
* (v0.42.0, #64) Migration & refugee flows this turn — people moving between
* nations: ordinary `migration`, `refugees` fleeing a war or disaster, a
* spreading `diaspora`, and an `exodus` (people leaving with no destination).
* Each op names a `from` (the source nation, by id or name) and, for every
* kind except `exodus`, a `to` (the destination nation, by id or name), plus
* a `scale` (small / moderate / large). Applied directly by `applyTurnResult`
* (like the disasters / hybrid layers): the source LOSES a small amount of
* population and economy, the destination GAINS a small amount of population
* and economy (a `large` influx additionally strains the destination's
* stability by 2), both clamped to 0–100, mirrored to each side's regions,
* and each side's derived `stats` re-derived afterwards; the from↔to
* relation moves one step toward the positive end of the
* ally/neutral/rival/hostile ladder (capped at `ally` for `refugees`,
* `neutral` for `migration` / `diaspora`, not at all for `exodus`; a `war`
* relation is never touched). The layer is turn-transient (no `GameState`
* field, no `db.ts` change) — a continuing flow is simply re-declared each
* turn it persists. It never moves borders (sole ownership is untouched) and
* never creates or escalates a war. At most 1–2 per turn, each narrated in
* `events` (a `social` or `diplomatic` event). Required; default
* `{ ops: [] }`.
*/
migration: MigrationOps;
/**
* (v0.27, #44) Map-marker ops this turn. `add` places new named markers
* (the engine positions each at its region's centroid + a small