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

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,27 @@ Versioning follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.1.39] — 2026-03-26
10
+
11
+ ### Added
12
+
13
+ - **Phase 94 · Laws & Governance Codes** (`src/governance.ts`)
14
+ - `GovernanceType`: `"tribal" | "monarchy" | "oligarchy" | "republic" | "empire" | "theocracy"`.
15
+ - `GovernanceModifiers { taxEfficiencyMul_Q, mobilizationMax_Q, researchBonus, unrestMitigation_Q, stabilityIncrement_Q }` — aggregate modifier bundle applied to downstream phases.
16
+ - `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).
17
+ - `LawCode { lawId, name, taxBonus_Q, researchBonus, mobilizationBonus_Q, unrestBonus_Q, stabilityCostPerDay_Q }` — discrete enacted policies.
18
+ - 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).
19
+ - `GovernanceState { polityId, governanceType, activeLawIds, changeCooldown }`.
20
+ - `computeGovernanceModifiers(state, lawRegistry?)` — stacks law bonuses on governance baseline; clamps all outputs.
21
+ - `enactLaw(state, lawId)` / `repealLaw(state, lawId)` — add/remove laws; enforces `MAX_ACTIVE_LAWS = 5`.
22
+ - `changeGovernance(polity, state, newType)` — hits `polity.stabilityQ` by q(0.20); sets 365-day cooldown; no-op on same type or during cooldown.
23
+ - `stepGovernanceCooldown(state, elapsedDays)` — ticks down cooldown.
24
+ - `stepGovernanceStability(polity, state, elapsedDays, lawRegistry?)` — applies net `stabilityIncrement_Q` per day to `polity.stabilityQ`; no-op when law costs cancel the baseline.
25
+ - Added `./governance` subpath export to `package.json`.
26
+ - 48 new tests; 4,932 total. 100% statement/branch/function/line coverage. All thresholds met.
27
+
28
+ ---
29
+
9
30
  ## [0.1.38] — 2026-03-26
10
31
 
11
32
  ### 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
+ }
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.39",
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,10 @@
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"
141
145
  }
142
146
  },
143
147
  "files": [
@@ -158,7 +162,7 @@
158
162
  },
159
163
  "repository": {
160
164
  "type": "git",
161
- "url": "https://github.com/its-not-rocket-science/ananke.git"
165
+ "url": "git+https://github.com/its-not-rocket-science/ananke.git"
162
166
  },
163
167
  "keywords": [
164
168
  "simulation",