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