@buley/hexgrid-3d 3.0.1 → 3.2.0

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.
Files changed (209) hide show
  1. package/dist/HexGridEnhanced.d.ts +15 -0
  2. package/dist/HexGridEnhanced.d.ts.map +1 -0
  3. package/dist/HexGridEnhanced.js +1 -0
  4. package/dist/Snapshot.d.ts +594 -0
  5. package/dist/Snapshot.d.ts.map +1 -0
  6. package/dist/Snapshot.js +757 -0
  7. package/dist/adapters/DashAdapter.d.ts +18 -0
  8. package/dist/adapters/DashAdapter.d.ts.map +1 -0
  9. package/dist/adapters/DashAdapter.js +42 -0
  10. package/dist/adapters.d.ts +53 -0
  11. package/dist/adapters.d.ts.map +1 -0
  12. package/dist/adapters.js +14 -0
  13. package/dist/algorithms/AdvancedStatistics.d.ts +52 -0
  14. package/dist/algorithms/AdvancedStatistics.d.ts.map +1 -0
  15. package/dist/algorithms/AdvancedStatistics.js +307 -0
  16. package/dist/algorithms/BayesianStatistics.d.ts +86 -0
  17. package/dist/algorithms/BayesianStatistics.d.ts.map +1 -0
  18. package/dist/algorithms/BayesianStatistics.js +263 -0
  19. package/dist/algorithms/FlowField.d.ts +55 -0
  20. package/dist/algorithms/FlowField.d.ts.map +1 -0
  21. package/dist/algorithms/FlowField.js +80 -0
  22. package/dist/algorithms/FlowField3D.d.ts +166 -0
  23. package/dist/algorithms/FlowField3D.d.ts.map +1 -0
  24. package/dist/algorithms/FlowField3D.js +327 -0
  25. package/dist/algorithms/FluidEngineFactory.d.ts +15 -0
  26. package/dist/algorithms/FluidEngineFactory.d.ts.map +1 -0
  27. package/dist/algorithms/FluidEngineFactory.js +41 -0
  28. package/dist/algorithms/FluidSimulation.d.ts +41 -0
  29. package/dist/algorithms/FluidSimulation.d.ts.map +1 -0
  30. package/dist/algorithms/FluidSimulation.js +74 -0
  31. package/dist/algorithms/FluidSimulation3D.d.ts +137 -0
  32. package/dist/algorithms/FluidSimulation3D.d.ts.map +1 -0
  33. package/dist/algorithms/FluidSimulation3D.js +464 -0
  34. package/dist/algorithms/FluidSimulation3DGPU.d.ts +41 -0
  35. package/dist/algorithms/FluidSimulation3DGPU.d.ts.map +1 -0
  36. package/dist/algorithms/FluidSimulation3DGPU.js +328 -0
  37. package/dist/algorithms/FluidSimulationWebNN.d.ts +56 -0
  38. package/dist/algorithms/FluidSimulationWebNN.d.ts.map +1 -0
  39. package/dist/algorithms/FluidSimulationWebNN.js +84 -0
  40. package/dist/algorithms/GraphAlgorithms.d.ts +48 -0
  41. package/dist/algorithms/GraphAlgorithms.d.ts.map +1 -0
  42. package/dist/algorithms/GraphAlgorithms.js +122 -0
  43. package/dist/algorithms/OutlierDetection.d.ts +49 -0
  44. package/dist/algorithms/OutlierDetection.d.ts.map +1 -0
  45. package/dist/algorithms/OutlierDetection.js +284 -0
  46. package/dist/algorithms/ParticleSystem.d.ts +36 -0
  47. package/dist/algorithms/ParticleSystem.d.ts.map +1 -0
  48. package/dist/algorithms/ParticleSystem.js +59 -0
  49. package/dist/algorithms/ParticleSystem3D.d.ts +206 -0
  50. package/dist/algorithms/ParticleSystem3D.d.ts.map +1 -0
  51. package/dist/algorithms/ParticleSystem3D.js +371 -0
  52. package/dist/algorithms/index.d.ts +16 -0
  53. package/dist/algorithms/index.d.ts.map +1 -0
  54. package/{src/algorithms/index.ts → dist/algorithms/index.js} +0 -2
  55. package/dist/compat.d.ts +24 -0
  56. package/dist/compat.d.ts.map +1 -0
  57. package/dist/compat.js +88 -0
  58. package/dist/components/HexGrid.d.ts +5 -0
  59. package/dist/components/HexGrid.d.ts.map +1 -0
  60. package/dist/components/HexGrid.js +39 -0
  61. package/dist/components/NarrationOverlay.d.ts +16 -0
  62. package/dist/components/NarrationOverlay.d.ts.map +1 -0
  63. package/dist/components/NarrationOverlay.js +132 -0
  64. package/{src/components/index.ts → dist/components/index.d.ts} +1 -1
  65. package/dist/components/index.d.ts.map +1 -0
  66. package/dist/components/index.js +1 -0
  67. package/dist/features.d.ts +54 -0
  68. package/dist/features.d.ts.map +1 -0
  69. package/dist/features.js +74 -0
  70. package/dist/index.d.ts +12 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +17 -0
  73. package/dist/lib/narration.d.ts +12 -0
  74. package/dist/lib/narration.d.ts.map +1 -0
  75. package/dist/lib/narration.js +8 -0
  76. package/dist/lib/stats-tracker.d.ts +7 -0
  77. package/dist/lib/stats-tracker.d.ts.map +1 -0
  78. package/dist/lib/stats-tracker.js +22 -0
  79. package/dist/lib/theme-colors.d.ts +7 -0
  80. package/dist/lib/theme-colors.d.ts.map +1 -0
  81. package/dist/lib/theme-colors.js +10 -0
  82. package/dist/math/HexCoordinates.d.ts +140 -0
  83. package/dist/math/HexCoordinates.d.ts.map +1 -0
  84. package/dist/math/HexCoordinates.js +741 -0
  85. package/dist/math/Matrix4.d.ts +9 -0
  86. package/dist/math/Matrix4.d.ts.map +1 -0
  87. package/dist/math/Matrix4.js +19 -0
  88. package/dist/math/Quaternion.d.ts +11 -0
  89. package/dist/math/Quaternion.d.ts.map +1 -0
  90. package/dist/math/Quaternion.js +23 -0
  91. package/dist/math/SpatialIndex.d.ts +34 -0
  92. package/dist/math/SpatialIndex.d.ts.map +1 -0
  93. package/dist/math/SpatialIndex.js +75 -0
  94. package/dist/math/Vector3.d.ts +110 -0
  95. package/dist/math/Vector3.d.ts.map +1 -0
  96. package/dist/math/Vector3.js +426 -0
  97. package/dist/math/index.d.ts +11 -0
  98. package/dist/math/index.d.ts.map +1 -0
  99. package/{src/math/index.ts → dist/math/index.js} +0 -1
  100. package/dist/note-adapter.d.ts +44 -0
  101. package/dist/note-adapter.d.ts.map +1 -0
  102. package/dist/note-adapter.js +86 -0
  103. package/dist/ontology-adapter.d.ts +13 -0
  104. package/dist/ontology-adapter.d.ts.map +1 -0
  105. package/dist/ontology-adapter.js +65 -0
  106. package/dist/stores/index.d.ts +2 -0
  107. package/dist/stores/index.d.ts.map +1 -0
  108. package/dist/stores/uiStore.d.ts +18 -0
  109. package/dist/stores/uiStore.d.ts.map +1 -0
  110. package/dist/stores/uiStore.js +77 -0
  111. package/dist/types/index.d.ts +4 -0
  112. package/dist/types/index.d.ts.map +1 -0
  113. package/dist/types/index.js +1 -0
  114. package/dist/types.d.ts +126 -0
  115. package/dist/types.d.ts.map +1 -0
  116. package/dist/types.js +4 -0
  117. package/dist/utils/image-utils.d.ts +13 -0
  118. package/dist/utils/image-utils.d.ts.map +1 -0
  119. package/dist/utils/image-utils.js +23 -0
  120. package/dist/wasm/HexGridWasmWrapper.d.ts +131 -0
  121. package/dist/wasm/HexGridWasmWrapper.d.ts.map +1 -0
  122. package/dist/wasm/HexGridWasmWrapper.js +610 -0
  123. package/dist/wasm/index.d.ts +7 -0
  124. package/dist/wasm/index.d.ts.map +1 -0
  125. package/{src/wasm/index.ts → dist/wasm/index.js} +0 -1
  126. package/dist/webgpu/WebGPUContext.d.ts +20 -0
  127. package/dist/webgpu/WebGPUContext.d.ts.map +1 -0
  128. package/dist/webgpu/WebGPUContext.js +60 -0
  129. package/dist/webnn/WebNNContext.d.ts +38 -0
  130. package/dist/webnn/WebNNContext.d.ts.map +1 -0
  131. package/dist/webnn/WebNNContext.js +66 -0
  132. package/dist/workers/hexgrid-math.d.ts +79 -0
  133. package/dist/workers/hexgrid-math.d.ts.map +1 -0
  134. package/dist/workers/hexgrid-math.js +136 -0
  135. package/dist/workers/hexgrid-worker.worker.d.ts +35 -0
  136. package/dist/workers/hexgrid-worker.worker.d.ts.map +1 -0
  137. package/dist/workers/hexgrid-worker.worker.js +2014 -0
  138. package/package.json +20 -7
  139. package/.eslintrc.json +0 -28
  140. package/build_log.txt +0 -500
  141. package/build_src_log.txt +0 -8
  142. package/examples/basic-usage.tsx +0 -52
  143. package/public/hexgrid-worker.js +0 -2475
  144. package/rust/Cargo.toml +0 -41
  145. package/rust/src/lib.rs +0 -740
  146. package/rust/src/math.rs +0 -574
  147. package/rust/src/spatial.rs +0 -245
  148. package/rust/src/statistics.rs +0 -496
  149. package/site/.eslintrc.json +0 -3
  150. package/site/DEPLOYMENT.md +0 -196
  151. package/site/INDEX.md +0 -127
  152. package/site/QUICK_START.md +0 -86
  153. package/site/README.md +0 -85
  154. package/site/SITE_SUMMARY.md +0 -180
  155. package/site/next.config.js +0 -12
  156. package/site/package.json +0 -26
  157. package/site/src/app/docs/page.tsx +0 -272
  158. package/site/src/app/examples/page.tsx +0 -151
  159. package/site/src/app/globals.css +0 -160
  160. package/site/src/app/layout.tsx +0 -39
  161. package/site/src/app/page.tsx +0 -235
  162. package/site/tsconfig.json +0 -29
  163. package/site/vercel.json +0 -6
  164. package/src/HexGridEnhanced.ts +0 -16
  165. package/src/Snapshot.ts +0 -1607
  166. package/src/adapters/DashAdapter.ts +0 -57
  167. package/src/adapters.ts +0 -63
  168. package/src/algorithms/AdvancedStatistics.ts +0 -362
  169. package/src/algorithms/BayesianStatistics.ts +0 -348
  170. package/src/algorithms/FlowField.ts +0 -150
  171. package/src/algorithms/FlowField3D.ts +0 -573
  172. package/src/algorithms/FluidEngineFactory.ts +0 -44
  173. package/src/algorithms/FluidSimulation.ts +0 -115
  174. package/src/algorithms/FluidSimulation3D.ts +0 -664
  175. package/src/algorithms/FluidSimulation3DGPU.ts +0 -402
  176. package/src/algorithms/FluidSimulationWebNN.ts +0 -141
  177. package/src/algorithms/GraphAlgorithms.ts +0 -191
  178. package/src/algorithms/OutlierDetection.ts +0 -425
  179. package/src/algorithms/ParticleSystem.ts +0 -95
  180. package/src/algorithms/ParticleSystem3D.ts +0 -567
  181. package/src/compat.ts +0 -96
  182. package/src/components/HexGrid.tsx +0 -61
  183. package/src/components/NarrationOverlay.tsx +0 -309
  184. package/src/features.ts +0 -125
  185. package/src/index.ts +0 -30
  186. package/src/lib/narration.ts +0 -17
  187. package/src/lib/stats-tracker.ts +0 -25
  188. package/src/lib/theme-colors.ts +0 -12
  189. package/src/math/HexCoordinates.ts +0 -863
  190. package/src/math/Matrix4.ts +0 -25
  191. package/src/math/Quaternion.ts +0 -37
  192. package/src/math/SpatialIndex.ts +0 -114
  193. package/src/math/Vector3.ts +0 -540
  194. package/src/note-adapter.ts +0 -132
  195. package/src/ontology-adapter.ts +0 -84
  196. package/src/stores/uiStore.ts +0 -85
  197. package/src/types/index.ts +0 -3
  198. package/src/types/shared-utils.d.ts +0 -10
  199. package/src/types/wgsl.d.ts +0 -4
  200. package/src/types.ts +0 -164
  201. package/src/utils/image-utils.ts +0 -28
  202. package/src/wasm/HexGridWasmWrapper.ts +0 -801
  203. package/src/webgpu/WebGPUContext.ts +0 -71
  204. package/src/webgpu/shaders/fluid_sim.wgsl +0 -140
  205. package/src/webnn/WebNNContext.ts +0 -99
  206. package/src/workers/hexgrid-math.ts +0 -182
  207. package/src/workers/hexgrid-worker.worker.ts +0 -2781
  208. package/tsconfig.json +0 -26
  209. /package/{src/stores/index.ts → dist/stores/index.js} +0 -0
