@its-not-rocket-science/ananke 0.1.38 → 0.1.40

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 CHANGED
@@ -6,6 +6,50 @@ Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.1.40] — 2026-03-26
10
+
11
+ ### Added
12
+
13
+ - **Phase 95 · Natural Resources & Extraction** (`src/resources.ts`)
14
+ - `ResourceType`: `"iron" | "silver" | "timber" | "stone" | "horses"`.
15
+ - `ResourceDeposit { depositId, polityId, type, richness_Q, maxWorkers }` — immutable site descriptor.
16
+ - `ExtractionState { depositId, assignedWorkers, cumulativeYield_cu }` — mutable accumulator stored externally.
17
+ - `BASE_YIELD_PER_WORKER: Record<ResourceType, number>` — silver 8, horses 5, iron 3, timber/stone 2 cu/worker/day at base.
18
+ - `TECH_EXTRACTION_MUL: Record<number, Q>` — numeric TechEra keys; Prehistoric q(0.40) → DeepSpace q(4.00).
19
+ - `computeDailyYield(deposit, state, techEra)` → cu/day: `workers × baseRate × techMul × richnessMul`; `richnessMul ∈ [q(0.50), q(1.00)]`; 0 when exhausted or no workers.
20
+ - `assignWorkers(deposit, state, workers)` — clamps to `[0, deposit.maxWorkers]`.
21
+ - `depleteDeposit(deposit, yield_cu)` — reduces `richness_Q` by `DEPLETION_RATE_PER_1000_CU = q(0.005)` per 1000 cu extracted.
22
+ - `stepExtraction(deposit, state, polity, elapsedDays)` → `ExtractionStepResult`: adds yield to `polity.treasury_cu`; depletes richness; returns `{ yield_cu, richness_Q, exhausted }`.
23
+ - `computeTotalDailyResourceIncome(deposits, states, techEra)` → cu/day total across all deposits.
24
+ - Secondary bonus sets: `MILITARY_BONUS_RESOURCES` (iron, horses), `CONSTRUCTION_BONUS_RESOURCES` (timber, stone), `MOBILITY_BONUS_RESOURCES` (horses) — advisory flags for Phase-61/89/93.
25
+ - `hasMilitaryBonus / hasConstructionBonus / hasMobilityBonus` helpers.
26
+ - `estimateDaysToExhaustion(deposit, state, techEra)` → ceiling days; Infinity with no workers; 0 when already exhausted.
27
+ - Added `./resources` subpath export to `package.json`.
28
+ - 49 new tests; 4,981 total. Coverage maintained above all thresholds.
29
+
30
+ ---
31
+
32
+ ## [0.1.39] — 2026-03-26
33
+
34
+ ### Added
35
+
36
+ - **Phase 94 · Laws & Governance Codes** (`src/governance.ts`)
37
+ - `GovernanceType`: `"tribal" | "monarchy" | "oligarchy" | "republic" | "empire" | "theocracy"`.
38
+ - `GovernanceModifiers { taxEfficiencyMul_Q, mobilizationMax_Q, researchBonus, unrestMitigation_Q, stabilityIncrement_Q }` — aggregate modifier bundle applied to downstream phases.
39
+ - `GOVERNANCE_BASE: Record<GovernanceType, GovernanceModifiers>` — baseline modifiers per type; tribal maximises mobilisation (q(0.20)) but has lowest tax efficiency (q(0.60)); oligarchy and empire share highest tax efficiency (q(1.00)); theocracy has highest unrest mitigation (q(0.18)); republic has highest research bonus (+3).
40
+ - `LawCode { lawId, name, taxBonus_Q, researchBonus, mobilizationBonus_Q, unrestBonus_Q, stabilityCostPerDay_Q }` — discrete enacted policies.
41
+ - Five preset laws: `LAW_CONSCRIPTION` (+mobilisation, stability cost), `LAW_TAX_REFORM` (+tax), `LAW_SCHOLAR_PATRONAGE` (+5 research), `LAW_RULE_OF_LAW` (+tax +unrest mitigation), `LAW_MARTIAL_LAW` (+unrest mitigation, heavy stability drain).
42
+ - `GovernanceState { polityId, governanceType, activeLawIds, changeCooldown }`.
43
+ - `computeGovernanceModifiers(state, lawRegistry?)` — stacks law bonuses on governance baseline; clamps all outputs.
44
+ - `enactLaw(state, lawId)` / `repealLaw(state, lawId)` — add/remove laws; enforces `MAX_ACTIVE_LAWS = 5`.
45
+ - `changeGovernance(polity, state, newType)` — hits `polity.stabilityQ` by q(0.20); sets 365-day cooldown; no-op on same type or during cooldown.
46
+ - `stepGovernanceCooldown(state, elapsedDays)` — ticks down cooldown.
47
+ - `stepGovernanceStability(polity, state, elapsedDays, lawRegistry?)` — applies net `stabilityIncrement_Q` per day to `polity.stabilityQ`; no-op when law costs cancel the baseline.
48
+ - Added `./governance` subpath export to `package.json`.
49
+ - 48 new tests; 4,932 total. 100% statement/branch/function/line coverage. All thresholds met.
50
+
51
+ ---
52
+
9
53
  ## [0.1.38] — 2026-03-26
10
54
 
11
55
  ### Added
