Skip to content

test(cliffs): the direction is ORE -> CLIFF, and it is worth 31 cells (#84) - #99

Merged
wormeyman merged 1 commit into
mainfrom
feat/cliff-ore-exclusion
Aug 2, 2026
Merged

test(cliffs): the direction is ORE -> CLIFF, and it is worth 31 cells (#84)#99
wormeyman merged 1 commit into
mainfrom
feat/cliff-ore-exclusion

Conversation

@wormeyman

Copy link
Copy Markdown
Collaborator

Closes the question #94 handed over. That PR ruled out lava, every other tile, the cliffiness gate and entity collision, then stopped: the cliff/ore correlation fits two stories demanding opposite fixes, and its note closed with "settle the direction, not add a rejection."

Settled with a lever rather than an argument. map_gen_settings.autoplace_controls is settable on the surface exactly like cliff_settings, so the resources can be switched off (size = 0) and the same regions regenerated - #82's collapse trick pointed one subsystem over.

arm cliff-vulcanus resources
[1500,1500] resources ON 885 3,933
[1500,1500] calcite OFF 912 19
[1500,1500] geyser OFF 889 3,914
[1500,1500] ALL OFF 916 0
[0,0] collapsed, ON 335 945
[0,0] collapsed, ALL OFF 345 0

Both arms, not one

Turning the ore off puts a cliff in all ten blob cells - 0/10 -> 10/10. The converse arm is what makes this a direction rather than a correlation: the collapsed settings force 335 cliffs through that region against the default's 283, straight through the tungsten field, and the ore does not move. The same 945 entities.

Three properties, each constraining the mechanism

  • One-way. Removing a resource only ever ADDS cliffs - nothing the game placed with them on disappears, across all four paired arms. A perturbed field would move cells both ways; a rejection cannot.
  • Additive. 27 calcite + 4 geyser = exactly the 31 of all-off, with no overlap. Each resource acts independently.
  • Local, and the geometry is a BOX OVERLAP against the resource entity's rectangle, not "is a resource tile inside the 4x4 cell". That is measurable only because sulfuric-acid-geyser's collision half-extent is 1.398 against the ores' 0.098 - a point-at-tile-centre test explains the calcite cells and cannot explain the geyser ones, which is exactly the residual that made every candidate geometry score 20-27 of 31.

Scored as a box overlap: 21 of 31 explained, zero false alarms across the 885 the game kept. The other 10 are not scattered - the suppressed set has six 4-connected components and every one contains a directly-overlapped cell. Adjacency is not a free pass: only 8 of the 885 kept cliffs touch a suppressed cell.

What it is worth

Every one of the 31 is a cell the port currently places, and none is a cliff the game kept. The rule is pure precision - it can only remove surplus, and it removes 31 of the 42 cells the port over-places at [1500,1500].

Still not a collision, now doubly established

The mask argument stands, and the fixture now carries the layers rather than a claim about them. The disassembly says it more strongly: computeInternal (0x101622860) calls generateCliffs at +44 and generateEntities at +148; apply (0x101623b48) calls applyCliffs at +124 and applyEntities at +164. The cliffs are computed and placed before any resource exists - so no collision test in the cliff path can see one, and #94's "maybe ore avoids cliffs" alternative is refuted by the converse arm.

crater-cliff is suppressed by calcite the same way (0 with it on, 8 with it off) and craters are placed on a completely different path (CliffCraterPlacer::tryToPlaceCliffAsCrater, 0x10160bcac, from applyEntities), so whatever this is, it is common to both cliff kinds.

Harness

sampleCliffEntitiesFull gains autoplaceControls, alsoResources and protoNames. Every arm dumps cliffs, resources, prototype collision geometry and the controls the surface read back, all from one generated surface - so an override that silently failed cannot be mistaken for a term that does not matter, and "the ore moved" and "the cliffs moved" are never compared across two different worlds.

Deliberately NOT done

Porting the rejection. The 31/0 score uses the game's resource entities as input, which isolates the rule from the accuracy of the resource port; driving it from renderVulcanusResources is a second question and has not been measured. The notes say so at the point of use.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LT6oy76sLRqoC1QzLocQ1h

…#84)

#94 ruled out lava, every other tile, the cliffiness gate and entity collision,
then handed the mechanism over open: the cliff/ore correlation fits two stories
demanding opposite fixes, and the note closed "settle the direction, not add a
rejection."

Settled with a lever rather than an argument. `autoplace_controls` is settable
on the surface exactly like `cliff_settings`, so the resources can be switched
off (`size = 0`) and the same regions regenerated - #82's collapse trick pointed
one subsystem over.

| arm | cliff-vulcanus | resources |
| --- | --- | --- |
| `[1500,1500]` resources ON | 885 | 3,933 |
| `[1500,1500]` calcite OFF | 912 | 19 |
| `[1500,1500]` geyser OFF | 889 | 3,914 |
| `[1500,1500]` ALL OFF | 916 | 0 |
| `[0,0]` collapsed, ON | 335 | 945 |
| `[0,0]` collapsed, ALL OFF | **345** | 0 |

**Both arms, not one.** Ore off puts a cliff in all ten blob cells, 0/10 ->
10/10. The converse arm is what makes it a direction: the collapsed settings
force 335 cliffs through the region against the default's 283, straight through
the tungsten field, and the ore does not move - the same 945 entities.

Three properties, each constraining the mechanism: it is ONE-WAY (removing a
resource only ever adds cliffs, in all four paired arms), ADDITIVE (27 calcite +
4 geyser = exactly the 31 of all-off, disjoint), and LOCAL with a geometry that
is a BOX OVERLAP against the resource ENTITY's rectangle - measurable only
because the geyser's collision half-extent is 1.398 against the ores' 0.098,
which is why every point-at-tile-centre geometry scored 20-27 of 31. Scored as
box overlap: 21 of 31 explained, ZERO false alarms in 885, and all six connected
components of the suppressed set contain a directly-overlapped cell (adjacency
is not a free pass - only 8 of 885 kept cliffs touch one).

Worth 31 of the 42 cells the port over-places at `[1500,1500]`: every one of the
31 is a cell the port places and none is a cliff the game kept, so the rule is
pure precision.

Still not a collision, now doubly established. The mask argument stands and the
fixture carries the layers rather than a claim about them. The disassembly says
it more strongly: `computeInternal` (0x101622860) calls `generateCliffs` at +44
and `generateEntities` at +148; `apply` (0x101623b48) calls `applyCliffs` at
+124 and `applyEntities` at +164 - the cliffs are computed AND placed before any
resource exists, which also refutes #94's "maybe ore avoids cliffs" guess.

Harness: `sampleCliffEntitiesFull` gains `autoplaceControls`, `alsoResources`
and `protoNames`, dumping cliffs, resources, prototype collision geometry and
the controls the SURFACE read back from ONE generated surface - so an override
that failed to apply cannot be mistaken for a term that does not matter, and
"the ore moved" and "the cliffs moved" are never compared across two worlds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LT6oy76sLRqoC1QzLocQ1h
@wormeyman
wormeyman force-pushed the feat/cliff-ore-exclusion branch from 6187d70 to b540470 Compare August 2, 2026 18:18
@wormeyman
wormeyman merged commit 97420aa into main Aug 2, 2026
2 checks passed
wormeyman added a commit that referenced this pull request Aug 2, 2026
#99 characterised the rule and stopped short of porting it, flagging one open
sub-question: whether driving it from our own resource model, rather than the
game's entities, is accurate enough. It is - and it costs exactly one cell.

Scored across all three oracle regions, driving `makeVulcanusOreRejection` off
the same field stack the ore overlay paints from:

| region | game | placed | fires | false rejections | surplus |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 283 | 283 | 0 | 0 | 2 -> 2 |
| [1500,1500] | 885 | 900 | 20 | 0 | 42 -> 22 |
| [-1200,800] | 401 | 387 | 0 | 0 | 1 -> 1 |

Precision at [1500,1500] 0.953 -> 0.975 with the 858 true positives untouched.
**Recall is not touched anywhere**, which was the gate: this rule may only ever
cost precision.

Three variants were scored and the two that lose are kept in the spec rather
than dismissed in a comment, because #88/#90 already paid for that lesson here:

| variant | fires | correct of 31 | false rejections |
| --- | --- | --- | --- |
| base box, ores only (SHIPPED) | 20 | 20 | 0 |
| base box + geyser | 21 | 20 | 1 |
| per-orientation box | 23 | 21 | 2 |

The geyser arm is strictly HARMFUL - one more false rejection and not one
additional correct suppression - so it is implemented behind `includeGeyser`,
defaulting off. The per-orientation rotbb box catches one more true cell and
pays two kept cliffs for it; higher `correct` is exactly the trap.

Not claimed: the mechanism is still open (the disassembly says cliffs are placed
before any resource entity exists), and 11 of the 31 stay unexplained with the
box deliberately not widened to cover them. The spec pins that 11.

Lands as `CliffBands.cellRejects`, an opaque per-cell predicate beside
`tileCollides` - so the shared cliff core stays planet-agnostic, and the model
the specs score is the model the renderer ships. The predicate enumerates no
entities: the overlapping tiles follow in closed form (2 tiles for an ore
against the lava rejection's ~30), guarded by a wider brute-force scan.


Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 3, 2026
…0.9961 (#84) (#101)

* feat(cliffs): port the ORE -> CLIFF rejection (#84 item 1)

#99 characterised the rule and stopped short of porting it, flagging one open
sub-question: whether driving it from our own resource model, rather than the
game's entities, is accurate enough. It is - and it costs exactly one cell.

Scored across all three oracle regions, driving `makeVulcanusOreRejection` off
the same field stack the ore overlay paints from:

| region | game | placed | fires | false rejections | surplus |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 283 | 283 | 0 | 0 | 2 -> 2 |
| [1500,1500] | 885 | 900 | 20 | 0 | 42 -> 22 |
| [-1200,800] | 401 | 387 | 0 | 0 | 1 -> 1 |

Precision at [1500,1500] 0.953 -> 0.975 with the 858 true positives untouched.
**Recall is not touched anywhere**, which was the gate: this rule may only ever
cost precision.

Three variants were scored and the two that lose are kept in the spec rather
than dismissed in a comment, because #88/#90 already paid for that lesson here:

| variant | fires | correct of 31 | false rejections |
| --- | --- | --- | --- |
| base box, ores only (SHIPPED) | 20 | 20 | 0 |
| base box + geyser | 21 | 20 | 1 |
| per-orientation box | 23 | 21 | 2 |

The geyser arm is strictly HARMFUL - one more false rejection and not one
additional correct suppression - so it is implemented behind `includeGeyser`,
defaulting off. The per-orientation rotbb box catches one more true cell and
pays two kept cliffs for it; higher `correct` is exactly the trap.

Not claimed: the mechanism is still open (the disassembly says cliffs are placed
before any resource entity exists), and 11 of the 31 stay unexplained with the
box deliberately not widened to cover them. The spec pins that 11.

Lands as `CliffBands.cellRejects`, an opaque per-cell predicate beside
`tileCollides` - so the shared cliff core stays planet-agnostic, and the model
the specs score is the model the renderer ships. The predicate enumerates no
entities: the overlapping tiles follow in closed form (2 tiles for an ore
against the lava rejection's ~30), guarded by a wider brute-force scan.

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

* test(cliffs): the budget FLIPPED - recall is now the bigger defect (#84)

Every cliff defect found since #18 has been a rule the port over-places without
(lava collision, the rotbb box shape, the ore suppression), so "find another
rejection" has been the shape of the work throughout. After #100 that is no
longer where the error is.

| region | surplus | missing | lava-killed | ore-killed | never generated |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 2 | 2 | 2 | 0 | 0 |
| [1500,1500] | 22 | 27 | 3 | 0 | 24 |
| [-1200,800] | 1 | 15 | 1 | 0 | 14 |
| total | 25 | 44 | 6 | 0 | 38 |

**The port now misses more cells than it over-places, 44 to 25**, and 38 of the
44 are cells the crossings stage never produces at all - a different defect in a
different part of the port from everything solved so far.

`[0,0]` generates every cell the game does: its whole miss is the two the lava
rejection took, and `neverGenerated` is zero there. All 38 sit in the two
far-field regions, which agrees with #93 finding the port exact at [0,0] and
[-1200,800] at cliff_smoothing = 0 and still wrong at [1500,1500].

Also closes item 3 (the entity half of `Surface::wouldCollide`) UNPORTED, by
size rather than by difficulty - the same move that retired fixImpossibleCells
as a suspect. It is a rejection, and rejections can only remove cells: total
surplus is 25, which bounds what rocks and craters together could ever be worth
against a 44-cell recall gap they cannot touch.

The crater arm is settled exactly, since craters are already in the fixtures:
all 8 sit in [-1200,800] and not one touches a cell the port over-places, nor
any cliff the game kept. Worth zero. The rock arm needs no fixture - the ceiling
covers it.

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

* test(cliffs): the recall gap was a QUERY-WINDOW ARTIFACT - recall is 0.9961 (#84)

Supersedes this branch's first commit, whose central claim was wrong.

`find_entities_filtered` selects entities whose BOUNDING BOX touches the query
area; `placedCells` emits cells whose CENTRE lies inside it. Different inclusion
rules, so the fixtures carry cliffs centred just outside the box and every one
has been scored as a miss.

| region | game rows | centred inside | centred OUTSIDE |
| --- | --- | --- | --- |
| [0,0] | 283 | 283 | 0 |
| [1500,1500] | 885 | 861 | 24 |
| [-1200,800] | 401 | 387 | 14 |

That is 38 cells - the entire apparent recall gap - and the port places 38 of 38
once the query box includes their centres. Every one is an agreement being
scored as a failure. The widening arm is the load-bearing one: "we never looked
there" alone is equally consistent with the port being wrong.

Corrected budget, both sides scored alike:

| region | game | port | matched | surplus | missing |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 283 | 283 | 281 | 2 | 2 |
| [1500,1500] | 861 | 880 | 858 | 22 | 3 |
| [-1200,800] | 387 | 387 | 386 | 1 | 1 |
| total | 1531 | 1550 | 1525 | 25 | 6 |

**Recall 0.9961, precision 0.9839.** The 0.972 recall in the notes divided the
same 1525 matches by 1569 rather than 1531 - the match count was never wrong,
only the denominator.

All 6 missing cells are ones our own lava rejection removed; there is no cell
the port simply fails to generate. So precision is the only real defect left.

Consequently item 3 (the entity half of Surface::wouldCollide) is RE-OPENED.
The earlier commit closed it by size, arguing a rejection cannot help a 44-cell
recall gap - that argument died with the gap. With recall at 0.9961 the dominant
defect is the 25 surplus cells, which is exactly what a rejection removes.

The crater arm stays settled at zero (all 8 sit in [-1200,800], none touches a
surplus cell). The rock arm has no oracle capture at all, so capturing one is
the next step - now with a 25-cell target rather than a ceiling against it.

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

* test(cliffs): the rock arm fails on the mechanism's own geometry (#84)

Completes the item 3 thread in this branch rather than opening a second PR for
it, since the previous commit had just re-opened it.

No rock capture is needed to kill it. `computeInternal` runs `generateCliffs`
before `generateEntities`, and `apply` runs `applyCliffs` (+124) before
`applyEntities` (+164), so within a chunk no rock exists when the cliff is
applied. A rock can only block a cliff from an ALREADY-GENERATED NEIGHBOUR,
which confines the whole mechanism to cells near a 32-tile chunk border.

| | n | near chunk border |
| --- | --- | --- |
| surplus | 25 | 11 = 44.0% |
| matched | 1525 | 673 = 44.1% |

The base rate to three significant figures. The surplus has no chunk-border
character at all, so the one geometry the mechanism is confined to is not where
the errors are.

The direct overlap test agrees and is deliberately the weaker arm: 3 of 25
surplus cells overlap a modelled rock against a 6.6% base rate (~1.7 expected),
which is nothing - and our rock placement is a salt-dependent roll whose
individual positions are unreliable exactly as the geyser's were in #100.

So item 3 explains approximately none of the 25, and is closed on the
mechanism's geometry rather than on the ceiling argument that died with the
recall gap. Remaining unexplained: 25 surplus, 6 missing (all lava-rejection
over-rejections), 33 wrong orientations.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 3, 2026
)

* feat(cliffs): port the ORE -> CLIFF rejection (#84 item 1)

#99 characterised the rule and stopped short of porting it, flagging one open
sub-question: whether driving it from our own resource model, rather than the
game's entities, is accurate enough. It is - and it costs exactly one cell.

Scored across all three oracle regions, driving `makeVulcanusOreRejection` off
the same field stack the ore overlay paints from:

| region | game | placed | fires | false rejections | surplus |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 283 | 283 | 0 | 0 | 2 -> 2 |
| [1500,1500] | 885 | 900 | 20 | 0 | 42 -> 22 |
| [-1200,800] | 401 | 387 | 0 | 0 | 1 -> 1 |

Precision at [1500,1500] 0.953 -> 0.975 with the 858 true positives untouched.
**Recall is not touched anywhere**, which was the gate: this rule may only ever
cost precision.

Three variants were scored and the two that lose are kept in the spec rather
than dismissed in a comment, because #88/#90 already paid for that lesson here:

| variant | fires | correct of 31 | false rejections |
| --- | --- | --- | --- |
| base box, ores only (SHIPPED) | 20 | 20 | 0 |
| base box + geyser | 21 | 20 | 1 |
| per-orientation box | 23 | 21 | 2 |

The geyser arm is strictly HARMFUL - one more false rejection and not one
additional correct suppression - so it is implemented behind `includeGeyser`,
defaulting off. The per-orientation rotbb box catches one more true cell and
pays two kept cliffs for it; higher `correct` is exactly the trap.

Not claimed: the mechanism is still open (the disassembly says cliffs are placed
before any resource entity exists), and 11 of the 31 stay unexplained with the
box deliberately not widened to cover them. The spec pins that 11.

Lands as `CliffBands.cellRejects`, an opaque per-cell predicate beside
`tileCollides` - so the shared cliff core stays planet-agnostic, and the model
the specs score is the model the renderer ships. The predicate enumerates no
entities: the overlapping tiles follow in closed form (2 tiles for an ore
against the lava rejection's ~30), guarded by a wider brute-force scan.

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

* test(cliffs): the budget FLIPPED - recall is now the bigger defect (#84)

Every cliff defect found since #18 has been a rule the port over-places without
(lava collision, the rotbb box shape, the ore suppression), so "find another
rejection" has been the shape of the work throughout. After #100 that is no
longer where the error is.

| region | surplus | missing | lava-killed | ore-killed | never generated |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 2 | 2 | 2 | 0 | 0 |
| [1500,1500] | 22 | 27 | 3 | 0 | 24 |
| [-1200,800] | 1 | 15 | 1 | 0 | 14 |
| total | 25 | 44 | 6 | 0 | 38 |

**The port now misses more cells than it over-places, 44 to 25**, and 38 of the
44 are cells the crossings stage never produces at all - a different defect in a
different part of the port from everything solved so far.

`[0,0]` generates every cell the game does: its whole miss is the two the lava
rejection took, and `neverGenerated` is zero there. All 38 sit in the two
far-field regions, which agrees with #93 finding the port exact at [0,0] and
[-1200,800] at cliff_smoothing = 0 and still wrong at [1500,1500].

Also closes item 3 (the entity half of `Surface::wouldCollide`) UNPORTED, by
size rather than by difficulty - the same move that retired fixImpossibleCells
as a suspect. It is a rejection, and rejections can only remove cells: total
surplus is 25, which bounds what rocks and craters together could ever be worth
against a 44-cell recall gap they cannot touch.

The crater arm is settled exactly, since craters are already in the fixtures:
all 8 sit in [-1200,800] and not one touches a cell the port over-places, nor
any cliff the game kept. Worth zero. The rock arm needs no fixture - the ceiling
covers it.

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

* test(cliffs): the recall gap was a QUERY-WINDOW ARTIFACT - recall is 0.9961 (#84)

Supersedes this branch's first commit, whose central claim was wrong.

`find_entities_filtered` selects entities whose BOUNDING BOX touches the query
area; `placedCells` emits cells whose CENTRE lies inside it. Different inclusion
rules, so the fixtures carry cliffs centred just outside the box and every one
has been scored as a miss.

| region | game rows | centred inside | centred OUTSIDE |
| --- | --- | --- | --- |
| [0,0] | 283 | 283 | 0 |
| [1500,1500] | 885 | 861 | 24 |
| [-1200,800] | 401 | 387 | 14 |

That is 38 cells - the entire apparent recall gap - and the port places 38 of 38
once the query box includes their centres. Every one is an agreement being
scored as a failure. The widening arm is the load-bearing one: "we never looked
there" alone is equally consistent with the port being wrong.

Corrected budget, both sides scored alike:

| region | game | port | matched | surplus | missing |
| --- | --- | --- | --- | --- | --- |
| [0,0] | 283 | 283 | 281 | 2 | 2 |
| [1500,1500] | 861 | 880 | 858 | 22 | 3 |
| [-1200,800] | 387 | 387 | 386 | 1 | 1 |
| total | 1531 | 1550 | 1525 | 25 | 6 |

**Recall 0.9961, precision 0.9839.** The 0.972 recall in the notes divided the
same 1525 matches by 1569 rather than 1531 - the match count was never wrong,
only the denominator.

All 6 missing cells are ones our own lava rejection removed; there is no cell
the port simply fails to generate. So precision is the only real defect left.

Consequently item 3 (the entity half of Surface::wouldCollide) is RE-OPENED.
The earlier commit closed it by size, arguing a rejection cannot help a 44-cell
recall gap - that argument died with the gap. With recall at 0.9961 the dominant
defect is the 25 surplus cells, which is exactly what a rejection removes.

The crater arm stays settled at zero (all 8 sit in [-1200,800], none touches a
surplus cell). The rock arm has no oracle capture at all, so capturing one is
the next step - now with a 25-cell target rather than a ceiling against it.

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

* test(cliffs): the rock arm fails on the mechanism's own geometry (#84)

Completes the item 3 thread in this branch rather than opening a second PR for
it, since the previous commit had just re-opened it.

No rock capture is needed to kill it. `computeInternal` runs `generateCliffs`
before `generateEntities`, and `apply` runs `applyCliffs` (+124) before
`applyEntities` (+164), so within a chunk no rock exists when the cliff is
applied. A rock can only block a cliff from an ALREADY-GENERATED NEIGHBOUR,
which confines the whole mechanism to cells near a 32-tile chunk border.

| | n | near chunk border |
| --- | --- | --- |
| surplus | 25 | 11 = 44.0% |
| matched | 1525 | 673 = 44.1% |

The base rate to three significant figures. The surplus has no chunk-border
character at all, so the one geometry the mechanism is confined to is not where
the errors are.

The direct overlap test agrees and is deliberately the weaker arm: 3 of 25
surplus cells overlap a modelled rock against a 6.6% base rate (~1.7 expected),
which is nothing - and our rock placement is a salt-dependent roll whose
individual positions are unreliable exactly as the geyser's were in #100.

So item 3 explains approximately none of the 25, and is closed on the
mechanism's geometry rather than on the ceiling argument that died with the
recall gap. Remaining unexplained: 25 surplus, 6 missing (all lava-rejection
over-rejections), 33 wrong orientations.

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

* test(cliffs): the orientation residual is not a boundary tie (#84)

Stacked on the error-budget branch.

The residual's known shape - exactly one edge per wrong cell, always an
OVER-detection - has an obvious cheap explanation that turns out to be wrong,
and ruling it out eliminates a whole class of cause.

`crossesCliff` decides on the sign of `elevation - boundary`. If an endpoint sat
within float noise of a band boundary, the ~1e-6 our fields agree to would flip
the crossing, and the residual would be an irreducible precision limit with
nothing to fix.

Measured: every crossing edge in a wrong cell sits at least 0.205 from its
boundary, median ~9.9 - four to seven orders of magnitude clear of float noise.
For the game to disagree its elevation must differ from ours by more than 0.2,
which is a real field or rule difference.

Non-vacuity: the overall minimum across all 2,920 crossing edges is 6.4e-3,
thirty times tighter, so "far from the boundary" is a property of the wrong
cells and not of the sample.

Also pins WHICH channel the corner fixture holds, as a number rather than as
prose: against our per-tile elevation the worst corner differs by 4.8e-2;
against the grid-4 cliff channel the generator actually reads, by 96.09. #83 was
the most expensive mistake this subsystem has made and prose is not a guard.

The gap that leaves is the point: the grid-4 cliff-elevation channel has NO
per-corner oracle, is the only placement input never checked against the game
corner by corner, and after the margin result is the only remaining candidate
that could move an endpoint the required 0.2. Capturing it is the next step -
and not via calculate_tile_properties, which is the 1-tile program that produced
the wrong-channel fixture in the first place.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 3, 2026
…scade (#84) (#110)

`vulcanusOreRejection.ts` left an explicit open question: the run remainders
are "a cascade along cliff connections OR a wider box". #108 makes the
cascade half concrete - a rejection zeroes the cell's edge registers, so a
neighbour's code, hence its ORIENTATION, hence its collision box, changes -
so re-testing to a fixpoint IS that cascade. `rejectionCascades` is the arm.

**Refuted.** Bit-for-bit identical at the shipping settings, and on the
collapsed rule it loses 14 matched cells and 7 orientations to gain 4 of the
over-placement (18654/693/1200/103 -> 18640/700/1196/110). The collapsed-rule
row is what makes the shipping no-op a result rather than an untriggered
branch. A rejected cell never turns a neighbour into a rejectable
orientation.

**The crossing STAGE explains 2 remainders for free.** The predicate fires on
20 placed cells; the placement loses 22, because zeroing a rejected cell's
edges leaves two neighbours with codes that no longer place. No tuning - it
falls out of #108's mechanism, and it is the first thing to reduce the
remainder count since the rule was characterised.

**And the rule now has a precision and a recall, not a total.**
`oracle-vulcanus-cliff-ore-direction` re-ran the region with resources off
through `autoplace_controls`, so the ore's effect is a known SET: 31 cells
suppressed, 0 appeared (the one-way property of #99, re-confirmed on the
entity region), 5 re-coded. Our model suppresses 22, **all 22 correct** -
**precision 1.000, recall 0.710**. Exactly right where it fires, simply too
narrow, which says which direction is safe to move in.

Attribution from the per-control arms: 27 calcite, 4 geyser, 0 tungsten/coal.
Of the 9 misses, 4 are geyser cells `includeGeyser: false` deliberately
excludes, 5 are calcite, and all 9 are adjacent to another suppressed cell.

**Half of [1500,1500]'s residual is not ore at all.** Running BOTH sides with
resources off: 876 matched, **13 wrong, 10 surplus**, 3 missing - against
842/16/19/3 with them on. Tuning the ore rule cannot reach that half, and it
is the larger target now.

What stays open is the wider-box half - the one #88 says must not be tuned
into fitting.

Item of #84; the issue stays open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ExK3ASs5i2KMm9NwWqnZuj
wormeyman added a commit that referenced this pull request Aug 14, 2026
…ty (#84) (#200)

* test(oracle): add a data-stage prototype override hook (#84)

Checkpoint, NOT PR-ready - no regression test yet.

`autoplaceControls` and `cliffSettings` are surface settings and can be
changed at on_init. A prototype field cannot: it is read at map-gen from
the loaded prototype, so the only lever is the data stage. That is the
difference between "switch the ore off" and "leave the ore exactly where
it is and change one property of it", and only the second can separate a
resource's PRESENCE from a specific behaviour of it.

- `extraDataLua` is written to data-final-fixes.lua, not data.lua. This
  mod declares no dependencies, so Factorio may run its data.lua before
  space-age's, at which point data.raw.resource["tungsten-ore"] does not
  exist and the override would silently do nothing.
- The protos dump gains `cliff_removal_probability` and `map_grid`, read
  back off the running game, so an override arm has to prove it applied.

What it bought, on the blob region with COLLAPSED cliff settings:

  arm                              blob   cliffs  resources  removal_prob
  A control                        0/10      335        945             1
  B removal_prob = 0, ore present  10/10     345        945             0
  C resources OFF                  10/10     345          0             1

B leaves 945 resource entities exactly where the control has them and
the cliffs come back regardless, and B is indistinguishable from C. So
`ResourceEntityPrototype::cliff_removal_probability` is the mechanism
behind the ore -> cliff exclusion that #99, #100, #110, #128 and #129
characterised empirically and that vulcanusOreRejection.ts still records
as "NOT established". It defaults to 1.0 and no shipped prototype
overrides it, which is why the port's unconditional box-overlap rejection
is correct as written - the behaviour does not change, the explanation
does.

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

* test(cliffs): name the ore->cliff mechanism - cliff_removal_probability (#84)

`vulcanusOreRejection.ts` opened with "NOT established: the mechanism.
This is a characterised empirical rule, not a port of a known engine
path." It is established now, and the answer was a documented prototype
field: `ResourceEntityPrototype::cliff_removal_probability`, default
1.0, overridden by no shipped prototype - so it is invisible from the
data and can only be seen by changing it.

Five issues of `autoplace_controls` work could not reach it because a
surface lever can only add or remove the ore. A prototype lever leaves
the ore in place and changes one thing about it:

  arm                   blob cells  cliff-vulcanus  resources  field
  control                    0/10              335        945      1
  field = 0                 10/10              345        945      0
  resources OFF             10/10              345          0      1

The middle arm keeps all 945 resource entities exactly where the control
has them and the cliffs return regardless, so no account resting on the
ore's PRESENCE survives. It is indistinguishable from the resources-OFF
arm, and 345 - 335 is exactly the ten blob cells, so the field accounts
for the effect entirely rather than partly. Every arm reads the field
back off the running game, so an override that failed to apply cannot be
mistaken for a term that does not matter.

NO BEHAVIOUR CHANGES. At 1.0 the removal is unconditional, so the ported
box-overlap rejection is correct exactly as written. What changes is the
record: the disassembly finding (cliffs are computed and placed before
any resource entity exists) stops being a dead end and becomes the
reason the effect had to be a removal, and the thin n=1 destroy-stage
result gains outside support - a field that removes cliffs can only act
on cliffs that already exist.

Still open, and said so in every place touched: the GEOMETRY the engine
removes with. The base collision_box remains an empirical fit and the
box still must not be tuned.

- `OracleOptions.extraDataLua`, written to data-final-fixes.lua rather
  than data.lua because this probe mod declares no dependencies and may
  otherwise load before space-age.
- The protos dump gains cliff_removal_probability and map_grid.
- test/cliffRemovalProbability.spec.ts, its fixture and provenance.
  Discrimination checked by planting a confused-arm error: exactly the
  two mechanism tests fail, and the two that read only the control arm
  correctly do not.
- Stale "mechanism is open" text corrected in vulcanusOreRejection.ts,
  cliffPlacement.ts, cliffOreActsAtDestroyStage.spec.ts and
  vulcanus-cliffs-NOTES.md. The point-in-time spec under
  docs/superpowers/specs/ is left alone by convention.

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

---------

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