@buley/hexgrid-3d 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build_log.txt +500 -0
- package/build_src_log.txt +8 -0
- package/examples/basic-usage.tsx +19 -19
- package/package.json +1 -1
- package/public/hexgrid-worker.js +2350 -1638
- package/site/.eslintrc.json +3 -0
- package/site/DEPLOYMENT.md +196 -0
- package/site/INDEX.md +127 -0
- package/site/QUICK_START.md +86 -0
- package/site/README.md +85 -0
- package/site/SITE_SUMMARY.md +180 -0
- package/site/next.config.js +12 -0
- package/site/package.json +26 -0
- package/site/src/app/docs/page.tsx +148 -0
- package/site/src/app/examples/page.tsx +133 -0
- package/site/src/app/globals.css +160 -0
- package/site/src/app/layout.tsx +29 -0
- package/site/src/app/page.tsx +163 -0
- package/site/tsconfig.json +29 -0
- package/site/vercel.json +6 -0
- package/src/Snapshot.ts +790 -585
- package/src/adapters/DashAdapter.ts +57 -0
- package/src/adapters.ts +16 -18
- package/src/algorithms/AdvancedStatistics.ts +58 -24
- package/src/algorithms/BayesianStatistics.ts +43 -12
- package/src/algorithms/FlowField.ts +30 -6
- package/src/algorithms/FlowField3D.ts +573 -0
- package/src/algorithms/FluidSimulation.ts +19 -3
- package/src/algorithms/FluidSimulation3D.ts +664 -0
- package/src/algorithms/GraphAlgorithms.ts +19 -12
- package/src/algorithms/OutlierDetection.ts +72 -38
- package/src/algorithms/ParticleSystem.ts +12 -2
- package/src/algorithms/ParticleSystem3D.ts +567 -0
- package/src/algorithms/index.ts +14 -8
- package/src/compat.ts +10 -10
- package/src/components/HexGrid.tsx +10 -23
- package/src/components/NarrationOverlay.tsx +140 -52
- package/src/components/index.ts +2 -1
- package/src/features.ts +31 -31
- package/src/index.ts +11 -11
- package/src/lib/narration.ts +17 -0
- package/src/lib/stats-tracker.ts +25 -0
- package/src/lib/theme-colors.ts +12 -0
- package/src/math/HexCoordinates.ts +849 -4
- package/src/math/Matrix4.ts +2 -12
- package/src/math/Vector3.ts +49 -1
- package/src/math/index.ts +6 -6
- package/src/note-adapter.ts +50 -42
- package/src/ontology-adapter.ts +30 -23
- package/src/stores/uiStore.ts +34 -34
- package/src/types/shared-utils.d.ts +10 -0
- package/src/types.ts +110 -98
- package/src/utils/image-utils.ts +9 -6
- package/src/wasm/HexGridWasmWrapper.ts +436 -388
- package/src/wasm/index.ts +2 -2
- package/src/workers/hexgrid-math.ts +40 -35
- package/src/workers/hexgrid-worker.worker.ts +1992 -1018
- package/tsconfig.json +21 -14
package/src/Snapshot.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified Snapshot API
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Single comprehensive API to get ALL statistics, predictions, and insights
|
|
5
5
|
* about the current game state in one easy method call.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @module Snapshot
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* @example Basic Usage
|
|
10
10
|
* ```typescript
|
|
11
11
|
* import { generateSnapshot } from '@buley/hexgrid-3d'
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* const snapshot = generateSnapshot(cells, history, conquests, getNeighbors)
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* // Access player data
|
|
16
16
|
* console.log(snapshot.players[0].winProbability) // 0.72
|
|
17
17
|
* console.log(snapshot.players[0].sparklineAscii) // "▁▂▃▅▆█"
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
19
|
* // Access game state
|
|
20
20
|
* console.log(snapshot.indices.dominance) // 0.65
|
|
21
21
|
* console.log(snapshot.predictions.likelyWinner) // 2
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* // Get insights
|
|
24
24
|
* console.log(snapshot.insights)
|
|
25
25
|
* // ["🏆 Player 2 dominates with 65.2% of territory",
|
|
26
26
|
* // "🚀 Leader's territory growing steadily"]
|
|
27
27
|
* ```
|
|
28
|
-
*
|
|
28
|
+
*
|
|
29
29
|
* @example Full Response Object
|
|
30
30
|
* ```typescript
|
|
31
31
|
* // GameSnapshot example response:
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* totalCells: 1000,
|
|
36
36
|
* occupiedCells: 850,
|
|
37
37
|
* playerCount: 4,
|
|
38
|
-
*
|
|
38
|
+
*
|
|
39
39
|
* players: [{
|
|
40
40
|
* id: 2,
|
|
41
41
|
* cellCount: 320,
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
* leadOverSecond: 45,
|
|
65
65
|
* turnsUntilOvertake: null
|
|
66
66
|
* }, ...],
|
|
67
|
-
*
|
|
67
|
+
*
|
|
68
68
|
* inequality: {
|
|
69
69
|
* gini: 0.42,
|
|
70
70
|
* theil: 0.35,
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
* zipfCoefficient: 1.15,
|
|
75
75
|
* interpretation: "Emerging dominance"
|
|
76
76
|
* },
|
|
77
|
-
*
|
|
77
|
+
*
|
|
78
78
|
* indices: {
|
|
79
79
|
* dominance: 0.65,
|
|
80
80
|
* volatility: 0.35,
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
* competitiveness: 0.45,
|
|
83
83
|
* stability: 0.68
|
|
84
84
|
* },
|
|
85
|
-
*
|
|
85
|
+
*
|
|
86
86
|
* predictions: {
|
|
87
87
|
* likelyWinner: 2,
|
|
88
88
|
* winnerConfidence: 0.72,
|
|
@@ -91,13 +91,13 @@
|
|
|
91
91
|
* secondPlaceChallenger: 3,
|
|
92
92
|
* comebackPossibility: 0.28
|
|
93
93
|
* },
|
|
94
|
-
*
|
|
94
|
+
*
|
|
95
95
|
* anomalies: {
|
|
96
96
|
* outliers: [{ playerId: 4, type: 'growth_explosion', ... }],
|
|
97
97
|
* hasAnomalies: true,
|
|
98
98
|
* anomalyCount: 1
|
|
99
99
|
* },
|
|
100
|
-
*
|
|
100
|
+
*
|
|
101
101
|
* insights: [
|
|
102
102
|
* "📈 Player 2 leads with majority control (37.6%)",
|
|
103
103
|
* "🚀 Leader's territory growing steadily (slope: 4.2/turn)",
|
|
@@ -134,8 +134,8 @@ import {
|
|
|
134
134
|
sparkline,
|
|
135
135
|
sparklineSvg,
|
|
136
136
|
computeTerritoryStats,
|
|
137
|
-
type TerritoryStats
|
|
138
|
-
} from './algorithms/AdvancedStatistics'
|
|
137
|
+
type TerritoryStats,
|
|
138
|
+
} from './algorithms/AdvancedStatistics';
|
|
139
139
|
|
|
140
140
|
import {
|
|
141
141
|
BetaDistribution,
|
|
@@ -159,8 +159,8 @@ import {
|
|
|
159
159
|
bayesianConquestRate,
|
|
160
160
|
bayesianChangepoint,
|
|
161
161
|
generateProbabilitySnapshot,
|
|
162
|
-
type ProbabilitySnapshot
|
|
163
|
-
} from './algorithms/BayesianStatistics'
|
|
162
|
+
type ProbabilitySnapshot,
|
|
163
|
+
} from './algorithms/BayesianStatistics';
|
|
164
164
|
|
|
165
165
|
import {
|
|
166
166
|
detectOutliersZScore,
|
|
@@ -178,10 +178,13 @@ import {
|
|
|
178
178
|
type OutlierResult,
|
|
179
179
|
type TimeSeriesAnomaly,
|
|
180
180
|
type GameAnomaly,
|
|
181
|
-
type MultivariateOutlierResult
|
|
182
|
-
} from './algorithms/OutlierDetection'
|
|
181
|
+
type MultivariateOutlierResult,
|
|
182
|
+
} from './algorithms/OutlierDetection';
|
|
183
183
|
|
|
184
|
-
import {
|
|
184
|
+
import {
|
|
185
|
+
findConnectedComponents,
|
|
186
|
+
analyzeTerritorBoundaries,
|
|
187
|
+
} from './algorithms/GraphAlgorithms';
|
|
185
188
|
|
|
186
189
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
187
190
|
// UNIFIED SNAPSHOT TYPES
|
|
@@ -189,7 +192,7 @@ import { findConnectedComponents, analyzeTerritorBoundaries } from './algorithms
|
|
|
189
192
|
|
|
190
193
|
/**
|
|
191
194
|
* Player-specific statistics
|
|
192
|
-
*
|
|
195
|
+
*
|
|
193
196
|
* @example
|
|
194
197
|
* ```typescript
|
|
195
198
|
* const player: PlayerSnapshot = {
|
|
@@ -224,76 +227,76 @@ import { findConnectedComponents, analyzeTerritorBoundaries } from './algorithms
|
|
|
224
227
|
*/
|
|
225
228
|
export interface PlayerSnapshot {
|
|
226
229
|
/** Unique player identifier */
|
|
227
|
-
id: number
|
|
228
|
-
|
|
230
|
+
id: number;
|
|
231
|
+
|
|
229
232
|
// Territory
|
|
230
233
|
/** Current number of cells owned */
|
|
231
|
-
cellCount: number
|
|
234
|
+
cellCount: number;
|
|
232
235
|
/** Fraction of total occupied cells (0-1) */
|
|
233
|
-
shareOfTotal: number
|
|
236
|
+
shareOfTotal: number;
|
|
234
237
|
/** Current ranking (1 = leader) */
|
|
235
|
-
rank: number
|
|
236
|
-
|
|
238
|
+
rank: number;
|
|
239
|
+
|
|
237
240
|
// History
|
|
238
241
|
/** Number of turns of history available */
|
|
239
|
-
historyLength: number
|
|
242
|
+
historyLength: number;
|
|
240
243
|
/** Territory count history (last 20 turns unless includeFullHistory=true) */
|
|
241
|
-
history: number[]
|
|
244
|
+
history: number[];
|
|
242
245
|
/** Recent territory trend direction */
|
|
243
|
-
recentTrend: 'growing' | 'shrinking' | 'stable'
|
|
246
|
+
recentTrend: 'growing' | 'shrinking' | 'stable';
|
|
244
247
|
/** Linear regression slope (cells per turn) */
|
|
245
|
-
trendSlope: number
|
|
248
|
+
trendSlope: number;
|
|
246
249
|
/** R² confidence in trend (0-1) */
|
|
247
|
-
trendConfidence: number
|
|
248
|
-
|
|
250
|
+
trendConfidence: number;
|
|
251
|
+
|
|
249
252
|
// Predictions
|
|
250
253
|
/** Bayesian probability of winning (0-1) */
|
|
251
|
-
winProbability: number
|
|
254
|
+
winProbability: number;
|
|
252
255
|
/** 95% credible interval for win probability */
|
|
253
|
-
winCredibleInterval: [number, number]
|
|
256
|
+
winCredibleInterval: [number, number];
|
|
254
257
|
/** Forecasted territory for next 10 turns */
|
|
255
|
-
forecastNext10: number[]
|
|
258
|
+
forecastNext10: number[];
|
|
256
259
|
/** Kalman filter smoothed estimate */
|
|
257
|
-
kalmanEstimate: number
|
|
260
|
+
kalmanEstimate: number;
|
|
258
261
|
/** Kalman filter uncertainty (±) */
|
|
259
|
-
kalmanUncertainty: number
|
|
260
|
-
|
|
262
|
+
kalmanUncertainty: number;
|
|
263
|
+
|
|
261
264
|
// Performance
|
|
262
265
|
/** Bayesian conquest success rate */
|
|
263
|
-
conquestRate: number
|
|
266
|
+
conquestRate: number;
|
|
264
267
|
/** 95% credible interval for conquest rate */
|
|
265
|
-
conquestRateCI: [number, number]
|
|
268
|
+
conquestRateCI: [number, number];
|
|
266
269
|
/** Average cells gained per turn */
|
|
267
|
-
avgGrowthPerTurn: number
|
|
270
|
+
avgGrowthPerTurn: number;
|
|
268
271
|
/** Territory volatility (coefficient of variation) */
|
|
269
|
-
volatility: number
|
|
270
|
-
|
|
272
|
+
volatility: number;
|
|
273
|
+
|
|
271
274
|
// Topology
|
|
272
275
|
/** Number of disconnected territory regions */
|
|
273
|
-
numRegions: number
|
|
276
|
+
numRegions: number;
|
|
274
277
|
/** Size of largest connected region */
|
|
275
|
-
largestRegionSize: number
|
|
278
|
+
largestRegionSize: number;
|
|
276
279
|
/** Number of cells on territory border */
|
|
277
|
-
borderCellCount: number
|
|
280
|
+
borderCellCount: number;
|
|
278
281
|
/** Territory shape compactness (0-1, higher = more compact) */
|
|
279
|
-
compactness: number
|
|
280
|
-
|
|
282
|
+
compactness: number;
|
|
283
|
+
|
|
281
284
|
// Sparklines
|
|
282
285
|
/** ASCII sparkline visualization of history */
|
|
283
|
-
sparklineAscii: string
|
|
286
|
+
sparklineAscii: string;
|
|
284
287
|
/** SVG path data for sparkline */
|
|
285
|
-
sparklineSvgPath: string
|
|
286
|
-
|
|
288
|
+
sparklineSvgPath: string;
|
|
289
|
+
|
|
287
290
|
// Relative metrics
|
|
288
291
|
/** Lead over second place (null if not leader) */
|
|
289
|
-
leadOverSecond: number | null
|
|
292
|
+
leadOverSecond: number | null;
|
|
290
293
|
/** Estimated turns until this player could overtake leader */
|
|
291
|
-
turnsUntilOvertake: number | null
|
|
294
|
+
turnsUntilOvertake: number | null;
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
/**
|
|
295
298
|
* Inequality metrics for territory distribution
|
|
296
|
-
*
|
|
299
|
+
*
|
|
297
300
|
* @example
|
|
298
301
|
* ```typescript
|
|
299
302
|
* const inequality: InequalityMetrics = {
|
|
@@ -308,18 +311,18 @@ export interface PlayerSnapshot {
|
|
|
308
311
|
* ```
|
|
309
312
|
*/
|
|
310
313
|
export interface InequalityMetrics {
|
|
311
|
-
gini: number
|
|
312
|
-
theil: number
|
|
313
|
-
atkinson: number
|
|
314
|
-
herfindahl: number
|
|
315
|
-
paretoRatio: number
|
|
316
|
-
zipfCoefficient: number
|
|
317
|
-
interpretation: string
|
|
314
|
+
gini: number;
|
|
315
|
+
theil: number;
|
|
316
|
+
atkinson: number;
|
|
317
|
+
herfindahl: number;
|
|
318
|
+
paretoRatio: number;
|
|
319
|
+
zipfCoefficient: number;
|
|
320
|
+
interpretation: string;
|
|
318
321
|
}
|
|
319
322
|
|
|
320
323
|
/**
|
|
321
324
|
* Diversity/entropy metrics for territory distribution
|
|
322
|
-
*
|
|
325
|
+
*
|
|
323
326
|
* @example
|
|
324
327
|
* ```typescript
|
|
325
328
|
* const diversity: DiversityMetrics = {
|
|
@@ -332,16 +335,16 @@ export interface InequalityMetrics {
|
|
|
332
335
|
* ```
|
|
333
336
|
*/
|
|
334
337
|
export interface DiversityMetrics {
|
|
335
|
-
shannon: number
|
|
336
|
-
normalized: number
|
|
337
|
-
renyi: number
|
|
338
|
-
tsallis: number
|
|
339
|
-
interpretation: string
|
|
338
|
+
shannon: number;
|
|
339
|
+
normalized: number;
|
|
340
|
+
renyi: number;
|
|
341
|
+
tsallis: number;
|
|
342
|
+
interpretation: string;
|
|
340
343
|
}
|
|
341
344
|
|
|
342
345
|
/**
|
|
343
346
|
* Distribution statistics for territory counts
|
|
344
|
-
*
|
|
347
|
+
*
|
|
345
348
|
* @example
|
|
346
349
|
* ```typescript
|
|
347
350
|
* const distribution: DistributionMetrics = {
|
|
@@ -359,21 +362,21 @@ export interface DiversityMetrics {
|
|
|
359
362
|
* ```
|
|
360
363
|
*/
|
|
361
364
|
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
|
|
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;
|
|
372
375
|
}
|
|
373
376
|
|
|
374
377
|
/**
|
|
375
378
|
* Game state indices (0-1 scale)
|
|
376
|
-
*
|
|
379
|
+
*
|
|
377
380
|
* @example
|
|
378
381
|
* ```typescript
|
|
379
382
|
* const indices: GameIndices = {
|
|
@@ -387,20 +390,20 @@ export interface DistributionMetrics {
|
|
|
387
390
|
*/
|
|
388
391
|
export interface GameIndices {
|
|
389
392
|
/** How dominated by a single player (0=even, 1=total domination) */
|
|
390
|
-
dominance: number
|
|
393
|
+
dominance: number;
|
|
391
394
|
/** How much territory changes each turn (0=static, 1=chaotic) */
|
|
392
|
-
volatility: number
|
|
395
|
+
volatility: number;
|
|
393
396
|
/** How predictable outcomes are (0=random, 1=deterministic) */
|
|
394
|
-
predictability: number
|
|
397
|
+
predictability: number;
|
|
395
398
|
/** How close the competition (0=decided, 1=neck-and-neck) */
|
|
396
|
-
competitiveness: number
|
|
399
|
+
competitiveness: number;
|
|
397
400
|
/** How stable territory boundaries are (0=fluid, 1=locked) */
|
|
398
|
-
stability: number
|
|
401
|
+
stability: number;
|
|
399
402
|
}
|
|
400
403
|
|
|
401
404
|
/**
|
|
402
405
|
* Game outcome predictions
|
|
403
|
-
*
|
|
406
|
+
*
|
|
404
407
|
* @example
|
|
405
408
|
* ```typescript
|
|
406
409
|
* const predictions: GamePredictions = {
|
|
@@ -415,22 +418,22 @@ export interface GameIndices {
|
|
|
415
418
|
*/
|
|
416
419
|
export interface GamePredictions {
|
|
417
420
|
/** Player ID most likely to win */
|
|
418
|
-
likelyWinner: number | null
|
|
421
|
+
likelyWinner: number | null;
|
|
419
422
|
/** Confidence in winner prediction (0-1) */
|
|
420
|
-
winnerConfidence: number
|
|
423
|
+
winnerConfidence: number;
|
|
421
424
|
/** Estimated turns until victory condition */
|
|
422
|
-
estimatedTurnsToVictory: number | null
|
|
425
|
+
estimatedTurnsToVictory: number | null;
|
|
423
426
|
/** Whether game is in endgame phase */
|
|
424
|
-
isEndgame: boolean
|
|
427
|
+
isEndgame: boolean;
|
|
425
428
|
/** Second place player who could challenge */
|
|
426
|
-
secondPlaceChallenger: number | null
|
|
429
|
+
secondPlaceChallenger: number | null;
|
|
427
430
|
/** Probability of comeback by non-leader */
|
|
428
|
-
comebackPossibility: number
|
|
431
|
+
comebackPossibility: number;
|
|
429
432
|
}
|
|
430
433
|
|
|
431
434
|
/**
|
|
432
435
|
* Territory topology metrics
|
|
433
|
-
*
|
|
436
|
+
*
|
|
434
437
|
* @example
|
|
435
438
|
* ```typescript
|
|
436
439
|
* const topology: TopologyMetrics = {
|
|
@@ -444,20 +447,20 @@ export interface GamePredictions {
|
|
|
444
447
|
*/
|
|
445
448
|
export interface TopologyMetrics {
|
|
446
449
|
/** Total disconnected regions across all players */
|
|
447
|
-
totalRegions: number
|
|
450
|
+
totalRegions: number;
|
|
448
451
|
/** Average cells per region */
|
|
449
|
-
averageRegionSize: number
|
|
452
|
+
averageRegionSize: number;
|
|
450
453
|
/** How fragmented territories are (0=solid, 1=scattered) */
|
|
451
|
-
territoryFragmentation: number
|
|
454
|
+
territoryFragmentation: number;
|
|
452
455
|
/** Percentage of cells that are border cells */
|
|
453
|
-
borderCellPercentage: number
|
|
456
|
+
borderCellPercentage: number;
|
|
454
457
|
/** Average compactness across all territories */
|
|
455
|
-
avgCompactness: number
|
|
458
|
+
avgCompactness: number;
|
|
456
459
|
}
|
|
457
460
|
|
|
458
461
|
/**
|
|
459
462
|
* Time series analysis metrics
|
|
460
|
-
*
|
|
463
|
+
*
|
|
461
464
|
* @example
|
|
462
465
|
* ```typescript
|
|
463
466
|
* const timeSeries: TimeSeriesMetrics = {
|
|
@@ -471,20 +474,20 @@ export interface TopologyMetrics {
|
|
|
471
474
|
*/
|
|
472
475
|
export interface TimeSeriesMetrics {
|
|
473
476
|
/** Overall trend type */
|
|
474
|
-
overallTrend: 'convergent' | 'divergent' | 'cyclical' | 'chaotic'
|
|
477
|
+
overallTrend: 'convergent' | 'divergent' | 'cyclical' | 'chaotic';
|
|
475
478
|
/** Turn numbers where significant changes occurred */
|
|
476
|
-
changePoints: number[]
|
|
479
|
+
changePoints: number[];
|
|
477
480
|
/** Strength of overall trend (0-1) */
|
|
478
|
-
trendStrength: number
|
|
481
|
+
trendStrength: number;
|
|
479
482
|
/** Autocorrelation coefficient */
|
|
480
|
-
autocorrelation: number
|
|
483
|
+
autocorrelation: number;
|
|
481
484
|
/** Whether periodic patterns detected */
|
|
482
|
-
seasonality: boolean
|
|
485
|
+
seasonality: boolean;
|
|
483
486
|
}
|
|
484
487
|
|
|
485
488
|
/**
|
|
486
489
|
* Comparison metrics with past states
|
|
487
|
-
*
|
|
490
|
+
*
|
|
488
491
|
* @example
|
|
489
492
|
* ```typescript
|
|
490
493
|
* const comparisons: ComparisonMetrics = {
|
|
@@ -497,18 +500,18 @@ export interface TimeSeriesMetrics {
|
|
|
497
500
|
*/
|
|
498
501
|
export interface ComparisonMetrics {
|
|
499
502
|
/** Territory change per player vs 5 turns ago */
|
|
500
|
-
vs5TurnsAgo: { [playerId: number]: number }
|
|
503
|
+
vs5TurnsAgo: { [playerId: number]: number };
|
|
501
504
|
/** Territory change per player vs 10 turns ago */
|
|
502
|
-
vs10TurnsAgo: { [playerId: number]: number }
|
|
505
|
+
vs10TurnsAgo: { [playerId: number]: number };
|
|
503
506
|
/** KL divergence from uniform distribution */
|
|
504
|
-
divergenceFromUniform: number
|
|
507
|
+
divergenceFromUniform: number;
|
|
505
508
|
/** JS divergence from previous turn */
|
|
506
|
-
divergenceFromPrevious: number
|
|
509
|
+
divergenceFromPrevious: number;
|
|
507
510
|
}
|
|
508
511
|
|
|
509
512
|
/**
|
|
510
513
|
* Anomaly detection summary
|
|
511
|
-
*
|
|
514
|
+
*
|
|
512
515
|
* @example
|
|
513
516
|
* ```typescript
|
|
514
517
|
* const anomalies: AnomalySummary = {
|
|
@@ -528,18 +531,18 @@ export interface ComparisonMetrics {
|
|
|
528
531
|
*/
|
|
529
532
|
export interface AnomalySummary {
|
|
530
533
|
/** Detected game anomalies */
|
|
531
|
-
outliers: GameAnomaly[]
|
|
534
|
+
outliers: GameAnomaly[];
|
|
532
535
|
/** Whether any anomalies were detected */
|
|
533
|
-
hasAnomalies: boolean
|
|
536
|
+
hasAnomalies: boolean;
|
|
534
537
|
/** Total count of anomalies */
|
|
535
|
-
anomalyCount: number
|
|
538
|
+
anomalyCount: number;
|
|
536
539
|
/** Type of most severe anomaly (if any) */
|
|
537
|
-
mostSevere: string | null
|
|
540
|
+
mostSevere: string | null;
|
|
538
541
|
}
|
|
539
542
|
|
|
540
543
|
/**
|
|
541
544
|
* Complete game snapshot with all statistics
|
|
542
|
-
*
|
|
545
|
+
*
|
|
543
546
|
* @example Full Response Structure
|
|
544
547
|
* ```typescript
|
|
545
548
|
* const snapshot: GameSnapshot = {
|
|
@@ -548,16 +551,16 @@ export interface AnomalySummary {
|
|
|
548
551
|
* totalCells: 1000,
|
|
549
552
|
* occupiedCells: 850,
|
|
550
553
|
* playerCount: 4,
|
|
551
|
-
*
|
|
554
|
+
*
|
|
552
555
|
* players: [
|
|
553
556
|
* { id: 2, cellCount: 320, shareOfTotal: 0.376, rank: 1, ... },
|
|
554
557
|
* { id: 1, cellCount: 210, shareOfTotal: 0.247, rank: 2, ... },
|
|
555
558
|
* { id: 3, cellCount: 180, shareOfTotal: 0.212, rank: 3, ... },
|
|
556
559
|
* { id: 4, cellCount: 140, shareOfTotal: 0.165, rank: 4, ... }
|
|
557
560
|
* ],
|
|
558
|
-
*
|
|
561
|
+
*
|
|
559
562
|
* territoryStats: { ... },
|
|
560
|
-
*
|
|
563
|
+
*
|
|
561
564
|
* inequality: {
|
|
562
565
|
* gini: 0.42,
|
|
563
566
|
* theil: 0.35,
|
|
@@ -567,7 +570,7 @@ export interface AnomalySummary {
|
|
|
567
570
|
* zipfCoefficient: 1.15,
|
|
568
571
|
* interpretation: "Emerging dominance"
|
|
569
572
|
* },
|
|
570
|
-
*
|
|
573
|
+
*
|
|
571
574
|
* diversity: {
|
|
572
575
|
* shannon: 1.85,
|
|
573
576
|
* normalized: 0.92,
|
|
@@ -575,14 +578,14 @@ export interface AnomalySummary {
|
|
|
575
578
|
* tsallis: 1.52,
|
|
576
579
|
* interpretation: "High diversity"
|
|
577
580
|
* },
|
|
578
|
-
*
|
|
581
|
+
*
|
|
579
582
|
* distribution: {
|
|
580
583
|
* mean: 212.5, median: 195, stdDev: 68.2,
|
|
581
584
|
* skewness: 0.65, kurtosis: 2.8,
|
|
582
585
|
* coefficientOfVariation: 0.32,
|
|
583
586
|
* iqr: 95, min: 140, max: 320, range: 180
|
|
584
587
|
* },
|
|
585
|
-
*
|
|
588
|
+
*
|
|
586
589
|
* indices: {
|
|
587
590
|
* dominance: 0.65,
|
|
588
591
|
* volatility: 0.35,
|
|
@@ -590,7 +593,7 @@ export interface AnomalySummary {
|
|
|
590
593
|
* competitiveness: 0.45,
|
|
591
594
|
* stability: 0.68
|
|
592
595
|
* },
|
|
593
|
-
*
|
|
596
|
+
*
|
|
594
597
|
* predictions: {
|
|
595
598
|
* likelyWinner: 2,
|
|
596
599
|
* winnerConfidence: 0.72,
|
|
@@ -599,19 +602,19 @@ export interface AnomalySummary {
|
|
|
599
602
|
* secondPlaceChallenger: 1,
|
|
600
603
|
* comebackPossibility: 0.28
|
|
601
604
|
* },
|
|
602
|
-
*
|
|
605
|
+
*
|
|
603
606
|
* anomalies: {
|
|
604
607
|
* outliers: [],
|
|
605
608
|
* hasAnomalies: false,
|
|
606
609
|
* anomalyCount: 0,
|
|
607
610
|
* mostSevere: null
|
|
608
611
|
* },
|
|
609
|
-
*
|
|
612
|
+
*
|
|
610
613
|
* probability: { winProbabilities: Map, ... },
|
|
611
614
|
* topology: { totalRegions: 8, ... },
|
|
612
615
|
* timeSeries: { overallTrend: 'divergent', ... },
|
|
613
616
|
* comparisons: { vs5TurnsAgo: {...}, ... },
|
|
614
|
-
*
|
|
617
|
+
*
|
|
615
618
|
* insights: [
|
|
616
619
|
* "📈 Player 2 leads with 37.6% of territory",
|
|
617
620
|
* "🚀 Leader's territory growing steadily",
|
|
@@ -623,67 +626,67 @@ export interface AnomalySummary {
|
|
|
623
626
|
export interface GameSnapshot {
|
|
624
627
|
// Basic
|
|
625
628
|
/** Unix timestamp when snapshot was generated */
|
|
626
|
-
timestamp: number
|
|
629
|
+
timestamp: number;
|
|
627
630
|
/** Current turn number */
|
|
628
|
-
turnNumber: number
|
|
631
|
+
turnNumber: number;
|
|
629
632
|
/** Total cells on the grid */
|
|
630
|
-
totalCells: number
|
|
633
|
+
totalCells: number;
|
|
631
634
|
/** Number of cells with an owner */
|
|
632
|
-
occupiedCells: number
|
|
633
|
-
|
|
635
|
+
occupiedCells: number;
|
|
636
|
+
|
|
634
637
|
// Players
|
|
635
638
|
/** Number of active players */
|
|
636
|
-
playerCount: number
|
|
639
|
+
playerCount: number;
|
|
637
640
|
/** Detailed stats for each player, sorted by rank */
|
|
638
|
-
players: PlayerSnapshot[]
|
|
639
|
-
|
|
641
|
+
players: PlayerSnapshot[];
|
|
642
|
+
|
|
640
643
|
// Overall territory stats
|
|
641
644
|
/** Aggregate territory statistics */
|
|
642
|
-
territoryStats: TerritoryStats
|
|
643
|
-
|
|
645
|
+
territoryStats: TerritoryStats;
|
|
646
|
+
|
|
644
647
|
// Inequality metrics (all players)
|
|
645
648
|
/** Inequality measures for territory distribution */
|
|
646
|
-
inequality: InequalityMetrics
|
|
647
|
-
|
|
649
|
+
inequality: InequalityMetrics;
|
|
650
|
+
|
|
648
651
|
// Entropy/diversity
|
|
649
652
|
/** Diversity/entropy measures */
|
|
650
|
-
diversity: DiversityMetrics
|
|
651
|
-
|
|
653
|
+
diversity: DiversityMetrics;
|
|
654
|
+
|
|
652
655
|
// Distribution metrics
|
|
653
656
|
/** Statistical distribution of territory counts */
|
|
654
|
-
distribution: DistributionMetrics
|
|
655
|
-
|
|
657
|
+
distribution: DistributionMetrics;
|
|
658
|
+
|
|
656
659
|
// Game state indices
|
|
657
660
|
/** Normalized game state indicators */
|
|
658
|
-
indices: GameIndices
|
|
659
|
-
|
|
661
|
+
indices: GameIndices;
|
|
662
|
+
|
|
660
663
|
// Predictions
|
|
661
664
|
/** Game outcome predictions */
|
|
662
|
-
predictions: GamePredictions
|
|
663
|
-
|
|
665
|
+
predictions: GamePredictions;
|
|
666
|
+
|
|
664
667
|
// Anomaly detection
|
|
665
668
|
/** Detected anomalies and outliers */
|
|
666
|
-
anomalies: AnomalySummary
|
|
667
|
-
|
|
669
|
+
anomalies: AnomalySummary;
|
|
670
|
+
|
|
668
671
|
// Probability snapshot (Bayesian)
|
|
669
672
|
/** Full Bayesian probability analysis */
|
|
670
|
-
probability: ProbabilitySnapshot
|
|
671
|
-
|
|
673
|
+
probability: ProbabilitySnapshot;
|
|
674
|
+
|
|
672
675
|
// Topology
|
|
673
676
|
/** Territory topology metrics */
|
|
674
|
-
topology: TopologyMetrics
|
|
675
|
-
|
|
677
|
+
topology: TopologyMetrics;
|
|
678
|
+
|
|
676
679
|
// Time series insights
|
|
677
680
|
/** Time series analysis */
|
|
678
|
-
timeSeries: TimeSeriesMetrics
|
|
679
|
-
|
|
681
|
+
timeSeries: TimeSeriesMetrics;
|
|
682
|
+
|
|
680
683
|
// Comparisons
|
|
681
684
|
/** Comparisons with past states */
|
|
682
|
-
comparisons: ComparisonMetrics
|
|
683
|
-
|
|
685
|
+
comparisons: ComparisonMetrics;
|
|
686
|
+
|
|
684
687
|
// Recommendations / insights
|
|
685
688
|
/** Human-readable insights and observations */
|
|
686
|
-
insights: string[]
|
|
689
|
+
insights: string[];
|
|
687
690
|
}
|
|
688
691
|
|
|
689
692
|
// Re-export all types for easy importing
|
|
@@ -693,8 +696,8 @@ export type {
|
|
|
693
696
|
OutlierResult,
|
|
694
697
|
TimeSeriesAnomaly,
|
|
695
698
|
GameAnomaly,
|
|
696
|
-
MultivariateOutlierResult
|
|
697
|
-
}
|
|
699
|
+
MultivariateOutlierResult,
|
|
700
|
+
};
|
|
698
701
|
|
|
699
702
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
700
703
|
// SNAPSHOT GENERATOR
|
|
@@ -702,15 +705,15 @@ export type {
|
|
|
702
705
|
|
|
703
706
|
export interface SnapshotConfig {
|
|
704
707
|
/** Number of turns for forecasting */
|
|
705
|
-
forecastHorizon?: number
|
|
708
|
+
forecastHorizon?: number;
|
|
706
709
|
/** Number of Monte Carlo samples */
|
|
707
|
-
monteCarloSamples?: number
|
|
710
|
+
monteCarloSamples?: number;
|
|
708
711
|
/** Include detailed history */
|
|
709
|
-
includeFullHistory?: boolean
|
|
712
|
+
includeFullHistory?: boolean;
|
|
710
713
|
/** Calculate topology (can be expensive) */
|
|
711
|
-
calculateTopology?: boolean
|
|
714
|
+
calculateTopology?: boolean;
|
|
712
715
|
/** Generate insights */
|
|
713
|
-
generateInsights?: boolean
|
|
716
|
+
generateInsights?: boolean;
|
|
714
717
|
}
|
|
715
718
|
|
|
716
719
|
/**
|
|
@@ -728,174 +731,220 @@ export function generateSnapshot(
|
|
|
728
731
|
monteCarloSamples = 1000,
|
|
729
732
|
includeFullHistory = false,
|
|
730
733
|
calculateTopology = true,
|
|
731
|
-
generateInsights = true
|
|
732
|
-
} = config
|
|
733
|
-
|
|
734
|
-
const timestamp = Date.now()
|
|
735
|
-
const totalCells = cells.length
|
|
736
|
-
|
|
734
|
+
generateInsights = true,
|
|
735
|
+
} = config;
|
|
736
|
+
|
|
737
|
+
const timestamp = Date.now();
|
|
738
|
+
const totalCells = cells.length;
|
|
739
|
+
|
|
737
740
|
// Count territories
|
|
738
|
-
const territoryCounts = new Map<number, number>()
|
|
741
|
+
const territoryCounts = new Map<number, number>();
|
|
739
742
|
for (const cell of cells) {
|
|
740
743
|
if (cell.owner !== 0) {
|
|
741
|
-
territoryCounts.set(
|
|
744
|
+
territoryCounts.set(
|
|
745
|
+
cell.owner,
|
|
746
|
+
(territoryCounts.get(cell.owner) ?? 0) + 1
|
|
747
|
+
);
|
|
742
748
|
}
|
|
743
749
|
}
|
|
744
|
-
|
|
745
|
-
const occupiedCells = Array.from(territoryCounts.values()).reduce(
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
|
|
749
758
|
// Sort by territory size for ranking
|
|
750
|
-
const sortedBySize = [...players].sort(
|
|
751
|
-
(territoryCounts.get(b) ?? 0) - (territoryCounts.get(a) ?? 0)
|
|
752
|
-
)
|
|
753
|
-
|
|
759
|
+
const sortedBySize = [...players].sort(
|
|
760
|
+
(a, b) => (territoryCounts.get(b) ?? 0) - (territoryCounts.get(a) ?? 0)
|
|
761
|
+
);
|
|
762
|
+
|
|
754
763
|
// Compute turn number from history length
|
|
755
|
-
const turnNumber = Math.max(
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
//
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
+
|
|
766
783
|
// Compute player snapshots
|
|
767
|
-
const playerSnapshots: PlayerSnapshot[] = []
|
|
768
|
-
|
|
784
|
+
const playerSnapshots: PlayerSnapshot[] = [];
|
|
785
|
+
|
|
769
786
|
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
|
-
|
|
787
|
+
const playerId = sortedBySize[rankIdx];
|
|
788
|
+
const cellCount = territoryCounts.get(playerId) ?? 0;
|
|
789
|
+
const history = territoryHistory.get(playerId) ?? [];
|
|
790
|
+
|
|
774
791
|
// Trend detection
|
|
775
|
-
const trend = history.length > 3 ? detectTrend(history) : null
|
|
776
|
-
|
|
777
|
-
// Forecast
|
|
778
|
-
const
|
|
779
|
-
? doubleExponentialSmoothing(history, 0.3, 0.1)
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
+
|
|
783
799
|
// Kalman filter
|
|
784
|
-
let kalmanEstimate = cellCount
|
|
785
|
-
let kalmanUncertainty = 0
|
|
800
|
+
let kalmanEstimate = cellCount;
|
|
801
|
+
let kalmanUncertainty = 0;
|
|
786
802
|
if (history.length > 3) {
|
|
787
|
-
const variance =
|
|
788
|
-
sum + (v - history[i]) ** 2, 0) /
|
|
789
|
-
|
|
803
|
+
const variance =
|
|
804
|
+
history.slice(1).reduce((sum, v, i) => sum + (v - history[i]) ** 2, 0) /
|
|
805
|
+
history.length;
|
|
806
|
+
|
|
790
807
|
const filter = new KalmanFilter(
|
|
791
808
|
history[0],
|
|
792
809
|
variance || 1,
|
|
793
810
|
(variance || 1) * 0.1,
|
|
794
811
|
(variance || 1) * 0.5
|
|
795
|
-
)
|
|
796
|
-
|
|
812
|
+
);
|
|
813
|
+
|
|
797
814
|
for (const measurement of history) {
|
|
798
|
-
filter.
|
|
815
|
+
filter.update(measurement);
|
|
799
816
|
}
|
|
800
|
-
|
|
801
|
-
kalmanEstimate = filter.getState()
|
|
802
|
-
kalmanUncertainty = filter.getUncertainty()
|
|
817
|
+
|
|
818
|
+
kalmanEstimate = filter.getState();
|
|
819
|
+
kalmanUncertainty = filter.getUncertainty();
|
|
803
820
|
}
|
|
804
|
-
|
|
805
|
-
// Win probability
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
|
|
813
846
|
// Volatility
|
|
814
|
-
const volatility =
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
+
|
|
819
857
|
// Avg growth
|
|
820
|
-
const avgGrowth =
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
858
|
+
const avgGrowth =
|
|
859
|
+
history.length > 1
|
|
860
|
+
? (history[history.length - 1] - history[0]) / history.length
|
|
861
|
+
: 0;
|
|
862
|
+
|
|
824
863
|
// Topology
|
|
825
|
-
let numRegions = 1
|
|
826
|
-
let largestRegionSize = cellCount
|
|
827
|
-
let borderCellCount = 0
|
|
828
|
-
let playerCompactness = 0
|
|
829
|
-
|
|
864
|
+
let numRegions = 1;
|
|
865
|
+
let largestRegionSize = cellCount;
|
|
866
|
+
let borderCellCount = 0;
|
|
867
|
+
let playerCompactness = 0;
|
|
868
|
+
|
|
830
869
|
if (calculateTopology && cellCount > 0) {
|
|
831
|
-
const playerCells = new Set<number>()
|
|
870
|
+
const playerCells = new Set<number>();
|
|
832
871
|
cells.forEach((cell, idx) => {
|
|
833
|
-
if (cell.owner === playerId) playerCells.add(idx)
|
|
834
|
-
})
|
|
835
|
-
|
|
872
|
+
if (cell.owner === playerId) playerCells.add(idx);
|
|
873
|
+
});
|
|
874
|
+
|
|
836
875
|
// Count regions using BFS
|
|
837
|
-
const visited = new Set<number>()
|
|
838
|
-
const regionSizes: number[] = []
|
|
839
|
-
|
|
876
|
+
const visited = new Set<number>();
|
|
877
|
+
const regionSizes: number[] = [];
|
|
878
|
+
|
|
840
879
|
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
|
-
|
|
880
|
+
if (visited.has(cellIdx)) continue;
|
|
881
|
+
|
|
882
|
+
const queue = [cellIdx];
|
|
883
|
+
visited.add(cellIdx);
|
|
884
|
+
let regionSize = 0;
|
|
885
|
+
|
|
847
886
|
while (queue.length > 0) {
|
|
848
|
-
const current = queue.shift()
|
|
849
|
-
regionSize
|
|
850
|
-
|
|
887
|
+
const current = queue.shift()!;
|
|
888
|
+
regionSize++;
|
|
889
|
+
|
|
851
890
|
for (const neighbor of getNeighbors(current)) {
|
|
852
891
|
if (playerCells.has(neighbor) && !visited.has(neighbor)) {
|
|
853
|
-
visited.add(neighbor)
|
|
854
|
-
queue.push(neighbor)
|
|
892
|
+
visited.add(neighbor);
|
|
893
|
+
queue.push(neighbor);
|
|
855
894
|
}
|
|
856
895
|
}
|
|
857
896
|
}
|
|
858
|
-
|
|
859
|
-
regionSizes.push(regionSize)
|
|
897
|
+
|
|
898
|
+
regionSizes.push(regionSize);
|
|
860
899
|
}
|
|
861
|
-
|
|
862
|
-
numRegions = regionSizes.length
|
|
863
|
-
largestRegionSize = Math.max(...regionSizes, 0)
|
|
864
|
-
|
|
900
|
+
|
|
901
|
+
numRegions = regionSizes.length;
|
|
902
|
+
largestRegionSize = Math.max(...regionSizes, 0);
|
|
903
|
+
|
|
865
904
|
// Border cells
|
|
866
905
|
for (const cellIdx of playerCells) {
|
|
867
|
-
const neighbors = getNeighbors(cellIdx)
|
|
868
|
-
if (neighbors.some(n => !playerCells.has(n))) {
|
|
869
|
-
borderCellCount
|
|
906
|
+
const neighbors = getNeighbors(cellIdx);
|
|
907
|
+
if (neighbors.some((n) => !playerCells.has(n))) {
|
|
908
|
+
borderCellCount++;
|
|
870
909
|
}
|
|
871
910
|
}
|
|
872
|
-
|
|
873
|
-
// Compactness
|
|
874
|
-
|
|
911
|
+
|
|
912
|
+
// Compactness - compactness expects (area: number, perimeter: number)
|
|
913
|
+
const area = playerCells.size;
|
|
914
|
+
const perimeter = borderCellCount;
|
|
915
|
+
playerCompactness = compactness(area, perimeter);
|
|
875
916
|
}
|
|
876
|
-
|
|
917
|
+
|
|
877
918
|
// Lead over second
|
|
878
|
-
const leadOverSecond =
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
919
|
+
const leadOverSecond =
|
|
920
|
+
rankIdx === 0 && sortedBySize.length > 1
|
|
921
|
+
? cellCount - (territoryCounts.get(sortedBySize[1]) ?? 0)
|
|
922
|
+
: null;
|
|
923
|
+
|
|
882
924
|
// Turns until overtake (for non-leaders)
|
|
883
|
-
let turnsUntilOvertake: number | null = null
|
|
925
|
+
let turnsUntilOvertake: number | null = null;
|
|
884
926
|
if (rankIdx > 0 && forecastNext10.length > 0) {
|
|
885
|
-
const leaderHistory = territoryHistory.get(sortedBySize[0]) ?? []
|
|
927
|
+
const leaderHistory = territoryHistory.get(sortedBySize[0]) ?? [];
|
|
886
928
|
if (leaderHistory.length > 2) {
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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;
|
|
894
943
|
}
|
|
895
944
|
}
|
|
896
945
|
}
|
|
897
946
|
}
|
|
898
|
-
|
|
947
|
+
|
|
899
948
|
playerSnapshots.push({
|
|
900
949
|
id: playerId,
|
|
901
950
|
cellCount,
|
|
@@ -903,7 +952,12 @@ export function generateSnapshot(
|
|
|
903
952
|
rank: rankIdx + 1,
|
|
904
953
|
historyLength: history.length,
|
|
905
954
|
history: includeFullHistory ? [...history] : history.slice(-20),
|
|
906
|
-
recentTrend:
|
|
955
|
+
recentTrend:
|
|
956
|
+
trend?.direction === 'increasing'
|
|
957
|
+
? 'growing'
|
|
958
|
+
: trend?.direction === 'decreasing'
|
|
959
|
+
? 'shrinking'
|
|
960
|
+
: 'stable',
|
|
907
961
|
trendSlope: trend?.slope ?? 0,
|
|
908
962
|
trendConfidence: trend?.rSquared ?? 0,
|
|
909
963
|
winProbability: winProb,
|
|
@@ -911,321 +965,424 @@ export function generateSnapshot(
|
|
|
911
965
|
forecastNext10,
|
|
912
966
|
kalmanEstimate,
|
|
913
967
|
kalmanUncertainty,
|
|
914
|
-
conquestRate:
|
|
915
|
-
conquestRateCI:
|
|
968
|
+
conquestRate: conquestRate,
|
|
969
|
+
conquestRateCI: conquestRateCI,
|
|
916
970
|
avgGrowthPerTurn: avgGrowth,
|
|
917
971
|
volatility,
|
|
918
972
|
numRegions,
|
|
919
973
|
largestRegionSize,
|
|
920
974
|
borderCellCount,
|
|
921
975
|
compactness: playerCompactness,
|
|
922
|
-
sparklineAscii: sparkline(history.slice(-30)
|
|
976
|
+
sparklineAscii: sparkline(history.slice(-30)),
|
|
923
977
|
sparklineSvgPath: sparklineSvg(history.slice(-30), 100, 30),
|
|
924
978
|
leadOverSecond,
|
|
925
|
-
turnsUntilOvertake
|
|
926
|
-
})
|
|
979
|
+
turnsUntilOvertake,
|
|
980
|
+
});
|
|
927
981
|
}
|
|
928
|
-
|
|
982
|
+
|
|
929
983
|
// 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)
|
|
940
|
-
|
|
941
|
-
else if (gini < 0.
|
|
942
|
-
else if (gini < 0.
|
|
943
|
-
else inequalityInterpretation = '
|
|
944
|
-
|
|
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
|
+
|
|
945
1000
|
// 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)
|
|
953
|
-
|
|
954
|
-
else if (normalized > 0.
|
|
955
|
-
else if (normalized > 0.
|
|
956
|
-
else
|
|
957
|
-
|
|
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
|
+
|
|
958
1015
|
// Distribution metrics
|
|
959
|
-
const sorted = [...values].sort((a, b) => a - b)
|
|
960
|
-
const mean =
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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;
|
|
972
1033
|
for (const v of values) {
|
|
973
|
-
const diff = v - mean
|
|
974
|
-
m3 += diff ** 3
|
|
975
|
-
m4 += diff ** 4
|
|
1034
|
+
const diff = v - mean;
|
|
1035
|
+
m3 += diff ** 3;
|
|
1036
|
+
m4 += diff ** 4;
|
|
976
1037
|
}
|
|
977
|
-
const skewness =
|
|
978
|
-
?
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const
|
|
991
|
-
|
|
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
|
+
|
|
992
1053
|
// Competitiveness: inverse of lead
|
|
993
|
-
const topTwoShare =
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
+
|
|
1002
1070
|
// Stability: inverse of avg change rate
|
|
1003
|
-
let totalChangeRate = 0
|
|
1004
|
-
let historyCount = 0
|
|
1071
|
+
let totalChangeRate = 0;
|
|
1072
|
+
let historyCount = 0;
|
|
1005
1073
|
for (const [, history] of territoryHistory) {
|
|
1006
1074
|
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
|
|
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++;
|
|
1011
1079
|
}
|
|
1012
1080
|
}
|
|
1013
|
-
const stability =
|
|
1014
|
-
|
|
1081
|
+
const stability =
|
|
1082
|
+
historyCount > 0 ? Math.max(0, 1 - totalChangeRate / historyCount) : 0.5;
|
|
1083
|
+
|
|
1015
1084
|
// Predictions
|
|
1016
|
-
|
|
1017
|
-
const
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
compactnessSum += player.compactness
|
|
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
|
+
}
|
|
1041
1109
|
}
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
|
1048
1120
|
// Time series insights
|
|
1049
|
-
let overallTrend: 'convergent' | 'divergent' | 'cyclical' | 'chaotic' =
|
|
1050
|
-
|
|
1121
|
+
let overallTrend: 'convergent' | 'divergent' | 'cyclical' | 'chaotic' =
|
|
1122
|
+
'chaotic';
|
|
1123
|
+
|
|
1051
1124
|
// Check if shares are converging or diverging
|
|
1052
|
-
const recentVariances: number[] = []
|
|
1053
|
-
const historyLen = Math.min(
|
|
1054
|
-
|
|
1125
|
+
const recentVariances: number[] = [];
|
|
1126
|
+
const historyLen = Math.min(
|
|
1127
|
+
...Array.from(territoryHistory.values()).map((h) => h.length)
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1055
1130
|
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 =
|
|
1062
|
-
|
|
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);
|
|
1063
1140
|
}
|
|
1064
|
-
|
|
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
|
+
|
|
1065
1175
|
if (recentVariances.length > 3) {
|
|
1066
|
-
const varianceTrend = detectTrend(recentVariances)
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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';
|
|
1073
1184
|
} else {
|
|
1074
|
-
overallTrend = 'cyclical'
|
|
1185
|
+
overallTrend = 'cyclical';
|
|
1075
1186
|
}
|
|
1076
1187
|
}
|
|
1077
|
-
|
|
1188
|
+
|
|
1078
1189
|
// Overall change points
|
|
1079
|
-
const overallHistory = Array.from(territoryHistory.values())[0] ?? []
|
|
1080
|
-
const changePoints = detectChangePoints(overallHistory
|
|
1081
|
-
|
|
1190
|
+
const overallHistory = Array.from(territoryHistory.values())[0] ?? [];
|
|
1191
|
+
const changePoints = detectChangePoints(overallHistory);
|
|
1192
|
+
|
|
1082
1193
|
// Trend strength
|
|
1083
|
-
const overallHistoryTrend =
|
|
1084
|
-
|
|
1085
|
-
|
|
1194
|
+
const overallHistoryTrend =
|
|
1195
|
+
overallHistory.length > 3 ? detectTrend(overallHistory) : null;
|
|
1196
|
+
const trendStrength = overallHistoryTrend?.rSquared ?? 0;
|
|
1197
|
+
|
|
1086
1198
|
// Autocorrelation (lag 1)
|
|
1087
|
-
let autocorrelation = 0
|
|
1199
|
+
let autocorrelation = 0;
|
|
1088
1200
|
if (overallHistory.length > 5) {
|
|
1089
|
-
const ohMean =
|
|
1090
|
-
|
|
1201
|
+
const ohMean =
|
|
1202
|
+
overallHistory.reduce((a, b) => a + b, 0) / overallHistory.length;
|
|
1203
|
+
let num = 0,
|
|
1204
|
+
denom = 0;
|
|
1091
1205
|
for (let i = 1; i < overallHistory.length; i++) {
|
|
1092
|
-
num += (overallHistory[i] - ohMean) * (overallHistory[i - 1] - ohMean)
|
|
1206
|
+
num += (overallHistory[i] - ohMean) * (overallHistory[i - 1] - ohMean);
|
|
1093
1207
|
}
|
|
1094
1208
|
for (let i = 0; i < overallHistory.length; i++) {
|
|
1095
|
-
denom += (overallHistory[i] - ohMean) ** 2
|
|
1209
|
+
denom += (overallHistory[i] - ohMean) ** 2;
|
|
1096
1210
|
}
|
|
1097
|
-
autocorrelation = denom > 0 ? num / denom : 0
|
|
1211
|
+
autocorrelation = denom > 0 ? num / denom : 0;
|
|
1098
1212
|
}
|
|
1099
|
-
|
|
1213
|
+
|
|
1100
1214
|
// Seasonality (simple check)
|
|
1101
|
-
const seasonality = false // TODO: implement proper seasonality detection
|
|
1102
|
-
|
|
1215
|
+
const seasonality = false; // TODO: implement proper seasonality detection
|
|
1216
|
+
|
|
1103
1217
|
// Comparisons
|
|
1104
|
-
const vs5TurnsAgo: { [playerId: number]: number } = {}
|
|
1105
|
-
const vs10TurnsAgo: { [playerId: number]: number } = {}
|
|
1106
|
-
|
|
1218
|
+
const vs5TurnsAgo: { [playerId: number]: number } = {};
|
|
1219
|
+
const vs10TurnsAgo: { [playerId: number]: number } = {};
|
|
1220
|
+
|
|
1107
1221
|
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
|
|
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;
|
|
1114
1228
|
}
|
|
1115
|
-
|
|
1229
|
+
|
|
1116
1230
|
// 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
|
-
|
|
1231
|
+
const uniformProbs = values.map(() => 1 / values.length);
|
|
1232
|
+
const actualProbs = values.map((v) => v / (occupiedCells || 1));
|
|
1233
|
+
const divergenceFromUniform = klDivergence(actualProbs, uniformProbs);
|
|
1234
|
+
|
|
1121
1235
|
// Divergence from previous turn
|
|
1122
|
-
let divergenceFromPrevious = 0
|
|
1236
|
+
let divergenceFromPrevious = 0;
|
|
1123
1237
|
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)
|
|
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);
|
|
1129
1243
|
}
|
|
1130
|
-
|
|
1244
|
+
|
|
1131
1245
|
// Generate insights
|
|
1132
|
-
const insights: string[] = []
|
|
1133
|
-
|
|
1246
|
+
const insights: string[] = [];
|
|
1247
|
+
|
|
1134
1248
|
if (generateInsights) {
|
|
1135
1249
|
// Leader insights
|
|
1136
1250
|
if (sortedBySize.length > 0) {
|
|
1137
|
-
const leader = playerSnapshots.find(p => p.rank === 1)
|
|
1138
|
-
const leaderShare = leader.shareOfTotal
|
|
1139
|
-
|
|
1251
|
+
const leader = playerSnapshots.find((p) => p.rank === 1)!;
|
|
1252
|
+
const leaderShare = leader.shareOfTotal;
|
|
1253
|
+
|
|
1140
1254
|
if (leaderShare > 0.8) {
|
|
1141
|
-
insights.push(
|
|
1255
|
+
insights.push(
|
|
1256
|
+
`🏆 Player ${leader.id} dominates with ${(leaderShare * 100).toFixed(
|
|
1257
|
+
1
|
|
1258
|
+
)}% of territory`
|
|
1259
|
+
);
|
|
1142
1260
|
} else if (leaderShare > 0.5) {
|
|
1143
|
-
insights.push(
|
|
1261
|
+
insights.push(
|
|
1262
|
+
`📈 Player ${leader.id} leads with majority control (${(
|
|
1263
|
+
leaderShare * 100
|
|
1264
|
+
).toFixed(1)}%)`
|
|
1265
|
+
);
|
|
1144
1266
|
}
|
|
1145
|
-
|
|
1267
|
+
|
|
1146
1268
|
if (leader.recentTrend === 'growing' && leader.trendConfidence > 0.7) {
|
|
1147
|
-
insights.push(
|
|
1148
|
-
|
|
1149
|
-
|
|
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!`);
|
|
1150
1279
|
}
|
|
1151
1280
|
}
|
|
1152
|
-
|
|
1281
|
+
|
|
1153
1282
|
// Challenger insights
|
|
1154
1283
|
if (sortedBySize.length >= 2) {
|
|
1155
|
-
const challenger = playerSnapshots.find(p => p.rank === 2)
|
|
1156
|
-
|
|
1284
|
+
const challenger = playerSnapshots.find((p) => p.rank === 2)!;
|
|
1285
|
+
|
|
1157
1286
|
if (challenger.winProbability > 0.3) {
|
|
1158
|
-
insights.push(
|
|
1287
|
+
insights.push(
|
|
1288
|
+
`🎯 Player ${challenger.id} has ${(
|
|
1289
|
+
challenger.winProbability * 100
|
|
1290
|
+
).toFixed(1)}% chance of winning`
|
|
1291
|
+
);
|
|
1159
1292
|
}
|
|
1160
|
-
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
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
|
+
);
|
|
1163
1301
|
}
|
|
1164
1302
|
}
|
|
1165
|
-
|
|
1303
|
+
|
|
1166
1304
|
// Game state insights
|
|
1167
1305
|
if (isEndgame) {
|
|
1168
|
-
insights.push(`🔚 Endgame detected - victory imminent`)
|
|
1306
|
+
insights.push(`🔚 Endgame detected - victory imminent`);
|
|
1169
1307
|
}
|
|
1170
|
-
|
|
1308
|
+
|
|
1171
1309
|
if (competitiveness > 0.9) {
|
|
1172
|
-
insights.push(`🔥 Extremely close competition - anyone could win!`)
|
|
1310
|
+
insights.push(`🔥 Extremely close competition - anyone could win!`);
|
|
1173
1311
|
}
|
|
1174
|
-
|
|
1312
|
+
|
|
1175
1313
|
if (volatility > 0.7) {
|
|
1176
|
-
insights.push(`🌊 High volatility - expect rapid changes`)
|
|
1314
|
+
insights.push(`🌊 High volatility - expect rapid changes`);
|
|
1177
1315
|
} else if (volatility < 0.2) {
|
|
1178
|
-
insights.push(`🪨 Low volatility - stable territorial lines`)
|
|
1316
|
+
insights.push(`🪨 Low volatility - stable territorial lines`);
|
|
1179
1317
|
}
|
|
1180
|
-
|
|
1318
|
+
|
|
1181
1319
|
if (comebackPossibility > 0.5) {
|
|
1182
|
-
insights.push(
|
|
1320
|
+
insights.push(
|
|
1321
|
+
`🔄 Comeback still possible (${(comebackPossibility * 100).toFixed(
|
|
1322
|
+
0
|
|
1323
|
+
)}% chance)`
|
|
1324
|
+
);
|
|
1183
1325
|
}
|
|
1184
|
-
|
|
1326
|
+
|
|
1185
1327
|
// Topology insights
|
|
1186
1328
|
if (territoryFragmentation > 0.2) {
|
|
1187
|
-
insights.push(`🧩 High fragmentation - territories are scattered`)
|
|
1329
|
+
insights.push(`🧩 High fragmentation - territories are scattered`);
|
|
1188
1330
|
}
|
|
1189
|
-
|
|
1331
|
+
|
|
1190
1332
|
if (avgCompactness < 0.3) {
|
|
1191
|
-
insights.push(
|
|
1333
|
+
insights.push(
|
|
1334
|
+
`📏 Territories have irregular borders - vulnerable to attack`
|
|
1335
|
+
);
|
|
1192
1336
|
}
|
|
1193
|
-
|
|
1337
|
+
|
|
1194
1338
|
// Change point insights
|
|
1195
1339
|
if (changePoints.length > 0) {
|
|
1196
|
-
const recentChangePoint = changePoints[changePoints.length - 1]
|
|
1340
|
+
const recentChangePoint = changePoints[changePoints.length - 1];
|
|
1197
1341
|
if (turnNumber - recentChangePoint < 5) {
|
|
1198
|
-
insights.push(
|
|
1342
|
+
insights.push(
|
|
1343
|
+
`📊 Recent momentum shift detected at turn ${recentChangePoint}`
|
|
1344
|
+
);
|
|
1199
1345
|
}
|
|
1200
1346
|
}
|
|
1201
|
-
|
|
1347
|
+
|
|
1202
1348
|
// Trend insights
|
|
1203
1349
|
if (overallTrend === 'convergent') {
|
|
1204
|
-
insights.push(
|
|
1350
|
+
insights.push(
|
|
1351
|
+
`📉 Territories are converging - expect stalemate or final push`
|
|
1352
|
+
);
|
|
1205
1353
|
} else if (overallTrend === 'divergent') {
|
|
1206
|
-
insights.push(`📈 Gap widening - leader pulling ahead`)
|
|
1354
|
+
insights.push(`📈 Gap widening - leader pulling ahead`);
|
|
1207
1355
|
}
|
|
1208
1356
|
}
|
|
1209
|
-
|
|
1210
|
-
// Detect anomalies
|
|
1211
|
-
const
|
|
1212
|
-
|
|
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
|
+
|
|
1213
1365
|
const anomalies: AnomalySummary = {
|
|
1214
1366
|
outliers: gameAnomalies,
|
|
1215
1367
|
hasAnomalies: gameAnomalies.length > 0,
|
|
1216
1368
|
anomalyCount: gameAnomalies.length,
|
|
1217
|
-
mostSevere:
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
+
|
|
1222
1378
|
// Add anomaly insights
|
|
1223
1379
|
if (generateInsights && anomalies.hasAnomalies) {
|
|
1224
|
-
for (const anomaly of gameAnomalies.slice(0, 3)) {
|
|
1225
|
-
|
|
1380
|
+
for (const anomaly of gameAnomalies.slice(0, 3)) {
|
|
1381
|
+
// Top 3 anomalies
|
|
1382
|
+
insights.push(`⚠️ Anomaly: ${anomaly.description}`);
|
|
1226
1383
|
}
|
|
1227
1384
|
}
|
|
1228
|
-
|
|
1385
|
+
|
|
1229
1386
|
return {
|
|
1230
1387
|
timestamp,
|
|
1231
1388
|
turnNumber,
|
|
@@ -1241,14 +1398,14 @@ export function generateSnapshot(
|
|
|
1241
1398
|
herfindahl,
|
|
1242
1399
|
paretoRatio: pareto.ratioHeld,
|
|
1243
1400
|
zipfCoefficient: zipf,
|
|
1244
|
-
interpretation: inequalityInterpretation
|
|
1401
|
+
interpretation: inequalityInterpretation,
|
|
1245
1402
|
},
|
|
1246
1403
|
diversity: {
|
|
1247
1404
|
shannon,
|
|
1248
1405
|
normalized,
|
|
1249
1406
|
renyi,
|
|
1250
1407
|
tsallis,
|
|
1251
|
-
interpretation: diversityInterpretation
|
|
1408
|
+
interpretation: diversityInterpretation,
|
|
1252
1409
|
},
|
|
1253
1410
|
distribution: {
|
|
1254
1411
|
mean,
|
|
@@ -1260,14 +1417,14 @@ export function generateSnapshot(
|
|
|
1260
1417
|
iqr: q3 - q1,
|
|
1261
1418
|
min: sorted[0] ?? 0,
|
|
1262
1419
|
max: sorted[sorted.length - 1] ?? 0,
|
|
1263
|
-
range: (sorted[sorted.length - 1] ?? 0) - (sorted[0] ?? 0)
|
|
1420
|
+
range: (sorted[sorted.length - 1] ?? 0) - (sorted[0] ?? 0),
|
|
1264
1421
|
},
|
|
1265
1422
|
indices: {
|
|
1266
1423
|
dominance,
|
|
1267
1424
|
volatility,
|
|
1268
1425
|
predictability,
|
|
1269
1426
|
competitiveness,
|
|
1270
|
-
stability
|
|
1427
|
+
stability,
|
|
1271
1428
|
},
|
|
1272
1429
|
predictions: {
|
|
1273
1430
|
likelyWinner,
|
|
@@ -1275,7 +1432,7 @@ export function generateSnapshot(
|
|
|
1275
1432
|
estimatedTurnsToVictory,
|
|
1276
1433
|
isEndgame,
|
|
1277
1434
|
secondPlaceChallenger,
|
|
1278
|
-
comebackPossibility
|
|
1435
|
+
comebackPossibility,
|
|
1279
1436
|
},
|
|
1280
1437
|
anomalies,
|
|
1281
1438
|
probability,
|
|
@@ -1284,119 +1441,167 @@ export function generateSnapshot(
|
|
|
1284
1441
|
averageRegionSize,
|
|
1285
1442
|
territoryFragmentation,
|
|
1286
1443
|
borderCellPercentage,
|
|
1287
|
-
avgCompactness
|
|
1444
|
+
avgCompactness,
|
|
1288
1445
|
},
|
|
1289
1446
|
timeSeries: {
|
|
1290
1447
|
overallTrend,
|
|
1291
1448
|
changePoints,
|
|
1292
1449
|
trendStrength,
|
|
1293
1450
|
autocorrelation,
|
|
1294
|
-
seasonality
|
|
1451
|
+
seasonality,
|
|
1295
1452
|
},
|
|
1296
1453
|
comparisons: {
|
|
1297
1454
|
vs5TurnsAgo,
|
|
1298
1455
|
vs10TurnsAgo,
|
|
1299
1456
|
divergenceFromUniform,
|
|
1300
|
-
divergenceFromPrevious
|
|
1457
|
+
divergenceFromPrevious,
|
|
1301
1458
|
},
|
|
1302
|
-
insights
|
|
1303
|
-
}
|
|
1459
|
+
insights,
|
|
1460
|
+
};
|
|
1304
1461
|
}
|
|
1305
1462
|
|
|
1306
1463
|
/**
|
|
1307
1464
|
* Format snapshot as human-readable text
|
|
1308
1465
|
*/
|
|
1309
1466
|
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(
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
lines.push('
|
|
1322
|
-
|
|
1323
|
-
lines.push('
|
|
1324
|
-
|
|
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
|
+
|
|
1325
1484
|
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 =
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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('│');
|
|
1335
1506
|
}
|
|
1336
|
-
|
|
1337
|
-
lines.push('└─────────────────────────────────────────────────────────┘')
|
|
1338
|
-
lines.push('')
|
|
1339
|
-
|
|
1340
|
-
lines.push('┌─────────────────────────────────────────────────────────┐')
|
|
1341
|
-
lines.push('│ GAME STATE │')
|
|
1342
|
-
lines.push('├─────────────────────────────────────────────────────────┤')
|
|
1343
|
-
lines.push(
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
lines.push(
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
lines.push(
|
|
1354
|
-
|
|
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
|
+
|
|
1355
1546
|
if (snapshot.predictions.likelyWinner !== null) {
|
|
1356
|
-
lines.push(`│ Likely Winner: Player ${snapshot.predictions.likelyWinner}`)
|
|
1357
|
-
lines.push(
|
|
1547
|
+
lines.push(`│ Likely Winner: Player ${snapshot.predictions.likelyWinner}`);
|
|
1548
|
+
lines.push(
|
|
1549
|
+
`│ Confidence: ${(snapshot.predictions.winnerConfidence * 100).toFixed(
|
|
1550
|
+
1
|
|
1551
|
+
)}%`
|
|
1552
|
+
);
|
|
1358
1553
|
if (snapshot.predictions.estimatedTurnsToVictory !== null) {
|
|
1359
|
-
lines.push(
|
|
1554
|
+
lines.push(
|
|
1555
|
+
`│ Est. Victory In: ${snapshot.predictions.estimatedTurnsToVictory} turns`
|
|
1556
|
+
);
|
|
1360
1557
|
}
|
|
1361
1558
|
} else {
|
|
1362
|
-
lines.push('│ No clear winner predicted yet')
|
|
1559
|
+
lines.push('│ No clear winner predicted yet');
|
|
1363
1560
|
}
|
|
1364
|
-
|
|
1561
|
+
|
|
1365
1562
|
if (snapshot.predictions.isEndgame) {
|
|
1366
|
-
lines.push('│ ⚠️ ENDGAME DETECTED')
|
|
1563
|
+
lines.push('│ ⚠️ ENDGAME DETECTED');
|
|
1367
1564
|
}
|
|
1368
|
-
|
|
1369
|
-
lines.push(
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1565
|
+
|
|
1566
|
+
lines.push(
|
|
1567
|
+
`│ Comeback Chance: ${(
|
|
1568
|
+
snapshot.predictions.comebackPossibility * 100
|
|
1569
|
+
).toFixed(0)}%`
|
|
1570
|
+
);
|
|
1571
|
+
lines.push('└─────────────────────────────────────────────────────────┘');
|
|
1572
|
+
lines.push('');
|
|
1573
|
+
|
|
1373
1574
|
if (snapshot.insights.length > 0) {
|
|
1374
|
-
lines.push('┌─────────────────────────────────────────────────────────┐')
|
|
1375
|
-
lines.push('│ INSIGHTS │')
|
|
1376
|
-
lines.push('├─────────────────────────────────────────────────────────┤')
|
|
1575
|
+
lines.push('┌─────────────────────────────────────────────────────────┐');
|
|
1576
|
+
lines.push('│ INSIGHTS │');
|
|
1577
|
+
lines.push('├─────────────────────────────────────────────────────────┤');
|
|
1377
1578
|
for (const insight of snapshot.insights) {
|
|
1378
|
-
lines.push(`│ ${insight}`)
|
|
1579
|
+
lines.push(`│ ${insight}`);
|
|
1379
1580
|
}
|
|
1380
|
-
lines.push('└─────────────────────────────────────────────────────────┘')
|
|
1581
|
+
lines.push('└─────────────────────────────────────────────────────────┘');
|
|
1381
1582
|
}
|
|
1382
|
-
|
|
1383
|
-
return lines.join('\n')
|
|
1583
|
+
|
|
1584
|
+
return lines.join('\n');
|
|
1384
1585
|
}
|
|
1385
1586
|
|
|
1386
1587
|
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) + ']'
|
|
1588
|
+
const filled = Math.round(value * width);
|
|
1589
|
+
const empty = width - filled;
|
|
1590
|
+
return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']';
|
|
1390
1591
|
}
|
|
1391
1592
|
|
|
1392
1593
|
/**
|
|
1393
1594
|
* Export snapshot as JSON (removes functions)
|
|
1394
1595
|
*/
|
|
1395
1596
|
export function exportSnapshotAsJSON(snapshot: GameSnapshot): string {
|
|
1396
|
-
return JSON.stringify(
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
+
);
|
|
1402
1607
|
}
|