Skip to content

test(cliffs): the border effect is ORIENTATION-BLIND - a rival mechanism REFUTED (#84) - #134

Merged
wormeyman merged 1 commit into
mainfrom
cliffs-border-orientation-blind
Aug 4, 2026
Merged

test(cliffs): the border effect is ORIENTATION-BLIND - a rival mechanism REFUTED (#84)#134
wormeyman merged 1 commit into
mainfrom
cliffs-border-orientation-blind

Conversation

@wormeyman

Copy link
Copy Markdown
Owner

#132 gave Cliff::updateConnections the lead on one clause: chunk borders are its entire domain, and "nothing else in the pipeline treats border cells differently at all". That clause was false. Finding the counter-example and killing it is what this PR does, and it leaves the lead stronger than it found it.

The rival

Pure arithmetic over two things already in the repo. The largest half-extent in CLIFF_ORIENTATION_COLLISION_BOX is 3.371 tiles, and:

distance from cell centre to the nearest chunk edge
border ring 1.5 - 2.5 tiles
every interior ring 5.5 - 6.5 tiles

So 16 of the 20 orientations reach across a chunk edge from the outer ring and none can from anywhere else. Asserted against the real population, not left as arithmetic: 1407 crossing cells, every one on a border.

And a rule waits on the far side. applyCliffs rejects through Surface::wouldCollide (0x10160c088) -> constCollideWithTile (0x100732eec) -> Surface::checkTileCollisions (0x101b579e0), which per tile calls Surface::getEffectiveTileID (0x10049399c) and skips the tile when the id is 0. getEffectiveTileID returns exactly 0 for an absent chunk. A box reaching into a not-yet-generated chunk reads no tile and does not collide; this port reads the real tile everywhere.

The discriminator, registered before the run

The two mechanisms differ in exactly one observable: whether the ORIENTATION matters. updateConnections' gate is !onChunkBorder, a cell-index test blind to orientation. The box mechanism fires only when the box actually crosses. Losing condition written down first: a non-crossing border rate that is not lower than the crossing one refutes the box mechanism.

Result: refuted, not marginally

population unexplained rate
interior 9 / 4484 0.20%
border, box crosses an edge 7 / 1407 0.50%
border, box does not cross 14 / 2479 0.56%

Spread the 21 border cells over the two populations in proportion and the crossing share is 7.6; observed 7. Indistinguishable from no information, with the non-crossing rate the higher of the two.

Its own direction fails independently. An absent chunk makes the game KEEP a cliff we destroy, so its signature is a false rejection whose blocking tile lies across a chunk edge. 0 of 9, and the predicate is non-vacuous.

What it buys

The border enrichment is orientation-blind - what a cell-index gate looks like, not what a geometric reach looks like. That is a property the 2.91-sigma result did not have and could not get from the border statistic alone. Both halves keep the 2.7x enrichment over the interior, so the partition had something to find and found nothing. Every border-only channel this port can name is now scored; only updateConnections is left, and #127 already showed why it cannot be scored from map-gen output.

Seven engine facts from the same read

Cliff's connection system is decompiled whole. Three change what to try next:

  • onDestroy's cascade has FOUR gates (0x1007a8854 on): entity flag bit 5 of +0x6e clear, +0x82 set, +0x83 set, map->[0x240] zero. Map generation satisfies all four (feat(cliffs): the queue has a CONSUMER, and the wrong orientations are its cascade (#84) #113 measured the cascade running) but a Lua or editor probe need not - a live hazard for exactly the runtime probe test(cliffs): the chunk-border gate cannot be scored from any fixture (#84) #127 asked for.
  • crater-cliff has no connections at all. The whole system is gated on proto->place_as_crater == nullptr (cset w8, eq on proto+0xb90), and scaled_cliff_crater is the only thing that sets it.
  • No second connection pass during map generation. updateAndFixConnections (0x1007a94d0) is called from exactly one site in __text, CliffEditor::buildCliffs - found by scanning every BL encoding. The "a later chunk cleans up after an earlier one" reading is closed.

Also recorded: destroyEnd refuses to forceDestroy when entity flag bit 4 is set and leaves the orientation unchanged; after a shrink it re-searches its new bbox and destroys colliding non-cliff entities; getNeighbor requires an exact prototype-id AND position match.

Scope

No new fixtures, no shipping behaviour change. Recall 0.9961 / precision 0.9858 unchanged. pnpm run verify green - 182 files, 1507 tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc

…ism REFUTED (#84)

#132 gave `updateConnections` the lead on the clause "nothing else in the
pipeline treats border cells differently at all". That clause was false, and
the counter-example makes the lead stronger rather than weaker.

A cliff's collision box reaches 3.371 tiles; a border cell's centre is 1.5-2.5
tiles from the chunk edge and every interior cell's is 5.5-6.5. So 16 of the 20
orientations cross a chunk edge from the outer ring and none can from anywhere
else - a second border-only channel. And there is a rule on the far side of it:
`Surface::checkTileCollisions` skips a tile whose id is 0, and
`getEffectiveTileID` returns 0 for an absent chunk, so a box reaching into a
not-yet-generated chunk reads no tile and does not collide.

Scored on twelve regions already on disk, with the losing condition registered
before the run - "a non-crossing border rate that is not lower than the
crossing one refutes it":

| population | unexplained | rate |
| --- | --- | --- |
| interior | 9 / 4484 | 0.20% |
| border, box crosses an edge | 7 / 1407 | 0.50% |
| border, box does not cross | 14 / 2479 | 0.56% |

No-information predicts 7.6 crossing cells; observed 7. Refuted. Its own
direction fails too: an absent chunk makes the game KEEP a cliff, so its
signature is a false rejection with a blocking tile across an edge - 0 of 9,
and the predicate is non-vacuous.

The gain is a property the 2.91-sigma result did not have: the enrichment is
orientation-BLIND, which is what a cell-index gate looks like. Both halves keep
the 2.7x enrichment over the interior.

`Cliff`'s connection system is also now decompiled whole. Three facts change
what to try next:

- `onDestroy`'s cascade has FOUR gates, including `map->[0x240] == 0` and an
  entity flag set from the creation parameters. Map generation satisfies all
  four; a Lua or editor probe need not - a hazard for exactly the runtime probe
  #127 asked for.
- `crater-cliff` has no connections at all: the whole system is gated on
  `proto->place_as_crater == nullptr`.
- There is no second connection pass during map generation.
  `updateAndFixConnections` is called from one site in the binary,
  `CliffEditor::buildCliffs`.

No new fixtures, no shipping behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmitdaFJxN9Wm8LkQu33kc
@wormeyman
wormeyman merged commit 8409b48 into main Aug 4, 2026
6 checks passed
@wormeyman
wormeyman deleted the cliffs-border-orientation-blind branch August 4, 2026 04:23
wormeyman added a commit that referenced this pull request Aug 4, 2026
…#84) (#135)

#134 recorded `Cliff::onDestroy`'s four cascade gates and warned that a Lua or
editor destroy "need not" satisfy them, which would make #127's runtime probe
vacuous. **That warning is withdrawn.** It was stated from inference - "map
generation evidently satisfies all four, because #113 measured the cascade
running" - and inference is what keeps going wrong on this issue.

| gate | cascade needs | `applyCliffs` | `luaCreateEntity` |
| --- | --- | --- | --- |
| entity flag bit 5 of `+0x6e` | clear | `params[0x80] = 0` | `params[0x80] = 0` |
| `this+0x82` | set | ctor writes 1 unconditionally | same ctor |
| `this+0x83` | set | `place_as_crater == nullptr` | same |
| `map->[0x240]` | zero | only `Map::~Map` writes 1 | same |

- Bit 5 is exactly `params[0x80] != 0` at construction. `applyCliffs` builds its
  params at `sp+0x20` and zeroes that byte with `str wzr, [x26]`, `x26 = params
  + 0x80` (`0x101623d9c`). `luaCreateEntity` builds its own at `sp+0x228` and
  zeroes the same byte on both paths (`0x1019e5ba8`, `0x1019e5c18`) - and that
  arm is identifiably the cliff one, storing the `0x14` sentinel into
  `params+0x87` before `LuaTable::getDefault<CliffOrientation>`.
- `map->[0x240]` is "the map is being torn down": `Map::~Map` sets it as its
  first act, before `ToDeleteList::clear` (`0x10163461c`), and `Map::resume`
  bails on it. Dozens of unrelated `onDestroy` handlers read the same byte.
  `Cliff+0x78` is the `Map` - `Entity::getGame` is `[[this+0x78]+0x490]`.
- The one path that WOULD differ, `Cliff::destroyWithoutCorrection`, is
  unreachable: zero direct callers under a scan of every `BL` AND `B` in
  `__text`, and no pointer in any vtable - only four in the STAB tables.

The same widened scan re-confirms #134's other caller claim, which had been made
on `BL` alone and could have missed a tail call: `updateAndFixConnections` still
has exactly one caller, `CliffEditor::buildCliffs`.

Documentation only - no code, no fixtures, no 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