Skip to content

feat(cliffs): port the tile-collision rejection (#18) - #72

Closed
wormeyman wants to merge 2 commits into
feat/cliff-orientation-tablesfrom
feat/cliff-collision-rejection
Closed

feat(cliffs): port the tile-collision rejection (#18)#72
wormeyman wants to merge 2 commits into
feat/cliff-orientation-tablesfrom
feat/cliff-collision-rejection

Conversation

@wormeyman

Copy link
Copy Markdown
Collaborator

Stacked on #71 - base is feat/cliff-orientation-tables, so review or merge that one first. GitHub will retarget this to main automatically once #71 lands.

Ports the rejection #71's tables exist for. EntityMapGenerationTask::tryToAddCliff tests the cell's orientation collision box against the tile mask grid and drops the entity on a hit; generateCliffs ignores the return value - no retry, no alternative orientation, no effect on neighbouring cells - so this is a pure post-filter on the emit loop. That is what makes it safe to run per-cell in any order, and tiled output stays byte-identical (tiledEquality.spec.ts still passes).

Result

Vulcanus, against oracle-vulcanus-cliff-entities.seed123456:

region ours recall precision ratio was
[0,0] 317 0.784 0.700 1.120 335 / 0.806 / 0.681 / 1.184
[1500,1500] 888 0.933 0.930 1.003 1065 / 0.938 / 0.779 / 1.203
[-1200,800] 371 0.853 0.922 0.925 375 / 0.853 / 0.912 / 0.935

Region 1 - the case the rule was found on - drops 173 false positives against 4 true ones and lands within 0.3% on count. Regions 0 and 2 barely move, so this is not the whole of #18.

Recall gets worse in two regions, and that is reported rather than smoothed. 6 true positives lost at [0,0] and 4 at [1500,1500] are cells where the game placed a cliff and our tile resolver puts lava in its box. The resolver is ~98.2% accurate overall and is plausibly worse at a lava boundary, but that is untested - it is the first thing to check before suspecting the collision geometry.

Nauvis

Kept on its cheap paint-time water-colour skip rather than resolving tiles. That is only sound while no Nauvis cliff's collision box touches water, which was a remembered one-off measurement. It is now a standing check: test/cliffPlacement.spec.ts runs the real rejection and asserts the placed list is unchanged, with three non-vacuity guards - the predicate ran, it found no water under any cliff, and the same resolver does find water elsewhere in the region. If it ever fails, NAUVIS_CLIFF_BLOCKING_TILES is what to pass the placement.

Cost

Two arms, same machine, back to back:

figure without with delta
terrain (the control arm) 3948 3947 -0.0%
cliffs marginal 1500 1917 +28%
ratio all/terrain, whole 1.944 2.040 +4.9%
ratio all/terrain, TILED 2.455 2.569 +4.6%

Terrain landing within 1 ms across the arms is what makes this readable - the bench's own header records a 4.8% baseline drift between runs.

It puts the whole-image "under 2x terrain" gate back over the line, 1.944 -> 2.040. Recorded rather than buried, as the same gate was when the Vulcanus V3 overlays crossed it. If it needs to come back down, the cheap lever is a "could lava possibly win here" pre-gate in front of the full 19-tile argmax - scoped and deliberately not taken here.

Plumbing

renderVulcanusCliffs now takes the composite's sharedStack like renderVulcanusRocks does, and makeVulcanusCliffFields grows the same optional-shared-stack seam as makeVulcanusRockFields. Without it the tile resolver would rebuild the whole field DAG - memoXY is single-entry, so separate copies share nothing.

Also corrects that file's header, which asserted the opposite of what the game does ("Lava plays that visual role but is not a water tile, and the game does not exclude cliffs from it here") - wrong in both halves.

pnpm run verify green: 143 test files, 1268 tests.

🤖 Generated with Claude Code

https://claude.ai/code/session_012cd6B2MKoJF5W2T175BDoe

wormeyman and others added 2 commits July 30, 2026 13:21
`EntityMapGenerationTask::tryToAddCliff` tests the cell's orientation
collision box against the tile mask grid and drops the entity on a hit.
`generateCliffs` ignores the return value - no retry, no alternative
orientation, no effect on neighbouring cells - so this is a pure
post-filter on the emit loop, which is why it runs per-cell in any order
and leaves tiled output byte-identical.

`CliffBands.tileCollides` is the seam, planet-agnostic: a tile blocks a
cliff when its CollisionMask shares a layer with the cliff's. The cliff
mask holds `water_tile`, so that is lava on Vulcanus
(`tile_collision_masks.lava()` sets it) and water on Nauvis.

Vulcanus, against `oracle-vulcanus-cliff-entities.seed123456`:

| region | ours | recall | precision | ratio | was |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 317 | 0.784 | 0.700 | 1.120 | 335 / 0.806 / 0.681 / 1.184 |
| [1500,1500] | 888 | 0.933 | 0.930 | 1.003 | 1065 / 0.938 / 0.779 / 1.203 |
| [-1200,800] | 371 | 0.853 | 0.922 | 0.925 | 375 / 0.853 / 0.912 / 0.935 |

Region 1 - the case the rule was found on - drops 173 false positives
against 4 true ones and lands within 0.3% on count. Regions 0 and 2 barely
move, so this is not the whole of #18.

Recall gets WORSE in two regions and that is reported rather than
smoothed: 6 true positives lost at [0,0] and 4 at [1500,1500] are cells
where the game placed a cliff and our tile resolver puts lava in its box.
The resolver is ~98.2% accurate overall and is plausibly worse at a lava
boundary, but that is untested and is the first thing to check before
suspecting the collision geometry.

Nauvis keeps its cheap paint-time water-colour skip rather than resolving
tiles, which is only sound while no Nauvis cliff's box touches water.
That was a remembered one-off measurement; `test/cliffPlacement.spec.ts`
now runs the real rejection and asserts the placed list is unchanged,
with three non-vacuity checks - the predicate ran, it found no water under
any cliff, and the same resolver does find water elsewhere in the region.

`renderVulcanusCliffs` now takes the composite's `sharedStack` like
`renderVulcanusRocks` does, and `makeVulcanusCliffFields` grows the same
optional-shared-stack seam as `makeVulcanusRockFields`; without it the
tile resolver would rebuild the whole field DAG, since `memoXY` is
single-entry and separate copies share nothing.

Also corrects the header of `renderVulcanusCliffs`, which asserted the
opposite of what the game does ("Lava plays that visual role but is not a
water tile, and the game does not exclude cliffs from it here") - wrong in
both halves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cd6B2MKoJF5W2T175BDoe
Two arms of the vulcanus block, same machine, back to back, with and
without `tileCollides`:

| figure                    | without | with  | delta |
| ------------------------- | ------- | ----- | ----- |
| terrain (the control arm) |    3948 |  3947 | -0.0% |
| cliffs marginal           |    1500 |  1917 | +28%  |
| ratio all/terrain, whole  |   1.944 | 2.040 | +4.9% |
| ratio all/terrain, TILED  |   2.455 | 2.569 | +4.6% |

Terrain landing within 1 ms across the arms is what makes the number
readable - this file's own header records a 4.8% baseline drift between
runs, which would otherwise swamp a change this size.

It puts the whole-image "under 2x terrain" gate back over the line,
1.944 -> 2.040. Recorded rather than buried, as the same gate was when the
Vulcanus V3 overlays crossed it. The tiled figure - the geometry the app
actually renders - was already over at 2.455 and is now 2.569.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cd6B2MKoJF5W2T175BDoe
@wormeyman

Copy link
Copy Markdown
Collaborator Author

Superseded by #73 — GitHub auto-closed this when its base branch feat/cliff-orientation-tables was deleted on #71's merge, and a closed PR cannot be retargeted or reopened. #73 is the same two commits rebased onto main.

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