fix(cliffs): multisample offsets are in GRID UNITS - #18 root cause - #83
Merged
Conversation
) vulcanus-multisample-NOTES proved multisample(e,dx,dy) == e(x+dx, y+dy) at 150/150 comparisons with residual exactly zero. That is correct - for LuaSurface.calculate_tile_properties, whose noise program has a 1-TILE grid. It was never checked in any other channel, and the primitive's own docs say it evaluates "in a separate noise program with a larger grid" whose "sub-grids are copied to the main program". That phrase is load-bearing. Asked through the CLIFF GENERATOR, whose grid is the 4-tile corner lattice, by routing a probe onto cliff_elevation with the rule collapsed so cliffs mark exactly where the field crosses 71: x column 70 baseline multisample(x, 0, 0) column 70 identity multisample(x, 4, 0) column 54 shifted 16 TILES, not 4 multisample(x, 0, 4) column 70 null control dx = 4 moves the field by 4 x the 4-tile grid step. So the true rule is e(x + dx*G, y + dy*G) for the CALLING program's grid G. Consequence: vulcanus_basalt_lakes_multisample's min over {0,1}x{0,1} spans 4 tiles for cliffs and 1 tile for every per-tile consumer. min is erosion, so the cliff channel's elevation is much smoother. The port used the 1-tile field for both, making the cliff elevation too rough and over-placing by ~40%. before after wrong orientation 175 = 12.5% 37 = 2.4% recall 0.806/0.938/0.853 1.000/0.973/0.965 [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 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 - a private DAG for cliffs was tried first and is much dearer. Do not collapse them back together: they are different fields, not a cache miss. Nothing here refutes the multisample port or the per-tile consumers. calculate_tile_properties and the tile renderer both live in the 1-tile channel where e(x+dx, y+dy) is exactly right. Specs that pinned the defect are inverted rather than deleted, because the inversions are the evidence: the s=0 smoothing arm is now exact, all four collapsed arms are exact, the level sweep matches at every level, and the corner-fields substitutions now MOVE cells - those fixtures are the tile channel, so they are the right numbers for the wrong consumer, and their agreeing for months is how the wrong channel stayed invisible. cliffResidual's Vulcanus block is retired: its mismatched population is now too small to compare. The lesson is the third form of the same trap. Not a fixture captured at the wrong SITE (#70's grid_offset), not a wrong value - a fixture captured through the wrong CHANNEL, agreeing with a port that made the same mistake. Ask which code path CONSUMES a value, not only which coordinates it is sampled at. And note that no sweep inside the port could have found this: the smoothing, band and rule sweeps all searched a family that shared the defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GRx1CU29RKLsRuUAErEDRB
This was referenced Aug 2, 2026
wormeyman
added a commit
that referenced
this pull request
Aug 2, 2026
An audit after the multisample grid fix. Several documents still asserted the opposite of what is now measured, and three of them were TITLES - the lines a reader trusts fastest. - `cliffs-NOTES.md` gains a STATUS banner at the top. The root cause sat at line 636 under 635 lines of superseded investigation, so a cold reader met the wrong conclusions first. The banner carries the current recall and orientation numbers, points at the ROOT CAUSE section, and names the specific stale figures below it (the ~90%, the 12.5%/29.8%/8.1%/11.7% tables, the 0.806/0.938/0.853 recalls) as pre-fix. - Two headings there were plainly wrong: "validated ~90% tile-for-tile", and "Validation result: EXACT since 2026-07-30 (the residual is resolved)" - which was only ever true of Nauvis. - `vulcanus-cliffs-NOTES.md` gains the same banner, and its "## The residual is in the RULE, not the fields" section is marked FALSIFIED. Its experiment and numbers are sound; the verdict is not. The fixture it substitutes came through calculate_tile_properties (1-tile grid) while the cliff generator reads a 4-tile grid, so the fixture and the port shared the mistake and could never disagree. - `client-preview-ROADMAP.md` said the residual "is now believed to be a RULE error rather than a field error (PR #57)". It was a field error. Corrected with why PR #57's substitution missed it. - `vulcanusCliffCornerFields.spec.ts`'s doc header and `vulcanusOreCliffSeparation.spec.ts`'s describe name both still claimed the fields are exact and the residual is in the rule. Both now say the fields are exact IN THE TILE CHANNEL, which is the accurate and more useful statement. - CLAUDE.md's spec-file count was 143; it is 152. No behaviour change. `pnpm run verify`: 310 files clean, 1306 passed / 3 skipped, preview 12 passed - the suite is green on an unloaded machine, which also confirms the earlier local timeouts were load and not a regression. Claude-Session: https://claude.ai/code/session_01GRx1CU29RKLsRuUAErEDRB Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
This was referenced Aug 2, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Root cause of #18, found and fixed.
vulcanus-multisample-NOTES.mdprovedmultisample(e,dx,dy) == e(x+dx, y+dy)at 150/150 comparisons, residual exactly zero. That is correct - forcalculate_tile_properties, whose noise program has a 1-tile grid. It was never checked in any other channel, and the primitive's own docs say it evaluates "in a separate noise program with a larger grid" whose "sub-grids are copied to the main program". That phrase is load-bearing.Asked through the cliff generator (grid = the 4-tile corner lattice) by routing a probe onto
cliff_elevationwith the rule collapsed, so cliffs mark exactly where the field crosses 71:xmultisample(x, 0, 0)multisample(x, 4, 0)multisample(x, 0, 4)dx = 4moves the field by 4 × the 4-tile grid step. The true rule ise(x + dx*G, y + dy*G)for the calling program's gridG.So
vulcanus_basalt_lakes_multisample'sminover{0,1}²spans 4 tiles for cliffs and 1 tile for every per-tile consumer.minis erosion, so the cliff channel's elevation is far smoother. The port used the 1-tile field for both - too rough, over-placing ~40%.Result
[0,0](worst region)VulcanusElevationnow exposescliffElevationbesideelevation. Both hang off one stack and share every sub-expression below the multisample, so the cost is a second memo table - a private DAG for cliffs was tried first and is much dearer. Do not collapse them back together: they are different fields, not a cache miss.Nothing here refutes the multisample port or the per-tile consumers -
calculate_tile_propertiesand the tile renderer both live in the 1-tile channel wheree(x+dx, y+dy)is exactly right.Specs are inverted, not deleted
The inversions are the evidence: the
s=0smoothing arm is now exact, all four collapsed arms are exact, the level sweep matches at every level, and the corner-fields substitutions now move cells - those fixtures are the tile channel, so they are the right numbers for the wrong consumer, and their agreeing for months is how this stayed invisible.cliffResidual's Vulcanus block is retired: its mismatched population is now too small to compare.The lesson
The third form of the same trap: not a fixture captured at the wrong site (#70's
grid_offset), not a wrong value - a fixture captured through the wrong channel, agreeing with a port that made the same mistake. Ask which code path consumes a value, not only which coordinates it is sampled at. And no sweep inside the port could have found it: the smoothing, band and rule sweeps all searched a family that shared the defect.Local test note
pnpm vp check: 310 files clean. The full suite currently shows 8 timeout-only failures on cleanmainas well - verified by stashing and re-running - after hours of Factorio captures loaded this machine. No assertion failures on either. CI is the arbiter.🤖 Generated with Claude Code
https://claude.ai/code/session_01GRx1CU29RKLsRuUAErEDRB