Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/noise/client-preview-ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,9 @@ Done = ore patches overlaid on land, responding to the frequency/size/richness s
the cliff generator (4-tile lattice) and the tile generator (1 tile) read
different `vulcanus_elevation`. PR #57's substitution missed it because its
fixture came through the same 1-tile channel the port used. Vulcanus recall
is now 1.000/0.973/0.965 with 2.4% wrong orientations; see the ROOT CAUSE
section of `cliffs-NOTES.md` and issue #84 for the remainder. `VoronoiNoise` (layer-1 primitive table below) is
on the shipping path is now 0.9675 with precision 0.9743 and 2.0% wrong
orientations; see the ROOT CAUSE section of `cliffs-NOTES.md` and issue #84
for the remainder. `VoronoiNoise` (layer-1 primitive table below) is
confirmed **unneeded for Nauvis** - it appears nowhere in the cliff tree or
any other Nauvis expression traced so far, only on Space-Age planets - so it
remains un-ported with no open TODO against it.
Expand Down
22 changes: 19 additions & 3 deletions docs/noise/cliffs-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@
> | | recall | precision | wrong orientation |
> | --- | --- | --- | --- |
> | **Nauvis** | 1.0000 | 1.0000 | **0 / 334** |
> | **Vulcanus** | 1.000 / 0.973 / 0.965 | 0.872 | **37 / 1531 = 2.4%** |
> | **Vulcanus**, as shipped | **0.9675** | **0.9743** | **31 / 1518 = 2.0%** |
> | **Vulcanus**, no lava rejection | 0.9758 | 0.8719 | 37 / 1531 = 2.4% |
>
> Vulcanus's precision is measured **without** the lava-collision rejection, which
> the shipping renderer does apply; the remainder is tracked in **issue #84**.
> **Read the shipping row.** The renderer applies `tryToAddCliff`'s lava-collision
> rejection and the second row does not; leaving it off is what produced the
> "precision 0.872 / 187-cell excess" figure #84 opened with. The rejection drops
> 198 cells, **185 of them false positives and 13 true**, so almost the whole
> excess was a rule the measurement was not applying. Measured 2026-08-01 in
> `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**.
>
> **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
Expand Down Expand Up @@ -690,6 +701,11 @@ behaves the same when the calling program's grid is 4 tiles rather than 1.
> | `[0,0]` (worst region) | 29.8% wrong | **2.5%**, recall 1.000 |
> | level sweep, ratio | 1.20-1.49 below 120 | **1.00-1.09 at every level** |
>
> Both columns are measured **without** the lava-collision rejection, which is the
> right control for isolating this one change but is not the shipping path. On the
> path the renderer runs, "after" is recall 0.9675 / precision 0.9743 / 31 wrong -
> see the banner at the top of this file.
>
> `VulcanusElevation` now exposes `cliffElevation` beside `elevation`; both hang
> off one stack and share every sub-expression below the multisample, so the cost
> is a second memo table. **Do not collapse them back together** - they are
Expand Down
96 changes: 90 additions & 6 deletions docs/noise/vulcanus-cliffs-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

> ## STATUS, 2026-08-01: issue #18 is CLOSED; remainder tracked in #84
>
> Recall **1.000 / 0.973 / 0.965** across the three regions, **37 / 1531 = 2.4%**
> wrong orientations (was 0.806 / 0.938 / 0.853 and 12.5%). Precision 0.872,
> measured **without** the lava-collision rejection the shipping renderer applies.
> **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.
>
> **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
Expand All @@ -14,7 +17,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.
> 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.

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
Expand Down Expand Up @@ -138,8 +143,16 @@ positions - which is part of why `sulfuricAcidPatches` carries a 2.9e-3 bound.
geometry (`makeCliffPlacementFromFields`, factored out of `makeCliffPlacement`
for this) and the paint loop (`paintCliffCells`). Two differences:

- **No water exclusion.** Vulcanus has no water tile. Lava plays that visual
role but is not water, and the game does not exclude cliffs from it here.
- **No water exclusion, but there IS a lava exclusion.** Vulcanus has no water
tile, so `renderCliffs`' water check has nothing to test. This section used to
read "the game does not exclude cliffs from lava" and **that was wrong** - it
was inferred from the absence of a *water* tile rather than measured.
`tryToAddCliff` tests the orientation's collision box against the tile mask
grid, and `tile_collision_masks.lava()` sets the same `water_tile` bit the
cliff mask excludes, so lava rejects cliffs by exactly the same mechanism water
does. Ported in #71/#73; it is worth 185 false positives across the three
oracle regions (precision 0.8719 -> 0.9743) and is passed as `tileCollides`
from `VULCANUS_CLIFF_BLOCKING_TILES`.
- **No disable path.** With no continuity slider there is nothing to zero.

`cliff-vulcanus` declares `map_color = {144, 119, 87}`
Expand Down Expand Up @@ -912,3 +925,74 @@ cliff 78% of the time (93 of 120 at `[1100,2600]`) - they are edge-of-line
offsets along a real cliff face. The ones **inside** ore sit 2-5 cells away (4 of
37 at distance 1). They are not misaligned cliff lines; they are cliff faces the
game does not have at all.

## The lava rejection accounted for the "excess", 2026-08-01 (#84 items 1-2)

Issue #84 opened with two symptoms, and they turned out to be one measurement
error. Both arms compared **our placement, which did not run the lava-collision
rejection, against the game's, which always does.** `tryToAddCliff` drops any
cliff whose collision box touches a lava tile, and on Vulcanus the lava is the
basalt lakes - so the comparison was missing a deletion rule whose effect is
concentrated in exactly the low-elevation range where the residual sat.

**Item 1, the 187-cell excess.** Turning the rejection on removes 198 cells
across the three regions: **185 false positives against 13 true**.

| | game | ours | matched | recall | precision | wrong orientation |
| --- | --- | --- | --- | --- | --- | --- |
| no rejection | 1569 | 1756 | 1531 | 0.9758 | 0.8719 | 37 = 2.42% |
| **with rejection** | 1569 | **1558** | 1518 | 0.9675 | **0.9743** | 31 = 2.04% |

Per region, with it on: `[0,0]` 283/277, precision **exactly 1.000**;
`[1500,1500]` 885/895, precision 0.9564; `[-1200,800]` 401/386, precision 0.9974.
The port goes from over-placing 12% to under-placing 0.7%.

**Item 2, the surviving regime split.** `test/vulcanusElevationLevels.spec.ts`
had the low-elevation over-placement ratio at 1.085 against 1.018 high, a gap of
0.067 that read as a second-order error in the same `multisample` term. Run both
sides with the rejection and it collapses:

| `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 |
| 140 - 200 | 1.000 - 1.009 | 1.000 - 1.009 |