@@ -0,0 +1,757 @@
1
+ /**
2
+ * Unified Snapshot API
3
+ *
4
+ * Single comprehensive API to get ALL statistics, predictions, and insights
5
+ * about the current game state in one easy method call.
6
+ *
7
+ * @module Snapshot
8
+ *
9
+ * @example Basic Usage
10
+ * ```typescript
11
+ * import { generateSnapshot } from '@buley/hexgrid-3d'
12
+ *
13
+ * const snapshot = generateSnapshot(cells, history, conquests, getNeighbors)
14
+ *
15
+ * // Access player data
16
+ * console.log(snapshot.players[0].winProbability) // 0.72
17
+ * console.log(snapshot.players[0].sparklineAscii) // "▁▂▃▅▆█"
18
+ *
19
+ * // Access game state
20
+ * console.log(snapshot.indices.dominance) // 0.65
21
+ * console.log(snapshot.predictions.likelyWinner) // 2
22
+ *
23
+ * // Get insights
24
+ * console.log(snapshot.insights)
25
+ * // ["🏆 Player 2 dominates with 65.2% of territory",
26
+ * // "🚀 Leader's territory growing steadily"]
27
+ * ```
28
+ *
29
+ * @example Full Response Object
30
+ * ```typescript
31
+ * // GameSnapshot example response:
32
+ * const exampleSnapshot: GameSnapshot = {
33
+ * timestamp: 1705678800000,
34
+ * turnNumber: 45,
35
+ * totalCells: 1000,
36
+ * occupiedCells: 850,
37
+ * playerCount: 4,
38
+ *
39
+ * players: [{
40
+ * id: 2,
41
+ * cellCount: 320,
42
+ * shareOfTotal: 0.376,
43
+ * rank: 1,
44
+ * historyLength: 45,
45
+ * history: [10, 15, 22, 35, ...],
46
+ * recentTrend: 'growing',
47
+ * trendSlope: 4.2,
48
+ * trendConfidence: 0.89,
49
+ * winProbability: 0.72,
50
+ * winCredibleInterval: [0.58, 0.84],
51
+ * forecastNext10: [325, 330, 338, ...],
52
+ * kalmanEstimate: 322.5,
53
+ * kalmanUncertainty: 12.3,
54
+ * conquestRate: 0.15,
55
+ * conquestRateCI: [0.12, 0.18],
56
+ * avgGrowthPerTurn: 6.8,
57
+ * volatility: 0.23,
58
+ * numRegions: 2,
59
+ * largestRegionSize: 290,
60
+ * borderCellCount: 85,
61
+ * compactness: 0.72,
62
+ * sparklineAscii: "▁▂▃▄▅▆▇█",
63
+ * sparklineSvgPath: "M0,30 L10,25 L20,18...",
64
+ * leadOverSecond: 45,
65
+ * turnsUntilOvertake: null
66
+ * }, ...],
67
+ *
68
+ * inequality: {
69
+ * gini: 0.42,
70
+ * theil: 0.35,
71
+ * atkinson: 0.28,
72
+ * herfindahl: 0.31,
73
+ * paretoRatio: 0.72,
74
+ * zipfCoefficient: 1.15,
75
+ * interpretation: "Emerging dominance"
76
+ * },
77
+ *
78
+ * indices: {
79
+ * dominance: 0.65,
80
+ * volatility: 0.35,
81
+ * predictability: 0.72,
82
+ * competitiveness: 0.45,
83
+ * stability: 0.68
84
+ * },
85
+ *
86
+ * predictions: {
87
+ * likelyWinner: 2,
88
+ * winnerConfidence: 0.72,
89
+ * estimatedTurnsToVictory: 25,
90
+ * isEndgame: false,
91
+ * secondPlaceChallenger: 3,
92
+ * comebackPossibility: 0.28
93
+ * },
94
+ *
95
+ * anomalies: {
96
+ * outliers: [{ playerId: 4, type: 'growth_explosion', ... }],
97
+ * hasAnomalies: true,
98
+ * anomalyCount: 1
99
+ * },
100
+ *
101
+ * insights: [
102
+ * "📈 Player 2 leads with majority control (37.6%)",
103
+ * "🚀 Leader's territory growing steadily (slope: 4.2/turn)",
104
+ * "🎯 Player 3 has 22.5% chance of winning"
105
+ * ]
106
+ * }
107
+ * ```
108
+ */
109
+ import { giniCoefficient, theilIndex, atkinsonIndex, paretoRatio, zipfCoefficient, herfindahlIndex, shannonEntropy, normalizedEntropy, renyiEntropy, tsallisEntropy, klDivergence, jsDivergence, doubleExponentialSmoothing, detectTrend, detectChangePoints, compactness, sparkline, sparklineSvg, computeTerritoryStats, } from './algorithms/AdvancedStatistics';
110
+ import { KalmanFilter, bayesianConquestRate, generateProbabilitySnapshot, } from './algorithms/BayesianStatistics';
111
+ import { detectGameAnomalies, } from './algorithms/OutlierDetection';
112
+ /**
113
+ * Generate comprehensive snapshot from game state
114
+ */
115
+ export function generateSnapshot(cells, territoryHistory, conquestCounts, getNeighbors, config = {}) {
116
+ const { forecastHorizon = 10, monteCarloSamples = 1000, includeFullHistory = false, calculateTopology = true, generateInsights = true, } = config;
117
+ const timestamp = Date.now();
118
+ const totalCells = cells.length;
119
+ // Count territories
120
+ const territoryCounts = new Map();
121
+ for (const cell of cells) {
122
+ if (cell.owner !== 0) {
123
+ territoryCounts.set(cell.owner, (territoryCounts.get(cell.owner) ?? 0) + 1);
124
+ }
125
+ }
126
+ const occupiedCells = Array.from(territoryCounts.values()).reduce((a, b) => a + b, 0);
127
+ const players = Array.from(territoryCounts.keys()).sort((a, b) => a - b);
128
+ const playerCount = players.length;
129
+ // Sort by territory size for ranking
130
+ const sortedBySize = [...players].sort((a, b) => (territoryCounts.get(b) ?? 0) - (territoryCounts.get(a) ?? 0));
131
+ // Compute turn number from history length
132
+ const turnNumber = Math.max(...Array.from(territoryHistory.values()).map((h) => h.length), 0);
133
+ // Territory stats - computeTerritoryStats expects Array<{ area: number; perimeter: number }>
134
+ // For now, create a simplified version
135
+ const territoryAreas = Array.from(territoryCounts.entries()).map(([_, count]) => ({
136
+ area: count,
137
+ perimeter: Math.sqrt(count) * 4, // Simplified perimeter calculation
138
+ }));
139
+ const territoryStats = computeTerritoryStats(territoryAreas);
140
+ // Get probability snapshot - generateProbabilitySnapshot only takes labels: string[]
141
+ const playerLabels = Array.from(territoryCounts.keys()).map(String);
142
+ const probability = generateProbabilitySnapshot(playerLabels);
143
+ // Compute player snapshots
144
+ const playerSnapshots = [];
145
+ for (let rankIdx = 0; rankIdx < sortedBySize.length; rankIdx++) {
146
+ const playerId = sortedBySize[rankIdx];
147
+ const cellCount = territoryCounts.get(playerId) ?? 0;
148
+ const history = territoryHistory.get(playerId) ?? [];
149
+ // Trend detection
150
+ const trend = history.length > 3 ? detectTrend(history) : null;
151
+ // Forecast - doubleExponentialSmoothing returns number[], not an object with forecast method
152
+ const forecastValues = history.length > 2 ? doubleExponentialSmoothing(history, 0.3, 0.1) : [];
153
+ const forecastNext10 = forecastValues.slice(0, forecastHorizon);
154
+ // Kalman filter
155
+ let kalmanEstimate = cellCount;
156
+ let kalmanUncertainty = 0;
157
+ if (history.length > 3) {
158
+ const variance = history.slice(1).reduce((sum, v, i) => sum + (v - history[i]) ** 2, 0) /
159
+ history.length;
160
+ const filter = new KalmanFilter(history[0], variance || 1, (variance || 1) * 0.1, (variance || 1) * 0.5);
161
+ for (const measurement of history) {
162
+ filter.update(measurement);
163
+ }
164
+ kalmanEstimate = filter.getState();
165
+ kalmanUncertainty = filter.getUncertainty();
166
+ }
167
+ // Win probability - ProbabilitySnapshot only has probabilities array
168
+ const playerProb = probability.probabilities.find((p) => p.label === String(playerId));
169
+ const winProb = playerProb?.probability ?? 0;
170
+ const winCI = [
171
+ Math.max(0, winProb - 0.1),
172
+ Math.min(1, winProb + 0.1),
173
+ ]; // Simplified CI
174
+ // Conquest rate - bayesianConquestRate returns a number, not an object
175
+ const conquests = conquestCounts.get(playerId) ?? {
176
+ successes: 0,
177
+ opportunities: 0,
178
+ };
179
+ const conquestRate = bayesianConquestRate(conquests.successes, conquests.opportunities);
180
+ const conquestRateCI = [
181
+ Math.max(0, conquestRate - 0.1),
182
+ Math.min(1, conquestRate + 0.1),
183
+ ]; // Simplified CI
184
+ // Volatility
185
+ const volatility = history.length > 1
186
+ ? Math.sqrt(history
187
+ .slice(1)
188
+ .reduce((sum, v, i) => sum + (v - history[i]) ** 2, 0) /
189
+ (history.length - 1)) / (cellCount || 1)
190
+ : 0;
191
+ // Avg growth
192
+ const avgGrowth = history.length > 1
193
+ ? (history[history.length - 1] - history[0]) / history.length
194
+ : 0;
195
+ // Topology
196
+ let numRegions = 1;
197
+ let largestRegionSize = cellCount;
198
+ let borderCellCount = 0;
199
+ let playerCompactness = 0;
200
+ if (calculateTopology && cellCount > 0) {
201
+ const playerCells = new Set();
202
+ cells.forEach((cell, idx) => {
203
+ if (cell.owner === playerId)
204
+ playerCells.add(idx);
205
+ });
206
+ // Count regions using BFS
207
+ const visited = new Set();
208
+ const regionSizes = [];
209
+ for (const cellIdx of playerCells) {
210
+ if (visited.has(cellIdx))
211
+ continue;
212
+ const queue = [cellIdx];
213
+ visited.add(cellIdx);
214
+ let regionSize = 0;
215
+ while (queue.length > 0) {
216
+ const current = queue.shift();
217
+ regionSize++;
218
+ for (const neighbor of getNeighbors(current)) {
219
+ if (playerCells.has(neighbor) && !visited.has(neighbor)) {
220
+ visited.add(neighbor);
221
+ queue.push(neighbor);
222
+ }
223
+ }
224
+ }
225
+ regionSizes.push(regionSize);
226
+ }
227
+ numRegions = regionSizes.length;
228
+ largestRegionSize = Math.max(...regionSizes, 0);
229
+ // Border cells
230
+ for (const cellIdx of playerCells) {
231
+ const neighbors = getNeighbors(cellIdx);
232
+ if (neighbors.some((n) => !playerCells.has(n))) {
233
+ borderCellCount++;
234
+ }
235
+ }
236
+ // Compactness - compactness expects (area: number, perimeter: number)
237
+ const area = playerCells.size;
238
+ const perimeter = borderCellCount;
239
+ playerCompactness = compactness(area, perimeter);
240
+ }
241
+ // Lead over second
242
+ const leadOverSecond = rankIdx === 0 && sortedBySize.length > 1
243
+ ? cellCount - (territoryCounts.get(sortedBySize[1]) ?? 0)
244
+ : null;
245
+ // Turns until overtake (for non-leaders)
246
+ let turnsUntilOvertake = null;
247
+ if (rankIdx > 0 && forecastNext10.length > 0) {
248
+ const leaderHistory = territoryHistory.get(sortedBySize[0]) ?? [];
249
+ if (leaderHistory.length > 2) {
250
+ const leaderForecast = doubleExponentialSmoothing(leaderHistory, 0.3, 0.1);
251
+ const leaderPred = leaderForecast[leaderForecast.length - 1] ??
252
+ leaderHistory[leaderHistory.length - 1] ??
253
+ 0;
254
+ for (let t = 0; t < forecastHorizon && t < forecastNext10.length; t++) {
255
+ if (forecastNext10[t] > leaderPred) {
256
+ turnsUntilOvertake = t + 1;
257
+ break;
258
+ }
259
+ }
260
+ }
261
+ }
262
+ playerSnapshots.push({
263
+ id: playerId,
264
+ cellCount,
265
+ shareOfTotal: occupiedCells > 0 ? cellCount / occupiedCells : 0,
266
+ rank: rankIdx + 1,
267
+ historyLength: history.length,
268
+ history: includeFullHistory ? [...history] : history.slice(-20),
269
+ recentTrend: trend?.direction === 'increasing'
270
+ ? 'growing'
271
+ : trend?.direction === 'decreasing'
272
+ ? 'shrinking'
273
+ : 'stable',
274
+ trendSlope: trend?.slope ?? 0,
275
+ trendConfidence: trend?.rSquared ?? 0,
276
+ winProbability: winProb,
277
+ winCredibleInterval: winCI,
278
+ forecastNext10,
279
+ kalmanEstimate,
280
+ kalmanUncertainty,
281
+ conquestRate: conquestRate,
282
+ conquestRateCI: conquestRateCI,
283
+ avgGrowthPerTurn: avgGrowth,
284
+ volatility,
285
+ numRegions,
286
+ largestRegionSize,
287
+ borderCellCount,
288
+ compactness: playerCompactness,
289
+ sparklineAscii: sparkline(history.slice(-30)),
290
+ sparklineSvgPath: sparklineSvg(history.slice(-30), 100, 30),
291
+ leadOverSecond,
292
+ turnsUntilOvertake,
293
+ });
294
+ }
295
+ // Inequality metrics
296
+ const values = Array.from(territoryCounts.values());
297
+ const gini = giniCoefficient(values);
298
+ const theil = theilIndex(values);
299
+ const atkinson = atkinsonIndex(values, 0.5); // epsilon = 0.5 for standard calculation
300
+ const herfindahl = herfindahlIndex(values);
301
+ const pareto = paretoRatio(values, 0.2);
302
+ const zipf = zipfCoefficient(values);
303
+ let inequalityInterpretation;
304
+ if (gini < 0.2)
305
+ inequalityInterpretation = 'Highly balanced - fierce competition';
306
+ else if (gini < 0.4)
307
+ inequalityInterpretation = 'Moderately balanced';
308
+ else if (gini < 0.6)
309
+ inequalityInterpretation = 'Emerging dominance';
310
+ else if (gini < 0.8)
311
+ inequalityInterpretation = 'Clear leader emerging';
312
+ else
313
+ inequalityInterpretation = 'Near monopoly - game almost decided';
314
+ // Diversity metrics
315
+ const shannon = shannonEntropy(values);
316
+ const normalized = normalizedEntropy(values);
317
+ const renyi = renyiEntropy(values, 2); // alpha = 2 for standard Renyi entropy
318
+ const tsallis = tsallisEntropy(values, 2); // q = 2 for standard Tsallis entropy
319
+ let diversityInterpretation;
320
+ if (normalized > 0.9)
321
+ diversityInterpretation = 'Maximum diversity - all players equal';
322
+ else if (normalized > 0.7)
323
+ diversityInterpretation = 'High diversity';
324
+ else if (normalized > 0.5)
325
+ diversityInterpretation = 'Moderate diversity';
326
+ else if (normalized > 0.3)
327
+ diversityInterpretation = 'Low diversity - consolidation';
328
+ else
329
+ diversityInterpretation = 'Minimal diversity - game nearly over';
330
+ // Distribution metrics
331
+ const sorted = [...values].sort((a, b) => a - b);
332
+ const mean = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
333
+ const median = values.length > 0
334
+ ? values.length % 2 === 0
335
+ ? (sorted[values.length / 2 - 1] + sorted[values.length / 2]) / 2
336
+ : sorted[Math.floor(values.length / 2)]
337
+ : 0;
338
+ const variance = values.length > 0
339
+ ? values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length
340
+ : 0;
341
+ const stdDev = Math.sqrt(variance);
342
+ let m3 = 0, m4 = 0;
343
+ for (const v of values) {
344
+ const diff = v - mean;
345
+ m3 += diff ** 3;
346
+ m4 += diff ** 4;
347
+ }
348
+ const skewness = values.length > 0 && stdDev > 0 ? m3 / values.length / stdDev ** 3 : 0;
349
+ const kurtosis = values.length > 0 && stdDev > 0 ? m4 / values.length / stdDev ** 4 - 3 : 0;
350
+ const q1 = sorted[Math.floor(sorted.length * 0.25)] ?? 0;
351
+ const q3 = sorted[Math.floor(sorted.length * 0.75)] ?? 0;
352
+ // Game state indices - ProbabilitySnapshot doesn't have these, calculate from data
353
+ const topPlayerShare = sortedBySize.length > 0
354
+ ? (territoryCounts.get(sortedBySize[0]) ?? 0) / occupiedCells
355
+ : 0;
356
+ const dominance = topPlayerShare; // Simplified dominance index
357
+ // Competitiveness: inverse of lead
358
+ const topTwoShare = sortedBySize.length >= 2
359
+ ? ((territoryCounts.get(sortedBySize[0]) ?? 0) +
360
+ (territoryCounts.get(sortedBySize[1]) ?? 0)) /
361
+ occupiedCells
362
+ : 1;
363
+ const competitiveness = sortedBySize.length >= 2
364
+ ? 1 -
365
+ Math.abs((territoryCounts.get(sortedBySize[0]) ?? 0) -
366
+ (territoryCounts.get(sortedBySize[1]) ?? 0)) /
367
+ occupiedCells
368
+ : 0;
369
+ // Stability: inverse of avg change rate
370
+ let totalChangeRate = 0;
371
+ let historyCount = 0;
372
+ for (const [, history] of territoryHistory) {
373
+ if (history.length > 1) {
374
+ const changes = history.slice(1).map((v, i) => Math.abs(v - history[i]));
375
+ const avgChange = changes.reduce((a, b) => a + b, 0) / changes.length;
376
+ totalChangeRate += avgChange / (history[history.length - 1] || 1);
377
+ historyCount++;
378
+ }
379
+ }
380
+ const stability = historyCount > 0 ? Math.max(0, 1 - totalChangeRate / historyCount) : 0.5;
381
+ // Predictions
382
+ // Calculate estimated turns to victory from forecast data
383
+ const likelyWinner = sortedBySize.length > 0 ? sortedBySize[0] : null;
384
+ const winnerConfidence = topPlayerShare; // Simplified confidence
385
+ // Estimate turns to victory based on forecast (simplified)
386
+ let estimatedTurnsToVictory = null;
387
+ if (sortedBySize.length > 0 && territoryHistory.has(sortedBySize[0])) {
388
+ const leaderHistory = territoryHistory.get(sortedBySize[0]) ?? [];
389
+ if (leaderHistory.length > 0) {
390
+ const currentShare = (territoryCounts.get(sortedBySize[0]) ?? 0) / occupiedCells;
391
+ const targetShare = 0.5; // 50% to win
392
+ if (currentShare < targetShare && leaderHistory.length > 1) {
393
+ const growthRate = (leaderHistory[leaderHistory.length - 1] - leaderHistory[0]) /
394
+ leaderHistory.length;
395
+ if (growthRate > 0) {
396
+ estimatedTurnsToVictory = Math.ceil((targetShare * occupiedCells -
397
+ (territoryCounts.get(sortedBySize[0]) ?? 0)) /
398
+ growthRate);
399
+ }
400
+ }
401
+ }
402
+ }
403
+ const isEndgame = gini > 0.7 ||
404
+ (sortedBySize.length >= 2 &&
405
+ (territoryCounts.get(sortedBySize[0]) ?? 0) > occupiedCells * 0.6);
406
+ const secondPlaceChallenger = sortedBySize.length >= 2 ? sortedBySize[1] : null;
407
+ // Calculate volatility early for use in comebackPossibility
408
+ // Time series insights
409
+ let overallTrend = 'chaotic';
410
+ // Check if shares are converging or diverging
411
+ const recentVariances = [];
412
+ const historyLen = Math.min(...Array.from(territoryHistory.values()).map((h) => h.length));
413
+ for (let t = Math.max(0, historyLen - 10); t < historyLen; t++) {
414
+ const sharesAtT = players.map((p) => {
415
+ const h = territoryHistory.get(p) ?? [];
416
+ return h[t] ?? 0;
417
+ });
418
+ const meanAtT = sharesAtT.reduce((a, b) => a + b, 0) / sharesAtT.length;
419
+ const varAtT = sharesAtT.reduce((sum, s) => sum + (s - meanAtT) ** 2, 0) /
420
+ sharesAtT.length;
421
+ recentVariances.push(varAtT);
422
+ }
423
+ // Calculate volatility and predictability from recentVariances
424
+ const volatility = recentVariances.length > 0
425
+ ? Math.sqrt(recentVariances.reduce((a, b) => a + b, 0) / recentVariances.length)
426
+ : 0;
427
+ const predictability = 1 - Math.min(1, volatility); // Simplified predictability (inverse of volatility)
428
+ // Comeback possibility based on volatility and current gap
429
+ const comebackPossibility = sortedBySize.length >= 2
430
+ ? Math.min(1, volatility + (1 - dominance)) * (1 - winnerConfidence)
431
+ : 0;
432
+ // Topology summary
433
+ let totalRegions = 0;
434
+ let totalBorderCells = 0;
435
+ let compactnessSum = 0;
436
+ for (const player of playerSnapshots) {
437
+ totalRegions += player.numRegions;
438
+ totalBorderCells += player.borderCellCount;
439
+ compactnessSum += player.compactness;
440
+ }
441
+ const averageRegionSize = totalRegions > 0 ? occupiedCells / totalRegions : 0;
442
+ const territoryFragmentation = playerCount > 0 ? (totalRegions - playerCount) / (occupiedCells || 1) : 0;
443
+ const borderCellPercentage = occupiedCells > 0 ? totalBorderCells / occupiedCells : 0;
444
+ const avgCompactness = playerCount > 0 ? compactnessSum / playerCount : 0;
445
+ if (recentVariances.length > 3) {
446
+ const varianceTrend = detectTrend(recentVariances);
447
+ const rSquared = varianceTrend.rSquared ?? 0;
448
+ if (varianceTrend.direction === 'decreasing' && rSquared > 0.5) {
449
+ overallTrend = 'convergent';
450
+ }
451
+ else if (varianceTrend.direction === 'increasing' && rSquared > 0.5) {
452
+ overallTrend = 'divergent';
453
+ }
454
+ else if (rSquared < 0.2) {
455
+ overallTrend = 'chaotic';
456
+ }
457
+ else {
458
+ overallTrend = 'cyclical';
459
+ }
460
+ }
461
+ // Overall change points
462
+ const overallHistory = Array.from(territoryHistory.values())[0] ?? [];
463
+ const changePoints = detectChangePoints(overallHistory);
464
+ // Trend strength
465
+ const overallHistoryTrend = overallHistory.length > 3 ? detectTrend(overallHistory) : null;
466
+ const trendStrength = overallHistoryTrend?.rSquared ?? 0;
467
+ // Autocorrelation (lag 1)
468
+ let autocorrelation = 0;
469
+ if (overallHistory.length > 5) {
470
+ const ohMean = overallHistory.reduce((a, b) => a + b, 0) / overallHistory.length;
471
+ let num = 0, denom = 0;
472
+ for (let i = 1; i < overallHistory.length; i++) {
473
+ num += (overallHistory[i] - ohMean) * (overallHistory[i - 1] - ohMean);
474
+ }
475
+ for (let i = 0; i < overallHistory.length; i++) {
476
+ denom += (overallHistory[i] - ohMean) ** 2;
477
+ }
478
+ autocorrelation = denom > 0 ? num / denom : 0;
479
+ }
480
+ // Seasonality (simple check)
481
+ const seasonality = false;
482
+ // Comparisons
483
+ const vs5TurnsAgo = {};
484
+ const vs10TurnsAgo = {};
485
+ for (const [player, history] of territoryHistory) {
486
+ const current = history[history.length - 1] ?? 0;
487
+ const fiveAgo = history[history.length - 6] ?? current;
488
+ const tenAgo = history[history.length - 11] ?? current;
489
+ vs5TurnsAgo[player] = current - fiveAgo;
490
+ vs10TurnsAgo[player] = current - tenAgo;
491
+ }
492
+ // Divergence from uniform
493
+ const uniformProbs = values.map(() => 1 / values.length);
494
+ const actualProbs = values.map((v) => v / (occupiedCells || 1));
495
+ const divergenceFromUniform = klDivergence(actualProbs, uniformProbs);
496
+ // Divergence from previous turn
497
+ let divergenceFromPrevious = 0;
498
+ if (historyLen > 1) {
499
+ const prevProbs = players.map((p) => {
500
+ const h = territoryHistory.get(p) ?? [];
501
+ return (h[h.length - 2] ?? 0) / (occupiedCells || 1);
502
+ });
503
+ divergenceFromPrevious = jsDivergence(actualProbs, prevProbs);
504
+ }
505
+ // Generate insights
506
+ const insights = [];
507
+ if (generateInsights) {
508
+ // Leader insights
509
+ if (sortedBySize.length > 0) {
510
+ const leader = playerSnapshots.find((p) => p.rank === 1);
511
+ const leaderShare = leader.shareOfTotal;
512
+ if (leaderShare > 0.8) {
513
+ insights.push(`🏆 Player ${leader.id} dominates with ${(leaderShare * 100).toFixed(1)}% of territory`);
514
+ }
515
+ else if (leaderShare > 0.5) {
516
+ insights.push(`📈 Player ${leader.id} leads with majority control (${(leaderShare * 100).toFixed(1)}%)`);
517
+ }
518
+ if (leader.recentTrend === 'growing' && leader.trendConfidence > 0.7) {
519
+ insights.push(`🚀 Leader's territory growing steadily (slope: ${leader.trendSlope.toFixed(2)}/turn)`);
520
+ }
521
+ else if (leader.recentTrend === 'shrinking' &&
522
+ leader.trendConfidence > 0.7) {
523
+ insights.push(`⚠️ Leader losing ground - opportunity for challengers!`);
524
+ }
525
+ }
526
+ // Challenger insights
527
+ if (sortedBySize.length >= 2) {
528
+ const challenger = playerSnapshots.find((p) => p.rank === 2);
529
+ if (challenger.winProbability > 0.3) {
530
+ insights.push(`🎯 Player ${challenger.id} has ${(challenger.winProbability * 100).toFixed(1)}% chance of winning`);
531
+ }
532
+ if (challenger.turnsUntilOvertake !== null &&
533
+ challenger.turnsUntilOvertake < 5) {
534
+ insights.push(`⚡ Player ${challenger.id} could overtake in ~${challenger.turnsUntilOvertake} turns!`);
535
+ }
536
+ }
537
+ // Game state insights
538
+ if (isEndgame) {
539
+ insights.push(`🔚 Endgame detected - victory imminent`);
540
+ }
541
+ if (competitiveness > 0.9) {
542
+ insights.push(`🔥 Extremely close competition - anyone could win!`);
543
+ }
544
+ if (volatility > 0.7) {
545
+ insights.push(`🌊 High volatility - expect rapid changes`);
546
+ }
547
+ else if (volatility < 0.2) {
548
+ insights.push(`🪨 Low volatility - stable territorial lines`);
549
+ }
550
+ if (comebackPossibility > 0.5) {
551
+ insights.push(`🔄 Comeback still possible (${(comebackPossibility * 100).toFixed(0)}% chance)`);
552
+ }
553
+ // Topology insights
554
+ if (territoryFragmentation > 0.2) {
555
+ insights.push(`🧩 High fragmentation - territories are scattered`);
556
+ }
557
+ if (avgCompactness < 0.3) {
558
+ insights.push(`📏 Territories have irregular borders - vulnerable to attack`);
559
+ }
560
+ // Change point insights
561
+ if (changePoints.length > 0) {
562
+ const recentChangePoint = changePoints[changePoints.length - 1];
563
+ if (turnNumber - recentChangePoint < 5) {
564
+ insights.push(`📊 Recent momentum shift detected at turn ${recentChangePoint}`);
565
+ }
566
+ }
567
+ // Trend insights
568
+ if (overallTrend === 'convergent') {
569
+ insights.push(`📉 Territories are converging - expect stalemate or final push`);
570
+ }
571
+ else if (overallTrend === 'divergent') {
572
+ insights.push(`📈 Gap widening - leader pulling ahead`);
573
+ }
574
+ }
575
+ // Detect anomalies - detectGameAnomalies expects number[], convert territoryHistory
576
+ const allHistoryValues = [];
577
+ for (const [, history] of territoryHistory) {
578
+ allHistoryValues.push(...history);
579
+ }
580
+ const gameAnomalies = detectGameAnomalies(allHistoryValues);
581
+ const anomalies = {
582
+ outliers: gameAnomalies,
583
+ hasAnomalies: gameAnomalies.length > 0,
584
+ anomalyCount: gameAnomalies.length,
585
+ mostSevere: gameAnomalies.length > 0
586
+ ? gameAnomalies.reduce((max, a) => (a.severity > (max?.severity ?? 0) ? a : max), gameAnomalies[0]).type
587
+ : null,
588
+ };
589
+ // Add anomaly insights
590
+ if (generateInsights && anomalies.hasAnomalies) {
591
+ for (const anomaly of gameAnomalies.slice(0, 3)) {
592
+ // Top 3 anomalies
593
+ insights.push(`⚠️ Anomaly: ${anomaly.description}`);
594
+ }
595
+ }
596
+ return {
597
+ timestamp,
598
+ turnNumber,
599
+ totalCells,
600
+ occupiedCells,
601
+ playerCount,
602
+ players: playerSnapshots,
603
+ territoryStats,
604
+ inequality: {
605
+ gini,
606
+ theil,
607
+ atkinson,
608
+ herfindahl,
609
+ paretoRatio: pareto.ratioHeld,
610
+ zipfCoefficient: zipf,
611
+ interpretation: inequalityInterpretation,
612
+ },
613
+ diversity: {
614
+ shannon,
615
+ normalized,
616
+ renyi,
617
+ tsallis,
618
+ interpretation: diversityInterpretation,
619
+ },
620
+ distribution: {
621
+ mean,
622
+ median,
623
+ stdDev,
624
+ skewness,
625
+ kurtosis,
626
+ coefficientOfVariation: mean > 0 ? stdDev / mean : 0,
627
+ iqr: q3 - q1,
628
+ min: sorted[0] ?? 0,
629
+ max: sorted[sorted.length - 1] ?? 0,
630
+ range: (sorted[sorted.length - 1] ?? 0) - (sorted[0] ?? 0),
631
+ },
632
+ indices: {
633
+ dominance,
634
+ volatility,
635
+ predictability,
636
+ competitiveness,
637
+ stability,
638
+ },
639
+ predictions: {
640
+ likelyWinner,
641
+ winnerConfidence,
642
+ estimatedTurnsToVictory,
643
+ isEndgame,
644
+ secondPlaceChallenger,
645
+ comebackPossibility,
646
+ },
647
+ anomalies,
648
+ probability,
649
+ topology: {
650
+ totalRegions,
651
+ averageRegionSize,
652
+ territoryFragmentation,
653
+ borderCellPercentage,
654
+ avgCompactness,
655
+ },
656
+ timeSeries: {
657
+ overallTrend,
658
+ changePoints,
659
+ trendStrength,
660
+ autocorrelation,
661
+ seasonality,
662
+ },
663
+ comparisons: {
664
+ vs5TurnsAgo,
665
+ vs10TurnsAgo,
666
+ divergenceFromUniform,
667
+ divergenceFromPrevious,
668
+ },
669
+ insights,
670
+ };
671
+ }
672
+ /**
673
+ * Format snapshot as human-readable text
674
+ */
675
+ export function formatSnapshotAsText(snapshot) {
676
+ const lines = [];
677
+ lines.push('═══════════════════════════════════════════════════════════');
678
+ lines.push(` GAME SNAPSHOT - Turn ${snapshot.turnNumber}`);
679
+ lines.push('═══════════════════════════════════════════════════════════');
680
+ lines.push('');
681
+ lines.push(`📊 Territory: ${snapshot.occupiedCells}/${snapshot.totalCells} cells occupied`);
682
+ lines.push(`👥 Players: ${snapshot.playerCount}`);
683
+ lines.push('');
684
+ lines.push('┌─────────────────────────────────────────────────────────┐');
685
+ lines.push('│ PLAYER STANDINGS │');
686
+ lines.push('├─────────────────────────────────────────────────────────┤');
687
+ for (const player of snapshot.players) {
688
+ const bar = '█'.repeat(Math.ceil(player.shareOfTotal * 20));
689
+ const pad = ' '.repeat(20 - bar.length);
690
+ const trend = player.recentTrend === 'growing'
691
+ ? '↑'
692
+ : player.recentTrend === 'shrinking'
693
+ ? '↓'
694
+ : '→';
695
+ lines.push(`│ #${player.rank} Player ${player.id}: ${player.cellCount} cells (${(player.shareOfTotal * 100).toFixed(1)}%)`);
696
+ lines.push(`│ ${bar}${pad} ${trend}`);
697
+ lines.push(`│ Win Prob: ${(player.winProbability * 100).toFixed(1)}% Sparkline: ${player.sparklineAscii}`);
698
+ lines.push('│');
699
+ }
700
+ lines.push('└─────────────────────────────────────────────────────────┘');
701
+ lines.push('');
702
+ lines.push('┌─────────────────────────────────────────────────────────┐');
703
+ lines.push('│ GAME STATE │');
704
+ lines.push('├─────────────────────────────────────────────────────────┤');
705
+ lines.push(`│ Dominance: ${progressBar(snapshot.indices.dominance)} ${(snapshot.indices.dominance * 100).toFixed(0)}%`);
706
+ lines.push(`│ Volatility: ${progressBar(snapshot.indices.volatility)} ${(snapshot.indices.volatility * 100).toFixed(0)}%`);
707
+ lines.push(`│ Competitiveness:${progressBar(snapshot.indices.competitiveness)} ${(snapshot.indices.competitiveness * 100).toFixed(0)}%`);
708
+ lines.push(`│ Stability: ${progressBar(snapshot.indices.stability)} ${(snapshot.indices.stability * 100).toFixed(0)}%`);
709
+ lines.push(`│ Predictability: ${progressBar(snapshot.indices.predictability)} ${(snapshot.indices.predictability * 100).toFixed(0)}%`);
710
+ lines.push('└─────────────────────────────────────────────────────────┘');
711
+ lines.push('');
712
+ lines.push('┌─────────────────────────────────────────────────────────┐');
713
+ lines.push('│ PREDICTIONS │');
714
+ lines.push('├─────────────────────────────────────────────────────────┤');
715
+ if (snapshot.predictions.likelyWinner !== null) {
716
+ lines.push(`│ Likely Winner: Player ${snapshot.predictions.likelyWinner}`);
717
+ lines.push(`│ Confidence: ${(snapshot.predictions.winnerConfidence * 100).toFixed(1)}%`);
718
+ if (snapshot.predictions.estimatedTurnsToVictory !== null) {
719
+ lines.push(`│ Est. Victory In: ${snapshot.predictions.estimatedTurnsToVictory} turns`);
720
+ }
721
+ }
722
+ else {
723
+ lines.push('│ No clear winner predicted yet');
724
+ }
725
+ if (snapshot.predictions.isEndgame) {
726
+ lines.push('│ ⚠️ ENDGAME DETECTED');
727
+ }
728
+ lines.push(`│ Comeback Chance: ${(snapshot.predictions.comebackPossibility * 100).toFixed(0)}%`);
729
+ lines.push('└─────────────────────────────────────────────────────────┘');
730
+ lines.push('');
731
+ if (snapshot.insights.length > 0) {
732
+ lines.push('┌─────────────────────────────────────────────────────────┐');
733
+ lines.push('│ INSIGHTS │');
734
+ lines.push('├─────────────────────────────────────────────────────────┤');
735
+ for (const insight of snapshot.insights) {
736
+ lines.push(`│ ${insight}`);
737
+ }
738
+ lines.push('└─────────────────────────────────────────────────────────┘');
739
+ }
740
+ return lines.join('\n');
741
+ }
742
+ function progressBar(value, width = 20) {
743
+ const filled = Math.round(value * width);
744
+ const empty = width - filled;
745
+ return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
746
+ }
747
+ /**
748
+ * Export snapshot as JSON (removes functions)
749
+ */
750
+ export function exportSnapshotAsJSON(snapshot) {
751
+ return JSON.stringify(snapshot, (key, value) => {
752
+ if (value instanceof Map) {
753
+ return Object.fromEntries(value);
754
+ }
755
+ return value;
756
+ }, 2);
757
+ }