Spaces:
Paused
Paused
Download engine/grid.js from Almaatla/SpaceCities: direct link, hf CLI and curl.
- Browser
- Download file 4.42 kB
-
https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/engine/grid.js
- Command line
-
hf download hf://spaces/Almaatla/SpaceCities/engine/grid.js
-
curl -L -o grid.js https://huggingface.co/spaces/Almaatla/SpaceCities/resolve/main/engine/grid.js
4.42 kB
| // @ts-check | |
| /* ============================================================ | |
| Uniform spatial hash over units, rebuilt once per tick by sim.js. It's a | |
| BROAD PHASE only: a query returns candidate units whose cell is near a point, | |
| and the caller still does the exact live-distance test it did before. That | |
| turns the three per-tick O(n^2) neighbour scans β separation, movement | |
| avoidance, and combat target acquisition β into local lookups, which is what | |
| lets a Gigantic (4x) map with hundreds of units stay inside the frame budget. | |
| The grid is built from pre-movement positions and reused through the whole | |
| tick (units move, then separate, after the build), so every query box is | |
| padded by an extra ring of cells. One tick's displacement is far under a | |
| cell, so the padded candidate set is always a superset of the true | |
| neighbours β no interaction is ever missed, only a few extra candidates get | |
| the cheap distance check and fall out. | |
| When state.unitGrid is absent (the many unit tests that call movement / | |
| combat / separation directly without a full tick) every consumer falls back | |
| to the original full scan, so their behaviour is byte-for-byte unchanged. | |
| ============================================================ */ | |
| ; | |
| const CELL = 96; | |
| // Integer cell key instead of a "cx,cy" string: the old key allocated + hashed a string for | |
| // every unit inserted AND every cell queried, every tick β pure per-tick garbage on the hot | |
| // path. Packing (cx,cy) into one int is a plain arithmetic Map key. KEY_PAD offsets the few | |
| // negative cells the query pad reaches; KEY_STRIDE exceeds the max cells-per-axis of any map | |
| // (Gigantic β 67), so the packing is collision-free. This changes NOTHING observable: cells | |
| // bucket the same units in the same order, and queryNeighbors visits cells in the same fixed | |
| // loop, so candidate lists are byte-for-byte identical β the determinism test stays green. | |
| const KEY_PAD = 16; // headroom for the most negative cell any query pad reaches (radius up to ~1400px) | |
| const KEY_STRIDE = 4096; // > max cells per axis (a huge map is ~67), so (cx+PAD) and (cy+PAD) never overlap | |
| function cellKey(cx, cy) { return (cx + KEY_PAD) * KEY_STRIDE + (cy + KEY_PAD); } | |
| /** @param {State} state */ | |
| export function buildUnitGrid(state) { | |
| const buckets = new Map(); | |
| let i = 0; | |
| for (const u of state.units.values()) { | |
| u._gi = i++; // stable Map-order index: lets separation process each pair once, deterministically | |
| const k = cellKey(Math.floor(u.x / CELL), Math.floor(u.y / CELL)); | |
| let arr = buckets.get(k); | |
| if (!arr) buckets.set(k, (arr = [])); | |
| arr.push(u); | |
| } | |
| return { cell: CELL, buckets }; | |
| } | |
| // Reused across calls instead of allocating a fresh result array every query: | |
| // every caller (repair.js, movement.js, separation.js, combat.js) reads the | |
| // returned array in an immediate for-of and discards it before making its next | |
| // move β none stores it past that, and none makes a second queryNeighbors call | |
| // while a prior result is still being iterated (all call sites are synchronous, | |
| // single-threaded, read-then-discard) β so one shared scratch buffer is safe and | |
| // matches this file's stated no-per-tick-garbage goal (see header comment). | |
| const _scratch = []; | |
| // Candidate units in every cell overlapping the (radius, +1 ring of padding) | |
| // box around (x, y). A superset of the units within `radius` β callers filter | |
| // by exact distance. Cells are visited in a fixed numeric order and bucket | |
| // contents keep Map insertion order, so iteration is fully deterministic. | |
| // Returns a reused scratch array (see _scratch above) β read it immediately | |
| // and don't hold onto it past that; the next queryNeighbors call overwrites it. | |
| /** @param {*} grid @param {number} x @param {number} y @param {number} radius @returns {(Unit)[]} */ | |
| export function queryNeighbors(grid, x, y, radius) { | |
| const cell = grid.cell; | |
| const mincx = Math.floor((x - radius) / cell) - 1; | |
| const maxcx = Math.floor((x + radius) / cell) + 1; | |
| const mincy = Math.floor((y - radius) / cell) - 1; | |
| const maxcy = Math.floor((y + radius) / cell) + 1; | |
| _scratch.length = 0; | |
| for (let cy = mincy; cy <= maxcy; cy++) { | |
| for (let cx = mincx; cx <= maxcx; cx++) { | |
| const arr = grid.buckets.get(cellKey(cx, cy)); | |
| if (arr) for (const u of arr) _scratch.push(u); | |
| } | |
| } | |
| return _scratch; | |
| } | |