Gap 0.067 -> **0.018**, and the low regime now straddles 1.0 instead of sitting
above it. There is no second `multisample` defect to find.

### What is actually left is a TILE boundary, not a cliff field

The rejection costs recall, and it costs it in the same regime: 0.951 at level
20 rising to 1.000 at 140 and above. Each real cliff is a negative-space oracle -
the game ran this rejection and kept it, so the game saw no lava in that box -
and every contradiction sits at Chebyshev depth 1 in our lava, our own perimeter,
**never deeper**: 32/32 at level 20, 52/52 across the sweep, 13/13 at default
settings.

**Depth discriminates in one of the two places it was checked, and not the
other.** At default settings it does: `[1500,1500]`'s 170 *correct* rejections
span depth 1 to 9, 45 of them 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 too, because the contour has walked down onto the lake edges
and every candidate is near a boundary. So the supported claim is that these
errors are boundary-**sited** in both directions and that at level 20 we call
about half of them right; not that depth alone proves the perimeter is one tile
fat. Narrowing that is a `vulcanusCatalog` question, not a cliff one.

Note what the tile resolver is already known to get right, so this is not a
retread: its binary lava/not classification is **exact on all 381 oracle
positions**, including 42 sitting directly on a lava boundary
(`vulcanusTiles.spec.ts`). Whatever is off is finer than the sample that pinned
it.

### Method note

This is the same trap as #83 wearing different clothes. There, a fixture was
right at the right site but captured through the wrong **channel**. Here, two
sides of a comparison ran different **rule sets** - and in both cases the
mismatch had a plausible mechanism ready to absorb it, so the wrong explanation
was the comfortable one. Before attributing a residual to a mechanism, check
that both sides of the comparison are running the same rules.
83 changes: 50 additions & 33 deletions test/cliffOrientationOracle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,43 +96,58 @@ describe("cliff orientation vs the game's own cliff_orientation", () => {
}, 120000);

