diff --git a/docs/noise/cliffs-NOTES.md b/docs/noise/cliffs-NOTES.md index ac5f151..fbba376 100644 --- a/docs/noise/cliffs-NOTES.md +++ b/docs/noise/cliffs-NOTES.md @@ -5,7 +5,7 @@ > | | recall | precision | wrong orientation | > | --- | --- | --- | --- | > | **Nauvis** | 1.0000 | 1.0000 | **0 / 334** | -> | **Vulcanus**, as shipped | **0.9675** | **0.9743** | **31 / 1518 = 2.0%** | +> | **Vulcanus**, as shipped | **0.9758** | **0.9727** | 2.0% | > | **Vulcanus**, no lava rejection | 0.9758 | 0.8719 | 37 / 1531 = 2.4% | > > **Read the shipping row.** The renderer applies `tryToAddCliff`'s lava-collision @@ -16,10 +16,12 @@ > `test/vulcanusCliffEntities.spec.ts`; the no-rejection row is kept because > `test/cliffOrientationOracle.spec.ts` deliberately scores the larger set. > -> What is left on Vulcanus: 51 of the game's 1569 missing, 40 of our 1558 -> spurious, 31 matched cells carrying a wrong orientation. **13 of the 51 are a -> TILE question, not a cliff one** - real cliffs whose collision box hits our lava -> at Chebyshev depth 1, our own perimeter, never deeper. Remainder in **#84**. +> What is left on Vulcanus: 38 of the game's 1569 missing and 43 of our 1574 +> spurious. **The 13 that used to be a "TILE question" were neither** - not the +> lava mask (a dense 994-position capture found ZERO mismatches) but the +> collision box, which the port had collapsed from `rotbb`'s rotated rectangle to +> its bounding box. Fixed; see `## The lava perimeter was the COLLISION BOX` in +> `vulcanus-cliffs-NOTES.md`. Remainder in **#84**. > > **Read `## ROOT CAUSE, 2026-08-01` (further down) before anything else in this > file.** It is the resolution of issue #18: `multisample`'s offsets are in the diff --git a/docs/noise/vulcanus-cliffs-NOTES.md b/docs/noise/vulcanus-cliffs-NOTES.md index 62d2f1e..d3c8c39 100644 --- a/docs/noise/vulcanus-cliffs-NOTES.md +++ b/docs/noise/vulcanus-cliffs-NOTES.md @@ -2,12 +2,15 @@ > ## STATUS, 2026-08-01: issue #18 is CLOSED; remainder tracked in #84 > -> **As shipped** (with `tryToAddCliff`'s lava-collision rejection, which is what -> the renderer runs): recall **0.9675**, precision **0.9743**, **31 / 1518 = 2.0%** -> wrong orientations. Without the rejection the same code scores recall 0.9758, -> precision 0.8719, 37 / 1531 = 2.4% - and it is that second row, not a defect, -> that #84 opened against. The rejection drops 198 cells, 185 false positives to -> 13 true. Before the fix: recall 0.806 / 0.938 / 0.853 and 12.5% wrong. +> **As shipped**: recall **0.9758**, precision **0.9727**, ratio 1.003. Region +> `[0,0]` reproduces the game's cliff set entirely (recall **1.0000**). Before +> #18's fix: recall 0.806 / 0.938 / 0.853 and 12.5% wrong orientations. +> +> Two rules do that work and both were found late: `tryToAddCliff`'s +> lava-collision rejection (185 false positives across the three oracle regions, +> precision 0.8719 -> 0.9743) and the fact that its box is a **rotated** +> rectangle rather than its bounding box (recovers 13 real cliffs, recall +> 0.9675 -> 0.9758). See the last two sections. > > **Root cause: `multisample`'s offsets are in the calling noise program's GRID > UNITS, not tiles**, so `vulcanus_basalt_lakes_multisample`'s `min` is a 4-tile @@ -17,9 +20,9 @@ > `test/multisampleGrid.spec.ts`. > > **Every accuracy table below this banner is the PRE-FIX state**, kept because the -> reasoning is the useful part. Do not quote one as current. The one exception is -> the final section, `## The lava rejection accounted for the "excess"`, which is -> post-fix and is where the numbers in this banner come from. +> reasoning is the useful part. Do not quote one as current. The exceptions are +> the last two sections, which are post-fix and are where this banner's numbers +> come from. Note the SECOND of them corrects the first - read both. Factorio 2.1.12 (build 87038, mac-arm64). Ported 2026-07-26. Companion to `cliffs-NOTES.md`, which holds the reverse-engineering of the placement rule @@ -1051,3 +1054,84 @@ indistinguishable from "the substitution never ran". their placement: reading the other elevation channel makes the lava classification dramatically worse, not better, so the perimeter error is not a channel mistake. It is somewhere else in `vulcanusCatalog`. + +## The lava perimeter was the COLLISION BOX, not the mask (2026-08-01) + +The section above concluded that a "sub-tile disagreement about where lava +stops" cost 13 real cliffs their placement, and `vulcanusCliffEntities.spec.ts` +carried the same claim: the tile resolver is "off by about one tile SOMEWHERE". +**That was wrong.** The mask is exact; the collision box was the wrong shape. + +### The mask was exonerated by a capture designed to convict it + +`oracle-vulcanus-lava-boundary.seed123456.json` samples Chebyshev radius-4 +neighbourhoods around the 35 tiles our mask calls lava inside a real cliff's box +- deliberately the hardest positions on the map rather than a representative +sample. Result over 994 positions: **0 lava mismatches in either direction**, and +at the 35 accusing tiles themselves **35/35 agreement**. The game has lava +exactly where we say it does. + +That capture was worth making because the existing 381-position survey +structurally could not answer the question: its sensitivity was measured by +planting scale factors on `lava`'s probability, and `1.02` and `1.2` both still +pass. A sparse survey cannot see a sub-tile boundary shift. + +### `rotbb` boxes are ROTATED, and the port used their bounding box + +`rotbb(x, y, size, intersect)` (`entity-util.lua:9`) returns +`{{cx - x_dist, cy - y_dist}, {cx + x_dist, cy + y_dist}, 1/8}` - a rectangle +**plus an orientation of 1/8**, i.e. 45 degrees. Sixteen of the twenty cliff +orientations are built with it; only the four straight ones are plain +axis-aligned rectangles. + +`CLIFF_ORIENTATION_COLLISION_BOX` holds the axis-aligned bounding box. That is +the correct BROAD phase - `wouldCollide` derives its tile rectangle from a +fixed-point floor and scans an inclusive rect, which `cliffCollisionTileBox` +reproduces - but the collision itself is against the rotated rectangle, and the +AABB overruns it at all four corners. `cliffBoxCoversTile` now runs a +separating-axis narrow phase; `test/cliffOrientedBox.spec.ts` pins the geometry. + +A note on `rotbbBox` used to say `intersect` could be dropped because it does not +move the AABB. True of the AABB, false of the collision: `intersect` sets how the +diagonal splits, hence which corners are empty. + +| | AABB (before) | oriented (after) | +| --- | --- | --- | +| recall | 0.9675 | **0.9758** | +| precision | 0.9743 | 0.9727 | +| `[0,0]` recall | 0.9788 | **1.0000** | +| level-sweep recall | 0.951 at level 20 | **~1.000 at every level** | + +It clears **13 of 13** false rejections while keeping 182 of the 185 rejections +that remove genuine false positives - so it is the correct shape, not a +loosening that trades precision for recall. + +### Two corrections this forces + +- **PR #86's "gap 0.067 -> 0.018" is wrong; it is 0.024.** That figure was + measured with the over-aggressive AABB rejection, which deleted cells the game + keeps and so flattered exactly the ratio it was reporting. **A too-strong + correction hides the thing it is correcting.** The remaining over-placement + below elevation 120 is real and still open - and it is now pure over-placement, + with no recall cost. +- **"All 13 sit at Chebyshev depth 1 in our lava" was a true measurement that + pointed at the wrong suspect.** The box's four corners ARE its perimeter, so a + corner-shaped box error produces exactly the signature a one-tile-fat mask + would. Two mechanisms, one fingerprint. A statistic can only rule a suspect + out if it would come out DIFFERENTLY for each candidate, and a depth histogram + comes out the same for both. + +### What was checked and cleared on the way + +- **The inclusive-floor fringe is real engine behaviour, not our bug.** + `wouldCollide` uses `(box + position) >> 8` and an inclusive rect, so a box + edge landing exactly on a tile boundary does pull that tile in. Already + established by disassembly; re-confirmed as not the cause. +- **Chunk ordering is not it.** If the generator read a partly-generated tile + grid, the offending tiles would sit disproportionately in neighbouring chunks. + They do not: 2 of 13 cross a chunk boundary against 50 of 185 in the control - + *less* than baseline. +- **6 of the 13 also carry a wrong orientation** (against a 2.0% base rate, ~23x + enrichment), and in every case the game's is a smaller `-to-none` variant of + ours. A wrong orientation means the wrong box, so the two defects compound. + Those 6 belong to the standing orientation residual, which is unchanged. diff --git a/src/noise/cliffs/cliffCatalog.ts b/src/noise/cliffs/cliffCatalog.ts index 0f9472c..cb684fe 100644 --- a/src/noise/cliffs/cliffCatalog.ts +++ b/src/noise/cliffs/cliffCatalog.ts @@ -274,13 +274,131 @@ export type CliffCollisionBox = readonly [number, number, number, number]; * `intersect` was. So the AABB is exactly the square `[x, x+size] x [y, y+size]` * and `intersect` only decides how the diagonal is split inside it. * - * That is why this helper does not take `intersect`. `test/cliffOrientation.spec.ts` - * re-derives it from the full rotated rectangle instead of restating it here. + * **The AABB is the BROAD phase only - `intersect` is load-bearing after all.** + * A note here used to say `intersect` could be dropped because it does not move + * the AABB. That is true of the AABB and false of the collision: `rotbb` tags + * the box with orientation `1/8` and the engine collides against the ROTATED + * rectangle, whose corners the AABB overruns. `intersect` decides how far the + * diagonal is split, hence which corners are empty. See + * {@link CLIFF_ORIENTATION_ROTBB} and {@link cliffBoxCoversTile}; measured in + * `test/cliffOrientedBox.spec.ts`. */ function rotbbBox(x: number, y: number, size: number): CliffCollisionBox { return [x, y, x + size, y + size]; } +/** + * `rotbb(x, y, size, intersect)`'s four arguments per orientation id, verbatim + * from `create_cliff_data_specification` (`entity-util.lua:85`), or `null` for + * the four straight orientations, whose boxes are written out as plain + * axis-aligned rectangles with no orientation tag. + * + * This exists because {@link CLIFF_ORIENTATION_COLLISION_BOX} is only the + * bounding box. The engine's collision uses the rotated rectangle itself. + */ +export const CLIFF_ORIENTATION_ROTBB: readonly ( + | readonly [number, number, number, number] + | null +)[] = [ + null, // 0 west-to-east + null, // 1 north-to-south + null, // 2 east-to-west + null, // 3 south-to-north + [-3.5, -3, 4.5, 3], // 4 west-to-north + [-1, -3, 4.5, 1.5], // 5 north-to-east + [-1, -0.5, 3.5, 2.5], // 6 east-to-south + [-2.5, -0.5, 3.5, 1], // 7 south-to-west + [-3.5, -1.5, 4.5, 1.5], // 8 west-to-south + [-2.5, -3, 3.5, 2.5], // 9 north-to-west + [-1, -3, 3.5, 1], // 10 east-to-north + [-1, -1.5, 4.5, 3], // 11 south-to-east + [-3, -1.5, 3, 2], // 12 west-to-none + [0, -1.5, 3, 1], // 13 none-to-east + [0, -0.5, 2.5, 2], // 14 east-to-none + [-2.5, -0.5, 2.51, 0.5], // 15 none-to-west + [-1, -2.5, 3, 1], // 16 north-to-none + [-1, -0.5, 3, 2.5], // 17 none-to-south + [-2, -0.5, 3, 0.5], // 18 south-to-none + [-2, -2.5, 3, 2], // 19 none-to-north +]; + +const SQRT2 = 1.4142135623730951; + +/** + * Does the tile `[tx, tx+1] x [ty, ty+1]` overlap the collision shape of a + * cliff of orientation `id` centred at `(centerX, centerY)`? + * + * For the four straight orientations the shape IS the axis-aligned box, so any + * tile the broad phase enumerated overlaps it and this returns `true`. For the + * sixteen `rotbb` orientations the shape is that rectangle rotated 45 degrees + * clockwise (Factorio orientation `1/8`, and `+y` is south), which the AABB + * overruns at all four corners - a separating-axis test over the two world axes + * and the rectangle's own two decides it. + * + * **Why this is not gold-plating.** Using the AABB drops real cliffs: across the + * three Vulcanus oracle regions the game placed 13 cliffs whose AABB contains + * lava and whose rotated box does not, and it kept every one. Narrowing to the + * oriented rectangle clears **13 of 13** while retaining 182 of the 185 + * rejections that were removing genuine false positives - so it is not a + * loosening that trades precision for recall, it is the correct shape. + */ +export function cliffBoxCoversTile( + id: number, + centerX: number, + centerY: number, + tx: number, + ty: number, +): boolean { + const spec = CLIFF_ORIENTATION_ROTBB[id]; + if (spec === undefined || spec === null) return true; + const [bx, by, size, intersect] = spec; + const dist = (size / 2) * SQRT2; + const yRatio = intersect / size; + const xDist = (1 - yRatio) * dist; + const yDist = yRatio * dist; + const cx = centerX + bx + size / 2; + const cy = centerY + by + size / 2; + // cos 45 = sin 45; clockwise in screen coords (x east, y south). + const k = Math.SQRT1_2; + const corners: readonly (readonly [number, number])[] = [ + [-xDist, -yDist], + [xDist, -yDist], + [xDist, yDist], + [-xDist, yDist], + ].map(([u, v]) => [cx + (u - v) * k, cy + (u + v) * k] as const); + const square: readonly (readonly [number, number])[] = [ + [tx, ty], + [tx + 1, ty], + [tx + 1, ty + 1], + [tx, ty + 1], + ]; + const axes: readonly (readonly [number, number])[] = [ + [1, 0], + [0, 1], + [k, k], + [-k, k], + ]; + for (const [ax, ay] of axes) { + let aMin = Infinity; + let aMax = -Infinity; + let bMin = Infinity; + let bMax = -Infinity; + for (const [px, py] of corners) { + const d = px * ax + py * ay; + if (d < aMin) aMin = d; + if (d > aMax) aMax = d; + } + for (const [px, py] of square) { + const d = px * ax + py * ay; + if (d < bMin) bMin = d; + if (d > bMax) bMax = d; + } + // Touching is not overlapping: a tile the rectangle only grazes is free. + if (aMax <= bMin || bMax <= aMin) return false; + } + return true; +} + /** * `CliffOrientation` id -> the orientation's `collision_bounding_box`, at * `scale = 1.0` (both `cliff` and `cliff-vulcanus`), relative to the cliff's diff --git a/src/noise/cliffs/cliffPlacement.ts b/src/noise/cliffs/cliffPlacement.ts index a0df68b..fd9c875 100644 --- a/src/noise/cliffs/cliffPlacement.ts +++ b/src/noise/cliffs/cliffPlacement.ts @@ -13,7 +13,9 @@ import { CLIFF_CELL_CENTER_X, CLIFF_CELL_CENTER_Y, CLIFF_GRID_SIZE, + cliffBoxCoversTile, cliffCollisionTileBox, + cliffOrientationForCode, getModifiedElevationInterval, isCliffPlaced, } from "./cliffCatalog"; @@ -318,14 +320,25 @@ export function makeCliffPlacementFromFields( * `tryToAddCliff`'s rejection, as a predicate on an already-placed cell: scan * the orientation's collision box and drop the cell if any tile in it collides. * With no `tileCollides` supplied this is a constant `false` and costs nothing. + * + * **Two phases, because sixteen of the twenty boxes are rotated.** + * `cliffCollisionTileBox` is the BROAD phase - the axis-aligned tile rectangle + * `wouldCollide` derives with `(box + position) >> 8`. For the four straight + * orientations that is the whole shape. For the sixteen `rotbb` ones the real + * shape is that rectangle turned 45 degrees, so `cliffBoxCoversTile` runs a + * narrow phase and discards the AABB's four empty corners. Skipping it drops + * 13 real Vulcanus cliffs whose corners happen to overhang lava. */ const rejected = (code: number, x: number, y: number): boolean => { if (tileCollides === undefined) return false; const box = cliffCollisionTileBox(code, x, y); // `undefined` only for a code that places nothing, which cannot reach here. if (box === undefined) return false; + const id = cliffOrientationForCode(code); + if (id === undefined) return false; for (let tx = box.left; tx <= box.right; tx++) - for (let ty = box.top; ty <= box.bottom; ty++) if (tileCollides(tx, ty)) return true; + for (let ty = box.top; ty <= box.bottom; ty++) + if (tileCollides(tx, ty) && cliffBoxCoversTile(id, x, y, tx, ty)) return true; return false; }; diff --git a/test/cliffOrientedBox.spec.ts b/test/cliffOrientedBox.spec.ts new file mode 100644 index 0000000..9ef3d03 --- /dev/null +++ b/test/cliffOrientedBox.spec.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + CLIFF_ORIENTATION_COLLISION_BOX, + CLIFF_ORIENTATION_NAMES, + CLIFF_ORIENTATION_ROTBB, + cliffBoxCoversTile, +} from "../src/noise/cliffs/cliffCatalog"; + +/** + * **`rotbb` boxes are ROTATED, and the port used their bounding box** (issue + * #84, the lava-perimeter thread). + * + * `rotbb(x, y, size, intersect)` (`base/prototypes/entity/entity-util.lua:9`) + * returns `{{cx - x_dist, cy - y_dist}, {cx + x_dist, cy + y_dist}, 1/8}` - a + * rectangle **plus an orientation of 1/8**, i.e. 45 degrees. Sixteen of the + * twenty cliff orientations are built with it; only the four straight ones are + * written as plain axis-aligned rectangles. + * + * `CLIFF_ORIENTATION_COLLISION_BOX` holds the axis-aligned BOUNDING box, which + * is the right broad phase - `wouldCollide` derives its tile rectangle from a + * fixed-point floor, and that is what `cliffCollisionTileBox` reproduces. But + * the collision itself is against the rotated rectangle, and the AABB overruns + * it at all four corners. Using the AABB is therefore strictly too eager, and + * only ever in the corners. + * + * **How it was found, because the route matters more than the fix.** The + * negative-space oracle said 13 real Vulcanus cliffs had lava inside their box, + * and the standing explanation - written into `vulcanusCliffEntities.spec.ts` + * and the notes - was that our lava mask was "off by about one tile SOMEWHERE". + * A dense 994-position capture at exactly those boundaries + * (`oracle-vulcanus-lava-boundary.seed123456.json`) found **zero** lava + * mismatches, 35/35 correct at the accusing tiles. The mask was innocent; the + * shape was wrong. Re-testing every hit against the rotated rectangle clears + * **13 of 13** while keeping 182 of the 185 rejections that remove genuine false + * positives. + */ +describe("cliff collision boxes are rotated, not axis-aligned", () => { + it("has a rotbb spec for the 16 diagonal orientations and none for the 4 straight", () => { + expect(CLIFF_ORIENTATION_ROTBB.length).toBe(CLIFF_ORIENTATION_COLLISION_BOX.length); + const straight = CLIFF_ORIENTATION_ROTBB.filter((s) => s === null).length; + expect(straight).toBe(4); + // The four axis-aligned ones are exactly the straight walls. + for (let id = 0; id < 4; id++) expect(CLIFF_ORIENTATION_ROTBB[id]).toBeNull(); + for (const name of [0, 1, 2, 3].map((i) => CLIFF_ORIENTATION_NAMES[i])) + expect(name).toMatch(/^(west-to-east|north-to-south|east-to-west|south-to-north)$/); + }); + + /** + * The AABB is derived from the same `(x, y, size)` the rotbb spec carries, so + * the two cannot drift apart silently. This is the invariant that makes the + * broad phase a genuine superset of the narrow one. + */ + it("each rotbb's bounding box is the square the AABB table already holds", () => { + for (const [id, spec] of CLIFF_ORIENTATION_ROTBB.entries()) { + if (spec === null) continue; + const [x, y, size] = spec; + expect(CLIFF_ORIENTATION_COLLISION_BOX[id]).toEqual([x, y, x + size, y + size]); + } + }); + + /** + * The point of the whole change: for a rotated box the AABB's corner tiles are + * NOT covered, and its edge-midpoint tiles are. A test that only checked the + * centre would pass for the AABB too. + */ + it("excludes the AABB's corners and keeps the box's own centre", () => { + let cornersExcluded = 0; + let centresIncluded = 0; + let rotated = 0; + for (const [id, spec] of CLIFF_ORIENTATION_ROTBB.entries()) { + if (spec === null) continue; + rotated++; + const [x, y, size] = spec; + // Corner tiles of the AABB, in cell-centre-relative coordinates. + for (const [cx, cy] of [ + [x, y], + [x + size - 1, y], + [x, y + size - 1], + [x + size - 1, y + size - 1], + ]) { + if (!cliffBoxCoversTile(id, 0, 0, Math.floor(cx), Math.floor(cy))) cornersExcluded++; + } + // The tile containing the rectangle's own centre is inside it under any + // rotation, so it must survive. Edge midpoints were tried here first and + // are the WRONG probe: `Math.floor` of an AABB edge can name a tile that + // is only partly inside the AABB at all (ids 7, 9, 17, 18), so a miss + // there says nothing about the narrow phase. + if (cliffBoxCoversTile(id, 0, 0, Math.floor(x + size / 2), Math.floor(y + size / 2))) + centresIncluded++; + } + // 16 rotated orientations x 4 corners = 64 candidate corners, of which + // **26 are excluded** (measured 2026-08-01). Not a majority: how much of its + // AABB a rotated rectangle fills depends on `intersect`, and the corner tile + // is a whole 1x1 square sitting inside the corner rather than the corner + // point itself. The bound is the measured value with headroom, not a round + // number - what it has to catch is the narrow phase becoming a no-op, which + // would give 0. + expect(cornersExcluded).toBeGreaterThan(20); + // And the narrow phase must not have eaten the box: every rotated box keeps + // its own centre. Without this, `cliffBoxCoversTile` could return `false` + // everywhere and the corner assertion above would still pass. + expect(rotated).toBe(16); + expect(centresIncluded).toBe(16); + }); + + /** The four straight orientations are unrotated, so nothing is ever carved off. */ + it("never narrows the four axis-aligned boxes", () => { + for (let id = 0; id < 4; id++) + for (let tx = -4; tx <= 4; tx++) + for (let ty = -4; ty <= 4; ty++) expect(cliffBoxCoversTile(id, 0, 0, tx, ty)).toBe(true); + }); +}); diff --git a/test/fixtures/PROVENANCE.json b/test/fixtures/PROVENANCE.json index 7f9838d..854988d 100644 --- a/test/fixtures/PROVENANCE.json +++ b/test/fixtures/PROVENANCE.json @@ -244,6 +244,10 @@ "factorioVersion": "2.1.12", "evidence": "stated in the fixture's own _comment" }, + "oracle-vulcanus-lava-boundary.seed123456.json": { + "factorioVersion": "2.1.12", + "evidence": "captured 2026-08-01 by test/oracle/capture.ts vulcanus-lava-boundary against the installed binary, which pnpm refs:sync --check reported in sync at 2.1.12 at capture time. Dense radius-4 neighbourhoods around the 35 tiles the cliff rejection accused the lava mask over; it EXONERATED the mask (0 lava mismatches in 994 positions) and redirected the search to the collision box." + }, "oracle-vulcanus-tile-names.natural-mapseed123456.json": { "factorioVersion": "2.1.12", "evidence": "stated in the fixture's own _comment" diff --git a/test/fixtures/oracle-vulcanus-lava-boundary.seed123456.json b/test/fixtures/oracle-vulcanus-lava-boundary.seed123456.json new file mode 100644 index 0000000..687741b --- /dev/null +++ b/test/fixtures/oracle-vulcanus-lava-boundary.seed123456.json @@ -0,0 +1,5122 @@ +{ + "_comment": "Ground truth from Factorio 2.1.12 (Space Age enabled) via the test/oracle harness. surface.get_tile(x, y).name on a real Vulcanus surface (game.planets['vulcanus'].create_surface(), seed 123456) after real chunk generation. DENSE: Chebyshev radius-4 neighbourhoods around the 35 tiles our lava mask wrongly places inside a real cliff's collision box, so the boundary error's shape is visible. Deliberately the hardest positions for the resolver, NOT a representative sample - the agreement rate here is not comparable with oracle-vulcanus-tile-names. positions are the mod's ECHOED floored get_tile input. Regenerate: node --experimental-strip-types test/oracle/capture.ts vulcanus-lava-boundary", + "seed0": 123456, + "planet": "vulcanus", + "seeds": [ + { + "x": 88, + "y": 41 + }, + { + "x": 89, + "y": 40 + }, + { + "x": 89, + "y": 41 + }, + { + "x": 85, + "y": 45 + }, + { + "x": 86, + "y": 43 + }, + { + "x": 86, + "y": 44 + }, + { + "x": 87, + "y": 42 + }, + { + "x": 87, + "y": 43 + }, + { + "x": 87, + "y": 44 + }, + { + "x": 82, + "y": 48 + }, + { + "x": 83, + "y": 47 + }, + { + "x": 83, + "y": 48 + }, + { + "x": 24, + "y": 181 + }, + { + "x": 25, + "y": 181 + }, + { + "x": 4, + "y": 187 + }, + { + "x": 5, + "y": 187 + }, + { + "x": 6, + "y": 187 + }, + { + "x": 7, + "y": 187 + }, + { + "x": 1637, + "y": 1598 + }, + { + "x": 1693, + "y": 1599 + }, + { + "x": 1693, + "y": 1600 + }, + { + "x": 1694, + "y": 1599 + }, + { + "x": 1694, + "y": 1600 + }, + { + "x": 1695, + "y": 1600 + }, + { + "x": 1696, + "y": 1600 + }, + { + "x": 1697, + "y": 1600 + }, + { + "x": 1635, + "y": 1599 + }, + { + "x": 1636, + "y": 1599 + }, + { + "x": 1663, + "y": 1636 + }, + { + "x": 1663, + "y": 1637 + }, + { + "x": 1521, + "y": 1680 + }, + { + "x": -1052, + "y": 1016 + }, + { + "x": -1051, + "y": 1016 + }, + { + "x": -1051, + "y": 1017 + }, + { + "x": -1061, + "y": 1029 + } + ], + "radius": 4, + "positions": [ + { + "x": 84, + "y": 37 + }, + { + "x": 84, + "y": 38 + }, + { + "x": 84, + "y": 39 + }, + { + "x": 84, + "y": 40 + }, + { + "x": 84, + "y": 41 + }, + { + "x": 84, + "y": 42 + }, + { + "x": 84, + "y": 43 + }, + { + "x": 84, + "y": 44 + }, + { + "x": 84, + "y": 45 + }, + { + "x": 85, + "y": 37 + }, + { + "x": 85, + "y": 38 + }, + { + "x": 85, + "y": 39 + }, + { + "x": 85, + "y": 40 + }, + { + "x": 85, + "y": 41 + }, + { + "x": 85, + "y": 42 + }, + { + "x": 85, + "y": 43 + }, + { + "x": 85, + "y": 44 + }, + { + "x": 85, + "y": 45 + }, + { + "x": 86, + "y": 37 + }, + { + "x": 86, + "y": 38 + }, + { + "x": 86, + "y": 39 + }, + { + "x": 86, + "y": 40 + }, + { + "x": 86, + "y": 41 + }, + { + "x": 86, + "y": 42 + }, + { + "x": 86, + "y": 43 + }, + { + "x": 86, + "y": 44 + }, + { + "x": 86, + "y": 45 + }, + { + "x": 87, + "y": 37 + }, + { + "x": 87, + "y": 38 + }, + { + "x": 87, + "y": 39 + }, + { + "x": 87, + "y": 40 + }, + { + "x": 87, + "y": 41 + }, + { + "x": 87, + "y": 42 + }, + { + "x": 87, + "y": 43 + }, + { + "x": 87, + "y": 44 + }, + { + "x": 87, + "y": 45 + }, + { + "x": 88, + "y": 37 + }, + { + "x": 88, + "y": 38 + }, + { + "x": 88, + "y": 39 + }, + { + "x": 88, + "y": 40 + }, + { + "x": 88, + "y": 41 + }, + { + "x": 88, + "y": 42 + }, + { + "x": 88, + "y": 43 + }, + { + "x": 88, + "y": 44 + }, + { + "x": 88, + "y": 45 + }, + { + "x": 89, + "y": 37 + }, + { + "x": 89, + "y": 38 + }, + { + "x": 89, + "y": 39 + }, + { + "x": 89, + "y": 40 + }, + { + "x": 89, + "y": 41 + }, + { + "x": 89, + "y": 42 + }, + { + "x": 89, + "y": 43 + }, + { + "x": 89, + "y": 44 + }, + { + "x": 89, + "y": 45 + }, + { + "x": 90, + "y": 37 + }, + { + "x": 90, + "y": 38 + }, + { + "x": 90, + "y": 39 + }, + { + "x": 90, + "y": 40 + }, + { + "x": 90, + "y": 41 + }, + { + "x": 90, + "y": 42 + }, + { + "x": 90, + "y": 43 + }, + { + "x": 90, + "y": 44 + }, + { + "x": 90, + "y": 45 + }, + { + "x": 91, + "y": 37 + }, + { + "x": 91, + "y": 38 + }, + { + "x": 91, + "y": 39 + }, + { + "x": 91, + "y": 40 + }, + { + "x": 91, + "y": 41 + }, + { + "x": 91, + "y": 42 + }, + { + "x": 91, + "y": 43 + }, + { + "x": 91, + "y": 44 + }, + { + "x": 91, + "y": 45 + }, + { + "x": 92, + "y": 37 + }, + { + "x": 92, + "y": 38 + }, + { + "x": 92, + "y": 39 + }, + { + "x": 92, + "y": 40 + }, + { + "x": 92, + "y": 41 + }, + { + "x": 92, + "y": 42 + }, + { + "x": 92, + "y": 43 + }, + { + "x": 92, + "y": 44 + }, + { + "x": 92, + "y": 45 + }, + { + "x": 85, + "y": 36 + }, + { + "x": 86, + "y": 36 + }, + { + "x": 87, + "y": 36 + }, + { + "x": 88, + "y": 36 + }, + { + "x": 89, + "y": 36 + }, + { + "x": 90, + "y": 36 + }, + { + "x": 91, + "y": 36 + }, + { + "x": 92, + "y": 36 + }, + { + "x": 93, + "y": 36 + }, + { + "x": 93, + "y": 37 + }, + { + "x": 93, + "y": 38 + }, + { + "x": 93, + "y": 39 + }, + { + "x": 93, + "y": 40 + }, + { + "x": 93, + "y": 41 + }, + { + "x": 93, + "y": 42 + }, + { + "x": 93, + "y": 43 + }, + { + "x": 93, + "y": 44 + }, + { + "x": 93, + "y": 45 + }, + { + "x": 81, + "y": 41 + }, + { + "x": 81, + "y": 42 + }, + { + "x": 81, + "y": 43 + }, + { + "x": 81, + "y": 44 + }, + { + "x": 81, + "y": 45 + }, + { + "x": 81, + "y": 46 + }, + { + "x": 81, + "y": 47 + }, + { + "x": 81, + "y": 48 + }, + { + "x": 81, + "y": 49 + }, + { + "x": 82, + "y": 41 + }, + { + "x": 82, + "y": 42 + }, + { + "x": 82, + "y": 43 + }, + { + "x": 82, + "y": 44 + }, + { + "x": 82, + "y": 45 + }, + { + "x": 82, + "y": 46 + }, + { + "x": 82, + "y": 47 + }, + { + "x": 82, + "y": 48 + }, + { + "x": 82, + "y": 49 + }, + { + "x": 83, + "y": 41 + }, + { + "x": 83, + "y": 42 + }, + { + "x": 83, + "y": 43 + }, + { + "x": 83, + "y": 44 + }, + { + "x": 83, + "y": 45 + }, + { + "x": 83, + "y": 46 + }, + { + "x": 83, + "y": 47 + }, + { + "x": 83, + "y": 48 + }, + { + "x": 83, + "y": 49 + }, + { + "x": 84, + "y": 46 + }, + { + "x": 84, + "y": 47 + }, + { + "x": 84, + "y": 48 + }, + { + "x": 84, + "y": 49 + }, + { + "x": 85, + "y": 46 + }, + { + "x": 85, + "y": 47 + }, + { + "x": 85, + "y": 48 + }, + { + "x": 85, + "y": 49 + }, + { + "x": 86, + "y": 46 + }, + { + "x": 86, + "y": 47 + }, + { + "x": 86, + "y": 48 + }, + { + "x": 86, + "y": 49 + }, + { + "x": 87, + "y": 46 + }, + { + "x": 87, + "y": 47 + }, + { + "x": 87, + "y": 48 + }, + { + "x": 87, + "y": 49 + }, + { + "x": 88, + "y": 46 + }, + { + "x": 88, + "y": 47 + }, + { + "x": 88, + "y": 48 + }, + { + "x": 88, + "y": 49 + }, + { + "x": 89, + "y": 46 + }, + { + "x": 89, + "y": 47 + }, + { + "x": 89, + "y": 48 + }, + { + "x": 89, + "y": 49 + }, + { + "x": 82, + "y": 39 + }, + { + "x": 82, + "y": 40 + }, + { + "x": 83, + "y": 39 + }, + { + "x": 83, + "y": 40 + }, + { + "x": 90, + "y": 46 + }, + { + "x": 90, + "y": 47 + }, + { + "x": 90, + "y": 48 + }, + { + "x": 83, + "y": 38 + }, + { + "x": 91, + "y": 46 + }, + { + "x": 91, + "y": 47 + }, + { + "x": 91, + "y": 48 + }, + { + "x": 78, + "y": 44 + }, + { + "x": 78, + "y": 45 + }, + { + "x": 78, + "y": 46 + }, + { + "x": 78, + "y": 47 + }, + { + "x": 78, + "y": 48 + }, + { + "x": 78, + "y": 49 + }, + { + "x": 78, + "y": 50 + }, + { + "x": 78, + "y": 51 + }, + { + "x": 78, + "y": 52 + }, + { + "x": 79, + "y": 44 + }, + { + "x": 79, + "y": 45 + }, + { + "x": 79, + "y": 46 + }, + { + "x": 79, + "y": 47 + }, + { + "x": 79, + "y": 48 + }, + { + "x": 79, + "y": 49 + }, + { + "x": 79, + "y": 50 + }, + { + "x": 79, + "y": 51 + }, + { + "x": 79, + "y": 52 + }, + { + "x": 80, + "y": 44 + }, + { + "x": 80, + "y": 45 + }, + { + "x": 80, + "y": 46 + }, + { + "x": 80, + "y": 47 + }, + { + "x": 80, + "y": 48 + }, + { + "x": 80, + "y": 49 + }, + { + "x": 80, + "y": 50 + }, + { + "x": 80, + "y": 51 + }, + { + "x": 80, + "y": 52 + }, + { + "x": 81, + "y": 50 + }, + { + "x": 81, + "y": 51 + }, + { + "x": 81, + "y": 52 + }, + { + "x": 82, + "y": 50 + }, + { + "x": 82, + "y": 51 + }, + { + "x": 82, + "y": 52 + }, + { + "x": 83, + "y": 50 + }, + { + "x": 83, + "y": 51 + }, + { + "x": 83, + "y": 52 + }, + { + "x": 84, + "y": 50 + }, + { + "x": 84, + "y": 51 + }, + { + "x": 84, + "y": 52 + }, + { + "x": 85, + "y": 50 + }, + { + "x": 85, + "y": 51 + }, + { + "x": 85, + "y": 52 + }, + { + "x": 86, + "y": 50 + }, + { + "x": 86, + "y": 51 + }, + { + "x": 86, + "y": 52 + }, + { + "x": 79, + "y": 43 + }, + { + "x": 80, + "y": 43 + }, + { + "x": 87, + "y": 50 + }, + { + "x": 87, + "y": 51 + }, + { + "x": 87, + "y": 52 + }, + { + "x": 20, + "y": 177 + }, + { + "x": 20, + "y": 178 + }, + { + "x": 20, + "y": 179 + }, + { + "x": 20, + "y": 180 + }, + { + "x": 20, + "y": 181 + }, + { + "x": 20, + "y": 182 + }, + { + "x": 20, + "y": 183 + }, + { + "x": 20, + "y": 184 + }, + { + "x": 20, + "y": 185 + }, + { + "x": 21, + "y": 177 + }, + { + "x": 21, + "y": 178 + }, + { + "x": 21, + "y": 179 + }, + { + "x": 21, + "y": 180 + }, + { + "x": 21, + "y": 181 + }, + { + "x": 21, + "y": 182 + }, + { + "x": 21, + "y": 183 + }, + { + "x": 21, + "y": 184 + }, + { + "x": 21, + "y": 185 + }, + { + "x": 22, + "y": 177 + }, + { + "x": 22, + "y": 178 + }, + { + "x": 22, + "y": 179 + }, + { + "x": 22, + "y": 180 + }, + { + "x": 22, + "y": 181 + }, + { + "x": 22, + "y": 182 + }, + { + "x": 22, + "y": 183 + }, + { + "x": 22, + "y": 184 + }, + { + "x": 22, + "y": 185 + }, + { + "x": 23, + "y": 177 + }, + { + "x": 23, + "y": 178 + }, + { + "x": 23, + "y": 179 + }, + { + "x": 23, + "y": 180 + }, + { + "x": 23, + "y": 181 + }, + { + "x": 23, + "y": 182 + }, + { + "x": 23, + "y": 183 + }, + { + "x": 23, + "y": 184 + }, + { + "x": 23, + "y": 185 + }, + { + "x": 24, + "y": 177 + }, + { + "x": 24, + "y": 178 + }, + { + "x": 24, + "y": 179 + }, + { + "x": 24, + "y": 180 + }, + { + "x": 24, + "y": 181 + }, + { + "x": 24, + "y": 182 + }, + { + "x": 24, + "y": 183 + }, + { + "x": 24, + "y": 184 + }, + { + "x": 24, + "y": 185 + }, + { + "x": 25, + "y": 177 + }, + { + "x": 25, + "y": 178 + }, + { + "x": 25, + "y": 179 + }, + { + "x": 25, + "y": 180 + }, + { + "x": 25, + "y": 181 + }, + { + "x": 25, + "y": 182 + }, + { + "x": 25, + "y": 183 + }, + { + "x": 25, + "y": 184 + }, + { + "x": 25, + "y": 185 + }, + { + "x": 26, + "y": 177 + }, + { + "x": 26, + "y": 178 + }, + { + "x": 26, + "y": 179 + }, + { + "x": 26, + "y": 180 + }, + { + "x": 26, + "y": 181 + }, + { + "x": 26, + "y": 182 + }, + { + "x": 26, + "y": 183 + }, + { + "x": 26, + "y": 184 + }, + { + "x": 26, + "y": 185 + }, + { + "x": 27, + "y": 177 + }, + { + "x": 27, + "y": 178 + }, + { + "x": 27, + "y": 179 + }, + { + "x": 27, + "y": 180 + }, + { + "x": 27, + "y": 181 + }, + { + "x": 27, + "y": 182 + }, + { + "x": 27, + "y": 183 + }, + { + "x": 27, + "y": 184 + }, + { + "x": 27, + "y": 185 + }, + { + "x": 28, + "y": 177 + }, + { + "x": 28, + "y": 178 + }, + { + "x": 28, + "y": 179 + }, + { + "x": 28, + "y": 180 + }, + { + "x": 28, + "y": 181 + }, + { + "x": 28, + "y": 182 + }, + { + "x": 28, + "y": 183 + }, + { + "x": 28, + "y": 184 + }, + { + "x": 28, + "y": 185 + }, + { + "x": 29, + "y": 177 + }, + { + "x": 29, + "y": 178 + }, + { + "x": 29, + "y": 179 + }, + { + "x": 29, + "y": 180 + }, + { + "x": 29, + "y": 181 + }, + { + "x": 29, + "y": 182 + }, + { + "x": 29, + "y": 183 + }, + { + "x": 29, + "y": 184 + }, + { + "x": 29, + "y": 185 + }, + { + "x": 0, + "y": 183 + }, + { + "x": 0, + "y": 184 + }, + { + "x": 0, + "y": 185 + }, + { + "x": 0, + "y": 186 + }, + { + "x": 0, + "y": 187 + }, + { + "x": 0, + "y": 188 + }, + { + "x": 0, + "y": 189 + }, + { + "x": 0, + "y": 190 + }, + { + "x": 0, + "y": 191 + }, + { + "x": 1, + "y": 183 + }, + { + "x": 1, + "y": 184 + }, + { + "x": 1, + "y": 185 + }, + { + "x": 1, + "y": 186 + }, + { + "x": 1, + "y": 187 + }, + { + "x": 1, + "y": 188 + }, + { + "x": 1, + "y": 189 + }, + { + "x": 1, + "y": 190 + }, + { + "x": 1, + "y": 191 + }, + { + "x": 2, + "y": 183 + }, + { + "x": 2, + "y": 184 + }, + { + "x": 2, + "y": 185 + }, + { + "x": 2, + "y": 186 + }, + { + "x": 2, + "y": 187 + }, + { + "x": 2, + "y": 188 + }, + { + "x": 2, + "y": 189 + }, + { + "x": 2, + "y": 190 + }, + { + "x": 2, + "y": 191 + }, + { + "x": 3, + "y": 183 + }, + { + "x": 3, + "y": 184 + }, + { + "x": 3, + "y": 185 + }, + { + "x": 3, + "y": 186 + }, + { + "x": 3, + "y": 187 + }, + { + "x": 3, + "y": 188 + }, + { + "x": 3, + "y": 189 + }, + { + "x": 3, + "y": 190 + }, + { + "x": 3, + "y": 191 + }, + { + "x": 4, + "y": 183 + }, + { + "x": 4, + "y": 184 + }, + { + "x": 4, + "y": 185 + }, + { + "x": 4, + "y": 186 + }, + { + "x": 4, + "y": 187 + }, + { + "x": 4, + "y": 188 + }, + { + "x": 4, + "y": 189 + }, + { + "x": 4, + "y": 190 + }, + { + "x": 4, + "y": 191 + }, + { + "x": 5, + "y": 183 + }, + { + "x": 5, + "y": 184 + }, + { + "x": 5, + "y": 185 + }, + { + "x": 5, + "y": 186 + }, + { + "x": 5, + "y": 187 + }, + { + "x": 5, + "y": 188 + }, + { + "x": 5, + "y": 189 + }, + { + "x": 5, + "y": 190 + }, + { + "x": 5, + "y": 191 + }, + { + "x": 6, + "y": 183 + }, + { + "x": 6, + "y": 184 + }, + { + "x": 6, + "y": 185 + }, + { + "x": 6, + "y": 186 + }, + { + "x": 6, + "y": 187 + }, + { + "x": 6, + "y": 188 + }, + { + "x": 6, + "y": 189 + }, + { + "x": 6, + "y": 190 + }, + { + "x": 6, + "y": 191 + }, + { + "x": 7, + "y": 183 + }, + { + "x": 7, + "y": 184 + }, + { + "x": 7, + "y": 185 + }, + { + "x": 7, + "y": 186 + }, + { + "x": 7, + "y": 187 + }, + { + "x": 7, + "y": 188 + }, + { + "x": 7, + "y": 189 + }, + { + "x": 7, + "y": 190 + }, + { + "x": 7, + "y": 191 + }, + { + "x": 8, + "y": 183 + }, + { + "x": 8, + "y": 184 + }, + { + "x": 8, + "y": 185 + }, + { + "x": 8, + "y": 186 + }, + { + "x": 8, + "y": 187 + }, + { + "x": 8, + "y": 188 + }, + { + "x": 8, + "y": 189 + }, + { + "x": 8, + "y": 190 + }, + { + "x": 8, + "y": 191 + }, + { + "x": 9, + "y": 183 + }, + { + "x": 9, + "y": 184 + }, + { + "x": 9, + "y": 185 + }, + { + "x": 9, + "y": 186 + }, + { + "x": 9, + "y": 187 + }, + { + "x": 9, + "y": 188 + }, + { + "x": 9, + "y": 189 + }, + { + "x": 9, + "y": 190 + }, + { + "x": 9, + "y": 191 + }, + { + "x": 10, + "y": 183 + }, + { + "x": 10, + "y": 184 + }, + { + "x": 10, + "y": 185 + }, + { + "x": 10, + "y": 186 + }, + { + "x": 10, + "y": 187 + }, + { + "x": 10, + "y": 188 + }, + { + "x": 10, + "y": 189 + }, + { + "x": 10, + "y": 190 + }, + { + "x": 10, + "y": 191 + }, + { + "x": 11, + "y": 183 + }, + { + "x": 11, + "y": 184 + }, + { + "x": 11, + "y": 185 + }, + { + "x": 11, + "y": 186 + }, + { + "x": 11, + "y": 187 + }, + { + "x": 11, + "y": 188 + }, + { + "x": 11, + "y": 189 + }, + { + "x": 11, + "y": 190 + }, + { + "x": 11, + "y": 191 + }, + { + "x": 1633, + "y": 1594 + }, + { + "x": 1633, + "y": 1595 + }, + { + "x": 1633, + "y": 1596 + }, + { + "x": 1633, + "y": 1597 + }, + { + "x": 1633, + "y": 1598 + }, + { + "x": 1633, + "y": 1599 + }, + { + "x": 1633, + "y": 1600 + }, + { + "x": 1633, + "y": 1601 + }, + { + "x": 1633, + "y": 1602 + }, + { + "x": 1634, + "y": 1594 + }, + { + "x": 1634, + "y": 1595 + }, + { + "x": 1634, + "y": 1596 + }, + { + "x": 1634, + "y": 1597 + }, + { + "x": 1634, + "y": 1598 + }, + { + "x": 1634, + "y": 1599 + }, + { + "x": 1634, + "y": 1600 + }, + { + "x": 1634, + "y": 1601 + }, + { + "x": 1634, + "y": 1602 + }, + { + "x": 1635, + "y": 1594 + }, + { + "x": 1635, + "y": 1595 + }, + { + "x": 1635, + "y": 1596 + }, + { + "x": 1635, + "y": 1597 + }, + { + "x": 1635, + "y": 1598 + }, + { + "x": 1635, + "y": 1599 + }, + { + "x": 1635, + "y": 1600 + }, + { + "x": 1635, + "y": 1601 + }, + { + "x": 1635, + "y": 1602 + }, + { + "x": 1636, + "y": 1594 + }, + { + "x": 1636, + "y": 1595 + }, + { + "x": 1636, + "y": 1596 + }, + { + "x": 1636, + "y": 1597 + }, + { + "x": 1636, + "y": 1598 + }, + { + "x": 1636, + "y": 1599 + }, + { + "x": 1636, + "y": 1600 + }, + { + "x": 1636, + "y": 1601 + }, + { + "x": 1636, + "y": 1602 + }, + { + "x": 1637, + "y": 1594 + }, + { + "x": 1637, + "y": 1595 + }, + { + "x": 1637, + "y": 1596 + }, + { + "x": 1637, + "y": 1597 + }, + { + "x": 1637, + "y": 1598 + }, + { + "x": 1637, + "y": 1599 + }, + { + "x": 1637, + "y": 1600 + }, + { + "x": 1637, + "y": 1601 + }, + { + "x": 1637, + "y": 1602 + }, + { + "x": 1638, + "y": 1594 + }, + { + "x": 1638, + "y": 1595 + }, + { + "x": 1638, + "y": 1596 + }, + { + "x": 1638, + "y": 1597 + }, + { + "x": 1638, + "y": 1598 + }, + { + "x": 1638, + "y": 1599 + }, + { + "x": 1638, + "y": 1600 + }, + { + "x": 1638, + "y": 1601 + }, + { + "x": 1638, + "y": 1602 + }, + { + "x": 1639, + "y": 1594 + }, + { + "x": 1639, + "y": 1595 + }, + { + "x": 1639, + "y": 1596 + }, + { + "x": 1639, + "y": 1597 + }, + { + "x": 1639, + "y": 1598 + }, + { + "x": 1639, + "y": 1599 + }, + { + "x": 1639, + "y": 1600 + }, + { + "x": 1639, + "y": 1601 + }, + { + "x": 1639, + "y": 1602 + }, + { + "x": 1640, + "y": 1594 + }, + { + "x": 1640, + "y": 1595 + }, + { + "x": 1640, + "y": 1596 + }, + { + "x": 1640, + "y": 1597 + }, + { + "x": 1640, + "y": 1598 + }, + { + "x": 1640, + "y": 1599 + }, + { + "x": 1640, + "y": 1600 + }, + { + "x": 1640, + "y": 1601 + }, + { + "x": 1640, + "y": 1602 + }, + { + "x": 1641, + "y": 1594 + }, + { + "x": 1641, + "y": 1595 + }, + { + "x": 1641, + "y": 1596 + }, + { + "x": 1641, + "y": 1597 + }, + { + "x": 1641, + "y": 1598 + }, + { + "x": 1641, + "y": 1599 + }, + { + "x": 1641, + "y": 1600 + }, + { + "x": 1641, + "y": 1601 + }, + { + "x": 1641, + "y": 1602 + }, + { + "x": 1689, + "y": 1595 + }, + { + "x": 1689, + "y": 1596 + }, + { + "x": 1689, + "y": 1597 + }, + { + "x": 1689, + "y": 1598 + }, + { + "x": 1689, + "y": 1599 + }, + { + "x": 1689, + "y": 1600 + }, + { + "x": 1689, + "y": 1601 + }, + { + "x": 1689, + "y": 1602 + }, + { + "x": 1689, + "y": 1603 + }, + { + "x": 1690, + "y": 1595 + }, + { + "x": 1690, + "y": 1596 + }, + { + "x": 1690, + "y": 1597 + }, + { + "x": 1690, + "y": 1598 + }, + { + "x": 1690, + "y": 1599 + }, + { + "x": 1690, + "y": 1600 + }, + { + "x": 1690, + "y": 1601 + }, + { + "x": 1690, + "y": 1602 + }, + { + "x": 1690, + "y": 1603 + }, + { + "x": 1691, + "y": 1595 + }, + { + "x": 1691, + "y": 1596 + }, + { + "x": 1691, + "y": 1597 + }, + { + "x": 1691, + "y": 1598 + }, + { + "x": 1691, + "y": 1599 + }, + { + "x": 1691, + "y": 1600 + }, + { + "x": 1691, + "y": 1601 + }, + { + "x": 1691, + "y": 1602 + }, + { + "x": 1691, + "y": 1603 + }, + { + "x": 1692, + "y": 1595 + }, + { + "x": 1692, + "y": 1596 + }, + { + "x": 1692, + "y": 1597 + }, + { + "x": 1692, + "y": 1598 + }, + { + "x": 1692, + "y": 1599 + }, + { + "x": 1692, + "y": 1600 + }, + { + "x": 1692, + "y": 1601 + }, + { + "x": 1692, + "y": 1602 + }, + { + "x": 1692, + "y": 1603 + }, + { + "x": 1693, + "y": 1595 + }, + { + "x": 1693, + "y": 1596 + }, + { + "x": 1693, + "y": 1597 + }, + { + "x": 1693, + "y": 1598 + }, + { + "x": 1693, + "y": 1599 + }, + { + "x": 1693, + "y": 1600 + }, + { + "x": 1693, + "y": 1601 + }, + { + "x": 1693, + "y": 1602 + }, + { + "x": 1693, + "y": 1603 + }, + { + "x": 1694, + "y": 1595 + }, + { + "x": 1694, + "y": 1596 + }, + { + "x": 1694, + "y": 1597 + }, + { + "x": 1694, + "y": 1598 + }, + { + "x": 1694, + "y": 1599 + }, + { + "x": 1694, + "y": 1600 + }, + { + "x": 1694, + "y": 1601 + }, + { + "x": 1694, + "y": 1602 + }, + { + "x": 1694, + "y": 1603 + }, + { + "x": 1695, + "y": 1595 + }, + { + "x": 1695, + "y": 1596 + }, + { + "x": 1695, + "y": 1597 + }, + { + "x": 1695, + "y": 1598 + }, + { + "x": 1695, + "y": 1599 + }, + { + "x": 1695, + "y": 1600 + }, + { + "x": 1695, + "y": 1601 + }, + { + "x": 1695, + "y": 1602 + }, + { + "x": 1695, + "y": 1603 + }, + { + "x": 1696, + "y": 1595 + }, + { + "x": 1696, + "y": 1596 + }, + { + "x": 1696, + "y": 1597 + }, + { + "x": 1696, + "y": 1598 + }, + { + "x": 1696, + "y": 1599 + }, + { + "x": 1696, + "y": 1600 + }, + { + "x": 1696, + "y": 1601 + }, + { + "x": 1696, + "y": 1602 + }, + { + "x": 1696, + "y": 1603 + }, + { + "x": 1697, + "y": 1595 + }, + { + "x": 1697, + "y": 1596 + }, + { + "x": 1697, + "y": 1597 + }, + { + "x": 1697, + "y": 1598 + }, + { + "x": 1697, + "y": 1599 + }, + { + "x": 1697, + "y": 1600 + }, + { + "x": 1697, + "y": 1601 + }, + { + "x": 1697, + "y": 1602 + }, + { + "x": 1697, + "y": 1603 + }, + { + "x": 1689, + "y": 1604 + }, + { + "x": 1690, + "y": 1604 + }, + { + "x": 1691, + "y": 1604 + }, + { + "x": 1692, + "y": 1604 + }, + { + "x": 1693, + "y": 1604 + }, + { + "x": 1694, + "y": 1604 + }, + { + "x": 1695, + "y": 1604 + }, + { + "x": 1696, + "y": 1604 + }, + { + "x": 1697, + "y": 1604 + }, + { + "x": 1698, + "y": 1595 + }, + { + "x": 1698, + "y": 1596 + }, + { + "x": 1698, + "y": 1597 + }, + { + "x": 1698, + "y": 1598 + }, + { + "x": 1698, + "y": 1599 + }, + { + "x": 1698, + "y": 1600 + }, + { + "x": 1698, + "y": 1601 + }, + { + "x": 1698, + "y": 1602 + }, + { + "x": 1698, + "y": 1603 + }, + { + "x": 1698, + "y": 1604 + }, + { + "x": 1699, + "y": 1596 + }, + { + "x": 1699, + "y": 1597 + }, + { + "x": 1699, + "y": 1598 + }, + { + "x": 1699, + "y": 1599 + }, + { + "x": 1699, + "y": 1600 + }, + { + "x": 1699, + "y": 1601 + }, + { + "x": 1699, + "y": 1602 + }, + { + "x": 1699, + "y": 1603 + }, + { + "x": 1699, + "y": 1604 + }, + { + "x": 1700, + "y": 1596 + }, + { + "x": 1700, + "y": 1597 + }, + { + "x": 1700, + "y": 1598 + }, + { + "x": 1700, + "y": 1599 + }, + { + "x": 1700, + "y": 1600 + }, + { + "x": 1700, + "y": 1601 + }, + { + "x": 1700, + "y": 1602 + }, + { + "x": 1700, + "y": 1603 + }, + { + "x": 1700, + "y": 1604 + }, + { + "x": 1701, + "y": 1596 + }, + { + "x": 1701, + "y": 1597 + }, + { + "x": 1701, + "y": 1598 + }, + { + "x": 1701, + "y": 1599 + }, + { + "x": 1701, + "y": 1600 + }, + { + "x": 1701, + "y": 1601 + }, + { + "x": 1701, + "y": 1602 + }, + { + "x": 1701, + "y": 1603 + }, + { + "x": 1701, + "y": 1604 + }, + { + "x": 1631, + "y": 1595 + }, + { + "x": 1631, + "y": 1596 + }, + { + "x": 1631, + "y": 1597 + }, + { + "x": 1631, + "y": 1598 + }, + { + "x": 1631, + "y": 1599 + }, + { + "x": 1631, + "y": 1600 + }, + { + "x": 1631, + "y": 1601 + }, + { + "x": 1631, + "y": 1602 + }, + { + "x": 1631, + "y": 1603 + }, + { + "x": 1632, + "y": 1595 + }, + { + "x": 1632, + "y": 1596 + }, + { + "x": 1632, + "y": 1597 + }, + { + "x": 1632, + "y": 1598 + }, + { + "x": 1632, + "y": 1599 + }, + { + "x": 1632, + "y": 1600 + }, + { + "x": 1632, + "y": 1601 + }, + { + "x": 1632, + "y": 1602 + }, + { + "x": 1632, + "y": 1603 + }, + { + "x": 1633, + "y": 1603 + }, + { + "x": 1634, + "y": 1603 + }, + { + "x": 1635, + "y": 1603 + }, + { + "x": 1636, + "y": 1603 + }, + { + "x": 1637, + "y": 1603 + }, + { + "x": 1638, + "y": 1603 + }, + { + "x": 1639, + "y": 1603 + }, + { + "x": 1640, + "y": 1603 + }, + { + "x": 1659, + "y": 1632 + }, + { + "x": 1659, + "y": 1633 + }, + { + "x": 1659, + "y": 1634 + }, + { + "x": 1659, + "y": 1635 + }, + { + "x": 1659, + "y": 1636 + }, + { + "x": 1659, + "y": 1637 + }, + { + "x": 1659, + "y": 1638 + }, + { + "x": 1659, + "y": 1639 + }, + { + "x": 1659, + "y": 1640 + }, + { + "x": 1660, + "y": 1632 + }, + { + "x": 1660, + "y": 1633 + }, + { + "x": 1660, + "y": 1634 + }, + { + "x": 1660, + "y": 1635 + }, + { + "x": 1660, + "y": 1636 + }, + { + "x": 1660, + "y": 1637 + }, + { + "x": 1660, + "y": 1638 + }, + { + "x": 1660, + "y": 1639 + }, + { + "x": 1660, + "y": 1640 + }, + { + "x": 1661, + "y": 1632 + }, + { + "x": 1661, + "y": 1633 + }, + { + "x": 1661, + "y": 1634 + }, + { + "x": 1661, + "y": 1635 + }, + { + "x": 1661, + "y": 1636 + }, + { + "x": 1661, + "y": 1637 + }, + { + "x": 1661, + "y": 1638 + }, + { + "x": 1661, + "y": 1639 + }, + { + "x": 1661, + "y": 1640 + }, + { + "x": 1662, + "y": 1632 + }, + { + "x": 1662, + "y": 1633 + }, + { + "x": 1662, + "y": 1634 + }, + { + "x": 1662, + "y": 1635 + }, + { + "x": 1662, + "y": 1636 + }, + { + "x": 1662, + "y": 1637 + }, + { + "x": 1662, + "y": 1638 + }, + { + "x": 1662, + "y": 1639 + }, + { + "x": 1662, + "y": 1640 + }, + { + "x": 1663, + "y": 1632 + }, + { + "x": 1663, + "y": 1633 + }, + { + "x": 1663, + "y": 1634 + }, + { + "x": 1663, + "y": 1635 + }, + { + "x": 1663, + "y": 1636 + }, + { + "x": 1663, + "y": 1637 + }, + { + "x": 1663, + "y": 1638 + }, + { + "x": 1663, + "y": 1639 + }, + { + "x": 1663, + "y": 1640 + }, + { + "x": 1664, + "y": 1632 + }, + { + "x": 1664, + "y": 1633 + }, + { + "x": 1664, + "y": 1634 + }, + { + "x": 1664, + "y": 1635 + }, + { + "x": 1664, + "y": 1636 + }, + { + "x": 1664, + "y": 1637 + }, + { + "x": 1664, + "y": 1638 + }, + { + "x": 1664, + "y": 1639 + }, + { + "x": 1664, + "y": 1640 + }, + { + "x": 1665, + "y": 1632 + }, + { + "x": 1665, + "y": 1633 + }, + { + "x": 1665, + "y": 1634 + }, + { + "x": 1665, + "y": 1635 + }, + { + "x": 1665, + "y": 1636 + }, + { + "x": 1665, + "y": 1637 + }, + { + "x": 1665, + "y": 1638 + }, + { + "x": 1665, + "y": 1639 + }, + { + "x": 1665, + "y": 1640 + }, + { + "x": 1666, + "y": 1632 + }, + { + "x": 1666, + "y": 1633 + }, + { + "x": 1666, + "y": 1634 + }, + { + "x": 1666, + "y": 1635 + }, + { + "x": 1666, + "y": 1636 + }, + { + "x": 1666, + "y": 1637 + }, + { + "x": 1666, + "y": 1638 + }, + { + "x": 1666, + "y": 1639 + }, + { + "x": 1666, + "y": 1640 + }, + { + "x": 1667, + "y": 1632 + }, + { + "x": 1667, + "y": 1633 + }, + { + "x": 1667, + "y": 1634 + }, + { + "x": 1667, + "y": 1635 + }, + { + "x": 1667, + "y": 1636 + }, + { + "x": 1667, + "y": 1637 + }, + { + "x": 1667, + "y": 1638 + }, + { + "x": 1667, + "y": 1639 + }, + { + "x": 1667, + "y": 1640 + }, + { + "x": 1659, + "y": 1641 + }, + { + "x": 1660, + "y": 1641 + }, + { + "x": 1661, + "y": 1641 + }, + { + "x": 1662, + "y": 1641 + }, + { + "x": 1663, + "y": 1641 + }, + { + "x": 1664, + "y": 1641 + }, + { + "x": 1665, + "y": 1641 + }, + { + "x": 1666, + "y": 1641 + }, + { + "x": 1667, + "y": 1641 + }, + { + "x": 1517, + "y": 1676 + }, + { + "x": 1517, + "y": 1677 + }, + { + "x": 1517, + "y": 1678 + }, + { + "x": 1517, + "y": 1679 + }, + { + "x": 1517, + "y": 1680 + }, + { + "x": 1517, + "y": 1681 + }, + { + "x": 1517, + "y": 1682 + }, + { + "x": 1517, + "y": 1683 + }, + { + "x": 1517, + "y": 1684 + }, + { + "x": 1518, + "y": 1676 + }, + { + "x": 1518, + "y": 1677 + }, + { + "x": 1518, + "y": 1678 + }, + { + "x": 1518, + "y": 1679 + }, + { + "x": 1518, + "y": 1680 + }, + { + "x": 1518, + "y": 1681 + }, + { + "x": 1518, + "y": 1682 + }, + { + "x": 1518, + "y": 1683 + }, + { + "x": 1518, + "y": 1684 + }, + { + "x": 1519, + "y": 1676 + }, + { + "x": 1519, + "y": 1677 + }, + { + "x": 1519, + "y": 1678 + }, + { + "x": 1519, + "y": 1679 + }, + { + "x": 1519, + "y": 1680 + }, + { + "x": 1519, + "y": 1681 + }, + { + "x": 1519, + "y": 1682 + }, + { + "x": 1519, + "y": 1683 + }, + { + "x": 1519, + "y": 1684 + }, + { + "x": 1520, + "y": 1676 + }, + { + "x": 1520, + "y": 1677 + }, + { + "x": 1520, + "y": 1678 + }, + { + "x": 1520, + "y": 1679 + }, + { + "x": 1520, + "y": 1680 + }, + { + "x": 1520, + "y": 1681 + }, + { + "x": 1520, + "y": 1682 + }, + { + "x": 1520, + "y": 1683 + }, + { + "x": 1520, + "y": 1684 + }, + { + "x": 1521, + "y": 1676 + }, + { + "x": 1521, + "y": 1677 + }, + { + "x": 1521, + "y": 1678 + }, + { + "x": 1521, + "y": 1679 + }, + { + "x": 1521, + "y": 1680 + }, + { + "x": 1521, + "y": 1681 + }, + { + "x": 1521, + "y": 1682 + }, + { + "x": 1521, + "y": 1683 + }, + { + "x": 1521, + "y": 1684 + }, + { + "x": 1522, + "y": 1676 + }, + { + "x": 1522, + "y": 1677 + }, + { + "x": 1522, + "y": 1678 + }, + { + "x": 1522, + "y": 1679 + }, + { + "x": 1522, + "y": 1680 + }, + { + "x": 1522, + "y": 1681 + }, + { + "x": 1522, + "y": 1682 + }, + { + "x": 1522, + "y": 1683 + }, + { + "x": 1522, + "y": 1684 + }, + { + "x": 1523, + "y": 1676 + }, + { + "x": 1523, + "y": 1677 + }, + { + "x": 1523, + "y": 1678 + }, + { + "x": 1523, + "y": 1679 + }, + { + "x": 1523, + "y": 1680 + }, + { + "x": 1523, + "y": 1681 + }, + { + "x": 1523, + "y": 1682 + }, + { + "x": 1523, + "y": 1683 + }, + { + "x": 1523, + "y": 1684 + }, + { + "x": 1524, + "y": 1676 + }, + { + "x": 1524, + "y": 1677 + }, + { + "x": 1524, + "y": 1678 + }, + { + "x": 1524, + "y": 1679 + }, + { + "x": 1524, + "y": 1680 + }, + { + "x": 1524, + "y": 1681 + }, + { + "x": 1524, + "y": 1682 + }, + { + "x": 1524, + "y": 1683 + }, + { + "x": 1524, + "y": 1684 + }, + { + "x": 1525, + "y": 1676 + }, + { + "x": 1525, + "y": 1677 + }, + { + "x": 1525, + "y": 1678 + }, + { + "x": 1525, + "y": 1679 + }, + { + "x": 1525, + "y": 1680 + }, + { + "x": 1525, + "y": 1681 + }, + { + "x": 1525, + "y": 1682 + }, + { + "x": 1525, + "y": 1683 + }, + { + "x": 1525, + "y": 1684 + }, + { + "x": -1056, + "y": 1012 + }, + { + "x": -1056, + "y": 1013 + }, + { + "x": -1056, + "y": 1014 + }, + { + "x": -1056, + "y": 1015 + }, + { + "x": -1056, + "y": 1016 + }, + { + "x": -1056, + "y": 1017 + }, + { + "x": -1056, + "y": 1018 + }, + { + "x": -1056, + "y": 1019 + }, + { + "x": -1056, + "y": 1020 + }, + { + "x": -1055, + "y": 1012 + }, + { + "x": -1055, + "y": 1013 + }, + { + "x": -1055, + "y": 1014 + }, + { + "x": -1055, + "y": 1015 + }, + { + "x": -1055, + "y": 1016 + }, + { + "x": -1055, + "y": 1017 + }, + { + "x": -1055, + "y": 1018 + }, + { + "x": -1055, + "y": 1019 + }, + { + "x": -1055, + "y": 1020 + }, + { + "x": -1054, + "y": 1012 + }, + { + "x": -1054, + "y": 1013 + }, + { + "x": -1054, + "y": 1014 + }, + { + "x": -1054, + "y": 1015 + }, + { + "x": -1054, + "y": 1016 + }, + { + "x": -1054, + "y": 1017 + }, + { + "x": -1054, + "y": 1018 + }, + { + "x": -1054, + "y": 1019 + }, + { + "x": -1054, + "y": 1020 + }, + { + "x": -1053, + "y": 1012 + }, + { + "x": -1053, + "y": 1013 + }, + { + "x": -1053, + "y": 1014 + }, + { + "x": -1053, + "y": 1015 + }, + { + "x": -1053, + "y": 1016 + }, + { + "x": -1053, + "y": 1017 + }, + { + "x": -1053, + "y": 1018 + }, + { + "x": -1053, + "y": 1019 + }, + { + "x": -1053, + "y": 1020 + }, + { + "x": -1052, + "y": 1012 + }, + { + "x": -1052, + "y": 1013 + }, + { + "x": -1052, + "y": 1014 + }, + { + "x": -1052, + "y": 1015 + }, + { + "x": -1052, + "y": 1016 + }, + { + "x": -1052, + "y": 1017 + }, + { + "x": -1052, + "y": 1018 + }, + { + "x": -1052, + "y": 1019 + }, + { + "x": -1052, + "y": 1020 + }, + { + "x": -1051, + "y": 1012 + }, + { + "x": -1051, + "y": 1013 + }, + { + "x": -1051, + "y": 1014 + }, + { + "x": -1051, + "y": 1015 + }, + { + "x": -1051, + "y": 1016 + }, + { + "x": -1051, + "y": 1017 + }, + { + "x": -1051, + "y": 1018 + }, + { + "x": -1051, + "y": 1019 + }, + { + "x": -1051, + "y": 1020 + }, + { + "x": -1050, + "y": 1012 + }, + { + "x": -1050, + "y": 1013 + }, + { + "x": -1050, + "y": 1014 + }, + { + "x": -1050, + "y": 1015 + }, + { + "x": -1050, + "y": 1016 + }, + { + "x": -1050, + "y": 1017 + }, + { + "x": -1050, + "y": 1018 + }, + { + "x": -1050, + "y": 1019 + }, + { + "x": -1050, + "y": 1020 + }, + { + "x": -1049, + "y": 1012 + }, + { + "x": -1049, + "y": 1013 + }, + { + "x": -1049, + "y": 1014 + }, + { + "x": -1049, + "y": 1015 + }, + { + "x": -1049, + "y": 1016 + }, + { + "x": -1049, + "y": 1017 + }, + { + "x": -1049, + "y": 1018 + }, + { + "x": -1049, + "y": 1019 + }, + { + "x": -1049, + "y": 1020 + }, + { + "x": -1048, + "y": 1012 + }, + { + "x": -1048, + "y": 1013 + }, + { + "x": -1048, + "y": 1014 + }, + { + "x": -1048, + "y": 1015 + }, + { + "x": -1048, + "y": 1016 + }, + { + "x": -1048, + "y": 1017 + }, + { + "x": -1048, + "y": 1018 + }, + { + "x": -1048, + "y": 1019 + }, + { + "x": -1048, + "y": 1020 + }, + { + "x": -1047, + "y": 1012 + }, + { + "x": -1047, + "y": 1013 + }, + { + "x": -1047, + "y": 1014 + }, + { + "x": -1047, + "y": 1015 + }, + { + "x": -1047, + "y": 1016 + }, + { + "x": -1047, + "y": 1017 + }, + { + "x": -1047, + "y": 1018 + }, + { + "x": -1047, + "y": 1019 + }, + { + "x": -1047, + "y": 1020 + }, + { + "x": -1055, + "y": 1021 + }, + { + "x": -1054, + "y": 1021 + }, + { + "x": -1053, + "y": 1021 + }, + { + "x": -1052, + "y": 1021 + }, + { + "x": -1051, + "y": 1021 + }, + { + "x": -1050, + "y": 1021 + }, + { + "x": -1049, + "y": 1021 + }, + { + "x": -1048, + "y": 1021 + }, + { + "x": -1047, + "y": 1021 + }, + { + "x": -1065, + "y": 1025 + }, + { + "x": -1065, + "y": 1026 + }, + { + "x": -1065, + "y": 1027 + }, + { + "x": -1065, + "y": 1028 + }, + { + "x": -1065, + "y": 1029 + }, + { + "x": -1065, + "y": 1030 + }, + { + "x": -1065, + "y": 1031 + }, + { + "x": -1065, + "y": 1032 + }, + { + "x": -1065, + "y": 1033 + }, + { + "x": -1064, + "y": 1025 + }, + { + "x": -1064, + "y": 1026 + }, + { + "x": -1064, + "y": 1027 + }, + { + "x": -1064, + "y": 1028 + }, + { + "x": -1064, + "y": 1029 + }, + { + "x": -1064, + "y": 1030 + }, + { + "x": -1064, + "y": 1031 + }, + { + "x": -1064, + "y": 1032 + }, + { + "x": -1064, + "y": 1033 + }, + { + "x": -1063, + "y": 1025 + }, + { + "x": -1063, + "y": 1026 + }, + { + "x": -1063, + "y": 1027 + }, + { + "x": -1063, + "y": 1028 + }, + { + "x": -1063, + "y": 1029 + }, + { + "x": -1063, + "y": 1030 + }, + { + "x": -1063, + "y": 1031 + }, + { + "x": -1063, + "y": 1032 + }, + { + "x": -1063, + "y": 1033 + }, + { + "x": -1062, + "y": 1025 + }, + { + "x": -1062, + "y": 1026 + }, + { + "x": -1062, + "y": 1027 + }, + { + "x": -1062, + "y": 1028 + }, + { + "x": -1062, + "y": 1029 + }, + { + "x": -1062, + "y": 1030 + }, + { + "x": -1062, + "y": 1031 + }, + { + "x": -1062, + "y": 1032 + }, + { + "x": -1062, + "y": 1033 + }, + { + "x": -1061, + "y": 1025 + }, + { + "x": -1061, + "y": 1026 + }, + { + "x": -1061, + "y": 1027 + }, + { + "x": -1061, + "y": 1028 + }, + { + "x": -1061, + "y": 1029 + }, + { + "x": -1061, + "y": 1030 + }, + { + "x": -1061, + "y": 1031 + }, + { + "x": -1061, + "y": 1032 + }, + { + "x": -1061, + "y": 1033 + }, + { + "x": -1060, + "y": 1025 + }, + { + "x": -1060, + "y": 1026 + }, + { + "x": -1060, + "y": 1027 + }, + { + "x": -1060, + "y": 1028 + }, + { + "x": -1060, + "y": 1029 + }, + { + "x": -1060, + "y": 1030 + }, + { + "x": -1060, + "y": 1031 + }, + { + "x": -1060, + "y": 1032 + }, + { + "x": -1060, + "y": 1033 + }, + { + "x": -1059, + "y": 1025 + }, + { + "x": -1059, + "y": 1026 + }, + { + "x": -1059, + "y": 1027 + }, + { + "x": -1059, + "y": 1028 + }, + { + "x": -1059, + "y": 1029 + }, + { + "x": -1059, + "y": 1030 + }, + { + "x": -1059, + "y": 1031 + }, + { + "x": -1059, + "y": 1032 + }, + { + "x": -1059, + "y": 1033 + }, + { + "x": -1058, + "y": 1025 + }, + { + "x": -1058, + "y": 1026 + }, + { + "x": -1058, + "y": 1027 + }, + { + "x": -1058, + "y": 1028 + }, + { + "x": -1058, + "y": 1029 + }, + { + "x": -1058, + "y": 1030 + }, + { + "x": -1058, + "y": 1031 + }, + { + "x": -1058, + "y": 1032 + }, + { + "x": -1058, + "y": 1033 + }, + { + "x": -1057, + "y": 1025 + }, + { + "x": -1057, + "y": 1026 + }, + { + "x": -1057, + "y": 1027 + }, + { + "x": -1057, + "y": 1028 + }, + { + "x": -1057, + "y": 1029 + }, + { + "x": -1057, + "y": 1030 + }, + { + "x": -1057, + "y": 1031 + }, + { + "x": -1057, + "y": 1032 + }, + { + "x": -1057, + "y": 1033 + } + ], + "tileNames": [ + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "lava", + "lava", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-soil-dark", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-soil-dark", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "lava", + "lava", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava", + "lava", + "volcanic-cracks-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-soil-dark", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks-hot", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks-hot", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-hot", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-soil-dark", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-hot", + "lava", + "lava", + "volcanic-soil-dark", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-hot", + "lava", + "lava", + "volcanic-soil-dark", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "lava", + "volcanic-soil-dark", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "lava", + "lava", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds-warm", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava", + "lava", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds", + "volcanic-folds", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "volcanic-folds-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava-hot", + "lava-hot", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-cracks", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-hot", + "volcanic-cracks-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-hot", + "lava", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "lava", + "lava", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "lava", + "lava-hot", + "lava-hot", + "lava", + "lava", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-hot", + "lava-hot", + "lava-hot", + "lava-hot", + "lava", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-smooth-stone", + "volcanic-cracks-hot", + "lava", + "lava-hot", + "lava-hot", + "lava-hot", + "volcanic-cracks-hot", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-hot", + "lava", + "lava-hot", + "lava-hot", + "volcanic-cracks-hot", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-smooth-stone", + "volcanic-cracks-warm", + "volcanic-cracks-hot", + "lava", + "lava", + "lava", + "volcanic-cracks-warm", + "volcanic-cracks-warm", + "volcanic-cracks" + ] +} diff --git a/test/oracle/capture.ts b/test/oracle/capture.ts index 299afa8..ba02819 100644 --- a/test/oracle/capture.ts +++ b/test/oracle/capture.ts @@ -2262,6 +2262,118 @@ async function captureVulcanusTemperature(): Promise { * map_color port. Regenerate: node --experimental-strip-types test/oracle/capture.ts * vulcanus-tile-names */ +/** + * **A DENSE tile-name capture at the lava boundaries the cliff rejection reads** + * (issue #84, the lava-perimeter thread). + * + * `oracle-vulcanus-tile-names` is a sparse survey - a 64-tile grid plus a + * golden-angle spiral - and its lava classification is exact on all 381 of its + * positions. That exactness is real but it cannot settle a SUB-TILE boundary + * question: its sensitivity was measured by planting scale factors on `lava`'s + * probability, and `1.02` and `1.2` both still pass. Sparse positions simply do + * not sit close enough to a boundary in probability space. + * + * The negative-space oracle in `vulcanusCliffEntities.spec.ts` says the boundary + * IS off somewhere: 13 real cliffs the game placed have our lava inside their + * collision box, and the game ran that same rejection and kept them. The 35 + * distinct tiles responsible are the seeds here, each expanded to a Chebyshev + * radius-4 neighbourhood so the SHAPE of the disagreement is visible - whether + * our blob is a uniform tile too fat, fat only on one side, or something else. + * + * These are deliberately the hardest positions on the map for the resolver + * rather than a representative sample, so the agreement rate here is not + * comparable with the survey's and is not meant to be. + * + * Regenerate: node --experimental-strip-types test/oracle/capture.ts + * vulcanus-lava-boundary + */ +async function captureVulcanusLavaBoundary(): Promise { + const seed = 123456; + const planet = "vulcanus"; + // The 35 tiles our mask calls lava inside a REAL cliff's collision box, + // dumped from the placement itself (see the spec that consumes this fixture). + const seeds: readonly (readonly [number, number])[] = [ + [88, 41], + [89, 40], + [89, 41], + [85, 45], + [86, 43], + [86, 44], + [87, 42], + [87, 43], + [87, 44], + [82, 48], + [83, 47], + [83, 48], + [24, 181], + [25, 181], + [4, 187], + [5, 187], + [6, 187], + [7, 187], + [1637, 1598], + [1693, 1599], + [1693, 1600], + [1694, 1599], + [1694, 1600], + [1695, 1600], + [1696, 1600], + [1697, 1600], + [1635, 1599], + [1636, 1599], + [1663, 1636], + [1663, 1637], + [1521, 1680], + [-1052, 1016], + [-1051, 1016], + [-1051, 1017], + [-1061, 1029], + ]; + const RADIUS = 4; + const seen = new Set(); + const positions: Position[] = []; + for (const [sx, sy] of seeds) + for (let dx = -RADIUS; dx <= RADIUS; dx++) + for (let dy = -RADIUS; dy <= RADIUS; dy++) { + const x = sx + dx; + const y = sy + dy; + const k = `${String(x)},${String(y)}`; + if (seen.has(k)) continue; + seen.add(k); + // Sample the tile's own integer coordinate; `sampleTileNames` echoes the + // floored `get_tile` input back, so the fixture records what was asked. + positions.push({ x: x + 0.5, y: y + 0.5 }); + } + + const workDir = await mkdtemp(join(tmpdir(), "oracle-capture-")); + try { + const samples: TileSample[] = await sampleTileNames(positions, { + workDir, + seed, + spaceAge: true, + planet, + }); + const fixture = { + _comment: + "Ground truth from Factorio 2.1.12 (Space Age enabled) via the test/oracle harness. surface.get_tile(x, y).name on a real Vulcanus surface (game.planets['vulcanus'].create_surface(), seed 123456) after real chunk generation. DENSE: Chebyshev radius-4 neighbourhoods around the 35 tiles our lava mask wrongly places inside a real cliff's collision box, so the boundary error's shape is visible. Deliberately the hardest positions for the resolver, NOT a representative sample - the agreement rate here is not comparable with oracle-vulcanus-tile-names. positions are the mod's ECHOED floored get_tile input. Regenerate: node --experimental-strip-types test/oracle/capture.ts vulcanus-lava-boundary", + seed0: seed, + planet, + seeds: seeds.map(([x, y]) => ({ x, y })), + radius: RADIUS, + positions: samples.map((s) => ({ x: s.x, y: s.y })), + tileNames: samples.map((s) => s.name), + }; + const out = join(FIXTURES, "oracle-vulcanus-lava-boundary.seed123456.json"); + await writeFile(out, JSON.stringify(fixture, null, 2) + "\n"); + const distinct = [...new Set(fixture.tileNames)].sort(); + console.log( + `wrote ${out} (${String(positions.length)} points, ${String(distinct.length)} distinct tiles: ${distinct.join(", ")})`, + ); + } finally { + await rm(workDir, { recursive: true, force: true }); + } +} + async function captureVulcanusTileNames(): Promise { const seed = 123456; const planet = "vulcanus"; @@ -3373,5 +3485,6 @@ if (want("vulcanus-climate")) await captureVulcanusClimate(); if (want("vulcanus-elevation")) await captureVulcanusElevation(); if (want("vulcanus-temperature")) await captureVulcanusTemperature(); if (want("vulcanus-tile-names")) await captureVulcanusTileNames(); +if (want("vulcanus-lava-boundary")) await captureVulcanusLavaBoundary(); if (want("vulcanus-cliffs")) await captureVulcanusCliffs(); if (want("vulcanus-rocks")) await captureVulcanusRocks(); diff --git a/test/vulcanusCliffEntities.spec.ts b/test/vulcanusCliffEntities.spec.ts index dcd2fc0..6b14169 100644 --- a/test/vulcanusCliffEntities.spec.ts +++ b/test/vulcanusCliffEntities.spec.ts @@ -114,14 +114,19 @@ describe("Vulcanus cliff placement vs find_entities", () => { // That fix is measured in `test/multisampleGrid.spec.ts`; these are its // end-to-end numbers on the path the renderer actually runs. // + // **Updated 2026-08-01 again, after the collision box was narrowed from + // `rotbb`'s AABB to the rotated rectangle it actually is** + // (`cliffBoxCoversTile`). That recovered 13 real cliffs at the cost of 3 + // false positives. + // // | region | game | ours | recall | precision | ratio | // | --- | --- | --- | --- | --- | --- | - // | 0 `[0,0]` | 283 | 277 | 0.9788 | **1.0000** | 0.979 | - // | 1 `[1500,1500]` | 885 | 895 | 0.9672 | 0.9564 | 1.011 | - // | 2 `[-1200,800]` | 401 | 386 | 0.9601 | 0.9974 | 0.963 | - // | **total** | **1569** | **1558** | **0.9675** | **0.9743** | 0.993 | + // | 0 `[0,0]` | 283 | 285 | **1.0000** | 0.9930 | 1.007 | + // | 1 `[1500,1500]` | 885 | 901 | 0.9729 | 0.9556 | 1.018 | + // | 2 `[-1200,800]` | 401 | 388 | 0.9651 | 0.9974 | 0.968 | + // | **total** | **1569** | **1574** | **0.9758** | **0.9727** | 1.003 | // - // Region 0 now places no cell the game did not - precision is exactly 1. + // Region 0 reproduces the game's cliff set ENTIRELY - recall 1.0000. // // **The lava rejection is what closes the over-placement, and #84 item 1 // asked how much.** The answer is nearly all of it. Without it the same @@ -131,15 +136,17 @@ describe("Vulcanus cliff placement vs find_entities", () => { // 12% to under-placing 0.7%. So the "187-cell excess" recorded in #84 was // 185 cells of a rule the measurement was not applying, not a defect. // - // **Reported rather than smoothed over: the rejection costs recall.** - // Those 13 lost true positives are cells where the game placed a cliff and - // our tile resolver puts lava inside its box. + // **The rejection used to cost recall, and chasing those 13 lost true + // positives is what found the box bug.** They were cells where the game + // placed a cliff and our collision box found lava inside it. // - // **They are a one-tile boundary error, and that is measured (2026-07-30, - // re-measured 2026-08-01), not assumed.** This comment used to carry the - // guess that the resolver "is plausibly worse at a lava boundary"; the - // guess was wrong in its premise and right in its conclusion, so both - // halves are worth stating: + // **They were NOT a lava-mask error, and this comment said they were for + // two days.** The claim was that the resolver is "off by about one tile + // SOMEWHERE"; a dense 994-position capture at exactly those boundaries + // (`oracle-vulcanus-lava-boundary.seed123456.json`) found **zero** lava + // mismatches, 35/35 correct at the very tiles that accused it. The mask + // was innocent and the COLLISION BOX was wrong - see + // `test/cliffOrientedBox.spec.ts`. What survives of the original reasoning: // // - The resolver is NOT worse at a lava boundary. Its binary lava/not // classification - the only thing `tryToAddCliff` reads - is EXACT on @@ -147,20 +154,20 @@ describe("Vulcanus cliff placement vs find_entities", () => { // (`vulcanusTiles.spec.ts` now pins this at zero mismatches). The 42 // positions sitting directly on a lava boundary are 42/42 correct even // on the full 19-way name. - // - It is nonetheless off by about one tile SOMEWHERE, because each real - // cliff the game placed is itself a negative-space oracle: the game ran - // this same rejection and kept the cliff, so the game sees no lava in - // that box. Over the 1531 real cliffs we place across the three regions, - // 13 boxes contradict that - 0.85% - and in **all 13** the offending - // tile sits at Chebyshev depth 1 inside our lava, i.e. on our own - // perimeter. Not one is deep lava. - // - // Depth 1 is the common case for any lava tile, so that alone would not - // discriminate; region 1's 170 correct rejections are the control and they - // are spread right across the range (62 at depth 1, 61 deeper than 6, 45 - // of those bottomed out at depth 9). Wrong rejections are 100% perimeter - // against a control that is 36% perimeter, so the rule's real work is - // untouched by the boundary. + // - The negative-space oracle was RIGHT that something was wrong: each + // real cliff the game placed is a standing assertion that the game saw + // no lava in that box, and 13 of them contradicted ours. What the + // evidence could not do was name the culprit, and the depth statistic + // pointed at the wrong one. + // + // **Why "all 13 sit at Chebyshev depth 1 in our lava" misled.** It is a + // true measurement of a real perimeter effect - and the box's four corners + // ARE its perimeter, so a corner-shaped box error produces exactly that + // signature. Two mechanisms, one fingerprint. The statistic could not + // separate "our lava reaches one tile too far" from "our box reaches into + // corners the game's does not", and only the first was ever considered. + // Ruling out a suspect needs a measurement that would come out DIFFERENTLY + // for each candidate; a depth histogram comes out the same for both. // // A control run pins that the rejection is not just deleting cells at the // background lava rate: sampling the same lava field 10,000 tiles away @@ -168,17 +175,17 @@ describe("Vulcanus cliff placement vs find_entities", () => { // indiscriminate, ratio collapsing to 0.65 / 0.70. The real arm rejects // almost only false positives. // - // **Still not Nauvis-grade, but it is now one thing, not several.** + // **Still not Nauvis-grade, and what is left is now pure over-placement.** // `test/cliffPlacement.spec.ts` measures Nauvis at 1.0000 recall AND - // precision. Here 51 of the game's 1569 are missing and 40 of our 1558 are - // spurious. 13 of the 51 are the perimeter error above - a TILE question, - // not a cliff one. The residual is not one-directional (regions 0 and 2 - // UNDER-place, region 1 over-places), which is why the ratio is guarded on - // both sides below. + // precision. Here 38 of the game's 1569 are missing and 43 of our 1574 are + // spurious - and none of the 38 is a collision-rejection loss any more, + // which is what the box fix bought. The residual is not one-directional + // (region 2 UNDER-places, regions 0 and 1 over-place), which is why the + // ratio is guarded on both sides below. // // Guards sit just outside the measured values in the direction that would // signal a regression, and open in the direction of improvement. - expect(recall).toBeGreaterThan(0.95); + expect(recall).toBeGreaterThan(0.96); expect(precision).toBeGreaterThan(0.94); expect(predicted.size / actual.size).toBeLessThan(1.05); expect(predicted.size / actual.size).toBeGreaterThan(0.95); diff --git a/test/vulcanusElevationLevels.spec.ts b/test/vulcanusElevationLevels.spec.ts index 05a8cb1..68bc4ca 100644 --- a/test/vulcanusElevationLevels.spec.ts +++ b/test/vulcanusElevationLevels.spec.ts @@ -164,57 +164,66 @@ describe("Vulcanus elevation, inverted through a cliff_elevation_0 sweep", () => * So the arm reading "we over-place below 120" was really reading "we do not * delete what the game deletes, and there is more to delete down there." * - * Running both sides with the rejection collapses it: + * Running both sides with the rejection accounts for most of it: * * | `cliff_elevation_0` | ours/game, no rejection | with rejection | * | --- | --- | --- | - * | 20 | 1.085 | 0.988 | - * | 40 | 1.048 | 1.022 | - * | 60 | 1.044 | **1.027** | - * | 90 - 130 | 1.008 - 1.018 | 0.991 - 1.000 | + * | 20 | 1.085 | 1.037 | + * | 40 | 1.048 | **1.042** | + * | 60 | 1.044 | 1.039 | + * | 90 - 130 | 1.008 - 1.018 | 1.008 - 1.018 | * | 140 - 200 | 1.000 - 1.009 | 1.000 - 1.009 | * - * Worst-low 1.027 against worst-high 1.009: the gap goes 0.067 -> 0.018, and - * the low regime now straddles 1.0 rather than sitting above it. + * Worst-low 1.042 against worst-high 1.018: the gap goes 0.067 -> **0.024**. * - * **What remains at low levels is a boundary error in the TILES, not the - * elevation.** The rejection also costs recall, and it costs it in the same - * regime: 0.951 at level 20 rising to 1.000 at 140 and above. Every one of - * those losses is a real cliff whose box hits our lava at Chebyshev depth 1 - - * our own perimeter - never deeper: 32/32 at level 20, 52/52 across the sweep, - * 13/13 at default settings. A sub-tile disagreement about where lava stops. + * **These numbers correct PR #86, which reported 0.018.** That measurement was + * taken while the collision box was the `rotbb` AABB rather than the rotated + * rectangle, so the rejection was over-aggressive - it deleted cells the game + * keeps, which flattered exactly this ratio. Fixing the box + * (`cliffBoxCoversTile`) removes that flattery and the honest residual is + * larger. A too-strong correction hides the thing it is correcting. * - * **Stated carefully, because depth only discriminates in one of the two - * places it was checked.** At default settings it does: region `[1500,1500]`'s - * 170 CORRECT rejections span depth 1 to 9 with 45 bottomed out deep in lava, - * against wrong rejections that are 100% perimeter. At level 20 it does not - - * there the correct rejections are 32/32 perimeter as well, because the - * contour has walked down onto the lake edges and every candidate is near a - * boundary. So the honest claim is that the low-level errors are - * boundary-SITED in both directions, and that at level 20 we get about half of - * them right; not that depth alone proves the perimeter is one tile fat. + * **Recall is the strong signal here, and it is now ~1.000 everywhere**: every + * level from 30 to 200 reproduces the game's entire cliff set, and level 20 + * misses exactly 1 of 658. Before the box fix the rejection cost recall in + * precisely this regime - 0.951 at level 20 rising to 1.000 at 140 - which is + * what pointed at the box in the first place. The old reading of that deficit + * (a lava mask one tile too fat) was WRONG and is recorded in + * `docs/noise/vulcanus-cliffs-NOTES.md`: a dense 994-position capture at those + * exact boundaries found ZERO lava mismatches. + * + * What is left is pure over-placement concentrated below elevation 120, with + * no recall cost. That is a smaller and cleaner residual than #84 item 2 + * described, and it is still open. */ it("attributes the split to the lava rejection, not to the elevation field", () => { const withRejection = fx.cases.map((_, i) => atLevel(i, true)); // Non-vacuity: the rejection must actually remove cells, or "the split went // away" and "the predicate never fired" are the same observation. const removed = rows.reduce((n, r, i) => n + (r.ours - withRejection[i].ours), 0); - expect(removed).toBeGreaterThan(100); + console.log(`levels: rejection removed ${String(removed)} cells across the sweep`); + expect(removed).toBeGreaterThan(25); + for (const [i, w] of withRejection.entries()) + console.log( + ` level ${String(w.level).padStart(3)} game=${String(w.game).padStart(4)} noRej=${(rows[i].ours / w.game).toFixed(4)} rej=${(w.ours / w.game).toFixed(4)} rec=${(w.both / w.game).toFixed(4)}`, + ); const ratios = (rs: typeof rows, pick: (level: number) => boolean): number[] => rs.filter((r) => pick(r.level)).map((r) => r.ours / r.game); const low = Math.max(...ratios(withRejection, (l) => l <= 110)); const high = Math.max(...ratios(withRejection, (l) => l >= 120)); - // Measured 1.0266 and 1.0085. Both bounds are upper, so the port may improve - // without editing them. - expect(low).toBeLessThanOrEqual(1.03); - expect(high).toBeLessThanOrEqual(1.01); - // The gap is what #84 item 2 was about: 0.067 without the rejection, 0.018 + // Measured 1.0415 (level 40) and 1.0177 (level 130). Both bounds are upper, + // so the port may improve without editing them. + expect(low).toBeLessThanOrEqual(1.05); + expect(high).toBeLessThanOrEqual(1.02); + // The gap is what #84 item 2 was about: 0.067 without the rejection, 0.024 // with it. Guarded as an upper bound only - it may shrink to zero or invert. expect(low - high).toBeLessThan(0.03); - // And the low regime no longer sits entirely ABOVE the game, which is the - // part that read as over-placement: level 20 goes 1.085 -> 0.988. - expect(withRejection[0].ours / withRejection[0].game).toBeLessThan(1); + // **Recall, which is what the box fix bought.** Every level reproduces the + // game's whole cliff set; level 20 misses 1 of 658. Before `cliffBoxCoversTile` + // this ran 0.951 at level 20. Asserted per level rather than in aggregate so + // a regime-shaped regression cannot average itself away. + for (const w of withRejection) expect(w.both / w.game).toBeGreaterThan(0.99); }, 120000); });