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