@@ -0,0 +1,125 @@
1
+ import type { Q } from "./units.js";
2
+ import type { Polity } from "./polity.js";
3
+ /** Governance form of a polity. */
4
+ export type GovernanceType = "tribal" | "monarchy" | "oligarchy" | "republic" | "empire" | "theocracy";
5
+ /**
6
+ * Modifier bundle derived from a polity's governance type and enacted laws.
7
+ * Each field is a Q multiplier or bonus that callers pass to downstream phases.
8
+ */
9
+ export interface GovernanceModifiers {
10
+ /** Multiplier on Phase-92 annual tax revenue [Q]. */
11
+ taxEfficiencyMul_Q: Q;
12
+ /** Maximum mobilisation fraction override for Phase-93 [Q]. */
13
+ mobilizationMax_Q: Q;
14
+ /** Flat daily research point bonus for Phase-91 [integer]. */
15
+ researchBonus: number;
16
+ /**
17
+ * Passive unrest mitigation [Q].
18
+ * Subtract from raw unrest level before Phase-90 thresholds.
19
+ */
20
+ unrestMitigation_Q: Q;
21
+ /** Passive daily stability increment [Q/day × 100 to avoid sub-unit loss]. */
22
+ stabilityIncrement_Q: Q;
23
+ }
24
+ /** A discrete enacted law providing targeted modifiers. */
25
+ export interface LawCode {
26
+ lawId: string;
27
+ name: string;
28
+ /** Additive bonus to `taxEfficiencyMul_Q` [Q]. */
29
+ taxBonus_Q: Q;
30
+ /** Additive bonus to `researchBonus` [integer]. */
31
+ researchBonus: number;
32
+ /** Additive bonus to `mobilizationMax_Q` [Q]. */
33
+ mobilizationBonus_Q: Q;
34
+ /** Additive bonus to `unrestMitigation_Q` [Q]. */
35
+ unrestBonus_Q: Q;
36
+ /** Stability cost per day while this law is active [Q]. */
37
+ stabilityCostPerDay_Q: Q;
38
+ }
39
+ /** Per-polity governance state. Store one externally per polity. */
40
+ export interface GovernanceState {
41
+ polityId: string;
42
+ governanceType: GovernanceType;
43
+ /** IDs of currently enacted laws. Max `MAX_ACTIVE_LAWS`. */
44
+ activeLawIds: string[];
45
+ /**
46
+ * Cooldown days before governance type can be changed again.
47
+ * `stepGovernanceCooldown` decrements this.
48
+ */
49
+ changeCooldown: number;
50
+ }
51
+ /**
52
+ * Maximum number of laws that can be active simultaneously.
53
+ */
54
+ export declare const MAX_ACTIVE_LAWS = 5;
55
+ /**
56
+ * Stability penalty applied when changing governance type [Q].
57
+ * Represents the upheaval of political transition.
58
+ */
59
+ export declare const GOVERNANCE_CHANGE_STABILITY_HIT_Q: Q;
60
+ /**
61
+ * Cooldown days after a governance change before another is allowed.
62
+ */
63
+ export declare const GOVERNANCE_CHANGE_COOLDOWN_DAYS = 365;
64
+ /**
65
+ * Baseline governance modifiers for each type.
66
+ * Callers layer law-code bonuses on top.
67
+ */
68
+ export declare const GOVERNANCE_BASE: Record<GovernanceType, GovernanceModifiers>;
69
+ /** Conscription law: larger armies, minor stability cost. */
70
+ export declare const LAW_CONSCRIPTION: LawCode;
71
+ /** Tax reform: better tax efficiency, minor unrest from displacing old collectors. */
72
+ export declare const LAW_TAX_REFORM: LawCode;
73
+ /** Patronage of scholars: research bonus, expensive. */
74
+ export declare const LAW_SCHOLAR_PATRONAGE: LawCode;
75
+ /** Rule of law: stability bonus, research bonus, small unrest mitigation. */
76
+ export declare const LAW_RULE_OF_LAW: LawCode;
77
+ /** Martial law: strong unrest mitigation but heavy stability drain. */
78
+ export declare const LAW_MARTIAL_LAW: LawCode;
79
+ export declare const PRESET_LAW_CODES: LawCode[];
80
+ /** Create a fresh `GovernanceState` with no laws and no cooldown. */
81
+ export declare function createGovernanceState(polityId: string, governanceType?: GovernanceType): GovernanceState;
82
+ /**
83
+ * Compute the aggregate `GovernanceModifiers` for the given state plus active laws.
84
+ *
85
+ * Each law's bonuses are added on top of the governance baseline.
86
+ * `taxEfficiencyMul_Q` is clamped to SCALE.Q; others to [0, SCALE.Q].
87
+ *
88
+ * @param lawRegistry Map of lawId → LawCode. Pass only enacted laws.
89
+ */
90
+ export declare function computeGovernanceModifiers(state: GovernanceState, lawRegistry?: Map<string, LawCode>): GovernanceModifiers;
91
+ /**
92
+ * Enact a new law. Returns `false` if already enacted or at `MAX_ACTIVE_LAWS`.
93
+ */
94
+ export declare function enactLaw(state: GovernanceState, lawId: string): boolean;
95
+ /**
96
+ * Repeal an active law. Returns `false` if the law was not active.
97
+ */
98
+ export declare function repealLaw(state: GovernanceState, lawId: string): boolean;
99
+ /**
100
+ * Change the governance type of a polity.
101
+ *
102
+ * Applies `GOVERNANCE_CHANGE_STABILITY_HIT_Q` to `polity.stabilityQ` and
103
+ * sets `state.changeCooldown = GOVERNANCE_CHANGE_COOLDOWN_DAYS`.
104
+ *
105
+ * Returns `false` (no-op) if:
106
+ * - `newType` is the same as current type.
107
+ * - `state.changeCooldown > 0` (still cooling down).
108
+ */
109
+ export declare function changeGovernance(polity: Polity, state: GovernanceState, newType: GovernanceType): boolean;
110
+ /**
111
+ * Tick down the governance change cooldown.
112
+ * Mutates `state.changeCooldown`; never goes below 0.
113
+ */
114
+ export declare function stepGovernanceCooldown(state: GovernanceState, elapsedDays: number): void;
115
+ /**
116
+ * Apply the governance passive stability increment per elapsed days.
117
+ *
118
+ * Uses `computeGovernanceModifiers` to get the net `stabilityIncrement_Q`,
119
+ * then adds `increment × elapsedDays` to `polity.stabilityQ`.
120
+ *
121
+ * No-op if net increment is 0 (law costs cancel the baseline bonus).
122
+ *
123
+ * @param lawRegistry Active law registry.
124
+ */
125
+ export declare function stepGovernanceStability(polity: Polity, state: GovernanceState, elapsedDays: number, lawRegistry?: Map<string, LawCode>): void;
@@ -0,0 +1,251 @@
1
+ // src/governance.ts — Phase 94: Laws & Governance Codes
2
+ //
3
+ // The governance type of a polity shapes how effectively it taxes, mobilises
4
+ // armies, maintains stability, and advances research. Law codes are discrete
5
+ // enacted policies that provide targeted bonuses and penalties on top of the
6
+ // governance baseline.
7
+ //
8
+ // Design:
9
+ // - Pure data layer — no Entity fields, no kernel changes.
10
+ // - `GovernanceState` is stored externally per polity by the host.
11
+ // - `computeGovernanceModifiers` returns a single struct; callers apply each
12
+ // field to the appropriate Phase (92 tax, 91 research, 93 mobilisation, 90 unrest).
13
+ // - Governance changes trigger a stability hit and a cooldown before the
14
+ // next change is allowed.
15
+ // - All arithmetic is integer fixed-point; no floating-point accumulation.
16
+ //
17
+ // Integration:
18
+ // Phase 61 (Polity): stabilityQ mutated on governance change.
19
+ // Phase 90 (Unrest): unrestMitigation_Q reduces effective unrest.
20
+ // Phase 91 (Research): researchBonus added as flat bonus points/day.
21
+ // Phase 92 (Taxation): taxEfficiencyMul scales annual revenue.
22
+ // Phase 93 (Campaign): mobilizationMax_Q overrides default mobilisation cap.
23
+ import { q, SCALE, clampQ } from "./units.js";
24
+ // ── Constants ─────────────────────────────────────────────────────────────────
25
+ /**
26
+ * Maximum number of laws that can be active simultaneously.
27
+ */
28
+ export const MAX_ACTIVE_LAWS = 5;
29
+ /**
30
+ * Stability penalty applied when changing governance type [Q].
31
+ * Represents the upheaval of political transition.
32
+ */
33
+ export const GOVERNANCE_CHANGE_STABILITY_HIT_Q = q(0.20);
34
+ /**
35
+ * Cooldown days after a governance change before another is allowed.
36
+ */
37
+ export const GOVERNANCE_CHANGE_COOLDOWN_DAYS = 365;
38
+ /**
39
+ * Baseline governance modifiers for each type.
40
+ * Callers layer law-code bonuses on top.
41
+ */
42
+ export const GOVERNANCE_BASE = {
43
+ tribal: {
44
+ taxEfficiencyMul_Q: q(0.60),
45
+ mobilizationMax_Q: q(0.20), // can field a larger fraction but untrained
46
+ researchBonus: 0,
47
+ unrestMitigation_Q: q(0.05),
48
+ stabilityIncrement_Q: 0,
49
+ },
50
+ monarchy: {
51
+ taxEfficiencyMul_Q: q(0.80),
52
+ mobilizationMax_Q: q(0.12),
53
+ researchBonus: 1,
54
+ unrestMitigation_Q: q(0.08),
55
+ stabilityIncrement_Q: q(0.001),
56
+ },
57
+ oligarchy: {
58
+ taxEfficiencyMul_Q: q(1.00), // efficient tax extraction for the elite
59
+ mobilizationMax_Q: q(0.08), // mercenary-reliant, smaller citizen levy
60
+ researchBonus: 2,
61
+ unrestMitigation_Q: q(0.03),
62
+ stabilityIncrement_Q: 0,
63
+ },
64
+ republic: {
65
+ taxEfficiencyMul_Q: q(0.90),
66
+ mobilizationMax_Q: q(0.10),
67
+ researchBonus: 3,
68
+ unrestMitigation_Q: q(0.10),
69
+ stabilityIncrement_Q: q(0.002),
70
+ },
71
+ empire: {
72
+ taxEfficiencyMul_Q: q(1.00),
73
+ mobilizationMax_Q: q(0.15),
74
+ researchBonus: 2,
75
+ unrestMitigation_Q: q(0.12),
76
+ stabilityIncrement_Q: q(0.001),
77
+ },
78
+ theocracy: {
79
+ taxEfficiencyMul_Q: q(0.70), // tithes are less efficient than direct tax
80
+ mobilizationMax_Q: q(0.10),
81
+ researchBonus: 1,
82
+ unrestMitigation_Q: q(0.18), // religious legitimacy suppresses unrest
83
+ stabilityIncrement_Q: q(0.002),
84
+ },
85
+ };
86
+ // ── Preset law codes ──────────────────────────────────────────────────────────
87
+ /** Conscription law: larger armies, minor stability cost. */
88
+ export const LAW_CONSCRIPTION = {
89
+ lawId: "conscription",
90
+ name: "Conscription",
91
+ taxBonus_Q: 0,
92
+ researchBonus: 0,
93
+ mobilizationBonus_Q: q(0.03),
94
+ unrestBonus_Q: 0,
95
+ stabilityCostPerDay_Q: q(0.001),
96
+ };
97
+ /** Tax reform: better tax efficiency, minor unrest from displacing old collectors. */
98
+ export const LAW_TAX_REFORM = {
99
+ lawId: "tax_reform",
100
+ name: "Tax Reform",
101
+ taxBonus_Q: q(0.10),
102
+ researchBonus: 0,
103
+ mobilizationBonus_Q: 0,
104
+ unrestBonus_Q: q(0.02),
105
+ stabilityCostPerDay_Q: 0,
106
+ };
107
+ /** Patronage of scholars: research bonus, expensive. */
108
+ export const LAW_SCHOLAR_PATRONAGE = {
109
+ lawId: "scholar_patronage",
110
+ name: "Scholar Patronage",
111
+ taxBonus_Q: 0,
112
+ researchBonus: 5,
113
+ mobilizationBonus_Q: 0,
114
+ unrestBonus_Q: 0,
115
+ stabilityCostPerDay_Q: 0,
116
+ };
117
+ /** Rule of law: stability bonus, research bonus, small unrest mitigation. */
118
+ export const LAW_RULE_OF_LAW = {
119
+ lawId: "rule_of_law",
120
+ name: "Rule of Law",
121
+ taxBonus_Q: q(0.05),
122
+ researchBonus: 1,
123
+ mobilizationBonus_Q: 0,
124
+ unrestBonus_Q: q(0.05),
125
+ stabilityCostPerDay_Q: 0,
126
+ };
127
+ /** Martial law: strong unrest mitigation but heavy stability drain. */
128
+ export const LAW_MARTIAL_LAW = {
129
+ lawId: "martial_law",
130
+ name: "Martial Law",
131
+ taxBonus_Q: 0,
132
+ researchBonus: 0,
133
+ mobilizationBonus_Q: q(0.02),
134
+ unrestBonus_Q: q(0.12),
135
+ stabilityCostPerDay_Q: q(0.003),
136
+ };
137
+ export const PRESET_LAW_CODES = [
138
+ LAW_CONSCRIPTION,
139
+ LAW_TAX_REFORM,
140
+ LAW_SCHOLAR_PATRONAGE,
141
+ LAW_RULE_OF_LAW,
142
+ LAW_MARTIAL_LAW,
143
+ ];
144
+ // ── Factory ───────────────────────────────────────────────────────────────────
145
+ /** Create a fresh `GovernanceState` with no laws and no cooldown. */
146
+ export function createGovernanceState(polityId, governanceType = "monarchy") {
147
+ return { polityId, governanceType, activeLawIds: [], changeCooldown: 0 };
148
+ }
149
+ // ── Modifier computation ──────────────────────────────────────────────────────
150
+ /**
151
+ * Compute the aggregate `GovernanceModifiers` for the given state plus active laws.
152
+ *
153
+ * Each law's bonuses are added on top of the governance baseline.
154
+ * `taxEfficiencyMul_Q` is clamped to SCALE.Q; others to [0, SCALE.Q].
155
+ *
156
+ * @param lawRegistry Map of lawId → LawCode. Pass only enacted laws.
157
+ */
158
+ export function computeGovernanceModifiers(state, lawRegistry = new Map()) {
159
+ const base = GOVERNANCE_BASE[state.governanceType];
160
+ let taxMul = base.taxEfficiencyMul_Q;
161
+ let mobilMax = base.mobilizationMax_Q;
162
+ let research = base.researchBonus;
163
+ let unrestMit = base.unrestMitigation_Q;
164
+ let stabilityInc = base.stabilityIncrement_Q;
165
+ for (const lawId of state.activeLawIds) {
166
+ const law = lawRegistry.get(lawId);
167
+ if (!law)
168
+ continue;
169
+ taxMul = clampQ(taxMul + law.taxBonus_Q, 0, SCALE.Q);
170
+ mobilMax = clampQ(mobilMax + law.mobilizationBonus_Q, 0, SCALE.Q);
171
+ research += law.researchBonus;
172
+ unrestMit = clampQ(unrestMit + law.unrestBonus_Q, 0, SCALE.Q);
173
+ // stability cost from active laws reduces the passive increment
174
+ stabilityInc = clampQ(stabilityInc - law.stabilityCostPerDay_Q, 0, SCALE.Q);
175
+ }
176
+ return {
177
+ taxEfficiencyMul_Q: taxMul,
178
+ mobilizationMax_Q: mobilMax,
179
+ researchBonus: Math.max(0, research),
180
+ unrestMitigation_Q: unrestMit,
181
+ stabilityIncrement_Q: stabilityInc,
182
+ };
183
+ }
184
+ // ── Law management ────────────────────────────────────────────────────────────
185
+ /**
186
+ * Enact a new law. Returns `false` if already enacted or at `MAX_ACTIVE_LAWS`.
187
+ */
188
+ export function enactLaw(state, lawId) {
189
+ if (state.activeLawIds.includes(lawId))
190
+ return false;
191
+ if (state.activeLawIds.length >= MAX_ACTIVE_LAWS)
192
+ return false;
193
+ state.activeLawIds.push(lawId);
194
+ return true;
195
+ }
196
+ /**
197
+ * Repeal an active law. Returns `false` if the law was not active.
198
+ */
199
+ export function repealLaw(state, lawId) {
200
+ const idx = state.activeLawIds.indexOf(lawId);
201
+ if (idx === -1)
202
+ return false;
203
+ state.activeLawIds.splice(idx, 1);
204
+ return true;
205
+ }
206
+ // ── Governance change ─────────────────────────────────────────────────────────
207
+ /**
208
+ * Change the governance type of a polity.
209
+ *
210
+ * Applies `GOVERNANCE_CHANGE_STABILITY_HIT_Q` to `polity.stabilityQ` and
211
+ * sets `state.changeCooldown = GOVERNANCE_CHANGE_COOLDOWN_DAYS`.
212
+ *
213
+ * Returns `false` (no-op) if:
214
+ * - `newType` is the same as current type.
215
+ * - `state.changeCooldown > 0` (still cooling down).
216
+ */
217
+ export function changeGovernance(polity, state, newType) {
218
+ if (state.governanceType === newType)
219
+ return false;
220
+ if (state.changeCooldown > 0)
221
+ return false;
222
+ state.governanceType = newType;
223
+ state.changeCooldown = GOVERNANCE_CHANGE_COOLDOWN_DAYS;
224
+ polity.stabilityQ = clampQ(polity.stabilityQ - GOVERNANCE_CHANGE_STABILITY_HIT_Q, 0, SCALE.Q);
225
+ return true;
226
+ }
227
+ /**
228
+ * Tick down the governance change cooldown.
229
+ * Mutates `state.changeCooldown`; never goes below 0.
230
+ */
231
+ export function stepGovernanceCooldown(state, elapsedDays) {
232
+ state.changeCooldown = Math.max(0, state.changeCooldown - elapsedDays);
233
+ }
234
+ // ── Passive stability tick ────────────────────────────────────────────────────
235
+ /**
236
+ * Apply the governance passive stability increment per elapsed days.
237
+ *
238
+ * Uses `computeGovernanceModifiers` to get the net `stabilityIncrement_Q`,
239
+ * then adds `increment × elapsedDays` to `polity.stabilityQ`.
240
+ *
241
+ * No-op if net increment is 0 (law costs cancel the baseline bonus).
242
+ *
243
+ * @param lawRegistry Active law registry.
244
+ */
245
+ export function stepGovernanceStability(polity, state, elapsedDays, lawRegistry = new Map()) {
246
+ const mods = computeGovernanceModifiers(state, lawRegistry);
247
+ if (mods.stabilityIncrement_Q <= 0)
248
+ return;
249
+ const delta = Math.round(mods.stabilityIncrement_Q * elapsedDays);
250
+ polity.stabilityQ = clampQ(polity.stabilityQ + delta, 0, SCALE.Q);
251
+ }
@@ -0,0 +1,149 @@
1
+ import type { Q } from "./units.js";
2
+ import type { Polity } from "./polity.js";
3
+ /** Classification of natural resource. */
4
+ export type ResourceType = "iron" | "silver" | "timber" | "stone" | "horses";
5
+ /** Immutable descriptor for a natural resource deposit. */
6
+ export interface ResourceDeposit {
7
+ depositId: string;
8
+ polityId: string;
9
+ type: ResourceType;
10
+ /**
11
+ * Initial richness [0, SCALE.Q]. Declines with extraction via `depleteDeposit`.
12
+ * A richness of SCALE.Q represents an exceptionally rich find.
13
+ */
14
+ richness_Q: Q;
15
+ /**
16
+ * Maximum workers that can be productively assigned to this deposit.
17
+ * Additional workers beyond this are wasted.
18
+ */
19
+ maxWorkers: number;
20
+ }
21
+ /** Mutable extraction state — store one externally per deposit per polity. */
22
+ export interface ExtractionState {
23
+ depositId: string;
24
+ /** Current worker count (may not exceed `deposit.maxWorkers`). */
25
+ assignedWorkers: number;
26
+ /** Cumulative cost-units yielded since deposit was first worked. */
27
+ cumulativeYield_cu: number;
28
+ }
29
+ /** Output of `stepExtraction`. */
30
+ export interface ExtractionStepResult {
31
+ /** Cost-units added to polity treasury this step. */
32
+ yield_cu: number;
33
+ /** Current richness after any depletion [0, SCALE.Q]. */
34
+ richness_Q: Q;
35
+ /**
36
+ * Whether the deposit is now exhausted (`richness_Q <= DEPLETION_EXHAUSTED_Q`).
37
+ */
38
+ exhausted: boolean;
39
+ }
40
+ /**
41
+ * Base daily yield per worker [cost-units/worker/day] at full richness and
42
+ * base tech era (Ancient = 1, the reference point).
43
+ */
44
+ export declare const BASE_YIELD_PER_WORKER: Record<ResourceType, number>;
45
+ /**
46
+ * Tech era extraction efficiency multiplier [Q].
47
+ * Better tools, techniques, and logistics improve yield per worker.
48
+ */
49
+ export declare const TECH_EXTRACTION_MUL: Record<number, Q>;
50
+ /**
51
+ * Fraction of base yield that richness scales against [Q].
52
+ * At richness q(0.50), yield = base × (0.50 + 0.50×0.50) = base × 0.75
53
+ * — partial depletion still produces meaningful income.
54
+ */
55
+ export declare const RICHNESS_FLOOR_Q: Q;
56
+ /**
57
+ * Richness reduction per 1000 cost-units of cumulative yield.
58
+ * Controls the depletion rate. Lower values mean longer-lived deposits.
59
+ */
60
+ export declare const DEPLETION_RATE_PER_1000_CU: Q;
61
+ /**
62
+ * Richness threshold below which the deposit is considered exhausted [Q].
63
+ * Extraction becomes uneconomical below this level.
64
+ */
65
+ export declare const DEPLETION_EXHAUSTED_Q: Q;
66
+ /**
67
+ * Maximum fraction of polity population that can be assigned as resource
68
+ * workers without impacting farming/tax base [Q].
69
+ * Hosts should warn if this is exceeded.
70
+ */
71
+ export declare const WORKER_POP_FRACTION_Q: Q;
72
+ /**
73
+ * Resource types that provide a military equipment bonus when worked.
74
+ * Hosts apply this to Phase-61 `deriveMilitaryStrength` or Phase-93 strength.
75
+ */
76
+ export declare const MILITARY_BONUS_RESOURCES: ReadonlySet<ResourceType>;
77
+ /**
78
+ * Resource types that provide a construction cost discount when worked.
79
+ * Hosts apply this to Phase-89 `investInProject` cost calculations.
80
+ */
81
+ export declare const CONSTRUCTION_BONUS_RESOURCES: ReadonlySet<ResourceType>;
82
+ /**
83
+ * Resource types that improve march rate when worked.
84
+ * Hosts add a road-equivalent bonus to Phase-93 `stepCampaignMarch`.
85
+ */
86
+ export declare const MOBILITY_BONUS_RESOURCES: ReadonlySet<ResourceType>;
87
+ /** Create a new `ResourceDeposit`. */
88
+ export declare function createDeposit(depositId: string, polityId: string, type: ResourceType, richness_Q?: Q, maxWorkers?: number): ResourceDeposit;
89
+ /** Create a fresh `ExtractionState` with no workers assigned. */
90
+ export declare function createExtractionState(depositId: string): ExtractionState;
91
+ /**
92
+ * Assign workers to a deposit.
93
+ * Clamps to `[0, deposit.maxWorkers]`.
94
+ * Returns the effective worker count after clamping.
95
+ */
96
+ export declare function assignWorkers(deposit: ResourceDeposit, state: ExtractionState, workers: number): number;
97
+ /**
98
+ * Compute the daily extraction yield [cost-units/day].
99
+ *
100
+ * Formula:
101
+ * techMul = TECH_EXTRACTION_MUL[techEra] (default q(0.60))
102
+ * richnessScale = RICHNESS_FLOOR_Q + mulDiv(SCALE.Q - RICHNESS_FLOOR_Q, richness_Q, SCALE.Q)
103
+ * ∈ [q(0.50), q(1.00)]
104
+ * base = workers × BASE_YIELD_PER_WORKER[type]
105
+ * daily = max(0, round(base × techMul / SCALE.Q × richnessScale / SCALE.Q))
106
+ *
107
+ * Returns 0 if the deposit is exhausted or no workers assigned.
108
+ */
109
+ export declare function computeDailyYield(deposit: ResourceDeposit, state: ExtractionState, techEra: number): number;
110
+ /**
111
+ * Reduce deposit richness based on cumulative yield extracted.
112
+ *
113
+ * `richnessDrain = round(yield_cu × DEPLETION_RATE_PER_1000_CU / 1000)`
114
+ *
115
+ * Mutates `deposit.richness_Q`.
116
+ */
117
+ export declare function depleteDeposit(deposit: ResourceDeposit, yieldThisStep_cu: number): void;
118
+ /**
119
+ * Advance extraction for `elapsedDays` days.
120
+ *
121
+ * 1. Computes `computeDailyYield × elapsedDays`.
122
+ * 2. Adds yield to `polity.treasury_cu` and `state.cumulativeYield_cu`.
123
+ * 3. Depletes deposit richness proportional to yield.
124
+ *
125
+ * Mutates `polity.treasury_cu`, `state.cumulativeYield_cu`, and `deposit.richness_Q`.
126
+ */
127
+ export declare function stepExtraction(deposit: ResourceDeposit, state: ExtractionState, polity: Polity, elapsedDays: number): ExtractionStepResult;
128
+ /**
129
+ * Estimate daily bonus income from resource extraction across multiple deposits.
130
+ * Useful for treasury planning alongside Phase-92 tax revenue.
131
+ */
132
+ export declare function computeTotalDailyResourceIncome(deposits: ResourceDeposit[], states: Map<string, ExtractionState>, techEra: number): number;
133
+ /**
134
+ * Return true if this resource type provides a military bonus.
135
+ */
136
+ export declare function hasMilitaryBonus(type: ResourceType): boolean;
137
+ /**
138
+ * Return true if this resource type provides a construction bonus.
139
+ */
140
+ export declare function hasConstructionBonus(type: ResourceType): boolean;
141
+ /**
142
+ * Return true if this resource type provides a mobility bonus.
143
+ */
144
+ export declare function hasMobilityBonus(type: ResourceType): boolean;
145
+ /**
146
+ * Estimate how many days until the deposit is exhausted at the current
147
+ * extraction rate. Returns `Infinity` if no workers or already exhausted.
148
+ */
149
+ export declare function estimateDaysToExhaustion(deposit: ResourceDeposit, state: ExtractionState, techEra: number): number;
@@ -0,0 +1,219 @@
1
+ // src/resources.ts — Phase 95: Natural Resources & Extraction
2
+ //
3
+ // Polity economies are not purely population-driven. Mines, forests, quarries,
4
+ // and pastures provide resource income that is largely independent of population
5
+ // size. This module tracks resource deposits, worker assignment, daily yield,
6
+ // and gradual depletion.
7
+ //
8
+ // Design:
9
+ // - Pure data layer — no Entity fields, no kernel changes.
10
+ // - `ResourceDeposit` is the immutable site descriptor; `ExtractionState` is
11
+ // the mutable accumulator stored externally by the host.
12
+ // - Yield scales with workers, tech era, and deposit richness.
13
+ // - Richness slowly declines with cumulative extraction (depletion model).
14
+ // - Resource income is expressed in treasury cost-units for uniform integration
15
+ // with Phase-92 taxation and Phase-89 infrastructure.
16
+ // - Secondary bonus flags (`militaryBonus`, `constructionBonus`, `mobilityBonus`)
17
+ // are advisory — the host applies them to Phase-61/89/93 calls.
18
+ //
19
+ // Integration:
20
+ // Phase 11 (Tech): techEra multiplier improves extraction efficiency.
21
+ // Phase 61 (Polity): treasury_cu receives daily yield; population caps workers.
22
+ // Phase 89 (Infra): timber/stone → construction discount advisory flag.
23
+ // Phase 93 (Campaign): horses → march-rate advisory flag.
24
+ // Phase 92 (Taxation): resource income is additive to tax revenue.
25
+ import { q, SCALE, clampQ, mulDiv } from "./units.js";
26
+ import { TechEra } from "./sim/tech.js";
27
+ // ── Constants ─────────────────────────────────────────────────────────────────
28
+ /**
29
+ * Base daily yield per worker [cost-units/worker/day] at full richness and
30
+ * base tech era (Ancient = 1, the reference point).
31
+ */
32
+ export const BASE_YIELD_PER_WORKER = {
33
+ iron: 3, // ore smelting; military equipment
34
+ silver: 8, // coinage; highest raw value
35
+ timber: 2, // construction material
36
+ stone: 2, // construction material
37
+ horses: 5, // breeding; cavalry
38
+ };
39
+ /**
40
+ * Tech era extraction efficiency multiplier [Q].
41
+ * Better tools, techniques, and logistics improve yield per worker.
42
+ */
43
+ export const TECH_EXTRACTION_MUL = {
44
+ [TechEra.Prehistoric]: q(0.40),
45
+ [TechEra.Ancient]: q(0.60),
46
+ [TechEra.Medieval]: q(0.80),
47
+ [TechEra.EarlyModern]: q(1.00),
48
+ [TechEra.Industrial]: q(1.50),
49
+ [TechEra.Modern]: q(2.00),
50
+ [TechEra.NearFuture]: q(2.50),
51
+ [TechEra.FarFuture]: q(3.00),
52
+ [TechEra.DeepSpace]: q(4.00),
53
+ };
54
+ /**
55
+ * Fraction of base yield that richness scales against [Q].
56
+ * At richness q(0.50), yield = base × (0.50 + 0.50×0.50) = base × 0.75
57
+ * — partial depletion still produces meaningful income.
58
+ */
59
+ export const RICHNESS_FLOOR_Q = q(0.50);
60
+ /**
61
+ * Richness reduction per 1000 cost-units of cumulative yield.
62
+ * Controls the depletion rate. Lower values mean longer-lived deposits.
63
+ */
64
+ export const DEPLETION_RATE_PER_1000_CU = q(0.005);
65
+ /**
66
+ * Richness threshold below which the deposit is considered exhausted [Q].
67
+ * Extraction becomes uneconomical below this level.
68
+ */
69
+ export const DEPLETION_EXHAUSTED_Q = q(0.05);
70
+ /**
71
+ * Maximum fraction of polity population that can be assigned as resource
72
+ * workers without impacting farming/tax base [Q].
73
+ * Hosts should warn if this is exceeded.
74
+ */
75
+ export const WORKER_POP_FRACTION_Q = q(0.10);
76
+ // ── Secondary bonus flags ─────────────────────────────────────────────────────
77
+ /**
78
+ * Resource types that provide a military equipment bonus when worked.
79
+ * Hosts apply this to Phase-61 `deriveMilitaryStrength` or Phase-93 strength.
80
+ */
81
+ export const MILITARY_BONUS_RESOURCES = new Set(["iron", "horses"]);
82
+ /**
83
+ * Resource types that provide a construction cost discount when worked.
84
+ * Hosts apply this to Phase-89 `investInProject` cost calculations.
85
+ */
86
+ export const CONSTRUCTION_BONUS_RESOURCES = new Set(["timber", "stone"]);
87
+ /**
88
+ * Resource types that improve march rate when worked.
89
+ * Hosts add a road-equivalent bonus to Phase-93 `stepCampaignMarch`.
90
+ */
91
+ export const MOBILITY_BONUS_RESOURCES = new Set(["horses"]);
92
+ // ── Factory ───────────────────────────────────────────────────────────────────
93
+ /** Create a new `ResourceDeposit`. */
94
+ export function createDeposit(depositId, polityId, type, richness_Q = q(0.80), maxWorkers = 500) {
95
+ return { depositId, polityId, type, richness_Q: clampQ(richness_Q, 0, SCALE.Q), maxWorkers };
96
+ }
97
+ /** Create a fresh `ExtractionState` with no workers assigned. */
98
+ export function createExtractionState(depositId) {
99
+ return { depositId, assignedWorkers: 0, cumulativeYield_cu: 0 };
100
+ }
101
+ // ── Worker management ─────────────────────────────────────────────────────────
102
+ /**
103
+ * Assign workers to a deposit.
104
+ * Clamps to `[0, deposit.maxWorkers]`.
105
+ * Returns the effective worker count after clamping.
106
+ */
107
+ export function assignWorkers(deposit, state, workers) {
108
+ state.assignedWorkers = Math.max(0, Math.min(workers, deposit.maxWorkers));
109
+ return state.assignedWorkers;
110
+ }
111
+ // ── Yield computation ─────────────────────────────────────────────────────────
112
+ /**
113
+ * Compute the daily extraction yield [cost-units/day].
114
+ *
115
+ * Formula:
116
+ * techMul = TECH_EXTRACTION_MUL[techEra] (default q(0.60))
117
+ * richnessScale = RICHNESS_FLOOR_Q + mulDiv(SCALE.Q - RICHNESS_FLOOR_Q, richness_Q, SCALE.Q)
118
+ * ∈ [q(0.50), q(1.00)]
119
+ * base = workers × BASE_YIELD_PER_WORKER[type]
120
+ * daily = max(0, round(base × techMul / SCALE.Q × richnessScale / SCALE.Q))
121
+ *
122
+ * Returns 0 if the deposit is exhausted or no workers assigned.
123
+ */
124
+ export function computeDailyYield(deposit, state, techEra) {
125
+ if (deposit.richness_Q <= DEPLETION_EXHAUSTED_Q)
126
+ return 0;
127
+ if (state.assignedWorkers <= 0)
128
+ return 0;
129
+ const techMul = (TECH_EXTRACTION_MUL[techEra] ?? q(0.60));
130
+ const richnessMul = RICHNESS_FLOOR_Q + mulDiv(SCALE.Q - RICHNESS_FLOOR_Q, deposit.richness_Q, SCALE.Q);
131
+ const basePerWorker = BASE_YIELD_PER_WORKER[deposit.type] ?? 0;
132
+ const base = state.assignedWorkers * basePerWorker;
133
+ const withTech = Math.round(base * techMul / SCALE.Q);
134
+ return Math.max(0, Math.round(withTech * richnessMul / SCALE.Q));
135
+ }
136
+ // ── Depletion ─────────────────────────────────────────────────────────────────
137
+ /**
138
+ * Reduce deposit richness based on cumulative yield extracted.
139
+ *
140
+ * `richnessDrain = round(yield_cu × DEPLETION_RATE_PER_1000_CU / 1000)`
141
+ *
142
+ * Mutates `deposit.richness_Q`.
143
+ */
144
+ export function depleteDeposit(deposit, yieldThisStep_cu) {
145
+ if (yieldThisStep_cu <= 0)
146
+ return;
147
+ const drain = Math.round(yieldThisStep_cu * DEPLETION_RATE_PER_1000_CU / 1000);
148
+ deposit.richness_Q = clampQ(deposit.richness_Q - drain, 0, SCALE.Q);
149
+ }
150
+ // ── Extraction step ───────────────────────────────────────────────────────────
151
+ /**
152
+ * Advance extraction for `elapsedDays` days.
153
+ *
154
+ * 1. Computes `computeDailyYield × elapsedDays`.
155
+ * 2. Adds yield to `polity.treasury_cu` and `state.cumulativeYield_cu`.
156
+ * 3. Depletes deposit richness proportional to yield.
157
+ *
158
+ * Mutates `polity.treasury_cu`, `state.cumulativeYield_cu`, and `deposit.richness_Q`.
159
+ */
160
+ export function stepExtraction(deposit, state, polity, elapsedDays) {
161
+ const daily = computeDailyYield(deposit, state, polity.techEra);
162
+ const yield_cu = daily * elapsedDays;
163
+ polity.treasury_cu += yield_cu;
164
+ state.cumulativeYield_cu += yield_cu;
165
+ depleteDeposit(deposit, yield_cu);
166
+ const exhausted = deposit.richness_Q <= DEPLETION_EXHAUSTED_Q;
167
+ return { yield_cu, richness_Q: deposit.richness_Q, exhausted };
168
+ }
169
+ // ── Reporting ─────────────────────────────────────────────────────────────────
170
+ /**
171
+ * Estimate daily bonus income from resource extraction across multiple deposits.
172
+ * Useful for treasury planning alongside Phase-92 tax revenue.
173
+ */
174
+ export function computeTotalDailyResourceIncome(deposits, states, techEra) {
175
+ let total = 0;
176
+ for (const deposit of deposits) {
177
+ const state = states.get(deposit.depositId);
178
+ if (!state)
179
+ continue;
180
+ total += computeDailyYield(deposit, state, techEra);
181
+ }
182
+ return total;
183
+ }
184
+ /**
185
+ * Return true if this resource type provides a military bonus.
186
+ */
187
+ export function hasMilitaryBonus(type) {
188
+ return MILITARY_BONUS_RESOURCES.has(type);
189
+ }
190
+ /**
191
+ * Return true if this resource type provides a construction bonus.
192
+ */
193
+ export function hasConstructionBonus(type) {
194
+ return CONSTRUCTION_BONUS_RESOURCES.has(type);
195
+ }
196
+ /**
197
+ * Return true if this resource type provides a mobility bonus.
198
+ */
199
+ export function hasMobilityBonus(type) {
200
+ return MOBILITY_BONUS_RESOURCES.has(type);
201
+ }
202
+ /**
203
+ * Estimate how many days until the deposit is exhausted at the current
204
+ * extraction rate. Returns `Infinity` if no workers or already exhausted.
205
+ */
206
+ export function estimateDaysToExhaustion(deposit, state, techEra) {
207
+ if (deposit.richness_Q <= DEPLETION_EXHAUSTED_Q)
208
+ return 0;
209
+ const daily = computeDailyYield(deposit, state, techEra);
210
+ if (daily <= 0)
211
+ return Infinity;
212
+ // richness that needs to be drained = richness_Q - DEPLETION_EXHAUSTED_Q
213
+ // drain per day = daily × DEPLETION_RATE_PER_1000_CU / 1000
214
+ const drainPerDay = daily * DEPLETION_RATE_PER_1000_CU / 1000;
215
+ if (drainPerDay <= 0)
216
+ return Infinity;
217
+ const remainingRichness = deposit.richness_Q - DEPLETION_EXHAUSTED_Q;
218
+ return Math.ceil(remainingRichness / drainPerDay);
219
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",
@@ -138,6 +138,14 @@
138
138
  "./military-campaign": {
139
139
  "import": "./dist/src/military-campaign.js",
140
140
  "types": "./dist/src/military-campaign.d.ts"
141
+ },
142
+ "./governance": {
143
+ "import": "./dist/src/governance.js",
144
+ "types": "./dist/src/governance.d.ts"
145
+ },
146
+ "./resources": {
147
+ "import": "./dist/src/resources.js",
148
+ "types": "./dist/src/resources.d.ts"
141
149
  }
142
150
  },
143
151
  "files": [
@@ -158,7 +166,7 @@
158
166
  },
159
167
  "repository": {
160
168
  "type": "git",
161
- "url": "https://github.com/its-not-rocket-science/ananke.git"
169
+ "url": "git+https://github.com/its-not-rocket-science/ananke.git"
162
170
  },
163
171
  "keywords": [
164
172
  "simulation",