@buley/hexgrid-3d 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +28 -0
- package/LICENSE +39 -0
- package/README.md +291 -0
- package/examples/basic-usage.tsx +52 -0
- package/package.json +65 -0
- package/public/hexgrid-worker.js +1763 -0
- package/rust/Cargo.toml +41 -0
- package/rust/src/lib.rs +740 -0
- package/rust/src/math.rs +574 -0
- package/rust/src/spatial.rs +245 -0
- package/rust/src/statistics.rs +496 -0
- package/src/HexGridEnhanced.ts +16 -0
- package/src/Snapshot.ts +1402 -0
- package/src/adapters.ts +65 -0
- package/src/algorithms/AdvancedStatistics.ts +328 -0
- package/src/algorithms/BayesianStatistics.ts +317 -0
- package/src/algorithms/FlowField.ts +126 -0
- package/src/algorithms/FluidSimulation.ts +99 -0
- package/src/algorithms/GraphAlgorithms.ts +184 -0
- package/src/algorithms/OutlierDetection.ts +391 -0
- package/src/algorithms/ParticleSystem.ts +85 -0
- package/src/algorithms/index.ts +13 -0
- package/src/compat.ts +96 -0
- package/src/components/HexGrid.tsx +31 -0
- package/src/components/NarrationOverlay.tsx +221 -0
- package/src/components/index.ts +2 -0
- package/src/features.ts +125 -0
- package/src/index.ts +30 -0
- package/src/math/HexCoordinates.ts +15 -0
- package/src/math/Matrix4.ts +35 -0
- package/src/math/Quaternion.ts +37 -0
- package/src/math/SpatialIndex.ts +114 -0
- package/src/math/Vector3.ts +69 -0
- package/src/math/index.ts +11 -0
- package/src/note-adapter.ts +124 -0
- package/src/ontology-adapter.ts +77 -0
- package/src/stores/index.ts +1 -0
- package/src/stores/uiStore.ts +85 -0
- package/src/types/index.ts +3 -0
- package/src/types.ts +152 -0
- package/src/utils/image-utils.ts +25 -0
- package/src/wasm/HexGridWasmWrapper.ts +753 -0
- package/src/wasm/index.ts +7 -0
- package/src/workers/hexgrid-math.ts +177 -0
- package/src/workers/hexgrid-worker.worker.ts +1807 -0
- package/tsconfig.json +18 -0
package/rust/src/lib.rs
ADDED
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
//! HexGrid WASM - High-Performance Hexagonal Grid Computations
|
|
2
|
+
//!
|
|
3
|
+
//! This crate provides WebAssembly bindings for computationally intensive
|
|
4
|
+
//! hexgrid operations, achieving significant speedups over pure JavaScript.
|
|
5
|
+
//!
|
|
6
|
+
//! # Features
|
|
7
|
+
//! - O(1) neighbor lookups with precomputed tables
|
|
8
|
+
//! - SIMD-accelerated vector operations (when available)
|
|
9
|
+
//! - Efficient spatial indexing
|
|
10
|
+
//! - Optimized infection simulation
|
|
11
|
+
//! - Flow field computation
|
|
12
|
+
//!
|
|
13
|
+
//! # Usage from JavaScript
|
|
14
|
+
//! ```javascript
|
|
15
|
+
//! import init, { HexGridWasm } from './hexgrid_wasm';
|
|
16
|
+
//! await init();
|
|
17
|
+
//! const grid = new HexGridWasm(100, 100);
|
|
18
|
+
//! grid.stepInfection();
|
|
19
|
+
//! ```
|
|
20
|
+
|
|
21
|
+
mod spatial;
|
|
22
|
+
mod math;
|
|
23
|
+
mod statistics;
|
|
24
|
+
|
|
25
|
+
use wasm_bindgen::prelude::*;
|
|
26
|
+
use std::collections::{HashMap, HashSet, VecDeque};
|
|
27
|
+
use rand::prelude::*;
|
|
28
|
+
|
|
29
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
+
// INITIALIZATION
|
|
31
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
|
+
|
|
33
|
+
/// Initialize panic hook for better error messages
|
|
34
|
+
#[wasm_bindgen(start)]
|
|
35
|
+
pub fn init_panic_hook() {
|
|
36
|
+
#[cfg(feature = "console_error_panic_hook")]
|
|
37
|
+
console_error_panic_hook::set_once();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// CORE TYPES
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
43
|
+
|
|
44
|
+
/// Axial hex coordinate
|
|
45
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
|
46
|
+
pub struct Axial {
|
|
47
|
+
pub q: i32,
|
|
48
|
+
pub r: i32,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl Axial {
|
|
52
|
+
#[inline]
|
|
53
|
+
pub fn new(q: i32, r: i32) -> Self {
|
|
54
|
+
Self { q, r }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Get the 6 neighbors in axial coordinates
|
|
58
|
+
#[inline]
|
|
59
|
+
pub fn neighbors(&self) -> [Axial; 6] {
|
|
60
|
+
const DIRECTIONS: [(i32, i32); 6] = [
|
|
61
|
+
(1, 0), (1, -1), (0, -1),
|
|
62
|
+
(-1, 0), (-1, 1), (0, 1),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
let mut result = [Axial::new(0, 0); 6];
|
|
66
|
+
for (i, (dq, dr)) in DIRECTIONS.iter().enumerate() {
|
|
67
|
+
result[i] = Axial::new(self.q + dq, self.r + dr);
|
|
68
|
+
}
|
|
69
|
+
result
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Distance to another hex
|
|
73
|
+
#[inline]
|
|
74
|
+
pub fn distance(&self, other: &Axial) -> i32 {
|
|
75
|
+
let dq = self.q - other.q;
|
|
76
|
+
let dr = self.r - other.r;
|
|
77
|
+
let ds = -dq - dr;
|
|
78
|
+
(dq.abs() + dr.abs() + ds.abs()) / 2
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Convert to cube coordinates
|
|
82
|
+
#[inline]
|
|
83
|
+
pub fn to_cube(&self) -> (i32, i32, i32) {
|
|
84
|
+
let x = self.q;
|
|
85
|
+
let z = self.r;
|
|
86
|
+
let y = -x - z;
|
|
87
|
+
(x, y, z)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Convert to pixel coordinates (pointy-top)
|
|
91
|
+
#[inline]
|
|
92
|
+
pub fn to_pixel(&self, size: f32) -> (f32, f32) {
|
|
93
|
+
let x = size * (3.0_f32.sqrt() * self.q as f32 + 3.0_f32.sqrt() / 2.0 * self.r as f32);
|
|
94
|
+
let y = size * (3.0 / 2.0 * self.r as f32);
|
|
95
|
+
(x, y)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Cell state in the grid
|
|
100
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
101
|
+
pub struct CellState {
|
|
102
|
+
pub owner: u8, // 0 = neutral, 1-255 = player ID
|
|
103
|
+
pub population: f32, // Strength/population
|
|
104
|
+
pub infected_by: u8, // Who is currently attacking
|
|
105
|
+
pub infection: f32, // Infection progress (0-1)
|
|
106
|
+
pub resistance: f32, // Resistance to infection
|
|
107
|
+
pub flags: u8, // Bit flags for special states
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
111
|
+
// HEXGRID WASM
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
113
|
+
|
|
114
|
+
/// Main WebAssembly interface for hexgrid computations
|
|
115
|
+
#[wasm_bindgen]
|
|
116
|
+
pub struct HexGridWasm {
|
|
117
|
+
width: usize,
|
|
118
|
+
height: usize,
|
|
119
|
+
cells: Vec<CellState>,
|
|
120
|
+
// Neighbor lookup table (precomputed for O(1) access)
|
|
121
|
+
neighbor_indices: Vec<[i32; 6]>,
|
|
122
|
+
// Spatial hash for efficient queries
|
|
123
|
+
spatial_hash: HashMap<(i32, i32), Vec<usize>>,
|
|
124
|
+
spatial_cell_size: f32,
|
|
125
|
+
// Infection queue for BFS
|
|
126
|
+
infection_queue: VecDeque<(usize, u8)>,
|
|
127
|
+
// Statistics
|
|
128
|
+
territory_counts: HashMap<u8, usize>,
|
|
129
|
+
total_population: f32,
|
|
130
|
+
// Random number generator
|
|
131
|
+
rng: rand::rngs::SmallRng,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[wasm_bindgen]
|
|
135
|
+
impl HexGridWasm {
|
|
136
|
+
/// Create a new hexgrid
|
|
137
|
+
#[wasm_bindgen(constructor)]
|
|
138
|
+
pub fn new(width: usize, height: usize) -> Self {
|
|
139
|
+
let size = width * height;
|
|
140
|
+
let mut cells = Vec::with_capacity(size);
|
|
141
|
+
let mut neighbor_indices = Vec::with_capacity(size);
|
|
142
|
+
|
|
143
|
+
// Initialize cells and precompute neighbors
|
|
144
|
+
for y in 0..height {
|
|
145
|
+
for x in 0..width {
|
|
146
|
+
cells.push(CellState::default());
|
|
147
|
+
|
|
148
|
+
// Compute neighbor indices (handles odd-r offset coordinates)
|
|
149
|
+
let offset = if y % 2 == 1 { 1i32 } else { 0i32 };
|
|
150
|
+
let mut neighbors = [-1i32; 6];
|
|
151
|
+
|
|
152
|
+
// Direction offsets for odd-r offset coordinates
|
|
153
|
+
let dirs = if y % 2 == 1 {
|
|
154
|
+
[(1, 0), (0, -1), (-1, -1), (-1, 0), (-1, 1), (0, 1)]
|
|
155
|
+
} else {
|
|
156
|
+
[(1, 0), (1, -1), (0, -1), (-1, 0), (0, 1), (1, 1)]
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
for (i, (dx, dy)) in dirs.iter().enumerate() {
|
|
160
|
+
let nx = x as i32 + dx;
|
|
161
|
+
let ny = y as i32 + dy;
|
|
162
|
+
|
|
163
|
+
if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
|
|
164
|
+
neighbors[i] = (ny as usize * width + nx as usize) as i32;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
neighbor_indices.push(neighbors);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Self {
|
|
173
|
+
width,
|
|
174
|
+
height,
|
|
175
|
+
cells,
|
|
176
|
+
neighbor_indices,
|
|
177
|
+
spatial_hash: HashMap::new(),
|
|
178
|
+
spatial_cell_size: 10.0,
|
|
179
|
+
infection_queue: VecDeque::new(),
|
|
180
|
+
territory_counts: HashMap::new(),
|
|
181
|
+
total_population: 0.0,
|
|
182
|
+
rng: rand::rngs::SmallRng::from_entropy(),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Get cell owner
|
|
187
|
+
#[wasm_bindgen]
|
|
188
|
+
pub fn get_owner(&self, index: usize) -> u8 {
|
|
189
|
+
self.cells.get(index).map(|c| c.owner).unwrap_or(0)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Set cell owner
|
|
193
|
+
#[wasm_bindgen]
|
|
194
|
+
pub fn set_owner(&mut self, index: usize, owner: u8) {
|
|
195
|
+
if let Some(cell) = self.cells.get_mut(index) {
|
|
196
|
+
// Update territory counts
|
|
197
|
+
if cell.owner != 0 {
|
|
198
|
+
*self.territory_counts.entry(cell.owner).or_insert(0) -= 1;
|
|
199
|
+
}
|
|
200
|
+
cell.owner = owner;
|
|
201
|
+
if owner != 0 {
|
|
202
|
+
*self.territory_counts.entry(owner).or_insert(0) += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Set cell population
|
|
208
|
+
#[wasm_bindgen]
|
|
209
|
+
pub fn set_population(&mut self, index: usize, population: f32) {
|
|
210
|
+
if let Some(cell) = self.cells.get_mut(index) {
|
|
211
|
+
self.total_population -= cell.population;
|
|
212
|
+
cell.population = population;
|
|
213
|
+
self.total_population += population;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Get neighbors of a cell (returns JS array)
|
|
218
|
+
#[wasm_bindgen]
|
|
219
|
+
pub fn get_neighbors(&self, index: usize) -> Vec<i32> {
|
|
220
|
+
self.neighbor_indices.get(index)
|
|
221
|
+
.map(|n| n.to_vec())
|
|
222
|
+
.unwrap_or_default()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// Step the infection simulation
|
|
226
|
+
#[wasm_bindgen]
|
|
227
|
+
pub fn step_infection(&mut self, infection_rate: f32, infection_threshold: f32) -> Vec<u32> {
|
|
228
|
+
let mut changed = Vec::new();
|
|
229
|
+
|
|
230
|
+
// Phase 1: Spread infection from borders
|
|
231
|
+
for i in 0..self.cells.len() {
|
|
232
|
+
let cell = self.cells[i];
|
|
233
|
+
if cell.owner == 0 { continue; }
|
|
234
|
+
|
|
235
|
+
let neighbors = &self.neighbor_indices[i];
|
|
236
|
+
for &neighbor_idx in neighbors.iter() {
|
|
237
|
+
if neighbor_idx < 0 { continue; }
|
|
238
|
+
let ni = neighbor_idx as usize;
|
|
239
|
+
let neighbor = &self.cells[ni];
|
|
240
|
+
|
|
241
|
+
// Can infect if: different owner and has population
|
|
242
|
+
if neighbor.owner != cell.owner && cell.population > 0.0 {
|
|
243
|
+
// Calculate infection power
|
|
244
|
+
let power = cell.population * infection_rate;
|
|
245
|
+
let resistance = (neighbor.resistance + neighbor.population).max(0.1);
|
|
246
|
+
let infection_delta = power / resistance;
|
|
247
|
+
|
|
248
|
+
if let Some(target) = self.cells.get_mut(ni) {
|
|
249
|
+
if target.infected_by == 0 || target.infected_by == cell.owner {
|
|
250
|
+
target.infected_by = cell.owner;
|
|
251
|
+
target.infection += infection_delta;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Phase 2: Convert fully infected cells
|
|
259
|
+
for i in 0..self.cells.len() {
|
|
260
|
+
let cell = &self.cells[i];
|
|
261
|
+
if cell.infection >= infection_threshold && cell.infected_by != 0 {
|
|
262
|
+
let new_owner = cell.infected_by;
|
|
263
|
+
let old_owner = cell.owner;
|
|
264
|
+
|
|
265
|
+
if let Some(target) = self.cells.get_mut(i) {
|
|
266
|
+
// Update territory counts
|
|
267
|
+
if old_owner != 0 {
|
|
268
|
+
*self.territory_counts.entry(old_owner).or_insert(0) -= 1;
|
|
269
|
+
}
|
|
270
|
+
if new_owner != 0 {
|
|
271
|
+
*self.territory_counts.entry(new_owner).or_insert(0) += 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
target.owner = new_owner;
|
|
275
|
+
target.infection = 0.0;
|
|
276
|
+
target.infected_by = 0;
|
|
277
|
+
target.population = 1.0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
changed.push(i as u32);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Phase 3: Decay infection on cells that aren't being actively infected
|
|
285
|
+
for cell in self.cells.iter_mut() {
|
|
286
|
+
if cell.infected_by == 0 && cell.infection > 0.0 {
|
|
287
|
+
cell.infection *= 0.9;
|
|
288
|
+
if cell.infection < 0.01 {
|
|
289
|
+
cell.infection = 0.0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
changed
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Find connected components for a given owner
|
|
298
|
+
#[wasm_bindgen]
|
|
299
|
+
pub fn find_connected_regions(&self, owner: u8) -> Vec<u32> {
|
|
300
|
+
let mut visited = vec![false; self.cells.len()];
|
|
301
|
+
let mut region_ids = vec![0u32; self.cells.len()];
|
|
302
|
+
let mut current_region = 1u32;
|
|
303
|
+
|
|
304
|
+
for start in 0..self.cells.len() {
|
|
305
|
+
if visited[start] || self.cells[start].owner != owner {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// BFS from this cell
|
|
310
|
+
let mut queue = VecDeque::new();
|
|
311
|
+
queue.push_back(start);
|
|
312
|
+
visited[start] = true;
|
|
313
|
+
|
|
314
|
+
while let Some(idx) = queue.pop_front() {
|
|
315
|
+
region_ids[idx] = current_region;
|
|
316
|
+
|
|
317
|
+
for &neighbor_idx in self.neighbor_indices[idx].iter() {
|
|
318
|
+
if neighbor_idx < 0 { continue; }
|
|
319
|
+
let ni = neighbor_idx as usize;
|
|
320
|
+
|
|
321
|
+
if !visited[ni] && self.cells[ni].owner == owner {
|
|
322
|
+
visited[ni] = true;
|
|
323
|
+
queue.push_back(ni);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
current_region += 1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
region_ids
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// Find border cells for a given owner
|
|
335
|
+
#[wasm_bindgen]
|
|
336
|
+
pub fn find_border_cells(&self, owner: u8) -> Vec<u32> {
|
|
337
|
+
let mut borders = Vec::new();
|
|
338
|
+
|
|
339
|
+
for i in 0..self.cells.len() {
|
|
340
|
+
if self.cells[i].owner != owner { continue; }
|
|
341
|
+
|
|
342
|
+
let is_border = self.neighbor_indices[i].iter().any(|&ni| {
|
|
343
|
+
if ni < 0 { return true; } // Edge of grid
|
|
344
|
+
self.cells[ni as usize].owner != owner
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
if is_border {
|
|
348
|
+
borders.push(i as u32);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
borders
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// Compute shortest path between two cells using A*
|
|
356
|
+
#[wasm_bindgen]
|
|
357
|
+
pub fn find_path(&self, start: usize, end: usize, owner_filter: u8) -> Vec<u32> {
|
|
358
|
+
if start >= self.cells.len() || end >= self.cells.len() {
|
|
359
|
+
return Vec::new();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// A* implementation
|
|
363
|
+
let mut open_set = std::collections::BinaryHeap::new();
|
|
364
|
+
let mut came_from = HashMap::new();
|
|
365
|
+
let mut g_score = HashMap::new();
|
|
366
|
+
let mut in_open = HashSet::new();
|
|
367
|
+
|
|
368
|
+
g_score.insert(start, 0i32);
|
|
369
|
+
open_set.push(std::cmp::Reverse((self.heuristic(start, end), start)));
|
|
370
|
+
in_open.insert(start);
|
|
371
|
+
|
|
372
|
+
while let Some(std::cmp::Reverse((_, current))) = open_set.pop() {
|
|
373
|
+
in_open.remove(¤t);
|
|
374
|
+
|
|
375
|
+
if current == end {
|
|
376
|
+
// Reconstruct path
|
|
377
|
+
let mut path = Vec::new();
|
|
378
|
+
let mut curr = current;
|
|
379
|
+
path.push(curr as u32);
|
|
380
|
+
|
|
381
|
+
while let Some(&prev) = came_from.get(&curr) {
|
|
382
|
+
path.push(prev as u32);
|
|
383
|
+
curr = prev;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
path.reverse();
|
|
387
|
+
return path;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for &neighbor_idx in self.neighbor_indices[current].iter() {
|
|
391
|
+
if neighbor_idx < 0 { continue; }
|
|
392
|
+
let ni = neighbor_idx as usize;
|
|
393
|
+
|
|
394
|
+
// Skip if owner doesn't match (0 = any)
|
|
395
|
+
if owner_filter != 0 && self.cells[ni].owner != owner_filter {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let tentative_g = g_score.get(¤t).copied().unwrap_or(i32::MAX) + 1;
|
|
400
|
+
|
|
401
|
+
if tentative_g < g_score.get(&ni).copied().unwrap_or(i32::MAX) {
|
|
402
|
+
came_from.insert(ni, current);
|
|
403
|
+
g_score.insert(ni, tentative_g);
|
|
404
|
+
|
|
405
|
+
if !in_open.contains(&ni) {
|
|
406
|
+
let f_score = tentative_g + self.heuristic(ni, end);
|
|
407
|
+
open_set.push(std::cmp::Reverse((f_score, ni)));
|
|
408
|
+
in_open.insert(ni);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
Vec::new() // No path found
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
fn heuristic(&self, from: usize, to: usize) -> i32 {
|
|
418
|
+
// Convert to grid coordinates
|
|
419
|
+
let (x1, y1) = (from % self.width, from / self.width);
|
|
420
|
+
let (x2, y2) = (to % self.width, to / self.width);
|
|
421
|
+
|
|
422
|
+
// Manhattan distance as heuristic
|
|
423
|
+
((x2 as i32 - x1 as i32).abs() + (y2 as i32 - y1 as i32).abs())
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// Get territory counts for all players
|
|
427
|
+
#[wasm_bindgen]
|
|
428
|
+
pub fn get_territory_counts(&self) -> Vec<u32> {
|
|
429
|
+
let mut counts = vec![0u32; 256];
|
|
430
|
+
for (&owner, &count) in self.territory_counts.iter() {
|
|
431
|
+
counts[owner as usize] = count as u32;
|
|
432
|
+
}
|
|
433
|
+
counts
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/// Compute Gini coefficient
|
|
437
|
+
#[wasm_bindgen]
|
|
438
|
+
pub fn compute_gini(&self) -> f64 {
|
|
439
|
+
let mut populations: Vec<f32> = self.cells.iter()
|
|
440
|
+
.filter(|c| c.owner != 0)
|
|
441
|
+
.map(|c| c.population)
|
|
442
|
+
.collect();
|
|
443
|
+
|
|
444
|
+
if populations.is_empty() {
|
|
445
|
+
return 0.0;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
populations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
|
449
|
+
|
|
450
|
+
let n = populations.len() as f64;
|
|
451
|
+
let mut sum = 0.0;
|
|
452
|
+
|
|
453
|
+
for (i, &pop) in populations.iter().enumerate() {
|
|
454
|
+
sum += (2.0 * (i as f64 + 1.0) - n - 1.0) * pop as f64;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let mean = populations.iter().sum::<f32>() as f64 / n;
|
|
458
|
+
if mean == 0.0 {
|
|
459
|
+
return 0.0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
sum / (n * n * mean)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/// Compute Shannon entropy of territory distribution
|
|
466
|
+
#[wasm_bindgen]
|
|
467
|
+
pub fn compute_entropy(&self) -> f64 {
|
|
468
|
+
let total: f64 = self.territory_counts.values().sum::<usize>() as f64;
|
|
469
|
+
if total == 0.0 {
|
|
470
|
+
return 0.0;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
let mut entropy = 0.0;
|
|
474
|
+
for &count in self.territory_counts.values() {
|
|
475
|
+
if count > 0 {
|
|
476
|
+
let p = count as f64 / total;
|
|
477
|
+
entropy -= p * p.ln();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
entropy
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/// Run K-means clustering on cell positions
|
|
485
|
+
#[wasm_bindgen]
|
|
486
|
+
pub fn kmeans_cluster(&mut self, k: usize, iterations: usize) -> Vec<u32> {
|
|
487
|
+
let mut positions: Vec<(f32, f32)> = Vec::new();
|
|
488
|
+
let mut cell_indices: Vec<usize> = Vec::new();
|
|
489
|
+
|
|
490
|
+
// Collect all owned cells
|
|
491
|
+
for i in 0..self.cells.len() {
|
|
492
|
+
if self.cells[i].owner != 0 {
|
|
493
|
+
let x = (i % self.width) as f32;
|
|
494
|
+
let y = (i / self.width) as f32;
|
|
495
|
+
positions.push((x, y));
|
|
496
|
+
cell_indices.push(i);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if positions.is_empty() || k == 0 {
|
|
501
|
+
return vec![0; self.cells.len()];
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Initialize centroids using k-means++
|
|
505
|
+
let mut centroids = Vec::with_capacity(k);
|
|
506
|
+
let first_idx = self.rng.gen_range(0..positions.len());
|
|
507
|
+
centroids.push(positions[first_idx]);
|
|
508
|
+
|
|
509
|
+
for _ in 1..k {
|
|
510
|
+
let mut distances: Vec<f32> = positions.iter().map(|p| {
|
|
511
|
+
centroids.iter()
|
|
512
|
+
.map(|c| {
|
|
513
|
+
let dx = p.0 - c.0;
|
|
514
|
+
let dy = p.1 - c.1;
|
|
515
|
+
dx * dx + dy * dy
|
|
516
|
+
})
|
|
517
|
+
.fold(f32::MAX, f32::min)
|
|
518
|
+
}).collect();
|
|
519
|
+
|
|
520
|
+
let total: f32 = distances.iter().sum();
|
|
521
|
+
if total == 0.0 { break; }
|
|
522
|
+
|
|
523
|
+
let mut target = self.rng.gen::<f32>() * total;
|
|
524
|
+
for (i, &d) in distances.iter().enumerate() {
|
|
525
|
+
target -= d;
|
|
526
|
+
if target <= 0.0 {
|
|
527
|
+
centroids.push(positions[i]);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Run iterations
|
|
534
|
+
let mut assignments = vec![0usize; positions.len()];
|
|
535
|
+
|
|
536
|
+
for _ in 0..iterations {
|
|
537
|
+
// Assign points to nearest centroid
|
|
538
|
+
for (i, p) in positions.iter().enumerate() {
|
|
539
|
+
let mut min_dist = f32::MAX;
|
|
540
|
+
let mut best_cluster = 0;
|
|
541
|
+
|
|
542
|
+
for (j, c) in centroids.iter().enumerate() {
|
|
543
|
+
let dx = p.0 - c.0;
|
|
544
|
+
let dy = p.1 - c.1;
|
|
545
|
+
let dist = dx * dx + dy * dy;
|
|
546
|
+
|
|
547
|
+
if dist < min_dist {
|
|
548
|
+
min_dist = dist;
|
|
549
|
+
best_cluster = j;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
assignments[i] = best_cluster;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Update centroids
|
|
557
|
+
let mut sums = vec![(0.0f32, 0.0f32); k];
|
|
558
|
+
let mut counts = vec![0usize; k];
|
|
559
|
+
|
|
560
|
+
for (i, &cluster) in assignments.iter().enumerate() {
|
|
561
|
+
sums[cluster].0 += positions[i].0;
|
|
562
|
+
sums[cluster].1 += positions[i].1;
|
|
563
|
+
counts[cluster] += 1;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (j, c) in centroids.iter_mut().enumerate() {
|
|
567
|
+
if counts[j] > 0 {
|
|
568
|
+
c.0 = sums[j].0 / counts[j] as f32;
|
|
569
|
+
c.1 = sums[j].1 / counts[j] as f32;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Return cluster assignments for all cells
|
|
575
|
+
let mut result = vec![0u32; self.cells.len()];
|
|
576
|
+
for (i, &cell_idx) in cell_indices.iter().enumerate() {
|
|
577
|
+
result[cell_idx] = assignments[i] as u32;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
result
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/// Get size of grid
|
|
584
|
+
#[wasm_bindgen]
|
|
585
|
+
pub fn size(&self) -> usize {
|
|
586
|
+
self.cells.len()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/// Get width
|
|
590
|
+
#[wasm_bindgen]
|
|
591
|
+
pub fn width(&self) -> usize {
|
|
592
|
+
self.width
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/// Get height
|
|
596
|
+
#[wasm_bindgen]
|
|
597
|
+
pub fn height(&self) -> usize {
|
|
598
|
+
self.height
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/// Clear all cells
|
|
602
|
+
#[wasm_bindgen]
|
|
603
|
+
pub fn clear(&mut self) {
|
|
604
|
+
for cell in self.cells.iter_mut() {
|
|
605
|
+
*cell = CellState::default();
|
|
606
|
+
}
|
|
607
|
+
self.territory_counts.clear();
|
|
608
|
+
self.total_population = 0.0;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
613
|
+
// FLOW FIELD WASM
|
|
614
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
615
|
+
|
|
616
|
+
/// Flow field computation in WASM
|
|
617
|
+
#[wasm_bindgen]
|
|
618
|
+
pub struct FlowFieldWasm {
|
|
619
|
+
width: usize,
|
|
620
|
+
height: usize,
|
|
621
|
+
velocity_x: Vec<f32>,
|
|
622
|
+
velocity_y: Vec<f32>,
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[wasm_bindgen]
|
|
626
|
+
impl FlowFieldWasm {
|
|
627
|
+
#[wasm_bindgen(constructor)]
|
|
628
|
+
pub fn new(width: usize, height: usize) -> Self {
|
|
629
|
+
let size = width * height;
|
|
630
|
+
Self {
|
|
631
|
+
width,
|
|
632
|
+
height,
|
|
633
|
+
velocity_x: vec![0.0; size],
|
|
634
|
+
velocity_y: vec![0.0; size],
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/// Add a source (outward flow)
|
|
639
|
+
#[wasm_bindgen]
|
|
640
|
+
pub fn add_source(&mut self, x: f32, y: f32, strength: f32) {
|
|
641
|
+
for j in 0..self.height {
|
|
642
|
+
for i in 0..self.width {
|
|
643
|
+
let dx = i as f32 - x;
|
|
644
|
+
let dy = j as f32 - y;
|
|
645
|
+
let dist_sq = dx * dx + dy * dy + 0.0001;
|
|
646
|
+
let dist = dist_sq.sqrt();
|
|
647
|
+
|
|
648
|
+
let idx = j * self.width + i;
|
|
649
|
+
self.velocity_x[idx] += strength * dx / (dist * dist_sq);
|
|
650
|
+
self.velocity_y[idx] += strength * dy / (dist * dist_sq);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/// Add a vortex (rotational flow)
|
|
656
|
+
#[wasm_bindgen]
|
|
657
|
+
pub fn add_vortex(&mut self, x: f32, y: f32, strength: f32) {
|
|
658
|
+
for j in 0..self.height {
|
|
659
|
+
for i in 0..self.width {
|
|
660
|
+
let dx = i as f32 - x;
|
|
661
|
+
let dy = j as f32 - y;
|
|
662
|
+
let dist_sq = dx * dx + dy * dy + 0.0001;
|
|
663
|
+
|
|
664
|
+
let idx = j * self.width + i;
|
|
665
|
+
self.velocity_x[idx] += -strength * dy / dist_sq;
|
|
666
|
+
self.velocity_y[idx] += strength * dx / dist_sq;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/// Sample velocity at position (bilinear interpolation)
|
|
672
|
+
#[wasm_bindgen]
|
|
673
|
+
pub fn sample(&self, x: f32, y: f32) -> Vec<f32> {
|
|
674
|
+
let x0 = (x.floor() as usize).min(self.width - 2);
|
|
675
|
+
let y0 = (y.floor() as usize).min(self.height - 2);
|
|
676
|
+
let x1 = x0 + 1;
|
|
677
|
+
let y1 = y0 + 1;
|
|
678
|
+
|
|
679
|
+
let tx = x - x0 as f32;
|
|
680
|
+
let ty = y - y0 as f32;
|
|
681
|
+
|
|
682
|
+
let i00 = y0 * self.width + x0;
|
|
683
|
+
let i10 = y0 * self.width + x1;
|
|
684
|
+
let i01 = y1 * self.width + x0;
|
|
685
|
+
let i11 = y1 * self.width + x1;
|
|
686
|
+
|
|
687
|
+
let vx = self.velocity_x[i00] * (1.0 - tx) * (1.0 - ty)
|
|
688
|
+
+ self.velocity_x[i10] * tx * (1.0 - ty)
|
|
689
|
+
+ self.velocity_x[i01] * (1.0 - tx) * ty
|
|
690
|
+
+ self.velocity_x[i11] * tx * ty;
|
|
691
|
+
|
|
692
|
+
let vy = self.velocity_y[i00] * (1.0 - tx) * (1.0 - ty)
|
|
693
|
+
+ self.velocity_y[i10] * tx * (1.0 - ty)
|
|
694
|
+
+ self.velocity_y[i01] * (1.0 - tx) * ty
|
|
695
|
+
+ self.velocity_y[i11] * tx * ty;
|
|
696
|
+
|
|
697
|
+
vec![vx, vy]
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/// Compute divergence field
|
|
701
|
+
#[wasm_bindgen]
|
|
702
|
+
pub fn compute_divergence(&self) -> Vec<f32> {
|
|
703
|
+
let mut div = vec![0.0; self.width * self.height];
|
|
704
|
+
|
|
705
|
+
for j in 1..self.height - 1 {
|
|
706
|
+
for i in 1..self.width - 1 {
|
|
707
|
+
let idx = j * self.width + i;
|
|
708
|
+
let dudx = (self.velocity_x[idx + 1] - self.velocity_x[idx - 1]) * 0.5;
|
|
709
|
+
let dvdy = (self.velocity_y[idx + self.width] - self.velocity_y[idx - self.width]) * 0.5;
|
|
710
|
+
div[idx] = dudx + dvdy;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
div
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/// Compute curl field
|
|
718
|
+
#[wasm_bindgen]
|
|
719
|
+
pub fn compute_curl(&self) -> Vec<f32> {
|
|
720
|
+
let mut curl = vec![0.0; self.width * self.height];
|
|
721
|
+
|
|
722
|
+
for j in 1..self.height - 1 {
|
|
723
|
+
for i in 1..self.width - 1 {
|
|
724
|
+
let idx = j * self.width + i;
|
|
725
|
+
let dvdx = (self.velocity_y[idx + 1] - self.velocity_y[idx - 1]) * 0.5;
|
|
726
|
+
let dudy = (self.velocity_x[idx + self.width] - self.velocity_x[idx - self.width]) * 0.5;
|
|
727
|
+
curl[idx] = dvdx - dudy;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
curl
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/// Clear field
|
|
735
|
+
#[wasm_bindgen]
|
|
736
|
+
pub fn clear(&mut self) {
|
|
737
|
+
self.velocity_x.fill(0.0);
|
|
738
|
+
self.velocity_y.fill(0.0);
|
|
739
|
+
}
|
|
740
|
+
}
|