@buley/hexgrid-3d 3.1.0 → 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 (208) 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 -0
  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/{src/index.ts → dist/index.js} +0 -9
  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 -408
  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/lib/narration.ts +0 -17
  186. package/src/lib/stats-tracker.ts +0 -25
  187. package/src/lib/theme-colors.ts +0 -12
  188. package/src/math/HexCoordinates.ts +0 -863
  189. package/src/math/Matrix4.ts +0 -25
  190. package/src/math/Quaternion.ts +0 -37
  191. package/src/math/SpatialIndex.ts +0 -114
  192. package/src/math/Vector3.ts +0 -540
  193. package/src/note-adapter.ts +0 -132
  194. package/src/ontology-adapter.ts +0 -84
  195. package/src/stores/uiStore.ts +0 -85
  196. package/src/types/index.ts +0 -3
  197. package/src/types/shared-utils.d.ts +0 -10
  198. package/src/types/wgsl.d.ts +0 -4
  199. package/src/types.ts +0 -164
  200. package/src/utils/image-utils.ts +0 -28
  201. package/src/wasm/HexGridWasmWrapper.ts +0 -801
  202. package/src/webgpu/WebGPUContext.ts +0 -71
  203. package/src/webgpu/shaders/fluid_sim.wgsl +0 -140
  204. package/src/webnn/WebNNContext.ts +0 -99
  205. package/src/workers/hexgrid-math.ts +0 -182
  206. package/src/workers/hexgrid-worker.worker.ts +0 -2781
  207. package/tsconfig.json +0 -26
  208. /package/{src/stores/index.ts → dist/stores/index.js} +0 -0
