@its-not-rocket-science/ananke 0.1.34 → 0.1.35
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 +16 -0
- package/dist/src/unrest.d.ts +99 -0
- package/dist/src/unrest.js +147 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.1.35] — 2026-03-26
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Phase 90 · Civil Unrest & Rebellion** (`src/unrest.ts`)
|
|
14
|
+
- `UnrestFactors { faminePressure_Q?, epidemicPressure_Q?, heresyRisk_Q?, weakestBond_Q? }` — optional pressure inputs from Phases 85/87/88/79.
|
|
15
|
+
- `computeUnrestLevel(polity, factors?)` → Q: weighted composite of morale deficit (×q(0.30)), stability deficit (×q(0.25)), famine (×q(0.20)), epidemic (×q(0.10)), heresy (×q(0.10)), feudal bond deficit (×q(0.05)).
|
|
16
|
+
- `UNREST_ACTION_THRESHOLD_Q = q(0.30)` — excess above this drains morale/stability.
|
|
17
|
+
- `REBELLION_THRESHOLD_Q = q(0.65)` — above this `rebellionRisk` flag is set.
|
|
18
|
+
- `stepUnrest(polity, unrestLevel_Q, elapsedDays)` → `UnrestStepResult`: drains morale at `excess × UNREST_MORALE_DRAIN_Q = q(0.005)` per day, stability at `q(0.003)` per day; mutates polity in place; floor at 0.
|
|
19
|
+
- `resolveRebellion(polity, worldSeed, tick)` → `RebellionResult`: deterministic via `eventSeed`; outcomes `"quelled" | "uprising" | "civil_war"` weighted by polity `militaryStrength_Q` vs. unrest roll; each outcome applies morale/stability penalties and treasury raid (`REBELLION_TREASURY_RAID_Q = q(0.15)`; civil war = 2×).
|
|
20
|
+
- Added `./unrest` subpath export to `package.json`.
|
|
21
|
+
- 35 new tests; 4,722 total. Coverage maintained above all thresholds.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
9
25
|
## [0.1.34] — 2026-03-26
|
|
10
26
|
|
|
11
27
|
### Added
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Q } from "./units.js";
|
|
2
|
+
import type { Polity } from "./polity.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pressure signals fed into `computeUnrestLevel`.
|
|
5
|
+
* All fields are Q fractions [0, SCALE.Q]; omit any that are not applicable.
|
|
6
|
+
*/
|
|
7
|
+
export interface UnrestFactors {
|
|
8
|
+
/** Phase-87 famine push pressure. */
|
|
9
|
+
faminePressure_Q?: Q;
|
|
10
|
+
/** Phase-88 epidemic flight pressure. */
|
|
11
|
+
epidemicPressure_Q?: Q;
|
|
12
|
+
/** Phase-85 heresy risk. */
|
|
13
|
+
heresyRisk_Q?: Q;
|
|
14
|
+
/**
|
|
15
|
+
* Weakest feudal bond strength [0, SCALE.Q] from Phase-79.
|
|
16
|
+
* Low value → high feudal unrest contribution.
|
|
17
|
+
*/
|
|
18
|
+
weakestBond_Q?: Q;
|
|
19
|
+
}
|
|
20
|
+
/** Possible outcomes of a rebellion resolution. */
|
|
21
|
+
export type RebellionOutcome = "quelled" | "uprising" | "civil_war";
|
|
22
|
+
/** Result returned by `resolveRebellion`. */
|
|
23
|
+
export interface RebellionResult {
|
|
24
|
+
outcome: RebellionOutcome;
|
|
25
|
+
/** Morale penalty applied to the polity (always ≤ 0). */
|
|
26
|
+
moraleHit_Q: number;
|
|
27
|
+
/** Stability penalty applied to the polity (always ≤ 0). */
|
|
28
|
+
stabilityHit_Q: number;
|
|
29
|
+
/** Treasury plundered by rebels [cost units]. */
|
|
30
|
+
treasuryLoss: number;
|
|
31
|
+
}
|
|
32
|
+
/** Outcome of `stepUnrest` — the changes applied this step. */
|
|
33
|
+
export interface UnrestStepResult {
|
|
34
|
+
unrestLevel_Q: Q;
|
|
35
|
+
moraleDecay_Q: number;
|
|
36
|
+
stabilityDecay_Q: number;
|
|
37
|
+
/** Whether rebellion threshold was crossed (host should call resolveRebellion). */
|
|
38
|
+
rebellionRisk: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Weights applied to each pressure source in `computeUnrestLevel`. */
|
|
41
|
+
export declare const UNREST_MORALE_WEIGHT_Q: Q;
|
|
42
|
+
export declare const UNREST_STABILITY_WEIGHT_Q: Q;
|
|
43
|
+
export declare const UNREST_FAMINE_WEIGHT_Q: Q;
|
|
44
|
+
export declare const UNREST_EPIDEMIC_WEIGHT_Q: Q;
|
|
45
|
+
export declare const UNREST_HERESY_WEIGHT_Q: Q;
|
|
46
|
+
export declare const UNREST_FEUDAL_WEIGHT_Q: Q;
|
|
47
|
+
/** Unrest above this threshold → morale and stability begin draining. */
|
|
48
|
+
export declare const UNREST_ACTION_THRESHOLD_Q: Q;
|
|
49
|
+
/** Unrest above this threshold → rebellion risk flag raised. */
|
|
50
|
+
export declare const REBELLION_THRESHOLD_Q: Q;
|
|
51
|
+
/** Maximum daily morale drain from sustained unrest [Q/day]. */
|
|
52
|
+
export declare const UNREST_MORALE_DRAIN_Q: Q;
|
|
53
|
+
/** Maximum daily stability drain from sustained unrest [Q/day]. */
|
|
54
|
+
export declare const UNREST_STABILITY_DRAIN_Q: Q;
|
|
55
|
+
/** Fraction of treasury rebels plunder during an uprising or civil war. */
|
|
56
|
+
export declare const REBELLION_TREASURY_RAID_Q: Q;
|
|
57
|
+
/**
|
|
58
|
+
* Compute the composite unrest level [0, SCALE.Q] for a polity.
|
|
59
|
+
*
|
|
60
|
+
* Unrest is the weighted sum of:
|
|
61
|
+
* - Low morale (`(SCALE.Q - moraleQ) × MORALE_WEIGHT`)
|
|
62
|
+
* - Low stability (`(SCALE.Q - stabilityQ) × STABILITY_WEIGHT`)
|
|
63
|
+
* - Famine pressure × FAMINE_WEIGHT
|
|
64
|
+
* - Epidemic pressure × EPIDEMIC_WEIGHT
|
|
65
|
+
* - Heresy risk × HERESY_WEIGHT
|
|
66
|
+
* - Feudal deficit × FEUDAL_WEIGHT (`SCALE.Q − weakestBond_Q`)
|
|
67
|
+
*
|
|
68
|
+
* All inputs are optional; omitted factors contribute zero.
|
|
69
|
+
*/
|
|
70
|
+
export declare function computeUnrestLevel(polity: Polity, factors?: UnrestFactors): Q;
|
|
71
|
+
/**
|
|
72
|
+
* Apply unrest consequences to a polity for `elapsedDays` days.
|
|
73
|
+
*
|
|
74
|
+
* When `unrestLevel_Q > UNREST_ACTION_THRESHOLD_Q`:
|
|
75
|
+
* - Drains morale at rate `(unrest − threshold) × MORALE_DRAIN_Q / SCALE.Q` per day.
|
|
76
|
+
* - Drains stability at a lower rate.
|
|
77
|
+
*
|
|
78
|
+
* Mutates `polity.moraleQ` and `polity.stabilityQ` in place.
|
|
79
|
+
* Returns the step result for host inspection.
|
|
80
|
+
*/
|
|
81
|
+
export declare function stepUnrest(polity: Polity, unrestLevel_Q: Q, elapsedDays: number): UnrestStepResult;
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a rebellion event deterministically.
|
|
84
|
+
*
|
|
85
|
+
* Outcomes:
|
|
86
|
+
* - `"quelled"`: rebels dispersed — morale/treasury hit only.
|
|
87
|
+
* - `"uprising"`: significant unrest — larger morale/stability hit + treasury raid.
|
|
88
|
+
* - `"civil_war"`: polity fractures — severe penalties across all stats.
|
|
89
|
+
*
|
|
90
|
+
* Outcome probability is weighted by unrest level vs. military strength:
|
|
91
|
+
* - High military strength + moderate unrest → likely `"quelled"`
|
|
92
|
+
* - Low military + high unrest → risk of `"civil_war"`
|
|
93
|
+
*
|
|
94
|
+
* Mutates polity morale, stability, and treasury.
|
|
95
|
+
*
|
|
96
|
+
* @param worldSeed World seed for deterministic resolution.
|
|
97
|
+
* @param tick Current simulation tick.
|
|
98
|
+
*/
|
|
99
|
+
export declare function resolveRebellion(polity: Polity, worldSeed: number, tick: number): RebellionResult;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// src/unrest.ts — Phase 90: Civil Unrest & Rebellion
|
|
2
|
+
//
|
|
3
|
+
// Aggregates pressure signals from existing systems into a composite unrest
|
|
4
|
+
// level, drains polity morale and stability under sustained pressure, and
|
|
5
|
+
// resolves rebellion events deterministically.
|
|
6
|
+
//
|
|
7
|
+
// Design:
|
|
8
|
+
// - Pure data layer — no Entity fields, no kernel changes.
|
|
9
|
+
// - `computeUnrestLevel` is a pure aggregator: callers pass pre-computed
|
|
10
|
+
// pressure values from Phase-85 (heresy), Phase-87 (famine), Phase-88
|
|
11
|
+
// (epidemic), Phase-79 (weakest feudal bond), etc.
|
|
12
|
+
// - `stepUnrest` mutates polity.moraleQ and polity.stabilityQ when unrest
|
|
13
|
+
// exceeds thresholds.
|
|
14
|
+
// - `resolveRebellion` uses eventSeed for full determinism and replay safety.
|
|
15
|
+
//
|
|
16
|
+
// Integration:
|
|
17
|
+
// Phase 61 (Polity): mutates moraleQ / stabilityQ; reads militaryStrength_Q.
|
|
18
|
+
// Phase 79 (Feudal): weakestBond_Q input.
|
|
19
|
+
// Phase 85 (Faith): heresyRisk_Q input.
|
|
20
|
+
// Phase 87 (Granary): faminePressure_Q input from computeFamineMigrationPush.
|
|
21
|
+
// Phase 88 (Epidemic): epidemicPressure_Q input from computeEpidemicMigrationPush.
|
|
22
|
+
import { q, SCALE, clampQ, mulDiv } from "./units.js";
|
|
23
|
+
import { eventSeed, hashString } from "./sim/seeds.js";
|
|
24
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
25
|
+
/** Weights applied to each pressure source in `computeUnrestLevel`. */
|
|
26
|
+
export const UNREST_MORALE_WEIGHT_Q = q(0.30);
|
|
27
|
+
export const UNREST_STABILITY_WEIGHT_Q = q(0.25);
|
|
28
|
+
export const UNREST_FAMINE_WEIGHT_Q = q(0.20);
|
|
29
|
+
export const UNREST_EPIDEMIC_WEIGHT_Q = q(0.10);
|
|
30
|
+
export const UNREST_HERESY_WEIGHT_Q = q(0.10);
|
|
31
|
+
export const UNREST_FEUDAL_WEIGHT_Q = q(0.05);
|
|
32
|
+
/** Unrest above this threshold → morale and stability begin draining. */
|
|
33
|
+
export const UNREST_ACTION_THRESHOLD_Q = q(0.30);
|
|
34
|
+
/** Unrest above this threshold → rebellion risk flag raised. */
|
|
35
|
+
export const REBELLION_THRESHOLD_Q = q(0.65);
|
|
36
|
+
/** Maximum daily morale drain from sustained unrest [Q/day]. */
|
|
37
|
+
export const UNREST_MORALE_DRAIN_Q = q(0.005);
|
|
38
|
+
/** Maximum daily stability drain from sustained unrest [Q/day]. */
|
|
39
|
+
export const UNREST_STABILITY_DRAIN_Q = q(0.003);
|
|
40
|
+
/** Fraction of treasury rebels plunder during an uprising or civil war. */
|
|
41
|
+
export const REBELLION_TREASURY_RAID_Q = q(0.15);
|
|
42
|
+
// ── Unrest computation ────────────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Compute the composite unrest level [0, SCALE.Q] for a polity.
|
|
45
|
+
*
|
|
46
|
+
* Unrest is the weighted sum of:
|
|
47
|
+
* - Low morale (`(SCALE.Q - moraleQ) × MORALE_WEIGHT`)
|
|
48
|
+
* - Low stability (`(SCALE.Q - stabilityQ) × STABILITY_WEIGHT`)
|
|
49
|
+
* - Famine pressure × FAMINE_WEIGHT
|
|
50
|
+
* - Epidemic pressure × EPIDEMIC_WEIGHT
|
|
51
|
+
* - Heresy risk × HERESY_WEIGHT
|
|
52
|
+
* - Feudal deficit × FEUDAL_WEIGHT (`SCALE.Q − weakestBond_Q`)
|
|
53
|
+
*
|
|
54
|
+
* All inputs are optional; omitted factors contribute zero.
|
|
55
|
+
*/
|
|
56
|
+
export function computeUnrestLevel(polity, factors = {}) {
|
|
57
|
+
const moraleContrib = mulDiv(SCALE.Q - polity.moraleQ, UNREST_MORALE_WEIGHT_Q, SCALE.Q);
|
|
58
|
+
const stabilityContrib = mulDiv(SCALE.Q - polity.stabilityQ, UNREST_STABILITY_WEIGHT_Q, SCALE.Q);
|
|
59
|
+
const famineContrib = mulDiv(factors.faminePressure_Q ?? 0, UNREST_FAMINE_WEIGHT_Q, SCALE.Q);
|
|
60
|
+
const epidemicContrib = mulDiv(factors.epidemicPressure_Q ?? 0, UNREST_EPIDEMIC_WEIGHT_Q, SCALE.Q);
|
|
61
|
+
const heresyContrib = mulDiv(factors.heresyRisk_Q ?? 0, UNREST_HERESY_WEIGHT_Q, SCALE.Q);
|
|
62
|
+
const feudalDeficit = factors.weakestBond_Q != null
|
|
63
|
+
? clampQ(SCALE.Q - factors.weakestBond_Q, 0, SCALE.Q)
|
|
64
|
+
: 0;
|
|
65
|
+
const feudalContrib = mulDiv(feudalDeficit, UNREST_FEUDAL_WEIGHT_Q, SCALE.Q);
|
|
66
|
+
const total = moraleContrib + stabilityContrib + famineContrib +
|
|
67
|
+
epidemicContrib + heresyContrib + feudalContrib;
|
|
68
|
+
return clampQ(total, 0, SCALE.Q);
|
|
69
|
+
}
|
|
70
|
+
// ── Unrest step ───────────────────────────────────────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Apply unrest consequences to a polity for `elapsedDays` days.
|
|
73
|
+
*
|
|
74
|
+
* When `unrestLevel_Q > UNREST_ACTION_THRESHOLD_Q`:
|
|
75
|
+
* - Drains morale at rate `(unrest − threshold) × MORALE_DRAIN_Q / SCALE.Q` per day.
|
|
76
|
+
* - Drains stability at a lower rate.
|
|
77
|
+
*
|
|
78
|
+
* Mutates `polity.moraleQ` and `polity.stabilityQ` in place.
|
|
79
|
+
* Returns the step result for host inspection.
|
|
80
|
+
*/
|
|
81
|
+
export function stepUnrest(polity, unrestLevel_Q, elapsedDays) {
|
|
82
|
+
const excess = clampQ(unrestLevel_Q - UNREST_ACTION_THRESHOLD_Q, 0, SCALE.Q);
|
|
83
|
+
const moraleDecayPerDay = mulDiv(excess, UNREST_MORALE_DRAIN_Q, SCALE.Q);
|
|
84
|
+
const stabilityDecayPerDay = mulDiv(excess, UNREST_STABILITY_DRAIN_Q, SCALE.Q);
|
|
85
|
+
const totalMoraleDecay = Math.round(moraleDecayPerDay * elapsedDays);
|
|
86
|
+
const totalStabilityDecay = Math.round(stabilityDecayPerDay * elapsedDays);
|
|
87
|
+
polity.moraleQ = clampQ(polity.moraleQ - totalMoraleDecay, 0, SCALE.Q);
|
|
88
|
+
polity.stabilityQ = clampQ(polity.stabilityQ - totalStabilityDecay, 0, SCALE.Q);
|
|
89
|
+
return {
|
|
90
|
+
unrestLevel_Q,
|
|
91
|
+
moraleDecay_Q: totalMoraleDecay,
|
|
92
|
+
stabilityDecay_Q: totalStabilityDecay,
|
|
93
|
+
rebellionRisk: unrestLevel_Q > REBELLION_THRESHOLD_Q,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// ── Rebellion resolution ──────────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a rebellion event deterministically.
|
|
99
|
+
*
|
|
100
|
+
* Outcomes:
|
|
101
|
+
* - `"quelled"`: rebels dispersed — morale/treasury hit only.
|
|
102
|
+
* - `"uprising"`: significant unrest — larger morale/stability hit + treasury raid.
|
|
103
|
+
* - `"civil_war"`: polity fractures — severe penalties across all stats.
|
|
104
|
+
*
|
|
105
|
+
* Outcome probability is weighted by unrest level vs. military strength:
|
|
106
|
+
* - High military strength + moderate unrest → likely `"quelled"`
|
|
107
|
+
* - Low military + high unrest → risk of `"civil_war"`
|
|
108
|
+
*
|
|
109
|
+
* Mutates polity morale, stability, and treasury.
|
|
110
|
+
*
|
|
111
|
+
* @param worldSeed World seed for deterministic resolution.
|
|
112
|
+
* @param tick Current simulation tick.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveRebellion(polity, worldSeed, tick) {
|
|
115
|
+
const polityHash = hashString(polity.id);
|
|
116
|
+
const seed = eventSeed(worldSeed, tick, polityHash, 0, 9001); // salt: rebellion
|
|
117
|
+
const roll = seed % SCALE.Q; // [0, SCALE.Q)
|
|
118
|
+
// Suppression capacity = military strength
|
|
119
|
+
const suppressCap = polity.militaryStrength_Q;
|
|
120
|
+
// Civil war threshold = low suppression + any roll in top quarter
|
|
121
|
+
const civilWarThresh = clampQ(SCALE.Q - suppressCap, 0, SCALE.Q);
|
|
122
|
+
const uprisingThresh = Math.round(civilWarThresh * 0.6);
|
|
123
|
+
let outcome;
|
|
124
|
+
if (roll >= civilWarThresh) {
|
|
125
|
+
outcome = "quelled";
|
|
126
|
+
}
|
|
127
|
+
else if (roll >= uprisingThresh) {
|
|
128
|
+
outcome = "uprising";
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
outcome = "civil_war";
|
|
132
|
+
}
|
|
133
|
+
const moraleHit = outcome === "quelled" ? -q(0.05)
|
|
134
|
+
: outcome === "uprising" ? -q(0.15)
|
|
135
|
+
: -q(0.30);
|
|
136
|
+
const stabilityHit = outcome === "quelled" ? -q(0.03)
|
|
137
|
+
: outcome === "uprising" ? -q(0.10)
|
|
138
|
+
: -q(0.25);
|
|
139
|
+
const treasuryRaid = outcome === "quelled"
|
|
140
|
+
? 0
|
|
141
|
+
: Math.floor(mulDiv(polity.treasury_cu, REBELLION_TREASURY_RAID_Q, SCALE.Q)
|
|
142
|
+
* (outcome === "civil_war" ? 2 : 1));
|
|
143
|
+
polity.moraleQ = clampQ(polity.moraleQ + moraleHit, 0, SCALE.Q);
|
|
144
|
+
polity.stabilityQ = clampQ(polity.stabilityQ + stabilityHit, 0, SCALE.Q);
|
|
145
|
+
polity.treasury_cu = Math.max(0, polity.treasury_cu - treasuryRaid);
|
|
146
|
+
return { outcome, moraleHit_Q: moraleHit, stabilityHit_Q: stabilityHit, treasuryLoss: treasuryRaid };
|
|
147
|
+
}
|
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.35",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -122,6 +122,10 @@
|
|
|
122
122
|
"./infrastructure": {
|
|
123
123
|
"import": "./dist/src/infrastructure.js",
|
|
124
124
|
"types": "./dist/src/infrastructure.d.ts"
|
|
125
|
+
},
|
|
126
|
+
"./unrest": {
|
|
127
|
+
"import": "./dist/src/unrest.js",
|
|
128
|
+
"types": "./dist/src/unrest.d.ts"
|
|
125
129
|
}
|
|
126
130
|
},
|
|
127
131
|
"files": [
|