test(cliffs): the ore rule is NOT entity collision, and it acts at the destroy stage (#84) - #124
Merged
Merged
Conversation
…e destroy stage (#84) The section before this put 11 of the 25 missed destructions on the ore rule and named widening it as the next move. The mechanism was worth one more look first, because `vulcanusOreRejection.ts` has always said the rule is characterised rather than ported. It should NOT be widened. Entity collision is excluded, now VERIFIED by three routes rather than asserted from the call ordering alone: 1. Order - `computeInternal` calls `generateCliffs` at +0x2c, before it even builds the NoiseCache the three `generateEntities` passes use; `apply` calls `applyCliffs`, `applyDecoratives`, `applyEntities` in that order. 2. Inputs - `generateCliffs`' entire call list is `crossingsForChunk`, `MaybeCliffOrientation::value`, `tryToAddCliff`. No resource input at all. 3. Masks, at the PROTOTYPE level rather than the type default - calcite, tungsten-ore and sulfuric-acid-geyser are all `type = "resource"` and none overrides `collision_mask`, so all take `{layers={resource=true}}` against the cliff's `{item, meltable, object, player, water_tile, is_lower_object, is_object, cliff}`. Disjoint. Route 3 kills the variant nobody had written down: CROSS-CHUNK ordering. Chunk N's entities really are on the surface before chunk N+1's cliffs are applied, so "the resource was already there" is available as an escape - and it still cannot matter, at any box size, because the masks never intersect. The one entity-versus-cliff test that exists runs the other way: `applyEntities` calls `Surface::mapGeneratorWouldCollide` per queued entity and SKIPS the entity on a hit. It never destroys a cliff. The stage is measured: applying #122's discriminator to the lever's 31 cells, exactly one is decidable and it says DESTROYED (`1546,1550.5`, a geyser cell, neighbour `1546,1546.5` at `north-to-none`). So the effect enters at `applyCliffs`/`Surface::wouldCollide`, not at `crossingsForChunk`. n=1 - the spec says so, and carries the contrast arm showing the other 30 cost nothing. Consequence: widening the box would fit a shape to an unexplained effect rather than model a code path - exactly what #88 shipped and #90 had to undo. Close the recall gap by finding what the resource control actually changes. Also records `Surface::mapGeneratorWouldCollide` (0x101624a44), which was in neither notes file and is distinct from both other wouldCollide overloads. Measurement only - nothing shipping changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU
wormeyman
added a commit
that referenced
this pull request
Aug 4, 2026
#84) (#125) `vulcanusOreRejection.ts` records box overlap as explaining 21 of the 31 cells the ore suppresses, the other ten as "run remainders", and the cascade half of that question as REFUTED. That refutation is about a DIFFERENT cascade. It tested #108's crossing-stage mechanism - a rejection zeroes the cell's edge registers, a neighbour's code changes, re-test to a fixpoint - and its conclusion ("rejected cells do not turn neighbours rejectable") still stands. The `applyCliffs` cascade is `Cliff::onDestroy` taking the facing end of every connected neighbour and destroying a neighbour left with no end at all. It was read out of the binary in #113, AFTER #110 ran, and nothing had re-run the remainder question against it. Replayed entirely on the game's own data - its ore-off cliff set (892 with the game's orientations), its resource entity positions, the prototype boxes the fixture itself carries - so none of the port's field, ore model or geyser roll appears in the measurement: | arm | matched | wrong | surplus | | --- | --- | --- | --- | | control - destroy the lever's own 31 | 861 | 0 | 0 | | direct overlap only, no cascade | 856 | 5 | 10 | | direct overlap + onDestroy CASCADE | 859 | 2 | 6 | - Direct overlap fires on 21 and every one is in the lever's 31 - precision 1.000 re-derived with no data of ours. - The cascade closes 4 of the 10 remainders and 3 of the 5 orientation errors. Recall 21/31 = 0.677 -> 25/31 = 0.806, no new parameter, no wider box. - `missing` is 0 in both arms, so no cell the game kept is ever removed. That is what separates a mechanism from a bigger rectangle - and #124 established the box is not the engine's collision test to begin with. The control validates the harness, and getting it exact needs `noUpdateConnections: true`: the game's dumped set is POST-pipeline, so running `updateConnections` over it again double-applies the pass and scores wrong=13, which reads like a defect in the cascade model and is an artifact of the harness. Any experiment starting from a dumped set has the same hazard. Six remain: 1546,1550.5 1546,1554.5 1606,1590.5 1606,1594.5 1622,1614.5 1626,1614.5. Two are the geyser pair, and the first is the one cell in this whole residual whose destruction the game's own orientations directly witness (#124's n=1) - so the best-evidenced destruction is still unexplained. Measurement only - the renderer rejects at the crossing stage and does not run `applyCliffConnections` at all. Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 4, 2026
…for cliffs (#84) (#128) Every ore result in #84 uses one instrument: switch a resource off through `map_gen_settings.autoplace_controls` and see which cliffs come back, treated throughout as removing entities and nothing else. It does more, and the game's data files say so: vulcanus_calcite_size = slider_rescale(control:calcite:size, 2) vulcanus_calcite_region = ... vulcanus_calcite_size ..., control:calcite:frequency ... volcanic_jagged_ground_range = 5 * min(10, max(vulcanus_calcite_region + 0.2, ...)) That last one is a TILE range, so `calcite size = 0` changes which tile the argmax picks - a live route from the lever to `Surface::wouldCollide`'s tile half, exactly the kind #124 was left looking for after closing the entity half. Measured against the GAME, three arms over [1500,1500] on a uniform stride-2 grid of 16384 points: | arm | tiles changed | blocking gained | blocking lost | | --- | --- | --- | --- | | calcite OFF | 841 (5.1%) | 0 | 0 | | ALL resources OFF | 1066 | 0 | 0 | Every change is ground-to-ground - jagged-ground losing the argmax to folds, folds-warm, soil-dark, soil-light, folds-flat. Nothing crosses the lava/lava-hot boundary, so `constCollideWithTile` cannot see any of it and EVERY ORE RESULT IN #84 STANDS. But the instrument was mischaracterised, and anyone using `autoplace_controls` for a new question needs to know it moves tiles. Non-vacuity: 1682 of the 16384 sampled tiles ARE blocking, so there was a boundary to cross, and each arm records the controls the SURFACE read back. Our port reproduces it - 3335 of 65536 changed, 0 blocking, a rate of 5.09% against the game's 5.13%. That corroborates the port on a lever nobody had pointed at it, and is what lets its "no blocking flip anywhere" cover the 75% of tiles the grid skipped. Control: tungsten OFF moves zero tiles here, because there is no tungsten in this region. WHERE THIS LEAVES THE MECHANISM - a three-way contradiction, recorded rather than smoothed over. Both halves of `Surface::wouldCollide` are now closed against the ore (entity by #124, tile here), yet #124 also found the ore acting at the destroy stage, where `wouldCollide` is the only thing that destroys. All three cannot be right. Ranked by corroboration, the destroy-stage result is weakest at n=1 - and its one cell, `1546,1550.5`, is also one of the six #125's cascade does not explain, so it has been anomalous twice. Harness: `sampleTileNames` gains an `autoplaceControls` override and a `sampleTileNamesFull` sibling returning the controls the surface read back. New fixture + PROVENANCE entry; capture takes ~7s. Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 4, 2026
… effect remains (#84) (#129) * test(cliffs): every route from the ore to a cliff is closed - and the effect remains (#84) #128 closed the tile half and recorded a three-way contradiction. Two more routes are now closed, and the honest summary is stronger than "unknown": the search space is EXHAUSTED, not merely unexplored. | route | evidence | | --- | --- | | the cliff FIELD reads a resource | `cliff_elevation = cliff_elevation_from_elevation = elevation = vulcanus_elevation`, whose 47-node expression closure holds no resource region; and our port's raw cell set is BIT-IDENTICAL under every lever arm (2277 cells, same codes) | | `Surface::wouldCollide` does more | disassembled: per-orientation box, degenerate early-out, `constCollideWithTile`, `collideWithEntity`, both with the cliff's own mask at `proto+0x2b0`. No third input | | its ENTITY half | #124 - disjoint masks at prototype level, ordering | | its TILE half | #128 - 841 tiles move, none crosses lava/lava-hot | | the lever perturbs STRUCTURALLY | this PR - richness x2 and x0.5 move ZERO cliffs | The last row was the only route #128 left open. `control:calcite:richness` appears in `vulcanus_calcite_richness` ALONE - not in `vulcanus_calcite_probability` (where the ore lands) nor in `vulcanus_calcite_region` (which drives the volcanic_jagged_ground_range tile) - so it hands the generator a different CompiledMapGenSettings describing the same world. Over [1500,1500]: 861 cliffs and 3933 resources in all three arms, identical cell for cell and orientation for orientation. The surface reads the changed richness back, so the override applied. So the lever is not a structural perturbation: the effect tracks ore PRESENCE - and nothing that can see ore presence can reach a cliff. This does NOT say the rule is wrong. Box overlap against real resource positions reproduces the game at precision 1.000 over 31 cells and survives out of sample (#126). The rule is right; the mechanism is missing, and the port's model of map generation is incomplete in a way none of these five rows covers. Recorded so the next person does not re-walk a closed route. Three genuinely untouched ideas are listed in the notes: chunk-generation ORDER beyond the mask argument, whatever happens to the queue between `generateCliffs` and `applyCliffs`, and `CliffCraterPlacer::tryToPlaceCliffAsCrater` - ruled out for the residual by position but never as a mechanism. New fixture + PROVENANCE entry; capture takes ~7s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU * test(cliffs): close the cliffiness gap in the field argument (#84) The field-closure row named `cliff_elevation` only. The cliff generator reads TWO properties, and a closure argument that covers one of them leaves the gate unexamined - `planet-map-gen.lua` routes `cliffiness = "cliffiness_basic"` for Vulcanus, and nothing had traced that one. It is clean: `cliffiness_basic`'s closure is a single node referencing only `x`, `y` and `cliff_richness`. So the row stands, but now for both properties rather than for the one that happened to get traced first. Found while checking whether crater-cliff's appearance under the lever shared a cause; `crater_cliff` (47-node closure) and both Vulcanus biomes it reads are clean too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 4, 2026
…#136) #130 proposed a mechanism class new to #84 - entity autoplace ROLLS, so removing calcite shifts the per-chunk RNG stream - and said "nothing here tests it". This is that test. No capture: every fact is in fixtures already committed, one of them since #111. The hypothesis is not refuted. The evidence for it is. 1. **n is ONE crater, not eight.** The eight `crater-cliff` entities are eight segments of a single ring - centre (1646.62, 1679.75), radii 4.95 to 7.00, against the prototype's own `crater_radius = 7`. FFF #386 describes exactly that. Effective sample size 1; #130's table invites reading it as 8. 2. **The spatial reading would have been vacuous.** All eight sit within 0.33 tiles of a calcite entity - and their chunk holds 805 calcite over 1024 tiles, 78.6% coverage. Landing on calcite there is expected. 3. **#130's `geyser OFF` control is VACUOUS.** Zero geysers in the ring's chunk; all 19 are in (48,48), (48,49), (53,54). The lever works globally - removes all 19, moves cliff-vulcanus 885 -> 889 - and cannot touch that chunk's stream by construction. 4. **A lever unrelated to calcite also produces craters**, unread in the #111 fixture: `LAVA TILES OFF only` gives 7 craters with resources still ON, and both levers together give 18 against 8 and 7 alone. 5. **And that row is a data fact.** `crater-cliff` overrides the default cliff mask with `{item, object, player, water_tile}`, and on Vulcanus only lava and lava-hot carry `water_tile`. No RNG needed. Calcite's own route stays open, with two more closed by reading rather than assuming: not entity collision (`resource` mask is `{resource}`, disjoint from every cliff mask including crater-cliff's - #124's argument extended rather than carried over), and not lava tiles (#128 measured zero blocking tiles moved). A third fact from the same file closes a route this session had to consider: the default `cliff` mask carries `not_colliding_with_itself = true`, so cliffs never collide with same-prototype cliffs. That matters because `applyCliffs` adds each cliff to the surface before testing the next, and 57 of 400 adjacent orientation pairs have overlapping boxes. No new fixtures, no shipping behaviour change. Claude-Session: https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc 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.
#123 put 11 of the 25 missed destructions on the ore rule and named widening it as the next move. The mechanism was worth one more look first, because
vulcanusOreRejection.tshas always said the rule is characterised, not ported.Conclusion: do not widen the box.
Entity collision is excluded - verified three ways, not asserted
The file already claimed this from call ordering. All three routes are now read off the 2.1.12 arm64 slice or the game data:
computeInternalcallsgenerateCliffsat+0x2c- before it even builds theNoiseCachethe threegenerateEntitiespasses use - andapplycallsapplyCliffs,applyDecoratives,applyEntitiesin that order.generateCliffs' entire call list iscrossingsForChunk,MaybeCliffOrientation::value,tryToAddCliff. The queue has no resource input at all.calcite,tungsten-oreandsulfuric-acid-geyserare alltype = "resource"and none overridescollision_mask, so all three take{layers={resource=true}}. The cliff default is{item, meltable, object, player, water_tile, is_lower_object, is_object, cliff}. Disjoint.Route 3 kills the variant nobody had written down: cross-chunk ordering. Chunk N's entities really are on the surface before chunk N+1's cliffs are applied, so "the resource was already there" is a genuine escape from the ordering argument - and it still cannot matter, at any box size, because the masks never intersect.
And the one entity-versus-cliff test that exists runs the other way:
applyEntitiescallsSurface::mapGeneratorWouldCollideper queued entity and skips the entity on a hit (tbnz w0, #0x0to the loop tail). It never destroys a cliff. That is the CLIFF -> ORE direction, which #99 measured as inert.The stage IS measured: it is a destruction
Applying #122's discriminator to the lever's 31 cells - exactly one is decidable, and it says DESTROYED:
1546,1550.5(geyser)1546,1546.5north-to-noneTreating it as never-queued contradicts the game at that neighbour; doing the same to any of the other 30 costs nothing - the contrast arm, and also the honest statement of how thin this oracle is. n = 1. It is a stage localisation, not a survey, and the spec says so in those words.
So the ore's effect enters at
applyCliffs/Surface::wouldCollide, not atcrossingsForChunk. Combined with the exclusions, that is a sharper open question than "the mechanism is unknown": something the resource control changes reachesSurface::wouldCollideby a route that is not the entity half.Why this stops the widening
The box-overlap model is shaped like the resource entity's rectangle, and that shape is real - it is what distinguishes the geyser's 1.398 half-extent from the ores' 0.098. But that shape is now established not to be the engine's collision test. Widening it until the remaining cells fall out would be fitting a shape to an unexplained effect rather than modelling a code path - precisely what #88 shipped and #90 had to undo.
The recall gap is real and still worth closing. Close it by finding what the resource control actually changes.
What is NOT excluded, stated so it is not re-derived
includeGeyser: falseis a model gap (our geyser placement rolls and misses the game's 56), not a shape question, and fixing it needs no mechanism.autoplace_controlstoSurface::wouldCollideis open. Its tile half is the obvious next suspect precisely because the entity half is now closed - which would mean a resource control changing TILES, a strong and checkable claim that nothing here tests.CompiledMapGenSettings- the lever perturbing the cliff field rather than the ore suppressing it - is not excluded either, though test(cliffs): the direction is ORE -> CLIFF, and it is worth 31 cells (#84) #99's spatial locality argues against it.Also recorded
Surface::mapGeneratorWouldCollide(0x101624a44) was in neither notes file and is distinct from both otherwouldCollideoverloads (EntityMapGenerationTask::wouldCollide0x101625468, tile-index based;Surface::wouldCollide0x10160c088, the cliff one). The entity-stage call lists and both phase orderings are now incliffs-NOTES.md.Verification
pnpm run verifygreen - 1461 app (up 3) + 13 worker + 3 container tests, 0 warnings, exit 0. Measurement and documentation only; nothing shipping changed.🤖 Generated with Claude Code
https://claude.ai/code/session_015jeznBmWiZywUc7cBw9xFU