Skip to content

test(cliffs): the ore rule is NOT entity collision, and it acts at the destroy stage (#84) - #124

Merged
wormeyman merged 1 commit into
mainfrom
test/ore-mechanism-not-collision
Aug 4, 2026
Merged

test(cliffs): the ore rule is NOT entity collision, and it acts at the destroy stage (#84)#124
wormeyman merged 1 commit into
mainfrom
test/ore-mechanism-not-collision

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

#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.ts has 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:

  1. Order. computeInternal calls generateCliffs at +0x2c - before it even builds the NoiseCache the three generateEntities passes use - and apply calls applyCliffs, applyDecoratives, applyEntities in that order.
  2. Inputs. generateCliffs' entire call list is crossingsForChunk, MaybeCliffOrientation::value, tryToAddCliff. The queue has 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 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: applyEntities calls Surface::mapGeneratorWouldCollide per queued entity and skips the entity on a hit (tbnz w0, #0x0 to 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:

cell neighbour the game's orientation verdict
1546,1550.5 (geyser) 1546,1546.5 north-to-none end gone - destroyed

Treating 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 at crossingsForChunk. Combined with the exclusions, that is a sharper open question than "the mechanism is unknown": something the resource control changes reaches Surface::wouldCollide by 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

  • The geyser arm is a separate matter: includeGeyser: false is a model gap (our geyser placement rolls and misses the game's 56), not a shape question, and fixing it needs no mechanism.
  • The route from autoplace_controls to Surface::wouldCollide is 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.
  • A spurious dependence through 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 other wouldCollide overloads (EntityMapGenerationTask::wouldCollide 0x101625468, tile-index based; Surface::wouldCollide 0x10160c088, the cliff one). The entity-stage call lists and both phase orderings are now in cliffs-NOTES.md.

Verification

pnpm run verify green - 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

…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
wormeyman merged commit 2bc07da into main Aug 4, 2026
6 checks passed
@wormeyman
wormeyman deleted the test/ore-mechanism-not-collision branch August 4, 2026 00:30
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>
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