Skip to content

test(cliffs): the "excess" was the lava rejection, not a second defect (#84) - #86

Merged
wormeyman merged 1 commit into
mainfrom
fix/cliff-residual-with-lava-rejection
Aug 2, 2026
Merged

test(cliffs): the "excess" was the lava rejection, not a second defect (#84)#86
wormeyman merged 1 commit into
mainfrom
fix/cliff-residual-with-lava-rejection

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

Closes items 1 and 2 of #84.

Both symptoms turned out to be one measurement error. Every arm compared our placement, which did not run tryToAddCliff's lava-collision rejection, against the game's, which always does. On Vulcanus the lava is the basalt lakes, so the missing deletion rule's effect lands exactly in the low-elevation range where the residual sat.

Item 1 - the 187-cell excess

Turning the rejection on removes 198 cells across the three oracle 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%

Region [0,0] reaches precision exactly 1.000. The port goes from over-placing 12% to under-placing 0.7%.

Item 2 - the surviving regime split

The low/high over-placement gap of 0.067 read as a second-order error in the same multisample term. Running both sides with the rejection collapses it:

cliff_elevation_0 no rejection with rejection
20 1.085 0.988
60 1.044 1.027
140 - 200 1.000 - 1.009 1.000 - 1.009

Gap 0.067 -> 0.018, and the low regime 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 in the same regime (0.951 at level 20 rising to 1.000 at 140+). Each real cliff is a negative-space oracle - the game ran this rejection and kept it - 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.

Stated carefully in the notes, because depth discriminates in one of the two places it was checked and not the other. 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 - the correct rejections are 32/32 perimeter too. So the supported claim is that these errors are boundary-sited in both directions, not that depth alone proves the perimeter is one tile fat. That is a vulcanusCatalog question.

Changes

  • vulcanusCliffEntities.spec.ts - new table; guards tightened from recall>0.77 / precision>0.68 / ratio<1.15 to 0.95 / 0.94 / 1.05.
  • cliffOrientationOracle.spec.ts - bound 175 -> 37, floor 1350 -> 1500 so a change that shrinks both has not fixed anything, it has stopped comparing. Records why this arm deliberately omits the rejection (larger comparison set).
  • vulcanusElevationLevels.spec.ts - new arm attributing the split, with a non-vacuity guard on cells actually removed. Verified by planting a no-op rejection: fails with expected 0 to be greater than 100.
  • Docs - banners carry the shipping-path row. Corrected the Vulcanus rendering note claiming the game does not exclude cliffs from lava; that was inferred from the absence of a water tile rather than measured, and has been wrong since feat(cliffs): land the cliff orientation + collision-box tables (#18) #71/feat(cliffs): port the tile-collision rejection (#18) #73.
  • Cleared a pre-existing unused-import warning in cliffResidual.spec.ts, so vp check is back to zero warnings.

pnpm run verify green: 1307 passed / 3 skipped, 0 warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt

#84)

#84 opened with two symptoms and they were one measurement error: every arm
compared OUR placement, which did not run tryToAddCliff's lava-collision
rejection, against the GAME's, which always does. On Vulcanus the lava is the
basalt lakes, so the missing deletion rule's effect lands exactly in the
low-elevation range where the residual sat.

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

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

Region [0,0] reaches precision exactly 1.000. The port goes from over-placing
12% to under-placing 0.7%.

Item 2 - the surviving regime split. The low/high over-placement gap of 0.067
read as a second-order error in the same multisample term. Running both sides
with the rejection collapses it to 0.018, and the low regime straddles 1.0
instead of sitting above it (level 20: 1.085 -> 0.988). There is no second
multisample defect to find.

What is actually left is a TILE boundary, not a cliff field. The rejection costs
recall in the same regime (0.951 at level 20 -> 1.000 at 140+), 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.
Stated carefully in the notes, because depth discriminates at default settings
(region 1's 170 correct rejections span depth 1-9, 45 deep in lava) and does NOT
at level 20, where the correct rejections are 32/32 perimeter too.

- vulcanusCliffEntities.spec.ts: new table; guards tightened from
  recall>0.77/precision>0.68/ratio<1.15 to 0.95/0.94/1.05.
- cliffOrientationOracle.spec.ts: bound 175 -> 37, floor 1350 -> 1500 so a
  change that shrinks both has not fixed anything, it has stopped comparing.
  Records why this arm deliberately omits the rejection.
- vulcanusElevationLevels.spec.ts: new arm attributing the split, with a
  non-vacuity guard on cells actually removed (verified by planting a no-op
  rejection: it fails with "expected 0 to be greater than 100").
- Docs: banners carry the shipping-path row; corrected the Vulcanus rendering
  note that claimed the game does not exclude cliffs from lava - inferred from
  the absence of a water TILE rather than measured, and wrong since #71/#73.
- Cleared a pre-existing unused-import warning in cliffResidual.spec.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt
@wormeyman
wormeyman merged commit e027566 into main Aug 2, 2026
2 checks passed
wormeyman added a commit that referenced this pull request Aug 2, 2026
…#84) (#87)

#83 left an assumption behind: that cliffs are the only consumer reading a
multisample-bearing field on a grid other than 1 tile. That was never measured,
which is exactly the status the cliff case itself had before #83. Closes #84
item 4.

The surface is much smaller than "audit every consumer", and that is the first
finding. multisample appears in factorio-data @ 2.1.12 in exactly ONE place -
vulcanus_basalt_lakes_multisample, used only by vulcanus_elev. No other planet
uses the primitive. So the audit reduces to "who consumes Vulcanus elevation":

| consumer                  | reads elevation? | grid | how              |
| ------------------------- | ---------------- | ---- | ---------------- |
| cliff generator           | yes              | 4    | measured, #83    |
| tile generator (19 range) | yes, via elev    | 1    | measured, here   |
| vulcanus_temperature      | yes              | 1    | same program     |
| tungsten/coal/calcite/SA  | no               | n/a  | read the Lua     |
| rocks, geyser             | no               | n/a  | read the Lua     |

The resource row had to be checked rather than assumed, because the generic path
DOES couple to elevation: starting_resources_lake_mask = clamp((elevation-1)/10,
0, 1) feeds starting_patches' spot_favorability_expression, and CLAUDE.md flags
that coupling as exactly what changed at 2.1.9. Vulcanus does not take that
path - each of its four resources uses its own vulcanus_*_region expression, and
those four definitions contain 0 references to elevation.

The tile generator reads grid 1, measured against the game. Substituting the
cliff generator's 4-tile elevation into the tile resolver, over the 381 oracle
positions:

|                          | grid 1 (ships) | grid 4  |
| ------------------------ | -------------- | ------- |
| tile-name agreement      | 0.9816         | 0.8609  |
| lava misclassifications  | 0 / 381        | 27 / 381 |

46 tiles named wrongly, and the binary lava call - the only thing the cliff
collision rejection reads - goes from exact to 27 wrong. The metric is
demonstrably sensitive to the swap, which is the part worth insisting on: PR
#57's substitution failed precisely because "nothing changed" could not be
distinguished from "the substitution never ran". Both arms carry that guard
explicitly.

Side result: this clears the lava perimeter that costs 13 real cliffs their
placement (#86). Reading the other elevation channel makes lava dramatically
worse, not better, so the perimeter error is not a channel mistake - it is
somewhere else in vulcanusCatalog.

Also asserts the max(-500, ...) clamp never bites at these positions, so the
swap is exactly "the same field at grid 4" rather than something subtly else.


Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 2, 2026
#84) (#88)

The lava perimeter that cost 13 real Vulcanus cliffs their placement was never
the lava mask. It was the collision box's shape.

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 axis-aligned. The port kept only
the bounding box, which overruns the rotated rectangle at all four corners, so it
rejected cliffs on lava the game never looks at.

CLIFF_ORIENTATION_COLLISION_BOX remains the correct BROAD phase - wouldCollide
derives its tile rect from a fixed-point floor over an inclusive rectangle, which
cliffCollisionTileBox reproduces. cliffBoxCoversTile adds the narrow phase
(separating-axis over the two world axes and the rectangle's own two).

|                     | 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 @ lvl20 | ~1.000 everywhere |

Clears 13 of 13 false rejections while keeping 182 of the 185 rejections that
remove genuine false positives - the correct shape, not a loosening that trades
precision for recall.

How it was found, because the route matters more than the fix. The standing
explanation - in this spec's own comments and in the notes - was that the tile
resolver was "off by about one tile SOMEWHERE". A new dense capture
(oracle-vulcanus-lava-boundary, radius-4 neighbourhoods around the 35 accusing
tiles) found ZERO lava mismatches over 994 positions and 35/35 at the accusing
tiles themselves. The mask was innocent. It was worth capturing 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.

Two corrections this forces:

- PR #86's "gap 0.067 -> 0.018" is wrong; it is 0.024. That was measured with the
  over-aggressive AABB rejection, which deleted cells the game keeps and so
  flattered exactly the ratio it reported. A too-strong correction hides the
  thing it is correcting. The residual below elevation 120 is real, still open,
  and 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 the same signature a one-tile-fat mask would.
  A statistic can only rule a suspect out if it would come out DIFFERENTLY for
  each candidate.

Checked and cleared on the way: the inclusive-floor fringe is real engine
behaviour (disassembly), and chunk ordering is not it (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 (~23x enrichment over the 2.0% base rate), always a
smaller -to-none variant of ours; those belong to the standing orientation
residual, which is unchanged.

Guards: cliffOrientedBox.spec.ts pins the geometry, and the levels sweep asserts
per-level recall > 0.99. Both verified non-vacuous by planting AABB behaviour -
they fail with "expected 0 to be greater than 20" and "expected 0.9514 to be
greater than 0.99".


Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 2, 2026
cliffOrientationOracle.spec.ts counts the residual and bounds it. This pins what
it looks like, because the shape is the lead and a change in shape is a change in
cause even when the count holds.

Measured after #83 (multisample grid), #86 (lava rejection) and #90 (raw
collision box):

- 37 of 1531 matched cells, and all 37 differ in EXACTLY ONE edge. Not one
  two-edge difference survives; before #83 the dominant mode was two edges
  (125 of 175), a whole corner on the wrong side of a band.
- Every one is an OVER-detection: the game reports a -to-none orientation and the
  port reports a crossing on that edge, never the reverse.
- Spread over all four edges (L11/R6/T7/B13) and all three regions (7/26/4), so
  it is not a directional off-by-one.

Two candidate causes are already eliminated, which is why the shape is worth
pinning rather than re-deriving next session:

- crossesCliff is EXACT. Disassembled at 0x10160c914 under 2.1.12 (the VA in
  cliffs-NOTES.md had moved); cliffPlacement.ts reproduces it line for line
  including the a<0||b<0 early-out, the boundary<e0 check, and the strict >0.5
  gate and strict crossing comparisons. No >=-vs-> slip exists to find.
- cliffiness_basic is EXONERATED. Substituting the game's own corner cliffiness
  leaves the count at exactly 37/1531.

So the residual is in the grid-4 cliff-elevation field - the one input in the
chain with no direct per-corner oracle. A single-edge, strictly one-directional
over-detection is what a small positive field offset looks like.

The direction assertion is the load-bearing one: if under-detections ever appear,
the cause has changed and that reading is dead.


Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant