@its-not-rocket-science/ananke 0.1.17 → 0.1.22

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.
@@ -0,0 +1,197 @@
1
+ // src/succession.ts — Phase 77: Dynasty & Succession
2
+ //
3
+ // Resolves inheritance of political leadership when a ruler dies.
4
+ // Integrates Phase 76 (Kinship) for candidate discovery and Phase 75 (Renown)
5
+ // for claim-strength weighting. No kernel changes; no new Entity fields.
6
+ //
7
+ // Succession rules:
8
+ // primogeniture — first-born child (lowest entityId as proxy for birth order)
9
+ // renown_based — candidate with highest `claimStrength_Q` (renown + inherited)
10
+ // election — renown-weighted deterministic selection via eventSeed
11
+ //
12
+ // Stability impact:
13
+ // Direct heir (degree 1) → ±0 base impact
14
+ // Distant heir (degree 2+) → stability penalty per extra degree
15
+ // No heir found → large stability hit
16
+ // Contested succession (top-2 candidates within q(0.10)) → additional penalty
17
+ import { computeInheritedRenown, MAX_KINSHIP_DEPTH, } from "./kinship.js";
18
+ import { getRenownRecord } from "./renown.js";
19
+ import { eventSeed } from "./sim/seeds.js";
20
+ import { q, SCALE, clampQ, mulDiv } from "./units.js";
21
+ // ── Constants ─────────────────────────────────────────────────────────────────
22
+ /** Weight of own renown vs. inherited renown when computing claim strength. */
23
+ export const CLAIM_OWN_RENOWN_WEIGHT_Q = q(0.70);
24
+ export const CLAIM_INHERITED_RENOWN_WEIGHT_Q = q(0.30);
25
+ /** Stability penalty per extra degree of kinship beyond 1 (direct child/parent). */
26
+ export const STABILITY_DISTANT_HEIR_Q = q(0.05);
27
+ /** Stability penalty when no heir is found. */
28
+ export const STABILITY_NO_HEIR_Q = q(0.20);
29
+ /** Additional penalty when the top two candidates are within this band. */
30
+ export const CONTESTED_THRESHOLD_Q = q(0.10);
31
+ export const STABILITY_CONTESTED_Q = q(0.05);
32
+ /** Stability bonus when a direct child inherits with no contest. */
33
+ export const STABILITY_CLEAN_SUCCESSION_Q = q(0.03);
34
+ // ── Candidate discovery ───────────────────────────────────────────────────────
35
+ /**
36
+ * Find all kin of `deceasedId` up to `maxDegree` and compute their claim strength.
37
+ * Candidates are sorted by claimStrength_Q descending, then kinshipDegree ascending.
38
+ */
39
+ export function findSuccessionCandidates(lineage, deceasedId, renownRegistry, maxDegree = MAX_KINSHIP_DEPTH) {
40
+ // BFS over the family graph to collect all kin within maxDegree
41
+ const visited = new Set([deceasedId]);
42
+ const queue = [];
43
+ // Seed with immediate family
44
+ const deceasedNode = lineage.nodes.get(deceasedId);
45
+ if (!deceasedNode)
46
+ return [];
47
+ const seeds = [
48
+ ...deceasedNode.childIds,
49
+ ...deceasedNode.parentIds,
50
+ ...deceasedNode.partnerIds,
51
+ ];
52
+ for (const id of seeds) {
53
+ if (!visited.has(id)) {
54
+ visited.add(id);
55
+ queue.push({ id, degree: 1 });
56
+ }
57
+ }
58
+ const candidates = [];
59
+ while (queue.length > 0) {
60
+ const item = queue.shift();
61
+ if (item.degree > maxDegree)
62
+ continue;
63
+ // Compute claim
64
+ const record = getRenownRecord(renownRegistry, item.id);
65
+ const renown_Q = record.renown_Q;
66
+ const inheritedRenown = computeInheritedRenown(lineage, item.id, renownRegistry, 3);
67
+ candidates.push({
68
+ entityId: item.id,
69
+ kinshipDegree: item.degree,
70
+ renown_Q,
71
+ inheritedRenown_Q: inheritedRenown,
72
+ claimStrength_Q: 0, // filled in per-rule below
73
+ });
74
+ // Expand neighbours
75
+ const node = lineage.nodes.get(item.id);
76
+ if (!node)
77
+ continue;
78
+ for (const nbr of [...node.childIds, ...node.parentIds, ...node.partnerIds]) {
79
+ if (!visited.has(nbr)) {
80
+ visited.add(nbr);
81
+ queue.push({ id: nbr, degree: item.degree + 1 });
82
+ }
83
+ }
84
+ }
85
+ return candidates;
86
+ }
87
+ // ── Claim strength computation ─────────────────────────────────────────────────
88
+ /** Compute `claimStrength_Q` for a candidate under the given rule. */
89
+ function computeClaimStrength(candidate, rule, firstBornId) {
90
+ switch (rule) {
91
+ case "primogeniture":
92
+ // First-born (lowest entityId among children at degree 1) gets full claim;
93
+ // all others get proportionally less based on distance
94
+ if (candidate.entityId === firstBornId)
95
+ return SCALE.Q;
96
+ // Other children score by closeness only
97
+ return clampQ(SCALE.Q - candidate.kinshipDegree * STABILITY_DISTANT_HEIR_Q, 0, SCALE.Q);
98
+ case "renown_based":
99
+ case "election": {
100
+ // Weighted combination of own renown and inherited renown
101
+ const ownPart = mulDiv(candidate.renown_Q, CLAIM_OWN_RENOWN_WEIGHT_Q, SCALE.Q);
102
+ const inhPart = mulDiv(candidate.inheritedRenown_Q, CLAIM_INHERITED_RENOWN_WEIGHT_Q, SCALE.Q);
103
+ return clampQ(ownPart + inhPart, 0, SCALE.Q);
104
+ }
105
+ }
106
+ }
107
+ // ── Succession resolution ─────────────────────────────────────────────────────
108
+ /**
109
+ * Resolve succession after `deceasedId` dies.
110
+ *
111
+ * @param lineage Kinship registry (Phase 76).
112
+ * @param deceasedId The entity whose position must be inherited.
113
+ * @param renownRegistry Renown registry (Phase 75).
114
+ * @param rule Succession rule to apply.
115
+ * @param worldSeed For deterministic election roll.
116
+ * @param tick Current simulation tick.
117
+ */
118
+ export function resolveSuccession(lineage, deceasedId, renownRegistry, rule, worldSeed, tick) {
119
+ const maxDegree = rule.maxDegree ?? MAX_KINSHIP_DEPTH;
120
+ const ruleType = rule.type;
121
+ // Find candidates
122
+ const raw = findSuccessionCandidates(lineage, deceasedId, renownRegistry, maxDegree);
123
+ if (raw.length === 0) {
124
+ return {
125
+ heirId: null,
126
+ candidates: [],
127
+ rule: ruleType,
128
+ stabilityImpact_Q: -STABILITY_NO_HEIR_Q,
129
+ };
130
+ }
131
+ // Identify first-born (lowest entityId among direct children)
132
+ const directChildren = raw.filter(c => c.kinshipDegree === 1 &&
133
+ lineage.nodes.get(deceasedId)?.childIds.includes(c.entityId));
134
+ const firstBornId = directChildren.length > 0
135
+ ? Math.min(...directChildren.map(c => c.entityId))
136
+ : null;
137
+ // Fill claim strength
138
+ for (const c of raw) {
139
+ c.claimStrength_Q = computeClaimStrength(c, ruleType, firstBornId);
140
+ }
141
+ // Sort: claim strength desc, then kinshipDegree asc (closer kin breaks ties)
142
+ raw.sort((a, b) => b.claimStrength_Q - a.claimStrength_Q || a.kinshipDegree - b.kinshipDegree);
143
+ // Select heir
144
+ let heirId;
145
+ if (ruleType === "election" && raw.length > 1) {
146
+ // Renown-weighted lottery: for each candidate, roll eventSeed and weight by claimStrength
147
+ const totalClaim = raw.reduce((s, c) => s + c.claimStrength_Q, 0);
148
+ const roll = eventSeed(worldSeed, tick, deceasedId, 0, 77) % Math.max(totalClaim, 1);
149
+ let cumulative = 0;
150
+ let elected = raw[0].entityId;
151
+ for (const c of raw) {
152
+ cumulative += c.claimStrength_Q;
153
+ if (roll < cumulative) {
154
+ elected = c.entityId;
155
+ break;
156
+ }
157
+ }
158
+ heirId = elected;
159
+ }
160
+ else {
161
+ heirId = raw[0].entityId;
162
+ }
163
+ const winner = raw.find(c => c.entityId === heirId);
164
+ // Compute stability impact
165
+ let stability = 0;
166
+ // Bonus for clean direct succession
167
+ if (winner.kinshipDegree === 1 && raw.length === 1) {
168
+ stability += STABILITY_CLEAN_SUCCESSION_Q;
169
+ }
170
+ // Penalty for distant heir
171
+ if (winner.kinshipDegree > 1) {
172
+ stability -= (winner.kinshipDegree - 1) * STABILITY_DISTANT_HEIR_Q;
173
+ }
174
+ // Penalty for contested succession
175
+ if (raw.length >= 2) {
176
+ const gap = raw[0].claimStrength_Q - raw[1].claimStrength_Q;
177
+ if (gap < CONTESTED_THRESHOLD_Q) {
178
+ stability -= STABILITY_CONTESTED_Q;
179
+ }
180
+ }
181
+ return {
182
+ heirId,
183
+ candidates: raw,
184
+ rule: ruleType,
185
+ stabilityImpact_Q: clampQ(stability, -SCALE.Q, SCALE.Q),
186
+ };
187
+ }
188
+ // ── Polity integration ────────────────────────────────────────────────────────
189
+ /**
190
+ * Apply a succession result to a polity.
191
+ * Adjusts `stabilityQ` by `result.stabilityImpact_Q`.
192
+ * Does NOT change the ruler field (Polity has no rulerId); callers update faction
193
+ * leadership separately if needed.
194
+ */
195
+ export function applySuccessionToPolity(polity, result) {
196
+ polity.stabilityQ = clampQ(polity.stabilityQ + result.stabilityImpact_Q, 0, SCALE.Q);
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@its-not-rocket-science/ananke",
3
- "version": "0.1.17",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "description": "Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)",
6
6
  "license": "MIT",
@@ -58,6 +58,22 @@
58
58
  "./wasm-kernel": {
59
59
  "import": "./dist/src/wasm-kernel.js",
60
60
  "types": "./dist/src/wasm-kernel.d.ts"
61
+ },
62
+ "./narrative-prose": {
63
+ "import": "./dist/src/narrative-prose.js",
64
+ "types": "./dist/src/narrative-prose.d.ts"
65
+ },
66
+ "./renown": {
67
+ "import": "./dist/src/renown.js",
68
+ "types": "./dist/src/renown.d.ts"
69
+ },
70
+ "./kinship": {
71
+ "import": "./dist/src/kinship.js",
72
+ "types": "./dist/src/kinship.d.ts"
73
+ },
74
+ "./succession": {
75
+ "import": "./dist/src/succession.js",
76
+ "types": "./dist/src/succession.d.ts"
61
77
  }
62
78
  },
63
79
  "files": [
@@ -121,6 +137,7 @@
121
137
  "world-server": "node dist/tools/world-server.js",
122
138
  "persistent-world": "node dist/tools/persistent-world.js",
123
139
  "replication-server": "node dist/tools/replication-server.js",
140
+ "agent-server": "node dist/tools/agent-server.js",
124
141
  "benchmark-check": "node dist/tools/benchmark-check.js",
125
142
  "benchmark-check:strict": "node dist/tools/benchmark-check.js --threshold=0.10",
126
143
  "benchmark-check:update": "node dist/tools/benchmark-check.js --update-baseline",