feat(abilities): add migration & refugees lever (v0.42.0, #64)
CI / build + unit + e2e (pull_request) Successful in 2m12s
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:
@@ -12,7 +12,7 @@
|
||||
// by the disasters feature (its payload is always empty).
|
||||
//
|
||||
// 2. REGISTRY CONTRACT — the ABILITIES registry is the canonical ordered
|
||||
// list of the 9 abilities; each ability's default() equals the canonical
|
||||
// list of the 10 abilities; each ability's default() equals the canonical
|
||||
// TurnResult default for its field, is a FRESH value (never a shared
|
||||
// reference), its default is not "meaningful" while a populated value is,
|
||||
// and the registry composes (a hypothetical extra ability can be appended
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
activeWarPairs,
|
||||
applyDisasterEvent,
|
||||
applyIndustryEffects,
|
||||
applyMigrationOp,
|
||||
atWar,
|
||||
buildSystemPrompt,
|
||||
buildUserPrompt,
|
||||
@@ -44,10 +45,18 @@ import {
|
||||
isDisasterType,
|
||||
isHybridOpType,
|
||||
isIndustryType,
|
||||
isMigrationKind,
|
||||
isMigrationScale,
|
||||
LARGE_INFLUX_STABILITY_DIP,
|
||||
MIGRATION_DESTINATION_EFFECTS,
|
||||
MIGRATION_KINDS,
|
||||
MIGRATION_SCALES,
|
||||
MIGRATION_SOURCE_EFFECTS,
|
||||
NEW_NATION_STATS,
|
||||
resolveNationRef,
|
||||
resolveRegionKey,
|
||||
shiftRelationTowardHostile,
|
||||
shiftRelationTowardPositive,
|
||||
syncWarRelations,
|
||||
validateDisasterEvents,
|
||||
validateEconomyOps,
|
||||
@@ -55,6 +64,7 @@ import {
|
||||
validateIdeologyOps,
|
||||
validateIndustryOps,
|
||||
validateLeaderOps,
|
||||
validateMigrationOps,
|
||||
validateNuclearOps,
|
||||
validateTakeovers,
|
||||
validateWarOps,
|
||||
@@ -217,6 +227,7 @@ function fullDefaultTurn(): TurnResult {
|
||||
leaders: { changes: [] },
|
||||
ideology: { changes: [] },
|
||||
disasters: { events: [] },
|
||||
migration: { ops: [] },
|
||||
markers: { add: [], remove: [] },
|
||||
};
|
||||
}
|
||||
@@ -242,11 +253,13 @@ function meaningfulFor(field: AbilityField): TurnResult[AbilityField] {
|
||||
return { changes: [{ target: 'a1', to: 'democracy' }] };
|
||||
case 'disasters':
|
||||
return { events: [{ type: 'earthquake', target: 'a1', severity: 'minor' }] };
|
||||
case 'migration':
|
||||
return { ops: [{ kind: 'migration', from: 'a1', to: 'b2', scale: 'small' }] };
|
||||
}
|
||||
}
|
||||
|
||||
describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
it('is the canonical 9-ability order (takeovers → disasters)', () => {
|
||||
it('is the canonical 10-ability order (takeovers → migration)', () => {
|
||||
expect(ABILITIES.map((a) => a.field)).toEqual([
|
||||
'takeovers',
|
||||
'wars',
|
||||
@@ -257,13 +270,14 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
'leaders',
|
||||
'ideology',
|
||||
'disasters',
|
||||
'migration',
|
||||
]);
|
||||
});
|
||||
|
||||
it('lists 9 distinct fields, each a key of TurnResult', () => {
|
||||
it('lists 10 distinct fields, each a key of TurnResult', () => {
|
||||
const fields = ABILITIES.map((a) => a.field);
|
||||
expect(fields).toHaveLength(9);
|
||||
expect(new Set(fields).size).toBe(9);
|
||||
expect(fields).toHaveLength(10);
|
||||
expect(new Set(fields).size).toBe(10);
|
||||
const all = fullDefaultTurn();
|
||||
for (const f of fields) expect(f in all).toBe(true);
|
||||
});
|
||||
@@ -328,7 +342,7 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
it('composes: a hypothetical extra ability can be appended and every uniform loop still runs', () => {
|
||||
// The registry is a plain ordered array the engine iterates uniformly. A
|
||||
// structural (field-name-agnostic) loop over the abilities — schema line,
|
||||
// constraint bullet — must keep working when a 10th ability is appended.
|
||||
// constraint bullet — must keep working when an 11th ability is appended.
|
||||
// This documents the extension point without widening the closed
|
||||
// AbilityField union.
|
||||
interface Loopable {
|
||||
@@ -347,9 +361,9 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
'custom',
|
||||
]);
|
||||
// every uniform loop runs over all N abilities, the appended one included:
|
||||
expect(extended.map((a) => a.schemaLine())).toHaveLength(10);
|
||||
expect(extended.map((a) => a.constraint())).toHaveLength(10);
|
||||
expect(extended[9].schemaLine()).toBe(' "custom": [],');
|
||||
expect(extended.map((a) => a.schemaLine())).toHaveLength(11);
|
||||
expect(extended.map((a) => a.constraint())).toHaveLength(11);
|
||||
expect(extended[10].schemaLine()).toBe(' "custom": [],');
|
||||
});
|
||||
|
||||
it('the engine still re-exports every name its callers import from it', () => {
|
||||
@@ -388,6 +402,17 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
DISASTER_TYPES,
|
||||
DISASTER_SEVERITIES,
|
||||
DISASTER_EFFECTS,
|
||||
// migration (v0.42.0, #64)
|
||||
validateMigrationOps,
|
||||
applyMigrationOp,
|
||||
shiftRelationTowardPositive,
|
||||
isMigrationKind,
|
||||
isMigrationScale,
|
||||
MIGRATION_KINDS,
|
||||
MIGRATION_SCALES,
|
||||
MIGRATION_SOURCE_EFFECTS,
|
||||
MIGRATION_DESTINATION_EFFECTS,
|
||||
LARGE_INFLUX_STABILITY_DIP,
|
||||
};
|
||||
for (const [name, value] of Object.entries(reexported)) {
|
||||
expect(value, `engine should re-export ${name}`).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user