/**
* **Vulcanus does NOT agree, and that is issue #18's residual seen up close.**
* **Vulcanus still does not agree everywhere, and this is issue #18's
* residual seen up close.**
*
* Nauvis passing exactly means the table above is right, so a disagreement
* here is a disagreement about the CROSSINGS - the four edges - not about the
* lookup. Measured 2026-07-30, over the cells the port and the game both
* place:
* lookup. Measured 2026-08-01, after the `multisample` grid-units fix (#83),
* over the cells the port and the game both place:
*
* | region | matched | wrong orientation |
* | --- | --- | --- |
* | `[0,0]` | 228 | 68 = **29.8%** |
* | `[1500,1500]` | 830 | 67 = 8.1% |
* | `[-1200,800]` | 342 | 40 = 11.7% |
* | total | 1400 | 175 = 12.5% |
* | region | matched | wrong orientation | was, before #83 |
* | --- | --- | --- | --- |
* | `[0,0]` | 283 | 7 = 2.5% | 228 / 68 = **29.8%** |
* | `[1500,1500]` | 861 | 26 = 3.0% | 830 / 67 = 8.1% |
* | `[-1200,800]` | 387 | 4 = 1.0% | 342 / 40 = 11.7% |
* | total | 1531 | **37 = 2.4%** | 1400 / 175 = 12.5% |
*
* Note the comparison set GREW as the error shrank - the port now matches 131
* more of the game's cliffs - so this is not 175 falling to 37 by comparing
* fewer cells.
*
* This is a far sharper instrument than the counts in
* `vulcanusCliffEntities.spec.ts`: a cell can land in the right place for the
* wrong reason, and 175 of them do. The dominant failure is **exactly two
* edges differing** (125 of 175), which is one of the cell's two crossings
* sitting on a different side - a single corner on the wrong side of a band
* boundary. Errors are spread evenly over the four edges (L:87 R:80 T:87
* B:89), so it is not a directional off-by-one.
* wrong reason, and 37 of them still do. Before #83 the dominant failure was
* **exactly two edges differing** (125 of 175), which is one of the cell's two
* crossings sitting on a different side - a single corner on the wrong side of
* a band boundary - spread evenly over the four edges (L:87 R:80 T:87 B:89),
* so never a directional off-by-one.
*
* **This arm deliberately runs WITHOUT the lava rejection**, which is not the
* shipping path and is the point. Rejection only ever REMOVES cells, so
* leaving it off compares the larger set (1531 rather than 1518) and cannot
* hide a bad crossing behind a cell that got dropped for an unrelated reason.
* On the shipping path the same measurement is 31 / 1518 = 2.04%; the
* rejection removes 6 wrong ones with the 185 false positives it is there for.
*
* Three causes were tested against this metric and none of them explain it:
* Causes tested against this metric before #83, none of which explained it,
* kept because each is a closed door:
*
* - **The fields are exonerated a second time.** Re-running PR #57's
* substitution - the game's own corner elevation and cliffiness, at
* `[1500,1500]` - leaves the mismatch at 67/830, identical to the digit,
* while a +3 elevation bias moves it to 122/793. So the substitution is
* live and this metric is sensitive to it; the fields are simply right.
* #57 only scored PLACEMENT, one bit per cell, and could not have seen this.
* - **`fixImpossibleCells` is not it.** Turning it off moves the total from
* 12.5% to 14.3%, and region `[0,0]` from 29.8% to 30.8%. It helps slightly
* and explains almost nothing.
* - **Chunk borders are not it.** `generateCliffs` passes `tryToAddCliff` a
* - **The fields were exonerated at the site they were sampled.** Re-running
* PR #57's substitution - the game's own corner elevation and cliffiness, at
* `[1500,1500]` - left the mismatch at 67/830, identical to the digit, while
* a +3 elevation bias moved it to 122/793. The substitution was live and the
* metric sensitive to it. What that could not see is that the fixture had
* been captured through `calculate_tile_properties`, a DIFFERENT channel
* from the one the cliff generator reads - which is exactly what #83 turned
* out to be. A field can be right at the right site and still be the wrong
* field for the consumer.
* - **`fixImpossibleCells` was not it.** Turning it off moved the total from
* 12.5% to 14.3%, and region `[0,0]` from 29.8% to 30.8%.
* - **Chunk borders were not it.** `generateCliffs` passes `tryToAddCliff` a
* `!onChunkBorder` flag, and `fixImpossibleCells` cannot clear a border
* edge, so the outer ring of each 8x8 chunk was the obvious suspect. Border
* cells are wrong 13.3% of the time against interior's 11.9% - no
* cells were wrong 13.3% of the time against interior's 11.9% - no
* concentration - and the game places cliffs uniformly across all 64
* in-chunk positions (17-36 each), so that flag suppresses nothing.
*
Expand Down Expand Up @@ -163,14 +178,16 @@ describe("cliff orientation vs the game's own cliff_orientation", () => {
}
}
// Non-vacuity: this arm skips cells the port does not place, so without a
// floor a port that placed NOTHING would pass on an empty comparison. 1400
// is the measured matched count (2026-07-30, placement without the lava
// rejection, which is what is built above).
expect(compared).toBeGreaterThan(1350);
// Measured 175. An upper bound, not an equality, so fixing the rule does not
// require editing this line - but tight enough that a regression fails.
// floor a port that placed NOTHING would pass on an empty comparison. 1531
// is the measured matched count (2026-08-01, placement without the lava
// rejection, which is what is built above). The floor is raised with the
// bound below for a reason: a change that shrinks BOTH numbers has not
// fixed anything, it has stopped comparing.
expect(compared).toBeGreaterThan(1500);
// Measured 37. An upper bound, not an equality, so improving the rule does
// not require editing this line - but tight enough that a regression fails.
// Do NOT raise it to make a change pass: this number going up means the
// crossings got worse, which is the whole thing #18 is about.
expect(wrong.length).toBeLessThanOrEqual(175);
expect(wrong.length).toBeLessThanOrEqual(37);
}, 120000);
});
2 changes: 1 addition & 1 deletion test/cliffResidual.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vite-plus/test";

import { makeCliffElevation, makeCliffFields } from "../src/noise/cliffs/cliffFields";
import { makeCliffPlacementFromFields, smoothingKnots } from "../src/noise/cliffs/cliffPlacement";
import { makeCliffPlacementFromFields } from "../src/noise/cliffs/cliffPlacement";
import {
VULCANUS_CLIFF_ELEVATION_0,
VULCANUS_CLIFF_ELEVATION_INTERVAL,
Expand Down
Loading