package/src/Snapshot.ts DELETED
@@ -1,1607 +0,0 @@
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
-
110
- import {
111
- giniCoefficient,
112
- theilIndex,
113
- atkinsonIndex,
114
- paretoRatio,
115
- zipfCoefficient,
116
- herfindahlIndex,
117
- shannonEntropy,
118
- normalizedEntropy,
119
- renyiEntropy,
120
- tsallisEntropy,
121
- klDivergence,
122
- jsDivergence,
123
- bhattacharyyaCoefficient,
124
- hellingerDistance,
125
- movingAverage,
126
- exponentialMovingAverage,
127
- doubleExponentialSmoothing,
128
- detectTrend,
129
- detectChangePoints,
130
- predictWinner,
131
- eulerCharacteristic,
132
- estimateBettiNumbers,
133
- compactness,
134
- sparkline,
135
- sparklineSvg,
136
- computeTerritoryStats,
137
- type TerritoryStats,
138
- } from './algorithms/AdvancedStatistics';
139
-
140
- import {
141
- BetaDistribution,
142
- DirichletDistribution,
143
- NormalDistribution,
144
- PoissonDistribution,
145
- ExponentialDistribution,
146
- MarkovChain,
147
- KalmanFilter,
148
- HiddenMarkovModel,
149
- bayesianABTest,
150
- bayesFactor,
151
- mapEstimate,
152
- learnMarkovChain,
153
- bootstrapConfidenceInterval,
154
- monteCarloIntegrate,
155
- mutualInformation,
156
- conditionalEntropy,
157
- normalizedMutualInformation,
158
- bayesianWinProbability,
159
- bayesianConquestRate,
160
- bayesianChangepoint,
161
- generateProbabilitySnapshot,
162
- type ProbabilitySnapshot,
163
- } from './algorithms/BayesianStatistics';
164
-
165
- import {
166
- detectOutliersZScore,
167
- detectOutliersModifiedZScore,
168
- detectOutliersIQR,
169
- detectGrowthSpikes,
170
- detectVarianceChanges,
171
- detectGameAnomalies,
172
- comprehensiveOutlierAnalysis,
173
- mahalanobisOutliers,
174
- localOutlierFactor,
175
- isolationForest,
176
- cusumChart,
177
- ewmaChart,
178
- type OutlierResult,
179
- type TimeSeriesAnomaly,
180
- type GameAnomaly,
181
- type MultivariateOutlierResult,
182
- } from './algorithms/OutlierDetection';
183
-
184
- import {
185
- findConnectedComponents,
186
- analyzeTerritorBoundaries,
187
- } from './algorithms/GraphAlgorithms';
188
-
189
- // ═══════════════════════════════════════════════════════════════════════════
190
- // UNIFIED SNAPSHOT TYPES
191
- // ═══════════════════════════════════════════════════════════════════════════
192
-
193
- /**
194
- * Player-specific statistics
195
- *
196
- * @example
197
- * ```typescript
198
- * const player: PlayerSnapshot = {
199
- * id: 1,
200
- * cellCount: 150,
201
- * shareOfTotal: 0.25,
202
- * rank: 2,
203
- * historyLength: 30,
204
- * history: [10, 15, 25, 40, 60, 90, 120, 150],
205
- * recentTrend: 'growing',
206
- * trendSlope: 5.2,
207
- * trendConfidence: 0.92,
208
- * winProbability: 0.35,
209
- * winCredibleInterval: [0.22, 0.48],
210
- * forecastNext10: [155, 162, 170, 178, 186, 195, 204, 213, 223, 233],
211
- * kalmanEstimate: 152.3,
212
- * kalmanUncertainty: 8.5,
213
- * conquestRate: 0.12,
214
- * conquestRateCI: [0.08, 0.16],
215
- * avgGrowthPerTurn: 4.8,
216
- * volatility: 0.18,
217
- * numRegions: 1,
218
- * largestRegionSize: 150,
219
- * borderCellCount: 42,
220
- * compactness: 0.78,
221
- * sparklineAscii: "▁▂▃▄▅▆▇█",
222
- * sparklineSvgPath: "M0,30 L12.5,25 L25,18 L37.5,12...",
223
- * leadOverSecond: null, // not the leader
224
- * turnsUntilOvertake: 8
225
- * }
226
- * ```
227
- */
228
- export interface PlayerSnapshot {
229
- /** Unique player identifier */
230
- id: number;
231
-
232
- // Territory
233
- /** Current number of cells owned */
234
- cellCount: number;
235
- /** Fraction of total occupied cells (0-1) */
236
- shareOfTotal: number;
237
- /** Current ranking (1 = leader) */
238
- rank: number;
239
-
240
- // History
241
- /** Number of turns of history available */
242
- historyLength: number;
243
- /** Territory count history (last 20 turns unless includeFullHistory=true) */
244
- history: number[];
245
- /** Recent territory trend direction */
246
- recentTrend: 'growing' | 'shrinking' | 'stable';
247
- /** Linear regression slope (cells per turn) */
248
- trendSlope: number;
249
- /** R² confidence in trend (0-1) */
250
- trendConfidence: number;
251
-
252
- // Predictions
253
- /** Bayesian probability of winning (0-1) */
254
- winProbability: number;
255
- /** 95% credible interval for win probability */
256
- winCredibleInterval: [number, number];
257
- /** Forecasted territory for next 10 turns */
258
- forecastNext10: number[];
259
- /** Kalman filter smoothed estimate */
260
- kalmanEstimate: number;
261
- /** Kalman filter uncertainty (±) */
262
- kalmanUncertainty: number;
263
-
264
- // Performance
265
- /** Bayesian conquest success rate */
266
- conquestRate: number;
267
- /** 95% credible interval for conquest rate */
268
- conquestRateCI: [number, number];
269
- /** Average cells gained per turn */
270
- avgGrowthPerTurn: number;
271
- /** Territory volatility (coefficient of variation) */
272
- volatility: number;
273
-
274
- // Topology
275
- /** Number of disconnected territory regions */
276
- numRegions: number;
277
- /** Size of largest connected region */
278
- largestRegionSize: number;
279
- /** Number of cells on territory border */
280
- borderCellCount: number;
281
- /** Territory shape compactness (0-1, higher = more compact) */
282
- compactness: number;
283
-
284
- // Sparklines
285
- /** ASCII sparkline visualization of history */
286
- sparklineAscii: string;
287
- /** SVG path data for sparkline */
288
- sparklineSvgPath: string;
289
-
290
- // Relative metrics
291
- /** Lead over second place (null if not leader) */
292
- leadOverSecond: number | null;
293
- /** Estimated turns until this player could overtake leader */
294
- turnsUntilOvertake: number | null;
295
- }
296
-
297
- /**
298
- * Inequality metrics for territory distribution
299
- *
300
- * @example
301
- * ```typescript
302
- * const inequality: InequalityMetrics = {
303
- * gini: 0.42, // 0=equal, 1=one player has all
304
- * theil: 0.35, // 0=equal, higher=more unequal
305
- * atkinson: 0.28, // Sensitive to lower end of distribution
306
- * herfindahl: 0.31, // Market concentration (0.25=4 equal players)
307
- * paretoRatio: 0.72, // Top player's share of total
308
- * zipfCoefficient: 1.15, // Power law exponent
309
- * interpretation: "Emerging dominance - one player pulling ahead"
310
- * }
311
- * ```
312
- */
313
- export interface InequalityMetrics {
314
- gini: number;
315
- theil: number;
316
- atkinson: number;
317
- herfindahl: number;
318
- paretoRatio: number;
319
- zipfCoefficient: number;
320
- interpretation: string;
321
- }
322
-
323
- /**
324
- * Diversity/entropy metrics for territory distribution
325
- *
326
- * @example
327
- * ```typescript
328
- * const diversity: DiversityMetrics = {
329
- * shannon: 1.85, // Bits of information
330
- * normalized: 0.92, // 0-1, relative to max possible
331
- * renyi: 1.78, // Generalized entropy (order 2)
332
- * tsallis: 1.52, // Non-extensive entropy
333
- * interpretation: "High diversity - competitive game"
334
- * }
335
- * ```
336
- */
337
- export interface DiversityMetrics {
338
- shannon: number;
339
- normalized: number;
340
- renyi: number;
341
- tsallis: number;
342
- interpretation: string;
343
- }
344
-
345
- /**
346
- * Distribution statistics for territory counts
347
- *
348
- * @example
349
- * ```typescript
350
- * const distribution: DistributionMetrics = {
351
- * mean: 250,
352
- * median: 220,
353
- * stdDev: 85,
354
- * skewness: 0.65, // Positive = right-skewed
355
- * kurtosis: 2.8, // 3 = normal distribution
356
- * coefficientOfVariation: 0.34,
357
- * iqr: 120, // Interquartile range
358
- * min: 85,
359
- * max: 420,
360
- * range: 335
361
- * }
362
- * ```
363
- */
364
- export interface DistributionMetrics {
365
- mean: number;
366
- median: number;
367
- stdDev: number;
368
- skewness: number;
369
- kurtosis: number;
370
- coefficientOfVariation: number;
371
- iqr: number;
372
- min: number;
373
- max: number;
374
- range: number;
375
- }
376
-
377
- /**
378
- * Game state indices (0-1 scale)
379
- *
380
- * @example
381
- * ```typescript
382
- * const indices: GameIndices = {
383
- * dominance: 0.65, // One player controls 65% relative
384
- * volatility: 0.35, // 35% average change per turn
385
- * predictability: 0.72, // 72% confidence in predictions
386
- * competitiveness: 0.45, // 45% competitive (55% decided)
387
- * stability: 0.68 // 68% stable territories
388
- * }
389
- * ```
390
- */
391
- export interface GameIndices {
392
- /** How dominated by a single player (0=even, 1=total domination) */
393
- dominance: number;
394
- /** How much territory changes each turn (0=static, 1=chaotic) */
395
- volatility: number;
396
- /** How predictable outcomes are (0=random, 1=deterministic) */
397
- predictability: number;
398
- /** How close the competition (0=decided, 1=neck-and-neck) */
399
- competitiveness: number;
400
- /** How stable territory boundaries are (0=fluid, 1=locked) */
401
- stability: number;
402
- }
403
-
404
- /**
405
- * Game outcome predictions
406
- *
407
- * @example
408
- * ```typescript
409
- * const predictions: GamePredictions = {
410
- * likelyWinner: 2,
411
- * winnerConfidence: 0.72,
412
- * estimatedTurnsToVictory: 25,
413
- * isEndgame: false,
414
- * secondPlaceChallenger: 3,
415
- * comebackPossibility: 0.28
416
- * }
417
- * ```
418
- */
419
- export interface GamePredictions {
420
- /** Player ID most likely to win */
421
- likelyWinner: number | null;
422
- /** Confidence in winner prediction (0-1) */
423
- winnerConfidence: number;
424
- /** Estimated turns until victory condition */
425
- estimatedTurnsToVictory: number | null;
426
- /** Whether game is in endgame phase */
427
- isEndgame: boolean;
428
- /** Second place player who could challenge */
429
- secondPlaceChallenger: number | null;
430
- /** Probability of comeback by non-leader */
431
- comebackPossibility: number;
432
- }
433
-
434
- /**
435
- * Territory topology metrics
436
- *
437
- * @example
438
- * ```typescript
439
- * const topology: TopologyMetrics = {
440
- * totalRegions: 8,
441
- * averageRegionSize: 125,
442
- * territoryFragmentation: 0.35,
443
- * borderCellPercentage: 0.42,
444
- * avgCompactness: 0.68
445
- * }
446
- * ```
447
- */
448
- export interface TopologyMetrics {
449
- /** Total disconnected regions across all players */
450
- totalRegions: number;
451
- /** Average cells per region */
452
- averageRegionSize: number;
453
- /** How fragmented territories are (0=solid, 1=scattered) */
454
- territoryFragmentation: number;
455
- /** Percentage of cells that are border cells */
456
- borderCellPercentage: number;
457
- /** Average compactness across all territories */
458
- avgCompactness: number;
459
- }
460
-
461
- /**
462
- * Time series analysis metrics
463
- *
464
- * @example
465
- * ```typescript
466
- * const timeSeries: TimeSeriesMetrics = {
467
- * overallTrend: 'divergent', // Players spreading apart
468
- * changePoints: [12, 28, 45], // Turn numbers where behavior changed
469
- * trendStrength: 0.78, // Strength of overall trend
470
- * autocorrelation: 0.65, // How correlated with past values
471
- * seasonality: false // No periodic patterns detected
472
- * }
473
- * ```
474
- */
475
- export interface TimeSeriesMetrics {
476
- /** Overall trend type */
477
- overallTrend: 'convergent' | 'divergent' | 'cyclical' | 'chaotic';
478
- /** Turn numbers where significant changes occurred */
479
- changePoints: number[];
480
- /** Strength of overall trend (0-1) */
481
- trendStrength: number;
482
- /** Autocorrelation coefficient */
483
- autocorrelation: number;
484
- /** Whether periodic patterns detected */
485
- seasonality: boolean;
486
- }
487
-
488
- /**
489
- * Comparison metrics with past states
490
- *
491
- * @example
492
- * ```typescript
493
- * const comparisons: ComparisonMetrics = {
494
- * vs5TurnsAgo: { 1: 15, 2: 8, 3: -12, 4: -11 }, // Territory change
495
- * vs10TurnsAgo: { 1: 45, 2: 22, 3: -35, 4: -32 },
496
- * divergenceFromUniform: 0.42, // How far from equal distribution
497
- * divergenceFromPrevious: 0.08 // How much changed from last turn
498
- * }
499
- * ```
500
- */
501
- export interface ComparisonMetrics {
502
- /** Territory change per player vs 5 turns ago */
503
- vs5TurnsAgo: { [playerId: number]: number };
504
- /** Territory change per player vs 10 turns ago */
505
- vs10TurnsAgo: { [playerId: number]: number };
506
- /** KL divergence from uniform distribution */
507
- divergenceFromUniform: number;
508
- /** JS divergence from previous turn */
509
- divergenceFromPrevious: number;
510
- }
511
-
512
- /**
513
- * Anomaly detection summary
514
- *
515
- * @example
516
- * ```typescript
517
- * const anomalies: AnomalySummary = {
518
- * outliers: [{
519
- * playerId: 2,
520
- * type: 'growth_explosion',
521
- * severity: 3.2,
522
- * timestamp: 1705678800000,
523
- * description: 'Unusual territory gain: +45 cells in one turn',
524
- * metrics: { growth: 45, avgGrowth: 5.2, zscore: 3.2 }
525
- * }],
526
- * hasAnomalies: true,
527
- * anomalyCount: 1,
528
- * mostSevere: 'growth_explosion'
529
- * }
530
- * ```
531
- */
532
- export interface AnomalySummary {
533
- /** Detected game anomalies */
534
- outliers: GameAnomaly[];
535
- /** Whether any anomalies were detected */
536
- hasAnomalies: boolean;
537
- /** Total count of anomalies */
538
- anomalyCount: number;
539
- /** Type of most severe anomaly (if any) */
540
- mostSevere: string | null;
541
- }
542
-
543
- /**
544
- * Complete game snapshot with all statistics
545
- *
546
- * @example Full Response Structure
547
- * ```typescript
548
- * const snapshot: GameSnapshot = {
549
- * timestamp: 1705678800000,
550
- * turnNumber: 45,
551
- * totalCells: 1000,
552
- * occupiedCells: 850,
553
- * playerCount: 4,
554
- *
555
- * players: [
556
- * { id: 2, cellCount: 320, shareOfTotal: 0.376, rank: 1, ... },
557
- * { id: 1, cellCount: 210, shareOfTotal: 0.247, rank: 2, ... },
558
- * { id: 3, cellCount: 180, shareOfTotal: 0.212, rank: 3, ... },
559
- * { id: 4, cellCount: 140, shareOfTotal: 0.165, rank: 4, ... }
560
- * ],
561
- *
562
- * territoryStats: { ... },
563
- *
564
- * inequality: {
565
- * gini: 0.42,
566
- * theil: 0.35,
567
- * atkinson: 0.28,
568
- * herfindahl: 0.31,
569
- * paretoRatio: 0.72,
570
- * zipfCoefficient: 1.15,
571
- * interpretation: "Emerging dominance"
572
- * },
573
- *
574
- * diversity: {
575
- * shannon: 1.85,
576
- * normalized: 0.92,
577
- * renyi: 1.78,
578
- * tsallis: 1.52,
579
- * interpretation: "High diversity"
580
- * },
581
- *
582
- * distribution: {
583
- * mean: 212.5, median: 195, stdDev: 68.2,
584
- * skewness: 0.65, kurtosis: 2.8,
585
- * coefficientOfVariation: 0.32,
586
- * iqr: 95, min: 140, max: 320, range: 180
587
- * },
588
- *
589
- * indices: {
590
- * dominance: 0.65,
591
- * volatility: 0.35,
592
- * predictability: 0.72,
593
- * competitiveness: 0.45,
594
- * stability: 0.68
595
- * },
596
- *
597
- * predictions: {
598
- * likelyWinner: 2,
599
- * winnerConfidence: 0.72,
600
- * estimatedTurnsToVictory: 25,
601
- * isEndgame: false,
602
- * secondPlaceChallenger: 1,
603
- * comebackPossibility: 0.28
604
- * },
605
- *
606
- * anomalies: {
607
- * outliers: [],
608
- * hasAnomalies: false,
609
- * anomalyCount: 0,
610
- * mostSevere: null
611
- * },
612
- *
613
- * probability: { winProbabilities: Map, ... },
614
- * topology: { totalRegions: 8, ... },
615
- * timeSeries: { overallTrend: 'divergent', ... },
616
- * comparisons: { vs5TurnsAgo: {...}, ... },
617
- *
618
- * insights: [
619
- * "📈 Player 2 leads with 37.6% of territory",
620
- * "🚀 Leader's territory growing steadily",
621
- * "🎯 72% confidence in Player 2 victory"
622
- * ]
623
- * }
624
- * ```
625
- */
626
- export interface GameSnapshot {
627
- // Basic
628
- /** Unix timestamp when snapshot was generated */
629
- timestamp: number;
630
- /** Current turn number */
631
- turnNumber: number;
632
- /** Total cells on the grid */
633
- totalCells: number;
634
- /** Number of cells with an owner */
635
- occupiedCells: number;
636
-
637
- // Players
638
- /** Number of active players */
639
- playerCount: number;
640
- /** Detailed stats for each player, sorted by rank */
641
- players: PlayerSnapshot[];
642
-
643
- // Overall territory stats
644
- /** Aggregate territory statistics */
645
- territoryStats: TerritoryStats;
646
-
647
- // Inequality metrics (all players)
648
- /** Inequality measures for territory distribution */
649
- inequality: InequalityMetrics;
650
-
651
- // Entropy/diversity
652
- /** Diversity/entropy measures */
653
- diversity: DiversityMetrics;
654
-
655
- // Distribution metrics
656
- /** Statistical distribution of territory counts */
657
- distribution: DistributionMetrics;
658
-
659
- // Game state indices
660
- /** Normalized game state indicators */
661
- indices: GameIndices;
662
-
663
- // Predictions
664
- /** Game outcome predictions */
665
- predictions: GamePredictions;
666
-
667
- // Anomaly detection
668
- /** Detected anomalies and outliers */
669
- anomalies: AnomalySummary;
670
-
671
- // Probability snapshot (Bayesian)
672
- /** Full Bayesian probability analysis */
673
- probability: ProbabilitySnapshot;
674
-
675
- // Topology
676
- /** Territory topology metrics */
677
- topology: TopologyMetrics;
678
-
679
- // Time series insights
680
- /** Time series analysis */
681
- timeSeries: TimeSeriesMetrics;
682
-
683
- // Comparisons
684
- /** Comparisons with past states */
685
- comparisons: ComparisonMetrics;
686
-
687
- // Recommendations / insights
688
- /** Human-readable insights and observations */
689
- insights: string[];
690
- }
691
-
692
- // Re-export all types for easy importing
693
- export type {
694
- TerritoryStats,
695
- ProbabilitySnapshot,
696
- OutlierResult,
697
- TimeSeriesAnomaly,
698
- GameAnomaly,
699
- MultivariateOutlierResult,
700
- };
701
-
702
- // ═══════════════════════════════════════════════════════════════════════════
703
- // SNAPSHOT GENERATOR
704
- // ═══════════════════════════════════════════════════════════════════════════
705
-
706
- export interface SnapshotConfig {
707
- /** Number of turns for forecasting */
708
- forecastHorizon?: number;
709
- /** Number of Monte Carlo samples */
710
- monteCarloSamples?: number;
711
- /** Include detailed history */
712
- includeFullHistory?: boolean;
713
- /** Calculate topology (can be expensive) */
714
- calculateTopology?: boolean;
715
- /** Generate insights */
716
- generateInsights?: boolean;
717
- }
718
-
719
- /**
720
- * Generate comprehensive snapshot from game state
721
- */
722
- export function generateSnapshot(
723
- cells: { owner: number; population?: number }[],
724
- territoryHistory: Map<number, number[]>,
725
- conquestCounts: Map<number, { successes: number; opportunities: number }>,
726
- getNeighbors: (cellIndex: number) => number[],
727
- config: SnapshotConfig = {}
728
- ): GameSnapshot {
729
- const {
730
- forecastHorizon = 10,
731
- monteCarloSamples = 1000,
732
- includeFullHistory = false,
733
- calculateTopology = true,
734
- generateInsights = true,
735
- } = config;
736
-
737
- const timestamp = Date.now();
738
- const totalCells = cells.length;
739
-
740
- // Count territories
741
- const territoryCounts = new Map<number, number>();
742
- for (const cell of cells) {
743
- if (cell.owner !== 0) {
744
- territoryCounts.set(
745
- cell.owner,
746
- (territoryCounts.get(cell.owner) ?? 0) + 1
747
- );
748
- }
749
- }
750
-
751
- const occupiedCells = Array.from(territoryCounts.values()).reduce(
752
- (a, b) => a + b,
753
- 0
754
- );
755
- const players = Array.from(territoryCounts.keys()).sort((a, b) => a - b);
756
- const playerCount = players.length;
757
-
758
- // Sort by territory size for ranking
759
- const sortedBySize = [...players].sort(
760
- (a, b) => (territoryCounts.get(b) ?? 0) - (territoryCounts.get(a) ?? 0)
761
- );
762
-
763
- // Compute turn number from history length
764
- const turnNumber = Math.max(
765
- ...Array.from(territoryHistory.values()).map((h) => h.length),
766
- 0
767
- );
768
-
769
- // Territory stats - computeTerritoryStats expects Array<{ area: number; perimeter: number }>
770
- // For now, create a simplified version
771
- const territoryAreas = Array.from(territoryCounts.entries()).map(
772
- ([_, count]) => ({
773
- area: count,
774
- perimeter: Math.sqrt(count) * 4, // Simplified perimeter calculation
775
- })
776
- );
777
- const territoryStats = computeTerritoryStats(territoryAreas);
778
-
779
- // Get probability snapshot - generateProbabilitySnapshot only takes labels: string[]
780
- const playerLabels = Array.from(territoryCounts.keys()).map(String);
781
- const probability = generateProbabilitySnapshot(playerLabels);
782
-
783
- // Compute player snapshots
784
- const playerSnapshots: PlayerSnapshot[] = [];
785
-
786
- for (let rankIdx = 0; rankIdx < sortedBySize.length; rankIdx++) {
787
- const playerId = sortedBySize[rankIdx];
788
- const cellCount = territoryCounts.get(playerId) ?? 0;
789
- const history = territoryHistory.get(playerId) ?? [];
790
-
791
- // Trend detection
792
- const trend = history.length > 3 ? detectTrend(history) : null;
793
-
794
- // Forecast - doubleExponentialSmoothing returns number[], not an object with forecast method
795
- const forecastValues =
796
- history.length > 2 ? doubleExponentialSmoothing(history, 0.3, 0.1) : [];
797
- const forecastNext10 = forecastValues.slice(0, forecastHorizon);
798
-
799
- // Kalman filter
800
- let kalmanEstimate = cellCount;
801
- let kalmanUncertainty = 0;
802
- if (history.length > 3) {
803
- const variance =
804
- history.slice(1).reduce((sum, v, i) => sum + (v - history[i]) ** 2, 0) /
805
- history.length;
806
-
807
- const filter = new KalmanFilter(
808
- history[0],
809
- variance || 1,
810
- (variance || 1) * 0.1,
811
- (variance || 1) * 0.5
812
- );
813
-
814
- for (const measurement of history) {
815
- filter.update(measurement);
816
- }
817
-
818
- kalmanEstimate = filter.getState();
819
- kalmanUncertainty = filter.getUncertainty();
820
- }
821
-
822
- // Win probability - ProbabilitySnapshot only has probabilities array
823
- const playerProb = probability.probabilities.find(
824
- (p) => p.label === String(playerId)
825
- );
826
- const winProb = playerProb?.probability ?? 0;
827
- const winCI: [number, number] = [
828
- Math.max(0, winProb - 0.1),
829
- Math.min(1, winProb + 0.1),
830
- ]; // Simplified CI
831
-
832
- // Conquest rate - bayesianConquestRate returns a number, not an object
833
- const conquests = conquestCounts.get(playerId) ?? {
834
- successes: 0,
835
- opportunities: 0,
836
- };
837
- const conquestRate = bayesianConquestRate(
838
- conquests.successes,
839
- conquests.opportunities
840
- );
841
- const conquestRateCI: [number, number] = [
842
- Math.max(0, conquestRate - 0.1),
843
- Math.min(1, conquestRate + 0.1),
844
- ]; // Simplified CI
845
-
846
- // Volatility
847
- const volatility =
848
- history.length > 1
849
- ? Math.sqrt(
850
- history
851
- .slice(1)
852
- .reduce((sum, v, i) => sum + (v - history[i]) ** 2, 0) /
853
- (history.length - 1)
854
- ) / (cellCount || 1)
855
- : 0;
856
-
857
- // Avg growth
858
- const avgGrowth =
859
- history.length > 1
860
- ? (history[history.length - 1] - history[0]) / history.length
861
- : 0;
862
-
863
- // Topology
864
- let numRegions = 1;
865
- let largestRegionSize = cellCount;
866
- let borderCellCount = 0;
867
- let playerCompactness = 0;
868
-
869
- if (calculateTopology && cellCount > 0) {
870
- const playerCells = new Set<number>();
871
- cells.forEach((cell, idx) => {
872
- if (cell.owner === playerId) playerCells.add(idx);
873
- });
874
-
875
- // Count regions using BFS
876
- const visited = new Set<number>();
877
- const regionSizes: number[] = [];
878
-
879
- for (const cellIdx of playerCells) {
880
- if (visited.has(cellIdx)) continue;
881
-
882
- const queue = [cellIdx];
883
- visited.add(cellIdx);
884
- let regionSize = 0;
885
-
886
- while (queue.length > 0) {
887
- const current = queue.shift()!;
888
- regionSize++;
889
-
890
- for (const neighbor of getNeighbors(current)) {
891
- if (playerCells.has(neighbor) && !visited.has(neighbor)) {
892
- visited.add(neighbor);
893
- queue.push(neighbor);
894
- }
895
- }
896
- }
897
-
898
- regionSizes.push(regionSize);
899
- }
900
-
901
- numRegions = regionSizes.length;
902
- largestRegionSize = Math.max(...regionSizes, 0);
903
-
904
- // Border cells
905
- for (const cellIdx of playerCells) {
906
- const neighbors = getNeighbors(cellIdx);
907
- if (neighbors.some((n) => !playerCells.has(n))) {
908
- borderCellCount++;
909
- }
910
- }
911
-
912
- // Compactness - compactness expects (area: number, perimeter: number)
913
- const area = playerCells.size;
914
- const perimeter = borderCellCount;
915
- playerCompactness = compactness(area, perimeter);
916
- }
917
-
918
- // Lead over second
919
- const leadOverSecond =
920
- rankIdx === 0 && sortedBySize.length > 1
921
- ? cellCount - (territoryCounts.get(sortedBySize[1]) ?? 0)
922
- : null;
923
-
924
- // Turns until overtake (for non-leaders)
925
- let turnsUntilOvertake: number | null = null;
926
- if (rankIdx > 0 && forecastNext10.length > 0) {
927
- const leaderHistory = territoryHistory.get(sortedBySize[0]) ?? [];
928
- if (leaderHistory.length > 2) {
929
- const leaderForecast = doubleExponentialSmoothing(
930
- leaderHistory,
931
- 0.3,
932
- 0.1
933
- );
934
- const leaderPred =
935
- leaderForecast[leaderForecast.length - 1] ??
936
- leaderHistory[leaderHistory.length - 1] ??
937
- 0;
938
-
939
- for (let t = 0; t < forecastHorizon && t < forecastNext10.length; t++) {
940
- if (forecastNext10[t] > leaderPred) {
941
- turnsUntilOvertake = t + 1;
942
- break;
943
- }
944
- }
945
- }
946
- }
947
-
948
- playerSnapshots.push({
949
- id: playerId,
950
- cellCount,
951
- shareOfTotal: occupiedCells > 0 ? cellCount / occupiedCells : 0,
952
- rank: rankIdx + 1,
953
- historyLength: history.length,
954
- history: includeFullHistory ? [...history] : history.slice(-20),
955
- recentTrend:
956
- trend?.direction === 'increasing'
957
- ? 'growing'
958
- : trend?.direction === 'decreasing'
959
- ? 'shrinking'
960
- : 'stable',
961
- trendSlope: trend?.slope ?? 0,
962
- trendConfidence: trend?.rSquared ?? 0,
963
- winProbability: winProb,
964
- winCredibleInterval: winCI as [number, number],
965
- forecastNext10,
966
- kalmanEstimate,
967
- kalmanUncertainty,
968
- conquestRate: conquestRate,
969
- conquestRateCI: conquestRateCI,
970
- avgGrowthPerTurn: avgGrowth,
971
- volatility,
972
- numRegions,
973
- largestRegionSize,
974
- borderCellCount,
975
- compactness: playerCompactness,
976
- sparklineAscii: sparkline(history.slice(-30)),
977
- sparklineSvgPath: sparklineSvg(history.slice(-30), 100, 30),
978
- leadOverSecond,
979
- turnsUntilOvertake,
980
- });
981
- }
982
-
983
- // Inequality metrics
984
- const values = Array.from(territoryCounts.values());
985
- const gini = giniCoefficient(values);
986
- const theil = theilIndex(values);
987
- const atkinson = atkinsonIndex(values, 0.5); // epsilon = 0.5 for standard calculation
988
- const herfindahl = herfindahlIndex(values);
989
- const pareto = paretoRatio(values, 0.2);
990
- const zipf = zipfCoefficient(values);
991
-
992
- let inequalityInterpretation: string;
993
- if (gini < 0.2)
994
- inequalityInterpretation = 'Highly balanced - fierce competition';
995
- else if (gini < 0.4) inequalityInterpretation = 'Moderately balanced';
996
- else if (gini < 0.6) inequalityInterpretation = 'Emerging dominance';
997
- else if (gini < 0.8) inequalityInterpretation = 'Clear leader emerging';
998
- else inequalityInterpretation = 'Near monopoly - game almost decided';
999
-
1000
- // Diversity metrics
1001
- const shannon = shannonEntropy(values);
1002
- const normalized = normalizedEntropy(values);
1003
- const renyi = renyiEntropy(values, 2); // alpha = 2 for standard Renyi entropy
1004
- const tsallis = tsallisEntropy(values, 2); // q = 2 for standard Tsallis entropy
1005
-
1006
- let diversityInterpretation: string;
1007
- if (normalized > 0.9)
1008
- diversityInterpretation = 'Maximum diversity - all players equal';
1009
- else if (normalized > 0.7) diversityInterpretation = 'High diversity';
1010
- else if (normalized > 0.5) diversityInterpretation = 'Moderate diversity';
1011
- else if (normalized > 0.3)
1012
- diversityInterpretation = 'Low diversity - consolidation';
1013
- else diversityInterpretation = 'Minimal diversity - game nearly over';
1014
-
1015
- // Distribution metrics
1016
- const sorted = [...values].sort((a, b) => a - b);
1017
- const mean =
1018
- values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
1019
- const median =
1020
- values.length > 0
1021
- ? values.length % 2 === 0
1022
- ? (sorted[values.length / 2 - 1] + sorted[values.length / 2]) / 2
1023
- : sorted[Math.floor(values.length / 2)]
1024
- : 0;
1025
- const variance =
1026
- values.length > 0
1027
- ? values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length
1028
- : 0;
1029
- const stdDev = Math.sqrt(variance);
1030
-
1031
- let m3 = 0,
1032
- m4 = 0;
1033
- for (const v of values) {
1034
- const diff = v - mean;
1035
- m3 += diff ** 3;
1036
- m4 += diff ** 4;
1037
- }
1038
- const skewness =
1039
- values.length > 0 && stdDev > 0 ? m3 / values.length / stdDev ** 3 : 0;
1040
- const kurtosis =
1041
- values.length > 0 && stdDev > 0 ? m4 / values.length / stdDev ** 4 - 3 : 0;
1042
-
1043
- const q1 = sorted[Math.floor(sorted.length * 0.25)] ?? 0;
1044
- const q3 = sorted[Math.floor(sorted.length * 0.75)] ?? 0;
1045
-
1046
- // Game state indices - ProbabilitySnapshot doesn't have these, calculate from data
1047
- const topPlayerShare =
1048
- sortedBySize.length > 0
1049
- ? (territoryCounts.get(sortedBySize[0]) ?? 0) / occupiedCells
1050
- : 0;
1051
- const dominance = topPlayerShare; // Simplified dominance index
1052
-
1053
- // Competitiveness: inverse of lead
1054
- const topTwoShare =
1055
- sortedBySize.length >= 2
1056
- ? ((territoryCounts.get(sortedBySize[0]) ?? 0) +
1057
- (territoryCounts.get(sortedBySize[1]) ?? 0)) /
1058
- occupiedCells
1059
- : 1;
1060
- const competitiveness =
1061
- sortedBySize.length >= 2
1062
- ? 1 -
1063
- Math.abs(
1064
- (territoryCounts.get(sortedBySize[0]) ?? 0) -
1065
- (territoryCounts.get(sortedBySize[1]) ?? 0)
1066
- ) /
1067
- occupiedCells
1068
- : 0;
1069
-
1070
- // Stability: inverse of avg change rate
1071
- let totalChangeRate = 0;
1072
- let historyCount = 0;
1073
- for (const [, history] of territoryHistory) {
1074
- if (history.length > 1) {
1075
- const changes = history.slice(1).map((v, i) => Math.abs(v - history[i]));
1076
- const avgChange = changes.reduce((a, b) => a + b, 0) / changes.length;
1077
- totalChangeRate += avgChange / (history[history.length - 1] || 1);
1078
- historyCount++;
1079
- }
1080
- }
1081
- const stability =
1082
- historyCount > 0 ? Math.max(0, 1 - totalChangeRate / historyCount) : 0.5;
1083
-
1084
- // Predictions
1085
- // Calculate estimated turns to victory from forecast data
1086
- const likelyWinner = sortedBySize.length > 0 ? sortedBySize[0] : null;
1087
- const winnerConfidence = topPlayerShare; // Simplified confidence
1088
- // Estimate turns to victory based on forecast (simplified)
1089
- let estimatedTurnsToVictory: number | null = null;
1090
- if (sortedBySize.length > 0 && territoryHistory.has(sortedBySize[0])) {
1091
- const leaderHistory = territoryHistory.get(sortedBySize[0]) ?? [];
1092
- if (leaderHistory.length > 0) {
1093
- const currentShare =
1094
- (territoryCounts.get(sortedBySize[0]) ?? 0) / occupiedCells;
1095
- const targetShare = 0.5; // 50% to win
1096
- if (currentShare < targetShare && leaderHistory.length > 1) {
1097
- const growthRate =
1098
- (leaderHistory[leaderHistory.length - 1] - leaderHistory[0]) /
1099
- leaderHistory.length;
1100
- if (growthRate > 0) {
1101
- estimatedTurnsToVictory = Math.ceil(
1102
- (targetShare * occupiedCells -
1103
- (territoryCounts.get(sortedBySize[0]) ?? 0)) /
1104
- growthRate
1105
- );
1106
- }
1107
- }
1108
- }
1109
- }
1110
-
1111
- const isEndgame =
1112
- gini > 0.7 ||
1113
- (sortedBySize.length >= 2 &&
1114
- (territoryCounts.get(sortedBySize[0]) ?? 0) > occupiedCells * 0.6);
1115
-
1116
- const secondPlaceChallenger =
1117
- sortedBySize.length >= 2 ? sortedBySize[1] : null;
1118
-
1119
- // Calculate volatility early for use in comebackPossibility
1120
- // Time series insights
1121
- let overallTrend: 'convergent' | 'divergent' | 'cyclical' | 'chaotic' =
1122
- 'chaotic';
1123
-
1124
- // Check if shares are converging or diverging
1125
- const recentVariances: number[] = [];
1126
- const historyLen = Math.min(
1127
- ...Array.from(territoryHistory.values()).map((h) => h.length)
1128
- );
1129
-
1130
- for (let t = Math.max(0, historyLen - 10); t < historyLen; t++) {
1131
- const sharesAtT = players.map((p) => {
1132
- const h = territoryHistory.get(p) ?? [];
1133
- return h[t] ?? 0;
1134
- });
1135
- const meanAtT = sharesAtT.reduce((a, b) => a + b, 0) / sharesAtT.length;
1136
- const varAtT =
1137
- sharesAtT.reduce((sum, s) => sum + (s - meanAtT) ** 2, 0) /
1138
- sharesAtT.length;
1139
- recentVariances.push(varAtT);
1140
- }
1141
-
1142
- // Calculate volatility and predictability from recentVariances
1143
- const volatility =
1144
- recentVariances.length > 0
1145
- ? Math.sqrt(
1146
- recentVariances.reduce((a, b) => a + b, 0) / recentVariances.length
1147
- )
1148
- : 0;
1149
- const predictability = 1 - Math.min(1, volatility); // Simplified predictability (inverse of volatility)
1150
-
1151
- // Comeback possibility based on volatility and current gap
1152
- const comebackPossibility =
1153
- sortedBySize.length >= 2
1154
- ? Math.min(1, volatility + (1 - dominance)) * (1 - winnerConfidence)
1155
- : 0;
1156
-
1157
- // Topology summary
1158
- let totalRegions = 0;
1159
- let totalBorderCells = 0;
1160
- let compactnessSum = 0;
1161
-
1162
- for (const player of playerSnapshots) {
1163
- totalRegions += player.numRegions;
1164
- totalBorderCells += player.borderCellCount;
1165
- compactnessSum += player.compactness;
1166
- }
1167
-
1168
- const averageRegionSize = totalRegions > 0 ? occupiedCells / totalRegions : 0;
1169
- const territoryFragmentation =
1170
- playerCount > 0 ? (totalRegions - playerCount) / (occupiedCells || 1) : 0;
1171
- const borderCellPercentage =
1172
- occupiedCells > 0 ? totalBorderCells / occupiedCells : 0;
1173
- const avgCompactness = playerCount > 0 ? compactnessSum / playerCount : 0;
1174
-
1175
- if (recentVariances.length > 3) {
1176
- const varianceTrend = detectTrend(recentVariances);
1177
- const rSquared = varianceTrend.rSquared ?? 0;
1178
- if (varianceTrend.direction === 'decreasing' && rSquared > 0.5) {
1179
- overallTrend = 'convergent';
1180
- } else if (varianceTrend.direction === 'increasing' && rSquared > 0.5) {
1181
- overallTrend = 'divergent';
1182
- } else if (rSquared < 0.2) {
1183
- overallTrend = 'chaotic';
1184
- } else {
1185
- overallTrend = 'cyclical';
1186
- }
1187
- }
1188
-
1189
- // Overall change points
1190
- const overallHistory = Array.from(territoryHistory.values())[0] ?? [];
1191
- const changePoints = detectChangePoints(overallHistory);
1192
-
1193
- // Trend strength
1194
- const overallHistoryTrend =
1195
- overallHistory.length > 3 ? detectTrend(overallHistory) : null;
1196
- const trendStrength = overallHistoryTrend?.rSquared ?? 0;
1197
-
1198
- // Autocorrelation (lag 1)
1199
- let autocorrelation = 0;
1200
- if (overallHistory.length > 5) {
1201
- const ohMean =
1202
- overallHistory.reduce((a, b) => a + b, 0) / overallHistory.length;
1203
- let num = 0,
1204
- denom = 0;
1205
- for (let i = 1; i < overallHistory.length; i++) {
1206
- num += (overallHistory[i] - ohMean) * (overallHistory[i - 1] - ohMean);
1207
- }
1208
- for (let i = 0; i < overallHistory.length; i++) {
1209
- denom += (overallHistory[i] - ohMean) ** 2;
1210
- }
1211
- autocorrelation = denom > 0 ? num / denom : 0;
1212
- }
1213
-
1214
- // Seasonality (simple check)
1215
- const seasonality = false;
1216
-
1217
- // Comparisons
1218
- const vs5TurnsAgo: { [playerId: number]: number } = {};
1219
- const vs10TurnsAgo: { [playerId: number]: number } = {};
1220
-
1221
- for (const [player, history] of territoryHistory) {
1222
- const current = history[history.length - 1] ?? 0;
1223
- const fiveAgo = history[history.length - 6] ?? current;
1224
- const tenAgo = history[history.length - 11] ?? current;
1225
-
1226
- vs5TurnsAgo[player] = current - fiveAgo;
1227
- vs10TurnsAgo[player] = current - tenAgo;
1228
- }
1229
-
1230
- // Divergence from uniform
1231
- const uniformProbs = values.map(() => 1 / values.length);
1232
- const actualProbs = values.map((v) => v / (occupiedCells || 1));
1233
- const divergenceFromUniform = klDivergence(actualProbs, uniformProbs);
1234
-
1235
- // Divergence from previous turn
1236
- let divergenceFromPrevious = 0;
1237
- if (historyLen > 1) {
1238
- const prevProbs = players.map((p) => {
1239
- const h = territoryHistory.get(p) ?? [];
1240
- return (h[h.length - 2] ?? 0) / (occupiedCells || 1);
1241
- });
1242
- divergenceFromPrevious = jsDivergence(actualProbs, prevProbs);
1243
- }
1244
-
1245
- // Generate insights
1246
- const insights: string[] = [];
1247
-
1248
- if (generateInsights) {
1249
- // Leader insights
1250
- if (sortedBySize.length > 0) {
1251
- const leader = playerSnapshots.find((p) => p.rank === 1)!;
1252
- const leaderShare = leader.shareOfTotal;
1253
-
1254
- if (leaderShare > 0.8) {
1255
- insights.push(
1256
- `🏆 Player ${leader.id} dominates with ${(leaderShare * 100).toFixed(
1257
- 1
1258
- )}% of territory`
1259
- );
1260
- } else if (leaderShare > 0.5) {
1261
- insights.push(
1262
- `📈 Player ${leader.id} leads with majority control (${(
1263
- leaderShare * 100
1264
- ).toFixed(1)}%)`
1265
- );
1266
- }
1267
-
1268
- if (leader.recentTrend === 'growing' && leader.trendConfidence > 0.7) {
1269
- insights.push(
1270
- `🚀 Leader's territory growing steadily (slope: ${leader.trendSlope.toFixed(
1271
- 2
1272
- )}/turn)`
1273
- );
1274
- } else if (
1275
- leader.recentTrend === 'shrinking' &&
1276
- leader.trendConfidence > 0.7
1277
- ) {
1278
- insights.push(`⚠️ Leader losing ground - opportunity for challengers!`);
1279
- }
1280
- }
1281
-
1282
- // Challenger insights
1283
- if (sortedBySize.length >= 2) {
1284
- const challenger = playerSnapshots.find((p) => p.rank === 2)!;
1285
-
1286
- if (challenger.winProbability > 0.3) {
1287
- insights.push(
1288
- `🎯 Player ${challenger.id} has ${(
1289
- challenger.winProbability * 100
1290
- ).toFixed(1)}% chance of winning`
1291
- );
1292
- }
1293
-
1294
- if (
1295
- challenger.turnsUntilOvertake !== null &&
1296
- challenger.turnsUntilOvertake < 5
1297
- ) {
1298
- insights.push(
1299
- `⚡ Player ${challenger.id} could overtake in ~${challenger.turnsUntilOvertake} turns!`
1300
- );
1301
- }
1302
- }
1303
-
1304
- // Game state insights
1305
- if (isEndgame) {
1306
- insights.push(`🔚 Endgame detected - victory imminent`);
1307
- }
1308
-
1309
- if (competitiveness > 0.9) {
1310
- insights.push(`🔥 Extremely close competition - anyone could win!`);
1311
- }
1312
-
1313
- if (volatility > 0.7) {
1314
- insights.push(`🌊 High volatility - expect rapid changes`);
1315
- } else if (volatility < 0.2) {
1316
- insights.push(`🪨 Low volatility - stable territorial lines`);
1317
- }
1318
-
1319
- if (comebackPossibility > 0.5) {
1320
- insights.push(
1321
- `🔄 Comeback still possible (${(comebackPossibility * 100).toFixed(
1322
- 0
1323
- )}% chance)`
1324
- );
1325
- }
1326
-
1327
- // Topology insights
1328
- if (territoryFragmentation > 0.2) {
1329
- insights.push(`🧩 High fragmentation - territories are scattered`);
1330
- }
1331
-
1332
- if (avgCompactness < 0.3) {
1333
- insights.push(
1334
- `📏 Territories have irregular borders - vulnerable to attack`
1335
- );
1336
- }
1337
-
1338
- // Change point insights
1339
- if (changePoints.length > 0) {
1340
- const recentChangePoint = changePoints[changePoints.length - 1];
1341
- if (turnNumber - recentChangePoint < 5) {
1342
- insights.push(
1343
- `📊 Recent momentum shift detected at turn ${recentChangePoint}`
1344
- );
1345
- }
1346
- }
1347
-
1348
- // Trend insights
1349
- if (overallTrend === 'convergent') {
1350
- insights.push(
1351
- `📉 Territories are converging - expect stalemate or final push`
1352
- );
1353
- } else if (overallTrend === 'divergent') {
1354
- insights.push(`📈 Gap widening - leader pulling ahead`);
1355
- }
1356
- }
1357
-
1358
- // Detect anomalies - detectGameAnomalies expects number[], convert territoryHistory
1359
- const allHistoryValues: number[] = [];
1360
- for (const [, history] of territoryHistory) {
1361
- allHistoryValues.push(...history);
1362
- }
1363
- const gameAnomalies = detectGameAnomalies(allHistoryValues);
1364
-
1365
- const anomalies: AnomalySummary = {
1366
- outliers: gameAnomalies,
1367
- hasAnomalies: gameAnomalies.length > 0,
1368
- anomalyCount: gameAnomalies.length,
1369
- mostSevere:
1370
- gameAnomalies.length > 0
1371
- ? gameAnomalies.reduce(
1372
- (max, a) => (a.severity > (max?.severity ?? 0) ? a : max),
1373
- gameAnomalies[0]
1374
- ).type
1375
- : null,
1376
- };
1377
-
1378
- // Add anomaly insights
1379
- if (generateInsights && anomalies.hasAnomalies) {
1380
- for (const anomaly of gameAnomalies.slice(0, 3)) {
1381
- // Top 3 anomalies
1382
- insights.push(`⚠️ Anomaly: ${anomaly.description}`);
1383
- }
1384
- }
1385
-
1386
- return {
1387
- timestamp,
1388
- turnNumber,
1389
- totalCells,
1390
- occupiedCells,
1391
- playerCount,
1392
- players: playerSnapshots,
1393
- territoryStats,
1394
- inequality: {
1395
- gini,
1396
- theil,
1397
- atkinson,
1398
- herfindahl,
1399
- paretoRatio: pareto.ratioHeld,
1400
- zipfCoefficient: zipf,
1401
- interpretation: inequalityInterpretation,
1402
- },
1403
- diversity: {
1404
- shannon,
1405
- normalized,
1406
- renyi,
1407
- tsallis,
1408
- interpretation: diversityInterpretation,
1409
- },
1410
- distribution: {
1411
- mean,
1412
- median,
1413
- stdDev,
1414
- skewness,
1415
- kurtosis,
1416
- coefficientOfVariation: mean > 0 ? stdDev / mean : 0,
1417
- iqr: q3 - q1,
1418
- min: sorted[0] ?? 0,
1419
- max: sorted[sorted.length - 1] ?? 0,
1420
- range: (sorted[sorted.length - 1] ?? 0) - (sorted[0] ?? 0),
1421
- },
1422
- indices: {
1423
- dominance,
1424
- volatility,
1425
- predictability,
1426
- competitiveness,
1427
- stability,
1428
- },
1429
- predictions: {
1430
- likelyWinner,
1431
- winnerConfidence,
1432
- estimatedTurnsToVictory,
1433
- isEndgame,
1434
- secondPlaceChallenger,
1435
- comebackPossibility,
1436
- },
1437
- anomalies,
1438
- probability,
1439
- topology: {
1440
- totalRegions,
1441
- averageRegionSize,
1442
- territoryFragmentation,
1443
- borderCellPercentage,
1444
- avgCompactness,
1445
- },
1446
- timeSeries: {
1447
- overallTrend,
1448
- changePoints,
1449
- trendStrength,
1450
- autocorrelation,
1451
- seasonality,
1452
- },
1453
- comparisons: {
1454
- vs5TurnsAgo,
1455
- vs10TurnsAgo,
1456
- divergenceFromUniform,
1457
- divergenceFromPrevious,
1458
- },
1459
- insights,
1460
- };
1461
- }
1462
-
1463
- /**
1464
- * Format snapshot as human-readable text
1465
- */
1466
- export function formatSnapshotAsText(snapshot: GameSnapshot): string {
1467
- const lines: string[] = [];
1468
-
1469
- lines.push('═══════════════════════════════════════════════════════════');
1470
- lines.push(` GAME SNAPSHOT - Turn ${snapshot.turnNumber}`);
1471
- lines.push('═══════════════════════════════════════════════════════════');
1472
- lines.push('');
1473
-
1474
- lines.push(
1475
- `📊 Territory: ${snapshot.occupiedCells}/${snapshot.totalCells} cells occupied`
1476
- );
1477
- lines.push(`👥 Players: ${snapshot.playerCount}`);
1478
- lines.push('');
1479
-
1480
- lines.push('┌─────────────────────────────────────────────────────────┐');
1481
- lines.push('│ PLAYER STANDINGS │');
1482
- lines.push('├─────────────────────────────────────────────────────────┤');
1483
-
1484
- for (const player of snapshot.players) {
1485
- const bar = '█'.repeat(Math.ceil(player.shareOfTotal * 20));
1486
- const pad = ' '.repeat(20 - bar.length);
1487
- const trend =
1488
- player.recentTrend === 'growing'
1489
- ? '↑'
1490
- : player.recentTrend === 'shrinking'
1491
- ? '↓'
1492
- : '→';
1493
-
1494
- lines.push(
1495
- `│ #${player.rank} Player ${player.id}: ${player.cellCount} cells (${(
1496
- player.shareOfTotal * 100
1497
- ).toFixed(1)}%)`
1498
- );
1499
- lines.push(`│ ${bar}${pad} ${trend}`);
1500
- lines.push(
1501
- `│ Win Prob: ${(player.winProbability * 100).toFixed(
1502
- 1
1503
- )}% Sparkline: ${player.sparklineAscii}`
1504
- );
1505
- lines.push('│');
1506
- }
1507
-
1508
- lines.push('└─────────────────────────────────────────────────────────┘');
1509
- lines.push('');
1510
-
1511
- lines.push('┌─────────────────────────────────────────────────────────┐');
1512
- lines.push('│ GAME STATE │');
1513
- lines.push('├─────────────────────────────────────────────────────────┤');
1514
- lines.push(
1515
- `│ Dominance: ${progressBar(snapshot.indices.dominance)} ${(
1516
- snapshot.indices.dominance * 100
1517
- ).toFixed(0)}%`
1518
- );
1519
- lines.push(
1520
- `│ Volatility: ${progressBar(snapshot.indices.volatility)} ${(
1521
- snapshot.indices.volatility * 100
1522
- ).toFixed(0)}%`
1523
- );
1524
- lines.push(
1525
- `│ Competitiveness:${progressBar(snapshot.indices.competitiveness)} ${(
1526
- snapshot.indices.competitiveness * 100
1527
- ).toFixed(0)}%`
1528
- );
1529
- lines.push(
1530
- `│ Stability: ${progressBar(snapshot.indices.stability)} ${(
1531
- snapshot.indices.stability * 100
1532
- ).toFixed(0)}%`
1533
- );
1534
- lines.push(
1535
- `│ Predictability: ${progressBar(snapshot.indices.predictability)} ${(
1536
- snapshot.indices.predictability * 100
1537
- ).toFixed(0)}%`
1538
- );
1539
- lines.push('└─────────────────────────────────────────────────────────┘');
1540
- lines.push('');
1541
-
1542
- lines.push('┌─────────────────────────────────────────────────────────┐');
1543
- lines.push('│ PREDICTIONS │');
1544
- lines.push('├─────────────────────────────────────────────────────────┤');
1545
-
1546
- if (snapshot.predictions.likelyWinner !== null) {
1547
- lines.push(`│ Likely Winner: Player ${snapshot.predictions.likelyWinner}`);
1548
- lines.push(
1549
- `│ Confidence: ${(snapshot.predictions.winnerConfidence * 100).toFixed(
1550
- 1
1551
- )}%`
1552
- );
1553
- if (snapshot.predictions.estimatedTurnsToVictory !== null) {
1554
- lines.push(
1555
- `│ Est. Victory In: ${snapshot.predictions.estimatedTurnsToVictory} turns`
1556
- );
1557
- }
1558
- } else {
1559
- lines.push('│ No clear winner predicted yet');
1560
- }
1561
-
1562
- if (snapshot.predictions.isEndgame) {
1563
- lines.push('│ ⚠️ ENDGAME DETECTED');
1564
- }
1565
-
1566
- lines.push(
1567
- `│ Comeback Chance: ${(
1568
- snapshot.predictions.comebackPossibility * 100
1569
- ).toFixed(0)}%`
1570
- );
1571
- lines.push('└─────────────────────────────────────────────────────────┘');
1572
- lines.push('');
1573
-
1574
- if (snapshot.insights.length > 0) {
1575
- lines.push('┌─────────────────────────────────────────────────────────┐');
1576
- lines.push('│ INSIGHTS │');
1577
- lines.push('├─────────────────────────────────────────────────────────┤');
1578
- for (const insight of snapshot.insights) {
1579
- lines.push(`│ ${insight}`);
1580
- }
1581
- lines.push('└─────────────────────────────────────────────────────────┘');
1582
- }
1583
-
1584
- return lines.join('\n');
1585
- }
1586
-
1587
- function progressBar(value: number, width: number = 20): string {
1588
- const filled = Math.round(value * width);
1589
- const empty = width - filled;
1590
- return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
1591
- }
1592
-
1593
- /**
1594
- * Export snapshot as JSON (removes functions)
1595
- */
1596
- export function exportSnapshotAsJSON(snapshot: GameSnapshot): string {
1597
- return JSON.stringify(
1598
- snapshot,
1599
- (key, value) => {
1600
- if (value instanceof Map) {
1601
- return Object.fromEntries(value);
1602
- }
1603
- return value;
1604
- },
1605
- 2
1606
- );
1607
- }