@its-not-rocket-science/ananke 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/src/demography.d.ts +122 -0
- package/dist/src/demography.js +190 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.31] — 2026-03-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Phase 86 · Population Dynamics & Demographics** (`src/demography.ts`)
|
|
14
|
+
- Annual Q rates for birth and death (fraction of population per year) to preserve fixed-point precision.
|
|
15
|
+
- `BASELINE_BIRTH_RATE_ANNUAL_Q = q(0.035)` (≈ 3.5%/year); `BASELINE_DEATH_RATE_ANNUAL_Q = q(0.030)` (≈ 3.0%/year).
|
|
16
|
+
- `computeBirthRate(polity)` → Q: morale linearly scales rate between 50% and 150% of baseline.
|
|
17
|
+
- `computeDeathRate(polity, deathPressure_Q?, foodSupply_Q?)` → Q: baseline reduced by tech era (`TECH_ERA_DEATH_MUL`), plus instability bonus (up to `INSTABILITY_DEATH_ANNUAL_Q = q(0.015)`), optional external pressure, and famine bonus (`FAMINE_DEATH_ANNUAL_Q = q(0.030)`).
|
|
18
|
+
- `computeNetGrowthRate(polity, ...)` → signed number (may be negative).
|
|
19
|
+
- `stepPolityPopulation(polity, elapsedDays, deathPressure_Q?, foodSupply_Q?)` → `DemographicsStepResult`: mutates `polity.population`; formula `round(population × netAnnualRate_Q × days / (365 × SCALE.Q))`; clamps to ≥ 0.
|
|
20
|
+
- **Famine**: `FAMINE_THRESHOLD_Q = q(0.20)` — food below this activates extra mortality and migration push.
|
|
21
|
+
- `computeFamineMigrationPush(foodSupply_Q)` → Q [0, `FAMINE_MIGRATION_PUSH_Q = q(0.30)`]: linear from zero (at threshold) to peak (at food = 0); integrates with Phase-81 push pressure.
|
|
22
|
+
- `computeCarryingCapacity(polity)` — soft cap by tech era (Stone 50 k → Modern 200 M); `isOverCapacity(polity)`.
|
|
23
|
+
- `estimateAnnualBirths` / `estimateAnnualDeaths` — reporting utilities.
|
|
24
|
+
- Phase-56 (disease) and Phase-84 (siege) integrate via `deathPressure_Q`; Phase-81 (migration) integrates via `computeFamineMigrationPush`; Phase-78 (calendar) via caller-supplied seasonal multipliers.
|
|
25
|
+
- Added `./demography` subpath export to `package.json`.
|
|
26
|
+
- 51 new tests; 4,561 total. Coverage maintained above all thresholds.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
9
30
|
## [0.1.30] — 2026-03-26
|
|
10
31
|
|
|
11
32
|
### Added
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Polity } from "./polity.js";
|
|
3
|
+
/** Baseline annual birth rate [Q = fraction of population per year]. ≈ 3.5%/year. */
|
|
4
|
+
export declare const BASELINE_BIRTH_RATE_ANNUAL_Q: Q;
|
|
5
|
+
/** Baseline annual death rate [Q = fraction of population per year]. ≈ 3.0%/year. */
|
|
6
|
+
export declare const BASELINE_DEATH_RATE_ANNUAL_Q: Q;
|
|
7
|
+
/**
|
|
8
|
+
* Morale floor multiplier on birth rate.
|
|
9
|
+
*
|
|
10
|
+
* Birth rate factor = `BIRTH_RATE_MORALE_FLOOR_Q + moraleQ`, yielding:
|
|
11
|
+
* moraleQ = 0 → factor = q(0.50) → birth rate × 0.50
|
|
12
|
+
* moraleQ = SCALE.Q → factor = q(1.50) → birth rate × 1.50
|
|
13
|
+
*/
|
|
14
|
+
export declare const BIRTH_RATE_MORALE_FLOOR_Q: Q;
|
|
15
|
+
/**
|
|
16
|
+
* Additional annual death rate at zero stability.
|
|
17
|
+
* Linearly scaled by `(SCALE.Q − stabilityQ) / SCALE.Q`.
|
|
18
|
+
* Full bonus at stability = 0; zero at stability = SCALE.Q.
|
|
19
|
+
*/
|
|
20
|
+
export declare const INSTABILITY_DEATH_ANNUAL_Q: Q;
|
|
21
|
+
/** Food supply fraction below which famine is active [0, SCALE.Q]. */
|
|
22
|
+
export declare const FAMINE_THRESHOLD_Q: Q;
|
|
23
|
+
/** Additional annual death rate during famine (+3%/year on top of baseline). */
|
|
24
|
+
export declare const FAMINE_DEATH_ANNUAL_Q: Q;
|
|
25
|
+
/**
|
|
26
|
+
* Peak famine-driven migration push pressure (at food = 0).
|
|
27
|
+
* Integrates with Phase-81 `computePushPressure` as an additive bonus.
|
|
28
|
+
*/
|
|
29
|
+
export declare const FAMINE_MIGRATION_PUSH_Q: Q;
|
|
30
|
+
/**
|
|
31
|
+
* Tech-era multiplier applied to the baseline death rate.
|
|
32
|
+
* Better technology → lower mortality from disease and malnutrition.
|
|
33
|
+
* Expressed as a Q fraction of SCALE.Q.
|
|
34
|
+
*/
|
|
35
|
+
export declare const TECH_ERA_DEATH_MUL: Record<string, number>;
|
|
36
|
+
/**
|
|
37
|
+
* Soft carrying capacity by tech era.
|
|
38
|
+
* `stepPolityPopulation` does not enforce it — host checks `isOverCapacity`
|
|
39
|
+
* and applies extra emigration pressure via Phase-81 if desired.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CARRYING_CAPACITY_BY_ERA: Record<string, number>;
|
|
42
|
+
/** Outcome of a single `stepPolityPopulation` call. */
|
|
43
|
+
export interface DemographicsStepResult {
|
|
44
|
+
/** Signed population change (positive = growth, negative = decline). */
|
|
45
|
+
popDelta: number;
|
|
46
|
+
/** New population after applying delta (clamped to ≥ 0). */
|
|
47
|
+
newPopulation: number;
|
|
48
|
+
/** Effective annual birth rate used this step. */
|
|
49
|
+
effectiveBirthRate_Q: Q;
|
|
50
|
+
/** Effective annual death rate used this step (all pressures included). */
|
|
51
|
+
effectiveDeathRate_Q: Q;
|
|
52
|
+
/** Whether famine was active this step. */
|
|
53
|
+
famine: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Compute the effective annual birth rate for a polity [Q = fraction/year].
|
|
57
|
+
*
|
|
58
|
+
* Morale scales birth rate linearly between 50% and 150% of baseline:
|
|
59
|
+
* moraleQ = 0 → BASELINE × 0.50 (≈ 1.75%/year)
|
|
60
|
+
* moraleQ = SCALE.Q → BASELINE × 1.50 (≈ 5.25%/year)
|
|
61
|
+
*/
|
|
62
|
+
export declare function computeBirthRate(polity: Polity): Q;
|
|
63
|
+
/**
|
|
64
|
+
* Compute the effective annual death rate for a polity [Q = fraction/year].
|
|
65
|
+
*
|
|
66
|
+
* Factors (additive):
|
|
67
|
+
* 1. Baseline reduced by tech era (better medicine / nutrition).
|
|
68
|
+
* 2. Instability bonus: up to `INSTABILITY_DEATH_ANNUAL_Q` at stability = 0.
|
|
69
|
+
* 3. External death pressure (caller: Phase-56 epidemic or Phase-84 siege casualties).
|
|
70
|
+
* 4. Famine bonus: `FAMINE_DEATH_ANNUAL_Q` when `foodSupply_Q < FAMINE_THRESHOLD_Q`.
|
|
71
|
+
*
|
|
72
|
+
* @param deathPressure_Q Annual mortality fraction from external cause.
|
|
73
|
+
* @param foodSupply_Q Current food supply [0, SCALE.Q]; omit if unknown.
|
|
74
|
+
*/
|
|
75
|
+
export declare function computeDeathRate(polity: Polity, deathPressure_Q?: Q, foodSupply_Q?: Q): Q;
|
|
76
|
+
/**
|
|
77
|
+
* Compute the net annual growth rate (birth rate − death rate).
|
|
78
|
+
* Negative values indicate population decline.
|
|
79
|
+
*/
|
|
80
|
+
export declare function computeNetGrowthRate(polity: Polity, deathPressure_Q?: Q, foodSupply_Q?: Q): number;
|
|
81
|
+
/**
|
|
82
|
+
* Step polity population forward by `elapsedDays` simulated days.
|
|
83
|
+
*
|
|
84
|
+
* Mutates `polity.population` in place and returns step metadata.
|
|
85
|
+
*
|
|
86
|
+
* Delta formula (fixed-point, single rounding):
|
|
87
|
+
* `popDelta = round(population × netAnnualRate_Q × elapsedDays / (365 × SCALE.Q))`
|
|
88
|
+
*
|
|
89
|
+
* @param elapsedDays Number of simulated days to advance (typically 1–30).
|
|
90
|
+
* @param deathPressure_Q Annual mortality fraction from disease or siege casualties.
|
|
91
|
+
* @param foodSupply_Q Food supply level [0, SCALE.Q]; famine fires below
|
|
92
|
+
* `FAMINE_THRESHOLD_Q`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function stepPolityPopulation(polity: Polity, elapsedDays: number, deathPressure_Q?: Q, foodSupply_Q?: Q): DemographicsStepResult;
|
|
95
|
+
/**
|
|
96
|
+
* Compute famine-driven migration push pressure [0, SCALE.Q].
|
|
97
|
+
*
|
|
98
|
+
* Zero at or above `FAMINE_THRESHOLD_Q`. Scales linearly from zero (at the
|
|
99
|
+
* threshold) to `FAMINE_MIGRATION_PUSH_Q` (at food = 0).
|
|
100
|
+
*
|
|
101
|
+
* Add the result to Phase-81 `computePushPressure` output.
|
|
102
|
+
*/
|
|
103
|
+
export declare function computeFamineMigrationPush(foodSupply_Q: Q): Q;
|
|
104
|
+
/**
|
|
105
|
+
* Soft carrying capacity for a polity based on tech era.
|
|
106
|
+
*
|
|
107
|
+
* `stepPolityPopulation` does not enforce this cap. The host should call
|
|
108
|
+
* `isOverCapacity` after each step and pass additional emigration pressure
|
|
109
|
+
* to Phase-81 when it returns `true`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function computeCarryingCapacity(polity: Polity): number;
|
|
112
|
+
/** Return `true` if the polity's population exceeds its tech-era carrying capacity. */
|
|
113
|
+
export declare function isOverCapacity(polity: Polity): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Estimate annual births from a birth rate and population.
|
|
116
|
+
* Useful for host display and scenario planning.
|
|
117
|
+
*/
|
|
118
|
+
export declare function estimateAnnualBirths(population: number, birthRate_Q: Q): number;
|
|
119
|
+
/**
|
|
120
|
+
* Estimate annual deaths from a death rate and population.
|
|
121
|
+
*/
|
|
122
|
+
export declare function estimateAnnualDeaths(population: number, deathRate_Q: Q): number;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// src/demography.ts — Phase 86: Population Dynamics & Demographics
|
|
2
|
+
//
|
|
3
|
+
// Models natural population growth, mortality, and famine at polity level.
|
|
4
|
+
// All rates are annual Q fractions (fraction of population per year) so that
|
|
5
|
+
// SCALE.Q = 10000 gives sufficient precision (0.01%/year resolution).
|
|
6
|
+
//
|
|
7
|
+
// Step formula:
|
|
8
|
+
// popDelta = round(population × netAnnualRate_Q × elapsedDays / (365 × SCALE.Q))
|
|
9
|
+
//
|
|
10
|
+
// For small polities (< ~10 000) single-day steps yield zero delta;
|
|
11
|
+
// call with 7- or 30-day intervals for visible change.
|
|
12
|
+
//
|
|
13
|
+
// Integration:
|
|
14
|
+
// - Phase 61 (Polity): reads/mutates polity.population; uses morale/stability/techEra.
|
|
15
|
+
// - Phase 56 (Disease): caller passes epidemic annual mortality as deathPressure_Q.
|
|
16
|
+
// - Phase 81 (Migration): computeFamineMigrationPush() is an additive push bonus.
|
|
17
|
+
// - Phase 78 (Calendar): caller may pass seasonal birth/death multipliers.
|
|
18
|
+
import { q, SCALE, clampQ, mulDiv } from "./units.js";
|
|
19
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
20
|
+
/** Baseline annual birth rate [Q = fraction of population per year]. ≈ 3.5%/year. */
|
|
21
|
+
export const BASELINE_BIRTH_RATE_ANNUAL_Q = q(0.035);
|
|
22
|
+
/** Baseline annual death rate [Q = fraction of population per year]. ≈ 3.0%/year. */
|
|
23
|
+
export const BASELINE_DEATH_RATE_ANNUAL_Q = q(0.030);
|
|
24
|
+
/**
|
|
25
|
+
* Morale floor multiplier on birth rate.
|
|
26
|
+
*
|
|
27
|
+
* Birth rate factor = `BIRTH_RATE_MORALE_FLOOR_Q + moraleQ`, yielding:
|
|
28
|
+
* moraleQ = 0 → factor = q(0.50) → birth rate × 0.50
|
|
29
|
+
* moraleQ = SCALE.Q → factor = q(1.50) → birth rate × 1.50
|
|
30
|
+
*/
|
|
31
|
+
export const BIRTH_RATE_MORALE_FLOOR_Q = q(0.50);
|
|
32
|
+
/**
|
|
33
|
+
* Additional annual death rate at zero stability.
|
|
34
|
+
* Linearly scaled by `(SCALE.Q − stabilityQ) / SCALE.Q`.
|
|
35
|
+
* Full bonus at stability = 0; zero at stability = SCALE.Q.
|
|
36
|
+
*/
|
|
37
|
+
export const INSTABILITY_DEATH_ANNUAL_Q = q(0.015);
|
|
38
|
+
/** Food supply fraction below which famine is active [0, SCALE.Q]. */
|
|
39
|
+
export const FAMINE_THRESHOLD_Q = q(0.20);
|
|
40
|
+
/** Additional annual death rate during famine (+3%/year on top of baseline). */
|
|
41
|
+
export const FAMINE_DEATH_ANNUAL_Q = q(0.030);
|
|
42
|
+
/**
|
|
43
|
+
* Peak famine-driven migration push pressure (at food = 0).
|
|
44
|
+
* Integrates with Phase-81 `computePushPressure` as an additive bonus.
|
|
45
|
+
*/
|
|
46
|
+
export const FAMINE_MIGRATION_PUSH_Q = q(0.30);
|
|
47
|
+
/**
|
|
48
|
+
* Tech-era multiplier applied to the baseline death rate.
|
|
49
|
+
* Better technology → lower mortality from disease and malnutrition.
|
|
50
|
+
* Expressed as a Q fraction of SCALE.Q.
|
|
51
|
+
*/
|
|
52
|
+
export const TECH_ERA_DEATH_MUL = {
|
|
53
|
+
"Stone": SCALE.Q, // no reduction
|
|
54
|
+
"Bronze": Math.round(SCALE.Q * 0.95), // −5%
|
|
55
|
+
"Iron": Math.round(SCALE.Q * 0.90), // −10%
|
|
56
|
+
"Medieval": Math.round(SCALE.Q * 0.85), // −15%
|
|
57
|
+
"Renaissance": Math.round(SCALE.Q * 0.80), // −20%
|
|
58
|
+
"Industrial": Math.round(SCALE.Q * 0.65), // −35%
|
|
59
|
+
"Modern": Math.round(SCALE.Q * 0.50), // −50%
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Soft carrying capacity by tech era.
|
|
63
|
+
* `stepPolityPopulation` does not enforce it — host checks `isOverCapacity`
|
|
64
|
+
* and applies extra emigration pressure via Phase-81 if desired.
|
|
65
|
+
*/
|
|
66
|
+
export const CARRYING_CAPACITY_BY_ERA = {
|
|
67
|
+
"Stone": 50_000,
|
|
68
|
+
"Bronze": 200_000,
|
|
69
|
+
"Iron": 500_000,
|
|
70
|
+
"Medieval": 2_000_000,
|
|
71
|
+
"Renaissance": 5_000_000,
|
|
72
|
+
"Industrial": 20_000_000,
|
|
73
|
+
"Modern": 200_000_000,
|
|
74
|
+
};
|
|
75
|
+
// ── Rate computation ──────────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Compute the effective annual birth rate for a polity [Q = fraction/year].
|
|
78
|
+
*
|
|
79
|
+
* Morale scales birth rate linearly between 50% and 150% of baseline:
|
|
80
|
+
* moraleQ = 0 → BASELINE × 0.50 (≈ 1.75%/year)
|
|
81
|
+
* moraleQ = SCALE.Q → BASELINE × 1.50 (≈ 5.25%/year)
|
|
82
|
+
*/
|
|
83
|
+
export function computeBirthRate(polity) {
|
|
84
|
+
// factor ∈ [q(0.50), q(1.50)] = [5000, 15000]
|
|
85
|
+
const factor = BIRTH_RATE_MORALE_FLOOR_Q + polity.moraleQ;
|
|
86
|
+
return clampQ(mulDiv(BASELINE_BIRTH_RATE_ANNUAL_Q, factor, SCALE.Q), 0, SCALE.Q);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compute the effective annual death rate for a polity [Q = fraction/year].
|
|
90
|
+
*
|
|
91
|
+
* Factors (additive):
|
|
92
|
+
* 1. Baseline reduced by tech era (better medicine / nutrition).
|
|
93
|
+
* 2. Instability bonus: up to `INSTABILITY_DEATH_ANNUAL_Q` at stability = 0.
|
|
94
|
+
* 3. External death pressure (caller: Phase-56 epidemic or Phase-84 siege casualties).
|
|
95
|
+
* 4. Famine bonus: `FAMINE_DEATH_ANNUAL_Q` when `foodSupply_Q < FAMINE_THRESHOLD_Q`.
|
|
96
|
+
*
|
|
97
|
+
* @param deathPressure_Q Annual mortality fraction from external cause.
|
|
98
|
+
* @param foodSupply_Q Current food supply [0, SCALE.Q]; omit if unknown.
|
|
99
|
+
*/
|
|
100
|
+
export function computeDeathRate(polity, deathPressure_Q, foodSupply_Q) {
|
|
101
|
+
const techMul = TECH_ERA_DEATH_MUL[polity.techEra] ?? SCALE.Q;
|
|
102
|
+
const techBase = mulDiv(BASELINE_DEATH_RATE_ANNUAL_Q, techMul, SCALE.Q);
|
|
103
|
+
const instFrac = SCALE.Q - polity.stabilityQ; // [0, SCALE.Q]
|
|
104
|
+
const instDeath = mulDiv(INSTABILITY_DEATH_ANNUAL_Q, instFrac, SCALE.Q);
|
|
105
|
+
const famine = foodSupply_Q != null && foodSupply_Q < FAMINE_THRESHOLD_Q;
|
|
106
|
+
const famineBonus = famine ? FAMINE_DEATH_ANNUAL_Q : 0;
|
|
107
|
+
const pressure = deathPressure_Q ?? 0;
|
|
108
|
+
return clampQ(techBase + instDeath + famineBonus + pressure, 0, SCALE.Q);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Compute the net annual growth rate (birth rate − death rate).
|
|
112
|
+
* Negative values indicate population decline.
|
|
113
|
+
*/
|
|
114
|
+
export function computeNetGrowthRate(polity, deathPressure_Q, foodSupply_Q) {
|
|
115
|
+
return computeBirthRate(polity) - computeDeathRate(polity, deathPressure_Q, foodSupply_Q);
|
|
116
|
+
}
|
|
117
|
+
// ── Population step ───────────────────────────────────────────────────────────
|
|
118
|
+
/**
|
|
119
|
+
* Step polity population forward by `elapsedDays` simulated days.
|
|
120
|
+
*
|
|
121
|
+
* Mutates `polity.population` in place and returns step metadata.
|
|
122
|
+
*
|
|
123
|
+
* Delta formula (fixed-point, single rounding):
|
|
124
|
+
* `popDelta = round(population × netAnnualRate_Q × elapsedDays / (365 × SCALE.Q))`
|
|
125
|
+
*
|
|
126
|
+
* @param elapsedDays Number of simulated days to advance (typically 1–30).
|
|
127
|
+
* @param deathPressure_Q Annual mortality fraction from disease or siege casualties.
|
|
128
|
+
* @param foodSupply_Q Food supply level [0, SCALE.Q]; famine fires below
|
|
129
|
+
* `FAMINE_THRESHOLD_Q`.
|
|
130
|
+
*/
|
|
131
|
+
export function stepPolityPopulation(polity, elapsedDays, deathPressure_Q, foodSupply_Q) {
|
|
132
|
+
const birthRate = computeBirthRate(polity);
|
|
133
|
+
const deathRate = computeDeathRate(polity, deathPressure_Q, foodSupply_Q);
|
|
134
|
+
const netRate = birthRate - deathRate;
|
|
135
|
+
const famine = foodSupply_Q != null && foodSupply_Q < FAMINE_THRESHOLD_Q;
|
|
136
|
+
const rawDelta = Math.round(polity.population * netRate * elapsedDays / (365 * SCALE.Q));
|
|
137
|
+
const newPop = Math.max(0, polity.population + rawDelta);
|
|
138
|
+
polity.population = newPop;
|
|
139
|
+
return {
|
|
140
|
+
popDelta: rawDelta,
|
|
141
|
+
newPopulation: newPop,
|
|
142
|
+
effectiveBirthRate_Q: birthRate,
|
|
143
|
+
effectiveDeathRate_Q: deathRate,
|
|
144
|
+
famine,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// ── Famine ────────────────────────────────────────────────────────────────────
|
|
148
|
+
/**
|
|
149
|
+
* Compute famine-driven migration push pressure [0, SCALE.Q].
|
|
150
|
+
*
|
|
151
|
+
* Zero at or above `FAMINE_THRESHOLD_Q`. Scales linearly from zero (at the
|
|
152
|
+
* threshold) to `FAMINE_MIGRATION_PUSH_Q` (at food = 0).
|
|
153
|
+
*
|
|
154
|
+
* Add the result to Phase-81 `computePushPressure` output.
|
|
155
|
+
*/
|
|
156
|
+
export function computeFamineMigrationPush(foodSupply_Q) {
|
|
157
|
+
if (foodSupply_Q >= FAMINE_THRESHOLD_Q)
|
|
158
|
+
return 0;
|
|
159
|
+
const deficit = FAMINE_THRESHOLD_Q - foodSupply_Q; // (0, FAMINE_THRESHOLD_Q]
|
|
160
|
+
return clampQ(mulDiv(FAMINE_MIGRATION_PUSH_Q, deficit, FAMINE_THRESHOLD_Q), 0, SCALE.Q);
|
|
161
|
+
}
|
|
162
|
+
// ── Carrying capacity ─────────────────────────────────────────────────────────
|
|
163
|
+
/**
|
|
164
|
+
* Soft carrying capacity for a polity based on tech era.
|
|
165
|
+
*
|
|
166
|
+
* `stepPolityPopulation` does not enforce this cap. The host should call
|
|
167
|
+
* `isOverCapacity` after each step and pass additional emigration pressure
|
|
168
|
+
* to Phase-81 when it returns `true`.
|
|
169
|
+
*/
|
|
170
|
+
export function computeCarryingCapacity(polity) {
|
|
171
|
+
return CARRYING_CAPACITY_BY_ERA[polity.techEra] ?? 50_000;
|
|
172
|
+
}
|
|
173
|
+
/** Return `true` if the polity's population exceeds its tech-era carrying capacity. */
|
|
174
|
+
export function isOverCapacity(polity) {
|
|
175
|
+
return polity.population > computeCarryingCapacity(polity);
|
|
176
|
+
}
|
|
177
|
+
// ── Reporting utilities ───────────────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Estimate annual births from a birth rate and population.
|
|
180
|
+
* Useful for host display and scenario planning.
|
|
181
|
+
*/
|
|
182
|
+
export function estimateAnnualBirths(population, birthRate_Q) {
|
|
183
|
+
return Math.round(population * birthRate_Q / SCALE.Q);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Estimate annual deaths from a death rate and population.
|
|
187
|
+
*/
|
|
188
|
+
export function estimateAnnualDeaths(population, deathRate_Q) {
|
|
189
|
+
return Math.round(population * deathRate_Q / SCALE.Q);
|
|
190
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@its-not-rocket-science/ananke",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -106,6 +106,10 @@
|
|
|
106
106
|
"./faith": {
|
|
107
107
|
"import": "./dist/src/faith.js",
|
|
108
108
|
"types": "./dist/src/faith.d.ts"
|
|
109
|
+
},
|
|
110
|
+
"./demography": {
|
|
111
|
+
"import": "./dist/src/demography.js",
|
|
112
|
+
"types": "./dist/src/demography.d.ts"
|
|
109
113
|
}
|
|
110
114
|
},
|
|
111
115
|
"files": [
|